# 材料列表重构 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 重构材料管理列表页,实现分级表头(材料信息/品牌与供应商/案例信息)、列显隐配置(localStorage 持久化)、筛选扩展、工具栏紧凑化、详情页分块。 **Architecture:** 后端 `MaterialListSerializer` 扩展字段 + 筛选集追加;前端列表页改为数据驱动渲染,新增列偏好 composable + 分级表头;详情页(`MaterialForm` view 模式)拆为三个 `el-descriptions`。 **Tech Stack:** Django + DRF(后端)、Vue 3 + Element Plus(前端)、localStorage(前端持久化)。 **Spec:** `docs/superpowers/specs/2026-04-24-material-list-redesign-design.md` **Testing approach:** 项目当前无自动化测试。采用手动验证:后端用 `manage.py check` + curl/浏览器调用接口,前端用 `pnpm dev` / `npm run dev` + 浏览器核对。每个任务结束后在浏览器完成对应验证清单。 **Dev commands:** - 后端:`D:/projects/mat3/backend/.venv/Scripts/python.exe D:/projects/mat3/backend/manage.py runserver` - 前端:进入 `D:/projects/mat3/frontend` 后按项目约定启动(`npm run dev` 或 `pnpm dev`;查 package.json 确认) --- ## Task 1:后端 `MaterialListSerializer` 字段扩展 **Files:** - Modify: `backend/apps/material/serializers.py`(`MaterialListSerializer` 类) - [ ] **Step 1:阅读当前 `MaterialListSerializer`**,记录已有字段集合,明确哪些需要新增。 - [ ] **Step 2:新增 A 组直通字段** 在 `MaterialListSerializer.Meta.fields` 中追加: ``` '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', ``` - [ ] **Step 3:新增 B 组供应商扩展字段** 在类体添加: ```python factory_full_name = serializers.CharField(source='factory.factory_name', read_only=True, default=None) factory_cooperation_mode = serializers.CharField(source='factory.cooperation_mode', read_only=True, default=None) factory_cooperation_mode_display = serializers.SerializerMethodField() factory_province = serializers.CharField(source='factory.province', read_only=True, default=None) factory_city = serializers.CharField(source='factory.city', read_only=True, default=None) def get_factory_cooperation_mode_display(self, obj): return obj.factory.get_cooperation_mode_display() if obj.factory else None ``` 把新字段加入 `Meta.fields`。 - [ ] **Step 4:确认视图 queryset 有 `select_related('factory', 'brand')`** 检查 `backend/apps/material/views.py` 列表视图的 `get_queryset()` 或 `queryset`;如无则加上,避免 N+1。 - [ ] **Step 5:跑 `check`** ```bash D:/projects/mat3/backend/.venv/Scripts/python.exe D:/projects/mat3/backend/manage.py check ``` 预期:`System check identified no issues` - [ ] **Step 6:启动 runserver,curl / 浏览器请求 `/api/materials/?page_size=1`**,确认 JSON 中含所有新字段。 - [ ] **Step 7:Commit** ```bash git add backend/apps/material/serializers.py backend/apps/material/views.py git commit -m "feat(material): 列表序列化器扩展成本/优势/主要参数/供应商字段" ``` --- ## Task 2:后端列表筛选字段追加 **Files:** - Modify: `backend/apps/material/views.py`(或独立的 `filters.py`) - [ ] **Step 1:定位当前 filterset 配置**(`filterset_fields` 或 `FilterSet` 子类)。 - [ ] **Step 2:扩展筛选** 若使用 `filterset_fields` 简单 dict: ```python filterset_fields = { 'name': ['icontains'], 'status': ['exact'], 'material_subcategory': ['exact'], 'brand': ['exact'], 'major_category': ['exact'], 'material_category': ['icontains'], 'stage': ['exact'], 'importance_level': ['exact'], 'factory': ['exact'], 'factory__cooperation_mode': ['exact'], 'landing_project': ['icontains'], 'cost_compare': ['gte', 'lte'], 'score_level': ['gte'], 'contact_person': ['icontains'], 'handler': ['icontains'], } ``` 若使用 `FilterSet` 子类,添加对应字段(参考现有写法)。 - [ ] **Step 3:逐个 curl 验证**(至少抽查 3 个:`?major_category=xxx`、`?cost_compare__gte=10`、`?landing_project__icontains=abc`) - [ ] **Step 4:Commit** ```bash git add backend/apps/material/ git commit -m "feat(material): 列表新增大类/阶段/供应商/成本区间等筛选" ``` --- ## Task 3:前端列元数据常量 **Files:** - Create: `frontend/src/views/material/materialColumns.js` - [ ] **Step 1:新建文件**,导出 `COLUMN_GROUPS` 与 `MATERIAL_COLUMNS` ```js export const COLUMN_GROUPS = [ { key: 'material', label: '材料信息' }, { key: 'supplier', label: '品牌与供应商' }, { key: 'case', label: '案例信息' }, ] const fmt = (v) => (v === null || v === undefined || v === '' ? '-' : v) export const MATERIAL_COLUMNS = [ // ==== A. 材料信息 ==== { group: 'material', key: 'name', label: '材料名称', minWidth: 180, showOverflowTooltip: true }, { group: 'material', key: 'major_category_display', label: '材料大类', width: 100 }, { group: 'material', key: 'material_category', label: '细分种类', minWidth: 140, showOverflowTooltip: true }, { group: 'material', key: 'material_subcategory', label: '材料子类', minWidth: 140, showOverflowTooltip: true }, { group: 'material', key: 'stage_display', label: '阶段', width: 130, showOverflowTooltip: true }, { group: 'material', key: 'importance_level_display', label: '重要等级', width: 110 }, { group: 'material', key: 'status_display', label: '状态', width: 100 }, { group: 'material', key: 'cost_compare', label: '成本比较(%)', width: 120, formatter: (r) => fmt(r.cost_compare) }, { group: 'material', key: 'cost_desc', label: '成本说明', minWidth: 160, showOverflowTooltip: true }, { group: 'material', key: 'advantage', label: '优势', minWidth: 200, slot: 'tags' }, { group: 'material', key: 'advantage_desc', label: '优势说明', minWidth: 160, showOverflowTooltip: true }, { group: 'material', key: 'application_scene', label: '应用场景', minWidth: 200, slot: 'tags' }, { group: 'material', key: 'application_desc', label: '应用说明', minWidth: 160, showOverflowTooltip: true }, { group: 'material', key: 'replace_type', label: '替代类型', width: 120, showOverflowTooltip: true }, { group: 'material', key: 'connection_method', label: '连接方式', width: 120, showOverflowTooltip: true }, { group: 'material', key: 'construction_method', label: '施工方式', width: 120, showOverflowTooltip: true }, { group: 'material', key: 'limit_condition', label: '使用限制', minWidth: 140, showOverflowTooltip: true }, { group: 'material', key: 'spec', label: '规格', width: 120, showOverflowTooltip: true }, { group: 'material', key: 'standard', label: '执行标准', width: 120, showOverflowTooltip: true }, { group: 'material', key: 'quality_level', label: '质量', width: 90, slot: 'stars' }, { group: 'material', key: 'durability_level', label: '耐久', width: 90, slot: 'stars' }, { group: 'material', key: 'eco_level', label: '环保', width: 90, slot: 'stars' }, { group: 'material', key: 'carbon_level', label: '碳', width: 90, slot: 'stars' }, { group: 'material', key: 'score_level', label: '综合评分', width: 110, slot: 'stars' }, // ==== B. 品牌与供应商 ==== { group: 'supplier', key: 'brand_name', label: '品牌', minWidth: 140, showOverflowTooltip: true }, { group: 'supplier', key: 'factory_short_name', label: '供应商简称', minWidth: 140, showOverflowTooltip: true }, { group: 'supplier', key: 'factory_full_name', label: '供应商全称', minWidth: 180, showOverflowTooltip: true }, { group: 'supplier', key: 'factory_cooperation_mode_display', label: '合作模式', width: 110 }, { group: 'supplier', key: 'factory_location', label: '省-市', width: 140, formatter: (r) => [r.factory_province, r.factory_city].filter(Boolean).join('-') || '-' }, { group: 'supplier', key: 'contact_person', label: '对接人', width: 100, showOverflowTooltip: true }, { group: 'supplier', key: 'contact_phone', label: '对接电话', width: 150, showOverflowTooltip: true }, // ==== C. 案例信息 ==== { group: 'case', key: 'landing_project', label: '落地项目', minWidth: 140, showOverflowTooltip: true }, { group: 'case', key: 'cases', label: '案例', minWidth: 200, showOverflowTooltip: true }, { group: 'case', key: 'handler', label: '经办人', width: 100, showOverflowTooltip: true }, { group: 'case', key: 'remark', label: '备注', minWidth: 160, showOverflowTooltip: true }, ] export const ALL_COLUMN_KEYS = MATERIAL_COLUMNS.map((c) => c.key) ``` - [ ] **Step 2:Commit** ```bash git add frontend/src/views/material/materialColumns.js git commit -m "feat(material): 添加列表列元数据常量" ``` --- ## Task 4:列偏好 composable **Files:** - Create: `frontend/src/composables/useColumnPreferences.js` - [ ] **Step 1:新建 composable** ```js import { ref } from 'vue' export function useColumnPreferences(storageKey) { const hidden = ref([]) const load = () => { try { const raw = localStorage.getItem(storageKey) if (!raw) return const parsed = JSON.parse(raw) if (Array.isArray(parsed)) hidden.value = parsed.filter((x) => typeof x === 'string') } catch { hidden.value = [] } } const save = () => { try { localStorage.setItem(storageKey, JSON.stringify(hidden.value)) } catch { /* quota / private mode — ignore */ } } const isVisible = (key) => !hidden.value.includes(key) const toggle = (key) => { const i = hidden.value.indexOf(key) if (i >= 0) hidden.value.splice(i, 1) else hidden.value.push(key) save() } const setGroupVisible = (groupKeys, visible) => { if (visible) { hidden.value = hidden.value.filter((k) => !groupKeys.includes(k)) } else { const set = new Set(hidden.value) groupKeys.forEach((k) => set.add(k)) hidden.value = [...set] } save() } const reset = () => { hidden.value = [] save() } load() return { hidden, isVisible, toggle, setGroupVisible, reset } } ``` - [ ] **Step 2:Commit** ```bash git add frontend/src/composables/useColumnPreferences.js git commit -m "feat: 添加列显隐偏好 composable" ``` --- ## Task 5:列表页改造(分级表头 + 特殊 slot + 列设置) **Files:** - Modify: `frontend/src/views/MaterialManage.vue` - [ ] **Step 1:在 `