Merge branch 'lyf-dev-req0003' into lyf-dev

This commit is contained in:
LYFxiaoan 2025-07-25 09:39:56 +08:00
commit 919d7db7ad
7 changed files with 182 additions and 514 deletions

View File

@ -87,12 +87,12 @@
<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 || (currentPrediction.prediction_data?.[0]?.date) || 'N/A' }}</el-descriptions-item>
<el-descriptions-item label="预测时间">{{ formatDateTime(currentPrediction.created_at) }}</el-descriptions-item>
</el-descriptions>
</div>
</el-col>
@ -106,7 +106,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="chartCanvas" style="width: 100%; height: 500px;"></canvas>
</el-card>
</div>
</el-col>
@ -122,29 +122,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="sales" label="预测销量" sortable>
<template #default="{ row }">{{ row.sales ? row.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.sales > currentPrediction.data.prediction_data[$index-1].sales" color="#67C23A"><ArrowUp /></el-icon>
<el-icon v-else-if="row.sales < currentPrediction.data.prediction_data[$index-1].sales" color="#F56C6C"><ArrowDown /></el-icon>
<div v-if="$index > 0 && currentPrediction?.prediction_data">
<el-icon v-if="row.sales > currentPrediction.prediction_data[$index-1].sales" color="#67C23A"><ArrowUp /></el-icon>
<el-icon v-else-if="row.sales < currentPrediction.prediction_data[$index-1].sales" color="#F56C6C"><ArrowDown /></el-icon>
<el-icon v-else color="#909399"><Minus /></el-icon>
{{ Math.abs(row.sales - currentPrediction.data.prediction_data[$index-1].sales).toFixed(2) }}
{{ Math.abs(row.sales - currentPrediction.prediction_data[$index-1].sales).toFixed(2) }}
</div>
<span v-else>-</span>
</template>
@ -152,7 +152,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>
@ -160,7 +160,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, 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>
@ -248,7 +248,7 @@ import { ref, onMounted, reactive, watch, nextTick } from 'vue';
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { QuestionFilled, Search, View, Delete, ArrowUp, ArrowDown, Minus, Download } from '@element-plus/icons-vue';
import Chart from 'chart.js/auto'; // << Chart.js
import Chart from 'chart.js/auto';
import { computed, onUnmounted } from 'vue';
const loading = ref(false);
@ -257,10 +257,9 @@ const products = ref([]);
const modelTypes = ref([]);
const detailsVisible = ref(false);
const currentPrediction = ref(null);
const rawResponseData = ref(null);
const showRawDataFlag = ref(false);
const chartCanvas = ref(null); // ref
let predictionChart = null; // << 使chart
let predictionChart = null;
let historyChart = null;
const filters = reactive({
@ -327,7 +326,6 @@ const handleCurrentChange = (page) => {
fetchHistory();
};
// getProductName
const getProductName = (productId) => {
if (!productId) return '未知产品';
const product = products.value.find(p => p.product_id === productId);
@ -338,68 +336,11 @@ const viewDetails = async (id) => {
try {
const response = await axios.get(`/api/prediction/history/${id}`);
const responseData = response.data;
rawResponseData.value = JSON.stringify(responseData, null, 2);
if (responseData && responseData.status === 'success') {
// 使
//
// meta
if (!responseData.meta) {
const historyRecord = history.value.find(item => item.id === id);
responseData.meta = {
product_name: historyRecord?.product_name || responseData.product_name || '未知产品',
product_id: historyRecord?.product_id || responseData.product_id || 'N/A',
model_type: historyRecord?.model_type || responseData.model_type || '未知模型',
start_date: historyRecord?.start_date || responseData.start_date || 'N/A',
created_at: historyRecord?.created_at || responseData.created_at || new Date().toISOString(),
future_days: historyRecord?.future_days || responseData.future_days || 7,
model_id: historyRecord?.model_id || responseData.model_id || 'N/A'
};
}
// data
if (!responseData.data) {
responseData.data = {
prediction_data: [],
history_data: [],
data: []
};
}
//
if (!Array.isArray(responseData.data.prediction_data)) {
responseData.data.prediction_data = responseData.prediction_data || [];
}
if (!Array.isArray(responseData.data.history_data)) {
responseData.data.history_data = responseData.history_data || [];
}
if (!Array.isArray(responseData.data.data)) {
//
const historyData = responseData.data.history_data || [];
const predictionData = responseData.data.prediction_data || [];
//
const markedHistoryData = historyData.map(item => ({
...item,
data_type: item.data_type || '历史销量'
}));
const markedPredictionData = predictionData.map(item => ({
...item,
data_type: item.data_type || '预测销量'
}));
responseData.data.data = [...markedHistoryData, ...markedPredictionData];
}
currentPrediction.value = responseData;
if (responseData && responseData.status === 'success' && responseData.data) {
currentPrediction.value = responseData.data;
detailsVisible.value = true;
console.log('预测详情数据:', responseData);
console.log('标准化的预测详情数据:', currentPrediction.value);
} else {
ElMessage.error('获取详情失败: ' + (responseData?.message || responseData?.error || '数据格式错误'));
}
@ -425,13 +366,11 @@ const deleteHistory = (id) => {
try {
await axios.delete(`/api/prediction/history/${id}`);
ElMessage.success('删除成功');
fetchHistory(); //
fetchHistory();
} catch (error) {
ElMessage.error('删除失败');
}
}).catch(() => {
//
});
}).catch(() => {});
};
const formatDateTime = (isoString) => {
@ -462,400 +401,6 @@ const getModelTagType = (modelType) => {
return types[modelType] || 'info';
};
const showRawData = () => {
showRawDataFlag.value = !showRawDataFlag.value;
};
const useRawDataDirectly = () => {
try {
if (!rawResponseData.value) {
ElMessage.warning('没有原始数据可用');
return;
}
// JSON
const rawData = JSON.parse(rawResponseData.value);
console.log('尝试直接使用原始数据:', rawData);
//
const processedData = {
status: 'success',
meta: {},
data: {
prediction_data: [],
history_data: []
}
};
//
if (rawData.meta) {
processedData.meta = rawData.meta;
} else if (rawData.product_name || rawData.model_type) {
processedData.meta = {
product_name: rawData.product_name,
model_type: rawData.model_type,
created_at: rawData.created_at,
start_date: rawData.start_date,
future_days: rawData.future_days,
model_id: rawData.model_id
};
}
// -
if (rawData.data && rawData.data.history_data) {
console.log('从原始数据中提取历史数据');
processedData.data.history_data = rawData.data.history_data.map(item => {
// NaN
const newItem = {...item};
if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN')) {
newItem.predicted_sales = null;
}
if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN')) {
newItem.sales = null;
}
return newItem;
});
} else if (Array.isArray(rawData)) {
console.log('原始数据是数组,直接使用');
processedData.data.history_data = rawData.map(item => {
const newItem = {...item};
if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN')) {
newItem.predicted_sales = null;
}
if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN')) {
newItem.sales = null;
}
return newItem;
});
}
//
if (rawData.data && rawData.data.prediction_data) {
console.log('从原始数据中提取预测数据');
processedData.data.prediction_data = rawData.data.prediction_data.map(item => {
// NaN
const newItem = {...item};
if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN')) {
newItem.predicted_sales = null;
}
if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN')) {
newItem.sales = null;
}
return newItem;
});
}
//
if (processedData.data.history_data.length > 0 && processedData.data.prediction_data.length === 0) {
console.log('生成预测数据');
// 7
const lastWeekData = [...processedData.data.history_data]
.slice(-7)
.map(item => ({
...item,
data_type: '预测销量',
predicted_sales: item.sales,
// 7
date: new Date(new Date(item.date).getTime() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
}));
processedData.data.prediction_data = lastWeekData;
}
console.log('处理后的数据:', processedData);
//
currentPrediction.value = processedData;
//
nextTick(() => {
// initDetailsChart();
});
ElMessage.success('已尝试直接使用原始数据');
} catch (error) {
console.error('使用原始数据失败:', error);
ElMessage.error(`使用原始数据失败: ${error.message}`);
}
};
const exportRawData = () => {
if (!rawResponseData.value) {
ElMessage.warning('没有原始数据可导出');
return;
}
try {
// Blob
const blob = new Blob([rawResponseData.value], { type: 'application/json' });
//
const url = URL.createObjectURL(blob);
// a
const link = document.createElement('a');
link.href = url;
link.download = `prediction_raw_data_${new Date().getTime()}.json`;
document.body.appendChild(link);
link.click();
//
URL.revokeObjectURL(url);
document.body.removeChild(link);
ElMessage.success('原始数据已导出为JSON文件');
} catch (error) {
console.error('导出数据失败:', error);
ElMessage.error(`导出数据失败: ${error.message}`);
}
};
const handleFileChange = (file) => {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const jsonData = e.target.result;
console.log('导入的JSON文件大小:', jsonData.length, '字节');
//
rawResponseData.value = jsonData;
//
showRawDataFlag.value = true;
// JSON
try {
// 使JSONNaN
const parsedData = parseJSONWithNaN(jsonData);
console.log('解析JSON数据成功');
//
console.log('数据结构:', Object.keys(parsedData));
if (parsedData.data) {
console.log('data字段类型:', typeof parsedData.data);
if (typeof parsedData.data === 'object') {
console.log('data字段结构:', Object.keys(parsedData.data));
if (parsedData.data.history_data) {
console.log('history_data类型:', typeof parsedData.data.history_data);
console.log('history_data是否为数组:', Array.isArray(parsedData.data.history_data));
console.log('history_data长度:', Array.isArray(parsedData.data.history_data) ? parsedData.data.history_data.length : 'N/A');
if (Array.isArray(parsedData.data.history_data) && parsedData.data.history_data.length > 0) {
console.log('history_data第一项:', parsedData.data.history_data[0]);
}
}
if (parsedData.data.prediction_data) {
console.log('prediction_data类型:', typeof parsedData.data.prediction_data);
console.log('prediction_data是否为数组:', Array.isArray(parsedData.data.prediction_data));
console.log('prediction_data长度:', Array.isArray(parsedData.data.prediction_data) ? parsedData.data.prediction_data.length : 'N/A');
if (Array.isArray(parsedData.data.prediction_data) && parsedData.data.prediction_data.length > 0) {
console.log('prediction_data第一项:', parsedData.data.prediction_data[0]);
}
}
} else if (typeof parsedData.data === 'string') {
console.log('data字段是字符串可能需要进一步解析');
try {
// 使JSONNaN
const nestedData = parseJSONWithNaN(parsedData.data);
console.log('嵌套数据结构:', Object.keys(nestedData));
} catch (nestedError) {
console.error('解析嵌套数据失败:', nestedError);
}
}
} else if (Array.isArray(parsedData)) {
console.log('数据是数组,长度:', parsedData.length);
if (parsedData.length > 0) {
console.log('数组第一项:', parsedData[0]);
}
}
} catch (parseError) {
console.error('解析JSON数据失败:', parseError);
// 使
}
ElMessage.success('JSON文件导入成功');
} catch (error) {
console.error('读取JSON文件失败:', error);
ElMessage.error(`读取JSON文件失败: ${error.message}`);
}
};
reader.onerror = (error) => {
console.error('读取文件失败:', error);
ElMessage.error('读取文件失败');
};
reader.readAsText(file.raw);
};
const useImportedData = () => {
if (!rawResponseData.value) {
ElMessage.warning('没有导入的数据可用');
return;
}
try {
// JSON
let importedData;
try {
// 使JSONNaN
importedData = parseJSONWithNaN(rawResponseData.value);
console.log('成功解析导入的数据');
} catch (parseError) {
console.error('解析导入数据失败:', parseError);
ElMessage.error(`解析导入数据失败: ${parseError.message}`);
return;
}
console.log('使用导入的数据:', importedData);
//
const processedData = {
status: 'success',
meta: {},
data: {
prediction_data: [],
history_data: []
}
};
//
if (importedData.meta) {
processedData.meta = importedData.meta;
} else if (importedData.product_name || importedData.model_type) {
processedData.meta = {
product_name: importedData.product_name,
model_type: importedData.model_type,
created_at: importedData.created_at,
start_date: importedData.start_date,
future_days: importedData.future_days,
model_id: importedData.model_id
};
}
//
if (importedData.data && importedData.data.history_data && Array.isArray(importedData.data.history_data)) {
console.log('从导入数据中提取历史数据,长度:', importedData.data.history_data.length);
processedData.data.history_data = importedData.data.history_data.map(item => {
const newItem = {...item};
// NaN
if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN' || newItem.predicted_sales === null)) {
newItem.predicted_sales = null;
}
if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN' || newItem.sales === null)) {
newItem.sales = null;
}
return newItem;
});
} else if (Array.isArray(importedData)) {
// 使
console.log('导入数据是数组,尝试作为历史数据使用');
processedData.data.history_data = importedData.map(item => {
const newItem = {...item};
if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN' || newItem.predicted_sales === null)) {
newItem.predicted_sales = null;
}
if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN' || newItem.sales === null)) {
newItem.sales = null;
}
return newItem;
});
} else if (typeof importedData.data === 'string') {
// data
console.log('导入数据的data字段是字符串尝试解析');
try {
// 使JSONNaN
const nestedData = parseJSONWithNaN(importedData.data);
if (Array.isArray(nestedData)) {
console.log('嵌套数据是数组,作为历史数据使用');
processedData.data.history_data = nestedData;
} else if (nestedData.history_data) {
console.log('嵌套数据包含history_data字段');
processedData.data.history_data = nestedData.history_data;
}
} catch (error) {
console.error('解析嵌套数据失败:', error);
}
}
//
if (importedData.data && importedData.data.prediction_data && Array.isArray(importedData.data.prediction_data)) {
console.log('从导入数据中提取预测数据,长度:', importedData.data.prediction_data.length);
processedData.data.prediction_data = importedData.data.prediction_data.map(item => {
const newItem = {...item};
// NaN
if (newItem.predicted_sales !== undefined && (isNaN(newItem.predicted_sales) || newItem.predicted_sales === 'NaN' || newItem.predicted_sales === null)) {
newItem.predicted_sales = null;
}
if (newItem.sales !== undefined && (isNaN(newItem.sales) || newItem.sales === 'NaN' || newItem.sales === null)) {
newItem.sales = null;
}
return newItem;
});
}
//
if (processedData.data.history_data.length > 0 && processedData.data.prediction_data.length === 0) {
console.log('生成预测数据');
// 7
const lastWeekData = [...processedData.data.history_data]
.slice(-7)
.map(item => ({
...item,
data_type: '预测销量',
predicted_sales: item.sales,
// 7
date: new Date(new Date(item.date).getTime() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
}));
processedData.data.prediction_data = lastWeekData;
}
console.log('处理后的数据:', processedData);
//
if (processedData.data.prediction_data.length === 0 && processedData.data.history_data.length === 0) {
console.warn('导入的数据中未找到有效的预测或历史数据');
ElMessage.warning('导入的数据中未找到有效的预测或历史数据');
return;
}
//
currentPrediction.value = processedData;
//
nextTick(() => {
// initDetailsChart();
});
ElMessage.success('已使用导入的数据更新图表');
} catch (error) {
console.error('使用导入数据失败:', error);
ElMessage.error(`使用导入数据失败: ${error.message}`);
}
};
// JSONNaN
const parseJSONWithNaN = (jsonString) => {
// JSONNaNnull
const processedString = jsonString
.replace(/:NaN,/g, ':null,')
.replace(/:NaN}/g, ':null}');
return JSON.parse(processedString);
};
const calculateAverage = (data) => {
if (!data || data.length === 0) return 0;
return data.reduce((sum, item) => sum + item.sales, 0) / data.length;
@ -951,25 +496,21 @@ watch(detailsVisible, (newVal) => {
if (newVal && currentPrediction.value) {
nextTick(() => {
renderChart();
//
// renderHistoryAnalysisChart();
});
}
});
// << ProductPredictionView.vuerenderChart
// ProductPredictionView.vue
const renderChart = () => {
const chartCanvas = document.getElementById('fullscreen-prediction-chart-history');
if (!chartCanvas || !currentPrediction.value || !currentPrediction.value.data) return;
if (!chartCanvas.value || !currentPrediction.value) return;
if (predictionChart) {
predictionChart.destroy();
}
const formatDate = (date) => new Date(date).toISOString().split('T')[0];
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) }));
const historyData = (currentPrediction.value.history_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
const predictionData = (currentPrediction.value.prediction_data || []).map(p => ({ ...p, date: formatDate(p.date) }));
if (historyData.length === 0 && predictionData.length === 0) {
ElMessage.warning('没有可用于图表的数据。');
@ -977,20 +518,24 @@ const renderChart = () => {
}
const allLabels = [...new Set([...historyData.map(p => p.date), ...predictionData.map(p => p.date)])].sort();
const simplifiedLabels = allLabels.map(date => date.split('-')[2]);
const simplifiedLabels = allLabels.map(date => date.split('-').slice(1).join('/'));
const historyMap = new Map(historyData.map(p => [p.date, p.sales]));
// 使 'sales'
// 'sales'
const predictionMap = new Map(predictionData.map(p => [p.date, p.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) {
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 (lastHistoryIndex !== -1) {
alignedPredictionSales[lastHistoryIndex] = lastHistoryValue;
}
}
}
@ -1003,7 +548,7 @@ const renderChart = () => {
subtitleText += `预测数据: ${predictionData[0].date} ~ ${predictionData[predictionData.length - 1].date}`;
}
predictionChart = new Chart(chartCanvas, {
predictionChart = new Chart(chartCanvas.value, {
type: 'line',
data: {
labels: simplifiedLabels,
@ -1015,7 +560,6 @@ const renderChart = () => {
backgroundColor: 'rgba(103, 194, 58, 0.2)',
tension: 0.4,
fill: true,
spanGaps: false,
},
{
label: '预测销量',
@ -1032,10 +576,15 @@ const renderChart = () => {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: 'white'
}
},
title: {
display: true,
text: `${currentPrediction.value.data.product_name} - 销量预测趋势图`,
color: '#ffffff',
text: `${currentPrediction.value.product_name || '未知产品'} - 销量预测趋势图`,
color: 'white',
font: {
size: 20,
weight: 'bold',
@ -1044,7 +593,7 @@ const renderChart = () => {
subtitle: {
display: true,
text: subtitleText,
color: '#6c757d',
color: 'white',
font: {
size: 14,
},
@ -1057,7 +606,11 @@ const renderChart = () => {
x: {
title: {
display: true,
text: '日期 (日)'
text: '日期',
color: 'white'
},
ticks: {
color: 'white'
},
grid: {
display: false
@ -1066,10 +619,14 @@ const renderChart = () => {
y: {
title: {
display: true,
text: '销量'
text: '销量',
color: 'white'
},
ticks: {
color: 'white'
},
grid: {
color: '#e9e9e9',
color: 'rgba(255, 255, 255, 0.2)',
drawBorder: false,
},
beginAtZero: true
@ -1081,7 +638,7 @@ const renderChart = () => {
const exportHistoryData = () => {
if (!currentPrediction.value) return;
const data = currentPrediction.value.data.data;
const data = [...(currentPrediction.value.history_data || []).map(d => ({...d, data_type: '历史销量'})), ...(currentPrediction.value.prediction_data || []).map(d => ({...d, data_type: '预测销量'}))];
let csvContent = "日期,销量,数据类型\n";
data.forEach(row => {
csvContent += `${new Date(row.date).toLocaleDateString()},${row.sales.toFixed(2)},${row.data_type}\n`;
@ -1090,7 +647,7 @@ const exportHistoryData = () => {
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();
@ -1192,4 +749,4 @@ onMounted(() => {
font-size: 14px;
margin-bottom: 5px;
}
</style>
</style>

123
fix_old_predictions.py Normal file
View File

@ -0,0 +1,123 @@
import sys
import os
import json
import sqlite3
import traceback
# 确保脚本可以找到项目模块
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, 'server'))
from predictors.model_predictor import load_model_and_predict
from init_multi_store_db import get_db_connection
from utils.model_manager import ModelManager
from api import CustomJSONEncoder
def fix_old_prediction_data():
"""
遍历数据库中的历史预测记录重新生成并覆盖被截断的预测数据文件
"""
print("开始修复历史预测数据...")
conn = get_db_connection()
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM prediction_history ORDER BY id")
records = cursor.fetchall()
if not records:
print("✅ 数据库中没有历史预测记录,无需修复。")
return
print(f"发现 {len(records)} 条历史记录,开始逐一检查...")
model_manager = ModelManager(os.path.join(current_dir, 'saved_models'))
fixed_count = 0
skipped_count = 0
error_count = 0
for record in records:
record_id = record['id']
file_path = record['file_path']
future_days_db = record['future_days']
try:
if not file_path or not os.path.exists(file_path):
print(f"跳过记录 {record_id}: 文件不存在 at {file_path}")
skipped_count += 1
continue
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 兼容旧数据格式
prediction_data = data.get('prediction_data', data.get('predictions', []))
if len(prediction_data) < future_days_db:
print(f"记录 {record_id} 需要修复 (文件: {len(prediction_data)}天, 数据库: {future_days_db}天). 开始重新生成...")
# 从 model_id 解析所需信息
model_id_parts = record['model_id'].split('_')
version = model_id_parts[-1]
# 使用 list_models 查找对应的模型文件
models_result = model_manager.list_models(
product_id=record['product_id'],
model_type=record['model_type']
)
models = models_result.get('models', [])
found_model = None
for model in models:
if model.get('version') == version:
found_model = model
break
if not found_model:
print(f"❌ 无法找到用于修复的模型文件: product={record['product_id']}, type={record['model_type']}, version={version}")
error_count += 1
continue
model_path = found_model['file_path']
# 重新生成预测
new_prediction_result = load_model_and_predict(
model_path=model_path,
product_id=record['product_id'],
model_type=record['model_type'],
version=version,
future_days=future_days_db,
start_date=record['start_date'],
history_lookback_days=30 # 使用旧的默认值
)
if new_prediction_result:
# 覆盖旧的JSON文件
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(new_prediction_result, f, ensure_ascii=False, indent=4, cls=CustomJSONEncoder)
print(f"成功修复并覆盖文件: {file_path}")
fixed_count += 1
else:
print(f"修复记录 {record_id} 失败: 预测函数返回空结果。")
error_count += 1
else:
skipped_count += 1
except Exception as e:
print(f"处理记录 {record_id} 时发生错误: {e}")
traceback.print_exc()
error_count += 1
print("\n--- 修复完成 ---")
print(f"总记录数: {len(records)}")
print(f"已修复: {fixed_count}")
print(f"已跳过 (无需修复): {skipped_count}")
print(f"失败: {error_count}")
finally:
conn.close()
if __name__ == '__main__':
fix_old_prediction_data()

Binary file not shown.

View File

@ -1370,10 +1370,10 @@ def predict():
data = request.json
model_type = data.get('model_type')
version = data.get('version')
future_days = int(data.get('future_days', 7))
future_days = int(data['future_days'])
start_date = data.get('start_date', '')
include_visualization = data.get('include_visualization', False)
history_lookback_days = int(data.get('history_lookback_days', 30)) # 新增参数
history_lookback_days = int(data['history_lookback_days'])
# 确定训练模式和标识符
training_mode = data.get('training_mode', 'product')
@ -3096,18 +3096,6 @@ def save_prediction_result(prediction_result, product_id, product_name, model_ty
# 确保目录存在
os.makedirs('static/predictions', exist_ok=True)
# 限制数据量
if 'history_data' in prediction_result and isinstance(prediction_result['history_data'], list):
history_data = prediction_result['history_data']
if len(history_data) > 30:
print(f"保存时历史数据超过30天进行裁剪原始数量: {len(history_data)}")
prediction_result['history_data'] = history_data[-30:] # 只保留最近30天
if 'prediction_data' in prediction_result and isinstance(prediction_result['prediction_data'], list):
prediction_data = prediction_result['prediction_data']
if len(prediction_data) > 7:
print(f"保存时预测数据超过7天进行裁剪原始数量: {len(prediction_data)}")
prediction_result['prediction_data'] = prediction_data[:7] # 只保留前7天
# 处理预测结果中可能存在的NumPy类型
def convert_numpy_types(obj):