完善历史预测查看详情,删除功能
This commit is contained in:
parent
290e402181
commit
88f245b957
@ -14,7 +14,7 @@
|
||||
<el-form :model="filters" inline class="filter-form">
|
||||
<el-form-item label="产品">
|
||||
<el-select v-model="filters.product_id" placeholder="筛选产品" filterable clearable @change="fetchHistory">
|
||||
<el-option v-for="item in products" :key="item.product_id" :label="item.product_name" :value="item.product_id" />
|
||||
<el-option v-for="item in productFilterOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型类型">
|
||||
@ -82,18 +82,18 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailsVisible" title="预测详情" fullscreen destroy-on-close>
|
||||
<el-dialog v-model="detailsVisible" title="预测详情" fullscreen destroy-on-close @close="destroyCharts">
|
||||
<div v-if="currentPrediction" class="prediction-dialog-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<div class="prediction-summary">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="产品名称">{{ currentPrediction.meta?.product_name || currentPrediction.product_name || getProductName(currentPrediction.meta?.product_id) || '未知产品' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品名称">{{ currentPrediction.product_name || '未知产品' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="模型类型">
|
||||
<el-tag :type="getModelTagType(currentPrediction.meta?.model_type || currentPrediction.model_type)">{{ currentPrediction.meta?.model_type || currentPrediction.model_type || '未知模型' }}</el-tag>
|
||||
<el-tag :type="getModelTagType(currentPrediction.model_type)">{{ currentPrediction.model_type || '未知模型' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="预测起始日">{{ currentPrediction.meta?.start_date || currentPrediction.start_date || (currentPrediction.data?.prediction_data?.[0]?.date) || 'N/A' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预测时间">{{ formatDateTime(currentPrediction.meta?.created_at || currentPrediction.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预测起始日">{{ currentPrediction.start_date || 'N/A' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预测时间">{{ formatDateTime(currentPrediction.created_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-col>
|
||||
@ -107,7 +107,7 @@
|
||||
<div class="prediction-chart-container">
|
||||
<h3>预测趋势图</h3>
|
||||
<el-card shadow="hover" body-style="padding: 0;">
|
||||
<div id="fullscreen-prediction-chart-history" style="width: 100%; height: 500px;"></div>
|
||||
<canvas ref="predictionChartCanvas" style="width: 100%; height: 500px;"></canvas>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-col>
|
||||
@ -123,29 +123,29 @@
|
||||
<div class="stat-cards">
|
||||
<el-card shadow="hover">
|
||||
<template #header><div class="stat-header"><span>平均预测销量</span></div></template>
|
||||
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateAverage(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateAverage(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<template #header><div class="stat-header"><span>最高预测销量</span></div></template>
|
||||
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateMax(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateMax(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<template #header><div class="stat-header"><span>最低预测销量</span></div></template>
|
||||
<div class="stat-value">{{ currentPrediction?.data?.prediction_data ? calculateMin(currentPrediction.data.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||
<div class="stat-value">{{ currentPrediction?.prediction_data ? calculateMin(currentPrediction.prediction_data).toFixed(2) : '暂无数据' }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-table :data="currentPrediction?.data?.prediction_data || []" stripe height="330" border>
|
||||
<el-table :data="currentPrediction?.prediction_data || []" stripe height="330" border>
|
||||
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||
<el-table-column prop="predicted_sales" label="预测销量" sortable>
|
||||
<template #default="{ row }">{{ row.predicted_sales ? row.predicted_sales.toFixed(2) : '0.00' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="趋势">
|
||||
<template #default="{ row, $index }">
|
||||
<div v-if="$index > 0 && currentPrediction?.data?.prediction_data">
|
||||
<el-icon v-if="row.predicted_sales > currentPrediction.data.prediction_data[$index-1].predicted_sales" color="#67C23A"><ArrowUp /></el-icon>
|
||||
<el-icon v-else-if="row.predicted_sales < currentPrediction.data.prediction_data[$index-1].predicted_sales" color="#F56C6C"><ArrowDown /></el-icon>
|
||||
<div v-if="$index > 0 && currentPrediction?.prediction_data">
|
||||
<el-icon v-if="row.predicted_sales > currentPrediction.prediction_data[$index-1].predicted_sales" color="#67C23A"><ArrowUp /></el-icon>
|
||||
<el-icon v-else-if="row.predicted_sales < currentPrediction.prediction_data[$index-1].predicted_sales" color="#F56C6C"><ArrowDown /></el-icon>
|
||||
<el-icon v-else color="#909399"><Minus /></el-icon>
|
||||
{{ Math.abs(row.predicted_sales - currentPrediction.data.prediction_data[$index-1].predicted_sales).toFixed(2) }}
|
||||
{{ Math.abs(row.predicted_sales - currentPrediction.prediction_data[$index-1].predicted_sales).toFixed(2) }}
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
@ -153,7 +153,7 @@
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="历史数据">
|
||||
<el-table :data="currentPrediction?.data?.history_data || []" stripe height="400" border>
|
||||
<el-table :data="currentPrediction?.history_data || []" stripe height="400" border>
|
||||
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||
<el-table-column prop="sales" label="历史销量" sortable>
|
||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||
@ -161,7 +161,7 @@
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="全部数据">
|
||||
<el-table :data="currentPrediction?.data?.data || []" stripe height="400" border>
|
||||
<el-table :data="[...(currentPrediction?.history_data || []).map(d => ({...d, data_type: '历史销量'})), ...(currentPrediction?.prediction_data || []).map(d => ({...d, sales: d.predicted_sales, data_type: '预测销量'}))]" stripe height="400" border>
|
||||
<el-table-column prop="date" label="日期" width="150" :formatter="row => formatDate(row.date)" />
|
||||
<el-table-column prop="sales" label="销量" sortable>
|
||||
<template #default="{ row }">{{ row.sales ? row.sales.toFixed(2) : '0.00' }}</template>
|
||||
@ -212,7 +212,7 @@
|
||||
<el-col :span="24">
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||
<template #header><div class="analysis-header"><span>日环比变化</span></div></template>
|
||||
<div id="fullscreen-history-chart-history" style="width: 100%; height: 350px;"></div>
|
||||
<canvas ref="analysisChartCanvas" style="width: 100%; height: 350px;"></canvas>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -254,15 +254,17 @@ import { computed, onUnmounted } from 'vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const history = ref([]);
|
||||
const products = ref([]);
|
||||
const productFilterOptions = ref([]);
|
||||
const modelTypes = ref([]);
|
||||
const detailsVisible = ref(false);
|
||||
const currentPrediction = ref(null);
|
||||
const rawResponseData = ref(null);
|
||||
const showRawDataFlag = ref(false);
|
||||
const predictionChartCanvas = ref(null);
|
||||
const analysisChartCanvas = ref(null);
|
||||
|
||||
let predictionChart = null; // << 关键改动:使用单个chart实例
|
||||
let historyChart = null;
|
||||
let predictionChartInstance = null;
|
||||
let analysisChartInstance = null;
|
||||
|
||||
const filters = reactive({
|
||||
product_id: '',
|
||||
@ -287,14 +289,14 @@ const fetchModelTypes = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProducts = async () => {
|
||||
const fetchFilterOptions = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/products');
|
||||
const response = await axios.get('/api/history/filter-options');
|
||||
if (response.data.status === 'success') {
|
||||
products.value = response.data.data;
|
||||
productFilterOptions.value = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取产品列表失败');
|
||||
ElMessage.error('获取筛选选项列表失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
@ -311,7 +313,6 @@ const fetchHistory = async () => {
|
||||
if (response.data.status === 'success') {
|
||||
history.value = response.data.data;
|
||||
pagination.total = response.data.total;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取历史记录失败');
|
||||
@ -333,8 +334,8 @@ const handleCurrentChange = (page) => {
|
||||
// 添加getProductName函数
|
||||
const getProductName = (productId) => {
|
||||
if (!productId) return '未知产品';
|
||||
const product = products.value.find(p => p.product_id === productId);
|
||||
return product ? product.product_name : '未知产品';
|
||||
const option = productFilterOptions.value.find(p => p.value === productId);
|
||||
return option ? option.label : '未知产品';
|
||||
};
|
||||
|
||||
const viewDetails = async (id) => {
|
||||
@ -345,11 +346,19 @@ const viewDetails = async (id) => {
|
||||
|
||||
if (responseData && responseData.status === 'success' && responseData.data) {
|
||||
// 简化逻辑:直接信任后端返回的、结构正确的 responseData.data
|
||||
// 关键修复:将后端返回的扁平化数据结构正确赋值
|
||||
// 后端返回的 data 字段直接包含了所有需要的信息
|
||||
currentPrediction.value = responseData.data;
|
||||
detailsVisible.value = true;
|
||||
|
||||
console.log('接收到并准备渲染的预测详情数据:', currentPrediction.value);
|
||||
|
||||
// 最终修复:在数据加载和弹窗可见后,通过nextTick确保DOM渲染完毕再初始化图表
|
||||
nextTick(() => {
|
||||
renderPredictionChart(currentPrediction.value);
|
||||
renderHistoryAnalysisChart(currentPrediction.value);
|
||||
});
|
||||
|
||||
} else {
|
||||
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
|
||||
}
|
||||
@ -903,155 +912,116 @@ const getFactorsArray = computed(() => {
|
||||
return [];
|
||||
});
|
||||
|
||||
watch(detailsVisible, (newVal) => {
|
||||
if (newVal && currentPrediction.value) {
|
||||
nextTick(() => {
|
||||
renderChart();
|
||||
// 可以在这里添加渲染第二个图表的逻辑
|
||||
// renderHistoryAnalysisChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
const renderPredictionChart = (predictionData) => {
|
||||
const canvasEl = predictionChartCanvas.value;
|
||||
if (!canvasEl || !predictionData) return;
|
||||
|
||||
// << 关键改动:从ProductPredictionView.vue复制并适应的renderChart函数
|
||||
const renderChart = () => {
|
||||
const chartCanvas = document.getElementById('fullscreen-prediction-chart-history');
|
||||
if (!chartCanvas || !currentPrediction.value || !currentPrediction.value.data) return;
|
||||
|
||||
if (predictionChart) {
|
||||
predictionChart.destroy();
|
||||
if (predictionChartInstance) {
|
||||
predictionChartInstance.destroy();
|
||||
}
|
||||
|
||||
const formatDate = (date) => new Date(date).toISOString().split('T')[0];
|
||||
const historyData = (predictionData.history_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
|
||||
const predData = (predictionData.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
|
||||
|
||||
const historyData = (currentPrediction.value.data.history_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
|
||||
const predictionData = (currentPrediction.value.data.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
|
||||
|
||||
if (historyData.length === 0 && predictionData.length === 0) {
|
||||
const ctx = chartCanvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, chartCanvas.width, chartCanvas.height);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#909399';
|
||||
ctx.font = '20px Arial';
|
||||
ctx.fillText('暂无详细图表数据', chartCanvas.width / 2, chartCanvas.height / 2);
|
||||
if (historyData.length === 0 && predData.length === 0) {
|
||||
// Handle no data case if needed
|
||||
return;
|
||||
}
|
||||
|
||||
const allLabels = [...new Set([...historyData.map(p => p.date), ...predictionData.map(p => p.date)])].sort();
|
||||
const simplifiedLabels = allLabels.map(date => date.split('-')[2]);
|
||||
|
||||
const allLabels = [...new Set([...historyData.map(p => p.date), ...predData.map(p => p.date)])].sort();
|
||||
const simplifiedLabels = allLabels.map(date => date.split('-').slice(1).join('/'));
|
||||
const historyMap = new Map(historyData.map(p => [p.date, p.sales]));
|
||||
// 修正:预测数据使用 'predicted_sales' 字段
|
||||
const predictionMap = new Map(predictionData.map(p => [p.date, p.predicted_sales]));
|
||||
|
||||
const predictionMap = new Map(predData.map(p => [p.date, p.predicted_sales]));
|
||||
const alignedHistorySales = allLabels.map(label => historyMap.get(label) ?? null);
|
||||
const alignedPredictionSales = allLabels.map(label => predictionMap.get(label) ?? null);
|
||||
|
||||
if (historyData.length > 0 && predictionData.length > 0) {
|
||||
if (historyData.length > 0 && predData.length > 0) {
|
||||
const lastHistoryDate = historyData[historyData.length - 1].date;
|
||||
const lastHistoryValue = historyData[historyData.length - 1].sales;
|
||||
if (!predictionMap.has(lastHistoryDate)) {
|
||||
alignedPredictionSales[allLabels.indexOf(lastHistoryDate)] = lastHistoryValue;
|
||||
const lastHistoryIndex = allLabels.indexOf(lastHistoryDate);
|
||||
if (!predictionMap.has(lastHistoryDate) && lastHistoryIndex !== -1) {
|
||||
alignedPredictionSales[lastHistoryIndex] = lastHistoryValue;
|
||||
}
|
||||
}
|
||||
|
||||
let subtitleText = '';
|
||||
if (historyData.length > 0) {
|
||||
subtitleText += `历史数据: ${historyData[0].date} ~ ${historyData[historyData.length - 1].date}`;
|
||||
}
|
||||
if (predictionData.length > 0) {
|
||||
if (historyData.length > 0) subtitleText += `历史: ${historyData[0].date} ~ ${historyData[historyData.length - 1].date}`;
|
||||
if (predData.length > 0) {
|
||||
if (subtitleText) subtitleText += ' | ';
|
||||
subtitleText += `预测数据: ${predictionData[0].date} ~ ${predictionData[predictionData.length - 1].date}`;
|
||||
subtitleText += `预测: ${predData[0].date} ~ ${predData[predData.length - 1].date}`;
|
||||
}
|
||||
|
||||
predictionChart = new Chart(chartCanvas, {
|
||||
predictionChartInstance = new Chart(canvasEl, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: simplifiedLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: '历史销量',
|
||||
data: alignedHistorySales,
|
||||
borderColor: '#67C23A',
|
||||
backgroundColor: 'rgba(103, 194, 58, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
spanGaps: false,
|
||||
},
|
||||
{
|
||||
label: '预测销量',
|
||||
data: alignedPredictionSales,
|
||||
borderColor: '#409EFF',
|
||||
backgroundColor: 'rgba(64, 158, 255, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
borderDash: [5, 5],
|
||||
}
|
||||
{ label: '历史销量', data: alignedHistorySales, borderColor: '#67C23A', backgroundColor: 'rgba(103, 194, 58, 0.2)', tension: 0.4, fill: true, spanGaps: false },
|
||||
{ label: '预测销量', data: alignedPredictionSales, borderColor: '#409EFF', backgroundColor: 'rgba(64, 158, 255, 0.2)', tension: 0.4, fill: true, borderDash: [5, 5] }
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `${currentPrediction.value.data.product_name} - 销量预测趋势图`,
|
||||
color: '#ffffff',
|
||||
font: {
|
||||
size: 20,
|
||||
weight: 'bold',
|
||||
}
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: subtitleText,
|
||||
color: '#6c757d',
|
||||
font: {
|
||||
size: 14,
|
||||
},
|
||||
padding: {
|
||||
bottom: 20
|
||||
}
|
||||
}
|
||||
title: { display: true, text: `${predictionData.product_name} - 销量预测趋势图`, color: '#ffffff', font: { size: 20, weight: 'bold' } },
|
||||
subtitle: { display: true, text: subtitleText, color: '#6c757d', font: { size: 14 }, padding: { bottom: 20 } }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '日期 (日)'
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '销量'
|
||||
},
|
||||
grid: {
|
||||
color: '#e9e9e9',
|
||||
drawBorder: false,
|
||||
},
|
||||
beginAtZero: true
|
||||
x: { title: { display: true, text: '日期' }, grid: { display: false } },
|
||||
y: { title: { display: true, text: '销量' }, grid: { color: '#e9e9e9', drawBorder: false }, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderHistoryAnalysisChart = (predictionData) => {
|
||||
const canvasEl = analysisChartCanvas.value;
|
||||
if (!canvasEl || !predictionData.analysis || !predictionData.analysis.history_chart_data) return;
|
||||
|
||||
if (analysisChartInstance) {
|
||||
analysisChartInstance.destroy();
|
||||
}
|
||||
|
||||
const chartData = predictionData.analysis.history_chart_data;
|
||||
analysisChartInstance = new Chart(canvasEl, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: chartData.dates.map(d => d.split('-').slice(1).join('/')),
|
||||
datasets: [{
|
||||
label: '日环比变化 (%)',
|
||||
data: chartData.changes,
|
||||
backgroundColor: chartData.changes.map(c => c >= 0 ? 'rgba(103, 194, 58, 0.6)' : 'rgba(245, 108, 108, 0.6)'),
|
||||
borderColor: chartData.changes.map(c => c >= 0 ? '#67C23A' : '#F56C6C'),
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { title: { display: true, text: '变化率 (%)' } } }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const exportHistoryData = () => {
|
||||
if (!currentPrediction.value) return;
|
||||
const data = currentPrediction.value.data.data;
|
||||
const allData = [
|
||||
...(currentPrediction.value.history_data || []).map(d => ({...d, data_type: '历史销量'})),
|
||||
...(currentPrediction.value.prediction_data || []).map(d => ({...d, sales: d.predicted_sales, data_type: '预测销量'}))
|
||||
];
|
||||
let csvContent = "日期,销量,数据类型\n";
|
||||
data.forEach(row => {
|
||||
csvContent += `${new Date(row.date).toLocaleDateString()},${row.sales.toFixed(2)},${row.data_type}\n`;
|
||||
allData.forEach(row => {
|
||||
const salesValue = row.sales || row.predicted_sales || 0;
|
||||
csvContent += `${new Date(row.date).toLocaleDateString()},${salesValue.toFixed(2)},${row.data_type}\n`;
|
||||
});
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
const { product_name, model_type, start_date } = currentPrediction.value.meta;
|
||||
const { product_name, model_type, start_date } = currentPrediction.value;
|
||||
link.setAttribute('download', `历史预测_${product_name}_${model_type}_${start_date}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
@ -1060,28 +1030,34 @@ const exportHistoryData = () => {
|
||||
};
|
||||
|
||||
const resizeCharts = () => {
|
||||
if (predictionChart) {
|
||||
predictionChart.resize();
|
||||
if (predictionChartInstance) {
|
||||
predictionChartInstance.resize();
|
||||
}
|
||||
if (historyChart) {
|
||||
historyChart.resize();
|
||||
if (analysisChartInstance) {
|
||||
analysisChartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
|
||||
const destroyCharts = () => {
|
||||
if (predictionChartInstance) {
|
||||
predictionChartInstance.destroy();
|
||||
predictionChartInstance = null;
|
||||
}
|
||||
if (analysisChartInstance) {
|
||||
analysisChartInstance.destroy();
|
||||
analysisChartInstance = null;
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
if (predictionChart) {
|
||||
predictionChart.destroy();
|
||||
}
|
||||
if (historyChart) {
|
||||
historyChart.destroy();
|
||||
}
|
||||
destroyCharts(); // 在组件卸载时也调用
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchProducts();
|
||||
fetchFilterOptions();
|
||||
fetchModelTypes();
|
||||
fetchHistory();
|
||||
});
|
||||
|
Binary file not shown.
110
server/api.py
110
server/api.py
@ -1398,6 +1398,24 @@ def predict():
|
||||
if prediction_result is None:
|
||||
return jsonify({"status": "error", "message": "预测失败,核心预测器返回None"}), 500
|
||||
|
||||
# 调试步骤:在分析函数调用后,打印其输入和输出
|
||||
history_data_for_analysis = prediction_result.get('history_data', [])
|
||||
prediction_data_for_analysis = prediction_result.get('prediction_data', [])
|
||||
logger.info(f"DEBUG: 分析函数的输入参数检查 - "
|
||||
f"history_data 类型: {type(history_data_for_analysis)}, 长度: {len(history_data_for_analysis)}. "
|
||||
f"prediction_data 类型: {type(prediction_data_for_analysis)}, 长度: {len(prediction_data_for_analysis)}.")
|
||||
logger.info(f"DEBUG: 核心预测器返回的 'analysis' 字段内容: {prediction_result.get('analysis')}")
|
||||
|
||||
# 根本修复:确保 'analysis' 字段始终存在且结构正确
|
||||
if not prediction_result.get('analysis'):
|
||||
logger.warning(f"模型 {model_uid} 的预测结果中缺少分析数据,将使用默认空对象。")
|
||||
prediction_result['analysis'] = {
|
||||
'description': '未能生成有效的趋势分析。',
|
||||
'metrics': {},
|
||||
'peaks': [],
|
||||
'history_chart_data': {'dates': [], 'changes': []}
|
||||
}
|
||||
|
||||
# 遵循用户规范,使用相对路径生成文件名
|
||||
model_display_name = model_record.get('display_name', 'unknown_model')
|
||||
safe_model_name = secure_filename(model_display_name).replace(' ', '_').replace('-_', '_')
|
||||
@ -1733,12 +1751,25 @@ def get_prediction_history():
|
||||
query_params = []
|
||||
|
||||
if product_id:
|
||||
query_conditions.append("json_extract(prediction_scope, '$.product_id') = ?")
|
||||
# v12 修复:支持混合类型的筛选
|
||||
# 判断传入的ID类型:产品ID(数字), 店铺ID(S开头), 或特殊名称(其他字符串)
|
||||
if product_id.isdigit():
|
||||
# 按产品ID筛选
|
||||
query_conditions.append("prediction_scope LIKE ?")
|
||||
query_params.append(f'%"product_id": "{product_id}"%')
|
||||
elif product_id.startswith('S') and product_id[1:].isdigit():
|
||||
# 按店铺ID筛选
|
||||
query_conditions.append("prediction_scope LIKE ?")
|
||||
query_params.append(f'%"store_id": "{product_id}"%')
|
||||
else:
|
||||
# 按特殊名称筛选 (如 全局预测)
|
||||
query_conditions.append("product_name = ?")
|
||||
query_params.append(product_id)
|
||||
|
||||
if model_type:
|
||||
query_conditions.append("model_type = ?")
|
||||
query_params.append(model_type)
|
||||
# v10 修复: 使用LIKE以兼容(best)版本
|
||||
query_conditions.append("model_type LIKE ?")
|
||||
query_params.append(f"{model_type}%")
|
||||
|
||||
# 构建完整的查询语句
|
||||
query = "SELECT * FROM prediction_history"
|
||||
@ -1846,7 +1877,6 @@ def get_prediction_details(prediction_id):
|
||||
|
||||
if not full_path or not os.path.exists(full_path):
|
||||
logger.error(f"预测结果文件不存在或路径无效。相对路径: '{relative_file_path}', 检查的绝对路径: '{full_path}'")
|
||||
print(f"DEBUG: JSON文件路径检查失败。相对路径: '{relative_file_path}', 检查的绝对路径: '{full_path}'") # 添加调试打印
|
||||
# 即使文件不存在,也尝试返回基本信息,避免前端崩溃
|
||||
final_payload = {
|
||||
'product_name': record['product_name'] if 'product_name' in record_keys else 'N/A',
|
||||
@ -1860,7 +1890,6 @@ def get_prediction_details(prediction_id):
|
||||
return jsonify({"status": "success", "data": final_payload})
|
||||
|
||||
# 读取和解析JSON文件
|
||||
print(f"DEBUG: 正在尝试读取JSON文件,绝对路径: '{full_path}'") # 添加调试打印
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
saved_data = json.load(f)
|
||||
|
||||
@ -1871,8 +1900,6 @@ def get_prediction_details(prediction_id):
|
||||
history_data = core_data.get('history_data', [])
|
||||
prediction_data = core_data.get('prediction_data', [])
|
||||
|
||||
# 立即打印提取到的数据长度
|
||||
print(f"DEBUG: Extracted data lengths - History: {len(history_data)}, Prediction: {len(prediction_data)}")
|
||||
|
||||
# 构建最终的、完整的响应数据
|
||||
final_payload = {
|
||||
@ -1885,13 +1912,26 @@ def get_prediction_details(prediction_id):
|
||||
'analysis': core_data.get('analysis', {}),
|
||||
}
|
||||
|
||||
# 动态修复:如果从文件中加载的分析数据为空,则实时生成
|
||||
if not final_payload.get('analysis'):
|
||||
logger.info(f"记录 {prediction_id} 的分析数据为空,正在尝试动态生成...")
|
||||
# 构建调用 analyze_prediction 所需的输入
|
||||
dynamic_analysis_input = {
|
||||
"history_data": final_payload.get('history_data', []),
|
||||
"prediction_data": final_payload.get('prediction_data', [])
|
||||
}
|
||||
dynamic_analysis_result = analyze_prediction(dynamic_analysis_input)
|
||||
if dynamic_analysis_result:
|
||||
final_payload['analysis'] = dynamic_analysis_result
|
||||
logger.info(f"成功为记录 {prediction_id} 动态生成分析数据。")
|
||||
else:
|
||||
logger.warning(f"为记录 {prediction_id} 动态生成分析数据失败。")
|
||||
|
||||
response_data = {
|
||||
"status": "success",
|
||||
"data": final_payload
|
||||
}
|
||||
|
||||
# 在返回前,完整打印最终的响应数据
|
||||
print(f"DEBUG: Final response payload being sent to frontend: {json.dumps(response_data, cls=CustomJSONEncoder, ensure_ascii=False)}")
|
||||
logger.info(f"成功构建并返回历史预测详情 (v8): ID={prediction_id}")
|
||||
return jsonify(response_data)
|
||||
|
||||
@ -1909,14 +1949,15 @@ def delete_prediction(prediction_id):
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 查询预测记录
|
||||
cursor.execute("SELECT file_path FROM prediction_history WHERE id = ?", (prediction_id,))
|
||||
# v15 修复: 修正列名,并使用绝对路径删除文件
|
||||
cursor.execute("SELECT result_file_path FROM prediction_history WHERE id = ?", (prediction_id,))
|
||||
record = cursor.fetchone()
|
||||
|
||||
if not record:
|
||||
conn.close()
|
||||
return jsonify({"status": "error", "message": "预测记录不存在"}), 404
|
||||
|
||||
file_path = record[0]
|
||||
relative_file_path = record[0]
|
||||
|
||||
# 删除数据库记录
|
||||
cursor.execute("DELETE FROM prediction_history WHERE id = ?", (prediction_id,))
|
||||
@ -1924,8 +1965,19 @@ def delete_prediction(prediction_id):
|
||||
conn.close()
|
||||
|
||||
# 删除预测结果文件
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if relative_file_path:
|
||||
# 构建绝对路径以确保文件能被正确找到和删除
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
absolute_file_path = os.path.join(project_root, relative_file_path)
|
||||
|
||||
if os.path.exists(absolute_file_path):
|
||||
try:
|
||||
os.remove(absolute_file_path)
|
||||
logger.info(f"成功删除文件: {absolute_file_path}")
|
||||
except OSError as e:
|
||||
logger.error(f"删除文件失败: {absolute_file_path}, 错误: {e}")
|
||||
else:
|
||||
logger.warning(f"尝试删除但文件未找到: {absolute_file_path}")
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
@ -1937,6 +1989,38 @@ def delete_prediction(prediction_id):
|
||||
traceback.print_exc()
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
@app.route('/api/history/filter-options', methods=['GET'])
|
||||
def get_history_filter_options():
|
||||
"""获取历史记录页面用于筛选的选项列表"""
|
||||
try:
|
||||
# 1. 获取所有标准产品
|
||||
products = get_available_products()
|
||||
|
||||
# 2. 获取所有店铺
|
||||
stores = get_available_stores()
|
||||
|
||||
# 3. 从历史记录中获取特殊的预测名称(如全局预测)
|
||||
options_map = {}
|
||||
|
||||
# 添加产品
|
||||
for p in products:
|
||||
options_map[p['product_id']] = {'value': p['product_id'], 'label': p['product_name'], 'type': 'product'}
|
||||
|
||||
# 添加店铺
|
||||
for s in stores:
|
||||
options_map[s['store_id']] = {'value': s['store_id'], 'label': s['store_name'], 'type': 'store'}
|
||||
|
||||
# v14 修复: 硬编码添加“全局预测”选项
|
||||
global_prediction_key = "全局预测"
|
||||
if global_prediction_key not in options_map:
|
||||
options_map[global_prediction_key] = {'value': global_prediction_key, 'label': global_prediction_key, 'type': 'special'}
|
||||
|
||||
return jsonify({"status": "success", "data": list(options_map.values())})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取历史筛选选项失败: {e}\n{traceback.format_exc()}")
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
# 4. 模型管理API
|
||||
@app.route('/api/models', methods=['GET'])
|
||||
@swag_from({
|
||||
|
Loading…
x
Reference in New Issue
Block a user