mat/docs/superpowers/specs/2026-04-24-material-list-re...

300 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 材料管理列表页重构设计
**日期**2026-04-24
**范围**`frontend/src/views/MaterialManage.vue` 列表页与 `frontend/src/views/material/MaterialForm.vue` 详情视图;`backend/apps/material/` 列表序列化器与筛选集
**目标**:列表展示完整信息、分级分组呈现、可配置列显隐(持久化)、更多筛选、操作体验更紧凑
---
## 1. 背景与动机
当前 `MaterialManage.vue` 列表页存在以下问题:
- 14 列平铺展示,缺少语义分组,用户难以快速定位字段
- 重要字段(成本、优势、主要参数、星级)未在列表展示,必须点入详情
- 供应商/品牌仅展示 `factory_short_name``brand_name`,其余属性(合作模式、省市)不可见
- 筛选条件仅 4 项,无法按大类、阶段、供应商、成本区间等过滤
- 工具栏按钮松散,需手动点"查询"按钮,效率低
- 详情页为 `MaterialForm` 的 view 模式复用,字段扁平铺陈无分块
## 2. 设计原则
- **信息分组**:列表与详情使用同一套三分组语义——"材料信息 / 品牌与供应商 / 案例信息"
- **列表完整、可配置**:一次性加载完整字段,由前端决定显示哪几列;用户偏好持久化到 `localStorage`
- **即时响应**:下拉筛选 `@change` 立即触发查询、输入框 `@keyup.enter` 触发查询,删除显式"查询"按钮
- **YAGNI**:仅做列显隐(不做列拖拽排序);仅持久化列偏好(不持久化筛选条件)
## 3. 分组与列归属
### A. 材料信息
- 材料名称、材料大类、细分种类、材料子类
- 阶段、重要等级、状态
- 规格、执行标准
- 应用场景JSON 数组 → tag、应用说明、替代类型
- 连接方式、施工方式、使用限制
- 优势JSON 数组 → tag、优势说明
- 成本比较(%)、成本说明
- 质量/耐久/环保/碳/综合评分(星级 1-3
### B. 品牌与供应商
- 品牌名称
- 供应商简称、供应商全称、合作模式、省-市、对接人、对接电话
### C. 案例信息
- 落地项目、案例cases、经办人、备注
操作列独立固定右侧,不属于三组。
## 4. 后端改动
### 4.1 `MaterialListSerializer``backend/apps/material/serializers.py`
新增字段(所有字段均为只读,源自 `Material` 实例或其关系):
**A 组补充**(直接暴露 model 字段):
`spec`, `standard`, `application_scene`, `application_desc`, `replace_type`, `advantage`, `advantage_desc`, `connection_method`, `construction_method`, `limit_condition`, `cost_compare`, `cost_desc`, `quality_level`, `durability_level`, `eco_level`, `carbon_level`, `score_level`
**B 组补充**
- `factory_full_name``source='factory.factory_name'`
- `factory_cooperation_mode``source='factory.cooperation_mode'`
- `factory_cooperation_mode_display``SerializerMethodField`,返回 `get_cooperation_mode_display()`
- `factory_province``source='factory.province'`
- `factory_city``source='factory.city'`
**C 组**`cases` 已在,无需改动。
**性能**:视图层确认对列表查询加上 `select_related('factory', 'brand')`(若已有则保留)。
### 4.2 筛选集ViewSet `filterset_fields` 或 FilterSet 类)
追加:
- `major_category`exact
- `material_category`icontains
- `stage`exact
- `importance_level`exact
- `factory`exactid
- `factory__cooperation_mode`exact
- `landing_project`icontains
- `cost_compare__gte`, `cost_compare__lte`
- `score_level__gte`
- `contact_person`icontains
- `handler`icontains
保留原有:`name`icontains、`status`、`material_subcategory`、`brand`。
### 4.3 不改动
- `MaterialDetailSerializer`:字段已全
- `Brand` / `Factory` 模型
- 导入/导出/审批相关接口
## 5. 前端改动
### 5.1 新增文件
#### `frontend/src/views/material/materialColumns.js`
导出两个常量:
- `COLUMN_GROUPS``[{ key: 'material', label: '材料信息' }, { key: 'supplier', label: '品牌与供应商' }, { key: 'case', label: '案例信息' }]`
- `MATERIAL_COLUMNS`:有序数组,每项形如
```js
{
group: 'material',
key: 'name',
label: '材料名称',
minWidth: 180, // 或 width
showOverflowTooltip: true,
slot: null, // 特殊渲染时填 slot 名:'advantage' | 'appScene' | 'stars'
formatter: (row) => ..., // 普通文本格式化
}
```
列顺序(按用户"靠前展示"要求):
1. A 组:材料名称、材料大类、细分种类、材料子类、阶段、重要等级、状态、成本比较、成本说明、优势、优势说明、应用场景、应用说明、替代类型、连接方式、施工方式、使用限制、规格、执行标准、质量/耐久/环保/碳/综合评分
2. B 组:品牌、供应商简称、供应商全称、合作模式、省-市、对接人、对接电话
3. C 组:落地项目、案例、经办人、备注
#### `frontend/src/composables/useColumnPreferences.js`
通用 composable
```js
export function useColumnPreferences(storageKey, allColumnKeys) {
const hidden = ref([]) // 从 localStorage 初始化
const load = () => { /* try parse, fallback [] */ }
const save = () => localStorage.setItem(storageKey, JSON.stringify(hidden.value))
const isVisible = (key) => !hidden.value.includes(key)
const toggle = (key) => { /* add or remove, then save */ }
const setGroupVisible = (groupKeys, visible) => { /* batch, then save */ }
const reset = () => { hidden.value = []; save() }
load()
return { hidden, isVisible, toggle, setGroupVisible, reset }
}
```
**存储约定**
- 键:`mat3:material-list:columns:v1`
- 值:仅存"隐藏的列 key 数组"(新增列默认显示,不受旧偏好影响)
- 操作列 key `__actions__`(或类似)不纳入可切换列表
### 5.2 `MaterialManage.vue` 改造
#### 工具栏(两行,紧凑)
**第一行**(常用筛选 + 主操作,`gap: 8px`
- 材料名称 inputwidth 180`@keyup.enter="loadMaterials"`
- 状态 select140`@change="loadMaterials"`
- 材料子类 select160`@change`
- 品牌远程 select160`@change`
- "高级筛选"切换按钮text 按钮 + 箭头图标;若存在任意高级筛选值则右上角红点徽标)
- `.toolbar-spacer`flex: 1
- 导入 / 导出 / 新增材料 / **列设置**(齿轮图标按钮)
删除原"查询"按钮(自动触发)。
**第二行**`v-show="advancedOpen"``gap: 8px``flex-wrap: wrap`
- 材料大类 select、阶段 select、重要等级 select、合作模式 select`@change`
- 细分种类 input、落地项目 input、对接人 input、经办人 input`@keyup.enter`
- 供应商 select远程搜索`@change`
- 成本比较 min / max 双 InputNumber`@change`
- 综合评分 ≥ Nselect 1-3 或 slider`@change`
- "重置筛选"按钮(清空全部筛选并查询)
#### 触发逻辑
所有筛选值变更:`pagination.page = 1` → `loadMaterials()`
折叠"高级筛选"面板**不**清空字段值;仅控制显隐。"重置筛选"清空全部(含常用与高级)。
#### 表格(数据驱动 + 分级表头)
```html
<el-table :data="materials" border height="100%">
<template v-for="group in COLUMN_GROUPS" :key="group.key">
<el-table-column
v-if="hasVisibleInGroup(group.key)"
:label="group.label"
align="center"
>
<el-table-column
v-for="col in visibleColumnsOfGroup(group.key)"
:key="col.key"
:prop="col.key"
:label="col.label"
:min-width="col.minWidth"
:width="col.width"
:show-overflow-tooltip="col.showOverflowTooltip"
>
<template v-if="col.slot" #default="scope">
<AdvantageCell v-if="col.slot === 'advantage'" :value="scope.row[col.key]" />
<AppSceneCell v-else-if="col.slot === 'appScene'" :value="scope.row[col.key]" />
<StarsCell v-else-if="col.slot === 'stars'" :value="scope.row[col.key]" />
</template>
<template v-else-if="col.formatter" #default="scope">
{{ col.formatter(scope.row) ?? '-' }}
</template>
</el-table-column>
</el-table-column>
</template>
<el-table-column label="操作" width="320" fixed="right">
<!-- 原有操作按钮,不动 -->
</el-table-column>
</el-table>
```
特殊单元格组件(内嵌或单独 SFC 均可):
- **AdvantageCell / AppSceneCell**:接收 JSON 数组,渲染 `el-tag` 列表;空则 "-"
- **StarsCell**:接收 1-3 数字,渲染对应数量的黄色 ⭐,空显示 "-"
整组隐藏(`hasVisibleInGroup = false`)时整个分组表头消失。
#### 列设置 popover
齿轮图标按钮触发 `el-popover`(宽 ~420px最大高度带滚动
-`COLUMN_GROUPS` 分节,每节标题 + "全选 / 全不选" 小按钮
- 每列渲染 `el-checkbox`,绑定到 `isVisible(col.key)``@change` 调用 `toggle(col.key)`
- 底部一个"恢复默认"按钮调用 `reset()`
### 5.3 详情页分块(`MaterialForm.vue` view 模式)
将当前单个 `el-descriptions` 拆为三个,顺序与列表分组一致:
```html
<template v-if="mode === 'view'">
<el-descriptions title="材料信息" :column="2" border>
<!-- A 组字段 -->
</el-descriptions>
<el-descriptions title="品牌与供应商" :column="2" border class="mt-4">
<!-- B 组字段 -->
</el-descriptions>
<el-descriptions title="案例信息" :column="1" border class="mt-4">
<!-- C 组字段cases 单独一行 white-space: pre-wrap -->
</el-descriptions>
</template>
```
星级字段使用 ⭐ 渲染JSON 数组用 `el-tag` 列表;宣传册图片独占一行。
edit 模式不变。
## 6. 数据流
```
用户动作change / enter / checkbox
└─→ filters reactive 更新 / hidden 数组更新
├─→ loadMaterials() → GET /api/materials/?filters → 更新 materials
└─→ localStorage.setItem('mat3:material-list:columns:v1', JSON)
```
## 7. 错误与边界
- **localStorage 损坏**JSON.parse 失败):捕获异常,回退到 `hidden = []`,覆盖写回合法值
- **列结构演进**:后续新增列时,因只存"隐藏列"新列对老用户默认可见如需强制重置偏好bump 存储键版本号为 `v2`
- **空值**:所有 formatter 与 slot 遇 `null/undefined/[]` 显示 `-`
- **整组隐藏**:通过 `hasVisibleInGroup()` 计算属性剔除整个分组表头
## 8. 测试 / 验证清单
- [ ] 后端 `/api/materials/` 返回新增字段;新增筛选参数工作
- [ ] 前端列表三组表头正确渲染,列顺序符合设计
- [ ] `advantage / application_scene / stars` 特殊渲染正确
- [ ] 列设置 popover 勾选立即生效,刷新后偏好恢复
- [ ] 全部隐藏某组时整组表头消失;"恢复默认"还原全显
- [ ] 所有下拉 `@change` 自动触发查询;输入框 Enter 触发查询
- [ ] 高级筛选收起后值保留;有值时红点标记显示;"重置筛选"清全部
- [ ] 详情页三块分节展示
- [ ] 现有导入/导出/新增/编辑/审批/分页流程不受影响
- [ ] 工具栏按钮紧凑、无换行
## 9. 非目标YAGNI
- 列拖拽排序
- 筛选条件持久化
- 分页默认值调整
- 品牌字段扩展
- 列宽持久化
- 表格导出仅可见列(导出使用现有后端接口,所有字段)
## 10. 文件清单
**后端**
- `backend/apps/material/serializers.py`(改 `MaterialListSerializer`
- `backend/apps/material/views.py``filters.py`(追加筛选字段)
**前端新增**
- `frontend/src/views/material/materialColumns.js`
- `frontend/src/composables/useColumnPreferences.js`
- (可选)`frontend/src/views/material/cells/AdvantageCell.vue` 等,亦可内嵌
**前端改动**
- `frontend/src/views/MaterialManage.vue`
- `frontend/src/views/material/MaterialForm.vue`view 模式)