**日期**: 2025-07-15 14:05
**主题**: 仪表盘UI调整

### 描述
根据用户请求,将仪表盘上的“数据管理”卡片替换为“店铺管理”。

### 主要改动
*   **文件**: `UI/src/views/DashboardView.vue`
*   **修改**:
    1.  在 `featureCards` 数组中,将原“数据管理”的对象修改为“店铺管理”。
    2.  更新了卡片的 `title`, `description`, `icon` 和 `path`,使其指向店铺管理页面 (`/store-management`)。
    3.  在脚本中导入了新的 `Shop` 图标。

### 结果
仪表盘现在直接提供到“店铺管理”页面的快捷入口,提高了操作效率,调整店铺管理的样式。
This commit is contained in:
xz2000 2025-07-15 19:17:53 +08:00
parent 7a4bfedcaa
commit 9bd824c389
6 changed files with 369 additions and 88 deletions

View File

@ -237,11 +237,13 @@ body {
}
.logo-container {
padding: 20px;
padding: 10px 20px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--card-border);
height: 60px;
box-sizing: border-box;
}
.logo-icon {
@ -351,4 +353,4 @@ body {
::-webkit-scrollbar-thumb:hover {
background: rgba(93, 156, 255, 0.5);
}
</style>
</style>

View File

@ -5,6 +5,7 @@ import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import axios from 'axios'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// 导入Google Roboto字体
import '@/assets/fonts.css'
@ -22,6 +23,6 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
}
app.use(router)
app.use(ElementPlus)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')

View File

@ -90,7 +90,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ArrowRight, DataAnalysis, TrendCharts, CircleCheckFilled, WarningFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import { ArrowRight, DataAnalysis, TrendCharts, CircleCheckFilled, WarningFilled, CircleCloseFilled, Shop } from '@element-plus/icons-vue'
//
const data = ref({
@ -102,10 +102,10 @@ const data = ref({
//
const featureCards = [
{
title: '数据管理',
description: '管理产品和销售数据',
icon: 'FolderOpened',
path: '/data',
title: '店铺管理',
description: '管理店铺信息和库存',
icon: 'Shop',
path: '/store-management',
type: 'data'
},
{

View File

@ -1,6 +1,6 @@
<template>
<div class="store-management-container">
<el-card>
<el-card class="full-height-card">
<template #header>
<div class="card-header">
<span>店铺管理</span>
@ -18,67 +18,70 @@
</template>
<!-- 搜索和过滤 -->
<div class="filter-section">
<el-row :gutter="20">
<el-col :span="6">
<el-input
v-model="searchQuery"
placeholder="搜索店铺名称或ID"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="handleFilter">
<el-option label="全部状态" value="" />
<el-option label="营业中" value="active" />
<el-option label="暂停营业" value="inactive" />
</el-select>
</el-col>
<el-col :span="4">
<el-select v-model="typeFilter" placeholder="类型筛选" clearable @change="handleFilter">
<el-option label="全部类型" value="" />
<el-option label="旗舰店" value="旗舰店" />
<el-option label="标准店" value="标准店" />
<el-option label="便民店" value="便民店" />
<el-option label="社区店" value="社区店" />
</el-select>
</el-col>
</el-row>
</div>
<div class="table-container" ref="tableContainerRef">
<div class="filter-section" ref="filterSectionRef">
<el-row :gutter="20" align="middle">
<el-col :span="6">
<el-input
v-model="searchQuery"
placeholder="搜索店铺名称或ID"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="handleFilter" style="width: 100%;">
<el-option label="全部状态" value="" />
<el-option label="营业中" value="active" />
<el-option label="暂停营业" value="inactive" />
</el-select>
</el-col>
<el-col :span="4">
<el-select v-model="typeFilter" placeholder="类型筛选" clearable @change="handleFilter" style="width: 100%;">
<el-option label="全部类型" value="" />
<el-option label="旗舰店" value="旗舰店" />
<el-option label="标准店" value="标准店" />
<el-option label="便民店" value="便民店" />
<el-option label="社区店" value="社区店" />
</el-select>
</el-col>
</el-row>
</div>
<!-- 店铺列表 -->
<el-table
:data="filteredStores"
v-loading="loading"
stripe
@selection-change="handleSelectionChange"
>
<!-- 店铺列表 -->
<el-table
:data="pagedStores"
v-loading="loading"
stripe
@selection-change="handleSelectionChange"
class="store-table"
:height="tableHeight"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="store_id" label="店铺ID" width="100" />
<el-table-column prop="store_name" label="店铺名称" width="150" />
<el-table-column prop="location" label="位置" width="200" />
<el-table-column prop="type" label="类型" width="100">
<el-table-column prop="store_id" label="店铺ID" width="100" align="center" />
<el-table-column prop="store_name" label="店铺名称" width="250" align="center" show-overflow-tooltip />
<el-table-column prop="location" label="位置" width="250" align="center" show-overflow-tooltip/>
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="{ row }">
<el-tag :type="getStoreTypeTag(row.type)">
{{ row.type }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="size" label="面积(㎡)" width="100" align="right" />
<el-table-column prop="opening_date" label="开业日期" width="120" />
<el-table-column prop="status" label="状态" width="100">
<el-table-column prop="size" label="面积(㎡)" width="150" align="center"/>
<el-table-column prop="opening_date" label="开业日期" width="150" align="center"/>
<el-table-column prop="status" label="状态" width="150" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
{{ row.status === 'active' ? '营业中' : '暂停营业' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="viewStoreDetails(row)">
详情
@ -99,14 +102,16 @@
<!-- 分页 -->
<el-pagination
v-if="total > pageSize"
layout="total, sizes, prev, pager, next, jumper"
layout="total, prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:current-page="currentPage"
@current-change="handlePageChange"
@size-change="handleSizeChange"
class="pagination"
ref="paginationRef"
/>
</div>
</el-card>
<!-- 新增/编辑店铺对话框 -->
@ -115,6 +120,7 @@
:title="isEditing ? '编辑店铺' : '新增店铺'"
width="600px"
@close="resetForm"
class="form-dialog"
>
<el-form
ref="formRef"
@ -229,6 +235,7 @@
</div>
</el-dialog>
<!-- 店铺产品对话框 -->
<el-dialog
v-model="productsDialogVisible"
@ -255,7 +262,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
@ -272,9 +279,15 @@ const typeFilter = ref('')
//
const currentPage = ref(1)
const pageSize = ref(20)
const pageSize = ref(12)
const total = ref(0)
//
const tableContainerRef = ref(null);
const filterSectionRef = ref(null);
const paginationRef = ref(null);
const tableHeight = ref(400); //
//
const dialogVisible = ref(false)
const detailDialogVisible = ref(false)
@ -319,34 +332,34 @@ const rules = {
//
const filteredStores = computed(() => {
let result = stores.value
let result = stores.value;
//
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(store =>
store.store_name.toLowerCase().includes(query) ||
store.store_id.toLowerCase().includes(query)
)
const query = searchQuery.value.toLowerCase();
result = result.filter(
(store) =>
store.store_name.toLowerCase().includes(query) ||
store.store_id.toLowerCase().includes(query)
);
}
//
if (statusFilter.value) {
result = result.filter(store => store.status === statusFilter.value)
result = result.filter((store) => store.status === statusFilter.value);
}
//
if (typeFilter.value) {
result = result.filter(store => store.type === typeFilter.value)
result = result.filter((store) => store.type === typeFilter.value);
}
total.value = result.length
//
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return result.slice(start, end)
})
return result;
});
const pagedStores = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
total.value = filteredStores.value.length;
return filteredStores.value.slice(start, end);
});
//
const fetchStores = async () => {
@ -516,14 +529,49 @@ const viewStoreProducts = async (store) => {
}
//
const updateTableHeight = () => {
nextTick(() => {
if (tableContainerRef.value) {
const containerHeight = tableContainerRef.value.clientHeight;
const filterHeight = filterSectionRef.value?.offsetHeight || 0;
const paginationHeight = paginationRef.value?.$el.offsetHeight || 0;
//
const calculatedHeight = containerHeight - filterHeight - paginationHeight - 20;
tableHeight.value = calculatedHeight > 200 ? calculatedHeight : 200; //
}
});
};
onMounted(() => {
fetchStores()
})
fetchStores();
updateTableHeight();
window.addEventListener('resize', updateTableHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight);
});
</script>
<style scoped>
.store-management-container {
height: 97%;
padding: 6px 10px 15px 15px;
}
.full-height-card {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.el-card__body) {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
}
.card-header {
@ -537,21 +585,35 @@ onMounted(() => {
gap: 10px;
}
.table-container {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden; /* 确保容器本身不滚动 */
}
.filter-section {
margin-bottom: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
padding-bottom: 20px;
}
.store-table {
width: 100%;
}
:deep(.store-table .el-table__cell) {
padding: 12px 2px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
padding: 14px 0;
}
.store-detail {
padding: 10px 0;
padding: 5px 0;
}
.store-stats {
@ -579,4 +641,9 @@ onMounted(() => {
gap: 5px;
}
}
</style>
.form-dialog :deep(.el-dialog) {
background: transparent;
box-shadow: none;
}
</style>

View File

@ -783,4 +783,21 @@
* **原因**: 移除这个在新版PyTorch中已不存在的参数可以从根本上解决 `TypeError`并确保代码在不同版本的PyTorch环境中都能正常运行。此修改不影响学习率调度器的核心功能。
### 最终结果
通过移除已弃用的 `verbose` 参数,彻底解决了由于环境差异导致的版本兼容性问题,确保了项目在所有目标机器上都能稳定地执行训练任务。
通过移除已弃用的 `verbose` 参数,彻底解决了由于环境差异导致的版本兼容性问题,确保了项目在所有目标机器上都能稳定地执行训练任务。
---
**日期**: 2025-07-15 14:05
**主题**: 仪表盘UI调整
### 描述
根据用户请求,将仪表盘上的“数据管理”卡片替换为“店铺管理”。
### 主要改动
* **文件**: `UI/src/views/DashboardView.vue`
* **修改**:
1. 在 `featureCards` 数组中,将原“数据管理”的对象修改为“店铺管理”。
2. 更新了卡片的 `title`, `description`, `icon``path`,使其指向店铺管理页面 (`/store-management`)。
3. 在脚本中导入了新的 `Shop` 图标。
### 结果
仪表盘现在直接提供到“店铺管理”页面的快捷入口,提高了操作效率。

View File

@ -0,0 +1,194 @@
跟文件夹save_models
## 按药品训练 ##
1.创建 product 文件夹
2.选择药品 product下创建药品id 文件夹根据数据范围加上相应的后缀聚合所有店铺all指定店铺就店铺id
3.模型类型 对应的文件下创建模型名称的文件夹
4.在模型名称的文件夹下,版本文件夹version+第几次训练
5.在版本文件下存储对应的检查点文件,最终模型文件,损失曲线图
## 按店铺训练 ##
1.创建 store 文件夹
2.选择店铺 store下创建店铺id 文件夹根据药品范围加上相应的后缀所有药品all指定药品就药品id
3.模型类型 对应的文件下创建模型名称的文件夹
4.在模型名称的文件夹下,版本文件夹version+第几次训练
5.在版本文件下存储对应的检查点文件,最终模型文件,损失曲线图
## 按全局训练 ##
1.创建 global 文件夹
2.选择训练范围时 创建文件夹根据数据范围所有店铺所有药品为all选择店铺就店铺id选择药品就药品id 自定义范围就根据下面的店铺id创建再在店铺id文件夹下创建对应的药品id文件夹
3.聚合方式 根据聚合方式创建对应的文件
4.模型类型 对应的文件下创建模型名称的文件夹
5.在模型名称的文件夹下,版本文件夹version+第几次训练
6.在版本文件下存储对应的检查点文件,最终模型文件,损失曲线图
---
## 优化后模型保存规则分析总结
与当前系统中将模型信息编码到文件名并将文件存储在相对扁平目录中的做法相比,新规则引入了一套更具结构化和层级化的模型保存策略。这种优化旨在提高模型文件的可管理性、可追溯性和可扩展性。
### 核心思想
优化后的核心思想是**“目录即元数据”**。通过创建层级分明的目录结构,将模型的训练模式、范围、类型和版本等关键信息体现在目录路径中,而不是仅仅依赖于文件名。所有与单次训练相关的产物(最终模型、检查点、损失曲线图等)都将被统一存放在同一个版本文件夹下,便于管理和溯源。
### 统一根目录
所有模型都将保存在 `saved_models` 文件夹下。
### 优化后的目录结构规则
#### 1. 按药品训练 (Product Training)
* **目录结构**: `saved_models/product/{product_id}_{scope}/{model_type}/v{N}/`
* **路径解析**:
* `product`: 表示这是按“药品”为核心的训练模式。
* `{product_id}_{scope}`:
* `{product_id}`: 训练的药品ID 。
* `{scope}`: 数据的店铺范围。
* `all`: 使用所有店铺的聚合数据。
* `{store_id}`: 使用指定店铺的数据。
* `{model_type}`: 模型的类型 (例如 `mlstm`, `transformer`)。
* `v{N}`: 模型的版本号 (例如 `v1`, `v2`)。
* **文件夹内容**:
* 最终模型文件 (例如 `model_final.pth`)
* 训练检查点文件 (例如 `checkpoint_epoch_10.pth`, `checkpoint_best.pth`)
* 损失曲线图 (例如 `loss_curve.png`)
#### 2. 按店铺训练 (Store Training)
* **目录结构**: `saved_models/store/{store_id}_{scope}/{model_type}/v{N}/`
* **路径解析**:
* `store`: 表示这是按“店铺”为核心的训练模式。
* `{store_id}_{scope}`:
* `{store_id}`: 训练的店铺ID 。
* `{scope}`: 数据的药品范围。
* `all`: 使用该店铺所有药品的聚合数据。
* `{product_id}`: 使用该店铺指定药品
* `v{N}`: 模型的版本号。
* **文件夹内容**: 与“按药品训练”模式相同。
#### 3. 全局训练 (Global Training)
* **目录结构**: `saved_models/global/{scope_path}/{aggregation_method}/{model_type}/v{N}/`
* **路径解析**:
* `global`: 表示这是“全局”训练模式。
* `{scope_path}`: 描述训练所用数据的范围,结构比较灵活:
* `all`: 代表所有店铺的所有药品。
* `stores/{store_id}`: 代表选择了特定的店铺。
* `products/{product_id}`: 代表选择了特定的药品。
* `custom/{store_id}/{product_id}`: 代表自定义范围,同时指定了店铺和药品。
* `{aggregation_method}`: 数据的聚合方式 (例如 `sum`, `mean`)。
* `{model_type}`: 模型的类型。
* `v{N}`: 模型的版本号。
* **文件夹内容**: 与“按药品训练”模式相同。
### 总结
总的来说,优化后的规则通过一个清晰、自解释的目录结构,系统化地组织了所有训练产物。这不仅使得查找和管理特定模型变得极为方便,也为未来的自动化模型管理和部署流程奠定了坚实的基础。
---
### 优化规则下的详细文件保存、读取及数据库记录规范
基于优化后的目录结构规则,我们进一步定义详细的文件保存、读取、数据库记录及版本管理的具体规范。
#### 一、 详细文件保存路径规则
所有训练产物都保存在对应模型的版本文件夹内,并采用统一的命名约定。
* **最终模型文件**: `model.pth`
* **最佳性能检查点**: `checkpoint_best.pth`
* **定期检查点**: `checkpoint_epoch_{epoch_number}.pth` (例如: `checkpoint_epoch_50.pth`)
* **损失曲线图**: `loss_curve.png`
* **训练元数据**: `metadata.json` (包含训练参数、指标等详细信息)
**示例路径:**
1. **按药品训练 (P001, 所有店铺, mlstm, v2)**:
* **目录**: `saved_models/product/P001_all/mlstm/v2/`
* **最终模型**: `saved_models/product/P001_all/mlstm/v2/model.pth`
* **损失曲线**: `saved_models/product/P001_all/mlstm/v2/loss_curve.png`
2. **按店铺训练 (S001, 指定药品P002, transformer, v1)**:
* **目录**: `saved_models/store/S001_P002/transformer/v1/`
* **最终模型**: `saved_models/store/S001_P002/transformer/v1/model.pth`
3. **全局训练 (所有数据, sum聚合, kan, v5)**:
* **目录**: `saved_models/global/all/sum/kan/v5/`
* **最终模型**: `saved_models/global/all/sum/kan/v5/model.pth`
#### 二、 文件读取规则
读取模型或其产物时,首先根据模型的元数据构建其版本目录路径,然后在该目录内定位具体文件。
**读取逻辑:**
1. **确定模型元数据**:
* 训练模式 (`product`, `store`, `global`)
* 范围 (`{product_id}_{scope}`, `{store_id}_{scope}`, `{scope_path}`)
* 聚合方式 (仅全局模式)
* 模型类型 (`mlstm`, `kan`, etc.)
* 版本号 (`v{N}`)
2. **构建模型根目录路径**: 根据上述元数据拼接路径。
* *示例*: 要读取“店铺S001下P002药品的transformer模型v1”构建路径 `saved_models/store/S001_P002/transformer/v1/`
3. **定位具体文件**: 在构建好的目录下直接读取所需文件。
* **加载最终模型**: 读取 `model.pth`
* **加载最佳模型**: 读取 `checkpoint_best.pth`
* **查看损失曲线**: 读取 `loss_curve.png`
#### 三、 数据库保存规则
数据库的核心职责是**索引模型**,而不是存储冗余信息。因此,数据库中只保存足以定位到模型版本目录的**路径**信息。
**`model_versions` 表结构优化:**
| 字段名 | 类型 | 描述 | 示例 |
| :--- | :--- | :--- | :--- |
| `id` | INTEGER | 主键 | 1 |
| `model_identifier` | TEXT | 模型的唯一标识符,由模式和范围构成 | `product_P001_all` |
| `model_type` | TEXT | 模型类型 | `mlstm` |
| `version` | TEXT | 版本号 | `v2` |
| `model_path` | TEXT | **模型版本目录的相对路径** | `saved_models/product/P001_all/mlstm/v2/` |
| `created_at` | TEXT | 创建时间 | `2025-07-15 18:40:00` |
| `metrics_summary`| TEXT | 关键性能指标的JSON字符串 | `{"rmse": 10.5, "r2": 0.89}` |
**保存逻辑:**
* 当一次训练成功完成并生成版本 `v{N}` 后,向 `model_versions` 表中插入一条新记录。
* `model_path` 字段**只记录到版本目录**,如 `saved_models/product/P001_all/mlstm/v2/`。应用程序根据此路径和标准文件名(如 `model.pth`)来加载具体文件。
#### 四、 版本记录文件规则
为了快速、方便地获取和递增版本号,在 `saved_models` 根目录下维护一个版本记录文件。
* **文件名**: `versions.json`
* **位置**: `saved_models/versions.json`
* **结构**: 一个JSON对象`key` 是模型的唯一标识符,`value` 是该模型的**最新版本号 (整数)**。
**`versions.json` 示例:**
```json
{
"product_P001_all_mlstm": 2,
"store_S001_P002_transformer": 1,
"global_all_sum_kan": 5
}
```
**版本管理流程:**
1. **获取下一个版本号**:
* 在开始新训练前,根据训练参数构建模型的唯一标识符 (例如 `product_P001_all_mlstm`)。
* 读取 `saved_models/versions.json` 文件。
* 查找对应的 `key`,获取当前最新版本号。如果 `key` 不存在,则当前版本为 0。
* 下一个版本号即为 `当前版本号 + 1`
2. **更新版本号**:
* 训练成功后,将新的版本号写回到 `saved_models/versions.json` 文件中,更新对应 `key``value`
* 这个过程需要加锁以防止并发训练时出现版本号冲突。