Files
cma-management/frontend/src/views/PredictAccuracy.vue
T

196 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="page-header">
<h3>预测准确率报表</h3>
<div class="header-actions">
<el-select v-model="entityId" size="small" style="width:200px;" @change="loadAccuracy">
<el-option label="陕西酣客(白酒经销)" :value="1" />
<el-option label="陕西博海科技(IT服务)" :value="2" />
</el-select>
<el-button size="small" @click="loadAccuracy" :loading="loading" type="primary">刷新</el-button>
</div>
</div>
<!-- 汇总卡片 -->
<el-row :gutter="16" style="margin-bottom:16px;">
<el-col :span="8">
<el-card shadow="never">
<div class="summary-card">
<div class="s-label">平均绝对误差 (MAE)</div>
<div class="s-value" :class="summary.avg_mae < 5 ? 'green' : summary.avg_mae < 10 ? 'orange' : 'red'">
{{ summary.avg_mae?.toFixed(2) || '--' }}
<span style="font-size:12px;color:#909399;">万元</span>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<div class="summary-card">
<div class="s-label">平均百分比误差 (MAPE)</div>
<div class="s-value" :class="summary.avg_mape < 15 ? 'green' : summary.avg_mape < 30 ? 'orange' : 'red'">
{{ summary.avg_mape?.toFixed(2) || '--' }}
<span style="font-size:12px;color:#909399;">%</span>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<div class="summary-card">
<div class="s-label">统计周期数</div>
<div class="s-value" style="color:#409eff;">
{{ summary.total_periods || 0 }}
<span style="font-size:12px;color:#909399;">个月</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 准确率趋势图 -->
<el-card style="margin-bottom:16px;">
<template #header>
<span>📈 预测准确率趋势MAE/MAPE</span>
</template>
<div ref="accuracyChartRef" style="height:360px;width:100%;"></div>
</el-card>
<!-- 每月明细 -->
<el-card>
<template #header>
<span>📋 每月预测 vs 实际对比</span>
<span style="margin-left:12px;font-size:12px;color:#999;" v-if="accuracyData.length"> {{ accuracyData.length }} </span>
</template>
<el-table :data="accuracyData" v-loading="loading" border stripe size="small" style="width:100%;">
<el-table-column prop="period" label="期间" width="100" />
<el-table-column prop="forecast_value" label="预测值(万)" width="130" align="right">
<template #default="{ row }">{{ row.forecast_value?.toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="actual_value" label="实际值(万)" width="130" align="right">
<template #default="{ row }">{{ row.actual_value?.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="差异(万)" width="120" align="right">
<template #default="{ row }">
<span :style="{ color: Math.abs(row.forecast_value - row.actual_value) < 5 ? '#67c23a' : '#f56c6c', fontWeight:600 }">
{{ (row.forecast_value - row.actual_value) > 0 ? '+' : '' }}{{ (row.forecast_value - row.actual_value)?.toFixed(2) }}
</span>
</template>
</el-table-column>
<el-table-column prop="mae" label="MAE(万)" width="100" align="right">
<template #default="{ row }">
<span :style="{ color: row.mae < 5 ? '#67c23a' : row.mae < 10 ? '#e6a23c' : '#f56c6c' }">{{ row.mae?.toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column prop="mape" label="MAPE(%)" width="100" align="right">
<template #default="{ row }">
<span :style="{ color: row.mape < 15 ? '#67c23a' : row.mape < 30 ? '#e6a23c' : '#f56c6c' }">{{ row.mape?.toFixed(2) }}%</span>
</template>
</el-table-column>
<el-table-column label="评级" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.mape < 15 ? 'success' : row.mape < 30 ? 'warning' : 'danger'" size="small">
{{ row.mape < 15 ? '准确' : row.mape < 30 ? '一般' : '偏差大' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { predictApi } from '../api/index'
const loading = ref(false)
const entityId = ref(1)
const accuracyData = ref<any[]>([])
const summary = ref<any>({})
const accuracyChartRef = ref<HTMLElement | null>(null)
let accuracyChart: any = null
async function loadAccuracy() {
loading.value = true
try {
const r: any = await predictApi.forecastAccuracy({ entity_id: entityId.value })
accuracyData.value = r.data || []
summary.value = r.summary || {}
nextTick(() => renderAccuracyChart())
} catch (e) {
ElMessage.error('加载预测准确率失败')
}
loading.value = false
}
function renderAccuracyChart() {
if (!accuracyChartRef.value || !accuracyData.value.length) return
import('echarts').then(echarts => {
if (accuracyChart) accuracyChart.dispose()
accuracyChart = echarts.init(accuracyChartRef.value!)
const periods = accuracyData.value.map((d: any) => d.period)
const maeData = accuracyData.value.map((d: any) => d.mae)
const mapeData = accuracyData.value.map((d: any) => d.mape)
const forecastVals = accuracyData.value.map((d: any) => d.forecast_value)
const actualVals = accuracyData.value.map((d: any) => d.actual_value)
accuracyChart.setOption({
grid: [{ left: 60, right: 30, top: 20, bottom: 50 }, { left: 60, right: 30, top: 320, bottom: 50 }],
xAxis: [
{ type: 'category', data: periods, axisLabel: { rotate: 45, fontSize: 10 }, gridIndex: 0 },
{ type: 'category', data: periods, axisLabel: { rotate: 45, fontSize: 10 }, gridIndex: 1 },
],
yAxis: [
{ type: 'value', name: '误差', gridIndex: 0 },
{ type: 'value', name: '现金(万)', gridIndex: 1 },
],
series: [
{
name: 'MAE', type: 'bar', data: maeData, xAxisIndex: 0, yAxisIndex: 0,
itemStyle: { color: '#409eff', borderRadius: [4, 4, 0, 0] },
barWidth: '30%',
},
{
name: 'MAPE(%)', type: 'line', data: mapeData, xAxisIndex: 0, yAxisIndex: 0,
lineStyle: { color: '#e6a23c', width: 2 },
symbol: 'circle', symbolSize: 6,
itemStyle: { color: '#e6a23c' },
},
{
name: '预测值', type: 'line', data: forecastVals, xAxisIndex: 1, yAxisIndex: 1,
lineStyle: { color: '#409eff', width: 2 },
symbol: 'diamond', symbolSize: 8,
itemStyle: { color: '#409eff' },
},
{
name: '实际值', type: 'line', data: actualVals, xAxisIndex: 1, yAxisIndex: 1,
lineStyle: { color: '#67c23a', width: 2 },
symbol: 'circle', symbolSize: 6,
itemStyle: { color: '#67c23a' },
},
],
tooltip: { trigger: 'axis' },
legend: { bottom: 0, icon: 'circle', itemWidth: 8, itemHeight: 8 },
})
})
}
onMounted(() => {
loadAccuracy()
})
</script>
<style scoped>
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; }
.page-header h3 { margin:0; }
.header-actions { display:flex; gap:8px; }
.summary-card { text-align:center; padding:8px 0; }
.s-label { font-size:13px; color:#909399; margin-bottom:8px; }
.s-value { font-size:28px; font-weight:bold; }
.s-value.green { color:#67c23a; }
.s-value.orange { color:#e6a23c; }
.s-value.red { color:#f56c6c; }
</style>