|
@@ -0,0 +1,565 @@
|
|
|
|
|
+# 产品分类列表样式(方案 A 缩进展开树)Implementation Plan
|
|
|
|
|
+
|
|
|
|
|
+> **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:** 将移动端产品分类列表改为 Demo 方案 A:一体缩进展开树表,补齐行元信息、行内 `+` 与「全部展开/折叠」。
|
|
|
|
|
+
|
|
|
|
|
+**Architecture:** 抽出纯函数工具 `mp-crm-product-category-tree.ts`(展开集合、是否全展开、子类文案)供页面与单测复用;`categories.vue` 改模板为整卡一体树表并调整 Header 双按钮与 scoped 样式。不改后端契约与表单弹窗字段。
|
|
|
|
|
+
|
|
|
|
|
+**Tech Stack:** Vue 3 + Vant(WebView)+ TypeScript;node:test 单测;三墙 Token。
|
|
|
|
|
+
|
|
|
|
|
+**Spec:** `docs/superpowers/specs/2026-10-18-mp-crm-product-category-tree-style-design.md`
|
|
|
|
|
+
|
|
|
|
|
+**Demo:** `bak/mobile-ui/shanxia-mp-crm-product-category-tree-compare-demo.html`(方案 A)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## File map
|
|
|
|
|
+
|
|
|
|
|
+| File | Responsibility |
|
|
|
|
|
+|------|----------------|
|
|
|
|
|
+| `web-admin/src/utils/mp-crm-product-category-tree.ts` | 展开 ID 收集、是否全展开、子节点数与行元信息文案 |
|
|
|
|
|
+| `web-admin/src/utils/mp-crm-product-category-tree.test.ts` | 上述纯函数单测 |
|
|
|
|
|
+| `web-admin/src/views/m/crm/product/categories.vue` | 列表 DOM、Header 工具、调用工具函数、scoped 样式 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 1: 单测驱动 — 树展开与行元信息纯函数
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Create: `web-admin/src/utils/mp-crm-product-category-tree.ts`
|
|
|
|
|
+- Create: `web-admin/src/utils/mp-crm-product-category-tree.test.ts`
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
|
|
+
|
|
|
|
|
+创建 `web-admin/src/utils/mp-crm-product-category-tree.test.ts`:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+import assert from 'node:assert/strict'
|
|
|
|
|
+import { describe, it } from 'node:test'
|
|
|
|
|
+import {
|
|
|
|
|
+ collectExpandIds,
|
|
|
|
|
+ areAllExpandableExpanded,
|
|
|
|
|
+ categoryChildCount,
|
|
|
|
|
+ categoryChildMetaLabel
|
|
|
|
|
+} from './mp-crm-product-category-tree'
|
|
|
|
|
+
|
|
|
|
|
+type Node = {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ children?: Node[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const sample: Node[] = [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: '1',
|
|
|
|
|
+ children: [
|
|
|
|
|
+ { id: '11', children: [{ id: '111' }] },
|
|
|
|
|
+ { id: '12' }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ { id: '2' }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+describe('collectExpandIds', () => {
|
|
|
|
|
+ it('marks every node that has children', () => {
|
|
|
|
|
+ assert.deepEqual(collectExpandIds(sample), { '1': true, '11': true })
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+describe('areAllExpandableExpanded', () => {
|
|
|
|
|
+ it('returns true when all expandable ids are true', () => {
|
|
|
|
|
+ assert.equal(areAllExpandableExpanded(sample, { '1': true, '11': true }), true)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('returns false when any expandable id is missing or false', () => {
|
|
|
|
|
+ assert.equal(areAllExpandableExpanded(sample, { '1': true }), false)
|
|
|
|
|
+ assert.equal(areAllExpandableExpanded(sample, { '1': true, '11': false }), false)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('returns true for empty tree', () => {
|
|
|
|
|
+ assert.equal(areAllExpandableExpanded([], {}), true)
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+describe('categoryChildCount / categoryChildMetaLabel', () => {
|
|
|
|
|
+ it('counts direct children only', () => {
|
|
|
|
|
+ assert.equal(categoryChildCount(sample[0]), 2)
|
|
|
|
|
+ assert.equal(categoryChildCount(sample[1]), 0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('labels leaf vs parent', () => {
|
|
|
|
|
+ assert.equal(categoryChildMetaLabel(sample[0]), '2 个子类')
|
|
|
|
|
+ assert.equal(categoryChildMetaLabel(sample[1]), '末级')
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 跑测确认失败**
|
|
|
|
|
+
|
|
|
|
|
+Run:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd web-admin && node --import tsx --test src/utils/mp-crm-product-category-tree.test.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: FAIL(模块不存在或导出缺失)
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 最小实现**
|
|
|
|
|
+
|
|
|
|
|
+创建 `web-admin/src/utils/mp-crm-product-category-tree.ts`:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+export type CategoryTreeNodeLike = {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ children?: CategoryTreeNodeLike[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function categoryChildCount(node: CategoryTreeNodeLike | null | undefined): number {
|
|
|
|
|
+ const children = Array.isArray(node?.children) ? node!.children! : []
|
|
|
|
|
+ return children.length
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function categoryChildMetaLabel(node: CategoryTreeNodeLike | null | undefined): string {
|
|
|
|
|
+ const n = categoryChildCount(node)
|
|
|
|
|
+ return n > 0 ? `${n} 个子类` : '末级'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function collectExpandIds(
|
|
|
|
|
+ nodes: CategoryTreeNodeLike[],
|
|
|
|
|
+ acc: Record<string, boolean> = {}
|
|
|
|
|
+): Record<string, boolean> {
|
|
|
|
|
+ for (const node of nodes) {
|
|
|
|
|
+ if (categoryChildCount(node) > 0) {
|
|
|
|
|
+ acc[node.id] = true
|
|
|
|
|
+ collectExpandIds(node.children || [], acc)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return acc
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function areAllExpandableExpanded(
|
|
|
|
|
+ nodes: CategoryTreeNodeLike[],
|
|
|
|
|
+ expandedIds: Record<string, boolean>
|
|
|
|
|
+): boolean {
|
|
|
|
|
+ const required = collectExpandIds(nodes, {})
|
|
|
|
|
+ const ids = Object.keys(required)
|
|
|
|
|
+ if (!ids.length) return true
|
|
|
|
|
+ return ids.every((id) => !!expandedIds[id])
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: 跑测确认通过**
|
|
|
|
|
+
|
|
|
|
|
+Run:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd web-admin && node --import tsx --test src/utils/mp-crm-product-category-tree.test.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: PASS(全部用例通过)
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add web-admin/src/utils/mp-crm-product-category-tree.ts web-admin/src/utils/mp-crm-product-category-tree.test.ts
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+feat(web-admin): add product category tree expand helpers.
|
|
|
|
|
+
|
|
|
|
|
+Centralize expand-id collection and child meta labels for the mp category list.
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 2: 页面模板 — 一体树表 + Header 双按钮 + 行元信息与 `+`
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `web-admin/src/views/m/crm/product/categories.vue`
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 替换 Header 与列表模板**
|
|
|
|
|
+
|
|
|
|
|
+将模板中 Header 与列表区域(约第 3–50 行)改为:
|
|
|
|
|
+
|
|
|
|
|
+```vue
|
|
|
|
|
+ <MpBusinessHeader title="产品分类" back-label="返回产品列表" @back="goBack" />
|
|
|
|
|
+ <div class="mp-cat-header-tools">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="flatNodes.length"
|
|
|
|
|
+ class="mp-cat-header-btn"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ @click="toggleExpandAll"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ expandAllLabel }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="mp-cat-header-btn" type="button" @click="openAdd()">新增</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="loading && !tree.length" class="mp-product-loading">
|
|
|
|
|
+ <van-loading vertical>正在加载分类</van-loading>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <van-empty v-else-if="!flatNodes.length" description="暂无分类,点击右上角新增">
|
|
|
|
|
+ <van-button round type="primary" @click="openAdd()">新增分类</van-button>
|
|
|
|
|
+ </van-empty>
|
|
|
|
|
+ <div v-else class="mp-cat-tree">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="item in flatNodes"
|
|
|
|
|
+ :key="item.node.id"
|
|
|
|
|
+ class="mp-cat-row"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'is-l2': item.depth === 1,
|
|
|
|
|
+ 'is-l3': item.depth >= 2
|
|
|
|
|
+ }"
|
|
|
|
|
+ :style="{ paddingLeft: `${12 + item.depth * 16}px` }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="mp-cat-row__toggle"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ :class="{ 'is-leaf': !item.hasChildren, 'is-open': item.expanded }"
|
|
|
|
|
+ :aria-label="item.expanded ? '折叠' : '展开'"
|
|
|
|
|
+ :disabled="!item.hasChildren"
|
|
|
|
|
+ @click="toggleExpand(item.node.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span v-if="item.hasChildren">▶</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div class="mp-cat-row__main" @click="openNodeActions(item.node)">
|
|
|
|
|
+ <div class="mp-cat-row__name">{{ item.node.categoryName || '—' }}</div>
|
|
|
|
|
+ <div class="mp-cat-row__meta">
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="status-tag"
|
|
|
|
|
+ :class="`status-tag--${statusTone(item.node.status)}`"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ dictStore.getLabel('sys_normal_disable', item.node.status) || '—' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span>排序 {{ item.node.orderNum ?? 0 }}</span>
|
|
|
|
|
+ <span>{{ categoryChildMetaLabel(item.node) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="mp-cat-row__add"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ aria-label="新增子级"
|
|
|
|
|
+ @click="openAdd(item.node)"
|
|
|
|
|
+ >
|
|
|
|
|
+ +
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="mp-cat-row__more"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ aria-label="操作"
|
|
|
|
|
+ @click="openNodeActions(item.node)"
|
|
|
|
|
+ >
|
|
|
|
|
+ ⋯
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+保留其后的 `van-action-sheet`、表单 popup 等不变。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 接入工具函数与全部展开逻辑**
|
|
|
|
|
+
|
|
|
|
|
+在 `<script setup>` 中:
|
|
|
|
|
+
|
|
|
|
|
+1. 删除本地 `collectExpandIds` 函数。
|
|
|
|
|
+2. 增加 import:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+ import {
|
|
|
|
|
+ areAllExpandableExpanded,
|
|
|
|
|
+ categoryChildMetaLabel,
|
|
|
|
|
+ collectExpandIds
|
|
|
|
|
+ } from '@/utils/mp-crm-product-category-tree'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+3. 增加 computed 与 toggle:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+ const expandAllLabel = computed(() =>
|
|
|
|
|
+ areAllExpandableExpanded(tree.value, expandedIds.value) ? '全部折叠' : '全部展开'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ function toggleExpandAll() {
|
|
|
|
|
+ if (areAllExpandableExpanded(tree.value, expandedIds.value)) {
|
|
|
|
|
+ expandedIds.value = {}
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ expandedIds.value = collectExpandIds(tree.value)
|
|
|
|
|
+ }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+4. `loadTree` 内继续使用导入的 `collectExpandIds`(替换原本地函数调用,行为不变)。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add web-admin/src/views/m/crm/product/categories.vue
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+feat(web-admin): wire category tree list markup for scheme A.
|
|
|
|
|
+
|
|
|
|
|
+Add expand-all control, row meta, and inline add-child on the mp category page.
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 3: Scoped 样式对齐 Demo A 一体树表
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `web-admin/src/views/m/crm/product/categories.vue`(`<style scoped>`)
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 替换样式块**
|
|
|
|
|
+
|
|
|
|
|
+将 `<style scoped lang="scss">` 整段替换为:
|
|
|
|
|
+
|
|
|
|
|
+```scss
|
|
|
|
|
+ .mp-product-page {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-header-tools {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: calc(8px + env(safe-area-inset-top, 0px));
|
|
|
|
|
+ right: 12px;
|
|
|
|
|
+ z-index: 60;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-header-btn {
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ padding: 0 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ background: rgb(255 255 255 / 15%);
|
|
|
|
|
+ border: 1px solid rgb(255 255 255 / 75%);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-header-btn:active {
|
|
|
|
|
+ opacity: 0.8;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-tree {
|
|
|
|
|
+ margin: 12px 10px 24px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid #dcebe4;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ min-height: 54px;
|
|
|
|
|
+ padding: 10px 10px 10px 0;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-bottom: 1px solid #edf5f1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row:last-child {
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row.is-l2 {
|
|
|
|
|
+ background: #fbfdff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row.is-l3 {
|
|
|
|
|
+ background: #fafcfb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row:active {
|
|
|
|
|
+ background: #f7fbf9;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__toggle {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ line-height: 28px;
|
|
|
|
|
+ color: #0a9f6d;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ transition: transform 0.15s ease;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__toggle.is-open {
|
|
|
|
|
+ transform: rotate(90deg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__toggle.is-leaf {
|
|
|
|
|
+ visibility: hidden;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__main {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__name {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2a26;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-top: 3px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6b7b74;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__add,
|
|
|
|
|
+ .mp-cat-row__more {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ color: #087f57;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ background: #e9f9f2;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mp-cat-row__add {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status-tag {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ min-height: 20px;
|
|
|
|
|
+ padding: 1px 7px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status-tag--success {
|
|
|
|
|
+ color: #087f57;
|
|
|
|
|
+ background: #e9f9f2;
|
|
|
|
|
+ border-color: #b7ebce;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status-tag--danger {
|
|
|
|
|
+ color: #b91c1c;
|
|
|
|
|
+ background: #fef2f2;
|
|
|
|
|
+ border-color: #fecaca;
|
|
|
|
|
+ }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+删除已无用的 `.mp-cat-header-add`、`.mp-cat-list` 旧规则(已被上面替换)。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 本地目视对照**
|
|
|
|
|
+
|
|
|
|
|
+Run(若已在跑开发服可跳过 install):
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd web-admin && pnpm dev
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+打开 webview 产品分类页(或路由 `/#/m/crm/product/categories`),对照 Demo A:
|
|
|
|
|
+
|
|
|
|
|
+1. 一体树表(非整行散卡)
|
|
|
|
|
+2. 缩进 + 箭头旋转
|
|
|
|
|
+3. 行内状态/排序/子类数、`+`、`⋯`
|
|
|
|
|
+4. 「全部展开/全部折叠」文案切换
|
|
|
|
|
+
|
|
|
|
|
+Expected: 与 Demo A 一致,表单/删除流程无回归。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add web-admin/src/views/m/crm/product/categories.vue
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+style(web-admin): restyle mp product category list as unified tree.
|
|
|
|
|
+
|
|
|
|
|
+Match scheme A indent-expand demo with sanqiang tokens.
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 4: Spec 状态回写 + 图谱更新
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `docs/superpowers/specs/2026-10-18-mp-crm-product-category-tree-style-design.md`
|
|
|
|
|
+- Run: `./scripts/update-graph.sh`(或 `graphify update .` 于仓库根)
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 更新 Spec 状态**
|
|
|
|
|
+
|
|
|
|
|
+将文件头 `状态:待审阅` 改为 `状态:已实现`。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 再跑单测**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+cd web-admin && node --import tsx --test src/utils/mp-crm-product-category-tree.test.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: PASS
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 更新知识图谱**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+./scripts/update-graph.sh
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+若脚本不可用,则在仓库根执行:`graphify update .`
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add docs/superpowers/specs/2026-10-18-mp-crm-product-category-tree-style-design.md graphify-out backend/graphify-out web-admin/graphify-out 2>/dev/null
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+docs: mark product category tree style design as implemented.
|
|
|
|
|
+
|
|
|
|
|
+Refresh graphify after the mp category list style change.
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+(若图谱无变更,仅提交 spec 状态即可。)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Spec coverage checklist
|
|
|
|
|
+
|
|
|
|
|
+| Spec 要求 | Task |
|
|
|
|
|
+|-----------|------|
|
|
|
|
|
+| 一体树表 + 缩进展开 | Task 2 + 3 |
|
|
|
|
|
+| 行元信息:状态 · 排序 · 子类数 | Task 1 + 2 |
|
|
|
|
|
+| 行内 `+` 新增子级 | Task 2 |
|
|
|
|
|
+| Header「全部展开/折叠」 | Task 1 + 2 |
|
|
|
|
|
+| Token 三墙色、行高触控 | Task 3 |
|
|
|
|
|
+| 不改 API / 表单 / 删除权限 | 全任务未触碰这些逻辑 |
|
|
|
|
|
+| 验收清单 1–6 | Task 2–3 目视 + Task 1 单测覆盖展开辅助逻辑 |
|
|
|
|
|
+
|
|
|
|
|
+## Placeholder / 一致性自检
|
|
|
|
|
+
|
|
|
|
|
+- 无 TBD/TODO;工具函数名在 Task 1–2 一致(`collectExpandIds` / `areAllExpandableExpanded` / `categoryChildMetaLabel`)。
|
|
|
|
|
+- 缩进公式与现页一致:`12 + depth * 16`。
|
|
|
|
|
+- Header 用绝对定位双按钮组,兼容现有无右侧 slot 的 `MpBusinessHeader`。
|