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

12 KiB
Raw Blame History

材料管理列表页重构设计

日期2026-04-24 范围frontend/src/views/MaterialManage.vue 列表页与 frontend/src/views/material/MaterialForm.vue 详情视图;backend/apps/material/ 列表序列化器与筛选集 目标:列表展示完整信息、分级分组呈现、可配置列显隐(持久化)、更多筛选、操作体验更紧凑


1. 背景与动机

当前 MaterialManage.vue 列表页存在以下问题:

  • 14 列平铺展示,缺少语义分组,用户难以快速定位字段
  • 重要字段(成本、优势、主要参数、星级)未在列表展示,必须点入详情
  • 供应商/品牌仅展示 factory_short_namebrand_name,其余属性(合作模式、省市)不可见
  • 筛选条件仅 4 项,无法按大类、阶段、供应商、成本区间等过滤
  • 工具栏按钮松散,需手动点"查询"按钮,效率低
  • 详情页为 MaterialForm 的 view 模式复用,字段扁平铺陈无分块

2. 设计原则

  • 信息分组:列表与详情使用同一套三分组语义——"材料信息 / 品牌与供应商 / 案例信息"
  • 列表完整、可配置:一次性加载完整字段,由前端决定显示哪几列;用户偏好持久化到 localStorage
  • 即时响应:下拉筛选 @change 立即触发查询、输入框 @keyup.enter 触发查询,删除显式"查询"按钮
  • YAGNI:仅做列显隐(不做列拖拽排序);仅持久化列偏好(不持久化筛选条件)

3. 分组与列归属

A. 材料信息

  • 材料名称、材料大类、细分种类、材料子类
  • 阶段、重要等级、状态
  • 规格、执行标准
  • 应用场景JSON 数组 → tag、应用说明、替代类型
  • 连接方式、施工方式、使用限制
  • 优势JSON 数组 → tag、优势说明
  • 成本比较(%)、成本说明
  • 质量/耐久/环保/碳/综合评分(星级 1-3

B. 品牌与供应商

  • 品牌名称
  • 供应商简称、供应商全称、合作模式、省-市、对接人、对接电话

C. 案例信息

  • 落地项目、案例cases、经办人、备注

操作列独立固定右侧,不属于三组。

4. 后端改动

4.1 MaterialListSerializerbackend/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_namesource='factory.factory_name'
  • factory_cooperation_modesource='factory.cooperation_mode'
  • factory_cooperation_mode_displaySerializerMethodField,返回 get_cooperation_mode_display()
  • factory_provincesource='factory.province'
  • factory_citysource='factory.city'

C 组cases 已在,无需改动。

性能:视图层确认对列表查询加上 select_related('factory', 'brand')(若已有则保留)。

4.2 筛选集ViewSet filterset_fields 或 FilterSet 类)

追加:

  • major_categoryexact
  • material_categoryicontains
  • stageexact
  • importance_levelexact
  • factoryexactid
  • factory__cooperation_modeexact
  • landing_projecticontains
  • cost_compare__gte, cost_compare__lte
  • score_level__gte
  • contact_personicontains
  • handlericontains

保留原有:nameicontainsstatusmaterial_subcategorybrand

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) => ...,  // 普通文本格式化
    }
    

列顺序(按用户"靠前展示"要求):

  1. A 组:材料名称、材料大类、细分种类、材料子类、阶段、重要等级、状态、成本比较、成本说明、优势、优势说明、应用场景、应用说明、替代类型、连接方式、施工方式、使用限制、规格、执行标准、质量/耐久/环保/碳/综合评分
  2. B 组:品牌、供应商简称、供应商全称、合作模式、省-市、对接人、对接电话
  3. 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

  • 材料名称 inputwidth 180@keyup.enter="loadMaterials"
  • 状态 select140@change="loadMaterials"
  • 材料子类 select160@change
  • 品牌远程 select160@change
  • "高级筛选"切换按钮text 按钮 + 箭头图标;若存在任意高级筛选值则右上角红点徽标)
  • .toolbar-spacerflex: 1
  • 导入 / 导出 / 新增材料 / 列设置(齿轮图标按钮)

删除原"查询"按钮(自动触发)。

第二行v-show="advancedOpen"gap: 8pxflex-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 = 1loadMaterials()。 折叠"高级筛选"面板清空字段值;仅控制显隐。"重置筛选"清空全部(含常用与高级)。

表格(数据驱动 + 分级表头)

<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.pyfilters.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.vueview 模式)