<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<title>AI数据分析</title>
|
<link rel="stylesheet" href="../../static/vue/element/element.css" />
|
<style>
|
body {
|
margin: 0;
|
font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
|
background:
|
radial-gradient(900px 460px at 4% -8%, rgba(36, 113, 92, 0.16), transparent 52%),
|
radial-gradient(820px 420px at 106% 0%, rgba(20, 82, 128, 0.14), transparent 54%),
|
linear-gradient(180deg, #f4f8fb 0%, #eef4f8 100%);
|
color: #223046;
|
}
|
.console-page {
|
max-width: 1680px;
|
margin: 16px auto;
|
padding: 0 14px 22px;
|
}
|
.hero {
|
border-radius: 18px;
|
color: #fff;
|
padding: 16px;
|
background:
|
linear-gradient(135deg, rgba(14, 76, 82, 0.96), rgba(31, 115, 108, 0.92) 48%, rgba(44, 130, 86, 0.94)),
|
radial-gradient(460px 180px at 80% 0%, rgba(255, 255, 255, 0.24), transparent 60%);
|
box-shadow: 0 14px 34px rgba(26, 76, 91, 0.22);
|
}
|
.hero-top {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
flex-wrap: wrap;
|
}
|
.hero-title {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
min-width: 280px;
|
}
|
.hero-title .main {
|
font-size: 18px;
|
font-weight: 700;
|
letter-spacing: 0.2px;
|
}
|
.hero-title .sub {
|
margin-top: 4px;
|
font-size: 12px;
|
opacity: 0.9;
|
}
|
.hero-actions {
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
.panel {
|
border-radius: 16px;
|
border: 1px solid #dfe8f1;
|
background: rgba(255, 255, 255, 0.88);
|
box-shadow: 0 10px 28px rgba(31, 62, 92, 0.1);
|
overflow: hidden;
|
margin-top: 12px;
|
}
|
.panel-head {
|
padding: 12px 14px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 10px;
|
border-bottom: 1px solid #edf2f7;
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbfd 100%);
|
}
|
.panel-title {
|
font-weight: 700;
|
color: #223046;
|
}
|
.panel-body {
|
padding: 12px 14px 14px;
|
}
|
.status-grid {
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
gap: 12px;
|
}
|
.status-item {
|
padding: 12px 14px;
|
border-radius: 10px;
|
border: 1px solid #e4ebf2;
|
background: #f8fbfd;
|
}
|
.status-item .label {
|
font-size: 12px;
|
color: #718299;
|
margin-bottom: 4px;
|
}
|
.status-item .value {
|
font-size: 14px;
|
font-weight: 600;
|
color: #223046;
|
}
|
.status-item .desc {
|
font-size: 11px;
|
color: #999;
|
margin-top: 2px;
|
}
|
.report-summary {
|
margin-top: 12px;
|
padding: 14px;
|
border-radius: 12px;
|
border: 1px solid #e4ebf2;
|
background: #f8fbfd;
|
}
|
.report-summary h3 {
|
margin: 0 0 10px 0;
|
font-size: 15px;
|
color: #223046;
|
}
|
.report-summary pre {
|
white-space: pre-wrap;
|
word-break: break-word;
|
font-size: 13px;
|
line-height: 1.6;
|
color: #333;
|
margin: 0;
|
max-height: 500px;
|
overflow-y: auto;
|
}
|
</style>
|
</head>
|
<body>
|
<div id="app">
|
<div class="console-page">
|
<div class="hero">
|
<div class="hero-top">
|
<div class="hero-title">
|
<span v-html="headerIcon"></span>
|
<div>
|
<div class="main">AI 数据分析</div>
|
<div class="sub">基于 LLM 的 WCS 运营数据分析,支持手动触发和定时自动执行</div>
|
</div>
|
</div>
|
<div class="hero-actions">
|
<span style="font-size:13px;opacity:0.9;">功能开关:</span>
|
<el-switch
|
v-model="enabled"
|
active-text="启用"
|
inactive-text="关闭"
|
active-color="#13ce66"
|
inactive-color="#ff4949"
|
:disabled="enabledLoading"
|
@change="onEnabledChange">
|
</el-switch>
|
</div>
|
</div>
|
</div>
|
|
<div class="panel">
|
<div class="panel-head">
|
<div>
|
<div class="panel-title">当前配置状态</div>
|
<div style="color:#718299;font-size:12px;margin-top:2px;">开关控制定时分析和手动分析是否执行</div>
|
</div>
|
<el-button size="mini" @click="loadConfig">刷新</el-button>
|
</div>
|
<div class="panel-body">
|
<div class="status-grid">
|
<div class="status-item">
|
<div class="label">功能开关</div>
|
<div class="value" :style="{color: enabled ? '#67c23a' : '#f56c6c'}">
|
{{ enabled ? '已启用' : '已关闭' }}
|
</div>
|
<div class="desc">关闭后定时任务和手动触发均不执行</div>
|
</div>
|
<div class="status-item">
|
<div class="label">定时分析周期</div>
|
<div class="value">{{ periodLabel(config.scheduledPeriods || 'YESTERDAY') }}</div>
|
<div class="desc">定时任务分析的时间范围</div>
|
</div>
|
<div class="status-item">
|
<div class="label">定时执行时间</div>
|
<div class="value">{{ cronDesc }}</div>
|
<div class="desc">Cron: {{ config.cron || '0 0 1 * * ?' }}</div>
|
</div>
|
<div class="status-item">
|
<div class="label">公网上传</div>
|
<div class="value" :style="{color: config.uploadEnabled ? '#67c23a' : '#999'}">
|
{{ config.uploadEnabled ? '已启用' : '未启用' }}
|
</div>
|
<div class="desc">{{ config.uploadUrl || '未配置上传地址' }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="panel">
|
<div class="panel-head">
|
<div>
|
<div class="panel-title">手动分析</div>
|
<div style="color:#718299;font-size:12px;margin-top:2px;">点击按钮立即触发指定周期的 AI 数据分析</div>
|
</div>
|
</div>
|
<div class="panel-body">
|
<el-button-group>
|
<el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='TODAY'" @click="triggerAnalysis('TODAY')">分析今日</el-button>
|
<el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='YESTERDAY'" @click="triggerAnalysis('YESTERDAY')">分析昨日</el-button>
|
<el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='THIS_WEEK'" @click="triggerAnalysis('THIS_WEEK')">分析本周</el-button>
|
<el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='THIS_MONTH'" @click="triggerAnalysis('THIS_MONTH')">分析本月</el-button>
|
</el-button-group>
|
<span v-if="!enabled" style="margin-left:12px;color:#f56c6c;font-size:13px;">
|
<i class="el-icon-warning"></i> 功能未启用,请先打开上方开关
|
</span>
|
</div>
|
</div>
|
|
<div class="panel">
|
<div class="panel-head">
|
<div>
|
<div class="panel-title">分析报告</div>
|
<div style="color:#718299;font-size:12px;margin-top:2px;">最近生成的分析报告</div>
|
</div>
|
<el-button size="mini" :loading="reportsLoading" @click="loadReports">刷新</el-button>
|
</div>
|
<div class="panel-body">
|
<el-table :data="reports" v-loading="reportsLoading" stripe size="small" style="width:100%" @row-click="onReportClick">
|
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
<el-table-column prop="periodType" label="周期" min-width="80">
|
<template slot-scope="scope">
|
{{ periodLabel(scope.row.periodType) }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="triggerType" label="触发方式" width="90">
|
<template slot-scope="scope">
|
<el-tag size="mini" :type="scope.row.triggerType==='auto'?'success':'info'">
|
{{ scope.row.triggerType === 'auto' ? '定时' : '手动' }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="status" label="状态" width="80">
|
<template slot-scope="scope">
|
<el-tag size="mini" :type="statusType(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="createTime" label="创建时间" min-width="160">
|
<template slot-scope="scope">{{ formatTime(scope.row.createTime) }}</template>
|
</el-table-column>
|
<el-table-column prop="llmCallCount" label="LLM调用" width="80"></el-table-column>
|
<el-table-column prop="totalTokens" label="Token" width="90"></el-table-column>
|
<el-table-column prop="uploadStatus" label="上传" width="80">
|
<template slot-scope="scope">
|
<el-tag size="mini" :type="uploadType(scope.row.uploadStatus)">
|
{{ uploadLabel(scope.row.uploadStatus) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" fixed="right">
|
<template slot-scope="scope">
|
<el-button size="mini" type="text" @click.stop="viewReport(scope.row)">详情</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</div>
|
|
<div class="panel" v-if="selectedReport">
|
<div class="panel-head">
|
<div>
|
<div class="panel-title">报告详情 #{{ selectedReport.id }}</div>
|
<div style="color:#718299;font-size:12px;margin-top:2px;">
|
{{ periodLabel(selectedReport.periodType) }} · {{ formatTime(selectedReport.createTime) }}
|
</div>
|
</div>
|
<el-button size="mini" @click="selectedReport=null">关闭</el-button>
|
</div>
|
<div class="panel-body">
|
<div class="report-summary">
|
<h3>分析报告</h3>
|
<pre>{{ selectedReport.summary || '暂无报告内容' }}</pre>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
|
<script type="text/javascript" src="../../static/vue/element/element.js"></script>
|
<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
|
<script>
|
new Vue({
|
el: '#app',
|
data: function() {
|
return {
|
headerIcon: getAiIconHtml(36, 36),
|
baseUrl: baseUrl,
|
enabled: false,
|
config: {},
|
enabledLoading: false,
|
triggerLoading: false,
|
triggerPeriod: '',
|
reportsLoading: false,
|
reports: [],
|
selectedReport: null
|
};
|
},
|
computed: {
|
cronDesc: function() {
|
var cron = this.config.cron || '0 0 1 * * ?';
|
if (cron === '0 0 1 * * ?') return '每天凌晨 1:00';
|
if (cron === '0 0 2 * * ?') return '每天凌晨 2:00';
|
if (cron === '0 30 0 * * ?') return '每天 0:30';
|
return cron;
|
}
|
},
|
mounted: function() {
|
this.loadConfig();
|
this.loadReports();
|
},
|
methods: {
|
authHeaders: function() {
|
return { 'token': localStorage.getItem('token') };
|
},
|
requestJson: function(url, options) {
|
var requestOptions = options || {};
|
requestOptions.headers = requestOptions.headers || this.authHeaders();
|
return fetch(url, requestOptions).then(function(response) {
|
return response.json();
|
});
|
},
|
loadConfig: function() {
|
var self = this;
|
this.requestJson(this.baseUrl + '/ai/dataAnalysis/enabled/auth')
|
.then(function(res) {
|
if (res && res.code === 200 && res.data) {
|
self.enabled = res.data.enabled === true;
|
self.config = res.data;
|
}
|
});
|
},
|
onEnabledChange: function(val) {
|
var self = this;
|
this.enabledLoading = true;
|
this.requestJson(this.baseUrl + '/ai/dataAnalysis/enabled/auth?enabled=' + (val ? '1' : '0'), { method: 'POST' })
|
.then(function(res) {
|
self.enabledLoading = false;
|
if (res && res.code === 200) {
|
self.enabled = res.data && res.data.enabled === true;
|
self.$message.success(self.enabled ? '已启用数据分析' : '已关闭数据分析');
|
} else {
|
self.enabled = !val;
|
self.$message.error((res && res.msg) ? res.msg : '操作失败');
|
}
|
})
|
.catch(function() {
|
self.enabledLoading = false;
|
self.enabled = !val;
|
self.$message.error('请求失败');
|
});
|
},
|
triggerAnalysis: function(periodType) {
|
var self = this;
|
this.triggerLoading = true;
|
this.triggerPeriod = periodType;
|
this.requestJson(this.baseUrl + '/ai/dataAnalysis/trigger/auth?periodType=' + periodType, { method: 'POST' })
|
.then(function(res) {
|
self.triggerLoading = false;
|
self.triggerPeriod = '';
|
if (res && res.code === 200) {
|
var result = res.data;
|
if (result && result.skipped) {
|
self.$message.warning('已跳过: ' + (result.reason || '未知原因'));
|
} else {
|
self.$message.success('分析完成');
|
self.loadReports();
|
}
|
} else {
|
self.$message.error((res && res.msg) ? res.msg : '触发失败');
|
}
|
})
|
.catch(function() {
|
self.triggerLoading = false;
|
self.triggerPeriod = '';
|
self.$message.error('请求失败');
|
});
|
},
|
loadReports: function() {
|
var self = this;
|
this.reportsLoading = true;
|
this.requestJson(this.baseUrl + '/ai/dataAnalysis/reports/auth?limit=20')
|
.then(function(res) {
|
self.reportsLoading = false;
|
if (res && res.code === 200 && Array.isArray(res.data)) {
|
self.reports = res.data;
|
}
|
})
|
.catch(function() {
|
self.reportsLoading = false;
|
});
|
},
|
viewReport: function(row) {
|
var self = this;
|
this.requestJson(this.baseUrl + '/ai/dataAnalysis/report/' + row.id + '/auth')
|
.then(function(res) {
|
if (res && res.code === 200 && res.data) {
|
self.selectedReport = res.data;
|
}
|
});
|
},
|
onReportClick: function(row) {
|
this.viewReport(row);
|
},
|
periodLabel: function(t) {
|
var map = { 'TODAY': '今日', 'YESTERDAY': '昨日', 'THIS_WEEK': '本周', 'THIS_MONTH': '本月' };
|
return map[t] || t;
|
},
|
statusType: function(s) {
|
if (s === 'success') return 'success';
|
if (s === 'failed') return 'danger';
|
if (s === 'running') return 'warning';
|
return 'info';
|
},
|
statusLabel: function(s) {
|
var map = { 'pending': '待执行', 'running': '执行中', 'success': '成功', 'failed': '失败' };
|
return map[s] || s;
|
},
|
uploadType: function(s) {
|
if (s === 'uploaded') return 'success';
|
if (s === 'failed') return 'danger';
|
return 'info';
|
},
|
uploadLabel: function(s) {
|
var map = { 'pending': '待上传', 'uploaded': '已上传', 'failed': '失败', 'skipped': '跳过' };
|
return map[s] || s;
|
},
|
formatTime: function(t) {
|
if (!t) return '-';
|
var d = new Date(t);
|
if (isNaN(d.getTime())) return t;
|
var pad = function(n) { return n < 10 ? '0' + n : n; };
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
}
|
}
|
});
|
</script>
|
</body>
|
</html>
|