Compare commits

...

2 Commits

Author SHA1 Message Date
b1b697117b **日期**: 2025-07-14
**主题**: UI导航栏重构

### 描述
根据用户请求,对左侧功能导航栏进行了调整。

### 主要改动
1.  **删除“数据管理”**:
    *   从 `UI/src/App.vue` 的导航菜单中移除了“数据管理”项。
    *   从 `UI/src/router/index.js` 中删除了对应的 `/data` 路由。
    *   删除了视图文件 `UI/src/views/DataView.vue`。

2.  **提升“店铺管理”**:
    *   将“店铺管理”菜单项在 `UI/src/App.vue` 中的位置提升,以填补原“数据管理”的位置,使其在导航中更加突出。

### 涉及文件
*   `UI/src/App.vue`
*   `UI/src/router/index.js`
*   `UI/src/views/DataView.vue` (已删除)

**按药品模型预测**
---
**日期**: 2025-07-14
**主题**: 修复导航菜单高亮问题

### 描述
修复了首次进入或刷新页面时,左侧导航菜单项与当前路由不匹配导致不高亮的问题。

### 主要改动
*   **文件**: `UI/src/App.vue`
*   **修改**:
    1.  引入 `useRoute` 和 `computed`。
    2.  创建了一个计算属性 `activeMenu`,其值动态地等于当前路由的路径 (`route.path`)。
    3.  将 `el-menu` 组件的 `:default-active` 属性绑定到 `activeMenu`。

### 结果
确保了导航菜单的高亮状态始终与当前页面的URL保持同步。

---
**日期**: 2025-07-15
**主题**: 修复硬编码文件路径问题,提高项目可移植性

### 问题描述
项目在从一台计算机迁移到另一台时,由于数据文件路径被硬编码在代码中,导致程序无法找到数据文件而运行失败。

### 根本原因
多个Python文件(`predictor.py`, `multi_store_data_utils.py`)中直接写入了相对路径 `'data/timeseries_training_data_sample_10s50p.parquet'` 作为默认值。这种方式在不同运行环境下(如从根目录运行 vs 从子目录运行)会产生路径解析错误。

### 解决方案:集中配置,统一管理
1.  **修改 `server/core/config.py` (核心)**:
    *   动态计算并定义了一个全局变量 `PROJECT_ROOT`,它始终指向项目的根目录。
    *   基于 `PROJECT_ROOT`,使用 `os.path.join` 创建了一个跨平台的、绝对的默认数据路径 `DEFAULT_DATA_PATH` 和模型保存路径 `DEFAULT_MODEL_DIR`。
    *   这确保了无论从哪个位置执行代码,路径总能被正确解析。

2.  **修改 `server/utils/multi_store_data_utils.py`**:
    *   从 `server/core/config` 导入 `DEFAULT_DATA_PATH`。
    *   将所有数据加载函数的 `file_path` 参数的默认值从硬编码的字符串改为 `None`。
    *   在函数内部,如果 `file_path` 为 `None`,则自动使用导入的 `DEFAULT_DATA_PATH`。
    *   移除了原有的、复杂的、为了猜测正确路径而编写的冗余代码。

3.  **修改 `server/core/predictor.py`**:
    *   同样从 `server/core/config` 导入 `DEFAULT_DATA_PATH`。
    *   在初始化 `PharmacyPredictor` 时,如果未提供数据路径,则使用导入的 `DEFAULT_DATA_PATH` 作为默认值。

### 最终结果
通过将数据源路径集中到唯一的配置文件中进行管理,彻底解决了因硬编码路径导致的可移植性问题。项目现在可以在任何环境下可靠地运行。

---
### 未来如何修改数据源(例如,连接到服务器数据库)

本次重构为将来更换数据源打下了坚实的基础。操作非常简单:

1.  **定位配置文件**: 打开 `server/core/config.py` 文件。

