12 KiB
材料管理列表页重构设计
日期: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(exact,id)factory__cooperation_mode(exact)landing_project(icontains)cost_compare__gte,cost_compare__ltescore_level__gtecontact_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:有序数组,每项形如{ group: 'material', key: 'name', label: '材料名称', minWidth: 180, // 或 width showOverflowTooltip: true, slot: null, // 特殊渲染时填 slot 名:'advantage' | 'appScene' | 'stars' formatter: (row) => ..., // 普通文本格式化 }
列顺序(按用户"靠前展示"要求):
- A 组:材料名称、材料大类、细分种类、材料子类、阶段、重要等级、状态、成本比较、成本说明、优势、优势说明、应用场景、应用说明、替代类型、连接方式、施工方式、使用限制、规格、执行标准、质量/耐久/环保/碳/综合评分
- B 组:品牌、供应商简称、供应商全称、合作模式、省-市、对接人、对接电话
- C 组:落地项目、案例、经办人、备注
frontend/src/composables/useColumnPreferences.js
通用 composable:
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):
- 材料名称 input(width 180)
@keyup.enter="loadMaterials" - 状态 select(140)
@change="loadMaterials" - 材料子类 select(160)
@change - 品牌远程 select(160)
@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) - 综合评分 ≥ N(select 1-3 或 slider,
@change) - "重置筛选"按钮(清空全部筛选并查询)
触发逻辑
所有筛选值变更:pagination.page = 1 → loadMaterials()。
折叠"高级筛选"面板不清空字段值;仅控制显隐。"重置筛选"清空全部(含常用与高级)。
表格(数据驱动 + 分级表头)
<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 拆为三个,顺序与列表分组一致:
<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.jsfrontend/src/composables/useColumnPreferences.js- (可选)
frontend/src/views/material/cells/AdvantageCell.vue等,亦可内嵌
前端改动
frontend/src/views/MaterialManage.vuefrontend/src/views/material/MaterialForm.vue(view 模式)