From 24a9fabc880fc3da3ab01cfb0e1042b5ec3e870c Mon Sep 17 00:00:00 2001 From: hsu06 Date: Wed, 1 Jul 2026 17:46:29 +0800 Subject: [PATCH] Add supplier-management prototype and enhance customer-management risk tags. --- .../.spec/requirements-prd-list.md | 201 ++++++- .../customer-management/.spec/ui-review.md | 81 +++ .../annotation-source.json | 340 +++++------- .../customer-management/canvas.excalidraw | 10 + .../components/BatchRiskTagModal.tsx | 151 ++++++ .../components/CustomerTable.tsx | 26 +- .../components/RiskLabelTags.tsx | 82 +++ .../components/RiskTagDrawer.tsx | 308 +++++++++++ .../components/RiskTagForm.tsx | 242 +++++++++ .../data/risk-tag-operation-logs.json | 50 ++ src/prototypes/customer-management/index.tsx | 225 +++++++- .../customer-management/pages/CreatePage.tsx | 46 +- .../scripts/build-annotation-source.mjs | 14 +- .../customer-management/styles/index.css | 506 +++++++++++++++++- src/prototypes/customer-management/types.ts | 4 + .../customer-management/types/riskTag.ts | 98 ++++ .../customer-management/utils/address.ts | 37 ++ .../customer-management/utils/constants.ts | 2 + .../customer-management/utils/permissions.ts | 73 +++ .../utils/riskTagConstants.ts | 55 ++ .../customer-management/utils/riskTagStore.ts | 221 ++++++++ .../annotation-source.json | 196 +++++++ .../supplier-management/canvas.excalidraw | 10 + .../components/CoopStatusBadge.tsx | 22 + .../components/FilterPanel.tsx | 180 +++++++ .../components/KpiCards.tsx | 65 +++ .../components/SupplierTable.tsx | 99 ++++ .../supplier-management/data/suppliers.json | 154 ++++++ src/prototypes/supplier-management/index.tsx | 319 +++++++++++ .../supplier-management/pages/CreatePage.tsx | 265 +++++++++ .../supplier-management/styles/index.css | 320 +++++++++++ src/prototypes/supplier-management/types.ts | 93 ++++ .../supplier-management/utils/address.ts | 32 ++ .../supplier-management/utils/constants.ts | 38 ++ .../supplier-management/utils/filters.ts | 59 ++ 35 files changed, 4377 insertions(+), 247 deletions(-) create mode 100644 src/prototypes/customer-management/.spec/ui-review.md create mode 100644 src/prototypes/customer-management/canvas.excalidraw create mode 100644 src/prototypes/customer-management/components/BatchRiskTagModal.tsx create mode 100644 src/prototypes/customer-management/components/RiskLabelTags.tsx create mode 100644 src/prototypes/customer-management/components/RiskTagDrawer.tsx create mode 100644 src/prototypes/customer-management/components/RiskTagForm.tsx create mode 100644 src/prototypes/customer-management/data/risk-tag-operation-logs.json create mode 100644 src/prototypes/customer-management/types/riskTag.ts create mode 100644 src/prototypes/customer-management/utils/address.ts create mode 100644 src/prototypes/customer-management/utils/permissions.ts create mode 100644 src/prototypes/customer-management/utils/riskTagConstants.ts create mode 100644 src/prototypes/customer-management/utils/riskTagStore.ts create mode 100644 src/prototypes/supplier-management/annotation-source.json create mode 100644 src/prototypes/supplier-management/canvas.excalidraw create mode 100644 src/prototypes/supplier-management/components/CoopStatusBadge.tsx create mode 100644 src/prototypes/supplier-management/components/FilterPanel.tsx create mode 100644 src/prototypes/supplier-management/components/KpiCards.tsx create mode 100644 src/prototypes/supplier-management/components/SupplierTable.tsx create mode 100644 src/prototypes/supplier-management/data/suppliers.json create mode 100644 src/prototypes/supplier-management/index.tsx create mode 100644 src/prototypes/supplier-management/pages/CreatePage.tsx create mode 100644 src/prototypes/supplier-management/styles/index.css create mode 100644 src/prototypes/supplier-management/types.ts create mode 100644 src/prototypes/supplier-management/utils/address.ts create mode 100644 src/prototypes/supplier-management/utils/constants.ts create mode 100644 src/prototypes/supplier-management/utils/filters.ts diff --git a/src/prototypes/customer-management/.spec/requirements-prd-list.md b/src/prototypes/customer-management/.spec/requirements-prd-list.md index 21a3668..0340927 100644 --- a/src/prototypes/customer-management/.spec/requirements-prd-list.md +++ b/src/prototypes/customer-management/.spec/requirements-prd-list.md @@ -9,9 +9,11 @@ | 项 | 说明 | |---|---| | 模块名称 | 客户管理 — 列表页 | -| 目标用户 | 销售、业务负责人、运营、风控 | -| 核心任务 | 按多维度筛选客户台账,查看标签与状态,新增客户或进入详情/编辑 | -| 页面结构 | 面包屑 + 筛选区 + 工具栏 + 表格 + 分页 | +| 目标用户 | 销售、业务负责人、运营、**法务、财务、风控** | +| 核心任务 | 按多维度筛选客户台账,查看风险标签与状态,**在列表页维护客户风险标签**,新增客户或进入详情/编辑 | +| 页面结构 | 面包屑 + **操作角色切换** + 筛选区 + 工具栏 + 表格 + 分页 | + +**重要约束:** 风险标签**不在**客户新增页、客户编辑表单中维护,仅在列表页作为操作入口。 --- @@ -30,9 +32,14 @@ | 数据 | 来源 | 用途 | |---|---|---| | 客户台账 | `data/customers.json`(20 条样例) | 列表、筛选选项 | -| 客户标签 | `CUSTOMER_LABEL_OPTIONS` 常量 | 筛选与列表展示 | +| 客户标签(展示) | 客户 `labels` 字段 | 列表「风险标签」列、筛选区「标签」多选 | +| 风险标签明细 | `riskTagsByCustomer` 运行时状态 + 种子迁移 | 标签管理抽屉、审批、日志 | +| 标签目录 | `utils/riskTagConstants.ts` → `TAG_CATALOG` | 打标表单选项、级别、分组 | +| 操作日志 | `data/risk-tag-operation-logs.json` + 运行时追加 | 抽屉「操作日志」Tab | | 部门-负责人 | 由台账聚合 + `DEPT_MANAGER_MAP` | 筛选联动 | +列表 `labels` 与「生效中」风险标签自动同步,保证筛选器与列表展示一致。 + --- ## 4. 筛选区 @@ -43,7 +50,7 @@ |---|---|---| | 客户编号 | 可搜索单选 | 精确匹配 `code` | | 客户名称 | 可搜索单选 | 精确匹配 `name` | -| 标签 | 多选 | 客户 `labels` 包含任一选中标签 | +| 标签 | 多选 | 客户 `labels` 包含任一选中标签(OR) | | 区域 | 可搜索单选 | 精确匹配 `region` | | 客户状态 | 多选 | `status` 在选中集合内 | | 业务部门 | 多选 | `department` 在选中集合内 | @@ -54,8 +61,8 @@ | 操作 | 行为 | |---|---| -| **查询** | 将当前筛选项写入已应用条件,页码重置为 1 | -| **重置** | 清空待应用与已应用条件,页码重置为 1 | +| **查询** | 将当前筛选项写入已应用条件,页码重置为 1,清空行选中 | +| **重置** | 清空待应用与已应用条件,页码重置为 1,清空行选中 | | 修改筛选项未点查询 | 不影响当前列表 | --- @@ -66,8 +73,23 @@ |---|---| | 新增客户 | 进入新增客户页(分组表单) | | 批量导入 | Toast:批量导入(原型演示) | +| **批量打标** | 勾选 ≥1 行且当前角色有打标权限时显示;打开批量打标弹窗 | | 批量导出 | Toast:导出当前列表条数 | +### 5.1 操作角色切换(原型演示) + +列表顶部提供「当前操作角色」下拉,用于模拟不同部门用户: + +| 角色 | 部门 | 打标权限 | +|---|---|---| +| 王风控 | 风控部 | 全部标签 | +| 李法务 | 法务部 | 法务类标签 | +| 张财务 | 财务部 | 财务类标签 | +| 赵运维 | 运维部 | 运维类标签 | +| 陈销售 | 销售一部 | 仅查看,无打标入口 | + +切换角色后,风险标签管理抽屉内的**标签类型**、**可选标签**、**审批/移除按钮**随之联动更新。 + --- ## 6. 列表字段说明 @@ -76,21 +98,174 @@ |---|---| | 客户编号 | 左固定列 | | 客户名称 | 超长省略 | -| 标签 | 仅展示第一个标签;多于 1 个显示「+N」,悬浮展示全部 | +| **风险标签** | 展示「生效中」标签;按风险级别着色(低/中/高/极高);首标签 + `+N` 悬浮全部;有待审批时显示「审」标识;可点击打开管理抽屉 | | 客户区域 | 大区名称 | | 客户状态 | 绿点「正常」/ 红点「黑名单」 | | 业务部门 / 业务负责人 | 文本 | | 客户类型 / 所属行业 | 文本 | | 统一社会信用代码 / 法人 / 注册资本 / 经营状态 | 文本 | | 详细地址 / 创建时间 | 文本;创建时间不换行 | -| 操作 | 详情、编辑、删除(原型 Toast) | +| 操作 | 详情、编辑、**管理标签**(有权限时)、删除(原型 Toast) | -支持行多选(checkbox);横向滚动;操作列右固定。 +支持行多选(checkbox,仅当前页);横向滚动;操作列右固定。 --- -## 7. 客户标签枚举 +## 7. 客户标签枚举与分级目录 -逾期预警、严重拖欠、频繁改账期、资质存疑、涉诉/仲裁、制裁/高风险地区、频繁争议、车辆/资产高风险、信息不实、开票异常、多头签约。 +标签按业务分类维护,打标时下拉按 **optgroup 分组** 展示。风险级别由目录**自动带出且不可手改**。 -标签样式:白底 + 橙色描边(`#f77234`)。 +### 7.1 信用与回款 + +| 标签名称 | 标签类型 | 风险级别 | 典型触发 | +|---|---|---|---| +| 逾期预警 | 财务 | 中 | 当前逾期或近 6 个月内曾逾期 | +| 严重拖欠 | 财务 | 高 | 逾期 > 30 天未结清或历史坏账 | +| 频繁改账期 | 财务 | 低 | 一年内申请延长账期 ≥ 3 次 | + +### 7.2 合规与法律 + +| 标签名称 | 标签类型 | 风险级别 | 典型触发 | +|---|---|---|---| +| 资质存疑 | 法务 | 中 | 证照过期、抬头与付款主体不一致等 | +| 涉诉/仲裁 | 法务 | 高 | 与我司或第三方重大未结诉讼 | +| 制裁/高风险地区 | 法务 | 高 | 名单筛查命中(依公司合规清单) | + +### 7.3 经营与履约 + +| 标签名称 | 标签类型 | 风险级别 | 典型触发 | +|---|---|---|---| +| 频繁争议 | 法务 | 中 | 合同或扣款争议次数超阈值 | +| 车辆/资产高风险 | 运维 | 高 | 违章、事故、疑似骗保等 | +| 信息不实 | 风控 | 中 | 联系人/主体核查不实、空壳公司等 | + +### 7.4 操作与流程 + +| 标签名称 | 标签类型 | 风险级别 | 典型触发 | +|---|---|---|---| +| 开票异常 | 财务 | 低 | 频繁退票、抬头异常变更 | +| 多头签约 | 风控 | 低 | 同一实控人多主体分别签约 | + +### 7.5 列表展示样式 + +- 低:灰色边框 +- 中:橙色浅底 +- 高:红色浅底 +- 极高:深红浅底 +- 单元格最大宽度受限,避免撑乱表格 + +--- + +## 8. 风险标签维护(列表页专属) + +### 8.1 入口 + +| 入口 | 说明 | +|---|---| +| 行操作「管理标签」 | 打开右侧抽屉,管理单个客户 | +| 点击标签列 | 有权限时同「管理标签」 | +| 工具栏「批量打标」 | 对当前勾选客户批量提交同一标签 | + +**不在**新增页、编辑页维护标签。 + +### 8.2 管理抽屉结构 + +| Tab | 内容 | +|---|---| +| 当前标签 | 已打标签列表:名称、类型、级别、状态、原因摘要;支持查看详情、移除、审批 | +| 新增标签 | 打标表单(见 8.3) | +| 操作日志 | 该客户全部标签相关审计记录 | + +### 8.3 打标表单字段 + +| 字段 | 必填 | 规则 | +|---|---|---| +| 标签名称 | 是 | 按 7.1–7.4 分组下拉;仅当前角色有权限的标签可选;已存在同名有效标签不可重复选 | +| 标签类型 | 是 | **根据当前操作角色自动带出**(财务→财务、法务→法务、运维→运维、风控→风控),**置灰不可编辑** | +| 风险级别 | 是 | **选择标签名称后按目录自动带出**,**置灰不可编辑** | +| 生效时间 | 是 | 日期,默认当天 | +| 有效期至 | 是 | 日期,须 ≥ 生效时间;默认一年后 | +| 原因说明 | 是 | 至少 10 字,供法务/财务/风控审批留痕 | +| 附件 | 否 | 支持 .pdf / .jpg / .png / .xlsx,可多个 | + +### 8.4 批量打标 + +- 展示已选客户 chips(超过 6 个显示 +N) +- 表单字段与单客户打标一致 +- 已存在同名有效标签的客户自动跳过 +- 提交后写入批量操作日志 + +### 8.5 标签生命周期 + +| 状态 | 说明 | +|---|---| +| 生效中 | 参与列表展示,同步至 `labels` | +| 待审批 | 高风险标签提交后进入;列表暂不展示为生效 | +| 待确认 | 预留 | +| 已驳回 | 审批未通过,不同步至 `labels` | +| 已移除 | 操作人主动移除,需填写原因 | +| 已过期 | 预留 | + +**高风险规则:** 风险级别为「高」或「极高」的标签提交后状态为「待审批」,须法务或风控审批通过后变为「生效中」。 + +### 8.6 移除标签 + +- 仅「生效中」且当前角色有移除权限的标签可移除 +- 须填写移除原因(至少 5 字) +- 写入操作日志,并从列表 `labels` 同步清除 + +--- + +## 9. 权限与审批 + +### 9.1 打标权限 + +| 角色 | 可选标签范围 | 标签类型(自动) | +|---|---|---| +| 财务 | 财务类(信用与回款、操作与流程中的财务标签) | 财务 | +| 法务 | 法务类(合规与法律、经营与履约中的法务标签) | 法务 | +| 运维 | 运维类(车辆/资产高风险) | 运维 | +| 风控 / 管理员 | 全部 | 风控 | +| 销售 | 无 | — | + +### 9.2 审批权限 + +- 法务、风控、管理员可审批「待审批」标签 +- 支持通过 / 驳回;驳回须填写原因 + +### 9.3 移除权限 + +与打标权限一致:各角色仅能移除本类型标签;风控 / 管理员可移除全部类型。 + +--- + +## 10. 操作日志 + +所有标签 **新增、移除、提交审批、审批通过、审批驳回、批量打标** 均写入日志。 + +| 字段 | 说明 | +|---|---| +| 操作类型 | 新增 / 移除 / 提交审批 / 审批通过 / 审批驳回 / 批量打标 | +| 客户 | 客户名称 + ID | +| 标签名称 | 涉及标签 | +| 详情 | 原因、审批结论等 | +| 操作人 | 姓名 + 角色 | +| 操作时间 | `YYYY-MM-DD HH:mm:ss` | + +抽屉「操作日志」Tab 按客户过滤;原型预置样例日志见 `data/risk-tag-operation-logs.json`。 + +--- + +## 11. 原型实现索引 + +| 能力 | 代码位置 | +|---|---| +| 列表编排 | `index.tsx` | +| 风险标签列 | `components/RiskLabelTags.tsx` | +| 管理抽屉 | `components/RiskTagDrawer.tsx` | +| 批量打标 | `components/BatchRiskTagModal.tsx` | +| 打标表单 | `components/RiskTagForm.tsx` | +| 标签目录 | `utils/riskTagConstants.ts` | +| 权限 | `utils/permissions.ts` | +| 业务逻辑 | `utils/riskTagStore.ts` | +| 类型定义 | `types/riskTag.ts` | diff --git a/src/prototypes/customer-management/.spec/ui-review.md b/src/prototypes/customer-management/.spec/ui-review.md new file mode 100644 index 0000000..848b2ff --- /dev/null +++ b/src/prototypes/customer-management/.spec/ui-review.md @@ -0,0 +1,81 @@ +# UI Review + +- 审查目标:src/prototypes/customer-management +- 使用设计依据:DESIGN.md(项目默认 OneOS 设计规范) +- 生成时间:2026-07-01 + +## 总体点评 + +客户管理原型在信息架构上与 OneOS 中后台惯例高度一致:列表页采用「筛选区 + 工具栏 + 表格 + 分页」四段式,新增页采用分组卡片 + 底部 sticky 操作栏,并复用 `vehicle-management` 的 `vm-*` 样式与令牌桥接,整体视觉气质专业、数据密度适中,与 DESIGN.md 描述的 Emerald 主色、Inter 字体、32px 控件高度等基调基本吻合。 + +亮点方面:筛选区默认折叠为 4 项并支持「更多筛选」,有效控制了首屏认知负荷;表格对编号列与操作列做了 sticky 处理,在 17 列宽表场景下保留了关键操作可达性;新增页省-市、部门-负责人联动逻辑清晰,发票抬头随客户全称自动带入,符合业务心智模型。 + +主要风险集中在三类:一是部分组件(状态徽章、客户标签)仍使用硬编码色值,偏离 OneOS 语义色与 Emerald 主色体系;二是新增页表单错误反馈仅依赖 3 秒消失的 Toast,缺少字段级错误态与焦点引导;三是列表页面包屑信息不完整,与新增页及 DESIGN.md 约定的页面结构存在不一致。Impeccable detect 扫描未检出自动化规则违规,独立评估标记为 degraded(未使用浏览器预览/截图)。 + +## P0-P3 优先级问题 + +### P1 - 新增页校验失败仅 Toast 提示,缺少字段级错误反馈 + +- 证据:`index.tsx` 中 `validateCreateForm` 返回首条错误文案后,经 `showToast` 展示并在 3 秒后消失;`CreatePage.tsx` 各字段无 `aria-invalid`、错误文案或错误边框态。 +- 影响:用户提交长表单后无法快速定位未填项,需记忆 Toast 内容或在多卡片间逐字段排查,显著增加完成新增任务的难度;不符合 Nielsen「错误识别与恢复」及 DESIGN.md 对校验失败使用错误色 `#ef4444` 的语义约定。 +- 修复方向:提交时在首个无效字段旁展示 inline 错误,并为对应控件设置 `aria-invalid` + `aria-describedby`;校验失败后将焦点移至首个错误字段;保留 Toast 作为成功态补充即可。 + +### P1 - 必填标识对屏幕阅读器不可感知 + +- 证据:`CreatePage.tsx` 中 `FormField` 的必填星号使用 `aria-hidden`;关联的 `input`/`select` 未设置 `aria-required="true"`。 +- 影响:依赖读屏或键盘的用户无法在进入字段前获知必填约束,提交失败后只能依赖 Toast 间接推断,构成 WCAG 可感知性缺口。 +- 修复方向:移除星号上的 `aria-hidden`,在必填控件上添加 `aria-required="true"`;可选在标签文案中追加「(必填)」供读屏朗读;星号保留视觉样式并配合 `aria-hidden` 仅当标签已含文字说明时。 + +### P2 - 状态徽章与客户标签硬编码非 OneOS 语义色 + +- 证据:`StatusBadge.tsx` 内联 `#00b42a` / `#f53f3f` / `#86909c`;`LabelTags.tsx` 内联 `#f77234` 橙色描边与 `#1d2129` 文字色,未使用 `--oneos-*` 或 `--ln-*` 令牌。 +- 影响:与 DESIGN.md「使用 CSS 变量 `--oneos-*`,不要硬编码色值」及「不要引入与 Emerald 主色冲突的第三方色板」冲突;列表中标签橙色与系统 Emerald 主色并置,削弱品牌一致性与状态色语义(成功/错误/警告)的可学习性。 +- 修复方向:状态点改用 `var(--oneos-success)` / `var(--oneos-error)` / `var(--oneos-muted)` 或等效 `--ln-*` 桥接变量;标签样式复用 `oneos-app.css` 中已有 tag/chip 模式,或基于 `--oneos-primary-light` 背景 + `--oneos-primary` 描边统一视觉。 + +### P2 - 列表页面包屑未标示当前页「客户管理」 + +- 证据:列表视图 `index.tsx` 面包屑仅渲染 `业务管理`;新增页则完整展示「业务管理 / 客户管理 / 新增」且前两级可点击返回。 +- 影响:用户在列表页无法从导航结构确认当前位置;与 DESIGN.md「列表页:面包屑 + 筛选区 + …」及 PRD 页面结构约定不一致,也弱化了「用户控制与自由」中的位置感知。 +- 修复方向:列表页补全为「业务管理 / 客户管理」,当前级使用 `cm-breadcrumb-current` 高亮;「业务管理」在具备上级路由时可设为链接,与新增页交互保持一致。 + +### P3 - 17 列宽表首屏信息密度过高,缺少列管理能力 + +- 证据:`styles/index.css` 设定 `.cm-table { min-width: 2240px; }`,表格含 17 个数据列;虽有个别 sticky 列,但中间字段(信用代码、地址等)需大量横向滚动才能浏览。 +- 影响:销售/运营在对比客户核心字段(名称、状态、负责人)时 extraneous cognitive load 偏高;存在可用绕行(横向滚动 + sticky),但不利于日常高频扫读。 +- 修复方向:原型阶段可标注默认可见列集合并提供「列设置」入口;或将次要工商字段折叠进行内「详情」抽屉,列表保留 8–10 个高频列,与 DESIGN.md「数据密度适中」目标对齐。 + +## 核心元件 + +### 筛选区(FilterPanel) + +符合 DESIGN.md 列表页「筛选区 + 工具栏 + 表格 + 分页」结构。标题 `h1.cm-page-title` 承担页面主标题职责,4 项默认展示 + 「更多筛选」折叠策略合理,查询/重置与 `pendingFilters` / `appliedFilters` 分离符合 PRD。保留点:部门-负责人联动、日期范围 `aria-label` 完整。调整点:筛选卡片与列表面包屑之间层级关系可再梳理(标题在筛选卡片内、面包屑在其外,略分散)。 + +### 工具栏与表格(CustomerTable + TablePagination) + +主操作「新增客户」使用 `vm-btn-primary` 且带图标+文字,符合 DESIGN.md 主按钮约定;批量导入/导出为 secondary/ghost 层级清晰。表格空态文案「暂无符合条件的客户」存在,但缺少引导操作(如「新增客户」链接)。行内操作「详情 | 编辑 | 删除」链接化呈现符合中后台惯例;删除使用 `cm-link-danger` 指向错误色,语义正确。分页与「共 N 条」footer 布局清晰。宽表 sticky 左右列实现到位,但列数过多见 P3。 + +### 新增表单(CreatePage) + +三组卡片(客户信息 / 联系人 / 开票)分组符合 PRD 与认知 chunking 原则;三列网格在 960px / 640px 有响应式断点。省-市、部门-负责人 disabled 联动、发票抬头只读同步逻辑正确。底部 sticky footer 固定「重置 / 提交」符合中后台表单模式。调整点:校验与必填可感知性见 P1;`cm-create-footer` 使用负 margin 对齐父级 padding,对 padding 变更较脆弱,后续可改为与 `.vm-page` 内边距一致的 layout token。 + +### 状态与标签(StatusBadge / LabelTags) + +状态徽章圆点 + 文案结构清晰,但色值未走设计令牌(见 P2)。LabelTags「首标签 + +N 悬浮」符合 PRD;`+N` 支持 focus 展开 tooltip,键盘可达性优于纯 hover。标签橙色硬编码与 OneOS Emerald 体系脱节,建议统一为系统 tag 样式。 + +### 面包屑与全局反馈 + +新增页面包屑可点击返回、当前页高亮,表现良好。Toast 使用 `role="status"` + `aria-live="polite"`,成功/演示提示合适;但不宜作为表单校验的唯一错误通道。列表页面包屑不完整见 P2。 + +## 响应式与可访问性 + +- **响应式**:新增表单网格在 ≤960px / ≤640px 降为 2 列 / 1 列;列表表格依赖横向滚动,无移动端列精简方案,窄屏下操作成本较高(中后台可接受但需知晓)。 +- **键盘**:`vm-btn`、分页、FilterPicker 等复用组件具备 `:focus-visible` 样式;MultiSelectField 的 `role="listbox"` 与 checkbox 组合非标准 listbox 模式,读屏语义可能混淆(未列入 TOP5,建议后续改为 `role="group"` + checkbox 或 Combobox 模式)。 +- **对比度**:正文与背景通过 `--ln-*` 桥接,整体可读;LabelTags / StatusBadge 硬编码色未经验证对比度。 +- **动效**:筛选 Chevron 旋转与 `prefers-reduced-motion: reduce` 降级已在模块 CSS 中处理。 +- **检查结论**:未发现阻断级 a11y 缺陷,但表单必填感知与错误恢复需优先补强。 + +## 证据与评估说明 + +- **浏览器/截图**:未使用(当前会话无预览环境接入)。 +- **Scanner**:已运行 `rules/references/impeccable/scripts/detect.mjs --json src/prototypes/customer-management`,结果为 `[]`(0 条自动化发现)。 +- **独立评估**:degraded — 设计评估基于源码、DESIGN.md 与 PRD;证据评估仅含 CLI scanner,缺少浏览器可视化与响应式实测,结论以静态审查为主。 diff --git a/src/prototypes/customer-management/annotation-source.json b/src/prototypes/customer-management/annotation-source.json index cfc8ea8..2594d62 100644 --- a/src/prototypes/customer-management/annotation-source.json +++ b/src/prototypes/customer-management/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "customer-management", "pageId": "list", - "updatedAt": 1782871095268, + "updatedAt": 1782896901748, "nodes": [ { "id": "cm-list-filter", @@ -28,8 +28,28 @@ "updatedAt": 1740787200000 }, { - "id": "cm-list-toolbar", + "id": "cm-risk-role-switch", "index": 2, + "title": "操作角色切换", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"cm-risk-role-switch\"]" + ], + "fingerprint": "cm-risk-role-switch", + "path": [] + }, + "aiPrompt": "模拟法务/财务/运维/风控/销售角色,驱动打标权限。", + "annotationText": "切换角色后抽屉内标签类型与可选标签联动更新。", + "hasMarkdown": true, + "color": "#64748b", + "images": [], + "createdAt": 1740787200000, + "updatedAt": 1740787200000 + }, + { + "id": "cm-list-toolbar", + "index": 3, "title": "列表工具栏", "pageId": "list", "locator": { @@ -39,8 +59,8 @@ "fingerprint": "cm-list-toolbar", "path": [] }, - "aiPrompt": "新增、导入、导出入口。", - "annotationText": "「新增客户」进入分组表单页。", + "aiPrompt": "新增、导入、导出、批量打标入口。", + "annotationText": "勾选客户后显示「批量打标」。", "hasMarkdown": true, "color": "#0f766e", "images": [], @@ -49,7 +69,7 @@ }, { "id": "cm-list-table", - "index": 3, + "index": 4, "title": "客户台账列表", "pageId": "list", "locator": { @@ -59,17 +79,77 @@ "fingerprint": "cm-list-table", "path": [] }, - "aiPrompt": "客户台账表格列与行操作。", - "annotationText": "", + "aiPrompt": "客户台账表格列与行操作,含风险标签列。", + "annotationText": "操作列含「管理标签」;标签列按风险级别着色。", "hasMarkdown": true, "color": "#2563eb", "images": [], "createdAt": 1740787200000, "updatedAt": 1740787200000 }, + { + "id": "cm-risk-tag-drawer", + "index": 5, + "title": "风险标签管理抽屉", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"cm-risk-tag-drawer\"]" + ], + "fingerprint": "cm-risk-tag-drawer", + "path": [] + }, + "aiPrompt": "单客户打标、审批、移除、日志。", + "annotationText": "仅列表页维护标签,不在新增/编辑表单维护。", + "hasMarkdown": true, + "color": "#d97706", + "images": [], + "createdAt": 1740787200000, + "updatedAt": 1740787200000 + }, + { + "id": "cm-batch-risk-tag-btn", + "index": 6, + "title": "批量打标入口", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"cm-batch-risk-tag-btn\"]" + ], + "fingerprint": "cm-batch-risk-tag-btn", + "path": [] + }, + "aiPrompt": "勾选多客户后批量提交同一标签。", + "annotationText": "", + "hasMarkdown": true, + "color": "#0f766e", + "images": [], + "createdAt": 1740787200000, + "updatedAt": 1740787200000 + }, + { + "id": "cm-batch-risk-tag-modal", + "index": 7, + "title": "批量打标弹窗", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"cm-batch-risk-tag-modal\"]" + ], + "fingerprint": "cm-batch-risk-tag-modal", + "path": [] + }, + "aiPrompt": "批量打标表单与提交逻辑。", + "annotationText": "", + "hasMarkdown": true, + "color": "#0f766e", + "images": [], + "createdAt": 1740787200000, + "updatedAt": 1740787200000 + }, { "id": "cm-create-base", - "index": 4, + "index": 8, "title": "客户信息", "pageId": "create", "locator": { @@ -79,8 +159,8 @@ "fingerprint": "cm-create-base", "path": [] }, - "aiPrompt": "客户类型、分级、全称、城市、部门负责人等。", - "annotationText": "客户全称变更时同步发票抬头。", + "aiPrompt": "客户类型、全称、城市、部门负责人、合作状态等。", + "annotationText": "通讯地址自动识别省市;不含风险标签字段。", "hasMarkdown": true, "color": "#2563eb", "images": [], @@ -89,7 +169,7 @@ }, { "id": "cm-create-contact", - "index": 5, + "index": 9, "title": "联系人信息", "pageId": "create", "locator": { @@ -109,7 +189,7 @@ }, { "id": "cm-create-invoice", - "index": 6, + "index": 10, "title": "付款及开票信息", "pageId": "create", "locator": { @@ -129,7 +209,7 @@ }, { "id": "cm-create-footer", - "index": 7, + "index": 11, "title": "底部操作栏", "pageId": "create", "locator": { @@ -150,187 +230,21 @@ ] }, "markdownMap": { - "cm-list-filter": "## 4. 筛选区\n\n默认展示前 4 项(一行),点击「更多筛选」显示全部。\n\n| 字段 | 控件 | 匹配规则 |\n|---|---|---|\n| 客户编号 | 可搜索单选 | 精确匹配 `code` |\n| 客户名称 | 可搜索单选 | 精确匹配 `name` |\n| 标签 | 多选 | 客户 `labels` 包含任一选中标签 |\n| 区域 | 可搜索单选 | 精确匹配 `region` |\n| 客户状态 | 多选 | `status` 在选中集合内 |\n| 业务部门 | 多选 | `department` 在选中集合内 |\n| 业务负责人 | 多选 | 随部门联动选项;`manager` 在选中集合内 |\n| 创建时间 | 日期范围 | `createTime` 落在区间内(含边界日) |\n\n### 4.1 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入已应用条件,页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n---", - "cm-list-toolbar": "## 5. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 新增客户 | 进入新增客户页(分组表单) |\n| 批量导入 | Toast:批量导入(原型演示) |\n| 批量导出 | Toast:导出当前列表条数 |\n\n---", - "cm-list-table": "## 6. 列表字段说明\n\n| 列 | 说明 |\n|---|---|\n| 客户编号 | 左固定列 |\n| 客户名称 | 超长省略 |\n| 标签 | 仅展示第一个标签;多于 1 个显示「+N」,悬浮展示全部 |\n| 客户区域 | 大区名称 |\n| 客户状态 | 绿点「正常」/ 红点「黑名单」 |\n| 业务部门 / 业务负责人 | 文本 |\n| 客户类型 / 所属行业 | 文本 |\n| 统一社会信用代码 / 法人 / 注册资本 / 经营状态 | 文本 |\n| 详细地址 / 创建时间 | 文本;创建时间不换行 |\n| 操作 | 详情、编辑、删除(原型 Toast) |\n\n支持行多选(checkbox);横向滚动;操作列右固定。\n\n---", - "cm-create-base": "## 2. 客户信息\n\n| 字段 | 必填 | 控件 | 说明 |\n|---|---|---|---|\n| 客户类型 | 是 | 下拉 | 企业 / 个人 / 事业单位 |\n| 客户分级 | 否 | 下拉 | A / B / C |\n| 客户全称 | 是 | 文本 | 与客户证照一致;变更时同步发票抬头 |\n| 客户简称 | 否 | 文本 | 选填 |\n| 所属城市 | 是 | 省-市级联 | 示例省市区数据 |\n| 通讯地址 | 否 | 文本 | |\n| 业务负责部门 | 是 | 下拉 | 变更时清空负责人 |\n| 业务负责人员 | 是 | 下拉 | 随部门联动,未选部门时禁用 |\n| 备注 | 否 | 多行文本 | |\n\n---", - "cm-create-contact": "## 3. 联系人信息\n\n| 字段 | 必填 | 控件 |\n|---|---|---|\n| 姓名 | 否 | 文本 |\n| 手机号 | 否 | 文本 |\n| 职位 | 否 | 文本 |\n\n---", - "cm-create-invoice": "## 4. 付款及开票信息\n\n| 字段 | 必填 | 控件 | 说明 |\n|---|---|---|---|\n| 发票抬头 | 是 | 文本(禁用) | 自动带入客户全称,不可手改 |\n| 纳税人识别号 | 是 | 文本 | |\n| 注册地址 | 否 | 文本 | |\n| 注册电话 | 否 | 文本 | |\n| 开户银行 | 是 | 文本 | |\n| 账号 | 否 | 文本 | |\n\n---", - "cm-create-footer": "## 5. 校验与提交\n\n- 提交前校验所有必填项\n- 通过后 Toast「客户已保存(原型演示)」并返回列表\n- 重置清空全部字段,客户类型默认「企业」" + "cm-list-filter": "## 4. 筛选区\r\n\r\n默认展示前 4 项(一行),点击「更多筛选」显示全部。\r\n\r\n| 字段 | 控件 | 匹配规则 |\r\n|---|---|---|\r\n| 客户编号 | 可搜索单选 | 精确匹配 `code` |\r\n| 客户名称 | 可搜索单选 | 精确匹配 `name` |\r\n| 标签 | 多选 | 客户 `labels` 包含任一选中标签(OR) |\r\n| 区域 | 可搜索单选 | 精确匹配 `region` |\r\n| 客户状态 | 多选 | `status` 在选中集合内 |\r\n| 业务部门 | 多选 | `department` 在选中集合内 |\r\n| 业务负责人 | 多选 | 随部门联动选项;`manager` 在选中集合内 |\r\n| 创建时间 | 日期范围 | `createTime` 落在区间内(含边界日) |\r\n\r\n### 4.1 操作逻辑\r\n\r\n| 操作 | 行为 |\r\n|---|---|\r\n| **查询** | 将当前筛选项写入已应用条件,页码重置为 1,清空行选中 |\r\n| **重置** | 清空待应用与已应用条件,页码重置为 1,清空行选中 |\r\n| 修改筛选项未点查询 | 不影响当前列表 |\r\n\r\n---", + "cm-risk-role-switch": "### 5.1 操作角色切换(原型演示)\r\n\r\n列表顶部提供「当前操作角色」下拉,用于模拟不同部门用户:\r\n\r\n| 角色 | 部门 | 打标权限 |\r\n|---|---|---|\r\n| 王风控 | 风控部 | 全部标签 |\r\n| 李法务 | 法务部 | 法务类标签 |\r\n| 张财务 | 财务部 | 财务类标签 |\r\n| 赵运维 | 运维部 | 运维类标签 |\r\n| 陈销售 | 销售一部 | 仅查看,无打标入口 |\r\n\r\n切换角色后,风险标签管理抽屉内的**标签类型**、**可选标签**、**审批/移除按钮**随之联动更新。\r\n\r\n---", + "cm-list-toolbar": "## 5. 列表工具栏\r\n\r\n| 按钮 | 行为 |\r\n|---|---|\r\n| 新增客户 | 进入新增客户页(分组表单) |\r\n| 批量导入 | Toast:批量导入(原型演示) |\r\n| **批量打标** | 勾选 ≥1 行且当前角色有打标权限时显示;打开批量打标弹窗 |\r\n| 批量导出 | Toast:导出当前列表条数 |\r\n\r\n### 5.1 操作角色切换(原型演示)\r\n\r\n列表顶部提供「当前操作角色」下拉,用于模拟不同部门用户:\r\n\r\n| 角色 | 部门 | 打标权限 |\r\n|---|---|---|\r\n| 王风控 | 风控部 | 全部标签 |\r\n| 李法务 | 法务部 | 法务类标签 |\r\n| 张财务 | 财务部 | 财务类标签 |\r\n| 赵运维 | 运维部 | 运维类标签 |\r\n| 陈销售 | 销售一部 | 仅查看,无打标入口 |\r\n\r\n切换角色后,风险标签管理抽屉内的**标签类型**、**可选标签**、**审批/移除按钮**随之联动更新。\r\n\r\n---", + "cm-list-table": "## 6. 列表字段说明\r\n\r\n| 列 | 说明 |\r\n|---|---|\r\n| 客户编号 | 左固定列 |\r\n| 客户名称 | 超长省略 |\r\n| **风险标签** | 展示「生效中」标签;按风险级别着色(低/中/高/极高);首标签 + `+N` 悬浮全部;有待审批时显示「审」标识;可点击打开管理抽屉 |\r\n| 客户区域 | 大区名称 |\r\n| 客户状态 | 绿点「正常」/ 红点「黑名单」 |\r\n| 业务部门 / 业务负责人 | 文本 |\r\n| 客户类型 / 所属行业 | 文本 |\r\n| 统一社会信用代码 / 法人 / 注册资本 / 经营状态 | 文本 |\r\n| 详细地址 / 创建时间 | 文本;创建时间不换行 |\r\n| 操作 | 详情、编辑、**管理标签**(有权限时)、删除(原型 Toast) |\r\n\r\n支持行多选(checkbox,仅当前页);横向滚动;操作列右固定。\r\n\r\n---", + "cm-risk-tag-drawer": "## 8. 风险标签维护(列表页专属)\r\n\r\n### 8.1 入口\r\n\r\n| 入口 | 说明 |\r\n|---|---|\r\n| 行操作「管理标签」 | 打开右侧抽屉,管理单个客户 |\r\n| 点击标签列 | 有权限时同「管理标签」 |\r\n| 工具栏「批量打标」 | 对当前勾选客户批量提交同一标签 |\r\n\r\n**不在**新增页、编辑页维护标签。\r\n\r\n### 8.2 管理抽屉结构\r\n\r\n| Tab | 内容 |\r\n|---|---|\r\n| 当前标签 | 已打标签列表:名称、类型、级别、状态、原因摘要;支持查看详情、移除、审批 |\r\n| 新增标签 | 打标表单(见 8.3) |\r\n| 操作日志 | 该客户全部标签相关审计记录 |\r\n\r\n### 8.3 打标表单字段\r\n\r\n| 字段 | 必填 | 规则 |\r\n|---|---|---|\r\n| 标签名称 | 是 | 按 7.1–7.4 分组下拉;仅当前角色有权限的标签可选;已存在同名有效标签不可重复选 |\r\n| 标签类型 | 是 | **根据当前操作角色自动带出**(财务→财务、法务→法务、运维→运维、风控→风控),**置灰不可编辑** |\r\n| 风险级别 | 是 | **选择标签名称后按目录自动带出**,**置灰不可编辑** |\r\n| 生效时间 | 是 | 日期,默认当天 |\r\n| 有效期至 | 是 | 日期,须 ≥ 生效时间;默认一年后 |\r\n| 原因说明 | 是 | 至少 10 字,供法务/财务/风控审批留痕 |\r\n| 附件 | 否 | 支持 .pdf / .jpg / .png / .xlsx,可多个 |\r\n\r\n### 8.4 批量打标\r\n\r\n- 展示已选客户 chips(超过 6 个显示 +N)\r\n- 表单字段与单客户打标一致\r\n- 已存在同名有效标签的客户自动跳过\r\n- 提交后写入批量操作日志\r\n\r\n### 8.5 标签生命周期\r\n\r\n| 状态 | 说明 |\r\n|---|---|\r\n| 生效中 | 参与列表展示,同步至 `labels` |\r\n| 待审批 | 高风险标签提交后进入;列表暂不展示为生效 |\r\n| 待确认 | 预留 |\r\n| 已驳回 | 审批未通过,不同步至 `labels` |\r\n| 已移除 | 操作人主动移除,需填写原因 |\r\n| 已过期 | 预留 |\r\n\r\n**高风险规则:** 风险级别为「高」或「极高」的标签提交后状态为「待审批」,须法务或风控审批通过后变为「生效中」。\r\n\r\n### 8.6 移除标签\r\n\r\n- 仅「生效中」且当前角色有移除权限的标签可移除\r\n- 须填写移除原因(至少 5 字)\r\n- 写入操作日志,并从列表 `labels` 同步清除\r\n\r\n---", + "cm-batch-risk-tag-btn": "### 8.1 入口\r\n\r\n| 入口 | 说明 |\r\n|---|---|\r\n| 行操作「管理标签」 | 打开右侧抽屉,管理单个客户 |\r\n| 点击标签列 | 有权限时同「管理标签」 |\r\n| 工具栏「批量打标」 | 对当前勾选客户批量提交同一标签 |\r\n\r\n**不在**新增页、编辑页维护标签。", + "cm-batch-risk-tag-modal": "### 8.4 批量打标\r\n\r\n- 展示已选客户 chips(超过 6 个显示 +N)\r\n- 表单字段与单客户打标一致\r\n- 已存在同名有效标签的客户自动跳过\r\n- 提交后写入批量操作日志", + "cm-create-base": "## 2. 客户信息\r\n\r\n| 字段 | 必填 | 控件 | 说明 |\r\n|---|---|---|---|\r\n| 客户类型 | 是 | 下拉 | 企业 / 个人 / 事业单位 |\r\n| 客户分级 | 否 | 下拉 | A / B / C |\r\n| 客户全称 | 是 | 文本 | 与客户证照一致;变更时同步发票抬头 |\r\n| 客户简称 | 否 | 文本 | 选填 |\r\n| 所属城市 | 是 | 省-市级联 | 示例省市区数据 |\r\n| 通讯地址 | 否 | 文本 | |\r\n| 业务负责部门 | 是 | 下拉 | 变更时清空负责人 |\r\n| 业务负责人员 | 是 | 下拉 | 随部门联动,未选部门时禁用 |\r\n| 备注 | 否 | 多行文本 | |\r\n\r\n---", + "cm-create-contact": "## 3. 联系人信息\r\n\r\n| 字段 | 必填 | 控件 |\r\n|---|---|---|\r\n| 姓名 | 否 | 文本 |\r\n| 手机号 | 否 | 文本 |\r\n| 职位 | 否 | 文本 |\r\n\r\n---", + "cm-create-invoice": "## 4. 付款及开票信息\r\n\r\n| 字段 | 必填 | 控件 | 说明 |\r\n|---|---|---|---|\r\n| 发票抬头 | 是 | 文本(禁用) | 自动带入客户全称,不可手改 |\r\n| 纳税人识别号 | 是 | 文本 | |\r\n| 注册地址 | 否 | 文本 | |\r\n| 注册电话 | 否 | 文本 | |\r\n| 开户银行 | 是 | 文本 | |\r\n| 账号 | 否 | 文本 | |\r\n\r\n---", + "cm-create-footer": "## 5. 校验与提交\r\n\r\n- 提交前校验所有必填项\r\n- 通过后 Toast「客户已保存(原型演示)」并返回列表\r\n- 重置清空全部字段,客户类型默认「企业」" }, "assetMap": {}, "directory": { "nodes": [ - { - "type": "folder", - "id": "oneos-project-nav", - "title": "ONE-OS 原型导航", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-contract-template-management", - "title": "合同模板管理", - "href": "/prototypes/contract-template-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-insurance-procurement", - "title": "保险采购", - "href": "/prototypes/insurance-procurement", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-vehicle-management", - "title": "车辆管理", - "href": "/prototypes/vehicle-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-lease-contract-management", - "title": "租赁合同管理", - "href": "/prototypes/lease-contract-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-customer-management", - "title": "客户管理(当前)", - "href": "/prototypes/customer-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-lease-business-ledger", - "title": "租赁业务台账", - "href": "/prototypes/lease-business-ledger", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-vehicle-h2-fee-ledger", - "title": "车辆氢费明细", - "href": "/prototypes/vehicle-h2-fee-ledger", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-self-operated-business-ledger", - "title": "自营业务台账", - "href": "/prototypes/self-operated-business-ledger", - "target": "self" - }, - { - "type": "folder", - "id": "nav-folder-prototypes-oneos-web", - "title": "ONE-OS Web 端", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-workbench", - "title": "工作台", - "href": "/prototypes/oneos-web-workbench", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-login", - "title": "登录", - "href": "/prototypes/oneos-web-login", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-vehicle-asset", - "title": "车辆管理", - "href": "/prototypes/oneos-web-vehicle-asset", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-contract-template", - "title": "合同模板管理", - "href": "/prototypes/oneos-web-contract-template", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-help-center", - "title": "帮助中心", - "href": "/prototypes/oneos-web-help-center", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-finance", - "title": "财务管理", - "href": "/prototypes/oneos-web-finance", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-procurement", - "title": "采购管理", - "href": "/prototypes/oneos-web-procurement", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-lease-contract", - "title": "车辆租赁合同", - "href": "/prototypes/oneos-web-lease-contract", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-h2-station", - "title": "加氢站管理", - "href": "/prototypes/oneos-web-h2-station", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-data-analysis", - "title": "数据分析", - "href": "/prototypes/oneos-web-data-analysis", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-ledger-data", - "title": "台账数据", - "href": "/prototypes/oneos-web-ledger-data", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-business", - "title": "业务管理", - "href": "/prototypes/oneos-web-business", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-ops", - "title": "运维管理", - "href": "/prototypes/oneos-web-ops", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-requirements", - "title": "需求说明", - "href": "/prototypes/oneos-web-requirements", - "target": "self" - } - ] - } - ] - }, { "type": "folder", "id": "cm-doc-root", @@ -341,13 +255,13 @@ "type": "markdown", "id": "cm-doc-overview", "title": "模块总览", - "markdown": "## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 客户管理 — 列表页 |\n| 目标用户 | 销售、业务负责人、运营、风控 |\n| 核心任务 | 按多维度筛选客户台账,查看标签与状态,新增客户或进入详情/编辑 |\n| 页面结构 | 面包屑 + 筛选区 + 工具栏 + 表格 + 分页 |\n\n---" + "markdown": "## 1. 模块定位\r\n\r\n| 项 | 说明 |\r\n|---|---|\r\n| 模块名称 | 客户管理 — 列表页 |\r\n| 目标用户 | 销售、业务负责人、运营、**法务、财务、风控** |\r\n| 核心任务 | 按多维度筛选客户台账,查看风险标签与状态,**在列表页维护客户风险标签**,新增客户或进入详情/编辑 |\r\n| 页面结构 | 面包屑 + **操作角色切换** + 筛选区 + 工具栏 + 表格 + 分页 |\r\n\r\n**重要约束:** 风险标签**不在**客户新增页、客户编辑表单中维护,仅在列表页作为操作入口。\r\n\r\n---" }, { "type": "link", "id": "cm-link-self", "title": "客户管理原型", - "href": "./index.html", + "href": "/prototypes/customer-management", "target": "self" }, { @@ -360,13 +274,13 @@ "type": "markdown", "id": "cm-doc-prd-list", "title": "列表页 PRD", - "markdown": "# 客户管理 · 列表页产品需求说明(PRD)\n\n> 本页为客户主数据台账列表。标注「原型演示」的跳转、导入、导出仅 Toast 提示。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 客户管理 — 列表页 |\n| 目标用户 | 销售、业务负责人、运营、风控 |\n| 核心任务 | 按多维度筛选客户台账,查看标签与状态,新增客户或进入详情/编辑 |\n| 页面结构 | 面包屑 + 筛选区 + 工具栏 + 表格 + 分页 |\n\n---\n\n## 2. 客户编号规则(集团多子公司)\n\n- 总长度 7,仅含大写字母 A–Z 与数字 0–9\n- 结构:第 1 位 = 签约/归属子公司代码;第 2–7 位 = 6 位数字流水号(000001–999999)\n- 流水号在「同一子公司代码」下递增\n- 校验正则:`/^[A-Z][0-9]{6}$/`\n- 示例:A000001(华东)、B000001(华南)、C000001(华北)等\n\n---\n\n## 3. 数据来源\n\n| 数据 | 来源 | 用途 |\n|---|---|---|\n| 客户台账 | `data/customers.json`(20 条样例) | 列表、筛选选项 |\n| 客户标签 | `CUSTOMER_LABEL_OPTIONS` 常量 | 筛选与列表展示 |\n| 部门-负责人 | 由台账聚合 + `DEPT_MANAGER_MAP` | 筛选联动 |\n\n---\n\n## 4. 筛选区\n\n默认展示前 4 项(一行),点击「更多筛选」显示全部。\n\n| 字段 | 控件 | 匹配规则 |\n|---|---|---|\n| 客户编号 | 可搜索单选 | 精确匹配 `code` |\n| 客户名称 | 可搜索单选 | 精确匹配 `name` |\n| 标签 | 多选 | 客户 `labels` 包含任一选中标签 |\n| 区域 | 可搜索单选 | 精确匹配 `region` |\n| 客户状态 | 多选 | `status` 在选中集合内 |\n| 业务部门 | 多选 | `department` 在选中集合内 |\n| 业务负责人 | 多选 | 随部门联动选项;`manager` 在选中集合内 |\n| 创建时间 | 日期范围 | `createTime` 落在区间内(含边界日) |\n\n### 4.1 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入已应用条件,页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n---\n\n## 5. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 新增客户 | 进入新增客户页(分组表单) |\n| 批量导入 | Toast:批量导入(原型演示) |\n| 批量导出 | Toast:导出当前列表条数 |\n\n---\n\n## 6. 列表字段说明\n\n| 列 | 说明 |\n|---|---|\n| 客户编号 | 左固定列 |\n| 客户名称 | 超长省略 |\n| 标签 | 仅展示第一个标签;多于 1 个显示「+N」,悬浮展示全部 |\n| 客户区域 | 大区名称 |\n| 客户状态 | 绿点「正常」/ 红点「黑名单」 |\n| 业务部门 / 业务负责人 | 文本 |\n| 客户类型 / 所属行业 | 文本 |\n| 统一社会信用代码 / 法人 / 注册资本 / 经营状态 | 文本 |\n| 详细地址 / 创建时间 | 文本;创建时间不换行 |\n| 操作 | 详情、编辑、删除(原型 Toast) |\n\n支持行多选(checkbox);横向滚动;操作列右固定。\n\n---\n\n## 7. 客户标签枚举\n\n逾期预警、严重拖欠、频繁改账期、资质存疑、涉诉/仲裁、制裁/高风险地区、频繁争议、车辆/资产高风险、信息不实、开票异常、多头签约。\n\n标签样式:白底 + 橙色描边(`#f77234`)。\n" + "markdown": "# 客户管理 · 列表页产品需求说明(PRD)\r\n\r\n> 本页为客户主数据台账列表。标注「原型演示」的跳转、导入、导出仅 Toast 提示。\r\n\r\n---\r\n\r\n## 1. 模块定位\r\n\r\n| 项 | 说明 |\r\n|---|---|\r\n| 模块名称 | 客户管理 — 列表页 |\r\n| 目标用户 | 销售、业务负责人、运营、**法务、财务、风控** |\r\n| 核心任务 | 按多维度筛选客户台账,查看风险标签与状态,**在列表页维护客户风险标签**,新增客户或进入详情/编辑 |\r\n| 页面结构 | 面包屑 + **操作角色切换** + 筛选区 + 工具栏 + 表格 + 分页 |\r\n\r\n**重要约束:** 风险标签**不在**客户新增页、客户编辑表单中维护,仅在列表页作为操作入口。\r\n\r\n---\r\n\r\n## 2. 客户编号规则(集团多子公司)\r\n\r\n- 总长度 7,仅含大写字母 A–Z 与数字 0–9\r\n- 结构:第 1 位 = 签约/归属子公司代码;第 2–7 位 = 6 位数字流水号(000001–999999)\r\n- 流水号在「同一子公司代码」下递增\r\n- 校验正则:`/^[A-Z][0-9]{6}$/`\r\n- 示例:A000001(华东)、B000001(华南)、C000001(华北)等\r\n\r\n---\r\n\r\n## 3. 数据来源\r\n\r\n| 数据 | 来源 | 用途 |\r\n|---|---|---|\r\n| 客户台账 | `data/customers.json`(20 条样例) | 列表、筛选选项 |\r\n| 客户标签(展示) | 客户 `labels` 字段 | 列表「风险标签」列、筛选区「标签」多选 |\r\n| 风险标签明细 | `riskTagsByCustomer` 运行时状态 + 种子迁移 | 标签管理抽屉、审批、日志 |\r\n| 标签目录 | `utils/riskTagConstants.ts` → `TAG_CATALOG` | 打标表单选项、级别、分组 |\r\n| 操作日志 | `data/risk-tag-operation-logs.json` + 运行时追加 | 抽屉「操作日志」Tab |\r\n| 部门-负责人 | 由台账聚合 + `DEPT_MANAGER_MAP` | 筛选联动 |\r\n\r\n列表 `labels` 与「生效中」风险标签自动同步,保证筛选器与列表展示一致。\r\n\r\n---\r\n\r\n## 4. 筛选区\r\n\r\n默认展示前 4 项(一行),点击「更多筛选」显示全部。\r\n\r\n| 字段 | 控件 | 匹配规则 |\r\n|---|---|---|\r\n| 客户编号 | 可搜索单选 | 精确匹配 `code` |\r\n| 客户名称 | 可搜索单选 | 精确匹配 `name` |\r\n| 标签 | 多选 | 客户 `labels` 包含任一选中标签(OR) |\r\n| 区域 | 可搜索单选 | 精确匹配 `region` |\r\n| 客户状态 | 多选 | `status` 在选中集合内 |\r\n| 业务部门 | 多选 | `department` 在选中集合内 |\r\n| 业务负责人 | 多选 | 随部门联动选项;`manager` 在选中集合内 |\r\n| 创建时间 | 日期范围 | `createTime` 落在区间内(含边界日) |\r\n\r\n### 4.1 操作逻辑\r\n\r\n| 操作 | 行为 |\r\n|---|---|\r\n| **查询** | 将当前筛选项写入已应用条件,页码重置为 1,清空行选中 |\r\n| **重置** | 清空待应用与已应用条件,页码重置为 1,清空行选中 |\r\n| 修改筛选项未点查询 | 不影响当前列表 |\r\n\r\n---\r\n\r\n## 5. 列表工具栏\r\n\r\n| 按钮 | 行为 |\r\n|---|---|\r\n| 新增客户 | 进入新增客户页(分组表单) |\r\n| 批量导入 | Toast:批量导入(原型演示) |\r\n| **批量打标** | 勾选 ≥1 行且当前角色有打标权限时显示;打开批量打标弹窗 |\r\n| 批量导出 | Toast:导出当前列表条数 |\r\n\r\n### 5.1 操作角色切换(原型演示)\r\n\r\n列表顶部提供「当前操作角色」下拉,用于模拟不同部门用户:\r\n\r\n| 角色 | 部门 | 打标权限 |\r\n|---|---|---|\r\n| 王风控 | 风控部 | 全部标签 |\r\n| 李法务 | 法务部 | 法务类标签 |\r\n| 张财务 | 财务部 | 财务类标签 |\r\n| 赵运维 | 运维部 | 运维类标签 |\r\n| 陈销售 | 销售一部 | 仅查看,无打标入口 |\r\n\r\n切换角色后,风险标签管理抽屉内的**标签类型**、**可选标签**、**审批/移除按钮**随之联动更新。\r\n\r\n---\r\n\r\n## 6. 列表字段说明\r\n\r\n| 列 | 说明 |\r\n|---|---|\r\n| 客户编号 | 左固定列 |\r\n| 客户名称 | 超长省略 |\r\n| **风险标签** | 展示「生效中」标签;按风险级别着色(低/中/高/极高);首标签 + `+N` 悬浮全部;有待审批时显示「审」标识;可点击打开管理抽屉 |\r\n| 客户区域 | 大区名称 |\r\n| 客户状态 | 绿点「正常」/ 红点「黑名单」 |\r\n| 业务部门 / 业务负责人 | 文本 |\r\n| 客户类型 / 所属行业 | 文本 |\r\n| 统一社会信用代码 / 法人 / 注册资本 / 经营状态 | 文本 |\r\n| 详细地址 / 创建时间 | 文本;创建时间不换行 |\r\n| 操作 | 详情、编辑、**管理标签**(有权限时)、删除(原型 Toast) |\r\n\r\n支持行多选(checkbox,仅当前页);横向滚动;操作列右固定。\r\n\r\n---\r\n\r\n## 7. 客户标签枚举与分级目录\r\n\r\n标签按业务分类维护,打标时下拉按 **optgroup 分组** 展示。风险级别由目录**自动带出且不可手改**。\r\n\r\n### 7.1 信用与回款\r\n\r\n| 标签名称 | 标签类型 | 风险级别 | 典型触发 |\r\n|---|---|---|---|\r\n| 逾期预警 | 财务 | 中 | 当前逾期或近 6 个月内曾逾期 |\r\n| 严重拖欠 | 财务 | 高 | 逾期 > 30 天未结清或历史坏账 |\r\n| 频繁改账期 | 财务 | 低 | 一年内申请延长账期 ≥ 3 次 |\r\n\r\n### 7.2 合规与法律\r\n\r\n| 标签名称 | 标签类型 | 风险级别 | 典型触发 |\r\n|---|---|---|---|\r\n| 资质存疑 | 法务 | 中 | 证照过期、抬头与付款主体不一致等 |\r\n| 涉诉/仲裁 | 法务 | 高 | 与我司或第三方重大未结诉讼 |\r\n| 制裁/高风险地区 | 法务 | 高 | 名单筛查命中(依公司合规清单) |\r\n\r\n### 7.3 经营与履约\r\n\r\n| 标签名称 | 标签类型 | 风险级别 | 典型触发 |\r\n|---|---|---|---|\r\n| 频繁争议 | 法务 | 中 | 合同或扣款争议次数超阈值 |\r\n| 车辆/资产高风险 | 运维 | 高 | 违章、事故、疑似骗保等 |\r\n| 信息不实 | 风控 | 中 | 联系人/主体核查不实、空壳公司等 |\r\n\r\n### 7.4 操作与流程\r\n\r\n| 标签名称 | 标签类型 | 风险级别 | 典型触发 |\r\n|---|---|---|---|\r\n| 开票异常 | 财务 | 低 | 频繁退票、抬头异常变更 |\r\n| 多头签约 | 风控 | 低 | 同一实控人多主体分别签约 |\r\n\r\n### 7.5 列表展示样式\r\n\r\n- 低:灰色边框\r\n- 中:橙色浅底\r\n- 高:红色浅底\r\n- 极高:深红浅底\r\n- 单元格最大宽度受限,避免撑乱表格\r\n\r\n---\r\n\r\n## 8. 风险标签维护(列表页专属)\r\n\r\n### 8.1 入口\r\n\r\n| 入口 | 说明 |\r\n|---|---|\r\n| 行操作「管理标签」 | 打开右侧抽屉,管理单个客户 |\r\n| 点击标签列 | 有权限时同「管理标签」 |\r\n| 工具栏「批量打标」 | 对当前勾选客户批量提交同一标签 |\r\n\r\n**不在**新增页、编辑页维护标签。\r\n\r\n### 8.2 管理抽屉结构\r\n\r\n| Tab | 内容 |\r\n|---|---|\r\n| 当前标签 | 已打标签列表:名称、类型、级别、状态、原因摘要;支持查看详情、移除、审批 |\r\n| 新增标签 | 打标表单(见 8.3) |\r\n| 操作日志 | 该客户全部标签相关审计记录 |\r\n\r\n### 8.3 打标表单字段\r\n\r\n| 字段 | 必填 | 规则 |\r\n|---|---|---|\r\n| 标签名称 | 是 | 按 7.1–7.4 分组下拉;仅当前角色有权限的标签可选;已存在同名有效标签不可重复选 |\r\n| 标签类型 | 是 | **根据当前操作角色自动带出**(财务→财务、法务→法务、运维→运维、风控→风控),**置灰不可编辑** |\r\n| 风险级别 | 是 | **选择标签名称后按目录自动带出**,**置灰不可编辑** |\r\n| 生效时间 | 是 | 日期,默认当天 |\r\n| 有效期至 | 是 | 日期,须 ≥ 生效时间;默认一年后 |\r\n| 原因说明 | 是 | 至少 10 字,供法务/财务/风控审批留痕 |\r\n| 附件 | 否 | 支持 .pdf / .jpg / .png / .xlsx,可多个 |\r\n\r\n### 8.4 批量打标\r\n\r\n- 展示已选客户 chips(超过 6 个显示 +N)\r\n- 表单字段与单客户打标一致\r\n- 已存在同名有效标签的客户自动跳过\r\n- 提交后写入批量操作日志\r\n\r\n### 8.5 标签生命周期\r\n\r\n| 状态 | 说明 |\r\n|---|---|\r\n| 生效中 | 参与列表展示,同步至 `labels` |\r\n| 待审批 | 高风险标签提交后进入;列表暂不展示为生效 |\r\n| 待确认 | 预留 |\r\n| 已驳回 | 审批未通过,不同步至 `labels` |\r\n| 已移除 | 操作人主动移除,需填写原因 |\r\n| 已过期 | 预留 |\r\n\r\n**高风险规则:** 风险级别为「高」或「极高」的标签提交后状态为「待审批」,须法务或风控审批通过后变为「生效中」。\r\n\r\n### 8.6 移除标签\r\n\r\n- 仅「生效中」且当前角色有移除权限的标签可移除\r\n- 须填写移除原因(至少 5 字)\r\n- 写入操作日志,并从列表 `labels` 同步清除\r\n\r\n---\r\n\r\n## 9. 权限与审批\r\n\r\n### 9.1 打标权限\r\n\r\n| 角色 | 可选标签范围 | 标签类型(自动) |\r\n|---|---|---|\r\n| 财务 | 财务类(信用与回款、操作与流程中的财务标签) | 财务 |\r\n| 法务 | 法务类(合规与法律、经营与履约中的法务标签) | 法务 |\r\n| 运维 | 运维类(车辆/资产高风险) | 运维 |\r\n| 风控 / 管理员 | 全部 | 风控 |\r\n| 销售 | 无 | — |\r\n\r\n### 9.2 审批权限\r\n\r\n- 法务、风控、管理员可审批「待审批」标签\r\n- 支持通过 / 驳回;驳回须填写原因\r\n\r\n### 9.3 移除权限\r\n\r\n与打标权限一致:各角色仅能移除本类型标签;风控 / 管理员可移除全部类型。\r\n\r\n---\r\n\r\n## 10. 操作日志\r\n\r\n所有标签 **新增、移除、提交审批、审批通过、审批驳回、批量打标** 均写入日志。\r\n\r\n| 字段 | 说明 |\r\n|---|---|\r\n| 操作类型 | 新增 / 移除 / 提交审批 / 审批通过 / 审批驳回 / 批量打标 |\r\n| 客户 | 客户名称 + ID |\r\n| 标签名称 | 涉及标签 |\r\n| 详情 | 原因、审批结论等 |\r\n| 操作人 | 姓名 + 角色 |\r\n| 操作时间 | `YYYY-MM-DD HH:mm:ss` |\r\n\r\n抽屉「操作日志」Tab 按客户过滤;原型预置样例日志见 `data/risk-tag-operation-logs.json`。\r\n\r\n---\r\n\r\n## 11. 原型实现索引\r\n\r\n| 能力 | 代码位置 |\r\n|---|---|\r\n| 列表编排 | `index.tsx` |\r\n| 风险标签列 | `components/RiskLabelTags.tsx` |\r\n| 管理抽屉 | `components/RiskTagDrawer.tsx` |\r\n| 批量打标 | `components/BatchRiskTagModal.tsx` |\r\n| 打标表单 | `components/RiskTagForm.tsx` |\r\n| 标签目录 | `utils/riskTagConstants.ts` |\r\n| 权限 | `utils/permissions.ts` |\r\n| 业务逻辑 | `utils/riskTagStore.ts` |\r\n| 类型定义 | `types/riskTag.ts` |\r\n" }, { "type": "markdown", "id": "cm-doc-prd-create", "title": "新增页 PRD", - "markdown": "# 客户管理 · 新增页产品需求说明(PRD)\n\n> 从列表页「新增客户」进入。保存为原型 Toast,不对接真实接口。\n\n---\n\n## 1. 页面框架\n\n| 项 | 说明 |\n|---|---|\n| 面包屑 | 业务管理 / 客户管理 / 新增 |\n| 布局 | 三组卡片表单 + 底部 sticky 操作栏 |\n| 操作 | 重置(清空表单)、提交(校验后 Toast 成功并返回列表) |\n\n---\n\n## 2. 客户信息\n\n| 字段 | 必填 | 控件 | 说明 |\n|---|---|---|---|\n| 客户类型 | 是 | 下拉 | 企业 / 个人 / 事业单位 |\n| 客户分级 | 否 | 下拉 | A / B / C |\n| 客户全称 | 是 | 文本 | 与客户证照一致;变更时同步发票抬头 |\n| 客户简称 | 否 | 文本 | 选填 |\n| 所属城市 | 是 | 省-市级联 | 示例省市区数据 |\n| 通讯地址 | 否 | 文本 | |\n| 业务负责部门 | 是 | 下拉 | 变更时清空负责人 |\n| 业务负责人员 | 是 | 下拉 | 随部门联动,未选部门时禁用 |\n| 备注 | 否 | 多行文本 | |\n\n---\n\n## 3. 联系人信息\n\n| 字段 | 必填 | 控件 |\n|---|---|---|\n| 姓名 | 否 | 文本 |\n| 手机号 | 否 | 文本 |\n| 职位 | 否 | 文本 |\n\n---\n\n## 4. 付款及开票信息\n\n| 字段 | 必填 | 控件 | 说明 |\n|---|---|---|---|\n| 发票抬头 | 是 | 文本(禁用) | 自动带入客户全称,不可手改 |\n| 纳税人识别号 | 是 | 文本 | |\n| 注册地址 | 否 | 文本 | |\n| 注册电话 | 否 | 文本 | |\n| 开户银行 | 是 | 文本 | |\n| 账号 | 否 | 文本 | |\n\n---\n\n## 5. 校验与提交\n\n- 提交前校验所有必填项\n- 通过后 Toast「客户已保存(原型演示)」并返回列表\n- 重置清空全部字段,客户类型默认「企业」\n" + "markdown": "# 客户管理 · 新增页产品需求说明(PRD)\r\n\r\n> 从列表页「新增客户」进入。保存为原型 Toast,不对接真实接口。\r\n\r\n---\r\n\r\n## 1. 页面框架\r\n\r\n| 项 | 说明 |\r\n|---|---|\r\n| 面包屑 | 业务管理 / 客户管理 / 新增 |\r\n| 布局 | 三组卡片表单 + 底部 sticky 操作栏 |\r\n| 操作 | 重置(清空表单)、提交(校验后 Toast 成功并返回列表) |\r\n\r\n---\r\n\r\n## 2. 客户信息\r\n\r\n| 字段 | 必填 | 控件 | 说明 |\r\n|---|---|---|---|\r\n| 客户类型 | 是 | 下拉 | 企业 / 个人 / 事业单位 |\r\n| 客户分级 | 否 | 下拉 | A / B / C |\r\n| 客户全称 | 是 | 文本 | 与客户证照一致;变更时同步发票抬头 |\r\n| 客户简称 | 否 | 文本 | 选填 |\r\n| 所属城市 | 是 | 省-市级联 | 示例省市区数据 |\r\n| 通讯地址 | 否 | 文本 | |\r\n| 业务负责部门 | 是 | 下拉 | 变更时清空负责人 |\r\n| 业务负责人员 | 是 | 下拉 | 随部门联动,未选部门时禁用 |\r\n| 备注 | 否 | 多行文本 | |\r\n\r\n---\r\n\r\n## 3. 联系人信息\r\n\r\n| 字段 | 必填 | 控件 |\r\n|---|---|---|\r\n| 姓名 | 否 | 文本 |\r\n| 手机号 | 否 | 文本 |\r\n| 职位 | 否 | 文本 |\r\n\r\n---\r\n\r\n## 4. 付款及开票信息\r\n\r\n| 字段 | 必填 | 控件 | 说明 |\r\n|---|---|---|---|\r\n| 发票抬头 | 是 | 文本(禁用) | 自动带入客户全称,不可手改 |\r\n| 纳税人识别号 | 是 | 文本 | |\r\n| 注册地址 | 否 | 文本 | |\r\n| 注册电话 | 否 | 文本 | |\r\n| 开户银行 | 是 | 文本 | |\r\n| 账号 | 否 | 文本 | |\r\n\r\n---\r\n\r\n## 5. 校验与提交\r\n\r\n- 提交前校验所有必填项\r\n- 通过后 Toast「客户已保存(原型演示)」并返回列表\r\n- 重置清空全部字段,客户类型默认「企业」\r\n" } ] }, @@ -389,19 +303,43 @@ "type": "markdown", "id": "cm-doc-node-cm-list-filter", "title": "筛选条件", - "markdown": "## 4. 筛选区\n\n默认展示前 4 项(一行),点击「更多筛选」显示全部。\n\n| 字段 | 控件 | 匹配规则 |\n|---|---|---|\n| 客户编号 | 可搜索单选 | 精确匹配 `code` |\n| 客户名称 | 可搜索单选 | 精确匹配 `name` |\n| 标签 | 多选 | 客户 `labels` 包含任一选中标签 |\n| 区域 | 可搜索单选 | 精确匹配 `region` |\n| 客户状态 | 多选 | `status` 在选中集合内 |\n| 业务部门 | 多选 | `department` 在选中集合内 |\n| 业务负责人 | 多选 | 随部门联动选项;`manager` 在选中集合内 |\n| 创建时间 | 日期范围 | `createTime` 落在区间内(含边界日) |\n\n### 4.1 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入已应用条件,页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n---" + "markdown": "## 4. 筛选区\r\n\r\n默认展示前 4 项(一行),点击「更多筛选」显示全部。\r\n\r\n| 字段 | 控件 | 匹配规则 |\r\n|---|---|---|\r\n| 客户编号 | 可搜索单选 | 精确匹配 `code` |\r\n| 客户名称 | 可搜索单选 | 精确匹配 `name` |\r\n| 标签 | 多选 | 客户 `labels` 包含任一选中标签(OR) |\r\n| 区域 | 可搜索单选 | 精确匹配 `region` |\r\n| 客户状态 | 多选 | `status` 在选中集合内 |\r\n| 业务部门 | 多选 | `department` 在选中集合内 |\r\n| 业务负责人 | 多选 | 随部门联动选项;`manager` 在选中集合内 |\r\n| 创建时间 | 日期范围 | `createTime` 落在区间内(含边界日) |\r\n\r\n### 4.1 操作逻辑\r\n\r\n| 操作 | 行为 |\r\n|---|---|\r\n| **查询** | 将当前筛选项写入已应用条件,页码重置为 1,清空行选中 |\r\n| **重置** | 清空待应用与已应用条件,页码重置为 1,清空行选中 |\r\n| 修改筛选项未点查询 | 不影响当前列表 |\r\n\r\n---" + }, + { + "type": "markdown", + "id": "cm-doc-node-cm-risk-role-switch", + "title": "操作角色切换", + "markdown": "### 5.1 操作角色切换(原型演示)\r\n\r\n列表顶部提供「当前操作角色」下拉,用于模拟不同部门用户:\r\n\r\n| 角色 | 部门 | 打标权限 |\r\n|---|---|---|\r\n| 王风控 | 风控部 | 全部标签 |\r\n| 李法务 | 法务部 | 法务类标签 |\r\n| 张财务 | 财务部 | 财务类标签 |\r\n| 赵运维 | 运维部 | 运维类标签 |\r\n| 陈销售 | 销售一部 | 仅查看,无打标入口 |\r\n\r\n切换角色后,风险标签管理抽屉内的**标签类型**、**可选标签**、**审批/移除按钮**随之联动更新。\r\n\r\n---" }, { "type": "markdown", "id": "cm-doc-node-cm-list-toolbar", "title": "列表工具栏", - "markdown": "## 5. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 新增客户 | 进入新增客户页(分组表单) |\n| 批量导入 | Toast:批量导入(原型演示) |\n| 批量导出 | Toast:导出当前列表条数 |\n\n---" + "markdown": "## 5. 列表工具栏\r\n\r\n| 按钮 | 行为 |\r\n|---|---|\r\n| 新增客户 | 进入新增客户页(分组表单) |\r\n| 批量导入 | Toast:批量导入(原型演示) |\r\n| **批量打标** | 勾选 ≥1 行且当前角色有打标权限时显示;打开批量打标弹窗 |\r\n| 批量导出 | Toast:导出当前列表条数 |\r\n\r\n### 5.1 操作角色切换(原型演示)\r\n\r\n列表顶部提供「当前操作角色」下拉,用于模拟不同部门用户:\r\n\r\n| 角色 | 部门 | 打标权限 |\r\n|---|---|---|\r\n| 王风控 | 风控部 | 全部标签 |\r\n| 李法务 | 法务部 | 法务类标签 |\r\n| 张财务 | 财务部 | 财务类标签 |\r\n| 赵运维 | 运维部 | 运维类标签 |\r\n| 陈销售 | 销售一部 | 仅查看,无打标入口 |\r\n\r\n切换角色后,风险标签管理抽屉内的**标签类型**、**可选标签**、**审批/移除按钮**随之联动更新。\r\n\r\n---" }, { "type": "markdown", "id": "cm-doc-node-cm-list-table", "title": "客户台账列表", - "markdown": "## 6. 列表字段说明\n\n| 列 | 说明 |\n|---|---|\n| 客户编号 | 左固定列 |\n| 客户名称 | 超长省略 |\n| 标签 | 仅展示第一个标签;多于 1 个显示「+N」,悬浮展示全部 |\n| 客户区域 | 大区名称 |\n| 客户状态 | 绿点「正常」/ 红点「黑名单」 |\n| 业务部门 / 业务负责人 | 文本 |\n| 客户类型 / 所属行业 | 文本 |\n| 统一社会信用代码 / 法人 / 注册资本 / 经营状态 | 文本 |\n| 详细地址 / 创建时间 | 文本;创建时间不换行 |\n| 操作 | 详情、编辑、删除(原型 Toast) |\n\n支持行多选(checkbox);横向滚动;操作列右固定。\n\n---" + "markdown": "## 6. 列表字段说明\r\n\r\n| 列 | 说明 |\r\n|---|---|\r\n| 客户编号 | 左固定列 |\r\n| 客户名称 | 超长省略 |\r\n| **风险标签** | 展示「生效中」标签;按风险级别着色(低/中/高/极高);首标签 + `+N` 悬浮全部;有待审批时显示「审」标识;可点击打开管理抽屉 |\r\n| 客户区域 | 大区名称 |\r\n| 客户状态 | 绿点「正常」/ 红点「黑名单」 |\r\n| 业务部门 / 业务负责人 | 文本 |\r\n| 客户类型 / 所属行业 | 文本 |\r\n| 统一社会信用代码 / 法人 / 注册资本 / 经营状态 | 文本 |\r\n| 详细地址 / 创建时间 | 文本;创建时间不换行 |\r\n| 操作 | 详情、编辑、**管理标签**(有权限时)、删除(原型 Toast) |\r\n\r\n支持行多选(checkbox,仅当前页);横向滚动;操作列右固定。\r\n\r\n---" + }, + { + "type": "markdown", + "id": "cm-doc-node-cm-risk-tag-drawer", + "title": "风险标签管理抽屉", + "markdown": "## 8. 风险标签维护(列表页专属)\r\n\r\n### 8.1 入口\r\n\r\n| 入口 | 说明 |\r\n|---|---|\r\n| 行操作「管理标签」 | 打开右侧抽屉,管理单个客户 |\r\n| 点击标签列 | 有权限时同「管理标签」 |\r\n| 工具栏「批量打标」 | 对当前勾选客户批量提交同一标签 |\r\n\r\n**不在**新增页、编辑页维护标签。\r\n\r\n### 8.2 管理抽屉结构\r\n\r\n| Tab | 内容 |\r\n|---|---|\r\n| 当前标签 | 已打标签列表:名称、类型、级别、状态、原因摘要;支持查看详情、移除、审批 |\r\n| 新增标签 | 打标表单(见 8.3) |\r\n| 操作日志 | 该客户全部标签相关审计记录 |\r\n\r\n### 8.3 打标表单字段\r\n\r\n| 字段 | 必填 | 规则 |\r\n|---|---|---|\r\n| 标签名称 | 是 | 按 7.1–7.4 分组下拉;仅当前角色有权限的标签可选;已存在同名有效标签不可重复选 |\r\n| 标签类型 | 是 | **根据当前操作角色自动带出**(财务→财务、法务→法务、运维→运维、风控→风控),**置灰不可编辑** |\r\n| 风险级别 | 是 | **选择标签名称后按目录自动带出**,**置灰不可编辑** |\r\n| 生效时间 | 是 | 日期,默认当天 |\r\n| 有效期至 | 是 | 日期,须 ≥ 生效时间;默认一年后 |\r\n| 原因说明 | 是 | 至少 10 字,供法务/财务/风控审批留痕 |\r\n| 附件 | 否 | 支持 .pdf / .jpg / .png / .xlsx,可多个 |\r\n\r\n### 8.4 批量打标\r\n\r\n- 展示已选客户 chips(超过 6 个显示 +N)\r\n- 表单字段与单客户打标一致\r\n- 已存在同名有效标签的客户自动跳过\r\n- 提交后写入批量操作日志\r\n\r\n### 8.5 标签生命周期\r\n\r\n| 状态 | 说明 |\r\n|---|---|\r\n| 生效中 | 参与列表展示,同步至 `labels` |\r\n| 待审批 | 高风险标签提交后进入;列表暂不展示为生效 |\r\n| 待确认 | 预留 |\r\n| 已驳回 | 审批未通过,不同步至 `labels` |\r\n| 已移除 | 操作人主动移除,需填写原因 |\r\n| 已过期 | 预留 |\r\n\r\n**高风险规则:** 风险级别为「高」或「极高」的标签提交后状态为「待审批」,须法务或风控审批通过后变为「生效中」。\r\n\r\n### 8.6 移除标签\r\n\r\n- 仅「生效中」且当前角色有移除权限的标签可移除\r\n- 须填写移除原因(至少 5 字)\r\n- 写入操作日志,并从列表 `labels` 同步清除\r\n\r\n---" + }, + { + "type": "markdown", + "id": "cm-doc-node-cm-batch-risk-tag-btn", + "title": "批量打标入口", + "markdown": "### 8.1 入口\r\n\r\n| 入口 | 说明 |\r\n|---|---|\r\n| 行操作「管理标签」 | 打开右侧抽屉,管理单个客户 |\r\n| 点击标签列 | 有权限时同「管理标签」 |\r\n| 工具栏「批量打标」 | 对当前勾选客户批量提交同一标签 |\r\n\r\n**不在**新增页、编辑页维护标签。" + }, + { + "type": "markdown", + "id": "cm-doc-node-cm-batch-risk-tag-modal", + "title": "批量打标弹窗", + "markdown": "### 8.4 批量打标\r\n\r\n- 展示已选客户 chips(超过 6 个显示 +N)\r\n- 表单字段与单客户打标一致\r\n- 已存在同名有效标签的客户自动跳过\r\n- 提交后写入批量操作日志" } ] }, @@ -424,25 +362,25 @@ "type": "markdown", "id": "cm-doc-node-cm-create-base", "title": "客户信息", - "markdown": "## 2. 客户信息\n\n| 字段 | 必填 | 控件 | 说明 |\n|---|---|---|---|\n| 客户类型 | 是 | 下拉 | 企业 / 个人 / 事业单位 |\n| 客户分级 | 否 | 下拉 | A / B / C |\n| 客户全称 | 是 | 文本 | 与客户证照一致;变更时同步发票抬头 |\n| 客户简称 | 否 | 文本 | 选填 |\n| 所属城市 | 是 | 省-市级联 | 示例省市区数据 |\n| 通讯地址 | 否 | 文本 | |\n| 业务负责部门 | 是 | 下拉 | 变更时清空负责人 |\n| 业务负责人员 | 是 | 下拉 | 随部门联动,未选部门时禁用 |\n| 备注 | 否 | 多行文本 | |\n\n---" + "markdown": "## 2. 客户信息\r\n\r\n| 字段 | 必填 | 控件 | 说明 |\r\n|---|---|---|---|\r\n| 客户类型 | 是 | 下拉 | 企业 / 个人 / 事业单位 |\r\n| 客户分级 | 否 | 下拉 | A / B / C |\r\n| 客户全称 | 是 | 文本 | 与客户证照一致;变更时同步发票抬头 |\r\n| 客户简称 | 否 | 文本 | 选填 |\r\n| 所属城市 | 是 | 省-市级联 | 示例省市区数据 |\r\n| 通讯地址 | 否 | 文本 | |\r\n| 业务负责部门 | 是 | 下拉 | 变更时清空负责人 |\r\n| 业务负责人员 | 是 | 下拉 | 随部门联动,未选部门时禁用 |\r\n| 备注 | 否 | 多行文本 | |\r\n\r\n---" }, { "type": "markdown", "id": "cm-doc-node-cm-create-contact", "title": "联系人信息", - "markdown": "## 3. 联系人信息\n\n| 字段 | 必填 | 控件 |\n|---|---|---|\n| 姓名 | 否 | 文本 |\n| 手机号 | 否 | 文本 |\n| 职位 | 否 | 文本 |\n\n---" + "markdown": "## 3. 联系人信息\r\n\r\n| 字段 | 必填 | 控件 |\r\n|---|---|---|\r\n| 姓名 | 否 | 文本 |\r\n| 手机号 | 否 | 文本 |\r\n| 职位 | 否 | 文本 |\r\n\r\n---" }, { "type": "markdown", "id": "cm-doc-node-cm-create-invoice", "title": "付款及开票信息", - "markdown": "## 4. 付款及开票信息\n\n| 字段 | 必填 | 控件 | 说明 |\n|---|---|---|---|\n| 发票抬头 | 是 | 文本(禁用) | 自动带入客户全称,不可手改 |\n| 纳税人识别号 | 是 | 文本 | |\n| 注册地址 | 否 | 文本 | |\n| 注册电话 | 否 | 文本 | |\n| 开户银行 | 是 | 文本 | |\n| 账号 | 否 | 文本 | |\n\n---" + "markdown": "## 4. 付款及开票信息\r\n\r\n| 字段 | 必填 | 控件 | 说明 |\r\n|---|---|---|---|\r\n| 发票抬头 | 是 | 文本(禁用) | 自动带入客户全称,不可手改 |\r\n| 纳税人识别号 | 是 | 文本 | |\r\n| 注册地址 | 否 | 文本 | |\r\n| 注册电话 | 否 | 文本 | |\r\n| 开户银行 | 是 | 文本 | |\r\n| 账号 | 否 | 文本 | |\r\n\r\n---" }, { "type": "markdown", "id": "cm-doc-node-cm-create-footer", "title": "底部操作栏", - "markdown": "## 5. 校验与提交\n\n- 提交前校验所有必填项\n- 通过后 Toast「客户已保存(原型演示)」并返回列表\n- 重置清空全部字段,客户类型默认「企业」" + "markdown": "## 5. 校验与提交\r\n\r\n- 提交前校验所有必填项\r\n- 通过后 Toast「客户已保存(原型演示)」并返回列表\r\n- 重置清空全部字段,客户类型默认「企业」" } ] } diff --git a/src/prototypes/customer-management/canvas.excalidraw b/src/prototypes/customer-management/canvas.excalidraw new file mode 100644 index 0000000..50522d0 --- /dev/null +++ b/src/prototypes/customer-management/canvas.excalidraw @@ -0,0 +1,10 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "@axhub/make", + "elements": [], + "appState": { + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/src/prototypes/customer-management/components/BatchRiskTagModal.tsx b/src/prototypes/customer-management/components/BatchRiskTagModal.tsx new file mode 100644 index 0000000..231479c --- /dev/null +++ b/src/prototypes/customer-management/components/BatchRiskTagModal.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { X } from 'lucide-react'; +import type { CustomerRecord } from '../types'; +import { EMPTY_RISK_TAG_FORM, type MockUser, type RiskTagApplyForm, type RiskTagsByCustomer } from '../types/riskTag'; +import { canApplyRiskTagByName, roleToTagCategory } from '../utils/permissions'; +import { TAG_CATALOG_MAP } from '../utils/riskTagConstants'; +import { defaultEffectiveTo, hasDuplicateActiveTag, todayDate, validateApplyForm } from '../utils/riskTagStore'; +import { RiskTagApplyFormSection } from './RiskTagForm'; + +interface BatchRiskTagModalProps { + open: boolean; + records: CustomerRecord[]; + selectedIds: Set; + riskTagsByCustomer: RiskTagsByCustomer; + currentUser: MockUser; + onClose: () => void; + onSubmit: (form: RiskTagApplyForm, customerIds: string[]) => string | null; +} + +export function BatchRiskTagModal({ + open, + records, + selectedIds, + riskTagsByCustomer, + currentUser, + onClose, + onSubmit, +}: BatchRiskTagModalProps) { + const [form, setForm] = useState(() => ({ + ...EMPTY_RISK_TAG_FORM, + tagType: roleToTagCategory(currentUser), + effectiveFrom: todayDate(), + effectiveTo: defaultEffectiveTo(), + })); + const [error, setError] = useState(''); + + const selectedRecords = useMemo( + () => records.filter((record) => selectedIds.has(record.id)), + [records, selectedIds], + ); + + useEffect(() => { + if (!open) return; + const nextType = roleToTagCategory(currentUser); + setForm((prev) => { + const keepTag = prev.tagName && canApplyRiskTagByName(currentUser, prev.tagName); + const catalog = keepTag ? TAG_CATALOG_MAP[prev.tagName] : undefined; + return { + ...prev, + tagType: nextType, + tagName: keepTag ? prev.tagName : '', + riskLevel: keepTag && catalog ? catalog.defaultLevel : '中', + }; + }); + }, [currentUser, open]); + + if (!open) return null; + + const handleSubmit = () => { + const validation = validateApplyForm(form); + if (validation) { + setError(validation); + return; + } + if (!canApplyRiskTagByName(currentUser, form.tagName)) { + setError('当前角色无权选择该标签'); + return; + } + const eligibleIds = selectedRecords + .filter((record) => !hasDuplicateActiveTag(riskTagsByCustomer[record.id] ?? [], form.tagName)) + .map((record) => record.id); + + if (eligibleIds.length === 0) { + setError('所选客户均已存在该标签'); + return; + } + + const result = onSubmit({ ...form, tagType: roleToTagCategory(currentUser) }, eligibleIds); + if (result) { + setError(result); + return; + } + setForm({ + ...EMPTY_RISK_TAG_FORM, + tagType: roleToTagCategory(currentUser), + effectiveFrom: todayDate(), + effectiveTo: defaultEffectiveTo(), + }); + setError(''); + onClose(); + }; + + const skippedCount = selectedRecords.length - selectedRecords.filter( + (record) => !hasDuplicateActiveTag(riskTagsByCustomer[record.id] ?? [], form.tagName), + ).length; + + return ( +
+
e.stopPropagation()} + > +
+
+

