From d458d6378631ea6749aaabd32e2752c90a2dd763 Mon Sep 17 00:00:00 2001 From: hsu06 Date: Fri, 3 Jul 2026 16:17:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(supplier-management):=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E7=AE=A1=E7=90=86=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E5=8E=9F=E5=9E=8B=E8=BF=AD=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐新增表单校验、角色切换、删除/离开确认等交互组件,并更新列表 KPI、样例数据与需求评审文档。 --- .../.spec/prototype-review.md | 94 +++++++ .../annotation-source.json | 44 ++- .../components/BankAccountConfirmModal.tsx | 48 ++++ .../components/ChangeLogDrawer.tsx | 70 +++++ .../components/DeleteConfirmModal.tsx | 70 +++++ .../components/KpiCards.tsx | 6 +- .../components/LeaveConfirmModal.tsx | 44 +++ .../components/RoleSwitcher.tsx | 33 +++ .../supplier-management/data/suppliers.json | 24 +- src/prototypes/supplier-management/index.tsx | 51 ++-- .../supplier-management/pages/CreatePage.tsx | 72 +++-- .../supplier-management/styles/index.css | 256 +++++++++++++++++- src/prototypes/supplier-management/types.ts | 15 +- .../supplier-management/utils/address.ts | 31 +++ .../supplier-management/utils/constants.ts | 4 +- .../supplier-management/utils/createForm.ts | 16 ++ .../supplier-management/utils/filters.ts | 8 +- .../supplier-management/utils/permissions.ts | 41 +++ .../supplier-management/utils/phone.ts | 32 +++ .../supplier-management/utils/region.ts | 31 +++ .../utils/supplierFields.ts | 127 +++++++++ .../supplier-management/utils/validation.ts | 100 +++++++ 22 files changed, 1138 insertions(+), 79 deletions(-) create mode 100644 src/prototypes/supplier-management/.spec/prototype-review.md create mode 100644 src/prototypes/supplier-management/components/BankAccountConfirmModal.tsx create mode 100644 src/prototypes/supplier-management/components/ChangeLogDrawer.tsx create mode 100644 src/prototypes/supplier-management/components/DeleteConfirmModal.tsx create mode 100644 src/prototypes/supplier-management/components/LeaveConfirmModal.tsx create mode 100644 src/prototypes/supplier-management/components/RoleSwitcher.tsx create mode 100644 src/prototypes/supplier-management/utils/createForm.ts create mode 100644 src/prototypes/supplier-management/utils/permissions.ts create mode 100644 src/prototypes/supplier-management/utils/phone.ts create mode 100644 src/prototypes/supplier-management/utils/region.ts create mode 100644 src/prototypes/supplier-management/utils/supplierFields.ts create mode 100644 src/prototypes/supplier-management/utils/validation.ts diff --git a/src/prototypes/supplier-management/.spec/prototype-review.md b/src/prototypes/supplier-management/.spec/prototype-review.md new file mode 100644 index 0000000..a0cf90a --- /dev/null +++ b/src/prototypes/supplier-management/.spec/prototype-review.md @@ -0,0 +1,94 @@ +# Prototype Review + +- 审查目标:`src/prototypes/supplier-management`(列表页 `list` + 新增页 `create`) +- 用户资料/参考资料: + - 本轮用户说明:按 `rules/prototype-review-guide.md` 对供应商管理做需求评审 + - `.spec/prototype-comments.json`(供应商名称筛选项、联系人必填与电话位数校验等近期批注) + - `src/resources/oneos-web-legacy/需求说明/业务管理/供应商管理` + - `src/resources/oneos-web-legacy/业务管理/供应商管理-新增.jsx`、`供应商管理-查看.jsx`、`供应商管理-编辑.jsx` + - 源码:`index.tsx`、`pages/CreatePage.tsx`、`components/*`、`utils/*`、`types.ts`、`data/suppliers.json` + - `annotation-source.json`、`canvas.excalidraw`(未逐元素核对画布) + - `.axhub/make/project.json`(入口 `supplier-management` → `/prototypes/supplier-management`) +- 生成时间:2026-07-03 + +## 总体点评 + +供应商管理原型在**列表台账展示**方面与 Legacy 需求高度对齐:筛选区(含近期批注要求的供应商名称模糊搜索)、8 张 KPI 统计卡片、宽表列(编码至创建时间)、分页与导入/导出入口均已具备,样例数据 `suppliers.json` 字段口径与表格列一致,列表侧可作为「供应商主数据台账」的有效演示。 + +当前最影响需求评审签字的问题,集中在**主流程出口与新增表单数据模型**。Legacy 与资源文档明确要求查看页、编辑页、删除二次确认;源码中三类操作均为 Toast(`index.tsx`),无法完成「查—改—删」闭环。新增页已按加氢站/客户管理风格扩展为主体信息、联系人、开票、银行账户、资质证照五段式,但**缺少列表与 Legacy 新增页共有的合作状态、供应商类型、城市/区域、信用代码/身份证、电子邮箱、备注等字段**,提交后也不写回 `records` 状态,导致「新增 → 列表可见」链路在业务上断裂。 + +近期 `.spec/prototype-comments.json` 与 `annotation-source.json` 已记录筛选区名称搜索、联系人校验等决策,与实现基本一致;但 `.spec` 下**无独立 PRD Markdown**,与 Legacy 2026-03-12 版需求在新增表单结构、列表名称搜索交互(联动 vs 点查询)上存在明显分歧,评审时需产品确认以哪套为准。 + +## P0-P3 优先级问题 + +### P1 - 查看 / 编辑 / 删除主流程未实现,Legacy 核心任务出口缺失 + +- 证据:Legacy `需求说明/业务管理/供应商管理` §2.15–§2.16 要求查看页、编辑页跳转及删除二次确认;`供应商管理.jsx` 含删除确认文案(历史数据保留规则);源码 `index.tsx` 中 `onView` / `onEdit` / `onDelete` 均仅 `showToast('…原型演示')`,`SupplierPageId` 仅 `'list' | 'create'`,无 view/edit 路由。 +- 影响:无法验证编辑态字段只读/可改规则、删除后对合同/账单选供应商的约束;与 OneOS 业务管理模块能力不对齐,需求评审无法确认主流程完整性。 +- 修复方向:至少补充查看页(只读回显)与编辑页(编码不可改等 Legacy 规则)原型;删除增加确认弹窗并 Mock 从可选列表移除、历史保留说明。 + +### P1 - 新增表单字段与列表台账 / Legacy 新增页严重不对齐 + +- 证据:Legacy `供应商管理-新增.jsx` 必填含合作状态、供应商名称、类型、城市(省-市级联)、供应商地址、联系人、联系人手机、信用代码/身份证;列表列含类型、区域、城市、信用代码、邮箱、备注(`SupplierTable.tsx`)。当前 `CreatePage.tsx` / `SupplierCreateForm` 含签约公司、地址解析、联系人、开票、银行、证照,但**无** `coopStatus`、`type`、`city`/`region`、`creditCodeOrId`、`email`、`remark`;校验(`validateCreateForm`)亦未覆盖上述字段。 +- 影响:即使用户完整填写并提交,也无法生成与表格列一致的台账记录;与加氢站「新建供应商」区块(`站点信息.jsx`)部分对齐,但与 Legacy 供应商模块口径冲突,下游合同/对账场景无法演示字段来源。 +- 修复方向:产品确认新增页以 Legacy 两卡片为准,还是以当前五段式(对齐加氢站)为准;若保留五段式,需补全台账关键字段及区域自动匹配逻辑,并在标注/PRD 写明与 Legacy 差异。 + +### P1 - 提交 / 保存不写回列表,新增主流程闭环断裂 + +- 证据:`index.tsx` 中 `records` 为 `useState` 初始 seed 且只读;`handleSubmitCreate` 校验通过后仅 Toast 并 `setView('list')`,不追加 `SupplierRecord`;`handleSaveCreate` 同样不落库。 +- 影响:无法演示「新增供应商后在列表/KPI 中可见」;筛选、KPI 计数、分页与新增联调失效;测试无法覆盖提交后编码生成、创建人/时间写入等规则。 +- 修复方向:提交时将表单 Mock 映射为 `SupplierRecord` 写入本地 state(含自动生成编码、创建人/时间);保存草稿可写入 session 或单独 drafts 列表。 + +### P2 - 删除缺少二次确认与业务规则表达 + +- 证据:Legacy §2.16.3 要求删除确认及「已删除供应商无法在合同或账单中选择,历史数据仍显示」;`index.tsx` `onDelete` 直接 Toast,无 Modal。 +- 影响:业务约束(软删 vs 硬删、下游引用)无法在原型中感知,评审易遗漏合规讨论。 +- 修复方向:增加确认对话框,文案对齐 Legacy;Mock 删除可标记 `coopStatus` 或 `deleted` 标志并过滤可选列表。 + +### P2 - KPI 卡片与筛选区口径分裂,且超出 Legacy 列表范围 + +- 证据:`KpiCards.tsx` + `utils/constants.ts` 定义 8 张卡片(含「资质异常」`licenseAbnormal`),点击即时过滤(`kpiTab`);筛选区 `pendingFilters` / `appliedFilters` 需点「查询」才生效(`FilterPanel.tsx` + `index.tsx`);Legacy 需求说明无 KPI 卡片章节。`licenseStatus` 仅存在于 seed 数据,新增页无法维护。 +- 影响:KPI 与高级筛选组合时用户难以理解最终列表范围;「资质异常」卡片无对应新增/编辑维护路径,业务含义难验收。 +- 修复方向:在 PRD/标注补充 KPI 与筛选优先级;或 KPI 激活时联动写入 `appliedFilters`;明确「资质异常」判定来源(证照上传/OCR/人工标记)。 + +## 完整性与项目对齐 + +| 维度 | 结论 | +|---|---| +| 核心用户 | Legacy 未细分角色;原型无权限模型 — **可接受为原型简化** | +| 主流程 | 列表筛选 → KPI → 表格 → 分页:**可演示**;新增 → 校验 → 返回列表:**部分可演示**(校验有、落库无);查看/编辑/删除:**未实现** | +| 入口/出口 | 入口:`project.json` → `/prototypes/supplier-management` — **对齐**;新增页返回列表 — **有**;缺查看/编辑出口 | +| 范围边界 | 导入/导出 Toast 演示 — **符合原型惯例**;8 KPI 卡片 — **实现有、Legacy 无**;五段式新增表单 — **与 Legacy 两卡片不一致** | +| 近期批注对齐 | 筛选区供应商名称(`FilterPanel.tsx` `nameKeyword` + 模糊匹配)— **已实现**;联系人必填 + 手机/座机至少一项 + 位数校验(`utils/phone.ts`)— **已实现** | +| 资料冲突/待确认 | ① 列表名称搜索:Legacy §2 要求工具栏左上方**输入即联动**;批注与实现放在筛选区且需点「查询」— **以批注为准,Legacy 待产品确认是否废弃**;② 新增表单结构:Legacy 基本信息+开票 vs 当前主体+联系人+开票+银行+证照 — **待确认**;③ 联系人手机:Legacy 新增页手机必填;批注与实现为手机/座机二选一 — **以实现+批注为准,Legacy 待确认**;④ `.spec` 无 `requirements-prd-*.md` — **规格缺口** | + +## 业务逻辑连贯性 + +**流程顺序**:列表加载 →(可选)KPI 切换 → 筛选填写 → 查询 → 表格浏览 → 新增 — 顺序合理。名称链接与「查看」均指向 Toast,未形成真实导航。 + +**状态迁移**:合作状态枚举(已合作/终止合作/洽谈中/合约过期)在列表、`CoopStatusBadge`、筛选与 KPI 间一致 — **自洽**。新增页不采集合作状态,新记录无法表达 KPI「已合作/洽谈中」等维度 — **断档**。 + +**区域与城市**:Legacy 要求城市级联后自动匹配区域;列表展示 `region` + `city`(省-市);筛选区区域多选 + 城市单选(扁平 `FilterPickerField`),新增页仅地址解析出 `provinceCity`,不写入 `SupplierRecord.region/city` — **新增与列表字段链路不完整**。 + +**跨模块一致性**:加氢站新建供应商含主体、开票、银行、证照(`oneos-web-legacy/加氢站管理/站点信息.jsx`),与当前新增页结构相近;Legacy 独立「供应商管理-新增」字段集更偏台账全字段 — **项目内存在两套供应商表单范式,需统一**。 + +**数据生命周期**:seed 数据含 `licenseStatus` 驱动 KPI「资质异常」,但无编辑入口维护 — **只读演示**。 + +## 状态、异常、边界与恢复 + +| 类别 | 检查结论 | +|---|---| +| 输入校验 | 新增:签约公司、名称、地址解析、联系人、电话格式 — **有**(Toast 提示);缺类型/城市/信用代码等 — **见 P1** | +| 空/无数据 | 表格空态「暂无符合条件的供应商」— **有** | +| 权限不足 | 未建模 — **N/A** | +| 筛选边界 | 名称模糊匹配、编码精确、多选合作状态/类型/区域 — **有**;城市非级联 — **与 Legacy 差异** | +| KPI + 筛选重置 | 「重置」清空筛选并 KPI 回「全部供应商」— **有** | +| 删除/并发 | 无确认、无防抖 — **见 P2** | +| 网络/集成 | OCR、导入校验均为 Mock Toast — **原型范围内可接受** | +| 恢复路径 | 校验失败 Toast 可重填 — **有**;提交成功后列表无新数据 — **无业务恢复感知** | + +## 证据与评估说明 + +- **用户资料优先级**:本轮无额外用户口头需求;`.spec/prototype-comments.json` 批注权重高于 Legacy 冲突点(名称筛选位置、联系人电话规则),已在「完整性与项目对齐」标为待确认处。 +- **读取范围**:`rules/prototype-review-guide.md`;`.spec/prototype-comments.json`;`src/resources/oneos-web-legacy/需求说明/业务管理/供应商管理`;Legacy JSX 新增/查看/编辑;`index.tsx`、`CreatePage.tsx`、`FilterPanel.tsx`、`SupplierTable.tsx`、`KpiCards.tsx`、`CoopStatusBadge.tsx`;`utils/filters.ts`、`constants.ts`、`address.ts`、`phone.ts`;`types.ts`、`data/suppliers.json`;`annotation-source.json`;`.axhub/make/project.json`(摘要)。未引用 Impeccable 产物。 +- **独立评估**:`degraded` — 未拆分子代理;业务基线(Legacy + 批注)与源码证据在同一轮完成对照。 diff --git a/src/prototypes/supplier-management/annotation-source.json b/src/prototypes/supplier-management/annotation-source.json index 2283bed..d7f7ba7 100644 --- a/src/prototypes/supplier-management/annotation-source.json +++ b/src/prototypes/supplier-management/annotation-source.json @@ -36,7 +36,7 @@ "path": [] }, "aiPrompt": "KPI 卡片点击切换列表数据范围。", - "annotationText": "参照车辆管理列表页,8 张统计卡片可点击筛选。", + "annotationText": "6 张统计卡片:全部、已合作、洽谈中、终止合作、合约过期、资质异常(未上传营业执照);已移除备件供应商/保险公司类型卡片。", "hasMarkdown": true, "color": "#0f766e", "images": [], @@ -82,24 +82,42 @@ { "id": "sm-create-entity", "index": 5, - "title": "主体信息", + "title": "地址信息", "pageId": "create", "locator": { "selectors": ["[data-annotation-id=\"sm-create-entity\"]"], "fingerprint": "sm-create-entity", "path": [] }, - "aiPrompt": "签约公司、供应商名称、地址解析字段。", - "annotationText": "签约公司下拉默认展示置灰占位「请选择羚牛签约主体」,不可作为有效选项。地址解析为必填项,填写完整地址后自动反写省市与详细地址。", + "aiPrompt": "签约公司、供应商名称、区域、城市、详细地址。", + "annotationText": "签约公司下拉默认展示置灰占位「请选择羚牛签约主体」。含签约公司、供应商名称、区域、城市(只读自动解析)、详细地址(无必填星号);填写详细地址后自动解析区域与城市。", "hasMarkdown": true, "color": "#2563eb", "images": [], "createdAt": 1740787200000, - "updatedAt": 1740787200000 + "updatedAt": 1783052400000 + }, + { + "id": "sm-create-contact", + "index": 6, + "title": "联系人信息", + "pageId": "create", + "locator": { + "selectors": ["[data-annotation-id=\"sm-create-contact\"]"], + "fingerprint": "sm-create-contact", + "path": [] + }, + "aiPrompt": "联系人、联系人手机、联系人座机。", + "annotationText": "联系人为必填;联系人手机(11位)与联系人座机(7-12位)至少填写一项,保存/提交时校验位数与格式。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740787200000, + "updatedAt": 1783052400000 }, { "id": "sm-create-invoice", - "index": 6, + "index": 7, "title": "开票信息", "pageId": "create", "locator": { @@ -108,7 +126,7 @@ "path": [] }, "aiPrompt": "纳税人识别号、注册电话、注册地址。", - "annotationText": "", + "annotationText": "纳税人识别号、注册电话、注册地址;所有有编辑权限的用户均可维护(不限于财务)。", "hasMarkdown": true, "color": "#2563eb", "images": [], @@ -117,7 +135,7 @@ }, { "id": "sm-create-bank", - "index": 7, + "index": 8, "title": "银行账户", "pageId": "create", "locator": { @@ -126,7 +144,7 @@ "path": [] }, "aiPrompt": "开户行与银行账号。", - "annotationText": "", + "annotationText": "开户行、银行账号;所有有编辑权限的用户均可维护(不限于财务)。", "hasMarkdown": true, "color": "#2563eb", "images": [], @@ -135,7 +153,7 @@ }, { "id": "sm-create-docs", - "index": 8, + "index": 9, "title": "资质证照", "pageId": "create", "locator": { @@ -144,7 +162,7 @@ "path": [] }, "aiPrompt": "营业执照 OCR 与其他证件上传。", - "annotationText": "营业执照上传后自动反写名称、税号、通讯地址。", + "annotationText": "营业执照与其他证件上传;所有有编辑权限的用户均可维护(不限于财务)。", "hasMarkdown": true, "color": "#2563eb", "images": [], @@ -153,7 +171,7 @@ }, { "id": "sm-create-footer", - "index": 9, + "index": 10, "title": "表单提交", "pageId": "create", "locator": { @@ -162,7 +180,7 @@ "path": [] }, "aiPrompt": "返回、保存与提交操作。", - "annotationText": "底部固定操作栏:左侧「返回」回到列表;右侧「保存」暂存草稿,「提交」校验通过后提交并返回列表。", + "annotationText": "底部操作栏按钮右对齐:「返回」「保存」「提交」。未保存时点击「返回」或面包屑离开,需二次确认。", "hasMarkdown": true, "color": "#0f766e", "images": [], diff --git a/src/prototypes/supplier-management/components/BankAccountConfirmModal.tsx b/src/prototypes/supplier-management/components/BankAccountConfirmModal.tsx new file mode 100644 index 0000000..0f621c1 --- /dev/null +++ b/src/prototypes/supplier-management/components/BankAccountConfirmModal.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ShieldAlert, X } from 'lucide-react'; + +interface BankAccountConfirmModalProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export function BankAccountConfirmModal({ + open, + onClose, + onConfirm, +}: BankAccountConfirmModalProps) { + if (!open) return null; + + return ( +
+
event.stopPropagation()} + > +
+