2.  **修改数据源定义**:
    *   **当前 (文件)**:
        ```python
        DEFAULT_DATA_PATH = os.path.join(PROJECT_ROOT, 'data', 'timeseries_training_data_sample_10s50p.parquet')
        ```
    *   **未来 (数据库示例)**:
        您可以将这行替换为数据库连接字符串,或者添加新的数据库配置变量。例如:
        ```python
        # 注释掉或删除旧的文件路径配置
        # DEFAULT_DATA_PATH = ...

        # 新增数据库连接配置
        DATABASE_URL = "postgresql://user:password@your_server_ip:5432/your_database_name"
        ```

3.  **修改数据加载逻辑**:
    *   **定位数据加载函数**: 打开 `server/utils/multi_store_data_utils.py`。
    *   **修改 `load_multi_store_data` 函数**:
        *   引入数据库连接库(如 `sqlalchemy` 或 `psycopg2`)。
        *   修改函数逻辑,使其使用 `config.py` 中的 `DATABASE_URL` 来连接数据库,并执行SQL查询来获取数据,而不是读取文件。
        *   **示例**:
            ```python
            from sqlalchemy import create_engine
            from core.config import DATABASE_URL # 导入新的数据库配置

            def load_multi_store_data(...):
                # ...
                engine = create_engine(DATABASE_URL)
                query = "SELECT * FROM sales_data" # 根据需要构建查询
                df = pd.read_sql(query, engine)
                # ... 后续处理逻辑保持不变 ...
            ```
2025-07-15 10:37:33 +08:00
cfb50d0573 ### 主要改动
1.  **删除“数据管理”**:
    *   从 `UI/src/App.vue` 的导航菜单中移除了“数据管理”项。
    *   从 `UI/src/router/index.js` 中删除了对应的 `/data` 路由。
    *   删除了视图文件 `UI/src/views/DataView.vue`。

2.  **提升“店铺管理”**:
    *   将“店铺管理”菜单项在 `UI/src/App.vue` 中的位置提升,以填补原“数据管理”的位置,使其在导航中更加突出。

### 涉及文件
*   `UI/src/App.vue`
*   `UI/src/router/index.js`
*   `UI/src/views/DataView.vue` (已删除)
2025-07-14 20:00:05 +08:00
7 changed files with 170 additions and 521 deletions

View File

@ -17,8 +17,9 @@
<el-scrollbar>
<el-menu
:default-openeds="['1']"
router
:default-active="activeMenu"
:default-openeds="['1']"
router
class="futuristic-menu"
background-color="transparent"
text-color="#e0e6ff"
@ -31,8 +32,8 @@
<el-menu-item index="/">
<el-icon><House /></el-icon>首页概览
</el-menu-item>
<el-menu-item index="/data">
<el-icon><FolderOpened /></el-icon>数据管理
<el-menu-item index="/store-management">
<el-icon><Shop /></el-icon>店铺管理
</el-menu-item>
<el-sub-menu index="training-submenu">
<template #title>
@ -70,9 +71,6 @@
<el-menu-item index="/management">
<el-icon><Files /></el-icon>模型管理
</el-menu-item>
<el-menu-item index="/store-management">
<el-icon><Shop /></el-icon>店铺管理
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
@ -112,7 +110,12 @@
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { DataAnalysis, Refresh, DataLine, House, FolderOpened, Cpu, MagicStick, Files, Histogram, Coin, Shop, Operation } from '@element-plus/icons-vue'
const route = useRoute()
const activeMenu = computed(() => route.path)
</script>
<style>

View File