批量打标

+

已选 {selectedRecords.length} 个客户 · 操作人 {currentUser.name}({currentUser.department})

+
+ +
+ +
+ {error &&
{error}
} + +
+ {selectedRecords.slice(0, 6).map((record) => ( + {record.name} + ))} + {selectedRecords.length > 6 && ( + +{selectedRecords.length - 6} + )} +
+ + {form.tagName && skippedCount > 0 && ( +
+ 其中 {skippedCount} 个客户已存在「{form.tagName}」,提交时将自动跳过。 +
+ )} + + setForm((prev) => ({ ...prev, ...patch }))} + /> +
+ +
+ + +
+
+
+ ); +} diff --git a/src/prototypes/customer-management/components/CustomerTable.tsx b/src/prototypes/customer-management/components/CustomerTable.tsx index ba53e61..b304d19 100644 --- a/src/prototypes/customer-management/components/CustomerTable.tsx +++ b/src/prototypes/customer-management/components/CustomerTable.tsx @@ -1,29 +1,38 @@ import React, { useState } from 'react'; import type { CustomerRecord } from '../types'; -import { LabelTags } from './LabelTags'; +import type { CustomerRiskTag, MockUser } from '../types/riskTag'; +import { RiskLabelTags } from './RiskLabelTags'; import { StatusBadge } from './StatusBadge'; +import { canManageRiskTags } from '../utils/permissions'; interface CustomerTableProps { records: CustomerRecord[]; selectedIds: Set; + riskTagsByCustomer: Record; + currentUser: MockUser; onToggleRow: (id: string, checked: boolean) => void; onToggleAll: (checked: boolean) => void; onView: (record: CustomerRecord) => void; onEdit: (record: CustomerRecord) => void; onDelete: (record: CustomerRecord) => void; + onManageTags: (record: CustomerRecord) => void; } export function CustomerTable({ records, selectedIds, + riskTagsByCustomer, + currentUser, onToggleRow, onToggleAll, onView, onEdit, onDelete, + onManageTags, }: CustomerTableProps) { const allSelected = records.length > 0 && records.every((record) => selectedIds.has(record.id)); const [hoverId, setHoverId] = useState(null); + const canManage = canManageRiskTags(currentUser); if (records.length === 0) { return ( @@ -48,7 +57,7 @@ export function CustomerTable({ 客户编号 客户名称 - 标签 + 风险标签 客户区域 客户状态 业务部门 @@ -82,7 +91,12 @@ export function CustomerTable({ {record.code} {record.name} - + + onManageTags(record) : undefined} + /> + {record.region} {record.department} @@ -100,6 +114,12 @@ export function CustomerTable({ | + {canManage && ( + <> + | + + + )} | diff --git a/src/prototypes/customer-management/components/RiskLabelTags.tsx b/src/prototypes/customer-management/components/RiskLabelTags.tsx new file mode 100644 index 0000000..b10be46 --- /dev/null +++ b/src/prototypes/customer-management/components/RiskLabelTags.tsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import type { CustomerRiskTag } from '../types/riskTag'; +import { getActiveTags, getPendingTags } from '../utils/riskTagStore'; + +interface RiskLabelTagsProps { + tags: CustomerRiskTag[]; + onView?: () => void; +} + +function levelClass(level: CustomerRiskTag['riskLevel']): string { + switch (level) { + case '低': return 'cm-risk-tag--low'; + case '中': return 'cm-risk-tag--medium'; + case '高': return 'cm-risk-tag--high'; + case '极高': return 'cm-risk-tag--critical'; + default: return 'cm-risk-tag--medium'; + } +} + +export function RiskLabelTags({ tags, onView }: RiskLabelTagsProps) { + const [open, setOpen] = useState(false); + const visibleTags = tags.filter((tag) => + tag.status === '生效中' || tag.status === '待审批' || tag.status === '待确认', + ); + const active = getActiveTags(visibleTags); + const pending = getPendingTags(visibleTags); + const displayTags = active.length > 0 ? active : pending; + + if (!displayTags.length) { + return ; + } + + const primary = displayTags[0]; + const overflow = displayTags.length - 1; + + return ( + + + {pending.length > 0 && active.length === 0 && ( + + )} + {overflow > 0 && ( + setOpen(true)} + onMouseLeave={() => setOpen(false)} + onFocus={() => setOpen(true)} + onBlur={() => setOpen(false)} + tabIndex={0} + role="button" + aria-label={`还有 ${overflow} 个标签`} + > + +{overflow} + {open && ( + + {displayTags.map((tag) => ( + + {tag.tagName} + {tag.status !== '生效中' && ( + {tag.status} + )} + + ))} + + )} + + )} + {pending.length > 0 && active.length > 0 && ( + + +{pending.length}审 + + )} + + ); +} diff --git a/src/prototypes/customer-management/components/RiskTagDrawer.tsx b/src/prototypes/customer-management/components/RiskTagDrawer.tsx new file mode 100644 index 0000000..27de564 --- /dev/null +++ b/src/prototypes/customer-management/components/RiskTagDrawer.tsx @@ -0,0 +1,308 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Clock, Plus, Tag, X } from 'lucide-react'; +import type { CustomerRecord } from '../types'; +import type { + CustomerRiskTag, + MockUser, + RiskTagApplyForm, + RiskTagOperationLog, +} from '../types/riskTag'; +import { canApproveRiskTag, canApplyRiskTagByName, canRemoveRiskTag, roleToTagCategory } from '../utils/permissions'; +import { TAG_CATALOG_MAP } from '../utils/riskTagConstants'; +import { EMPTY_RISK_TAG_FORM } from '../types/riskTag'; +import { + defaultEffectiveTo, + formatNow, + hasDuplicateActiveTag, + todayDate, + validateApplyForm, +} from '../utils/riskTagStore'; +import { RiskTagApplyFormSection, RiskTagDetailCard } from './RiskTagForm'; + +type DrawerTab = 'tags' | 'add' | 'logs'; + +interface RiskTagDrawerProps { + open: boolean; + record: CustomerRecord | null; + tags: CustomerRiskTag[]; + logs: RiskTagOperationLog[]; + currentUser: MockUser; + onClose: () => void; + onApply: (form: RiskTagApplyForm) => string | null; + onRemove: (tagId: string, reason: string) => string | null; + onApprove: (tagId: string, approved: boolean, rejectReason?: string) => string | null; +} + +export function RiskTagDrawer({ + open, + record, + tags, + logs, + currentUser, + onClose, + onApply, + onRemove, + onApprove, +}: RiskTagDrawerProps) { + const [tab, setTab] = useState('tags'); + const [detailId, setDetailId] = useState(null); + const [form, setForm] = useState(EMPTY_RISK_TAG_FORM); + const [removeReason, setRemoveReason] = useState(''); + const [removeTargetId, setRemoveTargetId] = useState(null); + const [rejectReason, setRejectReason] = useState(''); + const [approveTargetId, setApproveTargetId] = useState(null); + const [error, setError] = useState(''); + + const detailTag = useMemo( + () => tags.find((tag) => tag.id === detailId) ?? null, + [tags, detailId], + ); + + const customerLogs = useMemo( + () => logs.filter((log) => log.customerId === record?.id), + [logs, record?.id], + ); + + const disabledTagNames = useMemo( + () => tags + .filter((tag) => tag.status === '生效中' || tag.status === '待审批' || tag.status === '待确认') + .map((tag) => tag.tagName), + [tags], + ); + + /** 顶部切换操作角色时,同步打标表单中的标签类型与可选标签 */ + useEffect(() => { + if (!open) return; + const nextType = roleToTagCategory(currentUser); + setForm((prev) => { + const keepTag = prev.tagName && canApplyRiskTagByName(currentUser, prev.tagName); + const catalog = keepTag ? TAG_CATALOG_MAP[prev.tagName] : undefined; + return { + ...prev, + tagType: nextType, + tagName: keepTag ? prev.tagName : '', + riskLevel: keepTag && catalog ? catalog.defaultLevel : '中', + }; + }); + }, [currentUser, open]); + + if (!open || !record) return null; + + const resetAddForm = () => { + setForm({ + ...EMPTY_RISK_TAG_FORM, + tagType: roleToTagCategory(currentUser), + effectiveFrom: todayDate(), + effectiveTo: defaultEffectiveTo(), + }); + setError(''); + }; + + const handleSubmitApply = () => { + const validation = validateApplyForm(form); + if (validation) { + setError(validation); + return; + } + if (hasDuplicateActiveTag(tags, form.tagName)) { + setError('该客户已存在同名有效标签'); + return; + } + const result = onApply({ ...form, tagType: roleToTagCategory(currentUser) }); + if (result) { + setError(result); + return; + } + resetAddForm(); + setTab('tags'); + }; + + const handleConfirmRemove = () => { + if (!removeTargetId) return; + if (removeReason.trim().length < 5) { + setError('移除原因至少 5 个字'); + return; + } + const result = onRemove(removeTargetId, removeReason.trim()); + if (result) { + setError(result); + return; + } + setRemoveTargetId(null); + setRemoveReason(''); + setError(''); + }; + + const handleConfirmApprove = (approved: boolean) => { + if (!approveTargetId) return; + if (!approved && !rejectReason.trim()) { + setError('请填写驳回原因'); + return; + } + const result = onApprove(approveTargetId, approved, rejectReason.trim() || undefined); + if (result) { + setError(result); + return; + } + setApproveTargetId(null); + setRejectReason(''); + setError(''); + }; + + return ( +
+