**日期**: 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)
                # ... 后续处理逻辑保持不变 ...
            ```
This commit is contained in:
xz2000 2025-07-15 10:37:25 +08:00
parent cfb50d0573
commit b1b697117b
5 changed files with 144 additions and 50 deletions

View File

@ -17,8 +17,9 @@
<el-scrollbar> <el-scrollbar>
<el-menu <el-menu
:default-openeds="['1']" :default-active="activeMenu"
router :default-openeds="['1']"
router
class="futuristic-menu" class="futuristic-menu"
background-color="transparent" background-color="transparent"
text-color="#e0e6ff" text-color="#e0e6ff"
@ -109,7 +110,12 @@
</template> </template>
<script setup> <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' 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> </script>
<style> <style>

View File

@ -10,6 +10,13 @@ import os
import re import re
import glob 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['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False plt.rcParams['axes.unicode_minus'] = False
@ -26,8 +33,9 @@ def get_device():
DEVICE = get_device() DEVICE = get_device()
# 数据相关配置 # 数据相关配置
DEFAULT_DATA_PATH = 'pharmacy_sales.xlsx' # 使用 os.path.join 构造跨平台的路径
DEFAULT_MODEL_DIR = 'saved_models' 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'] DEFAULT_FEATURES = ['sales', 'price', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature']
# 时间序列参数 # 时间序列参数

View File

@ -41,7 +41,7 @@ class PharmacyPredictor:
""" """
# 设置默认数据路径为多店铺CSV文件 # 设置默认数据路径为多店铺CSV文件
if data_path is None: 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.data_path = data_path
self.model_dir = model_dir self.model_dir = model_dir

View File

@ -8,8 +8,9 @@ import numpy as np
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, List, Tuple, Dict, Any 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, store_id: Optional[str] = None,
product_id: Optional[str] = None, product_id: Optional[str] = None,
start_date: 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时返回所有店铺数据 store_id: 店铺ID为None时返回所有店铺数据
product_id: 产品ID为None时返回所有产品数据 product_id: 产品ID为None时返回所有产品数据
start_date: 开始日期 (YYYY-MM-DD) start_date: 开始日期 (YYYY-MM-DD)
@ -28,43 +29,27 @@ def load_multi_store_data(file_path: str = 'data/timeseries_training_data_sample
DataFrame: 过滤后的销售数据 DataFrame: 过滤后的销售数据
""" """
# 尝试多个可能的文件路径 # 如果未提供文件路径,则使用配置文件中的默认路径
# 获取当前脚本所在的目录 if file_path is None:
current_dir = os.path.dirname(os.path.abspath(__file__)) file_path = DEFAULT_DATA_PATH
# 假设项目根目录是 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 path.endswith('.csv'): if not os.path.exists(file_path):
df = pd.read_csv(path) raise FileNotFoundError(f"数据文件不存在: {file_path}")
elif path.endswith('.xlsx'):
df = pd.read_excel(path) try:
elif path.endswith('.parquet'): if file_path.endswith('.csv'):
df = pd.read_parquet(path) df = pd.read_csv(file_path)
elif file_path.endswith('.xlsx'):
if df is not None: df = pd.read_excel(file_path)
loaded_path = path elif file_path.endswith('.parquet'):
print(f"成功加载数据文件: {loaded_path}") df = pd.read_parquet(file_path)
break else:
except Exception as e: raise ValueError(f"不支持的文件格式: {file_path}")
print(f"加载文件 {path} 失败: {e}")
continue print(f"成功加载数据文件: {file_path}")
except Exception as e:
if df is None: print(f"加载文件 {file_path} 失败: {e}")
raise FileNotFoundError(f"无法找到或加载数据文件,尝试的路径: {possible_paths}") raise
# 按店铺过滤 # 按店铺过滤
if store_id: if store_id:
@ -192,7 +177,7 @@ def standardize_column_names(df: pd.DataFrame) -> pd.DataFrame:
return df 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}") print(f"获取店铺列表失败: {e}")
return [] 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]]: 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, def get_store_product_sales_data(store_id: str,
product_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, def aggregate_multi_store_data(product_id: Optional[str] = None,
store_id: Optional[str] = None, store_id: Optional[str] = None,
aggregation_method: str = 'sum', 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 # 返回只包含这些必需列的DataFrame
return aggregated_df[existing_columns] 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, store_id: Optional[str] = None,
product_id: Optional[str] = None) -> Dict[str, Any]: 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)} 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重构修改记录
**任务目标**: 将原有的、通过下拉菜单切换模式的单一预测页面重构为通过左侧子导航切换模式的多页面布局使其UI结构与“模型训练”模块保持一致。 **任务目标**: 将原有的、通过下拉菜单切换模式的单一预测页面重构为通过左侧子导航切换模式的多页面布局使其UI结构与“模型训练”模块保持一致。
@ -660,4 +668,91 @@
**按药品模型预测** **按药品模型预测**
---
**日期**: 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)
# ... 后续处理逻辑保持不变 ...
```
通过以上步骤,您就可以在不改动项目其他任何部分的情况下,轻松地将数据源从本地文件切换到服务器数据库。