@ -9,11 +9,6 @@ const router = createRouter({
name: 'dashboard',
component: DashboardView
},
{
path: '/data',
name: 'data',
component: () => import('../views/DataView.vue')
},
{
path: '/training',
name: 'training',

View File

@ -1,461 +0,0 @@
<template>
<el-card>
<template #header>
<div class="card-header">
<span>销售数据管理</span>
<el-upload
:show-file-list="false"
:http-request="handleUpload"
>
<el-button type="primary">上传销售数据</el-button>
</el-upload>
</div>
</template>
<!-- 查询过滤条件 -->
<div class="filter-section">
<el-row :gutter="20">
<el-col :span="6">
<el-select v-model="filters.store_id" placeholder="选择店铺" clearable @change="handleFilterChange">
<el-option label="全部店铺" value=""></el-option>
<el-option
v-for="store in stores"
:key="store.store_id"
:label="store.store_name"
:value="store.store_id">
</el-option>
</el-select>
</el-col>
<el-col :span="6">
<el-select v-model="filters.product_id" placeholder="选择产品" clearable @change="handleFilterChange">
<el-option label="全部产品" value=""></el-option>
<el-option
v-for="product in allProducts"
:key="product.product_id"
:label="product.product_name"
:value="product.product_id">
</el-option>
</el-select>
</el-col>
<el-col :span="8">
<el-date-picker
v-model="filters.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleFilterChange"
/>
</el-col>
<el-col :span="4">
<el-button type="primary" @click="handleFilterChange">查询</el-button>
</el-col>
</el-row>
</div>
<!-- 销售数据表格 -->
<el-table :data="salesData" stripe v-loading="loading" class="mt-4">
<el-table-column prop="date" label="日期" width="120"></el-table-column>
<el-table-column prop="store_name" label="店铺名称" width="150"></el-table-column>
<el-table-column prop="store_id" label="店铺ID" width="100"></el-table-column>
<el-table-column prop="product_name" label="产品名称" width="150"></el-table-column>
<el-table-column prop="product_id" label="产品ID" width="100"></el-table-column>
<el-table-column prop="quantity_sold" label="销量" width="80" align="right"></el-table-column>
<el-table-column prop="unit_price" label="单价" width="80" align="right">
<template #default="{ row }">
¥{{ row.unit_price?.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="sales_amount" label="销售额" width="100" align="right">
<template #default="{ row }">
¥{{ row.sales_amount?.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="store_type" label="店铺类型" width="100"></el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button link @click="viewStoreDetails(row.store_id)">店铺详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="total > 0"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@current-change="handlePageChange"
@size-change="handleSizeChange"
class="mt-4"
/>
<!-- 统计信息 -->
<div class="statistics-section mt-4" v-if="statistics">
<el-row :gutter="20">
<el-col :span="6">
<el-statistic title="总记录数" :value="statistics.total_records" />
</el-col>
<el-col :span="6">
<el-statistic title="总销售额" :value="statistics.total_sales_amount" :precision="2" prefix="¥" />
</el-col>
<el-col :span="6">
<el-statistic title="总销量" :value="statistics.total_quantity" />
</el-col>
<el-col :span="6">
<el-statistic title="店铺数量" :value="statistics.stores" />
</el-col>
</el-row>
</div>
<!-- 产品详情对话框 -->
<el-dialog
v-model="dialogVisible"
:title="`${selectedProduct?.product_name} - 销售详情`"
width="60%"
>
<div v-loading="detailLoading">
<div v-if="salesData.length > 0">
<div class="chart-container">
<canvas ref="salesChartCanvas"></canvas>
</div>
<el-table :data="paginatedSalesData" stripe>
<el-table-column prop="date" label="日期"></el-table-column>
<el-table-column prop="sales" label="销量"></el-table-column>
<el-table-column prop="price" label="价格"></el-table-column>
</el-table>
<el-pagination
layout="prev, pager, next"
:total="salesData.length"
:page-size="pageSize"
@current-change="handlePageChange"
class="mt-4"
/>
</div>
<el-empty v-else description="暂无销售数据"></el-empty>
</div>
</el-dialog>
</el-card>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import Chart from 'chart.js/auto';
import zoomPlugin from 'chartjs-plugin-zoom';
Chart.register(zoomPlugin);
//
const stores = ref([])
const allProducts = ref([])
const salesData = ref([])
const statistics = ref(null)
const loading = ref(true)
//
const pageSize = ref(20)
const currentPage = ref(1)
const total = ref(0)
//
const filters = ref({
store_id: '',
product_id: '',
dateRange: null
})
//
const dialogVisible = ref(false)
const detailLoading = ref(false)
const selectedProduct = ref(null)
const paginatedSalesData = ref([])
const salesChartCanvas = ref(null)
let salesChart = null;
//
const fetchStores = async () => {
try {
const response = await axios.get('/api/stores')
if (response.data.status === 'success') {
stores.value = response.data.data
} else {
ElMessage.error('获取店铺列表失败')
}
} catch (error) {
console.error('获取店铺列表失败:', error)
}
}
//
const fetchProducts = async () => {
try {
const response = await axios.get('/api/products')
if (response.data.status === 'success') {
allProducts.value = response.data.data
} else {
ElMessage.error('获取产品列表失败')
}
} catch (error) {
console.error('获取产品列表失败:', error)
}
}
//
const fetchSalesData = async () => {
try {
loading.value = true
//
const params = {
page: currentPage.value,
page_size: pageSize.value
}
if (filters.value.store_id) {
params.store_id = filters.value.store_id
}
if (filters.value.product_id) {
params.product_id = filters.value.product_id
}
if (filters.value.dateRange && filters.value.dateRange.length === 2) {
params.start_date = filters.value.dateRange[0]
params.end_date = filters.value.dateRange[1]
}
const response = await axios.get('/api/sales/data', { params })
if (response.data.status === 'success') {
salesData.value = response.data.data
total.value = response.data.total || 0
statistics.value = response.data.statistics
} else {
ElMessage.error('获取销售数据失败')
salesData.value = []
total.value = 0
statistics.value = null
}
} catch (error) {
ElMessage.error('请求销售数据时出错')
console.error(error)
salesData.value = []
total.value = 0
statistics.value = null
} finally {
loading.value = false
}
}
//
const handleFilterChange = () => {
currentPage.value = 1
fetchSalesData()
}
//
const handlePageChange = (page) => {
currentPage.value = page
fetchSalesData()
}
//
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchSalesData()
}
//
const viewStoreDetails = async (storeId) => {
try {
const response = await axios.get(`/api/stores/${storeId}`)
if (response.data.status === 'success') {
const store = response.data.data
ElMessage.info(`店铺:${store.store_name},位置:${store.location},类型:${store.type}`)
}
} catch (error) {
ElMessage.error('获取店铺详情失败')
}
}
//
const handleUpload = async (options) => {
const formData = new FormData()
formData.append('file', options.file)
try {
const response = await axios.post('/api/data/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.status === 'success') {
ElMessage.success('数据上传成功')
await fetchStores()
await fetchProducts()
await fetchSalesData()
} else {
ElMessage.error(response.data.message || '数据上传失败')
}
} catch (error) {
ElMessage.error('数据上传请求失败')
console.error(error)
}
}
const viewDetails = async (product) => {
selectedProduct.value = product;
dialogVisible.value = true;
detailLoading.value = true;
try {
const response = await axios.get(`/api/products/${product.product_id}/sales`);
if (response.data.status === 'success') {
salesData.value = response.data.data;
handlePageChange(1); // Show first page
await nextTick();
renderChart();
} else {
ElMessage.error('获取销售详情失败');
salesData.value = [];
}
} catch (error) {
ElMessage.error('请求销售详情时出错');
salesData.value = [];
} finally {
detailLoading.value = false;
}
}
// handlePageChange
const renderChart = () => {
if (salesChart) {
salesChart.destroy();
}
if (!salesChartCanvas.value || salesData.value.length === 0) return;
const labels = salesData.value.map(d => d.date);
const data = salesData.value.map(d => d.sales);
salesChart = new Chart(salesChartCanvas.value, {
type: 'line',
data: {
labels,
datasets: [{
label: '每日销量',
data,
borderColor: '#409EFF',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
title: function(context) {
return `日期: ${context[0].label}`;
},
label: (context) => {
const label = context.dataset.label || '';
const value = context.parsed.y;
const fullData = salesData.value[context.dataIndex];
let tooltipText = `${label}: ${value}`;
if (fullData) {
tooltipText += ` | 温度: ${fullData.temperature}°C`;
}
return tooltipText;
}
}
},
zoom: {
pan: {
enabled: true,
mode: 'x',
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
}
}
}
}
});
}
//
onMounted(async () => {
await fetchStores()
await fetchProducts()
await fetchSalesData()
})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-section {
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.statistics-section {
padding: 20px;
background-color: #f0f9ff;
border-radius: 8px;
border: 1px solid #e0f2fe;
}
.mt-4 {
margin-top: 24px;
}
.chart-container {
width: 100%;
height: 400px;
margin-bottom: 20px;
}
.el-statistic {
text-align: center;
}
.el-table .el-table__cell {
padding: 8px 0;
}
.filter-section .el-row {
align-items: center;
}
.filter-section .el-col {
margin-bottom: 10px;
}
@media (max-width: 768px) {
.filter-section .el-col {
margin-bottom: 15px;
}
.statistics-section .el-col {
margin-bottom: 15px;
}
}
</style>

View File

@ -10,6 +10,13 @@ import os
import re
import glob
# 项目根目录
# __file__ 是当前文件 (config.py) 的路径
# os.path.dirname(__file__) 是 server/core
# os.path.join(..., '..') 是 server
# os.path.join(..., '..', '..') 是项目根目录
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
# 解决画图中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
@ -26,8 +33,9 @@ def get_device():
DEVICE = get_device()
# 数据相关配置
DEFAULT_DATA_PATH = 'pharmacy_sales.xlsx'
DEFAULT_MODEL_DIR = 'saved_models'
# 使用 os.path.join 构造跨平台的路径
DEFAULT_DATA_PATH = os.path.join(PROJECT_ROOT, 'data', 'timeseries_training_data_sample_10s50p.parquet')
DEFAULT_MODEL_DIR = os.path.join(PROJECT_ROOT, 'saved_models')
DEFAULT_FEATURES = ['sales', 'price', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature']
# 时间序列参数

View File

@ -41,7 +41,7 @@ class PharmacyPredictor:
"""
# 设置默认数据路径为多店铺CSV文件
if data_path is None:
data_path = 'data/timeseries_training_data_sample_10s50p.parquet'
data_path = DEFAULT_DATA_PATH
self.data_path = data_path
self.model_dir = model_dir

View File

@ -8,8 +8,9 @@ import numpy as np
import os
from datetime import datetime, timedelta
from typing import Optional, List, Tuple, Dict, Any
from core.config import DEFAULT_DATA_PATH
def load_multi_store_data(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet',
def load_multi_store_data(file_path: str = None,
store_id: Optional[str] = None,
product_id: Optional[str] = None,
start_date: Optional[str] = None,
@ -18,7 +19,7 @@ def load_multi_store_data(file_path: str = 'data/timeseries_training_data_sample
加载多店铺销售数据支持按店铺产品时间范围过滤
参数:
file_path: 数据文件路径 (支持 .csv, .xlsx, .parquet)
file_path: 数据文件路径 (支持 .csv, .xlsx, .parquet)如果为None则使用config中定义的默认路径
store_id: 店铺ID为None时返回所有店铺数据
product_id: 产品ID为None时返回所有产品数据
start_date: 开始日期 (YYYY-MM-DD)
@ -28,43 +29,27 @@ def load_multi_store_data(file_path: str = 'data/timeseries_training_data_sample
DataFrame: 过滤后的销售数据
"""
# 尝试多个可能的文件路径
# 获取当前脚本所在的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 假设项目根目录是 server/utils 的上两级目录
project_root = os.path.abspath(os.path.join(current_dir, '..', '..'))
possible_paths = [
file_path, # 相对路径 (如果从根目录运行)
os.path.join(project_root, file_path), # 基于项目根目录的绝对路径
os.path.join('..', file_path), # 相对路径 (如果从 server 目录运行)
os.path.join('server', file_path) # 相对路径 (如果从根目录运行,但路径错误)
]
df = None
loaded_path = None
for path in possible_paths:
try:
if not os.path.exists(path):
continue
# 如果未提供文件路径,则使用配置文件中的默认路径
if file_path is None:
file_path = DEFAULT_DATA_PATH
if path.endswith('.csv'):
df = pd.read_csv(path)
elif path.endswith('.xlsx'):
df = pd.read_excel(path)
elif path.endswith('.parquet'):
df = pd.read_parquet(path)
if df is not None:
loaded_path = path
print(f"成功加载数据文件: {loaded_path}")
break
except Exception as e:
print(f"加载文件 {path} 失败: {e}")
continue
if df is None:
raise FileNotFoundError(f"无法找到或加载数据文件,尝试的路径: {possible_paths}")
if not os.path.exists(file_path):
raise FileNotFoundError(f"数据文件不存在: {file_path}")
try:
if file_path.endswith('.csv'):
df = pd.read_csv(file_path)
elif file_path.endswith('.xlsx'):
df = pd.read_excel(file_path)
elif file_path.endswith('.parquet'):
df = pd.read_parquet(file_path)
else:
raise ValueError(f"不支持的文件格式: {file_path}")
print(f"成功加载数据文件: {file_path}")
except Exception as e:
print(f"加载文件 {file_path} 失败: {e}")
raise
# 按店铺过滤
if store_id:
@ -192,7 +177,7 @@ def standardize_column_names(df: pd.DataFrame) -> pd.DataFrame:
return df
def get_available_stores(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet') -> List[Dict[str, Any]]:
def get_available_stores(file_path: str = None) -> List[Dict[str, Any]]:
"""
获取可用的店铺列表
@ -229,7 +214,7 @@ def get_available_stores(file_path: str = 'data/timeseries_training_data_sample_
print(f"获取店铺列表失败: {e}")
return []
def get_available_products(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet',
def get_available_products(file_path: str = None,
store_id: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取可用的产品列表
@ -260,7 +245,7 @@ def get_available_products(file_path: str = 'data/timeseries_training_data_sampl
def get_store_product_sales_data(store_id: str,
product_id: str,
file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet') -> pd.DataFrame:
file_path: str = None) -> pd.DataFrame:
"""
获取特定店铺和产品的销售数据用于模型训练
@ -305,7 +290,7 @@ def get_store_product_sales_data(store_id: str,
def aggregate_multi_store_data(product_id: Optional[str] = None,
store_id: Optional[str] = None,
aggregation_method: str = 'sum',
file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet') -> pd.DataFrame:
file_path: str = None) -> pd.DataFrame:
"""
聚合销售数据可按产品全局或按店铺所有产品
@ -393,7 +378,7 @@ def aggregate_multi_store_data(product_id: Optional[str] = None,
# 返回只包含这些必需列的DataFrame
return aggregated_df[existing_columns]
def get_sales_statistics(file_path: str = 'data/timeseries_training_data_sample_10s50p.parquet',
def get_sales_statistics(file_path: str = None,
store_id: Optional[str] = None,
product_id: Optional[str] = None) -> Dict[str, Any]:
"""
@ -432,7 +417,7 @@ def get_sales_statistics(file_path: str = 'data/timeseries_training_data_sample_
return {'error': str(e)}
# 向后兼容的函数
def load_data(file_path='pharmacy_sales.xlsx', store_id=None):
def load_data(file_path=None, store_id=None):
"""
向后兼容的数据加载函数
"""

View File

@ -1,3 +1,11 @@
### 根目录启动
`uv pip install loguru numpy pandas torch matplotlib flask flask_cors flask_socketio flasgger scikit-learn tqdm pytorch_tcn`
### UI
`npm install` `npm run dev`
# “预测分析”模块UI重构修改记录
**任务目标**: 将原有的、通过下拉菜单切换模式的单一预测页面重构为通过左侧子导航切换模式的多页面布局使其UI结构与“模型训练”模块保持一致。
@ -636,4 +644,115 @@
**按药品模型预测**
---
**日期**: 2025-07-14
**主题**: UI导航栏重构
### 描述
根据用户请求,对左侧功能导航栏进行了调整。
### 主要改动
1. **删除“数据管理”**:
* 从 `UI/src/App.vue` 的导航菜单中移除了“数据管理”项。
* 从 `UI/src/router/index.js` 中删除了对应的 `/data` 路由。
* 删除了视图文件 `UI/src/views/DataView.vue`
2. **提升“店铺管理”**:
* 将“店铺管理”菜单项在 `UI/src/App.vue` 中的位置提升,以填补原“数据管理”的位置,使其在导航中更加突出。
### 涉及文件
* `UI/src/App.vue`
* `UI/src/router/index.js`
* `UI/src/views/DataView.vue` (已删除)
**按药品模型预测**
---
**日期**: 2025-07-14
**主题**: 修复导航菜单高亮问题
### 描述
修复了首次进入或刷新页面时,左侧导航菜单项与当前路由不匹配导致不高亮的问题。
### 主要改动
* **文件**: `UI/src/App.vue`
* **修改**:
1. 引入 `useRoute``computed`
2. 创建了一个计算属性 `activeMenu`,其值动态地等于当前路由的路径 (`route.path`)。
3. 将 `el-menu` 组件的 `:default-active` 属性绑定到 `activeMenu`
### 结果
确保了导航菜单的高亮状态始终与当前页面的URL保持同步。
---
**日期**: 2025-07-15
**主题**: 修复硬编码文件路径问题,提高项目可移植性
### 问题描述
项目在从一台计算机迁移到另一台时,由于数据文件路径被硬编码在代码中,导致程序无法找到数据文件而运行失败。
### 根本原因
多个Python文件`predictor.py`, `multi_store_data_utils.py`)中直接写入了相对路径 `'data/timeseries_training_data_sample_10s50p.parquet'` 作为默认值。这种方式在不同运行环境下(如从根目录运行 vs 从子目录运行)会产生路径解析错误。
### 解决方案:集中配置,统一管理
1. **修改 `server/core/config.py` (核心)**:
* 动态计算并定义了一个全局变量 `PROJECT_ROOT`,它始终指向项目的根目录。
* 基于 `PROJECT_ROOT`,使用 `os.path.join` 创建了一个跨平台的、绝对的默认数据路径 `DEFAULT_DATA_PATH` 和模型保存路径 `DEFAULT_MODEL_DIR`
* 这确保了无论从哪个位置执行代码,路径总能被正确解析。
2. **修改 `server/utils/multi_store_data_utils.py`**:
* 从 `server/core/config` 导入 `DEFAULT_DATA_PATH`
* 将所有数据加载函数的 `file_path` 参数的默认值从硬编码的字符串改为 `None`
* 在函数内部,如果 `file_path``None`,则自动使用导入的 `DEFAULT_DATA_PATH`
* 移除了原有的、复杂的、为了猜测正确路径而编写的冗余代码。
3. **修改 `server/core/predictor.py`**:
* 同样从 `server/core/config` 导入 `DEFAULT_DATA_PATH`
* 在初始化 `PharmacyPredictor` 时,如果未提供数据路径,则使用导入的 `DEFAULT_DATA_PATH` 作为默认值。
### 最终结果
通过将数据源路径集中到唯一的配置文件中进行管理,彻底解决了因硬编码路径导致的可移植性问题。项目现在可以在任何环境下可靠地运行。
---
### 未来如何修改数据源(例如,连接到服务器数据库)
本次重构为将来更换数据源打下了坚实的基础。操作非常简单:
1. **定位配置文件**: 打开 `server/core/config.py` 文件。
2. **修改数据源定义**:
* **当前 (文件)**:
```python
DEFAULT_DATA_PATH = os.path.join(PROJECT_ROOT, 'data', 'timeseries_training_data_sample_10s50p.parquet')
```
* **未来 (数据库示例)**:
您可以将这行替换为数据库连接字符串,或者添加新的数据库配置变量。例如:
```python
# 注释掉或删除旧的文件路径配置
# DEFAULT_DATA_PATH = ...
# 新增数据库连接配置
DATABASE_URL = "postgresql://user:password@your_server_ip:5432/your_database_name"
```
3. **修改数据加载逻辑**:
* **定位数据加载函数**: 打开 `server/utils/multi_store_data_utils.py`
* **修改 `load_multi_store_data` 函数**:
* 引入数据库连接库(如 `sqlalchemy``psycopg2`)。
* 修改函数逻辑,使其使用 `config.py` 中的 `DATABASE_URL` 来连接数据库并执行SQL查询来获取数据而不是读取文件。
* **示例**:
```python
from sqlalchemy import create_engine
from core.config import DATABASE_URL # 导入新的数据库配置
def load_multi_store_data(...):
# ...
engine = create_engine(DATABASE_URL)
query = "SELECT * FROM sales_data" # 根据需要构建查询
df = pd.read_sql(query, engine)
# ... 后续处理逻辑保持不变 ...
```
通过以上步骤,您就可以在不改动项目其他任何部分的情况下,轻松地将数据源从本地文件切换到服务器数据库。