diff --git a/docs/superpowers/plans/2026-04-24-material-list-redesign.md b/docs/superpowers/plans/2026-04-24-material-list-redesign.md new file mode 100644 index 0000000..fc9bb1c --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-material-list-redesign.md @@ -0,0 +1,739 @@ +# 材料列表重构 实施计划 + +> **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:在 `