确认变更银行账户

+ +
+
+
+ +
+

银行账户属于关键主数据字段,变更后将写入变更记录并需财务复核(原型演示)。

+

请确认开户行与银行账号信息准确无误。

+
+
+
+
+ + +
+
+
+ ); +} diff --git a/src/prototypes/supplier-management/components/ChangeLogDrawer.tsx b/src/prototypes/supplier-management/components/ChangeLogDrawer.tsx new file mode 100644 index 0000000..abfdcb6 --- /dev/null +++ b/src/prototypes/supplier-management/components/ChangeLogDrawer.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { X } from 'lucide-react'; +import type { ChangeLogEntry } from '../types'; + +interface ChangeLogDrawerProps { + open: boolean; + supplierId: string; + supplierName: string; + entries: ChangeLogEntry[]; + onClose: () => void; +} + +export function ChangeLogDrawer({ + open, + supplierId, + supplierName, + entries, + onClose, +}: ChangeLogDrawerProps) { + if (!open) return null; + + const filtered = entries.filter((item) => item.supplierId === supplierId); + + return ( +
+ +
+ ); +} diff --git a/src/prototypes/supplier-management/components/DeleteConfirmModal.tsx b/src/prototypes/supplier-management/components/DeleteConfirmModal.tsx new file mode 100644 index 0000000..c8c3d27 --- /dev/null +++ b/src/prototypes/supplier-management/components/DeleteConfirmModal.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { AlertTriangle, X } from 'lucide-react'; +import type { SupplierRecord } from '../types'; +import { canDeleteSupplier } from '../utils/permissions'; + +interface DeleteConfirmModalProps { + open: boolean; + record: SupplierRecord | null; + onClose: () => void; + onConfirm: () => void; +} + +export function DeleteConfirmModal({ + open, + record, + onClose, + onConfirm, +}: DeleteConfirmModalProps) { + if (!open || !record) return null; + + const deletable = canDeleteSupplier(record); + + return ( +
+
event.stopPropagation()} + > +
+

{deletable ? '确认删除供应商' : '确认停用供应商'}

+ +
+
+
+ +
+ {deletable ? ( +

+ 确定删除供应商「{record.supplierName}」吗?删除后不可恢复(原型演示)。 +

+ ) : ( + <> +

+ 供应商「{record.supplierName}」已关联 + {record.hasLinkedContract && '合同'} + {record.hasLinkedContract && record.hasLinkedPayment && '、'} + {record.hasLinkedPayment && '付款记录'} + ,不可删除,仅可停用。 +

+

停用后该供应商将不再参与新业务,历史数据保留。

+ + )} +
+
+
+
+ + +
+
+
+ ); +} diff --git a/src/prototypes/supplier-management/components/KpiCards.tsx b/src/prototypes/supplier-management/components/KpiCards.tsx index 3e48053..fb39ea9 100644 --- a/src/prototypes/supplier-management/components/KpiCards.tsx +++ b/src/prototypes/supplier-management/components/KpiCards.tsx @@ -6,8 +6,6 @@ import { MessageSquare, Ban, Clock, - Package, - Shield, FileWarning, } from 'lucide-react'; import type { SupplierKpiKey } from '../types'; @@ -19,8 +17,6 @@ const ICONS: Record = { negotiating: , terminated: , expired: , - parts: , - insurance: , licenseAbnormal: , }; @@ -32,7 +28,7 @@ interface KpiCardsProps { export function KpiCards({ counts, active, onChange }: KpiCardsProps) { return ( -
+
{KPI_CARDS.map((card) => ( + +
+
+ +
+

当前表单尚未保存,确定要离开吗?

+

离开后已填写内容将不会保留。

+
+
+
+
+ + +
+
+
+ ); +} diff --git a/src/prototypes/supplier-management/components/RoleSwitcher.tsx b/src/prototypes/supplier-management/components/RoleSwitcher.tsx new file mode 100644 index 0000000..29e904f --- /dev/null +++ b/src/prototypes/supplier-management/components/RoleSwitcher.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type { SupplierRole } from '../types'; +import { SUPPLIER_ROLE_OPTIONS } from '../utils/constants'; +import { canCreateSupplier } from '../utils/permissions'; + +interface RoleSwitcherProps { + role: SupplierRole; + onChange: (role: SupplierRole) => void; +} + +export function RoleSwitcher({ role, onChange }: RoleSwitcherProps) { + const current = SUPPLIER_ROLE_OPTIONS.find((item) => item.value === role); + + return ( +
+ 当前操作角色(原型演示): + + {current?.desc} + {!canCreateSupplier(role) && ( + 当前角色仅可查看主数据 + )} +
+ ); +} diff --git a/src/prototypes/supplier-management/data/suppliers.json b/src/prototypes/supplier-management/data/suppliers.json index 9def188..df276df 100644 --- a/src/prototypes/supplier-management/data/suppliers.json +++ b/src/prototypes/supplier-management/data/suppliers.json @@ -16,7 +16,8 @@ "remark": "", "creator": "李四", "createTime": "2025-01-10 09:30", - "licenseStatus": "正常" + "licenseStatus": "正常", + "businessLicenseUploaded": true }, { "id": "2", @@ -35,7 +36,8 @@ "remark": "意向供应商", "creator": "李专员", "createTime": "2025-02-15 14:20", - "licenseStatus": "正常" + "licenseStatus": "正常", + "businessLicenseUploaded": true }, { "id": "3", @@ -54,7 +56,8 @@ "remark": "", "creator": "张经理", "createTime": "2025-01-20 11:00", - "licenseStatus": "异常" + "licenseStatus": "异常", + "businessLicenseUploaded": true }, { "id": "4", @@ -73,7 +76,8 @@ "remark": "已终止", "creator": "李四", "createTime": "2024-06-01 10:00", - "licenseStatus": "正常" + "licenseStatus": "正常", + "businessLicenseUploaded": true }, { "id": "5", @@ -92,7 +96,8 @@ "remark": "待续约", "creator": "王专员", "createTime": "2025-03-01 08:45", - "licenseStatus": "异常" + "licenseStatus": "异常", + "businessLicenseUploaded": false }, { "id": "6", @@ -111,7 +116,8 @@ "remark": "", "creator": "张经理", "createTime": "2025-03-12 16:10", - "licenseStatus": "正常" + "licenseStatus": "正常", + "businessLicenseUploaded": true }, { "id": "7", @@ -130,7 +136,8 @@ "remark": "", "creator": "李四", "createTime": "2025-04-02 10:20", - "licenseStatus": "正常" + "licenseStatus": "正常", + "businessLicenseUploaded": true }, { "id": "8", @@ -149,6 +156,7 @@ "remark": "评估中", "creator": "李专员", "createTime": "2025-04-18 11:35", - "licenseStatus": "正常" + "licenseStatus": "正常", + "businessLicenseUploaded": false } ] diff --git a/src/prototypes/supplier-management/index.tsx b/src/prototypes/supplier-management/index.tsx index acef4de..ab4c275 100644 --- a/src/prototypes/supplier-management/index.tsx +++ b/src/prototypes/supplier-management/index.tsx @@ -17,6 +17,7 @@ import { FilterPanel } from './components/FilterPanel'; import { KpiCards } from './components/KpiCards'; import { SupplierTable } from './components/SupplierTable'; import { CreatePage } from './pages/CreatePage'; +import { LeaveConfirmModal } from './components/LeaveConfirmModal'; import { ImportModal } from '../vehicle-management/components/ImportModal'; import { TablePagination } from '../vehicle-management/components/TablePagination'; import { @@ -29,6 +30,8 @@ import { type SupplierRecord, } from './types'; import { applyFilters, countKpi } from './utils/filters'; +import { validateContactPhones } from './utils/phone'; +import { cloneCreateForm, isCreateFormDirty } from './utils/createForm'; import annotationSourceDocument from './annotation-source.json'; const DEFAULT_PAGE_SIZE = 10; @@ -41,8 +44,8 @@ type NavigationRef = { function validateCreateForm(form: SupplierCreateForm): string | null { if (!form.contractingCompany) return '请选择签约公司'; if (!form.supplierName.trim()) return '请输入供应商名称'; - if (!form.fullAddress.trim()) return '请填写地址解析'; - return null; + if (!form.contact.trim()) return '请输入联系人'; + return validateContactPhones(form.contactMobile, form.contactPhone); } export default function SupplierManagementApp() { @@ -54,6 +57,8 @@ export default function SupplierManagementApp() { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [createForm, setCreateForm] = useState(EMPTY_CREATE_FORM); + const [createFormBaseline, setCreateFormBaseline] = useState(EMPTY_CREATE_FORM); + const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); const [toast, setToast] = useState(''); const navigationRef = useRef({}); @@ -66,6 +71,7 @@ export default function SupplierManagementApp() { navigationRef.current = { openCreate: () => { setCreateForm(EMPTY_CREATE_FORM); + setCreateFormBaseline(cloneCreateForm(EMPTY_CREATE_FORM)); setView('create'); }, backToList: () => setView('list'), @@ -77,8 +83,6 @@ export default function SupplierManagementApp() { negotiating: countKpi(records, 'negotiating'), terminated: countKpi(records, 'terminated'), expired: countKpi(records, 'expired'), - parts: countKpi(records, 'parts'), - insurance: countKpi(records, 'insurance'), licenseAbnormal: countKpi(records, 'licenseAbnormal'), }), [records]); @@ -112,15 +116,32 @@ export default function SupplierManagementApp() { const handleOpenCreate = () => { setCreateForm(EMPTY_CREATE_FORM); + setCreateFormBaseline(cloneCreateForm(EMPTY_CREATE_FORM)); setView('create'); }; + const leaveCreatePage = () => { + setLeaveConfirmOpen(false); + setCreateForm(EMPTY_CREATE_FORM); + setCreateFormBaseline(cloneCreateForm(EMPTY_CREATE_FORM)); + setView('list'); + }; + + const handleBackFromCreate = () => { + if (isCreateFormDirty(createForm, createFormBaseline)) { + setLeaveConfirmOpen(true); + return; + } + leaveCreatePage(); + }; + const handleSaveCreate = () => { const error = validateCreateForm(createForm); if (error) { showToast(error); return; } + setCreateFormBaseline(cloneCreateForm(createForm)); showToast('已保存草稿(原型演示)'); }; @@ -131,7 +152,7 @@ export default function SupplierManagementApp() { return; } showToast('供应商已提交(原型演示)'); - setView('list'); + leaveCreatePage(); }; const handleImport = (file: File) => { @@ -171,7 +192,7 @@ export default function SupplierManagementApp() { onChange={(patch) => setCreateForm((prev) => ({ ...prev, ...patch }))} onSave={handleSaveCreate} onSubmit={handleSubmitCreate} - onBack={() => setView('list')} + onBack={handleBackFromCreate} /> ) : ( <> @@ -241,6 +262,11 @@ export default function SupplierManagementApp() { setImportOpen(false)} onImport={handleImport} /> + setLeaveConfirmOpen(false)} + onConfirm={leaveCreatePage} + /> { - const parsed = parseFullAddress(fullAddress); + const handleDetailAddressChange = (value: string) => { + const resolved = resolveAddressFromDetail(value); onChange({ - fullAddress, - provinceCity: parsed.provinceCity, - detailAddress: parsed.detailAddress, + detailAddress: value, + region: resolved.region, + provinceCity: resolved.provinceCity, + cityDisplay: resolved.cityDisplay, }); }; @@ -129,7 +130,8 @@ export function CreatePage({
-

主体信息

+

地址信息

+

填写详细地址后自动解析所属区域与城市(直辖市显示为「xx市xx区」)

- + - + handleAddressParse(event.target.value)} + value={form.detailAddress} + placeholder="请输入完整地址,如:浙江省嘉兴市南湖区科技大道1号" + onChange={(event) => handleDetailAddressChange(event.target.value)} + /> + +
+
+ +
+

联系人信息

+
+ + onChange({ contact: event.target.value })} + /> + + + onChange({ contactMobile: event.target.value })} + /> + + + onChange({ contactPhone: event.target.value })} />
@@ -254,8 +286,8 @@ export function CreatePage({