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) => (
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({
+
+