diff --git a/.axhub/make/sidebar-tree.json b/.axhub/make/sidebar-tree.json index d8c64a3..aed63b0 100644 --- a/.axhub/make/sidebar-tree.json +++ b/.axhub/make/sidebar-tree.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-07-01T02:59:09.593Z", + "updatedAt": "2026-07-01T09:58:50.712Z", "prototypes": [ { "id": "folder-1782874576229-r8atbu", @@ -17,12 +17,6 @@ "kind": "item", "title": "车辆资产", "itemKey": "prototypes/vehicle-management" - }, - { - "id": "item:prototypes:vehicle-management-clay", - "kind": "item", - "title": "车辆资产(Clay)", - "itemKey": "prototypes/vehicle-management-clay" } ] }, @@ -69,6 +63,19 @@ "itemKey": "prototypes/contract-template-management" } ] + }, + { + "id": "folder-1782895924552-goclwo", + "kind": "folder", + "title": "加氢站管理", + "children": [ + { + "id": "item:prototypes:oneos-web-h2-station-site", + "kind": "item", + "title": "站点信息", + "itemKey": "prototypes/oneos-web-h2-station-site" + } + ] } ] }, diff --git a/.local/extract-annotation-source/output.json b/.local/extract-annotation-source/output.json index 74fac35..00c4b7d 100644 --- a/.local/extract-annotation-source/output.json +++ b/.local/extract-annotation-source/output.json @@ -1,6 +1,9 @@ { - "title": "合同模板管理 - 原型预览(开发)", - "errors": [], + "title": "站点信息 - 原型预览(开发)", + "errors": [ + "Warning: [antd: Tag] `bordered={false}` is deprecated. Please use `variant=\"filled\"` instead.", + "Warning: [antd: Modal] `destroyOnClose` is deprecated. Please use `destroyOnHidden` instead." + ], "source": { "directory": { "nodes": [ @@ -10,48 +13,6 @@ "title": "ONE-OS 原型导航", "defaultExpanded": true, "children": [ - { - "type": "link", - "id": "nav-link-insurance-procurement", - "title": "保险采购", - "href": "/prototypes/insurance-procurement", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-weekly-report", - "title": "数智部周报", - "href": "/prototypes/oneos-weekly-report", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-vehicle-management", - "title": "车辆管理", - "href": "/prototypes/vehicle-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-contract-template-management", - "title": "合同模板管理(当前)", - "href": "/prototypes/contract-template-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", @@ -73,21 +34,6 @@ "href": "/prototypes/self-operated-business-ledger", "target": "self" }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, { "type": "folder", "id": "nav-folder-prototypes-oneos-web", @@ -198,118 +144,103 @@ }, { "type": "folder", - "id": "ct-doc-root", - "title": "合同模板管理说明", + "id": "h2-site-doc-root", + "title": "站点信息说明", "defaultExpanded": true, "children": [ { "type": "markdown", - "id": "ct-doc-overview", + "id": "h2-site-doc-overview", "title": "模块总览", - "markdown": "## 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 合同模板管理 |\n| 目标用户 | 法务人员 |\n| 核心任务 | 维护 Word 版商用车租赁合同模板,按章节编辑,发布版本并控制启用态 |\n| 数据模型 | 一份 Word 文件 = 一条记录,内含 9 个章节内容 |\n| PRD 文档 | 见目录「PRD 全文 → PRD 完整文档」 |\n| 资源文件 | `src/resources/contract-template-management/PRD.md` |\n\n---\n\n### 产品目标(摘要)\n\n| 目标 | 说明 |\n|---|---|\n| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |\n| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |\n| 支撑业务差异化 | 附件章节按车型绑定;各章节均支持条件条款 |\n| 版本可追溯 | 草稿 / 已发布分离;发布变更写入版本日志 |\n| 多套模板并存 | 租赁创建时由用户选择已启用模板 |\n\n**同步命令**:`node src/prototypes/contract-template-management/scripts/build-annotation-source.mjs`" + "markdown": "## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块 | 加氢站管理 → 站点信息 |\n| 用户 | 运营、财务结算、管理员 |\n| 核心任务 | 站点主数据、运营配置、氢费对账结算 |\n| 页面结构 | 筛选 + KPI + 工具栏 + 列表 + 分页 |\n\n## 2. 关联模块\n\n- **车辆氢费明细**:氢费数据来源与对账回写目标\n- **加氢站合包**:加氢订单、加氢记录仍在合包入口\n\n## 3. 验收重点\n\n1. KPI 高频/低频/无加氢与氢费明细一致\n2. 对账单两阶段流程与跨页回写\n3. 列表布局与租赁合同 vm 规范一致" + }, + { + "type": "link", + "id": "h2-site-link-vh2", + "title": "车辆氢费明细", + "href": "../vehicle-h2-fee-ledger/index.html", + "target": "self" + }, + { + "type": "link", + "id": "h2-site-link-bundle", + "title": "加氢站管理(合包)", + "href": "../oneos-web-h2-station/index.html", + "target": "self" }, { "type": "folder", - "id": "ct-doc-prd", + "id": "h2-site-doc-prd", "title": "PRD 全文", "defaultExpanded": true, "children": [ { "type": "markdown", - "id": "ct-doc-prd-full", + "id": "h2-site-doc-prd-full", "title": "PRD 完整文档", - "markdown": "# 合同模板管理 · 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.0 |\n| 模块名称 | 合同模板管理 |\n| 所属系统 | ONE-OS 业务管理 |\n| 目标读者 | 前端 / 后端 / 测试 / 法务产品对接人 |\n| 交互原型 | `/prototypes/contract-template-management` |\n| 关联原型 | [车辆管理](/prototypes/vehicle-management)(车型主数据口径参考) |\n| 文档状态 | 已对齐原型(启用态 / 版本日志 / 全章节条件条款) |\n\n---\n\n## 1. 背景与目标\n\n### 1.1 背景\n\n商用车租赁合同以 Word 文档形式维护,内容长、结构复杂,包含合同主体与 8 个附件章节。不同车型可能需要不同的附件条款组合。法务人员需要:\n\n- 以 **Word 版式保真** 的方式维护整份合同;\n- 按 **章节** 管理内容,并配置 **适用车型**;\n- 发布 **版本**,通过 **启用/停用** 控制模板是否可供租赁合同选用;\n- 在合同主体及租赁订单~安全责任协议各章节中配置 **条件条款**(按订单车辆命中显隐)。\n\n### 1.2 产品目标\n\n| 目标 | 说明 |\n|---|---|\n| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |\n| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |\n| 支撑业务差异化 | 附件章节按车型绑定;各章节均支持条件条款 |\n| 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本并写入版本日志 |\n| 多套模板并存 | 正式/试用、不同车型等多套已发布模板可同时存在;租赁创建时由用户选择 |\n\n### 1.3 非目标(本期不做)\n\n- 采购合同、服务合同等其他分类的完整业务闭环(数据结构预留 `category`,当前仅启用 `lease`)\n- 在线多人协同编辑、审批流配置界面\n- Word 导出回写(本期以 HTML 结构化存储 + 预览为主)\n- 列表页顶部章节 Tab(**仅编辑页**保留章节 Tab)\n\n---\n\n## 2. 用户与场景\n\n### 2.1 目标用户\n\n| 角色 | 诉求 |\n|---|---|\n| 法务人员(主用户) | 导入/编辑 Word 模板、发布版本、启用/停用模板 |\n| 客服人员(间接用户) | 草拟新合同时从已启用模板中选择(下游「租赁合同管理」消费) |\n| 运营 / 风控(间接用户) | 查看风控红线条款配置情况 |\n\n### 2.2 核心用户故事\n\n1. **作为法务**,我希望导入一份完整 Word 合同,系统自动拆成 9 个章节,以便分章节维护。\n2. **作为法务**,我希望为「租赁订单」等附件章节绑定适用车型,以便创建合同时只纳入匹配车辆的条款。\n3. **作为法务**,我希望在合同主体及租赁订单~安全责任协议各章节为某段落设置「条件条款」并绑定车型,以便不同车型看到不同正文。\n4. **作为法务**,我希望保存草稿不产生版本号,发布后才生成版本号,以便区分未上线内容。\n5. **作为法务**,我希望对已发布模板执行启用/停用,停用后方可编辑,并在发布变更时自动归档旧版快照到版本日志。\n6. **作为客服**,我希望在创建租赁合同时第一步选择要使用的已启用合同模板。\n7. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。\n\n---\n\n## 3. 名词解释\n\n| 名词 | 定义 |\n|---|---|\n| Word 文档 / 模板记录 | 列表主表一行,对应一份完整合同文件 |\n| 章节 | 合同内的逻辑切片,共 9 个(1 个主体 + 8 个附件) |\n| 草稿 | `status = draft`,无版本号 |\n| 已发布 | `status = published`,有版本号 `vX.XX` |\n| 启用 | `enabled = true` 的已发布模板,可供租赁合同创建选用 |\n| 停用 | `enabled = false` 的已发布模板,不可编辑/删除,不可被租赁创建选用 |\n| 版本日志 | `versionLogs[]`,记录发布变更前自动归档的快照 |\n| 锁定区 | 编辑区内不可随意修改的段落,HTML 标记 `data-inline-lock=\"1\"` |\n| 风控红线条款 | 需重点合规关注的条款,HTML 标记 `data-risk-redline=\"1\"` |\n| 条件条款 | 各章节可用,按订单车辆命中显隐,HTML 标记 `data-vehicle-clause=\"1\"` |\n| 适用车型绑定 | 附件章节级别配置,决定该章节是否纳入某份合同 |\n| 模板占位符 | 形如 `{{contractCode}}` 的变量,创建合同时由系统替换 |\n\n---\n\n## 4. 信息架构\n\n```text\n合同模板管理\n├── 列表页\n│ ├── 筛选区\n│ ├── 工具栏(新增)\n│ └── 嵌套表格\n│ ├── 主表:Word 文件\n│ └── 子表:9 个章节(展开行)\n└── 编辑 / 新增页\n ├── 顶栏(返回、保存、保存并发布)\n ├── 章节 Tab(仅本页展示)\n ├── 条件条款说明(各章节均可使用工具栏「条件」)\n ├── 适用车型绑定(附件章节 Tab 时显示)\n └── 工作区\n ├── 左侧:Word 版式编辑区\n └── 右侧:A4 实时预览\n```\n\n---\n\n## 5. 数据模型\n\n### 5.1 模板记录(Word 文档级)\n\n一条记录代表 **一份 Word 合同文件**,不按章节拆成多行。\n\n```typescript\ninterface ContractTemplateDocument {\n id: string; // 唯一 ID\n category: 'lease'; // 合同分类,当前固定商用车租赁\n fileName: string; // Word 文件名,如「现代18吨车型-数智中心版.docx」\n versionName: string; // 展示用版本名称\n versionNo: string | null; // 已发布:v1.00;草稿:null\n status: 'draft' | 'published';\n enabled: boolean; // 已发布模板是否启用(草稿恒为 false)\n versionLogs: VersionLogEntry[]; // 版本变更日志与快照\n sectionContents: Record; // 各章节 HTML\n sectionVehicleBindings: Record; // 各章节车型绑定\n creator: string;\n createTime: string; // YYYY-MM-DD HH:mm\n updater: string;\n updateTime: string;\n remark?: string; // 内部备注(列表不展示)\n}\n\ninterface VersionLogEntry {\n id: string;\n versionNo: string; // 归档时的版本号\n action: string; // 如「发布变更」\n operator: string;\n time: string;\n remark?: string;\n snapshot: {\n fileName: string;\n versionName: string;\n versionNo: string;\n sectionContents: Record;\n sectionVehicleBindings: Record;\n remark?: string;\n archivedAt: string;\n };\n}\n\ninterface VehicleBinding {\n brand: string; // 品牌,如「现代」\n model: string; // 型号,如「18吨氢燃料电池车」\n}\n```\n\n### 5.2 章节枚举(SectionKey)\n\n| key | 章节名称 | 车型绑定 | 条件条款 |\n|---|---|:---:|:---:|\n| `main` | 合同主体 | 否 | 是 |\n| `lease_order` | 租赁订单 | 是 | 是 |\n| `vehicle_cost` | 车辆产生的费用 | 是 | 是 |\n| `safety_notice` | 安全责任告知书 | 是 | 是 |\n| `dynamic_supervision` | 重型普通货运车辆租赁动态监管责任告知与确认书 | 是 | 是 |\n| `safety_commitment` | 安全责任承诺书 | 是 | 是 |\n| `vehicle_spec` | 车辆使用规范和驾驶员要求 | 是 | 是 |\n| `authorization` | 授权委托书 | 是 | 是 |\n| `safety_agreement` | 安全责任协议 | 是 | 是 |\n\n### 5.3 章节内容 HTML 标记规范\n\n研发需保证存储、渲染、统计口径一致:\n\n| 类型 | 选择器 / 属性 | 统计函数 |\n|---|---|---|\n| 锁定区 | `.ct-inline-lock[data-inline-lock=\"1\"]` | `countLockRegions` |\n| 风控红线 | `.ct-risk-redline[data-risk-redline=\"1\"]` | `countRiskRedlineRegions` |\n| 条件条款 | `.ct-vehicle-clause[data-vehicle-clause=\"1\"]` | `countVehicleClauseRegions` |\n| 条件条款车型 | `data-vehicle-bindings`(JSON 数组) | 预览命中逻辑 |\n\n---\n\n## 6. 业务规则(全局)\n\n### 6.1 版本号规则\n\n- 格式:`v{major}.{minor}`,如 `v1.00`、`v1.01`\n- **仅发布时生成**;保存草稿时 `versionNo = null`\n- 同分类下递增:取当前已发布版本最大 minor +1;minor ≥ 99 时 major +1、minor 归 0\n- 首版默认 `v1.00`\n\n### 6.2 保存与发布\n\n| 操作 | 新增模式 | 编辑草稿 | 编辑已发布(须先停用) |\n|---|---|---|---|\n| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**保持已发布与版本号**,仍为停用态 |\n| **保存并发布** | 新建已发布记录 + 版本号,默认启用 | 原记录变已发布 + 版本号,默认启用 | **同记录升级版本号**;旧内容写入 `versionLogs` 快照;发布后默认停用,需手动启用 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n**离开编辑**:有未保存修改时返回列表需二次确认。\n\n### 6.3 启用 / 停用规则\n\n1. 仅 **已发布** 模板具有启用态;草稿显示「—」。\n2. **启用中**:不可编辑、不可删除;可供租赁合同创建选用。\n3. **停用**:需二次确认(提示可能导致标准合同无法正常使用);停用后可编辑、可删除;租赁创建不可选用。\n4. **启用**:直接生效,无需二次确认。\n5. 编辑已发布模板并发布后,模板自动变为 **停用**,需重新启用后下游才可选。\n\n### 6.4 版本日志规则\n\n1. 对已发布模板执行「保存并发布」前,系统将 **当前内容自动归档** 为快照写入 `versionLogs`。\n2. 列表操作列提供「版本日志」,可查看历史版本号、操作人、时间与快照正文(只读)。\n3. 列表主表 **一行对应一份 Word 文件**,历史版本通过日志追溯,不另增列表行。\n\n### 6.5 删除规则\n\n- **启用中** 的已发布模板不可删除。\n- 其余记录可删除;删除前二次确认;删除后不可恢复。\n\n### 6.6 列表排序\n\n按 `updateTime` 降序。\n\n### 6.7 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 下拉选择(支持输入模糊搜索候选项);精确匹配 `fileName` 或 `versionName` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 启用状态 | `enabled`:仅已发布且启用;`disabled`:仅已发布且停用 |\n\n> **已移除**:筛选项「适用品牌/适用车型」、主表列「备注」、「设为标准模板」能力。\n\n---\n\n## 7. 列表页需求\n\n### 7.1 页面结构\n\n筛选区 + 工具栏 + 嵌套表格。**列表页不展示章节 Tab。**\n\n### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 可搜索下拉 | 选项来自现有模板名称;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认\n\n### 7.3 工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 新增 | 进入新增页,默认章节为「合同主体」,模式为 create |\n\n### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同名称 | `fileName` | 超长省略,仅展示文件名 |\n| 版本号 | `versionNo` | 草稿显示「—」 |\n| 状态 | `status` | 已发布 / 草稿 Tag |\n| 启用状态 | `enabled` | 已发布:启用/停用;草稿:「—」 |\n| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 启用 / 停用 | 已发布 | 停用需确认;启用直接生效 |\n| 版本日志 | 已发布 | 查看历史快照 |\n| 编辑 | 草稿,或已发布且停用 | 启用中按钮禁用 |\n| 删除 | 草稿,或已发布且停用 | 启用中按钮禁用;删除前确认 |\n\n### 7.6 子表(展开行)\n\n主表每行可展开,展示该 Word 文档下 **9 个章节** 的摘要(原生 HTML 表,非嵌套 Ant Design Table)。\n\n| 列名 | 说明 |\n|---|---|\n| 章节 | 章节名称 |\n| 适用车型 | 附件章节展示绑定列表,格式「品牌 · 型号」;多个用「;」连接;未绑定显示「全部车型」;合同主体显示「—」 |\n| 风控条款数 | 该章节 HTML 内风控红线标记数量 |\n| 条件条款数 | 按各章节 HTML 统计条件条款标记数量 |\n| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |\n\n---\n\n## 8. 编辑 / 新增页需求\n\n### 8.1 页面结构\n\n顶栏 → 章节 Tab → 条件说明 / 车型绑定(按章节显隐)→ 左右分栏工作区。\n\n### 8.2 顶栏\n\n| 按钮 | 行为 |\n|---|---|\n| ← 返回列表 | 回到列表页,不自动保存 |\n| 保存 | 见 §6.2 |\n| 保存并发布 | 见 §6.2 |\n\n### 8.3 章节 Tab\n\n- 共 9 个章节;**前 4 个直显**,其余收入「更多」下拉。\n- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。\n- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**。\n- **其他 Tab**:仅展示该章节切片内容。\n\n章节顺序:\n\n1. 合同主体 \n2. 租赁订单 \n3. 车辆产生的费用 \n4. 安全责任告知书 \n5. 重型普通货运车辆租赁动态监管责任告知与确认书 \n6. 安全责任承诺书 \n7. 车辆使用规范和驾驶员要求 \n8. 授权委托书 \n9. 安全责任协议 \n\n### 8.4 条件条款说明(合同主体及租赁订单~安全责任协议)\n\n展示说明卡片,文案要点:\n\n> 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。适用于合同主体及全部附件章节。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。\n\n### 8.5 适用车型绑定(仅附件章节 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 品牌下拉 | 来自车型主数据 |\n| 型号下拉 | 随品牌联动 |\n| 添加车型 | 追加到当前章节绑定列表 |\n| 已绑定 Tag | 可删除单项 |\n| 空状态文案 | 「当前未绑定车型,表示全部车型适用」 |\n\n**规则**:\n\n- 绑定保存在 `sectionVehicleBindings[当前章节 key]`。\n- 切换章节 Tab 时加载对应章节的绑定到编辑表单。\n- 保存时将各章节绑定一并写入文档记录。\n\n### 8.6 编辑区(左侧)\n\n#### 8.6.1 面板头操作\n\n| 按钮 | 说明 |\n|---|---|\n| { } 模板占位符 | 弹层展示变量分组,点击插入光标处 |\n| 导入 Word 模板 | 接受 `.doc` / `.docx`;导入后按 §9 拆分章节 |\n\n#### 8.6.2 编辑能力\n\n- **Word 版式保真**:A4 分页编辑,保留 Word 导出样式类名。\n- **富文本工具栏**(节选):\n - 字号、加粗、对齐等基础格式\n - **锁定**:选中内容设为锁定区\n - **解锁**:解除锁定\n - **风控**:标记风控红线条款\n - **条件**:在各章节选中段落绑定车型条件\n\n#### 8.6.3 条件条款交互\n\n**适用范围**:合同主体、租赁订单、车辆产生的费用、安全责任告知书、重型普通货运车辆租赁动态监管责任告知与确认书、安全责任承诺书、车辆使用规范和驾驶员要求、授权委托书、安全责任协议。\n\n1. 用户选中段落或文字;\n2. 点击工具栏「条件」;\n3. 为选中内容绑定一个或多个「品牌 · 型号」;\n4. 生成 `.ct-vehicle-clause` 包裹结构,并写入 `data-vehicle-bindings`;\n5. 创建合同时:订单车辆 **任意一辆** 命中绑定车型 → 段落纳入正文;否则隐藏。\n\n### 8.7 实时预览(右侧)\n\n#### 8.7.1 预览模式\n\n| 模式 | 说明 |\n|---|---|\n| 连续预览 | 多页连续滚动 |\n| 单页预览 | 按 A4 单页翻页 |\n\n#### 8.7.2 预览顶栏统计\n\n展示当前章节:\n\n`锁定 N 处 · 风控红线 M 处 · 条件条款 K 处`\n\n当前章节支持条件且已选模拟车型时追加:`(预览命中 X)`\n\n#### 8.7.3 模拟车型\n\n- 在合同主体及全部附件章节可用;多选下拉,选项来自车型主数据(品牌 · 型号)。\n- 未选车型时:**隐藏**所有条件条款段落(与创建合同时无车辆命中行为一致)。\n- 选中后按命中规则过滤并展开条件条款正文。\n\n#### 8.7.4 占位符预览\n\n预览时替换部分占位符为样例值,如:\n\n- `{{customerName}}` → `【承租方公司名称】`\n- `{{contractCode}}` → `20260001`\n- 甲方相关变量按预览所选出租方公司填充\n\n### 8.8 空状态\n\n编辑区无内容时,预览区展示空状态引导;新增模式下突出「导入 Word 模板」按钮。\n\n---\n\n## 9. Word 导入与章节拆分\n\n### 9.1 导入范围\n\n- 一次导入 **整份合同**;\n- 导入后写入各 `sectionContents[key]`;\n- 默认进入「合同主体」章节编辑。\n\n### 9.2 拆分策略\n\n1. 对导入 HTML 做 Word 导出清洗(`sanitizeWordExportHtml`);\n2. 从附件章节起,按 **章节标记字符串** 定位切分点;\n3. 切分点优先落在 `

`、``、`` 之后,避免打断标签;\n4. `main` = 第一个切分点之前的全文;\n5. 各附件章节 = 相邻切分点之间的片段。\n\n### 9.3 章节标记(节选)\n\n研发需维护可扩展的 `markers` 列表,示例:\n\n| 章节 | 标记示例 |\n|---|---|\n| 租赁订单 | `附件 1:租赁订单`、`附件1:租赁订单` |\n| 车辆产生的费用 | `附件2:车辆产生的费用` |\n| 安全责任告知书 | `附件3:安全责任告知书` |\n| … | 完整列表见原型 `DOCUMENT_SECTION_TABS` |\n\n### 9.4 误匹配过滤\n\n以下情况 **不应** 作为切分点:\n\n- 标记出现在「详见附件…」「以附件…」等引用语境;\n- 标记过短且不在 `` 标题语境内;\n- 标记前为「(」「、」等不完整标题上下文。\n\n---\n\n## 10. 模板占位符\n\n### 10.1 变量分组\n\n**甲方签约主体**(创建合同时按所选签约公司自动替换):\n\n| key | 标签 |\n|---|---|\n| `lessorName` | 甲方全称 |\n| `lessorAddress` | 甲方地址 |\n| `lessorContact` | 甲方联系人 |\n| `lessorPhone` | 甲方电话 |\n| `lessorEmail` | 甲方邮箱 |\n| `lessorAccountName` | 收款户名 |\n| `lessorBankName` | 开户行 |\n| `lessorBankAccount` | 银行账号 |\n\n**合同通用**(创建合同时由系统或客服填写):\n\n| key | 标签 |\n|---|---|\n| `contractCode` | 合同编号 |\n| `customerName` | 乙方公司全称 |\n| `creditCode` | 乙方统一社会信用代码 |\n| `customerAddress` | 乙方地址 |\n| `customerContact` | 乙方联系人 |\n| `customerPhone` | 乙方联系电话 |\n| `signDateYear` | 签订年份 |\n| `signDateMonth` | 签订月份 |\n| `signDateDay` | 签订日期 |\n\n### 10.2 插入规则\n\n- 编辑区光标处插入 `{{key}}`;\n- 存储为 HTML 文本,不做运行时强校验;\n- 缺失变量创建合同时按空字符串或业务默认值处理(由下游定义)。\n\n---\n\n## 11. 车型主数据\n\n品牌 · 型号下拉数据与车辆管理模块保持一致,当前原型内置示例:\n\n| 品牌 | 型号示例 |\n|---|---|\n| 现代 | 18吨氢燃料电池车、帕力安牌4.5吨冷链车 |\n| 苏龙 | 9.6米氢燃料电池车 |\n| 柳汽乘龙 | 9.6米氢燃料电池车 |\n| 飞驰 | 集卡头、半挂车 |\n| 跃进 | 4.2米冷链车 |\n| 宇通 | 氢燃料电池客车 |\n\n> 正式环境应由车辆主数据服务提供;绑定存储仅保存品牌 + 型号字符串。\n\n---\n\n## 12. 与其他模块的关系\n\n```mermaid\nflowchart LR\n A[合同模板管理] -->|已启用模板 + 章节内容| B[租赁合同创建]\n A -->|车型绑定口径| C[车辆管理]\n A -->|占位符变量定义| B\n B -->|订单车辆列表| A\n```\n\n| 模块 | 关系 |\n|---|---|\n| 租赁合同创建 | 创建流程第一步选择 **已启用** 的已发布模板;按订单车辆过滤条件条款与附件章节 |\n| 车辆管理 | 车型品牌/型号枚举口径一致 |\n| 非标准合同审批 | 合同正文出现 `data-nonstd-trigger` 时触发(标记规范已预留) |\n\n---\n\n## 13. 权限与审计(建议)\n\n| 能力 | 建议权限 |\n|---|---|\n| 查看列表 | 法务、客服(只读) |\n| 新增 / 编辑 / 发布 | 法务 |\n| 启用 / 停用 | 法务 |\n| 删除 | 法务负责人 |\n\n**审计字段**:`creator`、`createTime`、`updater`、`updateTime` 必须在每次保存时更新;发布变更写入 `versionLogs`。\n\n---\n\n## 14. 接口需求(建议)\n\n### 14.1 列表查询\n\n```\nGET /api/contract-templates?category=lease&fileName=&status=&creator=&enabled=&page=1&pageSize=10\n```\n\n响应需包含主表字段;章节摘要可列表内嵌或展开时按需加载。\n\n### 14.2 详情\n\n```\nGET /api/contract-templates/{id}\n```\n\n返回完整 `sectionContents`、`sectionVehicleBindings`、`versionLogs`。\n\n### 14.3 创建 / 更新\n\n```\nPOST /api/contract-templates\nPUT /api/contract-templates/{id}\n```\n\n请求体含章节 HTML 与绑定;`publish: boolean` 控制是否发布。\n\n### 14.4 启用 / 停用\n\n```\nPOST /api/contract-templates/{id}/enable\nPOST /api/contract-templates/{id}/disable\n```\n\n停用前需二次确认;启用中记录拒绝编辑/删除类写操作(由业务层校验)。\n\n### 14.5 删除\n\n```\nDELETE /api/contract-templates/{id}\n```\n\n拒绝删除 `enabled=true` 的记录。\n\n### 14.6 Word 导入\n\n```\nPOST /api/contract-templates/import-word\nContent-Type: multipart/form-data\n```\n\n返回拆分后的 `sectionContents` 供前端预览确认(或后端直接落库,由研发评估)。\n\n---\n\n## 15. 非功能需求\n\n| 类别 | 要求 |\n|---|---|\n| 性能 | 单份合同 HTML ≤ 2MB 时,章节切换 < 500ms(前端缓存) |\n| 兼容性 | Chrome / Edge 最新两个大版本;编辑区最低宽度 1280px 推荐 |\n| 可访问性 | 表单控件有 `aria-label`;操作按钮具备可读文案 |\n| 安全 | 导入 Word 需做 XSS 清洗;仅白名单标签/属性入库 |\n| 可追溯 | 已发布版本不可原地覆盖内容,只能新发版 |\n\n---\n\n## 16. 验收标准\n\n### 16.1 列表页\n\n- [ ] 筛选项包含:合同名称(可搜索下拉)、状态、创建人、启用状态\n- [ ] 主表含启用状态列;操作含启用/停用、版本日志、编辑、删除\n- [ ] 启用中模板编辑/删除按钮禁用;停用需二次确认\n- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表\n- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态\n\n### 16.2 编辑页\n\n- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多\n- [ ] 导入 Word 后 9 章节正确拆分\n- [ ] 附件章节可绑定车型;各章节均可设条件条款\n- [ ] 预览支持连续/单页切换;各章节可模拟车型验证条件条款\n- [ ] 保存草稿无版本号;发布生成版本号\n- [ ] 编辑已发布(停用后)并发布时版本号递增,旧版写入版本日志\n- [ ] 编辑已发布(停用后)点「保存」不改变发布态与版本号\n- [ ] 有未保存修改时返回列表需确认\n\n### 16.3 跨模块与数据一致性\n\n- [ ] 模板管理停用后,租赁合同创建页不可再选用该模板\n- [ ] 列表子表「风控条款数」与编辑区统计一致\n- [ ] 列表子表「条件条款数」与各章节实际标记数一致\n\n### 16.4 标注与文档(原型)\n\n- [ ] 原型已接入 `@axhub/annotation`,关键区域有 `data-annotation-id`\n- [ ] 目录路由可切换列表页 / 新增页\n- [ ] `PRD.md` 与 `annotation-source.json` 与当前实现一致\n\n---\n\n## 17. 原型实现映射(供研发对照)\n\n| 能力 | 原型文件 / 符号 |\n|---|---|\n| 主逻辑 | `src/prototypes/contract-template-management/ContractTemplate.jsx` |\n| 跨模块模板状态 | `src/prototypes/contract-template-management/contract-template-store.js` |\n| 租赁侧消费目录 | `src/prototypes/contract-template-management/contract-template-catalog.js` |\n| 入口与标注 | `src/prototypes/contract-template-management/index.tsx` |\n| 标注数据 | `src/prototypes/contract-template-management/annotation-source.json` |\n| 下游选用 | `src/prototypes/lease-contract-management/LeaseContractCreate.jsx` |\n| 占位符与出租方 | `src/prototypes/contract-template-management/contract-template-vars.js` |\n| 样式 | `src/prototypes/contract-template-management/styles/` |\n\n> **原型限制**:模板数据存于内存 store,刷新页面后恢复种子数据;同一会话内模板管理与租赁创建联动。\n\n---\n\n## 18. 附录:停用确认弹窗(原文案)\n\n**标题**:确认停用\n\n**正文**:\n\n> 停用后,该模板将不可在租赁合同创建时选用,是否确定?\n\n**按钮**:取消 / 确定停用\n\n---\n\n## 19. 修订记录\n\n| 版本 | 日期 | 作者 | 说明 |\n|---|---|---|---|\n| v1.1 | 2026-06-29 | 产品 | 移除标准模板模型,改为启用/停用 + 版本日志;列表筛选与操作同步;各章节支持条件条款 |\n| v1.0 | 2026-06-25 | 产品 | 首版:Word 文档级模型、嵌套列表、章节拆分与条件条款 |\n", - "markdownPath": "src/resources/contract-template-management/PRD.md" + "markdown": "# 加氢站管理 · 站点信息 — 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.0 |\n| 模块名称 | 加氢站管理 → 站点信息 |\n| 所属系统 | ONE-OS Web 端 |\n| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 关联原型 | [车辆氢费明细](/prototypes/vehicle-h2-fee-ledger)、[加氢站合包](/prototypes/oneos-web-h2-station) |\n| 文档状态 | 已对齐原型 |\n\n---\n\n## 1. 背景与目标\n\n### 1.1 业务背景\n\n加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。\n\n### 1.2 本期目标\n\n- 站点台账的查询、新建、编辑、查看、删除与批量导入\n- 营业状态、成本价格、预付余额等运营配置\n- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写\n\n### 1.3 用户角色\n\n| 角色 | 典型诉求 |\n|---|---|\n| 加氢站运营 | 维护站点资料、营业与价格;查看加氢量与余额 |\n| 财务/结算 | 按周期生成对账单、登记收票、完成站点付款闭环 |\n| 管理员 | 批量导入站点、删除无效站点、配置系统账号 |\n\n### 1.4 非目标(本期不做)\n\n- 对账单审批流、ERP 自动过账\n- 发票 OCR 识别与验真\n- 加氢订单、加氢记录(仍在合包 `oneos-web-h2-station`)\n- 后端接口联调(原型使用 Mock + 共享 Store)\n\n---\n\n## 2. 信息架构\n\n```text\n站点信息\n├── 列表页\n│ ├── 筛选区(4 项)\n│ ├── KPI 卡片(6 张)\n│ ├── 工具栏\n│ └── 站点台账表 + 分页\n├── 新建 / 编辑(同页内嵌视图)\n│ ├── 基本信息\n│ ├── 系统账号绑定\n│ └── 供应商 / 付款信息\n└── 弹窗 / 抽屉\n ├── 查看详情\n ├── 营业状态 / 价格配置\n ├── 加氢量下钻 / 预付余额下钻\n ├── 生成对账单(两阶段)\n └── 查看对账记录\n```\n\n---\n\n## 3. 列表与 KPI\n\n> 详见 `.spec/requirements-prd-list.md`\n\n- 筛选:名称、签约、地区、营业状态\n- KPI:全部 / 签约·普通 / 高频≥3 / 低频1–2 / 无加氢0\n- 列表字段:名称地址、合作时间、营业、成本价、加氢次数/量、预付余额、联系方式、操作\n- 设计规范:与租赁合同管理一致的 vm 布局与 Inter 字体\n\n---\n\n## 4. 站点主数据\n\n### 4.1 新建站点\n\n同页内嵌视图:基本信息、系统账号绑定(可选)、供应商信息(新建或关联)。\n\n### 4.2 编辑 / 查看\n\n| 模式 | 交互 |\n|---|---|\n| 编辑 | 整页表单;系统账号仅 admin 可改 |\n| 查看 | 弹窗分块展示站点、账号、供应商、付款信息 |\n\n### 4.3 批量导入\n\n下载 CSV 模板 → 上传 → 解析预览 → 确认导入;站点名称不可重复。\n\n---\n\n## 5. 运营配置\n\n### 5.1 营业状态\n\n卡片式弹窗:营业状态、营业时间按钮组;非全天须配置时段;展示状态变更记录。\n\n### 5.2 价格配置\n\n配置成本价格(元/kg)与生效时间;到达生效时间后刷新列表「当前成本价格」;展示价格调整记录。\n\n### 5.3 下钻分析\n\n- **加氢量下钻**:统计卡 + 明细表 + 导出,字段对齐车辆氢费明细\n- **预付余额下钻**:充值/加氢流水 + 趋势图 + 导出\n\n---\n\n## 6. 对账与结算(重点)\n\n> 详见 `.spec/requirements-prd-statement.md`\n\n与「车辆氢费明细」配合完成站点侧氢费结算闭环:\n\n1. 拉取已对账且未结算的记录\n2. 两阶段:选期生成 → 填收票提交\n3. 提交后更新预付余额、写对账历史、回写台账(对账日期、收票日期、已付款)\n\n---\n\n## 7. 数据与集成\n\n| 能力 | 实现 |\n|---|---|\n| 氢费明细 | `src/common/h2VehicleLedgerBridge.js` |\n| 对账回写 | `H2_STATION_STATEMENT_LEDGER_UPDATES` |\n| 页面实现 | `src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx` |\n| 独立入口 | `src/prototypes/oneos-web-h2-station-site/index.tsx` |\n\n---\n\n## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅已对账未结算记录可进入对账单\n- [ ] 两阶段交互与必填校验完整\n- [ ] 提交后回写车辆氢费明细可跨页验收\n\n### 8.3 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---\n\n## 9. 修订记录\n\n| 版本 | 日期 | 说明 |\n|---|---|---|\n| v1.0 | 2026-07-01 | 独立入口、vm 规范对齐、标注目录与 PRD 接入 |\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md" }, { "type": "markdown", - "id": "ct-doc-prd-overview", - "title": "背景 · 数据模型 · 业务规则", - "markdown": "## 1. 背景与目标\n\n### 1.1 背景\n\n商用车租赁合同以 Word 文档形式维护,内容长、结构复杂,包含合同主体与 8 个附件章节。不同车型可能需要不同的附件条款组合。法务人员需要:\n\n- 以 **Word 版式保真** 的方式维护整份合同;\n- 按 **章节** 管理内容,并配置 **适用车型**;\n- 发布 **版本**,通过 **启用/停用** 控制模板是否可供租赁合同选用;\n- 在合同主体及租赁订单~安全责任协议各章节中配置 **条件条款**(按订单车辆命中显隐)。\n\n### 1.2 产品目标\n\n| 目标 | 说明 |\n|---|---|\n| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |\n| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |\n| 支撑业务差异化 | 附件章节按车型绑定;各章节均支持条件条款 |\n| 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本并写入版本日志 |\n| 多套模板并存 | 正式/试用、不同车型等多套已发布模板可同时存在;租赁创建时由用户选择 |\n\n### 1.3 非目标(本期不做)\n\n- 采购合同、服务合同等其他分类的完整业务闭环(数据结构预留 `category`,当前仅启用 `lease`)\n- 在线多人协同编辑、审批流配置界面\n- Word 导出回写(本期以 HTML 结构化存储 + 预览为主)\n- 列表页顶部章节 Tab(**仅编辑页**保留章节 Tab)\n\n---\n\n---\n\n## 2. 用户与场景\n\n### 2.1 目标用户\n\n| 角色 | 诉求 |\n|---|---|\n| 法务人员(主用户) | 导入/编辑 Word 模板、发布版本、启用/停用模板 |\n| 客服人员(间接用户) | 草拟新合同时从已启用模板中选择(下游「租赁合同管理」消费) |\n| 运营 / 风控(间接用户) | 查看风控红线条款配置情况 |\n\n### 2.2 核心用户故事\n\n1. **作为法务**,我希望导入一份完整 Word 合同,系统自动拆成 9 个章节,以便分章节维护。\n2. **作为法务**,我希望为「租赁订单」等附件章节绑定适用车型,以便创建合同时只纳入匹配车辆的条款。\n3. **作为法务**,我希望在合同主体及租赁订单~安全责任协议各章节为某段落设置「条件条款」并绑定车型,以便不同车型看到不同正文。\n4. **作为法务**,我希望保存草稿不产生版本号,发布后才生成版本号,以便区分未上线内容。\n5. **作为法务**,我希望对已发布模板执行启用/停用,停用后方可编辑,并在发布变更时自动归档旧版快照到版本日志。\n6. **作为客服**,我希望在创建租赁合同时第一步选择要使用的已启用合同模板。\n7. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。\n\n---\n\n---\n\n## 3. 名词解释\n\n| 名词 | 定义 |\n|---|---|\n| Word 文档 / 模板记录 | 列表主表一行,对应一份完整合同文件 |\n| 章节 | 合同内的逻辑切片,共 9 个(1 个主体 + 8 个附件) |\n| 草稿 | `status = draft`,无版本号 |\n| 已发布 | `status = published`,有版本号 `vX.XX` |\n| 启用 | `enabled = true` 的已发布模板,可供租赁合同创建选用 |\n| 停用 | `enabled = false` 的已发布模板,不可编辑/删除,不可被租赁创建选用 |\n| 版本日志 | `versionLogs[]`,记录发布变更前自动归档的快照 |\n| 锁定区 | 编辑区内不可随意修改的段落,HTML 标记 `data-inline-lock=\"1\"` |\n| 风控红线条款 | 需重点合规关注的条款,HTML 标记 `data-risk-redline=\"1\"` |\n| 条件条款 | 各章节可用,按订单车辆命中显隐,HTML 标记 `data-vehicle-clause=\"1\"` |\n| 适用车型绑定 | 附件章节级别配置,决定该章节是否纳入某份合同 |\n| 模板占位符 | 形如 `{{contractCode}}` 的变量,创建合同时由系统替换 |\n\n---\n\n---\n\n## 4. 信息架构\n\n```text\n合同模板管理\n├── 列表页\n│ ├── 筛选区\n│ ├── 工具栏(新增)\n│ └── 嵌套表格\n│ ├── 主表:Word 文件\n│ └── 子表:9 个章节(展开行)\n└── 编辑 / 新增页\n ├── 顶栏(返回、保存、保存并发布)\n ├── 章节 Tab(仅本页展示)\n ├── 条件条款说明(各章节均可使用工具栏「条件」)\n ├── 适用车型绑定(附件章节 Tab 时显示)\n └── 工作区\n ├── 左侧:Word 版式编辑区\n └── 右侧:A4 实时预览\n```\n\n---\n\n---\n\n## 5. 数据模型\n\n### 5.1 模板记录(Word 文档级)\n\n一条记录代表 **一份 Word 合同文件**,不按章节拆成多行。\n\n```typescript\ninterface ContractTemplateDocument {\n id: string; // 唯一 ID\n category: 'lease'; // 合同分类,当前固定商用车租赁\n fileName: string; // Word 文件名,如「现代18吨车型-数智中心版.docx」\n versionName: string; // 展示用版本名称\n versionNo: string | null; // 已发布:v1.00;草稿:null\n status: 'draft' | 'published';\n enabled: boolean; // 已发布模板是否启用(草稿恒为 false)\n versionLogs: VersionLogEntry[]; // 版本变更日志与快照\n sectionContents: Record; // 各章节 HTML\n sectionVehicleBindings: Record; // 各章节车型绑定\n creator: string;\n createTime: string; // YYYY-MM-DD HH:mm\n updater: string;\n updateTime: string;\n remark?: string; // 内部备注(列表不展示)\n}\n\ninterface VersionLogEntry {\n id: string;\n versionNo: string; // 归档时的版本号\n action: string; // 如「发布变更」\n operator: string;\n time: string;\n remark?: string;\n snapshot: {\n fileName: string;\n versionName: string;\n versionNo: string;\n sectionContents: Record;\n sectionVehicleBindings: Record;\n remark?: string;\n archivedAt: string;\n };\n}\n\ninterface VehicleBinding {\n brand: string; // 品牌,如「现代」\n model: string; // 型号,如「18吨氢燃料电池车」\n}\n```\n\n### 5.2 章节枚举(SectionKey)\n\n| key | 章节名称 | 车型绑定 | 条件条款 |\n|---|---|:---:|:---:|\n| `main` | 合同主体 | 否 | 是 |\n| `lease_order` | 租赁订单 | 是 | 是 |\n| `vehicle_cost` | 车辆产生的费用 | 是 | 是 |\n| `safety_notice` | 安全责任告知书 | 是 | 是 |\n| `dynamic_supervision` | 重型普通货运车辆租赁动态监管责任告知与确认书 | 是 | 是 |\n| `safety_commitment` | 安全责任承诺书 | 是 | 是 |\n| `vehicle_spec` | 车辆使用规范和驾驶员要求 | 是 | 是 |\n| `authorization` | 授权委托书 | 是 | 是 |\n| `safety_agreement` | 安全责任协议 | 是 | 是 |\n\n### 5.3 章节内容 HTML 标记规范\n\n研发需保证存储、渲染、统计口径一致:\n\n| 类型 | 选择器 / 属性 | 统计函数 |\n|---|---|---|\n| 锁定区 | `.ct-inline-lock[data-inline-lock=\"1\"]` | `countLockRegions` |\n| 风控红线 | `.ct-risk-redline[data-risk-redline=\"1\"]` | `countRiskRedlineRegions` |\n| 条件条款 | `.ct-vehicle-clause[data-vehicle-clause=\"1\"]` | `countVehicleClauseRegions` |\n| 条件条款车型 | `data-vehicle-bindings`(JSON 数组) | 预览命中逻辑 |\n\n---\n\n---\n\n## 6. 业务规则(全局)\n\n### 6.1 版本号规则\n\n- 格式:`v{major}.{minor}`,如 `v1.00`、`v1.01`\n- **仅发布时生成**;保存草稿时 `versionNo = null`\n- 同分类下递增:取当前已发布版本最大 minor +1;minor ≥ 99 时 major +1、minor 归 0\n- 首版默认 `v1.00`\n\n### 6.2 保存与发布\n\n| 操作 | 新增模式 | 编辑草稿 | 编辑已发布(须先停用) |\n|---|---|---|---|\n| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**保持已发布与版本号**,仍为停用态 |\n| **保存并发布** | 新建已发布记录 + 版本号,默认启用 | 原记录变已发布 + 版本号,默认启用 | **同记录升级版本号**;旧内容写入 `versionLogs` 快照;发布后默认停用,需手动启用 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n**离开编辑**:有未保存修改时返回列表需二次确认。\n\n### 6.3 启用 / 停用规则\n\n1. 仅 **已发布** 模板具有启用态;草稿显示「—」。\n2. **启用中**:不可编辑、不可删除;可供租赁合同创建选用。\n3. **停用**:需二次确认(提示可能导致标准合同无法正常使用);停用后可编辑、可删除;租赁创建不可选用。\n4. **启用**:直接生效,无需二次确认。\n5. 编辑已发布模板并发布后,模板自动变为 **停用**,需重新启用后下游才可选。\n\n### 6.4 版本日志规则\n\n1. 对已发布模板执行「保存并发布」前,系统将 **当前内容自动归档** 为快照写入 `versionLogs`。\n2. 列表操作列提供「版本日志」,可查看历史版本号、操作人、时间与快照正文(只读)。\n3. 列表主表 **一行对应一份 Word 文件**,历史版本通过日志追溯,不另增列表行。\n\n### 6.5 删除规则\n\n- **启用中** 的已发布模板不可删除。\n- 其余记录可删除;删除前二次确认;删除后不可恢复。\n\n### 6.6 列表排序\n\n按 `updateTime` 降序。\n\n### 6.7 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 下拉选择(支持输入模糊搜索候选项);精确匹配 `fileName` 或 `versionName` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 启用状态 | `enabled`:仅已发布且启用;`disabled`:仅已发布且停用 |\n\n> **已移除**:筛选项「适用品牌/适用车型」、主表列「备注」、「设为标准模板」能力。\n\n---" - }, - { - "type": "markdown", - "id": "ct-doc-prd-list", + "id": "h2-site-doc-prd-list", "title": "列表页 PRD", - "markdown": "# 合同模板管理 · 列表页 PRD\n\n## 7. 列表页需求\n\n### 7.1 页面结构\n\n筛选区 + 工具栏 + 嵌套表格。**列表页不展示章节 Tab。**\n\n### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 可搜索下拉 | 选项来自现有模板名称;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认\n\n### 7.3 工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 新增 | 进入新增页,默认章节为「合同主体」,模式为 create |\n\n### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同名称 | `fileName` | 超长省略,仅展示文件名 |\n| 版本号 | `versionNo` | 草稿显示「—」 |\n| 状态 | `status` | 已发布 / 草稿 Tag |\n| 启用状态 | `enabled` | 已发布:启用/停用;草稿:「—」 |\n| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 启用 / 停用 | 已发布 | 停用需确认;启用直接生效 |\n| 版本日志 | 已发布 | 查看历史快照 |\n| 编辑 | 草稿,或已发布且停用 | 启用中按钮禁用 |\n| 删除 | 草稿,或已发布且停用 | 启用中按钮禁用;删除前确认 |\n\n### 7.6 子表(展开行)\n\n主表每行可展开,展示该 Word 文档下 **9 个章节** 的摘要(原生 HTML 表,非嵌套 Ant Design Table)。\n\n| 列名 | 说明 |\n|---|---|\n| 章节 | 章节名称 |\n| 适用车型 | 附件章节展示绑定列表,格式「品牌 · 型号」;多个用「;」连接;未绑定显示「全部车型」;合同主体显示「—」 |\n| 风控条款数 | 该章节 HTML 内风控红线标记数量 |\n| 条件条款数 | 按各章节 HTML 统计条件条款标记数量 |\n| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |\n\n---" + "markdown": "# 加氢站管理 · 站点信息 — 列表页 PRD\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 加氢站管理 → 站点信息 |\n| 所属系统 | ONE-OS Web 端 |\n| 目标用户 | 加氢站运营、财务结算、平台管理员 |\n| 核心任务 | 查询站点台账、维护主数据、查看运营指标、发起对账与充值 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 设计规范 | 与租赁合同管理一致的 vm 布局(筛选 / KPI / 表格 / 分页) |\n\n---\n\n## 1. 页面结构\n\n用户进入「加氢站管理 → 站点信息」后,列表页自上而下为:\n\n1. **筛选区** — 名称、签约、地区、营业状态\n2. **KPI 分类** — 全部 / 签约·普通 / 高频 / 低频 / 无加氢\n3. **工具栏** — 全站余额提醒、批量导入、发起充值单、新建站点\n4. **站点列表** — 主数据 + 运营指标 + 操作列\n5. **分页栏** — 每页 5/10/20/50 条\n\n---\n\n## 2. 筛选区\n\n与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 |\n| 是否签约 | 下拉 | 签约站点 / 普通站点 |\n| 地区(省-市) | 级联 | 省、市两级,可只选省 |\n| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 |\n\n**交互**:\n\n- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页\n- **重置**:清空筛选条件并刷新列表\n\n---\n\n## 3. KPI 统计卡片\n\n与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部加氢站 | 列表全部站点 |\n| 签约站点 | 已签约,点击快捷筛选 |\n| 普通站点 | 未签约,点击快捷筛选 |\n| 高频加氢站 | 该站加氢次数 ≥ 3 |\n| 低频加氢站 | 有加氢但次数 < 3 |\n| 无加氢站点 | 加氢次数 = 0 |\n\n加氢次数来自「车辆氢费明细」共享 Store,按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。\n\n---\n\n## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 全站余额提醒设置 | 配置全局预付余额预警阈值 |\n| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 |\n| 发起充值单 | 为站点发起预付充值流程(原型演示) |\n| 新建站点 | 进入同页内嵌新建视图 |\n\n---\n\n## 5. 列表字段说明\n\n| 字段 | 说明 |\n|---|---|\n| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |\n| 合作时间 | 未签约可空 |\n| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |\n| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 |\n| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |\n| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |\n| 联系方式 | 联系人 + 电话 |\n| 操作 | 查看 + 更多菜单 |\n\n**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。\n\n**分页**:默认 10 条,可选 5/10/20/50。\n\n---\n\n## 6. 数据来源\n\n- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store\n- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed\n- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细\n\n---\n\n## 7. 双入口说明\n\n| 入口 | 路径 |\n|---|---|\n| 独立原型(本页) | OneOS → 加氢站管理 → 站点信息 |\n| 合包 | ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录 |\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-list.md" }, { "type": "markdown", - "id": "ct-doc-prd-editor", - "title": "编辑页 PRD", - "markdown": "# 合同模板管理 · 编辑/新增页 PRD\n\n## 8. 编辑 / 新增页需求\n\n### 8.1 页面结构\n\n顶栏 → 章节 Tab → 条件说明 / 车型绑定(按章节显隐)→ 左右分栏工作区。\n\n### 8.2 顶栏\n\n| 按钮 | 行为 |\n|---|---|\n| ← 返回列表 | 回到列表页,不自动保存 |\n| 保存 | 见 §6.2 |\n| 保存并发布 | 见 §6.2 |\n\n### 8.3 章节 Tab\n\n- 共 9 个章节;**前 4 个直显**,其余收入「更多」下拉。\n- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。\n- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**。\n- **其他 Tab**:仅展示该章节切片内容。\n\n章节顺序:\n\n1. 合同主体 \n2. 租赁订单 \n3. 车辆产生的费用 \n4. 安全责任告知书 \n5. 重型普通货运车辆租赁动态监管责任告知与确认书 \n6. 安全责任承诺书 \n7. 车辆使用规范和驾驶员要求 \n8. 授权委托书 \n9. 安全责任协议 \n\n### 8.4 条件条款说明(合同主体及租赁订单~安全责任协议)\n\n展示说明卡片,文案要点:\n\n> 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。适用于合同主体及全部附件章节。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。\n\n### 8.5 适用车型绑定(仅附件章节 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 品牌下拉 | 来自车型主数据 |\n| 型号下拉 | 随品牌联动 |\n| 添加车型 | 追加到当前章节绑定列表 |\n| 已绑定 Tag | 可删除单项 |\n| 空状态文案 | 「当前未绑定车型,表示全部车型适用」 |\n\n**规则**:\n\n- 绑定保存在 `sectionVehicleBindings[当前章节 key]`。\n- 切换章节 Tab 时加载对应章节的绑定到编辑表单。\n- 保存时将各章节绑定一并写入文档记录。\n\n### 8.6 编辑区(左侧)\n\n#### 8.6.1 面板头操作\n\n| 按钮 | 说明 |\n|---|---|\n| { } 模板占位符 | 弹层展示变量分组,点击插入光标处 |\n| 导入 Word 模板 | 接受 `.doc` / `.docx`;导入后按 §9 拆分章节 |\n\n#### 8.6.2 编辑能力\n\n- **Word 版式保真**:A4 分页编辑,保留 Word 导出样式类名。\n- **富文本工具栏**(节选):\n - 字号、加粗、对齐等基础格式\n - **锁定**:选中内容设为锁定区\n - **解锁**:解除锁定\n - **风控**:标记风控红线条款\n - **条件**:在各章节选中段落绑定车型条件\n\n#### 8.6.3 条件条款交互\n\n**适用范围**:合同主体、租赁订单、车辆产生的费用、安全责任告知书、重型普通货运车辆租赁动态监管责任告知与确认书、安全责任承诺书、车辆使用规范和驾驶员要求、授权委托书、安全责任协议。\n\n1. 用户选中段落或文字;\n2. 点击工具栏「条件」;\n3. 为选中内容绑定一个或多个「品牌 · 型号」;\n4. 生成 `.ct-vehicle-clause` 包裹结构,并写入 `data-vehicle-bindings`;\n5. 创建合同时:订单车辆 **任意一辆** 命中绑定车型 → 段落纳入正文;否则隐藏。\n\n### 8.7 实时预览(右侧)\n\n#### 8.7.1 预览模式\n\n| 模式 | 说明 |\n|---|---|\n| 连续预览 | 多页连续滚动 |\n| 单页预览 | 按 A4 单页翻页 |\n\n#### 8.7.2 预览顶栏统计\n\n展示当前章节:\n\n`锁定 N 处 · 风控红线 M 处 · 条件条款 K 处`\n\n当前章节支持条件且已选模拟车型时追加:`(预览命中 X)`\n\n#### 8.7.3 模拟车型\n\n- 在合同主体及全部附件章节可用;多选下拉,选项来自车型主数据(品牌 · 型号)。\n- 未选车型时:**隐藏**所有条件条款段落(与创建合同时无车辆命中行为一致)。\n- 选中后按命中规则过滤并展开条件条款正文。\n\n#### 8.7.4 占位符预览\n\n预览时替换部分占位符为样例值,如:\n\n- `{{customerName}}` → `【承租方公司名称】`\n- `{{contractCode}}` → `20260001`\n- 甲方相关变量按预览所选出租方公司填充\n\n### 8.8 空状态\n\n编辑区无内容时,预览区展示空状态引导;新增模式下突出「导入 Word 模板」按钮。\n\n---" + "id": "h2-site-doc-prd-statement", + "title": "对账与结算 PRD", + "markdown": "# 加氢站管理 · 站点信息 — 对账与结算 PRD\n\n> 本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。\n\n---\n\n## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细** 中同时满足以下条件的记录:\n\n1. 对账状态 = **已对账**(台账已完成「完成对账」)\n2. **尚未**被本站点历史对账单结算过(未绑定 `statementRecordId`)\n3. 加氢时间落在用户选择的 **[账单开始日期, 账单结束日期]** 内\n4. 加氢站名称与当前操作站点一致\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n---\n\n## 2. 两阶段交互\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的已对账记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n\n---\n\n## 3. 提交后的系统行为\n\n提交成功后需**原子性**完成:\n\n1. 写入对账历史(供「查看对账记录」查询)\n2. 扣减站点预付余额\n3. 标记台账记录已结算(绑定 `statementRecordId`)\n4. **回写车辆氢费明细**:\n\n| 车辆氢费明细字段 | 回写值 |\n|---|---|\n| 对账日期 | 操作完成时间 |\n| 收票日期 | 操作完成日期 |\n| 加氢站付款状态 | 已付款 |\n\n原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge` 模拟跨页回写。\n\n---\n\n## 4. 查看对账记录\n\n入口:列表操作列 → 更多 → **查看对账记录**\n\n历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。\n\n查看明细:账单区间、统计三卡、收票信息、发票附件、成本明细表。\n\n---\n\n## 5. 校验与异常\n\n| 场景 | 处理 |\n|---|---|\n| 日期未填 / 开始晚于结束 | 阻止生成 |\n| 区间内无已对账未结算记录 | 阻止生成,提示调整日期 |\n| 收票日期/金额/附件未填 | 阻止提交 |\n| 收票金额 ≤ 0 | 阻止提交 |\n| 预付余额结算后为负 | 允许提交,标「已欠费」 |\n\n---\n\n## 6. 研发要点\n\n1. **对账单幂等**:同一笔已对账记录只能进入一次成功提交的对账单\n2. **上次结束时间**:取该站点已成功提交对账单的最大 `endDate`\n3. **余额计算**:`balanceAfter = prepaidBalance - sum(costTotal)`\n4. **跨模块同步**:生产对接统一台账;原型使用 Bridge Store\n5. **明细字段范围**:对账弹窗不展示客户侧加氢单价/总价\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md" }, { "type": "markdown", - "id": "ct-doc-prd-acceptance", + "id": "h2-site-doc-scope", + "title": "范围与决策", + "markdown": "# 站点信息独立原型 — 范围与决策\n\n## 背景\n\n「站点信息」从 ONE-OS Web 合包 `oneos-web-h2-station` 拆出,作为 OneOS → 加氢站管理下的独立 Axhub 入口,便于与正式菜单路径对齐。\n\n## 范围\n\n| 包含 | 不包含 |\n|------|--------|\n| 站点列表、KPI、新建/编辑/查看/删除 | 加氢订单、加氢记录(仍在合包) |\n| 营业状态、价格配置、余额下钻 | 后端接口联调 |\n| 两阶段对账单、充值单、批量导入 | |\n\n## 数据来源\n\n- **氢费/加氢明细**:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」原型共享 Store。\n- 先打开「车辆氢费明细」时,台账全量行会同步至 Store;仅打开加氢站模块时使用 canonical seed(10 条演示数据)。\n- 对账单提交 patch 经 `window.H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细。\n\n## KPI 口径(已确认)\n\n全部 / 签约·普通 / **高频(≥3 次)** / **低频(1–2 次)** / **无加氢(0 次)**,与《站点管理使用说明书》及页面内嵌 PRD 一致。\n\n## 角色演示\n\n页面右上角提供「平台管理员 / 加氢站账号」切换,影响编辑页系统账号字段可编辑性。\n\n## 双入口说明\n\n- **本原型**:OneOS → 加氢站管理 → 站点信息\n- **合包**:ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements.md" + }, + { + "type": "markdown", + "id": "h2-site-doc-acceptance", "title": "验收标准", - "markdown": "## 16. 验收标准\n\n### 16.1 列表页\n\n- [ ] 筛选项包含:合同名称(可搜索下拉)、状态、创建人、启用状态\n- [ ] 主表含启用状态列;操作含启用/停用、版本日志、编辑、删除\n- [ ] 启用中模板编辑/删除按钮禁用;停用需二次确认\n- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表\n- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态\n\n### 16.2 编辑页\n\n- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多\n- [ ] 导入 Word 后 9 章节正确拆分\n- [ ] 附件章节可绑定车型;各章节均可设条件条款\n- [ ] 预览支持连续/单页切换;各章节可模拟车型验证条件条款\n- [ ] 保存草稿无版本号;发布生成版本号\n- [ ] 编辑已发布(停用后)并发布时版本号递增,旧版写入版本日志\n- [ ] 编辑已发布(停用后)点「保存」不改变发布态与版本号\n- [ ] 有未保存修改时返回列表需确认\n\n### 16.3 跨模块与数据一致性\n\n- [ ] 模板管理停用后,租赁合同创建页不可再选用该模板\n- [ ] 列表子表「风控条款数」与编辑区统计一致\n- [ ] 列表子表「条件条款数」与各章节实际标记数一致\n\n### 16.4 标注与文档(原型)\n\n- [ ] 原型已接入 `@axhub/annotation`,关键区域有 `data-annotation-id`\n- [ ] 目录路由可切换列表页 / 新增页\n- [ ] `PRD.md` 与 `annotation-source.json` 与当前实现一致\n\n---" + "markdown": "## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅已对账未结算记录可进入对账单\n- [ ] 两阶段交互与必填校验完整\n- [ ] 提交后回写车辆氢费明细可跨页验收\n\n### 8.3 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---" } ] }, { "type": "folder", - "id": "ct-doc-list", - "title": "列表页模块", + "id": "h2-site-doc-page", + "title": "页面标注", "defaultExpanded": true, "children": [ - { - "type": "route", - "id": "ct-route-list", - "title": "打开列表页", - "route": "contract-template:list", - "payload": { - "pageId": "list" - } - }, { "type": "markdown", - "id": "ct-doc-node-filter", + "id": "h2-site-doc-node-h2-site-filter", "title": "筛选条件", - "markdown": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 可搜索下拉 | 选项来自现有模板名称;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认" + "markdown": "## 2. 筛选区\n\n与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 |\n| 是否签约 | 下拉 | 签约站点 / 普通站点 |\n| 地区(省-市) | 级联 | 省、市两级,可只选省 |\n| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 |\n\n**交互**:\n\n- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页\n- **重置**:清空筛选条件并刷新列表\n\n---" }, { "type": "markdown", - "id": "ct-doc-node-table", - "title": "嵌套列表", - "markdown": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同名称 | `fileName` | 超长省略,仅展示文件名 |\n| 版本号 | `versionNo` | 草稿显示「—」 |\n| 状态 | `status` | 已发布 / 草稿 Tag |\n| 启用状态 | `enabled` | 已发布:启用/停用;草稿:「—」 |\n| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n---\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 启用 / 停用 | 已发布 | 停用需确认;启用直接生效 |\n| 版本日志 | 已发布 | 查看历史快照 |\n| 编辑 | 草稿,或已发布且停用 | 启用中按钮禁用 |\n| 删除 | 草稿,或已发布且停用 | 启用中按钮禁用;删除前确认 |\n\n---\n\n### 7.6 子表(展开行)\n\n主表每行可展开,展示该 Word 文档下 **9 个章节** 的摘要(原生 HTML 表,非嵌套 Ant Design Table)。\n\n| 列名 | 说明 |\n|---|---|\n| 章节 | 章节名称 |\n| 适用车型 | 附件章节展示绑定列表,格式「品牌 · 型号」;多个用「;」连接;未绑定显示「全部车型」;合同主体显示「—」 |\n| 风控条款数 | 该章节 HTML 内风控红线标记数量 |\n| 条件条款数 | 按各章节 HTML 统计条件条款标记数量 |\n| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |\n\n---" + "id": "h2-site-doc-node-h2-site-kpi", + "title": "KPI 快捷筛选", + "markdown": "## 3. KPI 统计卡片\n\n与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部加氢站 | 列表全部站点 |\n| 签约站点 | 已签约,点击快捷筛选 |\n| 普通站点 | 未签约,点击快捷筛选 |\n| 高频加氢站 | 该站加氢次数 ≥ 3 |\n| 低频加氢站 | 有加氢但次数 < 3 |\n| 无加氢站点 | 加氢次数 = 0 |\n\n加氢次数来自「车辆氢费明细」共享 Store,按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。\n\n---" + }, + { + "type": "markdown", + "id": "h2-site-doc-node-h2-site-statement", + "title": "工具栏与对账", + "markdown": "## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 全站余额提醒设置 | 配置全局预付余额预警阈值 |\n| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 |\n| 发起充值单 | 为站点发起预付充值流程(原型演示) |\n| 新建站点 | 进入同页内嵌新建视图 |\n\n---\n\n---\n\n## 6. 数据来源\n\n- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store\n- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed\n- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细\n\n---" + }, + { + "type": "markdown", + "id": "h2-site-doc-node-h2-site-table", + "title": "站点列表", + "markdown": "## 5. 列表字段说明\n\n| 字段 | 说明 |\n|---|---|\n| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |\n| 合作时间 | 未签约可空 |\n| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |\n| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 |\n| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |\n| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |\n| 联系方式 | 联系人 + 电话 |\n| 操作 | 查看 + 更多菜单 |\n\n**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。\n\n**分页**:默认 10 条,可选 5/10/20/50。\n\n---" } ] - }, - { - "type": "folder", - "id": "ct-doc-editor", - "title": "编辑页模块", - "defaultExpanded": false, - "children": [ - { - "type": "route", - "id": "ct-route-editor", - "title": "打开新增页", - "route": "contract-template:editor", - "payload": { - "pageId": "editor" - } - }, - { - "type": "markdown", - "id": "ct-doc-node-tabs", - "title": "章节 Tab", - "markdown": "### 8.3 章节 Tab\n\n- 共 9 个章节;**前 4 个直显**,其余收入「更多」下拉。\n- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。\n- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**。\n- **其他 Tab**:仅展示该章节切片内容。\n\n章节顺序:\n\n1. 合同主体 \n2. 租赁订单 \n3. 车辆产生的费用 \n4. 安全责任告知书 \n5. 重型普通货运车辆租赁动态监管责任告知与确认书 \n6. 安全责任承诺书 \n7. 车辆使用规范和驾驶员要求 \n8. 授权委托书 \n9. 安全责任协议\n\n---\n\n### 8.4 条件条款说明(合同主体及租赁订单~安全责任协议)\n\n展示说明卡片,文案要点:\n\n> 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。适用于合同主体及全部附件章节。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。\n\n---\n\n### 8.5 适用车型绑定(仅附件章节 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 品牌下拉 | 来自车型主数据 |\n| 型号下拉 | 随品牌联动 |\n| 添加车型 | 追加到当前章节绑定列表 |\n| 已绑定 Tag | 可删除单项 |\n| 空状态文案 | 「当前未绑定车型,表示全部车型适用」 |\n\n**规则**:\n\n- 绑定保存在 `sectionVehicleBindings[当前章节 key]`。\n- 切换章节 Tab 时加载对应章节的绑定到编辑表单。\n- 保存时将各章节绑定一并写入文档记录。" - }, - { - "type": "markdown", - "id": "ct-doc-node-workspace", - "title": "编辑与预览", - "markdown": "### 8.6 编辑区(左侧)\n\n#### 8.6.1 面板头操作\n\n| 按钮 | 说明 |\n|---|---|\n| { } 模板占位符 | 弹层展示变量分组,点击插入光标处 |\n| 导入 Word 模板 | 接受 `.doc` / `.docx`;导入后按 §9 拆分章节 |\n\n#### 8.6.2 编辑能力\n\n- **Word 版式保真**:A4 分页编辑,保留 Word 导出样式类名。\n- **富文本工具栏**(节选):\n - 字号、加粗、对齐等基础格式\n - **锁定**:选中内容设为锁定区\n - **解锁**:解除锁定\n - **风控**:标记风控红线条款\n - **条件**:在各章节选中段落绑定车型条件\n\n#### 8.6.3 条件条款交互\n\n**适用范围**:合同主体、租赁订单、车辆产生的费用、安全责任告知书、重型普通货运车辆租赁动态监管责任告知与确认书、安全责任承诺书、车辆使用规范和驾驶员要求、授权委托书、安全责任协议。\n\n1. 用户选中段落或文字;\n2. 点击工具栏「条件」;\n3. 为选中内容绑定一个或多个「品牌 · 型号」;\n4. 生成 `.ct-vehicle-clause` 包裹结构,并写入 `data-vehicle-bindings`;\n5. 创建合同时:订单车辆 **任意一辆** 命中绑定车型 → 段落纳入正文;否则隐藏。\n\n---\n\n### 8.7 实时预览(右侧)\n\n#### 8.7.1 预览模式\n\n| 模式 | 说明 |\n|---|---|\n| 连续预览 | 多页连续滚动 |\n| 单页预览 | 按 A4 单页翻页 |\n\n#### 8.7.2 预览顶栏统计\n\n展示当前章节:\n\n`锁定 N 处 · 风控红线 M 处 · 条件条款 K 处`\n\n当前章节支持条件且已选模拟车型时追加:`(预览命中 X)`\n\n#### 8.7.3 模拟车型\n\n- 在合同主体及全部附件章节可用;多选下拉,选项来自车型主数据(品牌 · 型号)。\n- 未选车型时:**隐藏**所有条件条款段落(与创建合同时无车辆命中行为一致)。\n- 选中后按命中规则过滤并展开条件条款正文。\n\n#### 8.7.4 占位符预览\n\n预览时替换部分占位符为样例值,如:\n\n- `{{customerName}}` → `【承租方公司名称】`\n- `{{contractCode}}` → `20260001`\n- 甲方相关变量按预览所选出租方公司填充" - } - ] - }, - { - "type": "markdown", - "id": "ct-doc-annotation-hint", - "title": "标注查看提示", - "markdown": "## 标注查看提示\n\n右侧标注工具栏可浏览目录、切换主题与颜色筛选。列表页与编辑页标注按当前页面自动显隐。\n\n**PRD 同步**:目录「PRD 全文」已与 `src/resources/contract-template-management/PRD.md` 对齐;修改 PRD 后运行 `node src/prototypes/contract-template-management/scripts/build-annotation-source.mjs` 重新生成 `annotation-source.json`。" } ] } @@ -317,318 +248,91 @@ }, "nodes": [ { - "id": "ct-list-filter", + "id": "h2-site-filter", "index": 1, "title": "筛选条件", - "pageId": "list", + "pageId": "site-info", "locator": { "selectors": [ - "[data-annotation-id=\"ct-list-filter\"]" + ".h2-station-page .vm-filter-card", + "[data-annotation-id=\"h2-site-filter\"]" ], - "fingerprint": "ct-list-filter", + "fingerprint": "filter|site-info", "path": [] }, - "aiPrompt": "列表页筛选区字段与查询逻辑。", - "annotationText": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 可搜索下拉 | 选项来自现有模板名称;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认", + "aiPrompt": "筛选字段与查询/重置行为。", + "annotationText": "## 2. 筛选区\n\n与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 |\n| 是否签约 | 下拉 | 签约站点 / 普通站点 |\n| 地区(省-市) | 级联 | 省、市两级,可只选省 |\n| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 |\n\n**交互**:\n\n- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页\n- **重置**:清空筛选条件并刷新列表\n\n---", "hasMarkdown": true, "color": "#2563eb", "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, + "createdAt": 1782912426507, + "updatedAt": 1782912426507, "controls": [] }, { - "id": "ct-list-toolbar", + "id": "h2-site-kpi", "index": 2, - "title": "新增模板", - "pageId": "list", + "title": "KPI 快捷筛选", + "pageId": "site-info", "locator": { "selectors": [ - "[data-annotation-id=\"ct-list-toolbar\"]" + ".h2-station-page .vm-kpi-row", + "[data-annotation-id=\"h2-site-kpi\"]" ], - "fingerprint": "ct-list-toolbar", + "fingerprint": "kpi|site-info", "path": [] }, - "aiPrompt": "列表工具栏新增入口。", - "annotationText": "进入新增页,默认从「合同主体」章节开始编辑。", - "hasMarkdown": false, + "aiPrompt": "六张 KPI 卡片口径与点击筛选。", + "annotationText": "## 3. KPI 统计卡片\n\n与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部加氢站 | 列表全部站点 |\n| 签约站点 | 已签约,点击快捷筛选 |\n| 普通站点 | 未签约,点击快捷筛选 |\n| 高频加氢站 | 该站加氢次数 ≥ 3 |\n| 低频加氢站 | 有加氢但次数 < 3 |\n| 无加氢站点 | 加氢次数 = 0 |\n\n加氢次数来自「车辆氢费明细」共享 Store,按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。\n\n---", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1782912426507, + "updatedAt": 1782912426507, + "controls": [] + }, + { + "id": "h2-site-statement", + "index": 3, + "title": "工具栏与对账", + "pageId": "site-info", + "locator": { + "selectors": [ + ".h2-station-page .vm-table-toolbar", + "[data-annotation-id=\"h2-site-toolbar\"]" + ], + "fingerprint": "toolbar|statement", + "path": [] + }, + "aiPrompt": "工具栏操作与对账单氢费联动。", + "annotationText": "## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 全站余额提醒设置 | 配置全局预付余额预警阈值 |\n| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 |\n| 发起充值单 | 为站点发起预付充值流程(原型演示) |\n| 新建站点 | 进入同页内嵌新建视图 |\n\n---\n\n---\n\n## 6. 数据来源\n\n- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store\n- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed\n- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细\n\n---", + "hasMarkdown": true, + "color": "#d97706", + "images": [], + "createdAt": 1782912426507, + "updatedAt": 1782912426507, + "controls": [] + }, + { + "id": "h2-site-table", + "index": 4, + "title": "站点列表", + "pageId": "site-info", + "locator": { + "selectors": [ + ".h2-station-page .vm-table-card", + "[data-annotation-id=\"h2-site-table\"]" + ], + "fingerprint": "table|site-info", + "path": [] + }, + "aiPrompt": "台账表格列、下钻与行内操作。", + "annotationText": "## 5. 列表字段说明\n\n| 字段 | 说明 |\n|---|---|\n| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |\n| 合作时间 | 未签约可空 |\n| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |\n| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 |\n| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |\n| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |\n| 联系方式 | 联系人 + 电话 |\n| 操作 | 查看 + 更多菜单 |\n\n**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。\n\n**分页**:默认 10 条,可选 5/10/20/50。\n\n---", + "hasMarkdown": true, "color": "#32a06e", "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-list-table", - "index": 3, - "title": "Word 模板列表", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-list-table\"]" - ], - "fingerprint": "ct-list-table", - "path": [] - }, - "aiPrompt": "主表与子表嵌套列表结构。", - "annotationText": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同名称 | `fileName` | 超长省略,仅展示文件名 |\n| 版本号 | `versionNo` | 草稿显示「—」 |\n| 状态 | `status` | 已发布 / 草稿 Tag |\n| 启用状态 | `enabled` | 已发布:启用/停用;草稿:「—」 |\n| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n---\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 启用 / 停用 | 已发布 | 停用需确认;启用直接生效 |\n| 版本日志 | 已发布 | 查看历史快照 |\n| 编辑 | 草稿,或已发布且停用 | 启用中按钮禁用 |\n| 删除 | 草稿,或已发布且停用 | 启用中按钮禁用;删除前确认 |\n\n---\n\n### 7.6 子表(展开行)\n\n主表每行可展开,展示该 Word 文档下 **9 个章节** 的摘要(原生 HTML 表,非嵌套 Ant Design Table)。\n\n| 列名 | 说明 |\n|---|---|\n| 章节 | 章节名称 |\n| 适用车型 | 附件章节展示绑定列表,格式「品牌 · 型号」;多个用「;」连接;未绑定显示「全部车型」;合同主体显示「—」 |\n| 风控条款数 | 该章节 HTML 内风控红线标记数量 |\n| 条件条款数 | 按各章节 HTML 统计条件条款标记数量 |\n| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |\n\n---", - "hasMarkdown": true, - "color": "#0f766e", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-editor-topbar", - "index": 4, - "title": "编辑页顶栏", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-editor-topbar\"]" - ], - "fingerprint": "ct-editor-topbar", - "path": [] - }, - "aiPrompt": "返回、保存草稿、保存并发布。", - "annotationText": "### 8.2 顶栏\n\n| 按钮 | 行为 |\n|---|---|\n| ← 返回列表 | 回到列表页,不自动保存 |\n| 保存 | 见 §6.2 |\n| 保存并发布 | 见 §6.2 |\n\n---\n\n### 6.2 保存与发布\n\n| 操作 | 新增模式 | 编辑草稿 | 编辑已发布(须先停用) |\n|---|---|---|---|\n| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**保持已发布与版本号**,仍为停用态 |\n| **保存并发布** | 新建已发布记录 + 版本号,默认启用 | 原记录变已发布 + 版本号,默认启用 | **同记录升级版本号**;旧内容写入 `versionLogs` 快照;发布后默认停用,需手动启用 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n**离开编辑**:有未保存修改时返回列表需二次确认。", - "hasMarkdown": true, - "color": "#64748b", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-section-tabs", - "index": 5, - "title": "章节 Tab", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-section-tabs\"]" - ], - "fingerprint": "ct-section-tabs", - "path": [] - }, - "aiPrompt": "9 个合同章节切换。", - "annotationText": "### 8.3 章节 Tab\n\n- 共 9 个章节;**前 4 个直显**,其余收入「更多」下拉。\n- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。\n- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**。\n- **其他 Tab**:仅展示该章节切片内容。\n\n章节顺序:\n\n1. 合同主体 \n2. 租赁订单 \n3. 车辆产生的费用 \n4. 安全责任告知书 \n5. 重型普通货运车辆租赁动态监管责任告知与确认书 \n6. 安全责任承诺书 \n7. 车辆使用规范和驾驶员要求 \n8. 授权委托书 \n9. 安全责任协议", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1782820000000, - "controls": [] - }, - { - "id": "ct-contract-type", - "index": 6, - "title": "合同类型", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-contract-type\"]" - ], - "fingerprint": "ct-contract-type", - "path": [] - }, - "aiPrompt": "合同主体章节下的合同类型配置。", - "annotationText": "### 8.4 合同类型(仅合同主体 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 合同类型输入框 | **必填**;用户自行填写,如「正式合同」「现代18吨试用合同」 |\n| 展示位置 | 章节 Tab 下方配置卡片,与附件章节的「适用车型」卡片交互一致 |\n| 用途 | 同步至列表页「合同类型」列;租赁合同创建时按类型匹配标准模板 |\n\n**规则**:\n\n- 仅当选中 **合同主体** 章节 Tab 时显示本卡片。\n- 未填写时无法保存或发布,并提示「请输入合同类型」。\n- 切换至租赁订单等附件章节时,显示「适用车型」卡片,本卡片隐藏。\n- 保存/发布时写入模板 `contractType` 字段。", - "hasMarkdown": true, - "color": "#7c3aed", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1782820000000, - "controls": [] - }, - { - "id": "ct-vehicle-binding", - "index": 7, - "title": "适用车型绑定", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-vehicle-binding\"]" - ], - "fingerprint": "ct-vehicle-binding", - "path": [] - }, - "aiPrompt": "附件章节适用车型配置。", - "annotationText": "### 8.5 适用车型绑定(仅附件章节 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 品牌下拉 | 来自车型主数据 |\n| 型号下拉 | 随品牌联动 |\n| 添加车型 | 追加到当前章节绑定列表 |\n| 已绑定 Tag | 可删除单项 |\n| 空状态文案 | 「当前未绑定车型,表示全部车型适用」 |\n\n**规则**:\n\n- 绑定保存在 `sectionVehicleBindings[当前章节 key]`。\n- 切换章节 Tab 时加载对应章节的绑定到编辑表单。\n- 保存时将各章节绑定一并写入文档记录。", - "hasMarkdown": true, - "color": "#059669", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-editor-panel", - "index": 8, - "title": "编辑区", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-editor-panel\"]" - ], - "fingerprint": "ct-editor-panel", - "path": [] - }, - "aiPrompt": "Word 版式分页编辑区。", - "annotationText": "### 8.6 编辑区(左侧)\n\n#### 8.6.1 面板头操作\n\n| 按钮 | 说明 |\n|---|---|\n| { } 模板占位符 | 弹层展示变量分组,点击插入光标处 |\n| 导入 Word 模板 | 接受 `.doc` / `.docx`;导入后按 §9 拆分章节 |\n\n#### 8.6.2 编辑能力\n\n- **Word 版式保真**:A4 分页编辑,保留 Word 导出样式类名。\n- **富文本工具栏**(节选):\n - 字号、加粗、对齐等基础格式\n - **锁定**:选中内容设为锁定区\n - **解锁**:解除锁定\n - **风控**:标记风控红线条款\n - **条件**:在各章节选中段落绑定车型条件\n\n#### 8.6.3 条件条款交互\n\n**适用范围**:合同主体、租赁订单、车辆产生的费用、安全责任告知书、重型普通货运车辆租赁动态监管责任告知与确认书、安全责任承诺书、车辆使用规范和驾驶员要求、授权委托书、安全责任协议。\n\n1. 用户选中段落或文字;\n2. 点击工具栏「条件」;\n3. 为选中内容绑定一个或多个「品牌 · 型号」;\n4. 生成 `.ct-vehicle-clause` 包裹结构,并写入 `data-vehicle-bindings`;\n5. 创建合同时:订单车辆 **任意一辆** 命中绑定车型 → 段落纳入正文;否则隐藏。", - "hasMarkdown": true, - "color": "#7c3aed", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-rte-risk-redline", - "index": 9, - "title": "红线", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-rte-risk-redline\"]" - ], - "fingerprint": "ct-rte-risk-redline", - "path": [] - }, - "aiPrompt": "风控红线条款标记。", - "annotationText": "红线功能主要用于在章节中选择特定内容,点击红线将其设置为篡改后自动变成非标合同。不修改红线内内容不会导致流程变为非标。", - "hasMarkdown": false, - "color": "#dc2626", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-rte-unrisk-redline", - "index": 10, - "title": "取消红线", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-rte-unrisk-redline\"]" - ], - "fingerprint": "ct-rte-unrisk-redline", - "path": [] - }, - "aiPrompt": "取消风控红线标记。", - "annotationText": "取消功能主要用于取消被选中的部分的红线内容。取消后,在新增租赁合同时修改该部分内容,不会导致流程变为非标准流程。", - "hasMarkdown": false, - "color": "#94a3b8", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-rte-vehicle-clause", - "index": 11, - "title": "条件", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-rte-vehicle-clause\"]" - ], - "fingerprint": "ct-rte-vehicle-clause", - "path": [] - }, - "aiPrompt": "条件条款(段落级车型显隐)。", - "annotationText": "条件可在合同主体及租赁订单~安全责任协议全部章节使用:圈定章节内具体内容,使部分条款仅对指定车型生效。与各章节「适用车型」(控制整章是否可见)不同,条件只作用于章节内部分段落。", - "hasMarkdown": false, - "color": "#2563eb", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-rte-unvehicle-clause", - "index": 12, - "title": "取消条件", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-rte-unvehicle-clause\"]" - ], - "fingerprint": "ct-rte-unvehicle-clause", - "path": [] - }, - "aiPrompt": "取消条件条款标记。", - "annotationText": "当条款被设置条件后,可以通过选中特定条款内容,点击取消来取消该部分内容的车型限定。", - "hasMarkdown": false, - "color": "#64748b", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-rte-inline-lock", - "index": 13, - "title": "锁定", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-rte-inline-lock\"]" - ], - "fingerprint": "ct-rte-inline-lock", - "path": [] - }, - "aiPrompt": "内容锁定区标记。", - "annotationText": "锁定主要用于选中章节内的内容;锁定之后该部分内容无法在新增租赁合同时被篡改。红线、条件以及锁定可以在同一段内容同时生效。", - "hasMarkdown": false, - "color": "#b45309", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-rte-uninline-lock", - "index": 14, - "title": "解除锁定", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-rte-uninline-lock\"]" - ], - "fingerprint": "ct-rte-uninline-lock", - "path": [] - }, - "aiPrompt": "解除内容锁定标记。", - "annotationText": "选中已锁定内容后点击解除;解除后新增租赁合同时可再次编辑该部分。", - "hasMarkdown": false, - "color": "#94a3b8", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, - "controls": [] - }, - { - "id": "ct-preview-panel", - "index": 15, - "title": "实时预览", - "pageId": "editor", - "locator": { - "selectors": [ - "[data-annotation-id=\"ct-preview-panel\"]" - ], - "fingerprint": "ct-preview-panel", - "path": [] - }, - "aiPrompt": "A4 分页预览与模拟车型。", - "annotationText": "### 8.7 实时预览(右侧)\n\n#### 8.7.1 预览模式\n\n| 模式 | 说明 |\n|---|---|\n| 连续预览 | 多页连续滚动 |\n| 单页预览 | 按 A4 单页翻页 |\n\n#### 8.7.2 预览顶栏统计\n\n展示当前章节:\n\n`锁定 N 处 · 风控红线 M 处 · 条件条款 K 处`\n\n当前章节支持条件且已选模拟车型时追加:`(预览命中 X)`\n\n#### 8.7.3 模拟车型\n\n- 在合同主体及全部附件章节可用;多选下拉,选项来自车型主数据(品牌 · 型号)。\n- 未选车型时:**隐藏**所有条件条款段落(与创建合同时无车辆命中行为一致)。\n- 选中后按命中规则过滤并展开条件条款正文。\n\n#### 8.7.4 占位符预览\n\n预览时替换部分占位符为样例值,如:\n\n- `{{customerName}}` → `【承租方公司名称】`\n- `{{contractCode}}` → `20260001`\n- 甲方相关变量按预览所选出租方公司填充", - "hasMarkdown": true, - "color": "#0ea5e9", - "images": [], - "createdAt": 1740508800000, - "updatedAt": 1740508800000, + "createdAt": 1782912426507, + "updatedAt": 1782912426507, "controls": [] } ] diff --git a/src/common/h2VehicleLedgerBridge.js b/src/common/h2VehicleLedgerBridge.js new file mode 100644 index 0000000..06cb1ec --- /dev/null +++ b/src/common/h2VehicleLedgerBridge.js @@ -0,0 +1,285 @@ +/** + * 车辆氢费明细 ↔ 加氢站模块 共享数据桥 + * 加氢站侧氢费/对账/加氢记录均读取本 Store;车辆氢费明细页运行时同步全量台账行。 + */ + +var H2_RECONCILE_RECONCILED = 'reconciled'; +var H2_RECONCILE_PENDING = 'pending'; + +var H2_STATION_CODE_MAP = { + '嘉兴加氢站(一期)': 'JX-H2-001', + '杭州临平加氢站': 'HZ-H2-002', + '上海宝山加氢站': 'SH-H2-003', + '苏州工业园区备用站': 'SZ-H2-004' +}; + +/** 与车辆氢费明细、加氢站模块对齐的 canonical seed(10 条) */ +var H2_CANONICAL_LEDGER_SEED = [ + { key: 'rf-1', id: 'rf-1', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 12.5, costUnitPrice: 42.5, costTotal: 531.25, customerUnitPrice: 45, customerAmount: 562.5, settlementStatus: 'customer', mileageKm: 128560, creatorName: '张三', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-2', id: 'rf-2', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', customerName: '浙江绿运物流有限公司', hydrogenKg: 10.0, costUnitPrice: 42.5, costTotal: 425.0, customerUnitPrice: 45, customerAmount: 450.0, settlementStatus: 'internal', mileageKm: 95230, creatorName: '李四', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-3', id: 'rf-3', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 18.3, costUnitPrice: 42.5, costTotal: 777.75, customerUnitPrice: 45, customerAmount: 823.5, settlementStatus: 'customer_self', mileageKm: 201880, creatorName: '王静', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-4', id: 'rf-4', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', customerName: '嘉兴港务氢能运输队', hydrogenKg: 15.6, costUnitPrice: 42.5, costTotal: 663.0, customerUnitPrice: 45, customerAmount: 702.0, settlementStatus: 'customer', mileageKm: 167420, creatorName: '张三', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-5', id: 'rf-5', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', customerName: '杭州临平城配中心', hydrogenKg: 15.3, costUnitPrice: 43.0, costTotal: 657.9, customerUnitPrice: 46, customerAmount: 703.8, settlementStatus: 'internal', mileageKm: 143200, creatorName: '赵敏', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-6', id: 'rf-6', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', customerName: '浙江氢运科技', hydrogenKg: 18.2, costUnitPrice: 43.0, costTotal: 782.6, customerUnitPrice: 46, customerAmount: 837.2, settlementStatus: 'customer_self', mileageKm: 189650, creatorName: '陈浩', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-7', id: 'rf-7', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costTotal: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer', mileageKm: 110340, creatorName: '李四', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-8', id: 'rf-8', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costTotal: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal', mileageKm: 88420, creatorName: '王静', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-9', id: 'rf-9', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costTotal: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self', mileageKm: 76500, creatorName: '张三', reconcileStatus: H2_RECONCILE_RECONCILED }, + { key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', reconcileStatus: H2_RECONCILE_PENDING } +]; + +function h2BridgeCloneRows(rows) { + return (rows || []).map(function (r) { return Object.assign({}, r); }); +} + +function h2BridgeFormatDateTime(value) { + if (!value) return ''; + if (typeof value === 'string') return value; + if (value.format) return value.format('YYYY-MM-DD HH:mm:ss'); + return String(value); +} + +function h2BridgeMatchStatementPatch(row, patch) { + if (patch.ledgerId && row.ledgerSyncId && patch.ledgerId === row.ledgerSyncId) return true; + var rowTime = h2BridgeFormatDateTime(row.hydrogenTime); + var patchTime = String(patch.hydrogenTime || ''); + return (row.stationName || '') === (patch.stationName || '') + && (row.plateNo || '') === (patch.plateNo || '') + && (rowTime === patchTime || String(row.hydrogenTime || '') === patchTime); +} + +function h2BridgeApplyStatementPatches(rows, patches) { + if (!patches || !patches.length) return h2BridgeCloneRows(rows); + return (rows || []).map(function (r) { + var patch = null; + var i; + for (i = 0; i < patches.length; i++) { + if (h2BridgeMatchStatementPatch(r, patches[i])) { + patch = patches[i]; + break; + } + } + if (!patch) return r; + return Object.assign({}, r, { + statementPatched: true, + reconcileDate: patch.reconcileDate, + receiptDate: patch.receiptDate, + stationPaymentStatus: patch.paymentStatus || 'paid', + paymentStatus: patch.paymentStatus || 'paid', + reconciledAt: patch.reconcileDate + }); + }); +} + +function h2BridgeBuildInitialRows() { + var patches = (typeof window !== 'undefined' && window.H2_STATION_STATEMENT_LEDGER_UPDATES) || []; + return h2BridgeApplyStatementPatches(h2BridgeCloneRows(H2_CANONICAL_LEDGER_SEED), patches); +} + +function h2BridgeCreateStore() { + var listeners = []; + var state = { rows: h2BridgeBuildInitialRows() }; + + function notify() { + listeners.forEach(function (fn) { + try { fn(state.rows); } catch (_e) { /* noop */ } + }); + } + + return { + getRows: function () { return h2BridgeCloneRows(state.rows); }, + setRows: function (rows) { + state.rows = h2BridgeCloneRows(rows); + notify(); + }, + subscribe: function (fn) { + listeners.push(fn); + return function () { + listeners = listeners.filter(function (item) { return item !== fn; }); + }; + }, + applyPatches: function (patches) { + if (!patches || !patches.length) return; + state.rows = h2BridgeApplyStatementPatches(state.rows, patches); + notify(); + } + }; +} + +function h2BridgeMapToRefuelRecord(row) { + var costTotal = row.costTotal != null ? row.costTotal : row.costAmount; + return { + id: row.id || row.key, + stationName: row.stationName, + hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime), + plateNo: row.plateNo, + customerName: row.customerName, + hydrogenKg: row.hydrogenKg, + costUnitPrice: row.costUnitPrice, + costAmount: costTotal, + customerUnitPrice: row.customerUnitPrice, + customerAmount: row.customerAmount, + settlementStatus: row.settlementStatus, + orderNo: row.orderNo, + mileageKm: row.mileageKm + }; +} + +function h2BridgeBuildRefuelOrderNo(record, index) { + if (record && record.orderNo) return record.orderNo; + var idPart = String((record && record.id) || (record && record.key) || '').replace(/\D/g, '') || String(index + 1); + return 'JQ' + idPart.padStart(4, '0') + String(100 + index); +} + +function h2BridgeMapToStationLedgerRow(row, index) { + var costTotal = row.costTotal != null ? row.costTotal : row.costAmount; + var isReconciled = row.reconcileStatus === H2_RECONCILE_RECONCILED; + return { + id: row.id || row.key || ('ledger-' + index), + stationName: row.stationName, + hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime), + plateNo: row.plateNo, + customerName: row.customerName, + hydrogenKg: row.hydrogenKg, + costUnitPrice: row.costUnitPrice, + costTotal: costTotal, + settlementStatus: row.settlementStatus, + orderNo: h2BridgeBuildRefuelOrderNo(row, index), + reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING), + statementRecordId: row.statementRecordId || null, + reconcileDate: row.reconcileDate || null, + receiptDate: row.receiptDate || null, + paymentStatus: row.paymentStatus || (isReconciled ? 'unpaid' : null) + }; +} + +function h2BridgeMapToHrRecord(row, index, fillerNames) { + var fillers = fillerNames || ['张三', '李四', '王静', '赵敏', '陈浩']; + return { + id: row.id || row.key || ('hr-' + (index + 1)), + hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime), + plateNo: row.plateNo, + hydrogenKg: row.hydrogenKg, + customerAmount: row.customerAmount, + mileageKm: row.mileageKm, + fillerName: row.creatorName || fillers[index % fillers.length], + settlementStatus: row.settlementStatus, + stationName: row.stationName, + stationCode: row.stationId || H2_STATION_CODE_MAP[row.stationName] || '' + }; +} + +function h2BridgeMapToOrderRecord(row, index) { + var time = h2BridgeFormatDateTime(row.hydrogenTime); + return { + id: index + 1, + stationCode: row.stationId || H2_STATION_CODE_MAP[row.stationName] || '', + stationName: row.stationName, + time: time.length >= 16 ? time.slice(0, 16) : time, + plateNo: (row.plateNo || '').replace(/F$/, ''), + amountKg: row.hydrogenKg, + amountYuan: row.customerAmount + }; +} + +function h2BridgeGetStore() { + if (typeof window === 'undefined') return null; + if (!window.H2_VEHICLE_LEDGER_STORE) { + window.H2_VEHICLE_LEDGER_STORE = h2BridgeCreateStore(); + } + return window.H2_VEHICLE_LEDGER_STORE; +} + +function h2BridgeEnsureApi() { + if (typeof window === 'undefined') return; + var store = h2BridgeGetStore(); + var existing = window.H2_VEHICLE_LEDGER_API || {}; + window.H2_VEHICLE_LEDGER_API = Object.assign({}, existing, { + getRows: function () { return store.getRows(); }, + syncRows: function (rows) { + if (!rows) return; + store.setRows(rows); + }, + applyPatches: function (patches) { + store.applyPatches(patches); + if (typeof existing.applyPatches === 'function') { + existing.applyPatches(patches); + } + } + }); +} + +function h2BridgeGetAllRefuelRecords() { + var store = h2BridgeGetStore(); + return store.getRows().map(h2BridgeMapToRefuelRecord); +} + +function h2BridgeGetRefuelRecordsByStation(stationName) { + var name = String(stationName || '').trim(); + return h2BridgeGetAllRefuelRecords() + .filter(function (r) { return (r.stationName || '') === name; }) + .sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); +} + +function h2BridgeBuildStationLedgerStore() { + var store = h2BridgeGetStore(); + return store.getRows().map(h2BridgeMapToStationLedgerRow); +} + +function h2BridgeGetHrRecords(fillerNames) { + var store = h2BridgeGetStore(); + return store.getRows().map(function (row, index) { + return h2BridgeMapToHrRecord(row, index, fillerNames); + }); +} + +function h2BridgeGetOrderRecords() { + var store = h2BridgeGetStore(); + return store.getRows().map(h2BridgeMapToOrderRecord); +} + +function h2BridgeGetStationList() { + var seen = {}; + var list = []; + h2BridgeGetStore().getRows().forEach(function (row) { + var code = row.stationId || H2_STATION_CODE_MAP[row.stationName]; + if (!code || seen[code]) return; + seen[code] = true; + list.push({ value: code, label: row.stationName }); + }); + return list; +} + +function h2BridgeInit() { + h2BridgeEnsureApi(); + var store = h2BridgeGetStore(); + if (typeof window !== 'undefined') { + window.H2VehicleLedgerBridge = { + getRows: function () { return store.getRows(); }, + getAllRefuelRecords: h2BridgeGetAllRefuelRecords, + getRefuelRecordsByStation: h2BridgeGetRefuelRecordsByStation, + buildStationLedgerStore: h2BridgeBuildStationLedgerStore, + getHrRecords: h2BridgeGetHrRecords, + getOrderRecords: h2BridgeGetOrderRecords, + getStationList: h2BridgeGetStationList, + subscribe: function (fn) { return store.subscribe(fn); } + }; + } + return store; +} + +h2BridgeInit(); + +export { + H2_CANONICAL_LEDGER_SEED, + H2_STATION_CODE_MAP, + h2BridgeInit, + h2BridgeGetStore, + h2BridgeGetAllRefuelRecords, + h2BridgeGetRefuelRecordsByStation, + h2BridgeBuildStationLedgerStore, + h2BridgeGetHrRecords, + h2BridgeGetOrderRecords, + h2BridgeGetStationList +}; diff --git a/src/prototypes/lease-business-ledger/components/DeleteConfirmModal.tsx b/src/prototypes/lease-business-ledger/components/DeleteConfirmModal.tsx new file mode 100644 index 0000000..89c2fd3 --- /dev/null +++ b/src/prototypes/lease-business-ledger/components/DeleteConfirmModal.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { X } from 'lucide-react'; + +interface DeleteConfirmModalProps { + open: boolean; + plateNo: string; + onCancel: () => void; + onConfirm: () => void; +} + +export function DeleteConfirmModal({ open, plateNo, onCancel, onConfirm }: DeleteConfirmModalProps) { + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > +
+

确认删除

+ +
+
+

+ 删除后该条台账记录将无法恢复,是否确认删除? +

+

{plateNo ? `车牌:${plateNo}` : ''}

+
+
+ + +
+
+
+ ); +} diff --git a/src/prototypes/lease-business-ledger/components/FilterPanel.tsx b/src/prototypes/lease-business-ledger/components/FilterPanel.tsx index c26775e..b8d38ac 100644 --- a/src/prototypes/lease-business-ledger/components/FilterPanel.tsx +++ b/src/prototypes/lease-business-ledger/components/FilterPanel.tsx @@ -35,37 +35,52 @@ export function FilterPanel({ records, filters, onChange, onReset, onSearch }: F { label: '付款日期', control: (
onChange({ paymentFrom: e.target.value })} /> - + onChange({ paymentTo: e.target.value })} />
), }, ]; - const visible = expanded ? fields : fields.slice(0, DEFAULT_VISIBLE); + const primaryFields = fields.slice(0, DEFAULT_VISIBLE); + const extraFields = fields.slice(DEFAULT_VISIBLE); return ( -
+

租赁业务台账

- {visible.map((field) => ( + {primaryFields.map((field) => ( ))}
+ {extraFields.length > 0 && ( +
+
+
+ {extraFields.map((field) => ( + + ))} +
+
+
+ )}
- {fields.length > DEFAULT_VISIBLE && ( - )} - - + diff --git a/src/prototypes/lease-business-ledger/components/LeaseLedgerTable.tsx b/src/prototypes/lease-business-ledger/components/LeaseLedgerTable.tsx index a30ce30..5328ce9 100644 --- a/src/prototypes/lease-business-ledger/components/LeaseLedgerTable.tsx +++ b/src/prototypes/lease-business-ledger/components/LeaseLedgerTable.tsx @@ -1,20 +1,24 @@ -import React from 'react'; -import { AlertTriangle, CircleHelp } from 'lucide-react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ArrowDown, ArrowUp, ArrowUpDown, AlertTriangle, CircleHelp } from 'lucide-react'; import type { AccidentRecord, LeaseLedgerRow, MaintenanceOrder } from '../types'; import { calcMonthlyIncome, calcUnreceived, canStaffDeleteRow, canStaffEditRow, + displayMoney, + displayText, formatMoney, + isBlankDisplayValue, needsAccidentLinkWarning, needsMaintenanceLinkWarning, } from '../utils/ledger'; -import { LEASE_TABLE_COLUMNS, STATUS_CLASS } from './tableColumns'; +import { LEASE_TABLE_COLUMNS, STATUS_CLASS, type LeaseTableColumn } from './tableColumns'; interface LeaseLedgerTableProps { records: LeaseLedgerRow[]; selectedIds: Set; + loading?: boolean; maintenanceOrders: MaintenanceOrder[]; accidentRecords: AccidentRecord[]; onToggleRow: (id: string, checked: boolean) => void; @@ -31,6 +35,62 @@ interface LeaseLedgerTableProps { onDeductionDetailChange: (recordId: string, value: string) => void; } +type SortDir = 'asc' | 'desc'; + +const CHECK_COL_WIDTH = 48; +const ACTION_COL_WIDTH = 168; + +const DEFAULT_COLUMN_WIDTHS: Record = { + _index: 56, + status: 88, + maintainerName: 88, + year: 72, + month: 64, + dept: 120, + paymentDate: 108, + plateNo: 120, + customerName: 160, + valueAddedService: 96, + exemptionPolicy: 96, + pickupDate: 108, + contractStartDate: 116, + contractEndDate: 116, + periodStartDate: 108, + returnDate: 108, + avgDays: 88, + deposit: 88, + contractRent: 112, + receivableTotal: 112, + receivableRent: 112, + monthlyIncome: 112, + monthlyRent: 112, + maintenanceIncome: 112, + insuranceSurcharge: 112, + opsFeeIncome: 100, + otherIncome: 100, + mileageDeduction: 112, + otherDeduction: 112, + receivedAmount: 112, + unreceived: 112, + invoiceDate: 108, + actualPaymentDate: 116, + paymentMethod: 96, + vehicleStandardCost: 112, + vehicleActualCost: 112, + insuranceFee: 96, + hydrogenFee: 96, + opsFeeCost: 100, + brokerageFee: 96, + otherCost: 96, + totalCost: 112, + profitLoss: 112, + gpsMileage: 96, + otherDeductionDetail: 160, + assetOwner: 140, + signingCompany: 160, + remark: 140, +}; + function IncomeWarning({ title, onClick }: { title: string; onClick: () => void }) { return ( + ); } function MoneyInput({ @@ -103,9 +193,32 @@ function TextInput({ ); } +function getSortValue(row: LeaseLedgerRow, col: LeaseTableColumn): string | number { + switch (col.render) { + case 'monthly-income': + return calcMonthlyIncome(row); + case 'stack-dept': + return row.dept; + case 'stack-plate': + return row.plateNo; + case 'stack-customer': + return row.customerName; + case 'status': + return row.status; + default: + break; + } + if (col.key === 'unreceived') return calcUnreceived(row); + if (col.key === 'profitLoss') return row.profitLoss; + const raw = row[col.key as keyof LeaseLedgerRow]; + if (typeof raw === 'number') return raw; + if (typeof raw === 'string') return raw; + return ''; +} + function renderCell( row: LeaseLedgerRow, - col: (typeof LEASE_TABLE_COLUMNS)[number], + col: LeaseTableColumn, index: number, ctx: { maintenanceOrders: MaintenanceOrder[]; @@ -126,33 +239,33 @@ function renderCell( case 'stack-dept': return (
- {row.dept} - {row.salesman} + {displayText(row.dept)} + {displayText(row.salesman)}
); case 'stack-plate': { const vehicleLine = row.vehicleType || [row.vehicleBrand, row.vehicleModel].filter(Boolean).join(''); return (
- {row.plateNo} - {vehicleLine} + {displayText(row.plateNo)} + {displayText(vehicleLine)}
); } case 'stack-customer': return (
- {row.customerName} - {row.projectName} + {displayText(row.customerName)} + {displayText(row.projectName)}
); case 'deposit': - if (row.deposit === '/' || row.deposit === '') return '—'; - return typeof row.deposit === 'number' ? formatMoney(row.deposit) : String(row.deposit); + if (isBlankDisplayValue(row.deposit)) return ''; + return typeof row.deposit === 'number' ? formatMoney(row.deposit) : displayText(row.deposit); case 'asset-owner': - return {row.assetOwner || '—'}; + return {displayText(row.assetOwner)}; case 'signing-company': - return {row.signingCompany || '—'}; + return {displayText(row.signingCompany)}; case 'monthly-income': return ( @@ -201,7 +314,7 @@ function renderCell( ); case 'other-income': - if (!ctx.canEdit) return row.otherIncome ? formatMoney(row.otherIncome) : '—'; + if (!ctx.canEdit) return displayMoney(row.otherIncome, true); return ( ); case 'other-cost': - if (!ctx.canEdit) return row.otherCost ? formatMoney(row.otherCost) : '—'; + if (!ctx.canEdit) return displayMoney(row.otherCost, true); return ( ); case 'brokerage-fee': - if (!ctx.canEdit) return row.brokerageFee ? formatMoney(row.brokerageFee) : '—'; + if (!ctx.canEdit) return displayMoney(row.brokerageFee, true); return ( ); case 'invoice-date': - return {row.invoiceDate || '—'}; + return {displayText(row.invoiceDate)}; case 'vehicle-standard-cost': return ( @@ -257,7 +370,7 @@ function renderCell( case 'insurance-readonly': return ( - {row.insuranceFee ? formatMoney(row.insuranceFee) : '—'} + {displayMoney(row.insuranceFee, true)} ); case 'hydrogen-fee': @@ -266,17 +379,17 @@ function renderCell( className={row.hydrogenBorneBy === '我司承担' ? 'ldb-computed-val' : 'ldb-cell-readonly'} title={row.hydrogenBorneBy === '我司承担' ? '我司承担氢费账单期汇总' : '客户承担或无氢费'} > - {row.hydrogenFee ? formatMoney(row.hydrogenFee) : '—'} + {displayMoney(row.hydrogenFee, true)} ); case 'gps-mileage': return ( - {row.gpsMileage || '—'} + {displayText(row.gpsMileage)} ); case 'deduction-detail': - if (!ctx.canEdit) return row.otherDeductionDetail || '—'; + if (!ctx.canEdit) return displayText(row.otherDeductionDetail); return ( ); case 'remark-text': - if (!ctx.canEdit) return row.remark || '—'; + if (!ctx.canEdit) return displayText(row.remark); return ( + {Array.from({ length: 6 }, (_, i) => ( +
+ ))} +
+ ); +} + export function LeaseLedgerTable({ records, selectedIds, + loading = false, maintenanceOrders, accidentRecords, onToggleRow, @@ -334,47 +458,145 @@ export function LeaseLedgerTable({ onRemarkChange, onDeductionDetailChange, }: LeaseLedgerTableProps) { - const allSelected = records.length > 0 && records.every((r) => selectedIds.has(r.id)); + const [sortKey, setSortKey] = useState(null); + const [sortDir, setSortDir] = useState('asc'); + const [columnWidths, setColumnWidths] = useState>(() => ({ ...DEFAULT_COLUMN_WIDTHS })); + const resizeRef = useRef<{ key: string; startX: number; startWidth: number } | null>(null); - if (!records.length) { + const sortedRecords = useMemo(() => { + if (!sortKey) return records; + const col = LEASE_TABLE_COLUMNS.find((c) => c.key === sortKey); + if (!col) return records; + const dir = sortDir === 'asc' ? 1 : -1; + return [...records].sort((a, b) => { + const av = getSortValue(a, col); + const bv = getSortValue(b, col); + if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir; + return String(av).localeCompare(String(bv), 'zh-CN') * dir; + }); + }, [records, sortKey, sortDir]); + + const allSelected = sortedRecords.length > 0 && sortedRecords.every((r) => selectedIds.has(r.id)); + + const handleSort = useCallback((key: string) => { + setSortKey((prev) => { + if (prev === key) { + setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); + return key; + } + setSortDir('asc'); + return key; + }); + }, []); + + const handleResizeStart = useCallback((key: string, event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + resizeRef.current = { + key, + startX: event.clientX, + startWidth: columnWidths[key] ?? DEFAULT_COLUMN_WIDTHS[key] ?? 100, + }; + }, [columnWidths]); + + useEffect(() => { + const onMove = (event: MouseEvent) => { + if (!resizeRef.current) return; + const delta = event.clientX - resizeRef.current.startX; + const next = Math.max(56, resizeRef.current.startWidth + delta); + setColumnWidths((prev) => ({ ...prev, [resizeRef.current!.key]: next })); + }; + const onUp = () => { + resizeRef.current = null; + }; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + return () => { + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + }; + }, []); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!sortedRecords.length) { return

暂无符合条件的台账记录

; } + const tableMinWidth = CHECK_COL_WIDTH + ACTION_COL_WIDTH + LEASE_TABLE_COLUMNS.reduce( + (sum, col) => sum + (columnWidths[col.key] ?? DEFAULT_COLUMN_WIDTHS[col.key] ?? 100), + 0, + ); + return (
- +
+ + + {LEASE_TABLE_COLUMNS.map((col) => ( + + ))} + + - {LEASE_TABLE_COLUMNS.map((col) => ( - - ))} + {LEASE_TABLE_COLUMNS.map((col) => { + const sortable = col.sortable !== false && col.key !== '_index'; + return ( + + ); + })} - {records.map((row, index) => { + {sortedRecords.map((row, index) => { const canEdit = canStaffEditRow(row); const canDelete = canStaffDeleteRow(row); + const isDraft = row.status === '草稿'; const showReceiptLink = row.status === '已确认' || row.status === '部分收款'; const cellCtx = { maintenanceOrders, @@ -412,37 +634,23 @@ export function LeaseLedgerTable({ ))} ); diff --git a/src/prototypes/lease-business-ledger/components/tableColumns.ts b/src/prototypes/lease-business-ledger/components/tableColumns.ts index 99b976a..4921944 100644 --- a/src/prototypes/lease-business-ledger/components/tableColumns.ts +++ b/src/prototypes/lease-business-ledger/components/tableColumns.ts @@ -10,6 +10,8 @@ export interface LeaseTableColumn { className?: string; annotationId?: string; headerTip?: string; + sortable?: boolean; + defaultWidth?: number; render?: | 'stack-dept' | 'stack-plate' | 'stack-customer' | 'status' | 'deposit' | 'asset-owner' | 'signing-company' diff --git a/src/prototypes/lease-business-ledger/index.tsx b/src/prototypes/lease-business-ledger/index.tsx index 070a363..83a8736 100644 --- a/src/prototypes/lease-business-ledger/index.tsx +++ b/src/prototypes/lease-business-ledger/index.tsx @@ -21,6 +21,7 @@ import { FilterPanel } from './components/FilterPanel'; import { LedgerKpiRow } from './components/LedgerKpiRow'; import { LeaseLedgerTable } from './components/LeaseLedgerTable'; import { ConfirmMarkModal } from './components/ConfirmMarkModal'; +import { DeleteConfirmModal } from './components/DeleteConfirmModal'; import { ReceiptLinkModal } from './components/ReceiptLinkModal'; import { MaintenanceLinkModal } from './components/MaintenanceLinkModal'; import { AccidentLinkModal } from './components/AccidentLinkModal'; @@ -77,6 +78,9 @@ export default function LeaseBusinessLedgerApp() { const [showUnpaidOnly, setShowUnpaidOnly] = useState(false); const [listViewMode, setListViewMode] = useState('all'); const [visibleCount, setVisibleCount] = useState(LOAD_BATCH_SIZE); + const [tableLoading, setTableLoading] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [submitApprovalOpen, setSubmitApprovalOpen] = useState(false); const showToast = useCallback((msg: string) => { setToast(msg); @@ -109,8 +113,12 @@ export default function LeaseBusinessLedgerApp() { ); const handleSearch = () => { - setAppliedFilters(pendingFilters); - setSelectedIds(new Set()); + setTableLoading(true); + window.setTimeout(() => { + setAppliedFilters(pendingFilters); + setSelectedIds(new Set()); + setTableLoading(false); + }, 320); }; const handleReset = () => { @@ -274,17 +282,17 @@ export default function LeaseBusinessLedgerApp() { 仅显示未收款数据
- - + + +
+ + )} ): number { return Math.round((row.receivableTotal - row.receivedAmount) * 100) / 100; } diff --git a/src/prototypes/lease-contract-management/LeaseContractCreate.jsx b/src/prototypes/lease-contract-management/LeaseContractCreate.jsx index 0784d26..e7e5908 100644 --- a/src/prototypes/lease-contract-management/LeaseContractCreate.jsx +++ b/src/prototypes/lease-contract-management/LeaseContractCreate.jsx @@ -8,6 +8,7 @@ import { getContractTemplatePreviewBadge, getContractTypeLabel, } from '../contract-template-management/contract-template-catalog.js'; +import { buildLeaseContractEditFormState } from './lease-contract-edit-bridge.js'; import { createDefaultLeaseOrderState, createDefaultPowerOfAttorneyState } from './lease-order-vars.js'; import { buildLeaseContractPreviewHtml } from './lease-contract-preview-build.js'; import { @@ -250,6 +251,40 @@ export default function LeaseContractCreate({ onBack, initialScrollSection, edit return function () { window.clearTimeout(timer); }; }, [initialScrollSection, editRecord && editRecord.id]); + useEffect(function () { + if (!editRecord || !editRecord.id) return; + var formState = buildLeaseContractEditFormState(editRecord); + if (!formState) return; + setContractTemplateId(formState.contractTemplateId || ''); + setLessorId(formState.lessorId || ''); + setCustomerId(formState.customerId || ''); + setContractCode(formState.contractCode || ''); + setBusinessDept(formState.businessDept || ''); + setBusinessOwner(formState.businessOwner || ''); + setMileage(Object.assign({}, formState.mileage)); + setFeeInfo(Object.assign({}, formState.feeInfo)); + setLeaseOrder({ + insuredVehicleCount: formState.leaseOrder.insuredVehicleCount, + thirdPartyLiabilityMillion: formState.leaseOrder.thirdPartyLiabilityMillion, + deliveryRegion: (formState.leaseOrder.deliveryRegion || []).slice(), + deliveryDate: formState.leaseOrder.deliveryDate, + rows: (formState.leaseOrder.rows || []).map(function (row) { + return Object.assign({}, row, { + brandModels: row.brandModels ? row.brandModels.map(function (pair) { return pair.slice(); }) : [], + plateNos: row.plateNos ? row.plateNos.slice() : undefined, + extraServices: row.extraServices ? row.extraServices.slice() : [], + }); + }), + }); + setPowerOfAttorney({ + delegates: (formState.powerOfAttorney.delegates || []).map(function (row) { + return Object.assign({}, row); + }), + }); + setContractRemark(formState.contractRemark || ''); + setSealTypes((formState.sealTypes || ['contract']).slice()); + }, [editRecord && editRecord.id]); + var createSteps = useMemo(function () { return [ { diff --git a/src/prototypes/lease-contract-management/LeaseContractFleetSummary.jsx b/src/prototypes/lease-contract-management/LeaseContractFleetSummary.jsx index 3b1d442..bafcb01 100644 --- a/src/prototypes/lease-contract-management/LeaseContractFleetSummary.jsx +++ b/src/prototypes/lease-contract-management/LeaseContractFleetSummary.jsx @@ -1,26 +1,13 @@ // 在租车辆概览 — 按品牌型号统计 -import { buildOnLeaseFleetByBrandModel, formatDeliveryRegion } from './lease-contract-list-data.js'; - -function formatPlateLabel(plateNo) { - var plate = plateNo && String(plateNo).trim(); - if (!plate || plate === '-') return '待上牌'; - return plate; -} - -function formatMileageLabel(vehicle) { - var raw = vehicle.currentMileage; - if (raw == null || raw === '' || raw === '-') return '-'; - var num = Number(String(raw).replace(/,/g, '')); - if (!Number.isFinite(num)) return String(raw); - return num.toLocaleString('zh-CN') + '\u00a0km'; -} +import { buildOnLeaseFleetByBrandModel } from './lease-contract-list-data.js'; function FleetDetailModal(props) { var Modal = props.Modal; var item = props.item; var open = props.open; var onClose = props.onClose; - var onViewContract = props.onViewContract; + var renderVehicleNativeTableHeader = props.renderVehicleNativeTableHeader; + var renderVehicleNativeTableRow = props.renderVehicleNativeTableRow; if (!Modal) return null; var titleNode = item @@ -31,6 +18,14 @@ function FleetDetailModal(props) { ) : null; + var canRenderTable = Boolean( + item + && item.entries + && item.entries.length + && renderVehicleNativeTableHeader + && renderVehicleNativeTableRow, + ); + return React.createElement(Modal, { className: 'lc-fleet-detail-modal', rootClassName: 'lc-fleet-detail-modal-root', @@ -46,86 +41,29 @@ function FleetDetailModal(props) { centered: true, destroyOnClose: true, }, - item && item.entries && item.entries.length + canRenderTable ? React.createElement('div', { className: 'lc-fleet-detail-modal__body' }, - React.createElement('div', { className: 'lc-fleet-detail-table-wrap' }, - React.createElement('table', { className: 'lc-fleet-detail-table' }, - React.createElement('thead', null, - React.createElement('tr', null, - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-plate' }, '车牌号'), - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-vin' }, '识别代码'), - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-type' }, '车辆类型'), - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-announce' }, '公告型号'), - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-city' }, '运营城市'), - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-mileage' }, '里程数'), - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-project' }, '所属项目'), - React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-action' }, '操作'), + React.createElement('div', { className: 'lc-fleet-detail-modal__table-host vm-page lc-page' }, + React.createElement('div', { className: 'lc-contract-vehicle-table-wrap' }, + React.createElement('table', { className: 'lc-contract-vehicle-native-table lc-fleet-detail-modal__vehicle-table' }, + renderVehicleNativeTableHeader(), + React.createElement('tbody', null, + item.entries.map(function (entry, index) { + var record = entry.record; + var vehicle = entry.vehicle; + var vehicleIndex = (record && record.vehicles) + ? record.vehicles.indexOf(vehicle) + : -1; + if (vehicleIndex < 0) vehicleIndex = index; + return renderVehicleNativeTableRow( + record, + vehicle, + vehicleIndex, + (record && record.id ? record.id : 'rec') + '-fleet-' + index, + ); + }), ), ), - React.createElement('tbody', null, - item.entries.map(function (entry, index) { - var vehicle = entry.vehicle; - var record = entry.record; - var plate = formatPlateLabel(vehicle.plateNo); - var vin = vehicle.vin && String(vehicle.vin).trim() ? vehicle.vin : '-'; - var regionLabel = formatDeliveryRegion(record.deliveryRegion) || '-'; - var mileageLabel = formatMileageLabel(vehicle); - return React.createElement('tr', { key: record.id + '-' + index }, - React.createElement('td', { className: 'lc-fleet-detail-table__col-plate' }, - React.createElement('span', { - className: 'lc-fleet-detail-table__plate tabular-nums', - title: plate, - }, plate), - ), - React.createElement('td', { className: 'lc-fleet-detail-table__col-vin' }, - React.createElement('span', { - className: 'lc-fleet-detail-table__vin tabular-nums', - title: vin, - }, vin), - ), - React.createElement('td', { className: 'lc-fleet-detail-table__col-type' }, - React.createElement('span', { className: 'lc-fleet-detail-table__type-pill' }, vehicle.vehicleType || '-'), - ), - React.createElement('td', { className: 'lc-fleet-detail-table__col-announce' }, - React.createElement('span', { - className: 'lc-fleet-detail-table__announce', - title: vehicle.officialModel || '-', - }, vehicle.officialModel || '-'), - ), - React.createElement('td', { className: 'lc-fleet-detail-table__col-city' }, - React.createElement('span', { className: 'lc-fleet-detail-table__city' }, regionLabel), - ), - React.createElement('td', { className: 'lc-fleet-detail-table__col-mileage' }, - React.createElement('span', { className: 'lc-fleet-detail-table__mileage tabular-nums' }, mileageLabel), - ), - React.createElement('td', { className: 'lc-fleet-detail-table__col-project' }, - React.createElement('div', { className: 'lc-fleet-detail-table__project' }, - React.createElement('span', { - className: 'lc-fleet-detail-table__project-name', - title: record.projectName || '-', - }, record.projectName || '-'), - React.createElement('button', { - type: 'button', - className: 'lc-fleet-detail-table__contract-link tabular-nums', - onClick: function () { onViewContract(record); }, - title: '查看合同 ' + (record.contractCode || ''), - }, record.contractCode || '-'), - React.createElement('span', { - className: 'lc-fleet-detail-table__customer', - title: record.customerName || '-', - }, record.customerName || '-'), - ), - ), - React.createElement('td', { className: 'lc-fleet-detail-table__col-action' }, - React.createElement('button', { - type: 'button', - className: 'lc-fleet-detail-table__action', - onClick: function () { onViewContract(record); }, - }, '查看合同'), - ), - ); - }), - ), ), ), ) @@ -135,8 +73,9 @@ function FleetDetailModal(props) { export default function LeaseContractFleetSummary(props) { var records = props.records || []; - var onViewContract = props.onViewContract || function () {}; var Modal = props.Modal; + var renderVehicleNativeTableHeader = props.renderVehicleNativeTableHeader; + var renderVehicleNativeTableRow = props.renderVehicleNativeTableRow; var useState = React.useState; var useMemo = React.useMemo; @@ -299,7 +238,8 @@ export default function LeaseContractFleetSummary(props) { item: detailItem, open: detailOpen, onClose: closeDetail, - onViewContract: onViewContract, + renderVehicleNativeTableHeader: renderVehicleNativeTableHeader, + renderVehicleNativeTableRow: renderVehicleNativeTableRow, }), ); } diff --git a/src/prototypes/lease-contract-management/LeaseContractManagement.jsx b/src/prototypes/lease-contract-management/LeaseContractManagement.jsx index 0b33535..ff5ed56 100644 --- a/src/prototypes/lease-contract-management/LeaseContractManagement.jsx +++ b/src/prototypes/lease-contract-management/LeaseContractManagement.jsx @@ -40,12 +40,13 @@ import { DELIVERY_DATE_TBD_LABEL, isDeliveryDateTbd, formatContractDeliveryDateLabel, + formatVehicleDeliveryPlanDateLabel, + resolveVehicleDeliveryDateTbd, + resolveVehiclePlannedDeliveryDate, resolveContractStatusByVehicleLease, } from './lease-contract-list-data.js'; import { getLessorCompanies } from '../contract-template-management/contract-template-vars.js'; -var LC_LIST_TABLE_EXPAND_WIDTH = 48; - const Component = function(props) { var useEffect = React.useEffect; var ONEOS_ANT_TABLE_GLOBAL_FIX = [ @@ -70,6 +71,7 @@ const Component = function(props) { var Tag = antd.Tag; var Avatar = antd.Avatar; var Dropdown = antd.Dropdown; + var Tooltip = antd.Tooltip; var Modal = antd.Modal; var Upload = antd.Upload; var message = antd.message; @@ -119,6 +121,9 @@ const Component = function(props) { var _deliveryChangePopoverRecord = useState(null); var _deliveryChangeDraftDate = useState(null); var _deliveryChangeDraftTbd = useState(false); + var _vehicleDeliveryChangeTarget = useState(null); + var _vehicleDeliveryChangeDraftDate = useState(null); + var _vehicleDeliveryChangeDraftTbd = useState(false); var _delegatePopoverRecord = useState(null); var _authorizedModalVisible = useState(false); var _authorizedModalRecord = useState(null); @@ -139,6 +144,7 @@ const Component = function(props) { var _stampedUploadedOverride = useState({}); // 撤回等列表行状态覆盖(原型) var _recordOverrides = useState({}); + var _expandedRowKeys = useState([]); useEffect(function() { if (props && props.onPageViewChange) { @@ -1000,16 +1006,16 @@ const Component = function(props) { message.info('查看(跳转车辆租赁合同-查看)' + (code ? ':' + code : '')); } - function renderMileageProgressCell(vehicle) { - var percent = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0)); - return React.createElement('div', { className: 'lc-vehicle-mileage-progress' }, + function renderMileageProgressBarRow(percent, ariaLabel) { + percent = Math.min(100, Math.max(0, Number(percent) || 0)); + return React.createElement('div', { className: 'lc-contract-mileage-completion__bar-row' }, React.createElement('div', { className: 'lc-vehicle-mileage-progress__track', role: 'progressbar', 'aria-valuenow': percent, 'aria-valuemin': 0, 'aria-valuemax': 100, - 'aria-label': '里程完成 ' + percent + '%', + 'aria-label': ariaLabel, }, React.createElement('div', { className: 'lc-vehicle-mileage-progress__bar', @@ -1261,15 +1267,17 @@ const Component = function(props) { ); } var rangeData = getMileageCompletionRangeData(vehicle, contractRecord); + var percent = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0)); + var rangeRow = rangeData + ? React.createElement('div', { className: 'lc-contract-mileage-completion__range-row' }, + renderMileageCompletionRangeStart(rangeData), + renderMileageCompletionRangeEnd(rangeData), + ) + : null; return React.createElement('div', { className: 'lc-contract-mileage-completion' }, remainingNode, - React.createElement('div', { className: 'lc-contract-mileage-completion__progress-row' }, - React.createElement('div', { className: 'lc-contract-mileage-completion__progress-main' }, - renderMileageProgressCell(vehicle), - ), - renderMileageCompletionRangeEnd(rangeData), - ), - renderMileageCompletionRangeStart(rangeData), + renderMileageProgressBarRow(percent, '里程完成 ' + percent + '%'), + rangeRow, ); } @@ -1376,6 +1384,98 @@ const Component = function(props) { ); } + function renderVehicleDeliveryPlanCell(record, vehicle, vehicleIndex) { + var regionLabel = formatDeliveryRegion(record.deliveryRegion); + var dateLabel = formatVehicleDeliveryPlanDateLabel(vehicle, record); + var delivered = isContractVehicleDelivered(vehicle); + var isTbd = !delivered && resolveVehicleDeliveryDateTbd(vehicle, record); + var target = _vehicleDeliveryChangeTarget[0]; + var popoverOpen = Boolean( + target + && target.recordId === record.id + && target.vehicleIndex === vehicleIndex, + ); + return React.createElement('div', { className: 'lc-delivery-cell lc-delivery-cell--vehicle' }, + React.createElement('span', { + className: 'lc-delivery-cell__region', + title: regionLabel, + }, regionLabel), + React.createElement('div', { className: 'lc-delivery-cell__date-row' }, + React.createElement('span', { + className: 'lc-delivery-cell__date tabular-nums' + (isTbd ? ' lc-delivery-cell__date--tbd' : ''), + title: delivered ? '实际交车 ' + dateLabel : '计划交车 ' + dateLabel, + }, dateLabel), + delivered + ? null + : React.createElement(Popover, { + overlayClassName: 'lc-delivery-change-popover', + placement: 'bottom', + trigger: 'click', + arrow: { pointAtCenter: true }, + destroyTooltipOnHide: true, + open: popoverOpen, + title: null, + content: renderVehicleDeliveryChangePanel(record, vehicle, vehicleIndex), + onOpenChange: function (visible) { + if (!visible) { + closeVehicleDeliveryChangePopover(); + return; + } + closeDeliveryChangePopover(); + _vehiclePopoverRecord[1](null); + _deliveredPopoverRecord[1](null); + closeDelegatePopover(); + var vehicleTbd = resolveVehicleDeliveryDateTbd(vehicle, record); + _vehicleDeliveryChangeTarget[1]({ + recordId: record.id, + vehicleIndex: vehicleIndex, + }); + _vehicleDeliveryChangeDraftTbd[1](!!vehicleTbd); + _vehicleDeliveryChangeDraftDate[1](vehicleTbd + ? null + : (resolveVehiclePlannedDeliveryDate(vehicle, record) || null)); + }, + }, + React.createElement('button', { + type: 'button', + className: 'lc-delivery-cell__edit-btn', + 'aria-label': '编辑本车交车时间', + }, renderDeliveryEditIcon()), + ), + ), + ); + } + + function renderContractVehicleNativeTableHeader() { + return React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__info' }, '车辆信息'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__pickup' }, '提车应收款'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '交车情况' }, '交车'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '还车情况' }, '还车'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__return-settlement' }, '还车应结款'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__delivery-plan', title: '交车安排' }, '交车安排'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req', title: '里程要求' }, '里程要求'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion', title: '里程完成情况' }, '里程完成情况'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__action' }, '操作'), + ), + ); + } + + function renderContractVehicleNativeTableRow(record, vehicle, index, rowKey) { + return React.createElement('tr', { key: rowKey || (record.id + '-vehicle-' + index) }, + React.createElement('td', { className: 'lc-contract-vehicle-native-table__info' }, renderVehicleInfoCell(vehicle)), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__pickup' }, renderPickupReceivableCell(vehicle)), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery', { compact: true })), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return', { compact: true })), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__return-settlement' }, renderReturnSettlementCell(vehicle)), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__delivery-plan' }, renderVehicleDeliveryPlanCell(record, vehicle, index)), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-req' }, renderMileageRequirementCell(record, vehicle)), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-completion' }, renderMileageCompletionCell(vehicle, record)), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__action' }, renderContractVehicleActionCell(vehicle)), + ); + } + function renderExpandedVehicleTable(record) { var vehicles = record.vehicles || []; if (!vehicles.length) { @@ -1383,30 +1483,10 @@ const Component = function(props) { } return React.createElement('div', { className: 'lc-contract-vehicle-table-wrap' }, React.createElement('table', { className: 'lc-contract-vehicle-native-table' }, - React.createElement('thead', null, - React.createElement('tr', null, - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__info' }, '车辆信息'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__pickup' }, '提车应收款'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '交车情况' }, '交车'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '还车情况' }, '还车'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__return-settlement' }, '还车应结款'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req', title: '里程要求' }, '里程要求'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion', title: '里程完成情况' }, '里程完成情况'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__action' }, '操作'), - ), - ), + renderContractVehicleNativeTableHeader(), React.createElement('tbody', null, vehicles.map(function (vehicle, index) { - return React.createElement('tr', { key: record.id + '-vehicle-' + index }, - React.createElement('td', { className: 'lc-contract-vehicle-native-table__info' }, renderVehicleInfoCell(vehicle)), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__pickup' }, renderPickupReceivableCell(vehicle)), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery', { compact: true })), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return', { compact: true })), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__return-settlement' }, renderReturnSettlementCell(vehicle)), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-req' }, renderMileageRequirementCell(record, vehicle)), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-completion' }, renderMileageCompletionCell(vehicle, record)), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__action' }, renderContractVehicleActionCell(vehicle)), - ); + return renderContractVehicleNativeTableRow(record, vehicle, index); }), ), ), @@ -1485,31 +1565,73 @@ const Component = function(props) { }, fullName); } + function toggleContractRowExpand(recordId) { + _expandedRowKeys[1](function (prev) { + if (prev.indexOf(recordId) >= 0) { + return prev.filter(function (key) { return key !== recordId; }); + } + return prev.concat([recordId]); + }); + } + + function renderContractRowExpandIcon(record) { + var vehicles = record.vehicles || []; + var expandable = vehicles.length > 0; + var expanded = _expandedRowKeys[0].indexOf(record.id) >= 0; + if (!expandable) { + return React.createElement('span', { + className: 'ant-table-row-expand-icon ant-table-row-spaced', + 'aria-hidden': true, + }); + } + return React.createElement('span', { + className: 'ant-table-row-expand-icon ' + (expanded ? 'ant-table-row-expanded' : 'ant-table-row-collapsed'), + role: 'button', + tabIndex: 0, + 'aria-label': expanded ? '收起车辆明细' : '展开车辆明细', + 'aria-expanded': expanded, + onClick: function (e) { + e.stopPropagation(); + toggleContractRowExpand(record.id); + }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + toggleContractRowExpand(record.id); + } + }, + }); + } + function renderProjectInfoCell(_, record) { var status = record.contractStatus || '-'; - return React.createElement('div', { className: 'lc-project-info-cell' }, - React.createElement( - 'button', - { - type: 'button', - className: 'lc-project-info-btn', - onClick: function() { openContractView(record); }, - title: '查看合同详情' - }, + return React.createElement('div', { className: 'lc-project-info-merged-cell' }, + renderContractRowExpandIcon(record), + React.createElement('div', { className: 'lc-project-info-cell' }, React.createElement( - 'div', - { className: 'vm-stack' }, - React.createElement('span', { className: 'primary' }, record.projectName || '-'), - React.createElement('span', { className: 'code' }, record.contractCode || '-'), - React.createElement('span', { className: 'sub' }, record.customerName || '-') - ) - ), - React.createElement('div', { className: 'lc-project-info-cell__status' }, - React.createElement(StatusTag, { - label: status, - tone: contractStatusTone(status), - title: status, - }), + 'button', + { + type: 'button', + className: 'lc-project-info-btn', + onClick: function() { openContractView(record); }, + title: '查看合同详情' + }, + React.createElement( + 'div', + { className: 'vm-stack' }, + React.createElement('span', { className: 'primary' }, record.projectName || '-'), + React.createElement('span', { className: 'code' }, record.contractCode || '-'), + React.createElement('span', { className: 'sub' }, record.customerName || '-') + ) + ), + React.createElement('div', { className: 'lc-project-info-cell__status' }, + React.createElement(StatusTag, { + label: status, + tone: contractStatusTone(status), + title: status, + }), + ), ), ); } @@ -1576,6 +1698,12 @@ const Component = function(props) { _deliveryChangeDraftTbd[1](false); } + function closeVehicleDeliveryChangePopover() { + _vehicleDeliveryChangeTarget[1](null); + _vehicleDeliveryChangeDraftDate[1](null); + _vehicleDeliveryChangeDraftTbd[1](false); + } + function confirmDeliveryDateChange(record) { var draftTbd = _deliveryChangeDraftTbd[0]; var newDate = draftTbd ? DELIVERY_DATE_TBD_LABEL : _deliveryChangeDraftDate[0]; @@ -1623,6 +1751,48 @@ const Component = function(props) { closeDeliveryChangePopover(); } + function confirmVehicleDeliveryDateChange(record, vehicleIndex) { + var vehicles = record.vehicles || []; + var vehicle = vehicles[vehicleIndex]; + if (!vehicle || isContractVehicleDelivered(vehicle)) { + closeVehicleDeliveryChangePopover(); + return; + } + var draftTbd = _vehicleDeliveryChangeDraftTbd[0]; + var newDate = draftTbd ? DELIVERY_DATE_TBD_LABEL : _vehicleDeliveryChangeDraftDate[0]; + var oldTbd = resolveVehicleDeliveryDateTbd(vehicle, record); + var oldDate = oldTbd + ? DELIVERY_DATE_TBD_LABEL + : (resolveVehiclePlannedDeliveryDate(vehicle, record) || null); + if (!draftTbd && !newDate) { + message.warning('请选择交车日期,或标记为暂未确定'); + return; + } + if (draftTbd === oldTbd && (draftTbd || newDate === oldDate)) { + closeVehicleDeliveryChangePopover(); + return; + } + _recordOverrides[1](function (prev) { + var prevRec = prev[record.id] || {}; + var baseVehicles = prevRec.vehicles || record.vehicles || []; + var updatedVehicles = baseVehicles.map(function (item, index) { + if (index !== vehicleIndex || isContractVehicleDelivered(item)) return item; + return Object.assign({}, item, { + plannedDeliveryDate: draftTbd ? null : newDate, + deliveryDateTbd: draftTbd, + deliveryTaskSuspended: draftTbd, + }); + }); + return Object.assign({}, prev, { + [record.id]: Object.assign({}, prevRec, { + vehicles: updatedVehicles, + }), + }); + }); + message.success(draftTbd ? '本车交车时间已标记为暂未确定(原型)' : '本车交车安排已更新(原型)'); + closeVehicleDeliveryChangePopover(); + } + function renderDeliveryChangePanel(record) { var dayjs = window.dayjs; var logs = record.deliveryDateChangeLogs || []; @@ -1726,6 +1896,70 @@ const Component = function(props) { ); } + function renderVehicleDeliveryChangePanel(record, vehicle, vehicleIndex) { + var dayjs = window.dayjs; + var draftDate = _vehicleDeliveryChangeDraftDate[0]; + var draftTbd = _vehicleDeliveryChangeDraftTbd[0]; + var draftValue = draftDate && dayjs && !draftTbd ? dayjs(draftDate) : null; + var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌'; + return React.createElement('div', { + className: 'lc-delivery-change__panel', + role: 'dialog', + 'aria-label': '修改本车交车时间', + }, + React.createElement('div', { className: 'lc-delivery-change__header' }, + React.createElement('div', { className: 'lc-delivery-change__title' }, '修改本车交车时间'), + React.createElement('div', { className: 'lc-delivery-change__subtitle tabular-nums' }, plate), + ), + React.createElement('div', { className: 'lc-delivery-change__form' }, + React.createElement('label', { className: 'lc-delivery-change__field' }, + React.createElement('span', { className: 'lc-delivery-change__label' }, '计划交车日期'), + React.createElement(DatePicker, { + style: { width: '100%' }, + value: draftValue, + disabled: draftTbd, + placeholder: draftTbd ? '暂未确定' : '请选择交车日期', + 'aria-label': '计划交车日期', + onChange: function (_date, dateString) { + _vehicleDeliveryChangeDraftDate[1](dateString || null); + }, + }), + ), + React.createElement('label', { className: 'lc-delivery-change__tbd' }, + React.createElement(Checkbox, { + checked: draftTbd, + onChange: function (event) { + var checked = event.target.checked; + _vehicleDeliveryChangeDraftTbd[1](checked); + if (checked) { + _vehicleDeliveryChangeDraftDate[1](null); + } else { + var vehicleTbd = resolveVehicleDeliveryDateTbd(vehicle, record); + _vehicleDeliveryChangeDraftDate[1](vehicleTbd + ? null + : (resolveVehiclePlannedDeliveryDate(vehicle, record) || null)); + } + }, + }, '暂未确定'), + ), + React.createElement('p', { className: 'lc-delivery-change__hint' }, + '仅修改当前车辆;在项目信息中调整交车安排后,仍会同步至所有未交车车辆。', + ), + React.createElement('div', { className: 'lc-delivery-change__actions' }, + React.createElement(Button, { + size: 'small', + onClick: closeVehicleDeliveryChangePopover, + }, '取消'), + React.createElement(Button, { + size: 'small', + type: 'primary', + onClick: function () { confirmVehicleDeliveryDateChange(record, vehicleIndex); }, + }, '确定'), + ), + ), + ); + } + function renderDeliveryEditIcon() { return React.createElement('svg', { xmlns: 'http://www.w3.org/2000/svg', @@ -1871,6 +2105,7 @@ const Component = function(props) { closeDeliveryChangePopover(); return; } + closeVehicleDeliveryChangePopover(); _vehiclePopoverRecord[1](null); _deliveredPopoverRecord[1](null); closeDelegatePopover(); @@ -1983,6 +2218,20 @@ const Component = function(props) { return renderStackCell(record.updater, record.updateTime, 'code'); } + function renderOperationMoreIcon() { + return React.createElement('svg', { + viewBox: '0 0 16 16', + width: 16, + height: 16, + fill: 'currentColor', + 'aria-hidden': true, + }, + React.createElement('circle', { cx: 8, cy: 3, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 8, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 13, r: 1.5 }), + ); + } + function getOperationButtons(record) { var menuItems = getMoreMenuItems(record); var moreDropdown = menuItems.length > 0 @@ -1990,21 +2239,29 @@ const Component = function(props) { Dropdown, { menu: { items: menuItems }, - trigger: ['click'] + trigger: ['click'], + placement: 'bottomRight', }, - React.createElement(Button, { type: 'link', size: 'small' }, '更多') + React.createElement(Tooltip, { title: '更多' }, + React.createElement('span', { + className: 'lc-action-more-btn', + role: 'button', + tabIndex: 0, + 'aria-label': '更多操作', + onClick: function (e) { e.stopPropagation(); }, + }, renderOperationMoreIcon()), + ), ) : null; return React.createElement( - Space, - { size: 4, wrap: true }, + 'div', + { className: 'lc-row-actions' }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openContractView(record); } }, '查看'), - moreDropdown + moreDropdown, ); } var columns = [ - Table.EXPAND_COLUMN, { title: '项目信息', key: 'projectInfo', @@ -2132,9 +2389,6 @@ const Component = function(props) { ]; var listTableScrollX = columns.reduce(function (sum, col) { - if (col === Table.EXPAND_COLUMN) { - return sum + LC_LIST_TABLE_EXPAND_WIDTH; - } return sum + (Number(col.width) || 0); }, 0); @@ -2358,6 +2612,8 @@ const Component = function(props) { React.createElement(LeaseContractFleetSummary, { records: fleetSourceList, Modal: Modal, + renderVehicleNativeTableHeader: renderContractVehicleNativeTableHeader, + renderVehicleNativeTableRow: renderContractVehicleNativeTableRow, onViewContract: function (record) { message.info('查看合同(原型):' + (record.contractCode || record.projectName || '')); }, @@ -2365,11 +2621,6 @@ const Component = function(props) { React.createElement('section', { className: 'vm-table-section' }, React.createElement('div', { className: 'vm-table-toolbar', 'data-annotation-id': 'lc-list-toolbar' }, React.createElement('div', { className: 'vm-table-actions' }, - React.createElement('button', { - type: 'button', - className: 'vm-btn vm-btn-ghost', - onClick: function() { message.info('租赁费用模板(原型)'); } - }, '租赁费用模板'), React.createElement('button', { type: 'button', className: 'vm-btn vm-btn-ghost', @@ -2412,8 +2663,10 @@ const Component = function(props) { size: 'small', pagination: false, expandable: { - fixed: 'left', - columnWidth: LC_LIST_TABLE_EXPAND_WIDTH, + showExpandColumn: false, + expandIconColumnIndex: -1, + expandedRowKeys: _expandedRowKeys[0], + onExpandedRowsChange: function (keys) { _expandedRowKeys[1](keys); }, expandedRowRender: renderExpandedVehicleTable, rowExpandable: function (record) { return (record.vehicles || []).length > 0; diff --git a/src/prototypes/lease-contract-management/lease-contract-edit-bridge.js b/src/prototypes/lease-contract-management/lease-contract-edit-bridge.js new file mode 100644 index 0000000..4b26c32 --- /dev/null +++ b/src/prototypes/lease-contract-management/lease-contract-edit-bridge.js @@ -0,0 +1,134 @@ +/** + * 列表合同记录 → 新增/编辑页表单初始态(原型回填) + */ + +import { DEFAULT_MILEAGE_STANDARD, DEFAULT_FEE_INFO } from '../contract-template-management/contract-template-vars.js'; +import { + createDefaultLeaseOrderState, + createEmptyLeaseOrderRow, + createEmptyDelegateRow, + PLATE_ACTUAL_DELIVERY, +} from './lease-order-vars.js'; +import { + inferContractTemplateId, + isDeliveryDateTbd, + SIGNING_COMPANY_LESSOR_IDS, +} from './lease-contract-list-data.js'; + +var LEASE_CUSTOMER_IDS_BY_NAME = { + '嘉兴某某物流有限公司': '1', + '上海某某运输公司': '2', + '杭州某某租赁有限公司': '3', +}; + +function extractContractCodeInput(code) { + if (!code) return ''; + return String(code).replace(/^HT-ZL-/, '').replace(/^LNZLHT-?/i, ''); +} + +function mapVehicleToLeaseOrderRow(vehicle, index) { + var row = createEmptyLeaseOrderRow(); + row.id = 'lo-bridge-' + index; + if (vehicle.brand && vehicle.model) { + row.brandModels = [[vehicle.brand, vehicle.model]]; + } + var plate = vehicle.plateNo; + row.plateNos = plate && plate !== '-' ? [plate] : [PLATE_ACTUAL_DELIVERY]; + row.rent = vehicle.rent != null ? vehicle.rent : 8800; + row.serviceFee = vehicle.serviceFee != null ? vehicle.serviceFee : 1200; + row.deposit = vehicle.deposit != null ? vehicle.deposit : 20000; + row.leasePeriodMonths = vehicle.leasePeriodMonths != null ? vehicle.leasePeriodMonths : 12; + return row; +} + +function mapAuthorizedDelegatesToPoaRows(delegates) { + if (!delegates || !delegates.length) { + return [createEmptyDelegateRow()]; + } + return delegates.map(function (person, index) { + return { + id: 'del-bridge-' + index, + name: person.name || '', + contact: person.contact || person.phone || '', + idNumber: person.idNumber || person.idCard || '', + }; + }); +} + +function buildLeaseOrderFromRecord(record) { + var defaultOrder = createDefaultLeaseOrderState(); + var vehicles = record.vehicles || []; + var rows = vehicles.length + ? vehicles.map(mapVehicleToLeaseOrderRow) + : defaultOrder.rows.slice(); + var deliveryDate = record.deliveryDate; + if (deliveryDate && isDeliveryDateTbd(deliveryDate)) { + deliveryDate = null; + } + return { + insuredVehicleCount: record.insuredVehicleCount != null ? record.insuredVehicleCount : rows.length || 1, + thirdPartyLiabilityMillion: record.thirdPartyLiabilityMillion != null + ? record.thirdPartyLiabilityMillion + : defaultOrder.thirdPartyLiabilityMillion, + deliveryRegion: (record.deliveryRegion || defaultOrder.deliveryRegion).slice(), + deliveryDate: deliveryDate || null, + rows: rows, + }; +} + +function buildMileageFromRecord(record) { + return Object.assign({}, DEFAULT_MILEAGE_STANDARD, { + hasRequirement: Boolean(record.hasMinimumMileage), + period: record.mileagePeriod || DEFAULT_MILEAGE_STANDARD.period, + targetKm: record.mileageTargetKm != null + ? record.mileageTargetKm + : DEFAULT_MILEAGE_STANDARD.targetKm, + reductionYuan: record.mileageReductionYuan != null + ? record.mileageReductionYuan + : DEFAULT_MILEAGE_STANDARD.reductionYuan, + validUntil: record.mileageValidUntil || DEFAULT_MILEAGE_STANDARD.validUntil, + }); +} + +function buildFeeInfoFromRecord(record) { + var isPrepay = record.hydrogenPaymentMethod === 'prepay'; + return Object.assign({}, DEFAULT_FEE_INFO, { + paymentPeriod: record.paymentPeriod != null ? record.paymentPeriod : DEFAULT_FEE_INFO.paymentPeriod, + hydrogenPaymentMethod: record.hydrogenPaymentMethod || DEFAULT_FEE_INFO.hydrogenPaymentMethod, + prepayAmount: record.prepayAmount != null ? record.prepayAmount : (isPrepay ? 50000 : null), + payAheadWorkdays: record.payAheadWorkdays != null ? record.payAheadWorkdays : DEFAULT_FEE_INFO.payAheadWorkdays, + returnHydrogenDiffUnitPrice: record.returnHydrogenDiffUnitPrice != null + ? record.returnHydrogenDiffUnitPrice + : 8.5, + }); +} + +/** 列表行 → 编辑页表单初始值;授权委托书未维护时 delegates 为空行 */ +export function buildLeaseContractEditFormState(record) { + if (!record) return null; + var lessorId = record.lessorId || SIGNING_COMPANY_LESSOR_IDS[record.signingCompany] || ''; + var customerId = record.customerId || LEASE_CUSTOMER_IDS_BY_NAME[record.customerName] || ''; + var poaMaintained = record.poaUploaded === true + && record.authorizedDelegates + && record.authorizedDelegates.length > 0; + return { + contractTemplateId: record.contractTemplateId || inferContractTemplateId(record) || '', + lessorId: lessorId, + customerId: customerId, + contractCode: extractContractCodeInput(record.contractCode), + businessDept: record.businessDept || '', + businessOwner: record.businessOwner || '', + mileage: buildMileageFromRecord(record), + feeInfo: buildFeeInfoFromRecord(record), + leaseOrder: buildLeaseOrderFromRecord(record), + powerOfAttorney: { + delegates: poaMaintained + ? mapAuthorizedDelegatesToPoaRows(record.authorizedDelegates) + : [createEmptyDelegateRow()], + }, + contractRemark: record.poaRemark || '', + sealTypes: Array.isArray(record.sealTypes) && record.sealTypes.length + ? record.sealTypes.slice() + : ['contract'], + }; +} diff --git a/src/prototypes/lease-contract-management/lease-contract-list-data.js b/src/prototypes/lease-contract-management/lease-contract-list-data.js index b04605c..30088e2 100644 --- a/src/prototypes/lease-contract-management/lease-contract-list-data.js +++ b/src/prototypes/lease-contract-management/lease-contract-list-data.js @@ -375,25 +375,32 @@ var SAMPLE_AUTHORIZED_DELEGATES = [ { name: '周磊', contact: '13900139005', idNumber: '330102199005055678' }, ]; +export function buildAuthorizedDelegatesSample(count) { + var total = Math.max(0, Number(count) || 0); + var list = []; + for (var i = 0; i < total; i++) { + list.push(Object.assign({}, SAMPLE_AUTHORIZED_DELEGATES[i % SAMPLE_AUTHORIZED_DELEGATES.length])); + } + return list; +} + export function getAuthorizedDelegates(record) { if (record.authorizedDelegates && record.authorizedDelegates.length) { return record.authorizedDelegates; } - var count = record.delegateCount != null ? record.delegateCount : 0; - if (count <= 0) return []; - var list = []; - for (var i = 0; i < count; i++) { - list.push(SAMPLE_AUTHORIZED_DELEGATES[i % SAMPLE_AUTHORIZED_DELEGATES.length]); + if (record.poaUploaded === true && (record.delegateCount || 0) > 0) { + return buildAuthorizedDelegatesSample(record.delegateCount); } - return list; + return []; } /** 列表「受托人」列是否需提示补充授权委托书与受托人信息 */ export function needsContractDelegateSupplement(record) { if (!record) return false; if (record.delegateSupplementRequired === false) return false; - if (record.poaUploaded === true && (record.delegateCount || 0) > 0) return false; - if ((record.delegateCount || 0) > 0 && getAuthorizedDelegates(record).length > 0) return false; + if (record.poaUploaded === true && (record.delegateCount || 0) > 0 && getAuthorizedDelegates(record).length > 0) { + return false; + } return true; } @@ -507,6 +514,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ mileagePeriod: 'month', mileageTargetKm: 6000, delegateCount: 0, + poaUploaded: false, contractEndDate: '2026-02-16', contactName: '张三', contactPhone: '13800138001', @@ -579,6 +587,12 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ mileagePeriod: 'quarter', mileageTargetKm: 18000, delegateCount: 2, + poaUploaded: true, + authorizedDelegates: [ + { name: '王明', contact: '13900139001', idNumber: '330102199001011234' }, + { name: '李华', contact: '13900139002', idNumber: '330102199002022345' }, + ], + poaRemark: '授权委托书已上传,两名受托人信息与现场核验一致。', contractEndDate: '2026-06-30', contactName: '王五', contactPhone: '13800138003', @@ -619,6 +633,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ mileagePeriod: 'month', mileageTargetKm: 8000, delegateCount: 1, + poaUploaded: true, + authorizedDelegates: [ + { name: '孙丽华', contact: '13900139011', idNumber: '330102198803033210' }, + ], + poaRemark: '授权委托书已归档,受托人孙丽华为客户指定接车人。', contractEndDate: '2026-03-01', contactName: '赵六', contactPhone: '13900139001', @@ -703,6 +722,13 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ mileagePeriod: 'year', mileageTargetKm: 120000, delegateCount: 3, + poaUploaded: true, + authorizedDelegates: [ + { name: '王明', contact: '13900139001', idNumber: '330102199001011234' }, + { name: '李华', contact: '13900139002', idNumber: '330102199002022345' }, + { name: '赵强', contact: '13900139003', idNumber: '330102199003033456' }, + ], + poaRemark: '三车批量合同授权委托书已上传,受托人名单与盖章扫描件一致。', contractEndDate: '2026-12-31', contactName: '周八', contactPhone: '13900139003', @@ -745,6 +771,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ mileagePeriod: 'month', mileageTargetKm: null, delegateCount: 1, + poaUploaded: true, + authorizedDelegates: [ + { name: '吴九', contact: '13900139004', idNumber: '320211199005051122' }, + ], + poaRemark: '试用合同授权委托书已维护,受托人吴九负责无锡区域接车。', contractEndDate: '2025-07-10', contactName: '吴九', contactPhone: '13900139004', @@ -869,6 +900,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ mileagePeriod: 'month', mileageTargetKm: null, delegateCount: 1, + poaUploaded: true, + authorizedDelegates: [ + { name: '张三', contact: '13800138001', idNumber: '330102198501011234' }, + ], + poaRemark: '历史合同授权委托书已补录归档。', contractEndDate: '2025-01-15', contactName: '张三', contactPhone: '13800138001', @@ -944,6 +980,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ mileageTargetKm: 8000, delegateCount: 2, poaUploaded: true, + authorizedDelegates: [ + { name: '陈静', contact: '13900139004', idNumber: '330102199004044567' }, + { name: '周磊', contact: '13900139005', idNumber: '330102199005055678' }, + ], + poaRemark: '在租车队合同授权委托书已维护,两名受托人负责七车接还对接。', contractEndDate: '2027-01-31', contactName: '张三', contactPhone: '13800138001', @@ -1018,6 +1059,36 @@ export function formatContractDeliveryDateLabel(record) { return record.deliveryDate || '-'; } +export function resolveVehicleDeliveryDateTbd(vehicle, record) { + if (!vehicle || isContractVehicleDelivered(vehicle)) return false; + if (vehicle.deliveryDateTbd != null) return Boolean(vehicle.deliveryDateTbd); + if (!record) return false; + return Boolean(record.deliveryDateTbd || isDeliveryDateTbd(record.deliveryDate)); +} + +export function resolveVehiclePlannedDeliveryDate(vehicle, record) { + if (!vehicle) return null; + if (isContractVehicleDelivered(vehicle)) { + return vehicle.deliveryTime || vehicle.actualDelivery || null; + } + if (vehicle.plannedDeliveryDate != null && String(vehicle.plannedDeliveryDate).trim()) { + return vehicle.plannedDeliveryDate; + } + return record ? record.deliveryDate || null : null; +} + +export function formatVehicleDeliveryPlanDateLabel(vehicle, record) { + if (!vehicle) return '-'; + if (isContractVehicleDelivered(vehicle)) { + var deliveredAt = formatContractHandoverDateTimeMinute(vehicle.deliveryTime || vehicle.actualDelivery); + return deliveredAt && deliveredAt !== '-' ? deliveredAt : '-'; + } + if (resolveVehicleDeliveryDateTbd(vehicle, record)) return DELIVERY_DATE_TBD_LABEL; + var date = resolveVehiclePlannedDeliveryDate(vehicle, record); + if (!date || isDeliveryDateTbd(date)) return DELIVERY_DATE_TBD_LABEL; + return date; +} + function addMonthsToDate(date, months) { var next = new Date(date.getTime()); next.setMonth(next.getMonth() + months); @@ -1271,6 +1342,9 @@ export function enrichLeaseContractVehicle(vehicle, index, recordIndex, record) plannedDeliveryDate: vehicle.plannedDeliveryDate != null ? vehicle.plannedDeliveryDate : (delivered ? null : (record && record.deliveryDate) || null), + deliveryDateTbd: vehicle.deliveryDateTbd != null + ? vehicle.deliveryDateTbd + : (delivered ? false : resolveVehicleDeliveryDateTbd(vehicle, record)), dailyAvgMileage7d: dailyAvgMileage7d, }); if (delivered && hasMinMileage && record) { diff --git a/src/prototypes/lease-contract-management/styles/lease-contract.css b/src/prototypes/lease-contract-management/styles/lease-contract.css index b340e43..8a3e3eb 100644 --- a/src/prototypes/lease-contract-management/styles/lease-contract.css +++ b/src/prototypes/lease-contract-management/styles/lease-contract.css @@ -40,6 +40,34 @@ margin-bottom: 0; } +.vm-page.lc-page .lc-row-actions { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.vm-page.lc-page .lc-action-more-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + color: var(--ln-muted, #64748b); + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.vm-page.lc-page .lc-action-more-btn:hover { + background: #f1f5f9; + color: var(--ln-body, #334155); +} + +.vm-page.lc-page .lc-action-more-btn:focus-visible { + outline: 2px solid var(--ln-primary, #32a06e); + outline-offset: 2px; +} + /* 列表区:Ant Table 嵌入 vm-table-card */ .vm-page.lc-page .vm-table-card .vm-list-table.ant-table-wrapper, .vm-page.lc-page .vm-table-card .vm-list-table .ant-table-wrapper { @@ -74,7 +102,7 @@ max-width: 100%; } -/* 左侧冻结:展开列 + 项目信息(背景与层级,不覆盖 antd 的 sticky 偏移) */ +/* 左侧冻结:项目信息列(含展开按钮) */ .vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-start, .vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-left { background: var(--ln-surface-card); @@ -97,15 +125,6 @@ background: var(--ln-canvas-soft) !important; } -.vm-page.lc-page .lc-list-table--nested .ant-table-row-expand-icon-cell.ant-table-cell-fix-start { - z-index: 4; -} - -.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-row-expand-icon-cell { - z-index: 4; - background: var(--ln-canvas-soft) !important; -} - .vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-start, .vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-left { z-index: 3; @@ -121,6 +140,73 @@ box-shadow: inset 10px 0 8px -8px rgba(15, 23, 42, 0.12); } +/* 右侧冻结:操作列 + 滚动条占位缝(避免横向滚动时右侧漏出其他列) */ +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-end:not(.ant-table-cell-scrollbar), +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) { + background: var(--ln-surface-card) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar), +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) { + z-index: 4; + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar), +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td.ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) { + z-index: 3; + background: var(--ln-surface-card) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar), +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td.ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) { + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-end-shadow-show:not(.ant-table-cell-scrollbar)::after, +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-right-last::after { + box-shadow: inset -10px 0 8px -8px rgba(15, 23, 42, 0.12); +} + +/* 盖住纵向滚动条占位缝,防止表头/表体滚动列从操作列右侧露出 */ +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar)::before, +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr > td.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar)::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + right: -17px; + width: 17px; + background: inherit; + pointer-events: none; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-scrollbar { + width: 0 !important; + min-width: 0 !important; + max-width: 0 !important; + padding: 0 !important; + border: none !important; + visibility: hidden; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th:not(.ant-table-cell-fix-start):not(.ant-table-cell-fix-end), +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td:not(.ant-table-cell-fix-start):not(.ant-table-cell-fix-end) { + overflow: hidden; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-container { + overflow: hidden; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td { + overflow: hidden; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row .lc-contract-vehicle-table-wrap { + max-width: 100%; +} + @media (max-width: 1280px) { .vm-page.lc-page .vm-kpi-row { grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -1129,23 +1215,31 @@ } } -/* 列表嵌套:展开列宽度与冻结态 */ -.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-row-expand-icon-cell, -.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell, -.vm-page.lc-page .lc-list-table--nested col.ant-table-expand-icon-col { - width: 48px !important; - min-width: 48px !important; - max-width: 48px !important; - padding-left: 12px !important; - padding-right: 8px !important; - box-sizing: border-box; +/* 列表嵌套:项目信息列内嵌展开按钮(单列合并,左冻结) */ +.vm-page.lc-page .lc-list-table--nested .lc-project-info-merged-cell { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-width: 0; } -.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover > td.ant-table-row-expand-icon-cell.ant-table-cell-fix-start { +.vm-page.lc-page .lc-list-table--nested .lc-project-info-merged-cell .ant-table-row-expand-icon { + flex-shrink: 0; + margin: 0; + cursor: pointer; +} + +.vm-page.lc-page .lc-list-table--nested .lc-project-info-merged-cell .lc-project-info-cell { + flex: 1; + min-width: 0; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover > td.lc-project-info-col.ant-table-cell-fix-start { background: var(--ln-canvas-soft) !important; } -.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td.ant-table-row-expand-icon-cell.ant-table-cell-fix-start { +.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td.lc-project-info-col.ant-table-cell-fix-start { background: var(--ln-canvas-soft, #f8fafc); z-index: 4; } @@ -1217,8 +1311,12 @@ width: 9%; } +.vm-page.lc-page .lc-contract-vehicle-native-table__delivery-plan { + width: 11%; +} + .vm-page.lc-page .lc-contract-vehicle-native-table__mileage-req { - width: 18%; + width: 16%; } .vm-page.lc-page .lc-contract-vehicle-native-table__mileage-completion { @@ -1235,6 +1333,7 @@ .vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-pickup-payment, .vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-return-settlement, .vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-handover-situation, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-delivery-cell, .vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-mileage-req-row, .vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-mileage-completion, .vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-vehicle-action-empty, @@ -1270,6 +1369,32 @@ text-align: left; } +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell { + min-width: 0; + gap: 2px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell__region { + font-size: 11px; + line-height: 1.3; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell__date { + font-size: 10px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell__edit-btn { + width: 22px; + height: 22px; +} + +.vm-page.lc-page .lc-delivery-change__subtitle { + margin-top: 2px; + font-size: 12px; + color: var(--ln-muted, #64748b); + line-height: 1.3; +} + /* 子表里程要求列:左右两栏(里程要求 | 当前里程) */ .vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual { flex-direction: row; @@ -1321,18 +1446,14 @@ white-space: normal; } -.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress { - gap: 6px; +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__track { + min-width: 72px; + height: 8px; } -.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress__track { - min-width: 40px; - height: 6px; -} - -.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress__label { - font-size: 11px; - min-width: 30px; +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__label { + font-size: 12px; + min-width: 36px; } .vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-handover-situation__line { @@ -1623,26 +1744,34 @@ flex-shrink: 0; } -.vm-page.lc-page .lc-contract-mileage-completion .lc-vehicle-mileage-progress { - width: 100%; - min-width: 0; -} - -.vm-page.lc-page .lc-contract-mileage-completion__progress-row { +.vm-page.lc-page .lc-contract-mileage-completion__bar-row { display: flex; align-items: center; gap: 8px; min-width: 0; + width: 100%; } -.vm-page.lc-page .lc-contract-mileage-completion__progress-main { - flex: 1 1 auto; +.vm-page.lc-page .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__track { + flex: 1; + min-width: 72px; + height: 8px; +} + +.vm-page.lc-page .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__label { + flex-shrink: 0; + font-size: 12px; + min-width: 36px; + text-align: right; +} + +.vm-page.lc-page .lc-contract-mileage-completion__range-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; min-width: 0; - max-width: 68%; -} - -.vm-page.lc-page .lc-contract-mileage-completion__progress-main .lc-vehicle-mileage-progress__track { - min-width: 32px; + width: 100%; } .vm-page.lc-page .lc-contract-mileage-completion__range-side { @@ -1655,13 +1784,13 @@ .vm-page.lc-page .lc-contract-mileage-completion__range-side--start { align-items: flex-start; text-align: left; - margin-top: 2px; } .vm-page.lc-page .lc-contract-mileage-completion__range-side--end { flex: 0 0 auto; align-items: flex-end; text-align: right; + margin-left: auto; } .vm-page.lc-page .lc-contract-mileage-completion__range-date { @@ -1677,18 +1806,6 @@ line-height: 1.25; } -.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__progress-main { - max-width: 72%; -} - -.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__progress-main .lc-vehicle-mileage-progress__track { - min-width: 36px; -} - -.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-side--end { - max-width: 28%; -} - .vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-date { font-size: 10px; } @@ -2204,169 +2321,19 @@ overflow: auto; } -.lc-fleet-detail-table-wrap { - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.lc-fleet-detail-table { - width: 100%; - min-width: 880px; - border-collapse: collapse; - table-layout: fixed; - font-size: 0.8125rem; -} - -.lc-fleet-detail-table thead th { - position: sticky; - top: 0; - z-index: 1; - padding: 10px 12px; - background: #f6f7f7; - color: #71717a; - font-size: 0.75rem; - font-weight: 600; - text-align: left; - white-space: nowrap; - border-bottom: 1px solid #e5e7eb; -} - -.lc-fleet-detail-table tbody td { - padding: 11px 12px; - vertical-align: middle; - border-bottom: 1px solid #f4f4f5; - color: #52525b; -} - -.lc-fleet-detail-table tbody tr:hover td { - background: rgba(50, 160, 110, 0.04); -} - -.lc-fleet-detail-table tbody tr:last-child td { - border-bottom: none; -} - -.lc-fleet-detail-table__col-plate { width: 88px; } -.lc-fleet-detail-table__col-vin { width: 148px; } -.lc-fleet-detail-table__col-type { width: 120px; } -.lc-fleet-detail-table__col-announce { width: 168px; } -.lc-fleet-detail-table__col-city { width: 96px; } -.lc-fleet-detail-table__col-mileage { width: 96px; } -.lc-fleet-detail-table__col-project { width: 180px; } -.lc-fleet-detail-table__col-action { width: 88px; } - -.lc-fleet-detail-table__plate { - display: block; - font-size: 0.875rem; - font-weight: 700; - color: #32a06e; - line-height: 1.3; - white-space: nowrap; -} - -.lc-fleet-detail-table__vin { - display: block; - font-size: 0.75rem; - line-height: 1.35; - color: #52525b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.lc-fleet-detail-table__city { - display: block; - white-space: nowrap; -} - -.lc-fleet-detail-table__type-pill { - display: inline-flex; - align-items: center; - max-width: 100%; - padding: 2px 8px; - border-radius: 999px; - background: #f4f4f5; - color: #71717a; - font-size: 0.6875rem; - line-height: 1.4; - white-space: normal; - word-break: break-word; -} - -.lc-fleet-detail-table__announce { - display: block; - color: #52525b; - line-height: 1.4; - font-size: 0.75rem; - word-break: break-all; -} - -.lc-fleet-detail-table__mileage { - display: inline-block; - font-weight: 600; - color: #18181b; - white-space: nowrap; -} - -.lc-fleet-detail-table__project { - display: flex; - flex-direction: column; - gap: 3px; - min-width: 0; -} - -.lc-fleet-detail-table__project-name { - font-weight: 600; - color: #18181b; - font-size: 0.8125rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.lc-fleet-detail-table__contract-link { +.lc-fleet-detail-modal .lc-fleet-detail-modal__table-host.vm-page.lc-page { padding: 0; - border: none; - background: none; - color: #32a06e; - font-size: 0.75rem; - font-weight: 600; - text-align: left; - cursor: pointer; - white-space: nowrap; + min-height: 0; + background: transparent; } -.lc-fleet-detail-table__contract-link:hover { - color: #3fb87c; - text-decoration: underline; - text-underline-offset: 2px; +.lc-fleet-detail-modal .lc-fleet-detail-modal__table-host .lc-contract-vehicle-table-wrap { + width: 100%; + max-width: 100%; } -.lc-fleet-detail-table__customer { - font-size: 0.75rem; - color: #71717a; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.lc-fleet-detail-table__action { - min-height: 32px; - padding: 0 10px; - border: 1px solid rgba(50, 160, 110, 0.24); - border-radius: 8px; - background: rgba(50, 160, 110, 0.1); - color: #2b9260; - font-size: 0.75rem; - font-weight: 600; - cursor: pointer; - white-space: nowrap; - transition: background 0.15s ease, border-color 0.15s ease; -} - -.lc-fleet-detail-table__action:hover { - background: rgba(50, 160, 110, 0.16); - border-color: #32a06e; +.lc-fleet-detail-modal .lc-fleet-detail-modal__vehicle-table { + min-width: 880px; } .lc-fleet-detail-modal__empty { @@ -2401,10 +2368,6 @@ } @media (prefers-reduced-motion: reduce) { - .lc-fleet-detail-table__action { - transition: none; - } - .vm-page.lc-page .lc-fleet-summary__card, .vm-page.lc-page .lc-fleet-summary__card-bar-fill, .vm-page.lc-page .lc-fleet-summary__toggle, diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/prototype-review.md b/src/prototypes/oneos-web-h2-station-site/.spec/prototype-review.md new file mode 100644 index 0000000..9b7ea59 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/.spec/prototype-review.md @@ -0,0 +1,35 @@ +# Prototype Review + +- 审查目标:`src/prototypes/oneos-web-h2-station-site` +- 用户资料/参考资料: + - `src/prototypes/oneos-web-h2-station-site/.spec/requirements.md` + - `src/resources/oneos-web-legacy/加氢站管理/站点管理使用说明书.md` + - 页面实现:`src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx` + - 氢费共享 Store:`src/common/h2VehicleLedgerBridge.js` +- 生成时间:2026-07-01(复审) + +## 总体点评 + +独立「站点信息」原型已完成 Review 项修复:KPI 恢复高频/低频/无加氢筛选,补齐标注与 `.spec` 决策,增加角色切换演示,氢费明细统一对接「车辆氢费明细」共享 Store。可作为培训与高保真业务演示使用。 + +## P0-P3 优先级问题(复审状态) + +| 级别 | 问题 | 状态 | +|------|------|------| +| P1 | KPI 与说明书不一致 | **已修复**:KPI 为全部/高频/低频/无加氢 | +| P2 | 缺 annotation / `.spec` | **已修复**:`annotation-source.json`、`.spec/requirements.md` | +| P2 | 角色权限无 UI 演示 | **已修复**:右上角角色切换 | +| P3 | 双入口未文档化 | **已修复**:标注与 requirements 中说明 | +| P3 | 氢费 Mock 未联动车辆氢费明细 | **已修复**:`h2VehicleLedgerBridge.js` 统一数据源 | + +## 验收建议 + +1. 打开「站点信息」,点击 KPI「高频/低频/无加氢」,列表应正确筛选。 +2. 切换「加氢站账号」,编辑站点时系统账号字段应为只读。 +3. 打开「车辆氢费明细」后再看站点加氢下钻/对账,数据应与台账一致(同 Store)。 +4. 侧边栏标注目录可查看入口、KPI、对账联动说明。 + +## 剩余风险(低) + +- 余额预警/欠费站点仍可通过「全站余额提醒设置」与行内余额下钻查看,但未再占用 KPI 主卡位;若产品需 KPI 同时展示余额类指标,需另行对齐口径。 +- 车辆氢费明细未打开时,加氢站模块使用 canonical seed(10 条),与台账页全量 mock 行数可能不同,属原型预期行为。 diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-list.md b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-list.md new file mode 100644 index 0000000..b57e54a --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-list.md @@ -0,0 +1,104 @@ +# 加氢站管理 · 站点信息 — 列表页 PRD + +| 项 | 说明 | +|---|---| +| 模块名称 | 加氢站管理 → 站点信息 | +| 所属系统 | ONE-OS Web 端 | +| 目标用户 | 加氢站运营、财务结算、平台管理员 | +| 核心任务 | 查询站点台账、维护主数据、查看运营指标、发起对账与充值 | +| 交互原型 | `/prototypes/oneos-web-h2-station-site` | +| 设计规范 | 与租赁合同管理一致的 vm 布局(筛选 / KPI / 表格 / 分页) | + +--- + +## 1. 页面结构 + +用户进入「加氢站管理 → 站点信息」后,列表页自上而下为: + +1. **筛选区** — 名称、签约、地区、营业状态 +2. **KPI 分类** — 全部 / 签约·普通 / 高频 / 低频 / 无加氢 +3. **工具栏** — 全站余额提醒、批量导入、发起充值单、新建站点 +4. **站点列表** — 主数据 + 运营指标 + 操作列 +5. **分页栏** — 每页 5/10/20/50 条 + +--- + +## 2. 筛选区 + +与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。 + +| 字段 | 控件 | 说明 | +|---|---|---| +| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 | +| 是否签约 | 下拉 | 签约站点 / 普通站点 | +| 地区(省-市) | 级联 | 省、市两级,可只选省 | +| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 | + +**交互**: + +- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页 +- **重置**:清空筛选条件并刷新列表 + +--- + +## 3. KPI 统计卡片 + +与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。 + +| 卡片 | 口径 | +|---|---| +| 全部加氢站 | 列表全部站点 | +| 签约站点 | 已签约,点击快捷筛选 | +| 普通站点 | 未签约,点击快捷筛选 | +| 高频加氢站 | 该站加氢次数 ≥ 3 | +| 低频加氢站 | 有加氢但次数 < 3 | +| 无加氢站点 | 加氢次数 = 0 | + +加氢次数来自「车辆氢费明细」共享 Store,按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。 + +--- + +## 4. 列表工具栏 + +| 按钮 | 行为 | +|---|---| +| 全站余额提醒设置 | 配置全局预付余额预警阈值 | +| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 | +| 发起充值单 | 为站点发起预付充值流程(原型演示) | +| 新建站点 | 进入同页内嵌新建视图 | + +--- + +## 5. 列表字段说明 + +| 字段 | 说明 | +|---|---| +| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 | +| 合作时间 | 未签约可空 | +| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 | +| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 | +| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 | +| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 | +| 联系方式 | 联系人 + 电话 | +| 操作 | 查看 + 更多菜单 | + +**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。 + +**分页**:默认 10 条,可选 5/10/20/50。 + +--- + +## 6. 数据来源 + +- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store +- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed +- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细 + +--- + +## 7. 双入口说明 + +| 入口 | 路径 | +|---|---| +| 独立原型(本页) | OneOS → 加氢站管理 → 站点信息 | +| 合包 | ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录 | diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md new file mode 100644 index 0000000..194fca8 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md @@ -0,0 +1,97 @@ +# 加氢站管理 · 站点信息 — 对账与结算 PRD + +> 本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。 + +--- + +## 1. 数据来源 + +对账单明细取自 **车辆氢费明细** 中同时满足以下条件的记录: + +1. 对账状态 = **已对账**(台账已完成「完成对账」) +2. **尚未**被本站点历史对账单结算过(未绑定 `statementRecordId`) +3. 加氢时间落在用户选择的 **[账单开始日期, 账单结束日期]** 内 +4. 加氢站名称与当前操作站点一致 + +> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。 + +--- + +## 2. 两阶段交互 + +**阶段一 · 选期生成** + +- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」) +- 默认开始日期建议 = 上次结束日 + 1 天 +- 点击「生成对账单」后进入阶段二,日期锁定不可改 +- 无符合条件记录时提示调整日期 + +**阶段二 · 结算提交** + +统计卡(明细表上方): + +| 指标 | 说明 | +|---|---| +| 加氢次数 | 本账单包含的已对账记录笔数 | +| 加氢总量 | kg 合计 | +| 成本总金额 | 成本总价合计 | + +明细表下方**必填**结算项: + +| 字段 | 规则 | +|---|---| +| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 | +| 收票日期 | 必填 | +| 收票金额 | 必填,默认填入成本总金额 | +| 发票附件 | 必填,支持上传 | + +--- + +## 3. 提交后的系统行为 + +提交成功后需**原子性**完成: + +1. 写入对账历史(供「查看对账记录」查询) +2. 扣减站点预付余额 +3. 标记台账记录已结算(绑定 `statementRecordId`) +4. **回写车辆氢费明细**: + +| 车辆氢费明细字段 | 回写值 | +|---|---| +| 对账日期 | 操作完成时间 | +| 收票日期 | 操作完成日期 | +| 加氢站付款状态 | 已付款 | + +原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge` 模拟跨页回写。 + +--- + +## 4. 查看对账记录 + +入口:列表操作列 → 更多 → **查看对账记录** + +历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。 + +查看明细:账单区间、统计三卡、收票信息、发票附件、成本明细表。 + +--- + +## 5. 校验与异常 + +| 场景 | 处理 | +|---|---| +| 日期未填 / 开始晚于结束 | 阻止生成 | +| 区间内无已对账未结算记录 | 阻止生成,提示调整日期 | +| 收票日期/金额/附件未填 | 阻止提交 | +| 收票金额 ≤ 0 | 阻止提交 | +| 预付余额结算后为负 | 允许提交,标「已欠费」 | + +--- + +## 6. 研发要点 + +1. **对账单幂等**:同一笔已对账记录只能进入一次成功提交的对账单 +2. **上次结束时间**:取该站点已成功提交对账单的最大 `endDate` +3. **余额计算**:`balanceAfter = prepaidBalance - sum(costTotal)` +4. **跨模块同步**:生产对接统一台账;原型使用 Bridge Store +5. **明细字段范围**:对账弹窗不展示客户侧加氢单价/总价 diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md new file mode 100644 index 0000000..fecb251 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md @@ -0,0 +1,163 @@ +# 加氢站管理 · 站点信息 — 产品需求文档(PRD) + +| 项 | 内容 | +|---|---| +| 文档版本 | v1.0 | +| 模块名称 | 加氢站管理 → 站点信息 | +| 所属系统 | ONE-OS Web 端 | +| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 | +| 交互原型 | `/prototypes/oneos-web-h2-station-site` | +| 关联原型 | [车辆氢费明细](/prototypes/vehicle-h2-fee-ledger)、[加氢站合包](/prototypes/oneos-web-h2-station) | +| 文档状态 | 已对齐原型 | + +--- + +## 1. 背景与目标 + +### 1.1 业务背景 + +加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。 + +### 1.2 本期目标 + +- 站点台账的查询、新建、编辑、查看、删除与批量导入 +- 营业状态、成本价格、预付余额等运营配置 +- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写 + +### 1.3 用户角色 + +| 角色 | 典型诉求 | +|---|---| +| 加氢站运营 | 维护站点资料、营业与价格;查看加氢量与余额 | +| 财务/结算 | 按周期生成对账单、登记收票、完成站点付款闭环 | +| 管理员 | 批量导入站点、删除无效站点、配置系统账号 | + +### 1.4 非目标(本期不做) + +- 对账单审批流、ERP 自动过账 +- 发票 OCR 识别与验真 +- 加氢订单、加氢记录(仍在合包 `oneos-web-h2-station`) +- 后端接口联调(原型使用 Mock + 共享 Store) + +--- + +## 2. 信息架构 + +```text +站点信息 +├── 列表页 +│ ├── 筛选区(4 项) +│ ├── KPI 卡片(6 张) +│ ├── 工具栏 +│ └── 站点台账表 + 分页 +├── 新建 / 编辑(同页内嵌视图) +│ ├── 基本信息 +│ ├── 系统账号绑定 +│ └── 供应商 / 付款信息 +└── 弹窗 / 抽屉 + ├── 查看详情 + ├── 营业状态 / 价格配置 + ├── 加氢量下钻 / 预付余额下钻 + ├── 生成对账单(两阶段) + └── 查看对账记录 +``` + +--- + +## 3. 列表与 KPI + +> 详见 `.spec/requirements-prd-list.md` + +- 筛选:名称、签约、地区、营业状态 +- KPI:全部 / 签约·普通 / 高频≥3 / 低频1–2 / 无加氢0 +- 列表字段:名称地址、合作时间、营业、成本价、加氢次数/量、预付余额、联系方式、操作 +- 设计规范:与租赁合同管理一致的 vm 布局与 Inter 字体 + +--- + +## 4. 站点主数据 + +### 4.1 新建站点 + +同页内嵌视图:基本信息、系统账号绑定(可选)、供应商信息(新建或关联)。 + +### 4.2 编辑 / 查看 + +| 模式 | 交互 | +|---|---| +| 编辑 | 整页表单;系统账号仅 admin 可改 | +| 查看 | 弹窗分块展示站点、账号、供应商、付款信息 | + +### 4.3 批量导入 + +下载 CSV 模板 → 上传 → 解析预览 → 确认导入;站点名称不可重复。 + +--- + +## 5. 运营配置 + +### 5.1 营业状态 + +卡片式弹窗:营业状态、营业时间按钮组;非全天须配置时段;展示状态变更记录。 + +### 5.2 价格配置 + +配置成本价格(元/kg)与生效时间;到达生效时间后刷新列表「当前成本价格」;展示价格调整记录。 + +### 5.3 下钻分析 + +- **加氢量下钻**:统计卡 + 明细表 + 导出,字段对齐车辆氢费明细 +- **预付余额下钻**:充值/加氢流水 + 趋势图 + 导出 + +--- + +## 6. 对账与结算(重点) + +> 详见 `.spec/requirements-prd-statement.md` + +与「车辆氢费明细」配合完成站点侧氢费结算闭环: + +1. 拉取已对账且未结算的记录 +2. 两阶段:选期生成 → 填收票提交 +3. 提交后更新预付余额、写对账历史、回写台账(对账日期、收票日期、已付款) + +--- + +## 7. 数据与集成 + +| 能力 | 实现 | +|---|---| +| 氢费明细 | `src/common/h2VehicleLedgerBridge.js` | +| 对账回写 | `H2_STATION_STATEMENT_LEDGER_UPDATES` | +| 页面实现 | `src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx` | +| 独立入口 | `src/prototypes/oneos-web-h2-station-site/index.tsx` | + +--- + +## 8. 验收标准 + +### 8.1 列表页 + +- [ ] 筛选与 KPI 六卡片联动正确 +- [ ] 高频/低频/无加氢口径与车辆氢费明细一致 +- [ ] 加氢量、预付余额下钻数据来自共享 Store +- [ ] 布局与租赁合同 vm 规范一致 + +### 8.2 对账流程 + +- [ ] 仅已对账未结算记录可进入对账单 +- [ ] 两阶段交互与必填校验完整 +- [ ] 提交后回写车辆氢费明细可跨页验收 + +### 8.3 标注与文档 + +- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id` +- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注 + +--- + +## 9. 修订记录 + +| 版本 | 日期 | 说明 | +|---|---|---| +| v1.0 | 2026-07-01 | 独立入口、vm 规范对齐、标注目录与 PRD 接入 | diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/requirements.md b/src/prototypes/oneos-web-h2-station-site/.spec/requirements.md new file mode 100644 index 0000000..237b247 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/.spec/requirements.md @@ -0,0 +1,32 @@ +# 站点信息独立原型 — 范围与决策 + +## 背景 + +「站点信息」从 ONE-OS Web 合包 `oneos-web-h2-station` 拆出,作为 OneOS → 加氢站管理下的独立 Axhub 入口,便于与正式菜单路径对齐。 + +## 范围 + +| 包含 | 不包含 | +|------|--------| +| 站点列表、KPI、新建/编辑/查看/删除 | 加氢订单、加氢记录(仍在合包) | +| 营业状态、价格配置、余额下钻 | 后端接口联调 | +| 两阶段对账单、充值单、批量导入 | | + +## 数据来源 + +- **氢费/加氢明细**:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」原型共享 Store。 +- 先打开「车辆氢费明细」时,台账全量行会同步至 Store;仅打开加氢站模块时使用 canonical seed(10 条演示数据)。 +- 对账单提交 patch 经 `window.H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细。 + +## KPI 口径(已确认) + +全部 / 签约·普通 / **高频(≥3 次)** / **低频(1–2 次)** / **无加氢(0 次)**,与《站点管理使用说明书》及页面内嵌 PRD 一致。 + +## 角色演示 + +页面右上角提供「平台管理员 / 加氢站账号」切换,影响编辑页系统账号字段可编辑性。 + +## 双入口说明 + +- **本原型**:OneOS → 加氢站管理 → 站点信息 +- **合包**:ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录 diff --git a/src/prototypes/oneos-web-h2-station-site/annotation-source.json b/src/prototypes/oneos-web-h2-station-site/annotation-source.json new file mode 100644 index 0000000..34eaa17 --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/annotation-source.json @@ -0,0 +1,348 @@ +{ + "documentVersion": 1, + "format": "axhub-annotation-source", + "data": { + "version": 2, + "prototypeName": "oneos-web-h2-station-site", + "pageId": "site-info", + "updatedAt": 1782912426557, + "nodes": [ + { + "id": "h2-site-filter", + "index": 1, + "title": "筛选条件", + "pageId": "site-info", + "locator": { + "selectors": [ + ".h2-station-page .vm-filter-card", + "[data-annotation-id=\"h2-site-filter\"]" + ], + "fingerprint": "filter|site-info", + "path": [] + }, + "aiPrompt": "筛选字段与查询/重置行为。", + "annotationText": "vm 筛选卡片:4 列栅格,标签在上控件在下。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1782912426507, + "updatedAt": 1782912426507, + "controls": [] + }, + { + "id": "h2-site-kpi", + "index": 2, + "title": "KPI 快捷筛选", + "pageId": "site-info", + "locator": { + "selectors": [ + ".h2-station-page .vm-kpi-row", + "[data-annotation-id=\"h2-site-kpi\"]" + ], + "fingerprint": "kpi|site-info", + "path": [] + }, + "aiPrompt": "六张 KPI 卡片口径与点击筛选。", + "annotationText": "全部 / 签约·普通 / 高频 / 低频 / 无加氢。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1782912426507, + "updatedAt": 1782912426507, + "controls": [] + }, + { + "id": "h2-site-statement", + "index": 3, + "title": "工具栏与对账", + "pageId": "site-info", + "locator": { + "selectors": [ + ".h2-station-page .vm-table-toolbar", + "[data-annotation-id=\"h2-site-toolbar\"]" + ], + "fingerprint": "toolbar|statement", + "path": [] + }, + "aiPrompt": "工具栏操作与对账单氢费联动。", + "annotationText": "全站余额提醒、批量导入、充值单、新建站点;对账回写车辆氢费明细。", + "hasMarkdown": true, + "color": "#d97706", + "images": [], + "createdAt": 1782912426507, + "updatedAt": 1782912426507, + "controls": [] + }, + { + "id": "h2-site-table", + "index": 4, + "title": "站点列表", + "pageId": "site-info", + "locator": { + "selectors": [ + ".h2-station-page .vm-table-card", + "[data-annotation-id=\"h2-site-table\"]" + ], + "fingerprint": "table|site-info", + "path": [] + }, + "aiPrompt": "台账表格列、下钻与行内操作。", + "annotationText": "名称、营业、成本价、加氢量、预付余额、更多菜单。", + "hasMarkdown": true, + "color": "#32a06e", + "images": [], + "createdAt": 1782912426507, + "updatedAt": 1782912426507, + "controls": [] + } + ] + }, + "markdownMap": { + "h2-site-filter": "## 2. 筛选区\n\n与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 |\n| 是否签约 | 下拉 | 签约站点 / 普通站点 |\n| 地区(省-市) | 级联 | 省、市两级,可只选省 |\n| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 |\n\n**交互**:\n\n- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页\n- **重置**:清空筛选条件并刷新列表\n\n---", + "h2-site-kpi": "## 3. KPI 统计卡片\n\n与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部加氢站 | 列表全部站点 |\n| 签约站点 | 已签约,点击快捷筛选 |\n| 普通站点 | 未签约,点击快捷筛选 |\n| 高频加氢站 | 该站加氢次数 ≥ 3 |\n| 低频加氢站 | 有加氢但次数 < 3 |\n| 无加氢站点 | 加氢次数 = 0 |\n\n加氢次数来自「车辆氢费明细」共享 Store,按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。\n\n---", + "h2-site-statement": "## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 全站余额提醒设置 | 配置全局预付余额预警阈值 |\n| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 |\n| 发起充值单 | 为站点发起预付充值流程(原型演示) |\n| 新建站点 | 进入同页内嵌新建视图 |\n\n---\n\n---\n\n## 6. 数据来源\n\n- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store\n- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed\n- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细\n\n---", + "h2-site-table": "## 5. 列表字段说明\n\n| 字段 | 说明 |\n|---|---|\n| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |\n| 合作时间 | 未签约可空 |\n| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |\n| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 |\n| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |\n| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |\n| 联系方式 | 联系人 + 电话 |\n| 操作 | 查看 + 更多菜单 |\n\n**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。\n\n**分页**:默认 10 条,可选 5/10/20/50。\n\n---" + }, + "assetMap": {}, + "directory": { + "nodes": [ + { + "type": "folder", + "id": "oneos-project-nav", + "title": "ONE-OS 原型导航", + "defaultExpanded": true, + "children": [ + { + "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": "h2-site-doc-root", + "title": "站点信息说明", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "h2-site-doc-overview", + "title": "模块总览", + "markdown": "## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块 | 加氢站管理 → 站点信息 |\n| 用户 | 运营、财务结算、管理员 |\n| 核心任务 | 站点主数据、运营配置、氢费对账结算 |\n| 页面结构 | 筛选 + KPI + 工具栏 + 列表 + 分页 |\n\n## 2. 关联模块\n\n- **车辆氢费明细**:氢费数据来源与对账回写目标\n- **加氢站合包**:加氢订单、加氢记录仍在合包入口\n\n## 3. 验收重点\n\n1. KPI 高频/低频/无加氢与氢费明细一致\n2. 对账单两阶段流程与跨页回写\n3. 列表布局与租赁合同 vm 规范一致" + }, + { + "type": "link", + "id": "h2-site-link-vh2", + "title": "车辆氢费明细", + "href": "../vehicle-h2-fee-ledger/index.html", + "target": "self" + }, + { + "type": "link", + "id": "h2-site-link-bundle", + "title": "加氢站管理(合包)", + "href": "../oneos-web-h2-station/index.html", + "target": "self" + }, + { + "type": "folder", + "id": "h2-site-doc-prd", + "title": "PRD 全文", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "h2-site-doc-prd-full", + "title": "PRD 完整文档", + "markdown": "# 加氢站管理 · 站点信息 — 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.0 |\n| 模块名称 | 加氢站管理 → 站点信息 |\n| 所属系统 | ONE-OS Web 端 |\n| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 关联原型 | [车辆氢费明细](/prototypes/vehicle-h2-fee-ledger)、[加氢站合包](/prototypes/oneos-web-h2-station) |\n| 文档状态 | 已对齐原型 |\n\n---\n\n## 1. 背景与目标\n\n### 1.1 业务背景\n\n加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。\n\n### 1.2 本期目标\n\n- 站点台账的查询、新建、编辑、查看、删除与批量导入\n- 营业状态、成本价格、预付余额等运营配置\n- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写\n\n### 1.3 用户角色\n\n| 角色 | 典型诉求 |\n|---|---|\n| 加氢站运营 | 维护站点资料、营业与价格;查看加氢量与余额 |\n| 财务/结算 | 按周期生成对账单、登记收票、完成站点付款闭环 |\n| 管理员 | 批量导入站点、删除无效站点、配置系统账号 |\n\n### 1.4 非目标(本期不做)\n\n- 对账单审批流、ERP 自动过账\n- 发票 OCR 识别与验真\n- 加氢订单、加氢记录(仍在合包 `oneos-web-h2-station`)\n- 后端接口联调(原型使用 Mock + 共享 Store)\n\n---\n\n## 2. 信息架构\n\n```text\n站点信息\n├── 列表页\n│ ├── 筛选区(4 项)\n│ ├── KPI 卡片(6 张)\n│ ├── 工具栏\n│ └── 站点台账表 + 分页\n├── 新建 / 编辑(同页内嵌视图)\n│ ├── 基本信息\n│ ├── 系统账号绑定\n│ └── 供应商 / 付款信息\n└── 弹窗 / 抽屉\n ├── 查看详情\n ├── 营业状态 / 价格配置\n ├── 加氢量下钻 / 预付余额下钻\n ├── 生成对账单(两阶段)\n └── 查看对账记录\n```\n\n---\n\n## 3. 列表与 KPI\n\n> 详见 `.spec/requirements-prd-list.md`\n\n- 筛选:名称、签约、地区、营业状态\n- KPI:全部 / 签约·普通 / 高频≥3 / 低频1–2 / 无加氢0\n- 列表字段:名称地址、合作时间、营业、成本价、加氢次数/量、预付余额、联系方式、操作\n- 设计规范:与租赁合同管理一致的 vm 布局与 Inter 字体\n\n---\n\n## 4. 站点主数据\n\n### 4.1 新建站点\n\n同页内嵌视图:基本信息、系统账号绑定(可选)、供应商信息(新建或关联)。\n\n### 4.2 编辑 / 查看\n\n| 模式 | 交互 |\n|---|---|\n| 编辑 | 整页表单;系统账号仅 admin 可改 |\n| 查看 | 弹窗分块展示站点、账号、供应商、付款信息 |\n\n### 4.3 批量导入\n\n下载 CSV 模板 → 上传 → 解析预览 → 确认导入;站点名称不可重复。\n\n---\n\n## 5. 运营配置\n\n### 5.1 营业状态\n\n卡片式弹窗:营业状态、营业时间按钮组;非全天须配置时段;展示状态变更记录。\n\n### 5.2 价格配置\n\n配置成本价格(元/kg)与生效时间;到达生效时间后刷新列表「当前成本价格」;展示价格调整记录。\n\n### 5.3 下钻分析\n\n- **加氢量下钻**:统计卡 + 明细表 + 导出,字段对齐车辆氢费明细\n- **预付余额下钻**:充值/加氢流水 + 趋势图 + 导出\n\n---\n\n## 6. 对账与结算(重点)\n\n> 详见 `.spec/requirements-prd-statement.md`\n\n与「车辆氢费明细」配合完成站点侧氢费结算闭环:\n\n1. 拉取已对账且未结算的记录\n2. 两阶段:选期生成 → 填收票提交\n3. 提交后更新预付余额、写对账历史、回写台账(对账日期、收票日期、已付款)\n\n---\n\n## 7. 数据与集成\n\n| 能力 | 实现 |\n|---|---|\n| 氢费明细 | `src/common/h2VehicleLedgerBridge.js` |\n| 对账回写 | `H2_STATION_STATEMENT_LEDGER_UPDATES` |\n| 页面实现 | `src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx` |\n| 独立入口 | `src/prototypes/oneos-web-h2-station-site/index.tsx` |\n\n---\n\n## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅已对账未结算记录可进入对账单\n- [ ] 两阶段交互与必填校验完整\n- [ ] 提交后回写车辆氢费明细可跨页验收\n\n### 8.3 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---\n\n## 9. 修订记录\n\n| 版本 | 日期 | 说明 |\n|---|---|---|\n| v1.0 | 2026-07-01 | 独立入口、vm 规范对齐、标注目录与 PRD 接入 |\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md" + }, + { + "type": "markdown", + "id": "h2-site-doc-prd-list", + "title": "列表页 PRD", + "markdown": "# 加氢站管理 · 站点信息 — 列表页 PRD\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 加氢站管理 → 站点信息 |\n| 所属系统 | ONE-OS Web 端 |\n| 目标用户 | 加氢站运营、财务结算、平台管理员 |\n| 核心任务 | 查询站点台账、维护主数据、查看运营指标、发起对账与充值 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 设计规范 | 与租赁合同管理一致的 vm 布局(筛选 / KPI / 表格 / 分页) |\n\n---\n\n## 1. 页面结构\n\n用户进入「加氢站管理 → 站点信息」后,列表页自上而下为:\n\n1. **筛选区** — 名称、签约、地区、营业状态\n2. **KPI 分类** — 全部 / 签约·普通 / 高频 / 低频 / 无加氢\n3. **工具栏** — 全站余额提醒、批量导入、发起充值单、新建站点\n4. **站点列表** — 主数据 + 运营指标 + 操作列\n5. **分页栏** — 每页 5/10/20/50 条\n\n---\n\n## 2. 筛选区\n\n与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 |\n| 是否签约 | 下拉 | 签约站点 / 普通站点 |\n| 地区(省-市) | 级联 | 省、市两级,可只选省 |\n| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 |\n\n**交互**:\n\n- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页\n- **重置**:清空筛选条件并刷新列表\n\n---\n\n## 3. KPI 统计卡片\n\n与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部加氢站 | 列表全部站点 |\n| 签约站点 | 已签约,点击快捷筛选 |\n| 普通站点 | 未签约,点击快捷筛选 |\n| 高频加氢站 | 该站加氢次数 ≥ 3 |\n| 低频加氢站 | 有加氢但次数 < 3 |\n| 无加氢站点 | 加氢次数 = 0 |\n\n加氢次数来自「车辆氢费明细」共享 Store,按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。\n\n---\n\n## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 全站余额提醒设置 | 配置全局预付余额预警阈值 |\n| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 |\n| 发起充值单 | 为站点发起预付充值流程(原型演示) |\n| 新建站点 | 进入同页内嵌新建视图 |\n\n---\n\n## 5. 列表字段说明\n\n| 字段 | 说明 |\n|---|---|\n| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |\n| 合作时间 | 未签约可空 |\n| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |\n| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 |\n| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |\n| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |\n| 联系方式 | 联系人 + 电话 |\n| 操作 | 查看 + 更多菜单 |\n\n**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。\n\n**分页**:默认 10 条,可选 5/10/20/50。\n\n---\n\n## 6. 数据来源\n\n- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store\n- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed\n- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细\n\n---\n\n## 7. 双入口说明\n\n| 入口 | 路径 |\n|---|---|\n| 独立原型(本页) | OneOS → 加氢站管理 → 站点信息 |\n| 合包 | ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录 |\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-list.md" + }, + { + "type": "markdown", + "id": "h2-site-doc-prd-statement", + "title": "对账与结算 PRD", + "markdown": "# 加氢站管理 · 站点信息 — 对账与结算 PRD\n\n> 本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。\n\n---\n\n## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细** 中同时满足以下条件的记录:\n\n1. 对账状态 = **已对账**(台账已完成「完成对账」)\n2. **尚未**被本站点历史对账单结算过(未绑定 `statementRecordId`)\n3. 加氢时间落在用户选择的 **[账单开始日期, 账单结束日期]** 内\n4. 加氢站名称与当前操作站点一致\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n---\n\n## 2. 两阶段交互\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的已对账记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n\n---\n\n## 3. 提交后的系统行为\n\n提交成功后需**原子性**完成:\n\n1. 写入对账历史(供「查看对账记录」查询)\n2. 扣减站点预付余额\n3. 标记台账记录已结算(绑定 `statementRecordId`)\n4. **回写车辆氢费明细**:\n\n| 车辆氢费明细字段 | 回写值 |\n|---|---|\n| 对账日期 | 操作完成时间 |\n| 收票日期 | 操作完成日期 |\n| 加氢站付款状态 | 已付款 |\n\n原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge` 模拟跨页回写。\n\n---\n\n## 4. 查看对账记录\n\n入口:列表操作列 → 更多 → **查看对账记录**\n\n历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。\n\n查看明细:账单区间、统计三卡、收票信息、发票附件、成本明细表。\n\n---\n\n## 5. 校验与异常\n\n| 场景 | 处理 |\n|---|---|\n| 日期未填 / 开始晚于结束 | 阻止生成 |\n| 区间内无已对账未结算记录 | 阻止生成,提示调整日期 |\n| 收票日期/金额/附件未填 | 阻止提交 |\n| 收票金额 ≤ 0 | 阻止提交 |\n| 预付余额结算后为负 | 允许提交,标「已欠费」 |\n\n---\n\n## 6. 研发要点\n\n1. **对账单幂等**:同一笔已对账记录只能进入一次成功提交的对账单\n2. **上次结束时间**:取该站点已成功提交对账单的最大 `endDate`\n3. **余额计算**:`balanceAfter = prepaidBalance - sum(costTotal)`\n4. **跨模块同步**:生产对接统一台账;原型使用 Bridge Store\n5. **明细字段范围**:对账弹窗不展示客户侧加氢单价/总价\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md" + }, + { + "type": "markdown", + "id": "h2-site-doc-scope", + "title": "范围与决策", + "markdown": "# 站点信息独立原型 — 范围与决策\n\n## 背景\n\n「站点信息」从 ONE-OS Web 合包 `oneos-web-h2-station` 拆出,作为 OneOS → 加氢站管理下的独立 Axhub 入口,便于与正式菜单路径对齐。\n\n## 范围\n\n| 包含 | 不包含 |\n|------|--------|\n| 站点列表、KPI、新建/编辑/查看/删除 | 加氢订单、加氢记录(仍在合包) |\n| 营业状态、价格配置、余额下钻 | 后端接口联调 |\n| 两阶段对账单、充值单、批量导入 | |\n\n## 数据来源\n\n- **氢费/加氢明细**:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」原型共享 Store。\n- 先打开「车辆氢费明细」时,台账全量行会同步至 Store;仅打开加氢站模块时使用 canonical seed(10 条演示数据)。\n- 对账单提交 patch 经 `window.H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细。\n\n## KPI 口径(已确认)\n\n全部 / 签约·普通 / **高频(≥3 次)** / **低频(1–2 次)** / **无加氢(0 次)**,与《站点管理使用说明书》及页面内嵌 PRD 一致。\n\n## 角色演示\n\n页面右上角提供「平台管理员 / 加氢站账号」切换,影响编辑页系统账号字段可编辑性。\n\n## 双入口说明\n\n- **本原型**:OneOS → 加氢站管理 → 站点信息\n- **合包**:ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录\n", + "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements.md" + }, + { + "type": "markdown", + "id": "h2-site-doc-acceptance", + "title": "验收标准", + "markdown": "## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅已对账未结算记录可进入对账单\n- [ ] 两阶段交互与必填校验完整\n- [ ] 提交后回写车辆氢费明细可跨页验收\n\n### 8.3 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---" + } + ] + }, + { + "type": "folder", + "id": "h2-site-doc-page", + "title": "页面标注", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "h2-site-doc-node-h2-site-filter", + "title": "筛选条件", + "markdown": "## 2. 筛选区\n\n与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 |\n| 是否签约 | 下拉 | 签约站点 / 普通站点 |\n| 地区(省-市) | 级联 | 省、市两级,可只选省 |\n| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 |\n\n**交互**:\n\n- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页\n- **重置**:清空筛选条件并刷新列表\n\n---" + }, + { + "type": "markdown", + "id": "h2-site-doc-node-h2-site-kpi", + "title": "KPI 快捷筛选", + "markdown": "## 3. KPI 统计卡片\n\n与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部加氢站 | 列表全部站点 |\n| 签约站点 | 已签约,点击快捷筛选 |\n| 普通站点 | 未签约,点击快捷筛选 |\n| 高频加氢站 | 该站加氢次数 ≥ 3 |\n| 低频加氢站 | 有加氢但次数 < 3 |\n| 无加氢站点 | 加氢次数 = 0 |\n\n加氢次数来自「车辆氢费明细」共享 Store,按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。\n\n---" + }, + { + "type": "markdown", + "id": "h2-site-doc-node-h2-site-statement", + "title": "工具栏与对账", + "markdown": "## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 全站余额提醒设置 | 配置全局预付余额预警阈值 |\n| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 |\n| 发起充值单 | 为站点发起预付充值流程(原型演示) |\n| 新建站点 | 进入同页内嵌新建视图 |\n\n---\n\n---\n\n## 6. 数据来源\n\n- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store\n- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed\n- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细\n\n---" + }, + { + "type": "markdown", + "id": "h2-site-doc-node-h2-site-table", + "title": "站点列表", + "markdown": "## 5. 列表字段说明\n\n| 字段 | 说明 |\n|---|---|\n| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |\n| 合作时间 | 未签约可空 |\n| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |\n| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 |\n| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |\n| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |\n| 联系方式 | 联系人 + 电话 |\n| 操作 | 查看 + 更多菜单 |\n\n**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。\n\n**分页**:默认 10 条,可选 5/10/20/50。\n\n---" + } + ] + } + ] + } + ] + } +} diff --git a/src/prototypes/oneos-web-h2-station-site/index.tsx b/src/prototypes/oneos-web-h2-station-site/index.tsx new file mode 100644 index 0000000..223418c --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/index.tsx @@ -0,0 +1,85 @@ +/** + * @name 站点信息 + * 自 ONE-OS web端/加氢站管理/站点信息 独立预览 + */ +import '../../common/oneosWebLegacy/legacyGlobals'; +import '../../common/h2VehicleLedgerBridge.js'; +import React, { useEffect, useMemo } from 'react'; +import * as antd from 'antd'; +import { ConfigProvider } from 'antd'; +import 'antd/dist/reset.css'; +import '../vehicle-management/style.css'; +import '../lease-contract-management/styles/lease-contract.css'; +import '../oneos-web-h2-station/styles/h2-station-vm-filter.css'; +import { + AnnotationViewer, + type AnnotationSourceDocument, + type AnnotationViewerOptions, +} from '@axhub/annotation'; +import { clearHostPrototypeRouteInfo } from '../../common/useHashPage'; +import SiteInfoPage from '../oneos-web-h2-station/pages/03-站点信息.jsx'; +import annotationSourceDocument from './annotation-source.json'; + +/** 与租赁合同管理 / vehicle-management 主题一致 */ +const vmTheme = { + token: { + colorPrimary: '#32a06e', + colorLink: '#32a06e', + colorLinkHover: '#3fb87c', + borderRadius: 8, + fontFamily: + 'Inter, -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif', + fontSize: 14, + colorText: '#18181b', + colorTextSecondary: '#52525b', + colorBorder: '#e5e7eb', + colorBgContainer: '#ffffff', + }, + components: { + Table: { + headerBg: '#f6f7f7', + headerColor: '#71717a', + rowHoverBg: '#f6f7f7', + borderColor: '#e5e7eb', + cellPaddingBlock: 8, + cellPaddingInline: 12, + }, + Card: { + borderRadiusLG: 12, + }, + InputNumber: { + handleVisible: true, + }, + }, +}; + +window.React = React; +window.antd = antd; + +export default function OneosWebH2StationSite() { + useEffect(() => { + clearHostPrototypeRouteInfo(); + }, []); + + const annotationOptions = useMemo( + () => ({ + showToolbar: true, + showThemeToggle: true, + showColorFilter: true, + emptyWhenNoData: false, + toolbarEdge: 'right', + currentPageId: 'site-info', + }), + [], + ); + + return ( + + + + + ); +} diff --git a/src/prototypes/oneos-web-h2-station-site/scripts/build-annotation-source.mjs b/src/prototypes/oneos-web-h2-station-site/scripts/build-annotation-source.mjs new file mode 100644 index 0000000..25c44bf --- /dev/null +++ b/src/prototypes/oneos-web-h2-station-site/scripts/build-annotation-source.mjs @@ -0,0 +1,267 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const protoRoot = path.resolve(__dirname, '..'); +const specRoot = path.join(protoRoot, '.spec'); + +const fullPrd = fs.readFileSync(path.join(specRoot, 'requirements-prd.md'), 'utf8'); +const listPrd = fs.readFileSync(path.join(specRoot, 'requirements-prd-list.md'), 'utf8'); +const statementPrd = fs.readFileSync(path.join(specRoot, 'requirements-prd-statement.md'), 'utf8'); +const scopeMd = fs.readFileSync(path.join(specRoot, 'requirements.md'), 'utf8'); + +const NODE_DEFS = [ + { + id: 'h2-site-filter', + title: '筛选条件', + pageId: 'site-info', + color: '#2563eb', + aiPrompt: '筛选字段与查询/重置行为。', + annotationText: 'vm 筛选卡片:4 列栅格,标签在上控件在下。', + locator: { + selectors: ['.h2-station-page .vm-filter-card', '[data-annotation-id="h2-site-filter"]'], + fingerprint: 'filter|site-info', + }, + }, + { + id: 'h2-site-kpi', + title: 'KPI 快捷筛选', + pageId: 'site-info', + color: '#2563eb', + aiPrompt: '六张 KPI 卡片口径与点击筛选。', + annotationText: '全部 / 签约·普通 / 高频 / 低频 / 无加氢。', + locator: { + selectors: ['.h2-station-page .vm-kpi-row', '[data-annotation-id="h2-site-kpi"]'], + fingerprint: 'kpi|site-info', + }, + }, + { + id: 'h2-site-statement', + title: '工具栏与对账', + pageId: 'site-info', + color: '#d97706', + aiPrompt: '工具栏操作与对账单氢费联动。', + annotationText: '全站余额提醒、批量导入、充值单、新建站点;对账回写车辆氢费明细。', + locator: { + selectors: ['.h2-station-page .vm-table-toolbar', '[data-annotation-id="h2-site-toolbar"]'], + fingerprint: 'toolbar|statement', + }, + }, + { + id: 'h2-site-table', + title: '站点列表', + pageId: 'site-info', + color: '#32a06e', + aiPrompt: '台账表格列、下钻与行内操作。', + annotationText: '名称、营业、成本价、加氢量、预付余额、更多菜单。', + locator: { + selectors: ['.h2-station-page .vm-table-card', '[data-annotation-id="h2-site-table"]'], + fingerprint: 'table|site-info', + }, + }, +]; + +const NODE_PRD_MAP = { + 'h2-site-filter': { file: 'list', heading: '## 2. 筛选区' }, + 'h2-site-kpi': { file: 'list', heading: '## 3. KPI 统计卡片' }, + 'h2-site-statement': { file: 'list', heading: '## 4. 列表工具栏', combine: ['## 4. 列表工具栏', '## 6. 数据来源'] }, + 'h2-site-table': { file: 'list', heading: '## 5. 列表字段说明' }, +}; + +const prdByKey = { list: listPrd, statement: statementPrd, full: fullPrd }; + +function extractSection(md, startHeading) { + const lines = md.split('\n'); + let start = lines.findIndex((line) => line.trim() === startHeading || line.startsWith(startHeading)); + if (start < 0) return null; + const startLevel = (lines[start].match(/^(#+)/)?.[1] ?? '##').length; + let end = lines.length; + for (let i = start + 1; i < lines.length; i++) { + if (/^#{1,6}\s/.test(lines[i])) { + const level = lines[i].match(/^(#+)/)?.[1].length ?? 2; + if (level <= startLevel) { + end = i; + break; + } + } + } + return lines.slice(start, end).join('\n').trim(); +} + +function extractFromPrd({ file, heading, combine }) { + const md = prdByKey[file]; + if (!md) return null; + if (combine?.length) { + return combine.map((h) => extractSection(md, h)).filter(Boolean).join('\n\n---\n\n') || null; + } + return extractSection(md, heading); +} + +function buildFallbackMarkdown(node) { + const parts = [`# ${node.title}`, '']; + if (node.aiPrompt) parts.push(node.aiPrompt, ''); + if (node.annotationText?.trim()) parts.push('## 说明', '', node.annotationText.trim()); + return parts.join('\n').trim(); +} + +function buildNodeMarkdown(node) { + const mapping = NODE_PRD_MAP[node.id]; + if (mapping) { + const body = extractFromPrd(mapping); + if (body) return body; + } + if (node.id === 'h2-site-statement') { + const toolbar = extractSection(listPrd, '## 4. 列表工具栏'); + const stmt = extractSection(statementPrd, '## 1. 数据来源'); + if (toolbar && stmt) return `${toolbar}\n\n---\n\n${stmt}`; + } + return buildFallbackMarkdown(node); +} + +const now = Date.now(); +const nodes = NODE_DEFS.map((def, index) => ({ + id: def.id, + index: index + 1, + title: def.title, + pageId: def.pageId, + locator: { ...def.locator, path: [] }, + aiPrompt: def.aiPrompt, + annotationText: def.annotationText, + hasMarkdown: true, + color: def.color, + images: [], + createdAt: now, + updatedAt: now, + controls: [], +})); + +const markdownMap = Object.fromEntries(nodes.map((node) => [node.id, buildNodeMarkdown(node)])); + +const overviewMd = [ + '## 1. 模块定位', + '', + '| 项 | 说明 |', + '|---|---|', + '| 模块 | 加氢站管理 → 站点信息 |', + '| 用户 | 运营、财务结算、管理员 |', + '| 核心任务 | 站点主数据、运营配置、氢费对账结算 |', + '| 页面结构 | 筛选 + KPI + 工具栏 + 列表 + 分页 |', + '', + '## 2. 关联模块', + '', + '- **车辆氢费明细**:氢费数据来源与对账回写目标', + '- **加氢站合包**:加氢订单、加氢记录仍在合包入口', + '', + '## 3. 验收重点', + '', + '1. KPI 高频/低频/无加氢与氢费明细一致', + '2. 对账单两阶段流程与跨页回写', + '3. 列表布局与租赁合同 vm 规范一致', +].join('\n'); + +const acceptanceMd = extractSection(fullPrd, '## 8. 验收标准') ?? '## 验收标准\n\n见 PRD 全文。'; + +const source = { + documentVersion: 1, + format: 'axhub-annotation-source', + data: { + version: 2, + prototypeName: 'oneos-web-h2-station-site', + pageId: 'site-info', + updatedAt: now, + nodes, + }, + markdownMap, + assetMap: {}, + directory: { + nodes: [ + { + type: 'folder', + id: 'h2-site-doc-root', + title: '站点信息说明', + defaultExpanded: true, + children: [ + { + type: 'markdown', + id: 'h2-site-doc-overview', + title: '模块总览', + markdown: overviewMd, + }, + { + type: 'link', + id: 'h2-site-link-vh2', + title: '车辆氢费明细', + href: '/prototypes/vehicle-h2-fee-ledger', + target: 'self', + }, + { + type: 'link', + id: 'h2-site-link-bundle', + title: '加氢站管理(合包)', + href: '/prototypes/oneos-web-h2-station', + target: 'self', + }, + { + type: 'folder', + id: 'h2-site-doc-prd', + title: 'PRD 全文', + defaultExpanded: true, + children: [ + { + type: 'markdown', + id: 'h2-site-doc-prd-full', + title: 'PRD 完整文档', + markdown: fullPrd, + markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md', + }, + { + type: 'markdown', + id: 'h2-site-doc-prd-list', + title: '列表页 PRD', + markdown: listPrd, + markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-list.md', + }, + { + type: 'markdown', + id: 'h2-site-doc-prd-statement', + title: '对账与结算 PRD', + markdown: statementPrd, + markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md', + }, + { + type: 'markdown', + id: 'h2-site-doc-scope', + title: '范围与决策', + markdown: scopeMd, + markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements.md', + }, + { + type: 'markdown', + id: 'h2-site-doc-acceptance', + title: '验收标准', + markdown: acceptanceMd, + }, + ], + }, + { + type: 'folder', + id: 'h2-site-doc-page', + title: '页面标注', + defaultExpanded: true, + children: nodes.map((node) => ({ + type: 'markdown', + id: `h2-site-doc-node-${node.id}`, + title: node.title, + markdown: markdownMap[node.id], + })), + }, + ], + }, + ], + }, +}; + +const outPath = path.join(protoRoot, 'annotation-source.json'); +fs.writeFileSync(outPath, `${JSON.stringify(source, null, 2)}\n`, 'utf8'); +console.log(`Wrote ${outPath} (${nodes.length} nodes, ${Object.keys(markdownMap).length} markdown entries)`); diff --git a/src/prototypes/oneos-web-h2-station/index.tsx b/src/prototypes/oneos-web-h2-station/index.tsx index 2a7adb5..fc2140c 100644 --- a/src/prototypes/oneos-web-h2-station/index.tsx +++ b/src/prototypes/oneos-web-h2-station/index.tsx @@ -1,18 +1,17 @@ /** * @name 加氢站管理 - * 自 ONE-OS web端 原稿复刻(3 个页面) + * 自 ONE-OS web端 原稿复刻(加氢订单、加氢记录) */ import '../../common/oneosWebLegacy/legacyGlobals'; +import '../../common/h2VehicleLedgerBridge.js'; import React from 'react'; import Page1 from './pages/01-加氢订单.jsx'; import Page2 from './pages/02-加氢记录.jsx'; -import Page3 from './pages/03-站点信息.jsx'; import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; const pages = [ { id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8ba2u5355', title: '加氢订单', component: Page1 }, { id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8bb0u5f55', title: '加氢记录', component: Page2 }, - { id: 'u52a0u6c22u7ad9u7ba1u7406-u7ad9u70b9u4fe1u606f', title: '站点信息', component: Page3 }, ]; export default function OneosWebH2Station() { diff --git a/src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx b/src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx index 94bd53a..859ce86 100644 --- a/src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx +++ b/src/prototypes/oneos-web-h2-station/pages/01-加氢订单.jsx @@ -32,16 +32,36 @@ const Component = function () { var requirementModalVisible = useState(false); - // 模拟:加氢站与加氢记录数据 + var orderDataRevisionState = useState(0); + var orderDataRevision = orderDataRevisionState[0]; + var setOrderDataRevision = orderDataRevisionState[1]; + + React.useEffect(function () { + var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null; + if (!bridge || typeof bridge.subscribe !== 'function') return undefined; + return bridge.subscribe(function () { + setOrderDataRevision(function (v) { return v + 1; }); + }); + }, []); + + // 模拟:加氢站与加氢记录数据(氢费明细来自车辆氢费明细共享 Store) var stationList = useMemo(function () { + var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null; + if (bridge && typeof bridge.getStationList === 'function') { + return bridge.getStationList(); + } return [ { value: 'JX-H2-001', label: '嘉兴加氢站(一期)' }, { value: 'HZ-H2-002', label: '杭州临平加氢站' }, { value: 'SH-H2-003', label: '上海宝山加氢站' } ]; - }, []); + }, [orderDataRevision]); var rawData = useMemo(function () { + var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null; + if (bridge && typeof bridge.getOrderRecords === 'function') { + return bridge.getOrderRecords(); + } return [ { id: 1, stationCode: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', time: '2026-03-01 10:21', plateNo: '浙A12345', amountKg: 12.5, amountYuan: 625.00 }, { id: 2, stationCode: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', time: '2026-03-01 14:08', plateNo: '浙A67890', amountKg: 10.0, amountYuan: 500.00 }, @@ -49,7 +69,7 @@ const Component = function () { { id: 4, stationCode: 'SH-H2-003', stationName: '上海宝山加氢站', time: '2026-03-03 16:45', plateNo: '沪A88888', amountKg: 8.0, amountYuan: 400.00 }, { id: 5, stationCode: 'HZ-H2-002', stationName: '杭州临平加氢站', time: '2026-03-03 18:10', plateNo: '浙B99999', amountKg: 18.2, amountYuan: 910.00 } ]; - }, []); + }, [orderDataRevision]); // 简单字符串时间过滤(示例用,实际可改为 dayjs 比较) function inRange(timeStr, range) { diff --git a/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx b/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx index a30fee1..6a2d871 100644 --- a/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx +++ b/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx @@ -217,7 +217,19 @@ function hrSettlementStatusLabel(value) { return info ? info.label : '—'; } -var HR_INITIAL_RECORDS = [ +function hrGetVehicleLedgerBridge() { + return typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null; +} + +function hrBuildInitialRecordsFromLedger() { + var bridge = hrGetVehicleLedgerBridge(); + if (bridge && typeof bridge.getHrRecords === 'function') { + return bridge.getHrRecords(HR_MOCK_FILLERS); + } + return HR_FALLBACK_RECORDS.map(function (r) { return Object.assign({}, r); }); +} + +var HR_FALLBACK_RECORDS = [ { id: 'hr-1', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', hydrogenKg: 12.5, customerAmount: 562.5, mileageKm: 128560, fillerName: '张三', settlementStatus: 'customer' }, { id: 'hr-2', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', hydrogenKg: 10.0, customerAmount: 450.0, mileageKm: 95230, fillerName: '李四', settlementStatus: 'internal' }, { id: 'hr-3', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', hydrogenKg: 18.3, customerAmount: 823.5, mileageKm: 201880, fillerName: '王静', settlementStatus: 'customer_self' }, @@ -492,11 +504,19 @@ const Component = function () { var setEditingId = editingIdState[1]; var listDataState = useState(function () { - return HR_INITIAL_RECORDS.map(function (r) { return Object.assign({}, r); }); + return hrBuildInitialRecordsFromLedger(); }); var listData = listDataState[0]; var setListData = listDataState[1]; + React.useEffect(function () { + var bridge = hrGetVehicleLedgerBridge(); + if (!bridge || typeof bridge.subscribe !== 'function') return undefined; + return bridge.subscribe(function () { + setListData(hrBuildInitialRecordsFromLedger()); + }); + }, []); + var draftFiltersState = useState({ timeRange: [], plateNo: undefined, fillerName: undefined }); var draftFilters = draftFiltersState[0]; var setDraftFilters = draftFiltersState[1]; diff --git a/src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx b/src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx index 7382f91..1a9356d 100644 --- a/src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx +++ b/src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx @@ -10,8 +10,8 @@ var H2_REGION_CASCADER_OPTIONS = [ var H2_STATION_KPI_CARDS = [ { key: 'all', type: 'total', title: '全部加氢站', desc: '纳入台账管理的全部站点' }, - { key: 'balanceAlert', type: 'warning', title: '预付余额预警站点', desc: '预付余额低于所设提醒阈值的站点' }, - { key: 'arrears', type: 'unuploaded', title: '已欠费站点', desc: '预付余额小于 0 的站点' }, + { key: 'high', type: 'normal', title: '高频加氢站', desc: '近周期内加氢次数 ≥ 3 次的站点' }, + { key: 'low', type: 'warning', title: '低频加氢站', desc: '近周期内有加氢但次数 < 3 次的站点' }, { key: 'none', type: 'unuploaded', title: '无加氢站点', desc: '暂无加氢记录的站点' } ]; @@ -31,83 +31,50 @@ var ONEOS_ANT_TABLE_GLOBAL_FIX = [ '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }', '.ant-table-container .ant-table-body { margin-top: 0 !important; }', '.ant-table-container .ant-table-body > table, .ant-table-content table { margin-top: 0 !important; }', - '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }' + '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }', + '.h2-site-list-table.vm-list-table .ant-table-thead th,.h2-site-list-table.vm-list-table .ant-table-tbody td{white-space:nowrap;}' ]; +function h2KpiHelpIcon() { + return React.createElement('svg', { + xmlns: 'http://www.w3.org/2000/svg', + width: 14, + height: 14, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, + React.createElement('circle', { cx: 12, cy: 12, r: 10 }), + React.createElement('path', { d: 'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3' }), + React.createElement('path', { d: 'M12 17h.01' }) + ); +} + var H2_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ - '.h2-station-page { --h2-form-row-gap: 20px; --h2-form-col-gap: 24px; --h2-form-label-gap: 8px; padding: 24px 24px 80px; height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow: hidden; box-sizing: border-box; }', - '.h2-station-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', - '.h2-station-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', - '.h2-station-page .lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', - '.h2-station-page .lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; }', - '.h2-station-page .lc-filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; align-items: center; }', - '@media (max-width: 1280px) { .h2-station-page .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', - '@media (max-width: 640px) { .h2-station-page .lc-filter-grid { grid-template-columns: 1fr; } }', - '.h2-station-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 32px; }', - '.h2-station-page .lc-filter-field-label { flex: 0 0 88px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 32px; white-space: nowrap; }', - '.h2-station-page .lc-filter-field-control { flex: 1; min-width: 0; }', - '.h2-station-page .lc-filter-field-control .ant-input, .h2-station-page .lc-filter-field-control .ant-input-affix-wrapper, .h2-station-page .lc-filter-field-control .ant-select .ant-select-selector, .h2-station-page .lc-filter-field-control .ant-cascader .ant-select-selector { width: 100%; height: 32px !important; min-height: 32px !important; border-radius: 8px !important; box-sizing: border-box; }', - '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper { display: inline-flex; align-items: center; padding-top: 0 !important; padding-bottom: 0 !important; }', - '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper > input.ant-input { height: 30px; line-height: 30px; padding-top: 0; padding-bottom: 0; }', - '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector { display: flex; align-items: center; }', - '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-search-input { height: 30px; }', - '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-item, .h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-placeholder { line-height: 30px !important; }', - '.h2-station-page .lc-filter-field-control .ant-cascader .ant-select-selector { display: flex; align-items: center; }', - '.h2-station-page .lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', - '.h2-station-page .lc-alert-stats-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }', - '@media (max-width: 1400px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }', - '@media (max-width: 900px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', - '@media (max-width: 640px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: 1fr; } }', - '.h2-station-page .lc-alert-card { display: flex; align-items: center; gap: 14px; padding: 16px 30px 16px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; min-height: 72px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', - '.h2-station-page .lc-alert-card-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }', - '.h2-station-page .lc-alert-card-icon { flex-shrink: 0; width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, #0d7a63 0%, #065f4a 100%); color: #fff; box-shadow: 0 4px 12px rgba(6, 95, 74, 0.28); }', - '.h2-station-page .lc-alert-card-icon svg { display: block; flex-shrink: 0; }', - '.h2-station-page .lc-alert-card-val { font-size: 24px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; }', - '.h2-station-page .lc-alert-card-title { font-size: 13px; font-weight: 500; color: #64748b; line-height: 1.3; }', - '.h2-station-page .lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; }', - '.h2-station-page .lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,0.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; }', - '.h2-station-page .lc-alert-card-tip:hover { color: #64748b; border-color: #cbd5e1; background: #fff; }', - '.h2-station-page .lc-alert-card--total, .h2-station-page .lc-alert-card--normal, .h2-station-page .lc-alert-card--warning, .h2-station-page .lc-alert-card--unuploaded { background: #fff; border-color: #e2e8f0; }', - '.h2-station-page .lc-alert-card--normal .lc-alert-card-val, .h2-station-page .lc-alert-card--warning .lc-alert-card-val, .h2-station-page .lc-alert-card--unuploaded .lc-alert-card-val { color: #0f172a; }', - '.h2-station-page .lc-alert-card-clickable { cursor: pointer; transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }', - '.h2-station-page .lc-alert-card-clickable:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); }', - '.h2-station-page .lc-alert-card-clickable:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', - '.h2-station-page .lc-alert-card-active { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25) !important; border-color: #10b981 !important; }', - '.h2-station-page .lc-table-section { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }', - '.h2-station-page .lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }', - '.h2-station-page .lc-table-legend-outer { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 6px 4px; font-size: 12px; color: #64748b; }', - '.h2-station-page .lc-table-legend-label { font-weight: 600; color: #64748b; }', - '.h2-station-page .lc-table-legend-item { display: inline-flex; align-items: center; gap: 6px; }', - '.h2-station-page .lc-table-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }', - '.h2-station-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', - '.h2-station-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; flex: 1; overflow-y: auto; display: flex; flex-direction: column; }', - '.h2-station-page .lc-list-table .ant-table-wrapper, .h2-station-page .lc-list-table .ant-table { width: 100% !important; }', - '.h2-station-page .lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; }', - '.h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row, .h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', - '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 12px 16px !important; font-size: 13px; }', - '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; }', - '.h2-station-page .lc-table-card .ant-pagination { margin: 0 !important; padding: 12px 16px !important; border-top: 1px solid #f1f5f9; }', - '.h2-station-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; min-height: 44px; }', - '.h2-station-page .lc-action-btn-danger { color: #ef4444 !important; }', + '.h2-station-page { --h2-form-row-gap: 20px; --h2-form-col-gap: 24px; --h2-form-label-gap: 8px; font-family: var(--vm-font); font-size: 0.875rem; line-height: 1.5; letter-spacing: -0.01em; color: var(--ln-body); -webkit-font-smoothing: antialiased; padding: 20px 24px 24px; height: 100vh; display: flex; flex-direction: column; background: #f8fafc; overflow: hidden; box-sizing: border-box; }', '.h2-station-page .h2-action-more-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; color: #64748b; cursor: pointer; transition: background 0.15s ease, color 0.15s ease; }', '.h2-station-page .h2-action-more-btn:hover { background: #f1f5f9; color: #334155; }', '.h2-station-page .h2-action-more-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', '.h2-station-page .h2-row-actions { display: inline-flex; align-items: center; gap: 4px; }', - '.h2-station-page .lc-station-name { font-weight: 700; color: #0f172a; font-size: 13px; }', + '.h2-station-page .lc-station-name { font-weight: 500; color: var(--ln-ink); font-size: 0.8125rem; }', '.h2-station-page .lc-station-name-cell { min-width: 0; }', '.h2-station-page .h2-current-cost-price-cell { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; max-width: 100%; flex-wrap: nowrap; }', - '.h2-station-page .h2-price-type-tag.ant-tag { margin: 0; font-size: 11px; line-height: 18px; padding: 0 6px; flex-shrink: 0; }', + '.h2-station-page .h2-price-type-tag.ant-tag { margin: 0; font-size: 0.75rem; font-weight: 400; line-height: 18px; padding: 0 6px; flex-shrink: 0; }', '.h2-station-page .h2-price-multi-warn { display: inline-flex; align-items: center; justify-content: center; color: #f59e0b; cursor: help; flex-shrink: 0; line-height: 0; }', '.h2-station-page .h2-price-multi-warn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; border-radius: 4px; }', '.h2-station-page .lc-station-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }', '.h2-station-page .lc-station-name-row .lc-station-name { min-width: 0; }', - '.h2-station-page .lc-station-address-line { margin-top: 4px; font-size: 12px; color: #64748b; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', - '.h2-station-page .lc-station-signed-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', + '.h2-station-page .lc-station-address-line { margin-top: 4px; font-size: 0.8125rem; color: var(--ln-muted); line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', + '.h2-station-page .lc-station-signed-tag { margin: 0 !important; border-radius: 6px !important; font-size: 0.75rem !important; font-weight: 500 !important; flex-shrink: 0; line-height: 20px !important; }', '.h2-station-page .lc-refuel-kg-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; min-width: 0; }', - '.h2-station-page .lc-refuel-freq-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', - '.h2-station-page .lc-station-region { color: #64748b; font-size: 12px; margin-bottom: 2px; }', - '.h2-station-page .lc-station-contact-name { font-weight: 600; color: #0f172a; font-size: 13px; line-height: 1.35; }', - '.h2-station-page .lc-station-contact-phone { color: #64748b; font-size: 12px; margin-top: 2px; line-height: 1.35; font-variant-numeric: tabular-nums; }', + '.h2-station-page .lc-refuel-freq-tag { margin: 0 !important; border-radius: 6px !important; font-size: 0.75rem !important; font-weight: 500 !important; flex-shrink: 0; line-height: 20px !important; }', + '.h2-station-page .lc-station-region { color: var(--ln-muted); font-size: 0.8125rem; margin-bottom: 2px; }', + '.h2-station-page .lc-station-contact-name { font-weight: 500; color: var(--ln-ink); font-size: 0.8125rem; line-height: 1.35; }', + '.h2-station-page .lc-station-contact-phone { color: var(--ln-muted); font-size: 0.8125rem; margin-top: 2px; line-height: 1.35; font-variant-numeric: tabular-nums; font-family: var(--vm-font-mono); }', '.h2-station-page .h2-region-address { display: flex; flex-direction: column; gap: 12px; width: 100%; }', '.h2-station-page .h2-region-address .ant-select, .h2-station-page .h2-region-address .ant-input { width: 100%; }', '.h2-station-page .h2-region-address--inline { flex-direction: row; align-items: center; gap: 8px; }', @@ -186,7 +153,7 @@ var H2_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ '.h2-station-page .h2-status-log-title { font-size: 14px; font-weight: 700; color: #0f172a; margin-bottom: 10px; }', '.h2-station-page .h2-status-log-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }', '.h2-station-page .h2-status-log-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }', - '.h2-station-page .h2-refuel-drill-link { border: none; background: none; padding: 0; font-weight: 700; color: #059669; cursor: pointer; font-variant-numeric: tabular-nums; font-size: 13px; }', + '.h2-station-page .h2-refuel-drill-link { border: none; background: none; padding: 0; font-weight: 600; color: var(--ln-primary); cursor: pointer; font-variant-numeric: tabular-nums; font-size: 0.8125rem; }', '.h2-station-page .h2-refuel-drill-link:hover { color: #047857; text-decoration: underline; }', '.h2-station-page .h2-refuel-drill-link:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }', '.h2-station-page .h2-ledger-totals-bar { display: flex; align-items: stretch; gap: 0; margin-bottom: 12px; border: 1px solid #bae6fd; border-radius: 10px; overflow: hidden; background: #f8fafc; box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04); }', @@ -244,10 +211,9 @@ var H2_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-money--customer { color: #4f46e5; font-weight: 700; }', '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-order-no { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #475569; background: #f1f5f9; border: 1px solid #e2e8f0; padding: 2px 7px; border-radius: 6px; letter-spacing: 0.02em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '.h2-station-page .h2-prepaid-balance-cell { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: nowrap; box-sizing: border-box; }', - '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount { flex: 0 0 auto; white-space: nowrap; border: none; background: none; padding: 0; font: inherit; font-weight: 700; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; font-variant-numeric: tabular-nums; }', + '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount { flex: 0 0 auto; white-space: nowrap; border: none; background: none; padding: 0; font: inherit; font-weight: 600; font-size: 0.8125rem; color: var(--ln-primary); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; font-variant-numeric: tabular-nums; font-family: var(--vm-font-mono); }', '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }', '.h2-station-page .h2-prepaid-balance-cell .lc-station-signed-tag { flex-shrink: 0; }', - '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance { overflow: visible !important; }', '.h2-balance-drill-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', '.h2-balance-drill-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', '.h2-balance-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', @@ -2142,24 +2108,20 @@ function h2KpiCardIcon(key) { { tag: 'line', x1: 19, y1: 16, x2: 16, y2: 19 } ], size); } - if (key === 'balanceAlert') { + if (key === 'high') { return h2SvgIcon([ - { d: 'M12 2v4' }, - { d: 'M12 18v4' }, - { d: 'M4.93 4.93l2.83 2.83' }, - { d: 'M16.24 16.24l2.83 2.83' }, - { d: 'M2 12h4' }, - { d: 'M18 12h4' }, - { d: 'M4.93 19.07l2.83-2.83' }, - { d: 'M16.24 7.76l2.83-2.83' }, - { tag: 'circle', cx: 12, cy: 12, r: 4 } + { d: 'M3 22h18' }, + { d: 'M6 22V12l4-2v12' }, + { d: 'M12 22V8l4-2v16' }, + { d: 'M18 22V14l3-1.5v9.5' } ], size); } - if (key === 'arrears') { + if (key === 'low') { return h2SvgIcon([ - { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, - { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, - { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 } + { d: 'M3 22h18' }, + { d: 'M8 22V16l3-1.5V22' }, + { d: 'M14 22V18l3-1.5V22' }, + { tag: 'line', x1: 6, y1: 22, x2: 20, y2: 22 } ], size); } if (key === 'none') { @@ -2990,19 +2952,15 @@ function h2SettlementStatusLabel(value) { return info ? info.label : '—'; } -/** 站点加氢记录 Mock(字段对齐车辆氢费明细) */ -var H2_MOCK_REFUEL_RECORDS = [ - { id: 'rf-1', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 12.5, costUnitPrice: 42.5, costAmount: 531.25, customerUnitPrice: 45, customerAmount: 562.5, settlementStatus: 'customer' }, - { id: 'rf-2', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', customerName: '浙江绿运物流有限公司', hydrogenKg: 10.0, costUnitPrice: 42.5, costAmount: 425.0, customerUnitPrice: 45, customerAmount: 450.0, settlementStatus: 'internal' }, - { id: 'rf-3', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 18.3, costUnitPrice: 42.5, costAmount: 777.75, customerUnitPrice: 45, customerAmount: 823.5, settlementStatus: 'customer_self' }, - { id: 'rf-4', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', customerName: '嘉兴港务氢能运输队', hydrogenKg: 15.6, costUnitPrice: 42.5, costAmount: 663.0, customerUnitPrice: 45, customerAmount: 702.0, settlementStatus: 'customer' }, - { id: 'rf-5', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', customerName: '杭州临平城配中心', hydrogenKg: 15.3, costUnitPrice: 43.0, costAmount: 657.9, customerUnitPrice: 46, customerAmount: 703.8, settlementStatus: 'internal' }, - { id: 'rf-6', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', customerName: '浙江氢运科技', hydrogenKg: 18.2, costUnitPrice: 43.0, costAmount: 782.6, customerUnitPrice: 46, customerAmount: 837.2, settlementStatus: 'customer_self' }, - { id: 'rf-7', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costAmount: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer' }, - { id: 'rf-8', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costAmount: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal' }, - { id: 'rf-9', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costAmount: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self' }, - { id: 'rf-10', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costAmount: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer' } -]; +/** 氢费明细统一来自「车辆氢费明细」共享 Store(见 common/h2VehicleLedgerBridge.js) */ +function h2GetVehicleLedgerBridge() { + return typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null; +} + +function h2GetAllRefuelRecordsFromLedger() { + var bridge = h2GetVehicleLedgerBridge(); + return bridge ? bridge.getAllRefuelRecords() : []; +} function h2FormatKgNum(v) { var n = typeof v === 'number' ? v : parseFloat(v); @@ -3513,32 +3471,10 @@ function h2FormatOperateDateOnly() { return h2OperateTimestamp().slice(0, 10); } -/** 台账加氢记录 Mock(字段对齐车辆氢费明细,costTotal 对应成本总价) */ -function h2BuildLedgerReconciledRecords() { - return H2_MOCK_REFUEL_RECORDS.map(function (r, index) { - var isReconciled = index < 9; - return { - id: r.id, - stationName: r.stationName, - hydrogenTime: r.hydrogenTime, - plateNo: r.plateNo, - customerName: r.customerName, - hydrogenKg: r.hydrogenKg, - costUnitPrice: r.costUnitPrice, - costTotal: r.costAmount, - settlementStatus: r.settlementStatus, - orderNo: h2BuildRefuelOrderNo(r, index), - reconcileStatus: isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING, - statementRecordId: null, - reconcileDate: null, - receiptDate: null, - paymentStatus: isReconciled ? H2_PAYMENT_STATUS_UNPAID : null - }; - }); -} - +/** 台账加氢记录(字段对齐车辆氢费明细,costTotal 对应成本总价) */ function h2InitLedgerRecordsStore() { - return h2BuildLedgerReconciledRecords(); + var bridge = h2GetVehicleLedgerBridge(); + return bridge ? bridge.buildStationLedgerStore() : []; } function h2InitStatementRecordsStore() { @@ -3627,10 +3563,9 @@ function h2ExportStatementCsv(stationName, startDate, endDate, records) { } function h2GetRefuelRecordsByStation(stationName) { - var name = String(stationName || '').trim(); - return H2_MOCK_REFUEL_RECORDS.filter(function (r) { return r.stationName === name; }).slice().sort(function (a, b) { - return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); - }).map(function (r, index) { + var bridge = h2GetVehicleLedgerBridge(); + var records = bridge ? bridge.getRefuelRecordsByStation(stationName) : []; + return records.map(function (r, index) { return Object.assign({}, r, { orderNo: h2BuildRefuelOrderNo(r, index) }); }); } @@ -4959,16 +4894,6 @@ var H2_PRIMARY_BTN_STYLE = { border: 'none' }; -var H2_REQ_BTN_STYLE = { - borderRadius: 8, - border: '1px solid #cbd5e1', - fontWeight: 600, - display: 'inline-flex', - alignItems: 'center', - boxShadow: '0 1px 2px rgba(0,0,0,0.05)', - color: '#475569' -}; - function h2ResolveRecordSupplierMode(record) { if (!record) return 'link'; if (record.supplierMode === 'none') return 'none'; @@ -5705,7 +5630,6 @@ const Component = function () { var useCallback = React.useCallback; var antd = window.antd; - var Breadcrumb = antd.Breadcrumb; var Card = antd.Card; var Table = antd.Table; var Button = antd.Button; @@ -5760,14 +5684,6 @@ const Component = function () { var filePreviewModal = filePreviewModalState[0]; var setFilePreviewModal = filePreviewModalState[1]; - var requirementModalState = useState(false); - var requirementModalOpen = requirementModalState[0]; - var setRequirementModalOpen = requirementModalState[1]; - - var userManualModalState = useState(false); - var userManualModalOpen = userManualModalState[0]; - var setUserManualModalOpen = userManualModalState[1]; - var importModalState = useState(false); var importModalOpen = importModalState[0]; var setImportModalOpen = importModalState[1]; @@ -5841,6 +5757,21 @@ const Component = function () { var ledgerStore = ledgerStoreState[0]; var setLedgerStore = ledgerStoreState[1]; + var ledgerDataRevisionState = useState(0); + var ledgerDataRevision = ledgerDataRevisionState[0]; + var setLedgerDataRevision = ledgerDataRevisionState[1]; + + React.useEffect(function () { + var bridge = h2GetVehicleLedgerBridge(); + if (!bridge || typeof bridge.subscribe !== 'function') return undefined; + return bridge.subscribe(function () { + setLedgerStore(h2InitLedgerRecordsStore()); + setLedgerDataRevision(function (v) { return v + 1; }); + }); + }, []); + + var isAdminUser = h2IsAdminUser(); + var statementRecordsState = useState(function () { return h2InitStatementRecordsStore(); }); var statementRecords = statementRecordsState[0]; var setStatementRecords = statementRecordsState[1]; @@ -5924,8 +5855,6 @@ const Component = function () { var editSupplier = editSupplierState[0]; var setEditSupplier = editSupplierState[1]; - var isAdminUser = h2IsAdminUser(); - var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId; var existingNameMap = useMemo(function () { @@ -5937,15 +5866,16 @@ const Component = function () { }, [listData]); var categoryCounts = useMemo(function () { - var counts = { all: listData.length, balanceAlert: 0, arrears: 0, none: 0 }; + var counts = { all: listData.length, high: 0, low: 0, none: 0 }; listData.forEach(function (r) { - if (h2IsArrearsStation(r)) counts.arrears += 1; - else if (h2IsBalanceAlertStation(r)) counts.balanceAlert += 1; var stats = h2CalcRefuelStats(r.name); - if (h2DeriveFrequencyByRefuelCount(stats.count) === 'none') counts.none += 1; + var freq = h2DeriveFrequencyByRefuelCount(stats.count); + if (freq === 'high') counts.high += 1; + else if (freq === 'low') counts.low += 1; + else counts.none += 1; }); return counts; - }, [listData]); + }, [listData, ledgerDataRevision]); var signedStats = useMemo(function () { var signed = 0; @@ -5957,12 +5887,11 @@ const Component = function () { var list = listData.slice(); if (categoryTab !== 'all') { list = list.filter(function (r) { - if (categoryTab === 'balanceAlert') return h2IsBalanceAlertStation(r); - if (categoryTab === 'arrears') return h2IsArrearsStation(r); - if (categoryTab === 'none') { - var stats = h2CalcRefuelStats(r.name); - return h2DeriveFrequencyByRefuelCount(stats.count) === 'none'; - } + var stats = h2CalcRefuelStats(r.name); + var freq = h2DeriveFrequencyByRefuelCount(stats.count); + if (categoryTab === 'high') return freq === 'high'; + if (categoryTab === 'low') return freq === 'low'; + if (categoryTab === 'none') return freq === 'none'; return true; }); } @@ -5984,7 +5913,7 @@ const Component = function () { refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count) }); }); - }, [listData, categoryTab, appliedFilters]); + }, [listData, categoryTab, appliedFilters, ledgerDataRevision]); var sortedList = useMemo(function () { var list = filteredList.slice(); @@ -6000,30 +5929,94 @@ const Component = function () { }, [filteredList, refuelSort]); var totalCount = sortedList.length; + var totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); + var safePage = Math.min(page, totalPages); var displayList = useMemo(function () { - var start = (page - 1) * pageSize; + var start = (safePage - 1) * pageSize; return sortedList.slice(start, start + pageSize); - }, [sortedList, page, pageSize]); + }, [sortedList, safePage, pageSize]); - var tablePagination = useMemo(function () { - return { - current: page, - pageSize: pageSize, - total: totalCount, - showSizeChanger: true, - pageSizeOptions: ['5', '10', '20', '50'], - showTotal: function (t) { return '共 ' + t + ' 条'; }, - onChange: function (p, size) { - setPage(p); - if (size && size !== pageSize) setPageSize(size); - } - }; - }, [page, pageSize, totalCount]); + function h2BuildPageItems(current, pages) { + if (pages <= 7) { + var all = []; + var pi; + for (pi = 1; pi <= pages; pi++) all.push(pi); + return all; + } + var items = [1]; + if (current > 3) items.push('ellipsis'); + var startP = Math.max(2, current - 1); + var endP = Math.min(pages - 1, current + 1); + for (var pj = startP; pj <= endP; pj++) items.push(pj); + if (current < pages - 2) items.push('ellipsis'); + if (pages > 1) items.push(pages); + return items; + } + + var renderH2TablePagination = useCallback(function () { + if (totalCount === 0) return null; + var pageItems = h2BuildPageItems(safePage, totalPages); + var pageSizeOptions = [5, 10, 20, 50]; + return React.createElement('div', { className: 'vm-pagination', role: 'navigation', 'aria-label': '表格分页' }, + React.createElement('label', { className: 'vm-pagination-size' }, + React.createElement('span', { className: 'vm-pagination-size-label' }, '每页'), + React.createElement('select', { + className: 'vm-pagination-select', + value: pageSize, + 'aria-label': '每页显示条数', + onChange: function (e) { + setPageSize(Number(e.target.value)); + setPage(1); + } + }, pageSizeOptions.map(function (size) { + return React.createElement('option', { key: size, value: size }, size); + })), + React.createElement('span', { className: 'vm-pagination-size-label' }, '条') + ), + React.createElement('div', { className: 'vm-pagination-pages' }, + React.createElement('button', { + type: 'button', + className: 'vm-pagination-nav', + disabled: safePage <= 1, + 'aria-label': '上一页', + onClick: function () { setPage(safePage - 1); } + }, + React.createElement('svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, + React.createElement('path', { d: 'm15 18-6-6 6-6' }) + ) + ), + pageItems.map(function (item, index) { + if (item === 'ellipsis') { + return React.createElement('span', { key: 'ellipsis-' + index, className: 'vm-pagination-ellipsis', 'aria-hidden': true }, '…'); + } + return React.createElement('button', { + key: item, + type: 'button', + className: 'vm-pagination-page' + (safePage === item ? ' active' : ''), + 'aria-current': safePage === item ? 'page' : undefined, + onClick: function () { setPage(item); } + }, item); + }), + React.createElement('button', { + type: 'button', + className: 'vm-pagination-nav', + disabled: safePage >= totalPages, + 'aria-label': '下一页', + onClick: function () { setPage(safePage + 1); } + }, + React.createElement('svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, + React.createElement('path', { d: 'm9 18 6-6-6-6' }) + ) + ) + ), + React.createElement('div', { className: 'vm-table-total' }, '共 ' + totalCount + ' 条') + ); + }, [totalCount, safePage, totalPages, pageSize]); var handleKpiCardClick = useCallback(function (key) { setCategoryTab(key); - if (key === 'all' || key === 'none') { + if (key === 'all' || key === 'high' || key === 'low' || key === 'none') { setListFilters(function (p) { return Object.assign({}, p, { signed: undefined }); }); setAppliedFilters(function (p) { return Object.assign({}, p, { signed: undefined }); }); } @@ -6072,49 +6065,40 @@ const Component = function () { }, []); var renderFilterField = useCallback(function (label, control) { - return React.createElement('div', { className: 'lc-filter-field' }, - React.createElement('span', { className: 'lc-filter-field-label' }, label), - React.createElement('div', { className: 'lc-filter-field-control' }, control) + return React.createElement('label', { className: 'vm-filter-field' }, + React.createElement('span', null, label), + control ); }, []); - var renderAlertStatCard = useCallback(function (card, options) { + var renderVmKpiCard = useCallback(function (card, options) { var active = options.active; var count = options.count; var onClick = options.onClick; var icon = options.icon; - return React.createElement('div', { + return React.createElement('button', { key: card.key, - role: 'button', - tabIndex: 0, - className: 'lc-alert-card lc-alert-card--' + card.type + ' lc-alert-card-clickable' + (active ? ' lc-alert-card-active' : ''), + type: 'button', + className: 'vm-kpi-card' + (active ? ' active' : ''), onClick: onClick, - onKeyDown: function (e) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onClick(); - } - }, 'aria-pressed': active, 'aria-label': card.title + ',共 ' + count + ' 个' }, - React.createElement('div', { className: 'lc-alert-card-tip-anchor' }, - React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 320 } }, - React.createElement('span', { - className: 'lc-alert-card-tip', - role: 'img', - 'aria-label': card.title + '说明', - onClick: function (e) { e.stopPropagation(); }, - onMouseDown: function (e) { e.stopPropagation(); } - }, - h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 12, y1: 16, x2: 12, y2: 12 }, { tag: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 }], 12) - ) - ) + React.createElement('span', { + className: 'vm-kpi-tip', + tabIndex: 0, + role: 'note', + 'aria-label': card.title + '说明', + onClick: function (e) { e.stopPropagation(); }, + onMouseDown: function (e) { e.stopPropagation(); } + }, + h2KpiHelpIcon(), + React.createElement('span', { className: 'vm-kpi-tooltip', role: 'tooltip' }, card.desc) ), - React.createElement('div', { className: 'lc-alert-card-icon', 'aria-hidden': true }, icon), - React.createElement('div', { className: 'lc-alert-card-main' }, - React.createElement('div', { className: 'lc-alert-card-title' }, card.title), - React.createElement('div', { className: 'lc-alert-card-val' }, count) + React.createElement('span', { className: 'vm-kpi-icon', 'aria-hidden': true }, icon), + React.createElement('span', { className: 'vm-kpi-main' }, + React.createElement('span', { className: 'vm-kpi-eyebrow' }, card.title), + React.createElement('span', { className: 'vm-kpi-val' }, count) ) ); }, []); @@ -7034,7 +7018,7 @@ const Component = function () { } } - var allCustomerOpts = h2CollectCustomerOptions(H2_MOCK_REFUEL_RECORDS); + var allCustomerOpts = h2CollectCustomerOptions(h2GetAllRefuelRecordsFromLedger()); var customerNameMap = {}; allCustomerOpts.forEach(function (opt) { customerNameMap[opt.value] = opt.label; }); @@ -8663,7 +8647,8 @@ const Component = function () { key: 'name', width: 300, fixed: 'left', - ellipsis: true, + className: 'h2-site-name-col', + onCell: function () { return { className: 'h2-site-name-col' }; }, render: function (v, r) { var addressLine = h2FormatStationAddressLine(r); return React.createElement('div', { className: 'lc-station-name-cell' }, @@ -8702,6 +8687,7 @@ const Component = function () { key: 'currentCostPrice', width: 168, align: 'right', + onHeaderCell: function () { return { style: { textAlign: 'right' } }; }, render: function (_, r) { return renderCurrentCostPriceCell(r); } @@ -8755,6 +8741,8 @@ const Component = function () { title: '联系方式', key: 'contactInfo', width: 120, + className: 'h2-site-contact-col', + onCell: function () { return { className: 'h2-site-contact-col' }; }, render: function (_, r) { var mobile = r.mobilePhone || r.phone || ''; var landline = r.landlinePhone || ''; @@ -8773,10 +8761,9 @@ const Component = function () { fixed: 'right', render: function (_, record) { return React.createElement('div', { className: 'h2-row-actions' }, - React.createElement(Button, { - type: 'link', - size: 'small', - className: 'lc-action-btn', + React.createElement('button', { + type: 'button', + className: 'vm-link', onClick: function () { openView(record); } }, '查看'), React.createElement(Dropdown, { @@ -8799,6 +8786,10 @@ const Component = function () { } ]; + var listTableScrollX = columns.reduce(function (sum, col) { + return sum + (Number(col.width) || 0); + }, 0); + var formItem = function (label, required, node, extra) { return React.createElement(Form.Item, { label: required ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) : label, @@ -8898,7 +8889,7 @@ const Component = function () { var priceConfigs = priceModal.priceConfigs || []; var priceType = priceModal.priceType || 'fixed'; var assignedCustomerIds = h2GetAssignedCustomerIds(priceConfigs, priceModal.editingConfigId); - var allCustomerOptions = h2CollectCustomerOptions(H2_MOCK_REFUEL_RECORDS); + var allCustomerOptions = h2CollectCustomerOptions(h2GetAllRefuelRecordsFromLedger()); var selectedCustomerIds = priceModal.selectedCustomerIds || []; var enabledTierCustomerOptions = allCustomerOptions.filter(function (opt) { return !assignedCustomerIds[opt.value]; @@ -9468,41 +9459,29 @@ const Component = function () { return React.createElement('div', { className: 'h2-station-page lc-edit-page' + + (subView === 'list' ? ' h2-station-page--list' : '') + (subView === 'create' || subView === 'edit' ? ' h2-station-page--create' : '') }, React.createElement('style', null, H2_PAGE_STYLE), createViewNode, editViewNode, - subView === 'list' ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } }, - React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 } }, - React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '站点信息' }] }), - React.createElement(Space, { size: 8 }, - React.createElement(Button, { - type: 'default', - icon: H2_ICONS.doc, - style: H2_REQ_BTN_STYLE, - onClick: function () { setUserManualModalOpen(true); }, - 'aria-label': '查看使用说明书' - }, '查看使用说明书'), - React.createElement(Button, { - type: 'default', - icon: H2_ICONS.doc, - style: H2_REQ_BTN_STYLE, - onClick: function () { setRequirementModalOpen(true); }, - 'aria-label': '查看需求说明' - }, '查看需求说明') - ) - ), - - React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, - React.createElement('div', { className: 'lc-filter-grid' }, + subView === 'list' ? React.createElement('div', { className: 'vm-page lc-page h2-site-page h2-site-list-shell' }, + React.createElement('section', { + className: 'vm-filter-card', + 'data-annotation-id': 'h2-site-filter', + 'aria-label': '筛选条件' + }, + React.createElement('header', { className: 'vm-filter-header' }, + React.createElement('h2', { className: 'vm-filter-title' }, '筛选条件') + ), + React.createElement('div', { className: 'vm-filter-grid' }, renderFilterField('加氢站名称', React.createElement(Input, { placeholder: '请输入加氢站名称', value: listFilters.name, onChange: function (e) { setListFilters(function (p) { return Object.assign({}, p, { name: e.target.value }); }); }, onPressEnter: handleListFilterQuery, allowClear: true, - style: { width: '100%', height: 32, borderRadius: 8 } + style: { width: '100%' } })), renderFilterField('是否签约', React.createElement(Select, { placeholder: '全部', @@ -9510,8 +9489,7 @@ const Component = function () { value: listFilters.signed, onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { signed: v }); }); }, options: [{ value: 'yes', label: '签约站点' }, { value: 'no', label: '普通站点' }], - style: { width: '100%' }, - dropdownStyle: { borderRadius: 8 } + style: { width: '100%' } })), renderFilterField('地区(省-市)', React.createElement(Cascader, { options: H2_REGION_CASCADER_OPTIONS, @@ -9519,7 +9497,7 @@ const Component = function () { onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { region: v || undefined }); }); }, placeholder: '请选择省 / 市', allowClear: true, - style: { width: '100%', borderRadius: 8 }, + style: { width: '100%' }, changeOnSelect: true })), renderFilterField('营业状态', React.createElement(Select, { @@ -9528,25 +9506,41 @@ const Component = function () { value: listFilters.businessStatus, onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { businessStatus: v }); }); }, options: H2_BUSINESS_STATUS_OPTIONS, - style: { width: '100%' }, - dropdownStyle: { borderRadius: 8 } + style: { width: '100%' } })) ), - React.createElement('div', { className: 'lc-filter-actions' }, - React.createElement(Button, { onClick: handleListFilterReset, style: { borderRadius: 8 } }, '重置'), - React.createElement(Button, { type: 'primary', onClick: handleListFilterQuery, style: H2_PRIMARY_BTN_STYLE }, '查询') + React.createElement('div', { className: 'vm-filter-actions' }, + React.createElement('button', { type: 'button', className: 'vm-btn vm-btn-ghost', onClick: handleListFilterReset }, '重置'), + React.createElement('button', { type: 'button', className: 'vm-btn vm-btn-primary', onClick: handleListFilterQuery }, + React.createElement('svg', { + xmlns: 'http://www.w3.org/2000/svg', + width: 16, + height: 16, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, + React.createElement('circle', { cx: 11, cy: 11, r: 8 }), + React.createElement('path', { d: 'm21 21-4.3-4.3' }) + ), + '查询' + ) ) ), - React.createElement('div', { className: 'lc-alert-stats-row' }, - renderAlertStatCard(H2_STATION_KPI_CARDS[0], { + React.createElement('div', { className: 'vm-kpi-row', role: 'group', 'aria-label': '站点统计', 'data-annotation-id': 'h2-site-kpi' }, + renderVmKpiCard(H2_STATION_KPI_CARDS[0], { active: categoryTab === 'all' && !appliedFilters.signed, count: categoryCounts.all || 0, onClick: function () { handleKpiCardClick('all'); }, icon: h2KpiCardIcon('all') }), H2_SIGNED_FILTER_CARDS.map(function (card) { - return renderAlertStatCard(card, { + return renderVmKpiCard(card, { active: appliedFilters.signed === card.key, count: card.key === 'yes' ? signedStats.signed : signedStats.unsigned, onClick: function () { handleSignedFilterCardClick(card.key); }, @@ -9554,65 +9548,63 @@ const Component = function () { }); }), H2_STATION_KPI_CARDS.slice(1).map(function (card) { - var isAlertKpi = card.key === 'balanceAlert' || card.key === 'arrears'; - return renderAlertStatCard(card, { - active: isAlertKpi - ? (alertStationModal.open && alertStationModal.type === card.key) - : (categoryTab === card.key && !appliedFilters.signed), + return renderVmKpiCard(card, { + active: categoryTab === card.key && !appliedFilters.signed, count: categoryCounts[card.key] || 0, - onClick: function () { - if (isAlertKpi) openAlertStationModal(card.key); - else handleKpiCardClick(card.key); - }, + onClick: function () { handleKpiCardClick(card.key); }, icon: h2KpiIcon(card.key) }); }) ), - React.createElement('div', { className: 'lc-table-section' }, - React.createElement('div', { className: 'lc-table-toolbar' }, - React.createElement('div', { className: 'lc-table-toolbar-actions' }, - React.createElement(Button, { - type: 'default', - icon: H2_ICONS.none, - style: { borderRadius: 8, fontWeight: 600, borderColor: '#f59e0b', color: '#d97706' }, + React.createElement('section', { className: 'vm-table-section' }, + React.createElement('div', { className: 'vm-table-toolbar', 'data-annotation-id': 'h2-site-toolbar' }, + React.createElement('div', { className: 'vm-table-actions' }, + React.createElement('button', { + type: 'button', + className: 'vm-btn vm-btn-ghost', onClick: openGlobalBalanceAlertSetting, 'aria-label': '全站余额提醒设置' }, '全站余额提醒设置'), - React.createElement(Button, { - type: 'default', - icon: H2_ICONS.upload, - style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + React.createElement('button', { + type: 'button', + className: 'vm-btn vm-btn-ghost', onClick: function () { setImportModalOpen(true); }, 'aria-label': '批量导入加氢站点' }, '批量导入'), - React.createElement(Button, { - type: 'default', - icon: H2_ICONS.doc, - style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + React.createElement('button', { + type: 'button', + className: 'vm-btn vm-btn-ghost', onClick: function () { openRechargeModal(); }, 'aria-label': '发起充值单' }, '发起充值单'), - React.createElement(Button, { - type: 'primary', - style: H2_PRIMARY_BTN_STYLE, + React.createElement('button', { + type: 'button', + className: 'vm-btn vm-btn-primary', onClick: openCreate, 'aria-label': '新建加氢站点' }, '新建站点') ) ), - React.createElement('div', { className: 'lc-table-card' }, + React.createElement('div', { className: 'vm-table-card', 'data-annotation-id': 'h2-site-table' }, + React.createElement('div', { + className: 'vm-table-wrap h2-site-table-wrap', + style: { '--h2-table-scroll-x': listTableScrollX + 'px' } + }, React.createElement(Table, { - className: 'lc-list-table', + className: 'vm-list-table h2-site-list-table', rowKey: 'id', columns: columns, dataSource: displayList, - pagination: tablePagination, - size: 'middle', - scroll: { x: 1290 }, - locale: { emptyText: emptyNode }, - onChange: handleListTableChange - }) + pagination: false, + size: 'small', + tableLayout: 'fixed', + scroll: { x: listTableScrollX, y: 'calc(100vh - 640px)' }, + locale: { emptyText: emptyNode }, + onChange: handleListTableChange + }) + ), + React.createElement('div', { className: 'vm-table-footer' }, renderH2TablePagination()) ) ) ) : null, @@ -9634,30 +9626,6 @@ const Component = function () { cancelText: '取消' }, '确定删除站点「' + ((deleteModal.record && deleteModal.record.name) || '') + '」吗?此操作不可撤销。'), - React.createElement(Modal, { - className: 'h2-prd-modal h2-user-manual-modal', - title: React.createElement('span', { style: { fontWeight: 700 } }, '使用说明书 · 站点信息'), - open: userManualModalOpen, - onCancel: function () { setUserManualModalOpen(false); }, - width: 900, - centered: true, - destroyOnClose: true, - styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, - footer: React.createElement(Button, { onClick: function () { setUserManualModalOpen(false); }, style: { borderRadius: 8 } }, '关闭') - }, renderH2StationUserManualDocPanel()), - - React.createElement(Modal, { - className: 'h2-prd-modal', - title: React.createElement('span', { style: { fontWeight: 700 } }, '需求说明 · 站点信息'), - open: requirementModalOpen, - onCancel: function () { setRequirementModalOpen(false); }, - width: 840, - centered: true, - destroyOnClose: true, - styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, - footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); }, style: { borderRadius: 8 } }, '关闭') - }, renderH2StationRequirementDocPanel()), - React.createElement(Modal, { title: '批量导入加氢站点', open: importModalOpen, diff --git a/src/prototypes/oneos-web-h2-station/styles/h2-station-vm-filter.css b/src/prototypes/oneos-web-h2-station/styles/h2-station-vm-filter.css new file mode 100644 index 0000000..31e6a8f --- /dev/null +++ b/src/prototypes/oneos-web-h2-station/styles/h2-station-vm-filter.css @@ -0,0 +1,254 @@ +/** + * 加氢站站点信息 — 与租赁合同管理 vm 布局 / 字体对齐 + */ + +.vm-page.lc-page.h2-site-page { + font-family: var(--vm-font); + font-size: 0.875rem; + line-height: 1.5; + letter-spacing: -0.01em; + color: var(--ln-body); + -webkit-font-smoothing: antialiased; +} + +.vm-page.lc-page.h2-site-page .vm-filter-title { + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.02em; + color: var(--ln-ink); +} + +.vm-page.lc-page.h2-site-page .vm-filter-field > span { + font-size: 0.75rem; + font-weight: 500; + color: var(--ln-muted); +} + +.vm-page.lc-page.h2-site-page .vm-btn { + font-family: var(--vm-font); + font-size: 0.875rem; + font-weight: 500; +} + +.vm-page.lc-page.h2-site-page .vm-kpi-eyebrow { + font-size: 0.75rem; + font-weight: 500; + color: var(--ln-muted); +} + +.vm-page.lc-page.h2-site-page .vm-kpi-val { + font-size: 1.375rem; + font-weight: 600; + letter-spacing: -0.03em; + font-variant-numeric: tabular-nums; + color: var(--ln-ink); +} + +.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-thead > tr > th { + font-size: 0.75rem !important; + font-weight: 500 !important; + color: var(--ln-muted) !important; +} + +.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { + font-size: 0.8125rem; + color: var(--ln-body); +} + +.vm-page.lc-page.h2-site-page .vm-list-table .vm-link { + font-size: 0.8125rem; + font-weight: 600; +} + +.vm-page.lc-page.h2-site-page .vm-table-total, +.vm-page.lc-page.h2-site-page .vm-pagination-size, +.vm-page.lc-page.h2-site-page .vm-pagination-nav, +.vm-page.lc-page.h2-site-page .vm-pagination-page, +.vm-page.lc-page.h2-site-page .vm-pagination-select { + font-family: var(--vm-font); + font-size: 0.875rem; +} + +.vm-page.lc-page.h2-site-page .vm-table-total { + color: var(--ln-muted); +} + +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-select, +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-picker, +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-cascader { + width: 100%; +} + +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-select .ant-select-selector, +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-picker, +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-cascader .ant-select-selector, +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input, +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input-affix-wrapper { + min-height: var(--vm-control-height) !important; + border-radius: var(--ln-radius-control) !important; + border-color: var(--ln-hairline-strong) !important; + font-family: var(--vm-font); + font-size: 0.875rem; +} + +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-select .ant-select-selector, +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-cascader .ant-select-selector { + align-items: center; +} + +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input-affix-wrapper { + display: inline-flex; + align-items: center; + padding-top: 0; + padding-bottom: 0; +} + +.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input-affix-wrapper > input.ant-input { + height: calc(var(--vm-control-height) - 2px); + line-height: calc(var(--vm-control-height) - 2px); +} + +.vm-page.lc-page.h2-site-page .vm-filter-card { + margin-bottom: 16px; +} + +.vm-page.lc-page.h2-site-page .vm-kpi-row { + grid-template-columns: repeat(6, minmax(0, 1fr)); + margin-bottom: 16px; +} + +@media (max-width: 1400px) { + .vm-page.lc-page.h2-site-page .vm-kpi-row { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 960px) { + .vm-page.lc-page.h2-site-page .vm-kpi-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .vm-page.lc-page.h2-site-page .vm-kpi-row { + grid-template-columns: 1fr; + } +} + +.vm-page.lc-page.h2-site-page .vm-table-section { + min-width: 0; + max-width: 100%; + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.vm-page.lc-page.h2-site-page .vm-table-card > .vm-table-wrap { + overflow: hidden; + max-height: none; + min-width: 0; + flex: 1; + min-height: 0; +} + +.vm-page.lc-page.h2-site-page .vm-table-card { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.vm-page.lc-page.h2-site-page .h2-site-table-wrap { + --h2-table-scroll-x: 1624px; +} + +/* 锁定表头/表体同宽,避免容器宽于列宽合计时表体被拉伸导致错位(租赁合同 scroll.x 大于容器则无此问题) */ +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-header > table, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-body > table { + width: var(--h2-table-scroll-x) !important; + min-width: var(--h2-table-scroll-x) !important; + max-width: var(--h2-table-scroll-x) !important; + table-layout: fixed !important; +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-content { + overflow-x: auto !important; +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-scrollbar { + width: 0 !important; + min-width: 0 !important; + max-width: 0 !important; + padding: 0 !important; + border: none !important; +} + +.vm-page.lc-page.h2-site-page .vm-table-section .vm-table-toolbar { + display: flex; + justify-content: flex-end; + align-items: center; + min-height: var(--vm-btn-height); + padding: 0 0 16px; + margin-bottom: 0; +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell { + vertical-align: middle; +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table .h2-site-name-col, +.vm-page.lc-page.h2-site-page .h2-site-list-table .h2-site-contact-col { + white-space: normal !important; +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-left, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-start, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-right, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-end { + background: var(--ln-surface-card); +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-start, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-end { + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-left, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-start, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-right, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-end { + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance { + overflow: hidden; +} + +.vm-page.lc-page.h2-site-page .h2-site-list-table.ant-table-wrapper, +.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-wrapper { + min-width: 0; + max-width: 100%; +} + +.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance { + overflow: visible !important; +} + +.h2-station-page.h2-station-page--list { + padding: 0; + height: 100vh; + background: transparent; + overflow: hidden; +} + +.vm-page.lc-page.h2-site-page.h2-site-list-shell { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; + box-sizing: border-box; +} diff --git a/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx b/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx index 2738ff1..a4e08e6 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx +++ b/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx @@ -2962,10 +2962,25 @@ const Component = function () { setAllRows(function (prev) { return recalcAllRows(applyStatementPatchesToRows(prev, patches)); }); + if (window.H2_VEHICLE_LEDGER_STORE) { + window.H2_VEHICLE_LEDGER_STORE.applyPatches(patches); + } + }, + getRows: function () { + return allRows; + }, + syncRows: function (rows) { + if (!rows) return; + setAllRows(recalcAllRows(rows)); } }; } + React.useEffect(function () { + if (typeof window === 'undefined' || !window.H2_VEHICLE_LEDGER_STORE) return; + window.H2_VEHICLE_LEDGER_STORE.setRows(allRows); + }, [allRows]); + var dateRangeDraftState = useState(null); var dateRangeDraft = dateRangeDraftState[0]; var setDateRangeDraft = dateRangeDraftState[1]; diff --git a/src/prototypes/vehicle-h2-fee-ledger/globals.ts b/src/prototypes/vehicle-h2-fee-ledger/globals.ts index 11a1af2..7f5947b 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/globals.ts +++ b/src/prototypes/vehicle-h2-fee-ledger/globals.ts @@ -1,6 +1,7 @@ import React from 'react'; import * as antd from 'antd'; import dayjs from 'dayjs'; +import '../../common/h2VehicleLedgerBridge.js'; declare global { interface Window { diff --git a/src/prototypes/vehicle-management-clay/annotation-source.json b/src/prototypes/vehicle-management-clay/annotation-source.json deleted file mode 100644 index 2cc307d..0000000 --- a/src/prototypes/vehicle-management-clay/annotation-source.json +++ /dev/null @@ -1,1117 +0,0 @@ -{ - "documentVersion": 1, - "format": "axhub-annotation-source", - "data": { - "version": 2, - "prototypeName": "vehicle-management-clay", - "pageId": "list", - "updatedAt": 1782871095272, - "nodes": [ - { - "id": "vm-doc-toolbar", - "index": 1, - "title": "测试用例下载", - "pageId": [ - "list", - "detail" - ], - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-doc-toolbar\"]" - ], - "fingerprint": "vm-doc-toolbar", - "path": [] - }, - "aiPrompt": "右上角测试用例下载工具栏。", - "annotationText": "下载 Excel 测试用例。", - "hasMarkdown": true, - "color": "#64748b", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-filter", - "index": 2, - "title": "筛选条件", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-filter\"]" - ], - "fingerprint": "vm-filter", - "path": [] - }, - "aiPrompt": "列表筛选区字段与查询逻辑。", - "annotationText": "", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-all", - "index": 3, - "title": "所有营运车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-all\"]" - ], - "fingerprint": "vm-kpi-all", - "path": [] - }, - "aiPrompt": "KPI 口径与点击筛选。", - "annotationText": "未退出运营、可用于交付的车辆总数。点击切换列表筛选。", - "hasMarkdown": true, - "color": "#64748b", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-lease", - "index": 4, - "title": "租赁车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-lease\"]" - ], - "fingerprint": "vm-kpi-lease", - "path": [] - }, - "aiPrompt": "租赁运营状态 KPI。", - "annotationText": "运营状态为「租赁」且已交付租赁业务项目的车辆。", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-self", - "index": 5, - "title": "自营车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-self\"]" - ], - "fingerprint": "vm-kpi-self", - "path": [] - }, - "aiPrompt": "自营运营状态 KPI。", - "annotationText": "运营状态为「自营」且已交付自营业务项目的车辆。", - "hasMarkdown": true, - "color": "#059669", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-stock", - "index": 6, - "title": "库存车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-stock\"]" - ], - "fingerprint": "vm-kpi-stock", - "path": [] - }, - "aiPrompt": "在库车辆 KPI。", - "annotationText": "运营状态为「可运营」或「待运营」的在库车辆。", - "hasMarkdown": true, - "color": "#d97706", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-nonOperating", - "index": 7, - "title": "非运营车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-nonOperating\"]" - ], - "fingerprint": "vm-kpi-nonOperating", - "path": [] - }, - "aiPrompt": "非运营车辆 KPI。", - "annotationText": "公务车、观光车等不可用于租赁或自营业务的车辆。", - "hasMarkdown": true, - "color": "#7c3aed", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-exit", - "index": 8, - "title": "退出运营车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-exit\"]" - ], - "fingerprint": "vm-kpi-exit", - "path": [] - }, - "aiPrompt": "退出运营 KPI。", - "annotationText": "已退出运营,不可再用于租赁或自营业务。", - "hasMarkdown": true, - "color": "#64748b", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-licenseAbnormal", - "index": 9, - "title": "证照异常车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-licenseAbnormal\"]" - ], - "fingerprint": "vm-kpi-licenseAbnormal", - "path": [] - }, - "aiPrompt": "证照异常与交车限制。", - "annotationText": "证照状态异常;须将行驶证有效期更新至未过期才可交车。", - "hasMarkdown": true, - "color": "#dc2626", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-kpi-insuranceAbnormal", - "index": 10, - "title": "保险异常车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-kpi-insuranceAbnormal\"]" - ], - "fingerprint": "vm-kpi-insuranceAbnormal", - "path": [] - }, - "aiPrompt": "保险异常与交车限制。", - "annotationText": "交强险或商业险已到期;完成采购前不可用于租赁/自营交车。", - "hasMarkdown": true, - "color": "#dc2626", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-table-toolbar", - "index": 11, - "title": "导出与批量导入", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-table-toolbar\"]" - ], - "fingerprint": "vm-table-toolbar", - "path": [] - }, - "aiPrompt": "列表工具栏操作。", - "annotationText": "导出当前筛选结果;批量导入按模板校验(原型为 Toast 演示)。", - "hasMarkdown": true, - "color": "#0f766e", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-vehicle-table", - "index": 12, - "title": "台账列表", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-vehicle-table\"]" - ], - "fingerprint": "vm-vehicle-table", - "path": [] - }, - "aiPrompt": "车辆台账表格列与操作。", - "annotationText": "", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-import-modal", - "index": 13, - "title": "批量导入", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-import-modal\"]" - ], - "fingerprint": "vm-import-modal", - "path": [] - }, - "aiPrompt": "CSV 模板下载与上传。", - "annotationText": "", - "hasMarkdown": true, - "color": "#0f766e", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-edit-modal", - "index": 14, - "title": "编辑车辆", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-edit-modal\"]" - ], - "fingerprint": "vm-edit-modal", - "path": [] - }, - "aiPrompt": "管理员编辑车辆档案。", - "annotationText": "管理员通过操作列「更多 → 编辑」打开。", - "hasMarkdown": true, - "color": "#7c3aed", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-ops-modal", - "index": 15, - "title": "运维负责人", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-ops-modal\"]" - ], - "fingerprint": "vm-ops-modal", - "path": [] - }, - "aiPrompt": "列表页运维负责人弹窗。", - "annotationText": "", - "hasMarkdown": true, - "color": "#d97706", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-operate-city-modal", - "index": 16, - "title": "修改运营城市", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-operate-city-modal\"]" - ], - "fingerprint": "vm-operate-city-modal", - "path": [] - }, - "aiPrompt": "列表页运营城市弹窗。", - "annotationText": "仅来源为「人工」时可修改。", - "hasMarkdown": true, - "color": "#0891b2", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-location-map-modal", - "index": 17, - "title": "车辆定位地图", - "pageId": "list", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-location-map-modal\"]" - ], - "fingerprint": "vm-location-map-modal", - "path": [] - }, - "aiPrompt": "GPS 地图弹窗。", - "annotationText": "点击「当前位置」打开。", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-hero", - "index": 18, - "title": "车辆概要", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-hero\"]" - ], - "fingerprint": "vm-detail-hero", - "path": [] - }, - "aiPrompt": "详情页头部信息。", - "annotationText": "车牌、品牌型号、VIN、在线状态、运营城市与 GPS 时间。", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-stats", - "index": 19, - "title": "统计条", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-stats\"]" - ], - "fingerprint": "vm-detail-stats", - "path": [] - }, - "aiPrompt": "运营城市、里程、停放区域、运维负责人。", - "annotationText": "", - "hasMarkdown": true, - "color": "#0f766e", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tabs", - "index": 20, - "title": "Tab 导航", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tabs\"]" - ], - "fingerprint": "vm-detail-tabs", - "path": [] - }, - "aiPrompt": "详情页 11 个 Tab。", - "annotationText": "", - "hasMarkdown": true, - "color": "#64748b", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-basic", - "index": 21, - "title": "基本信息 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-basic\"]" - ], - "fingerprint": "vm-detail-tab-basic", - "path": [] - }, - "aiPrompt": "档案、项目、证照摘要。", - "annotationText": "", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-model", - "index": 22, - "title": "型号参数 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-model\"]" - ], - "fingerprint": "vm-detail-tab-model", - "path": [] - }, - "aiPrompt": "车型参数与保养项目。", - "annotationText": "展示车型技术参数表与保养项目清单(只读)。", - "hasMarkdown": true, - "color": "#64748b", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-license", - "index": 23, - "title": "证照信息 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-license\"]" - ], - "fingerprint": "vm-detail-tab-license", - "path": [] - }, - "aiPrompt": "行驶证与营运证展示。", - "annotationText": "分组展示证照字段与照片预览。", - "hasMarkdown": true, - "color": "#d97706", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-insurance", - "index": 24, - "title": "保险记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-insurance\"]" - ], - "fingerprint": "vm-detail-tab-insurance", - "path": [] - }, - "aiPrompt": "险种概览与采购历史。", - "annotationText": "", - "hasMarkdown": true, - "color": "#059669", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-lease", - "index": 25, - "title": "租赁记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-lease\"]" - ], - "fingerprint": "vm-detail-tab-lease", - "path": [] - }, - "aiPrompt": "合同与交还车历史。", - "annotationText": "合同号、交车/还车日期可跳转对应管理模块(原型 Toast)。", - "hasMarkdown": true, - "color": "#2563eb", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-accident", - "index": 26, - "title": "事故记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-accident\"]" - ], - "fingerprint": "vm-detail-tab-accident", - "path": [] - }, - "aiPrompt": "事故记录筛选与列表。", - "annotationText": "按车牌过滤种子数据;可跳转事故详情(原型演示)。", - "hasMarkdown": true, - "color": "#dc2626", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-fault", - "index": 27, - "title": "故障记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-fault\"]" - ], - "fingerprint": "vm-detail-tab-fault", - "path": [] - }, - "aiPrompt": "故障记录筛选与操作。", - "annotationText": "支持查看详情与编辑故障记录(原型 Toast)。", - "hasMarkdown": true, - "color": "#d97706", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-violation", - "index": 28, - "title": "违章记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-violation\"]" - ], - "fingerprint": "vm-detail-tab-violation", - "path": [] - }, - "aiPrompt": "违章记录列表。", - "annotationText": "按当前车辆过滤;可跳转违章管理详情(原型演示)。", - "hasMarkdown": true, - "color": "#7c3aed", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-movement", - "index": 29, - "title": "异动记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-movement\"]" - ], - "fingerprint": "vm-detail-tab-movement", - "path": [] - }, - "aiPrompt": "异动记录列表。", - "annotationText": "展示车辆异动历史;可跳转异动管理详情(原型演示)。", - "hasMarkdown": true, - "color": "#0891b2", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-transfer", - "index": 30, - "title": "调拨记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-transfer\"]" - ], - "fingerprint": "vm-detail-tab-transfer", - "path": [] - }, - "aiPrompt": "调拨记录列表。", - "annotationText": "展示车辆调拨历史;可跳转调拨详情(原型演示)。", - "hasMarkdown": true, - "color": "#0f766e", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - }, - { - "id": "vm-detail-tab-annual", - "index": 31, - "title": "年审记录 Tab", - "pageId": "detail", - "locator": { - "selectors": [ - "[data-annotation-id=\"vm-detail-tab-annual\"]" - ], - "fingerprint": "vm-detail-tab-annual", - "path": [] - }, - "aiPrompt": "年审历史列表。", - "annotationText": "对齐年审管理历史列表;可跳转年审详情(原型演示)。", - "hasMarkdown": true, - "color": "#0891b2", - "images": [], - "createdAt": 1740096000000, - "updatedAt": 1740096000000 - } - ] - }, - "markdownMap": { - "vm-doc-toolbar": "## 下载测试用例\n\n右上角工具栏,列表页与详情页均可用;下载含列表/详情两个 Sheet 的 Excel。", - "vm-filter": "## 4. 筛选区\n\n### 4.1 字段清单\n\n默认展示前 4 项,点击「展开更多筛选项」显示全部。\n\n| 字段 | 控件 | 选项来源 | 匹配规则 |\n|---|---|---|---|\n| 车牌号 | 多行弹层输入 | 用户输入 | 确认后精确匹配(归一化:去空格、大写) |\n| 运营城市 | 省-市级联多选 | 种子 + 停车场映射 + 台账 location | 与车辆运营城市(省-市格式)包含关系 |\n| 品牌/型号/客户/部门/项目/合同/所有权/运营公司/租赁公司/停车场 | 可搜索单选 | 当前台账去重排序 | 精确相等 |\n| 项目类型 | 可搜索单选 | 租赁、自营 | 与创建合同时合同类型相等 |\n| 运营状态 | 可搜索单选 | 租赁、自营、可运营、待运营、退出运营 | 精确相等 |\n| 车辆来源 | 可搜索单选 | 自有、外租、挂靠 | 精确相等 |\n\n### 4.2 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入「已应用条件」,列表与 KPI 计数基于全量台账;页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n筛选与 KPI Tab **叠加**:先按 KPI 分类过滤,再按筛选条件过滤。\n\n\n---\n\n### 原型实现补充\n\n- **区域**:华东 / 华南 / 华中 / 华北 / 东北 / 西南 / 西北;按运营城市所在省份映射过滤", - "vm-kpi-all": "## 当前卡片口径\n\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-kpi-lease": "## 当前卡片口径\n\n| 租赁车辆 | `operateStatus === '租赁'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-kpi-self": "## 当前卡片口径\n\n| 自营车辆 | `operateStatus === '自营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-kpi-stock": "## 当前卡片口径\n\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-kpi-nonOperating": "## 当前卡片口径\n\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-kpi-exit": "## 当前卡片口径\n\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-kpi-licenseAbnormal": "## 当前卡片口径\n\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-kpi-insuranceAbnormal": "## 当前卡片口径\n\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", - "vm-table-toolbar": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---", - "vm-vehicle-table": "## 7. 列表字段说明\n\n### 7.1 车辆信息(冻结列)\n\n- 车牌:链接色,点击进入**车辆详情页**\n- VIN、品牌·型号:只读展示\n\n### 7.2 运营城市\n\n- 展示:省-市(由 `location` 解析)\n- 标签:车机 / GPS / 人工(来源 `operateCitySource` 或推导)\n- **可编辑**:仅来源为「人工」时可点击修改 → 运营城市修改弹窗\n- 车机/GPS 来源:不可改,hover 提示原因\n\n### 7.3 所属项目\n\n仅**租赁/自营**车辆展示项目名称、合同编码、客户名称;否则三行均为空。\n\n- 项目名称、合同编码:有值时可点击 → Toast 跳转合同详情(原型演示)\n\n### 7.4 里程数\n\n- 在线车辆:显示里程 + 「车机」标签,里程按车机数据格式化\n- 离线:仅里程或空\n\n### 7.5 证照 / 保险\n\n| 列 | 有数据 | 无数据/异常 |\n|---|---|---|\n| 检验有效期 | 到期日 + 剩余/已过期天数标签 | 「未上传」可点击 + 「禁止交车」标签 |\n| 保险状态 | 正常/异常 Tag;异常时 hover 展示具体到期险种 | — |\n| 交强险/商业险到期 | 到期日 + 剩余天数 | 「未购买」可点击 + 「禁止交车」 |\n\n未上传/未购买点击 → Toast 跳转证照管理或保险采购(原型演示)。\n\n### 7.6 上次交车 / 还车情况\n\n三行纯数值(无行内标题):里程(km)、人员、时间(至分钟)。**有数据时**可点击。\n\n| 列 | 点击行为 |\n|---|---|\n| 上次交车情况 | Toast:`即将跳转至交车管理(原型演示):{车牌}` |\n| 上次还车情况 | Toast:`即将跳转至还车管理(原型演示):{车牌}` |\n\n### 7.7 三方租赁公司\n\n仅外租车辆显示 `leaseCompany`,否则空白。\n\n### 7.8 当前位置\n\n- 在线状态圆点 + 文案\n- 详细地址:有 GPS 坐标时可点击 → 地图弹窗(OpenStreetMap 嵌入)\n- GPS 时间:精确到秒\n\n### 7.9 运维负责人\n\n可编辑时显示为链接;无负责人显示「设置负责人」→ 运维负责人弹窗。\n\n### 7.10 操作列(冻结)\n\n| 操作 | 条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 进入车辆详情页 |\n| 更多 | 管理员或有运维权限 | 下拉:编辑(管理员)、修改运维负责人 |\n\n---", - "vm-import-modal": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---", - "vm-edit-modal": "### 8.3 编辑车辆(管理员)\n\n- 可搜索选择器字段与筛选区一致规范\n- 保存时运营状态、车辆状态仍为必填(按钮禁用),但标签无红色 *\n- Toast「车辆信息已更新」", - "vm-ops-modal": "### 8.1 设置运维负责人\n\n- 标题下胶囊:车牌 | 运维区域(省-市)\n- 多选搜索选择器,候选人按车辆区域过滤\n- **保存**:写回 `opsManagers`,Toast「运维负责人已更新」\n- **取消**:不保存\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联编辑面板(非弹窗)", - "vm-operate-city-modal": "### 8.2 修改运营城市\n\n- 省-市选择,保存后 `location` 更新且 `operateCitySource` 置为「人工」\n- Toast「运营城市已更新」\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联选择器(非弹窗)", - "vm-location-map-modal": "### 8.4 地图\n\n展示坐标点与 GPS 时间;关闭不保存。\n\n---", - "vm-detail-hero": "### 2.1 顶栏\n\n| 元素 | 行为 |\n|---|---|\n| 返回列表 | 回到列表页,保留列表筛选/KPI/分页状态 |\n| 查看需求说明 | 打开详情页 PRD(本文档) |\n| PRD文档MD文件下载 | 下载列表页、详情页两份 Markdown PRD |\n| 下载测试用例 | 下载 Excel,含列表页与详情页测试用例两个 Sheet |\n\n---\n\n### 2.2 车辆概要区\n\n- 车牌号、品牌·型号、VIN\n- 在线状态(在线/离线圆点)\n- 运营城市文案、GPS 时间\n\n---\n\n### 原型实现补充\n\n- 顶栏仅保留「返回列表」与「下载测试用例」", - "vm-detail-stats": "### 2.3 统计条(4 项)\n\n| 字段 | 可编辑 | 行为 |\n|---|---|---|\n| 运营城市 | 仅来源为「人工」时可改 | 点击 → 运营城市修改弹窗;展示车机/GPS/人工来源标签 |\n| 里程数 | 否 | 在线时展示车机标签 |\n| 停放区域 | 否 | 只读 |\n| 运维负责人 | 有运维权限时可改 | 点击铅笔图标 → 运维负责人弹窗 |\n\n\n---\n\n### 原型实现补充\n\n- **运营城市**、**运维负责人**:详情页使用内联编辑面板(非弹窗)", - "vm-detail-tabs": "## 3. Tab 总览\n\n| 序号 | Tab | 说明 |\n|---|---|---|\n| 1 | 基本信息 | 档案、项目、证照摘要等,部分字段行内可编辑 |\n| 2 | 型号参数 | 车型参数与保养项目表 |\n| 3 | 证照信息 | 行驶证、营运证等分组展示与照片预览 |\n| 4 | 保险记录 | 险种概览卡片 + 采购历史表 |\n| 5 | 租赁记录 | 合同/交还车历史 |\n| 6 | 事故记录 | 筛选 + 事故列表 |\n| 7 | 故障记录 | 筛选 + 故障列表 |\n| 8 | 违章记录 | 筛选 + 违章列表 |\n| 9 | 异动记录 | 筛选 + 异动列表 |\n| 10 | 调拨记录 | 筛选 + 调拨列表 |\n| 11 | 年审记录 | 筛选 + 已完成年审历史(对齐年审管理历史列表) |\n\n各业务记录 Tab 通用规则:\n\n- 数据按当前车辆 `plateNo` 从对应 JSON 种子文件过滤\n- 筛选区:**查询**生效、**重置**清空,修改未查询不影响列表\n- 日期范围使用双月历弹层组件\n- 表格左对齐;金额/数字列使用等宽数字\n- 底栏:共 N 条 + 紧凑分页(默认 10 条/页)\n\n---", - "vm-detail-tab-basic": "## 4. 基本信息 Tab\n\n### 4.1 档案信息\n\n| 字段 | 可编辑 |\n|---|---|\n| 出厂年份 | 是(年份选择) |\n| 采购入库日期 | 是(日期选择) |\n| 运营状态、车辆来源、停车场等 | 只读展示 |\n\n保存后 Toast「车辆信息已更新」,写回内存台账。\n\n### 4.2 项目信息\n\n租赁/自营车辆展示项目名称、合同编码、客户名称;否则显示「无」。\n\n### 4.3 证照摘要\n\n- 检验有效期:展示到期日与剩余/过期标签\n- 未上传时可点击 → Toast 跳转证照管理编辑(原型演示)\n\n---", - "vm-detail-tab-model": "## 5. 型号参数 Tab\n\n- 展示车辆型号技术参数(只读)\n- 保养项目表:养护项目、保养公里/时间周期、工时费、材料费、合计\n\n---", - "vm-detail-tab-license": "## 6. 证照信息 Tab\n\n- 分组:行驶证、道路运输证、上海牌专项证件等(按车牌规则显隐)\n- 每证展示:证件号、有效期、余额(如有)、照片网格\n- 照片可点击全屏预览;证件状态色点图例\n- 数据来自 `utils/vehicleLicenses` 与车辆台账关联\n\n---", - "vm-detail-tab-insurance": "## 7. 保险记录 Tab\n\n### 7.1 险种概览卡片\n\n交强险、商业险、承运人责任险等;点击切换下方历史表。\n\n- 未购买险种卡片可点击 → Toast 跳转保险采购(原型演示)\n- 展示状态 Tag 与到期剩余天数\n\n### 7.2 采购历史表\n\n| 列 | 说明 |\n|---|---|\n| 保险导入时间、操作人、类型、保单号 | 只读 |\n| 保险状态 | Tag |\n| 保险公司、付款时间、到期日期 | 只读 |\n| 保单文件 | 有附件时「预览」「下载」可点击 → Toast 演示 |\n\n---", - "vm-detail-tab-lease": "## 8. 租赁记录 Tab\n\n### 8.1 筛选\n\n合同编码、项目名称、客户名称、业务类型。\n\n### 8.2 表格与跳转\n\n| 列 | 可点击 | 跳转目标(原型 Toast) |\n|---|---|---|\n| 合同编码 | 是 | 租赁合同详情 |\n| 项目名称 | 有值时 | 租赁合同详情 |\n| 交车日期 | 有值时 | 交车管理详情 |\n| 还车日期 | 有值时 | 还车管理详情 |\n\n---", - "vm-detail-tab-accident": "## 9. 事故记录 Tab\n\n### 9.1 筛选\n\n事故时间(日期范围)、客户名称、事故等级、事故状态。\n\n### 9.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 事故编码 | 是 | 事故管理详情页 |\n| 其余列 | 否 | — |\n\n---", - "vm-detail-tab-fault": "## 10. 故障记录 Tab\n\n### 10.1 筛选\n\n上报时间(日期范围)、故障编号、故障等级、故障类型、解决情况、AI 匹配状态。\n\n### 10.2 表格\n\n| 列/操作 | 可点击 | 行为 |\n|---|---|---|\n| 故障编号 | 是 | Toast 跳转故障管理详情 |\n| 操作「查看」 | 是 | 同上 |\n| 操作「编辑」 | 是 | Toast 编辑故障记录演示 |\n\n---", - "vm-detail-tab-violation": "## 11. 违章记录 Tab\n\n### 11.1 筛选\n\n违法时间、客户名称、缴费状态、是否处理。\n\n### 11.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 违法行为 | 是 | 违章管理详情页(按违法时间标识) |\n| 其余列 | 否 | — |\n\n---", - "vm-detail-tab-movement": "## 12. 异动记录 Tab\n\n### 12.1 筛选\n\n异动开始日期、异动目的地、异动类型。\n\n### 12.2 表格\n\n宽表展示异动起止里程/电量/氢量等;**异动开始日期**可点击 → Toast 跳转异动管理详情。\n\n---", - "vm-detail-tab-transfer": "## 13. 调拨记录 Tab\n\n### 13.1 筛选\n\n调拨日期、调出人、接收人。\n\n### 13.2 表格\n\n**调拨日期**可点击 → Toast 跳转车辆调拨详情。\n\n---", - "vm-detail-tab-annual": "## 14. 年审记录 Tab\n\n对齐「年审管理」历史列表字段(不含车牌/品牌/型号,因已在详情页上下文)。\n\n### 14.1 筛选\n\n| 字段 | 说明 |\n|---|---|\n| 完成时间 | 按办结日期范围 |\n| 检验有效期 | 按检验有效期至范围 |\n| 办理人 | 下拉选择 |\n\n### 14.2 表格列\n\n| 列 | 可点击 | 说明 |\n|---|---|---|\n| 检验有效期至 | **是** | Toast 跳转年审管理详情(原型演示) |\n| 检测服务站名称 | 否 | 无数据显示「—」 |\n| 检测费用 | 否 | 金额两位小数,无数据显示「—」 |\n| 二保服务站名称 | 否 | 同上 |\n| 二保费用 | 否 | 同上 |\n| 整备服务站名称 | 否 | 同上 |\n| 整备费用 | 否 | 同上 |\n| 办理人 | 否 | — |\n| 完成时间 | 否 | 办结时间,默认倒序 |\n\n### 14.3 数据\n\n- 来源:`data/annual-review-records.json`,按 `plateNo` 过滤\n- 样例车 `粤AGP5156` 含 3 条已完成年审记录\n\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-vehicle-management-clay", - "title": "车辆管理(Clay 备份)", - "href": "/prototypes/vehicle-management-clay", - "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": "vm-doc-root", - "title": "车辆管理说明(Clay 备份)", - "defaultExpanded": true, - "children": [ - { - "type": "markdown", - "id": "vm-doc-overview", - "title": "模块总览", - "markdown": "## Clay 主题备份\n\n| 项 | 说明 |\n|---|---|\n| 设计基底 | `src/themes/clay/DESIGN.md` |\n| 业务逻辑 | 与 [车辆管理原版](/prototypes/vehicle-management) 共用组件与数据 |\n| 视觉差异 | 奶油画布 `#fffaf0`、12px 控件圆角、24px 卡片圆角、饱和 KPI 色卡、44px 触控高度 |\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 车辆管理 — 列表页 |\n| 目标用户 | 运营、业务、运维、管理员 |\n| 核心任务 | 按多维度筛选车辆台账,查看证照/保险/交还车状态,维护运维负责人,进入车辆详情 |\n| 页面结构 | 右上角工具栏:「查看需求说明」「PRD文档MD文件下载」「下载测试用例」;筛选区 + KPI + 表格 + 分页 |\n\n---" - }, - { - "type": "link", - "id": "vm-link-self", - "title": "车辆管理 Clay 备份", - "href": "./index.html", - "target": "self" - }, - { - "type": "folder", - "id": "vm-doc-prd", - "title": "PRD 全文", - "defaultExpanded": false, - "children": [ - { - "type": "markdown", - "id": "vm-doc-prd-list", - "title": "列表页 PRD", - "markdown": "# 车辆管理 · 列表页产品需求说明(PRD)\n\n> 本页为车辆台账列表。标注「原型演示」的跳转、导入、导出仅 Toast 提示,不做真实页面跳转。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 车辆管理 — 列表页 |\n| 目标用户 | 运营、业务、运维、管理员 |\n| 核心任务 | 按多维度筛选车辆台账,查看证照/保险/交还车状态,维护运维负责人,进入车辆详情 |\n| 页面结构 | 右上角工具栏:「查看需求说明」「PRD文档MD文件下载」「下载测试用例」;筛选区 + KPI + 表格 + 分页 |\n\n---\n\n## 2. 权限与角色(原型常量)\n\n| 常量 | 原型取值 | 影响 |\n|---|---|---|\n| ADMIN | `true` | 操作列「更多」菜单显示「编辑」 |\n| 运维主管 | `true` | 可点击运维负责人、操作列「修改运维负责人」 |\n\n生产环境应由登录用户权限接口下发,原型写死便于演示。\n\n---\n\n## 3. 数据来源总览\n\n| 数据 | 来源文件/规则 | 用途 |\n|---|---|---|\n| 车辆台账 | `data/vehicles.json`(100 条样例) | 列表、筛选选项、详情、编辑回写 |\n| 保险采购 | `data/insurance-purchases.json` | 按车辆 + 险种取**最新一条**采购记录的到期日 |\n| 停车场→区域 | `PARKING_REGION_MAP` 常量 | 运维负责人候选区域、运营城市辅助 |\n| 运维人员 | `OPS_STAFF` 常量 | 按车辆区域过滤可选运维负责人 |\n| 运营城市种子 | `OPERATE_CITY_SEED` + 台账 `location` | 运营城市筛选/修改选项 |\n| GPS 坐标 | 优先 `gpsLat/gpsLng`;否则按 `location` 城市基准点 + 车辆 id 偏移模拟 | 地图弹窗 |\n| GPS 详细地址 | `locationAddress` 优先,否则 `location` 省市区 | 当前位置列 |\n| 所属项目三元组 | `projectName` / `contractNo` / `customer` | 仅运营状态为「租赁」「自营」时展示 |\n| 三方租赁公司 | `leaseCompany` | 仅 `vehicleSource === '外租'` 时展示 |\n| 交还车记录 | `lastDelivery*` / `lastReturn*` 字段 | 上次交车/还车情况列 |\n\n**空值约定**:台账中 `-`、空字符串视为无数据;列表列默认显示空白(非「—」)。\n\n---\n\n## 4. 筛选区\n\n### 4.1 字段清单\n\n默认展示前 4 项,点击「展开更多筛选项」显示全部。\n\n| 字段 | 控件 | 选项来源 | 匹配规则 |\n|---|---|---|---|\n| 车牌号 | 多行弹层输入 | 用户输入 | 确认后精确匹配(归一化:去空格、大写) |\n| 运营城市 | 省-市级联多选 | 种子 + 停车场映射 + 台账 location | 与车辆运营城市(省-市格式)包含关系 |\n| 品牌/型号/客户/部门/项目/合同/所有权/运营公司/租赁公司/停车场 | 可搜索单选 | 当前台账去重排序 | 精确相等 |\n| 项目类型 | 可搜索单选 | 租赁、自营 | 与创建合同时合同类型相等 |\n| 运营状态 | 可搜索单选 | 租赁、自营、可运营、待运营、退出运营 | 精确相等 |\n| 车辆来源 | 可搜索单选 | 自有、外租、挂靠 | 精确相等 |\n\n### 4.2 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入「已应用条件」,列表与 KPI 计数基于全量台账;页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n筛选与 KPI Tab **叠加**:先按 KPI 分类过滤,再按筛选条件过滤。\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---\n\n## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---\n\n## 7. 列表字段说明\n\n### 7.1 车辆信息(冻结列)\n\n- 车牌:链接色,点击进入**车辆详情页**\n- VIN、品牌·型号:只读展示\n\n### 7.2 运营城市\n\n- 展示:省-市(由 `location` 解析)\n- 标签:车机 / GPS / 人工(来源 `operateCitySource` 或推导)\n- **可编辑**:仅来源为「人工」时可点击修改 → 运营城市修改弹窗\n- 车机/GPS 来源:不可改,hover 提示原因\n\n### 7.3 所属项目\n\n仅**租赁/自营**车辆展示项目名称、合同编码、客户名称;否则三行均为空。\n\n- 项目名称、合同编码:有值时可点击 → Toast 跳转合同详情(原型演示)\n\n### 7.4 里程数\n\n- 在线车辆:显示里程 + 「车机」标签,里程按车机数据格式化\n- 离线:仅里程或空\n\n### 7.5 证照 / 保险\n\n| 列 | 有数据 | 无数据/异常 |\n|---|---|---|\n| 检验有效期 | 到期日 + 剩余/已过期天数标签 | 「未上传」可点击 + 「禁止交车」标签 |\n| 保险状态 | 正常/异常 Tag;异常时 hover 展示具体到期险种 | — |\n| 交强险/商业险到期 | 到期日 + 剩余天数 | 「未购买」可点击 + 「禁止交车」 |\n\n未上传/未购买点击 → Toast 跳转证照管理或保险采购(原型演示)。\n\n### 7.6 上次交车 / 还车情况\n\n三行纯数值(无行内标题):里程(km)、人员、时间(至分钟)。**有数据时**可点击。\n\n| 列 | 点击行为 |\n|---|---|\n| 上次交车情况 | Toast:`即将跳转至交车管理(原型演示):{车牌}` |\n| 上次还车情况 | Toast:`即将跳转至还车管理(原型演示):{车牌}` |\n\n### 7.7 三方租赁公司\n\n仅外租车辆显示 `leaseCompany`,否则空白。\n\n### 7.8 当前位置\n\n- 在线状态圆点 + 文案\n- 详细地址:有 GPS 坐标时可点击 → 地图弹窗(OpenStreetMap 嵌入)\n- GPS 时间:精确到秒\n\n### 7.9 运维负责人\n\n可编辑时显示为链接;无负责人显示「设置负责人」→ 运维负责人弹窗。\n\n### 7.10 操作列(冻结)\n\n| 操作 | 条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 进入车辆详情页 |\n| 更多 | 管理员或有运维权限 | 下拉:编辑(管理员)、修改运维负责人 |\n\n---\n\n## 8. 弹窗交互\n\n### 8.1 设置运维负责人\n\n- 标题下胶囊:车牌 | 运维区域(省-市)\n- 多选搜索选择器,候选人按车辆区域过滤\n- **保存**:写回 `opsManagers`,Toast「运维负责人已更新」\n- **取消**:不保存\n\n### 8.2 修改运营城市\n\n- 省-市选择,保存后 `location` 更新且 `operateCitySource` 置为「人工」\n- Toast「运营城市已更新」\n\n### 8.3 编辑车辆(管理员)\n\n- 可搜索选择器字段与筛选区一致规范\n- 保存时运营状态、车辆状态仍为必填(按钮禁用),但标签无红色 *\n- Toast「车辆信息已更新」\n\n### 8.4 地图\n\n展示坐标点与 GPS 时间;关闭不保存。\n\n---\n\n## 9. 进入详情页\n\n- 入口:列表车牌链接或操作列「查看」\n- 详情页右上角同样有「查看需求说明」,展示**详情页 PRD**(与本文档分离)\n- 从详情返回列表时,保留筛选/KPI/分页状态(内存态)\n\n---\n\n## 10. 提示信息汇总(Toast)\n\n| 触发场景 | 文案模板 |\n|---|---|\n| 导出 | 导出功能(原型演示):当前列表共 {n} 条 |\n| 导入成功 | 已接收文件 {name},导入校验为原型演示 |\n| 运维负责人保存 | 运维负责人已更新 |\n| 运营城市保存 | 运营城市已更新 |\n| 车辆编辑保存 | 车辆信息已更新 |\n| 合同跳转 | 即将跳转至合同详情(原型演示):{合同编码} |\n| 证照跳转 | 即将跳转至证照管理编辑页(原型演示):{车牌} |\n| 保险跳转 | 即将跳转至保险采购编辑页(原型演示):{车牌} · {险种} |\n| 交车跳转 | 即将跳转至交车管理(原型演示):{车牌} |\n| 还车跳转 | 即将跳转至还车管理(原型演示):{车牌} |\n\nToast 约 3 秒后自动消失。\n\n---\n\n## 11. 分页\n\n- 默认每页 10 条,可选 10 / 20 / 50 / 100\n- 切换每页条数回到第 1 页\n- 总条数为筛选后的数量\n\n---\n\n## 12. 原型局限\n\n1. 数据为前端 JSON,刷新页面后编辑结果丢失(内存态)。\n2. 导入、导出、外链跳转均为演示提示。\n3. 地图依赖外网 OpenStreetMap。\n\n---\n\n## 13. 验收要点\n\n- [ ] 筛选查询/重置与 KPI 叠加正确\n- [ ] 空值列显示空白\n- [ ] 人工运营城市可改,车机/GPS 不可改\n- [ ] 未上传/未购买可点击并提示跳转\n- [ ] 交还车有数据可点击、无数据不可点\n- [ ] 运维负责人、编辑弹窗保存后列表即时更新\n- [ ] 管理员「更多」菜单与权限开关联动\n- [ ] 右上角「查看需求说明」展示本文档(列表页 PRD)\n- [ ] 「PRD文档MD文件下载」可下载列表页、详情页两份 .md\n- [ ] 「下载测试用例」可下载含列表页/详情页 Sheet 的 Excel\n" - }, - { - "type": "markdown", - "id": "vm-doc-prd-detail", - "title": "详情页 PRD", - "markdown": "# 车辆管理 · 详情页产品需求说明(PRD)\n\n> 本页为单车档案详情。标注「原型演示」的跨模块跳转仅 Toast 提示,不做真实路由。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 车辆管理 — 详情页(查看页) |\n| 入口 | 列表页点击车牌或「查看」 |\n| 核心任务 | 查看单车档案、证照、保险、租赁及各类业务记录,维护可编辑字段 |\n| 页面结构 | 顶栏返回 + 车辆概要 + 统计条 + Tab 内容区;右上角工具栏:「查看需求说明」「PRD文档MD文件下载」「下载测试用例」 |\n\n---\n\n## 2. 页面框架\n\n### 2.1 顶栏\n\n| 元素 | 行为 |\n|---|---|\n| 返回列表 | 回到列表页,保留列表筛选/KPI/分页状态 |\n| 查看需求说明 | 打开详情页 PRD(本文档) |\n| PRD文档MD文件下载 | 下载列表页、详情页两份 Markdown PRD |\n| 下载测试用例 | 下载 Excel,含列表页与详情页测试用例两个 Sheet |\n\n### 2.2 车辆概要区\n\n- 车牌号、品牌·型号、VIN\n- 在线状态(在线/离线圆点)\n- 运营城市文案、GPS 时间\n\n### 2.3 统计条(4 项)\n\n| 字段 | 可编辑 | 行为 |\n|---|---|---|\n| 运营城市 | 仅来源为「人工」时可改 | 点击 → 运营城市修改弹窗;展示车机/GPS/人工来源标签 |\n| 里程数 | 否 | 在线时展示车机标签 |\n| 停放区域 | 否 | 只读 |\n| 运维负责人 | 有运维权限时可改 | 点击铅笔图标 → 运维负责人弹窗 |\n\n---\n\n## 3. Tab 总览\n\n| 序号 | Tab | 说明 |\n|---|---|---|\n| 1 | 基本信息 | 档案、项目、证照摘要等,部分字段行内可编辑 |\n| 2 | 型号参数 | 车型参数与保养项目表 |\n| 3 | 证照信息 | 行驶证、营运证等分组展示与照片预览 |\n| 4 | 保险记录 | 险种概览卡片 + 采购历史表 |\n| 5 | 租赁记录 | 合同/交还车历史 |\n| 6 | 事故记录 | 筛选 + 事故列表 |\n| 7 | 故障记录 | 筛选 + 故障列表 |\n| 8 | 违章记录 | 筛选 + 违章列表 |\n| 9 | 异动记录 | 筛选 + 异动列表 |\n| 10 | 调拨记录 | 筛选 + 调拨列表 |\n| 11 | 年审记录 | 筛选 + 已完成年审历史(对齐年审管理历史列表) |\n\n各业务记录 Tab 通用规则:\n\n- 数据按当前车辆 `plateNo` 从对应 JSON 种子文件过滤\n- 筛选区:**查询**生效、**重置**清空,修改未查询不影响列表\n- 日期范围使用双月历弹层组件\n- 表格左对齐;金额/数字列使用等宽数字\n- 底栏:共 N 条 + 紧凑分页(默认 10 条/页)\n\n---\n\n## 4. 基本信息 Tab\n\n### 4.1 档案信息\n\n| 字段 | 可编辑 |\n|---|---|\n| 出厂年份 | 是(年份选择) |\n| 采购入库日期 | 是(日期选择) |\n| 运营状态、车辆来源、停车场等 | 只读展示 |\n\n保存后 Toast「车辆信息已更新」,写回内存台账。\n\n### 4.2 项目信息\n\n租赁/自营车辆展示项目名称、合同编码、客户名称;否则显示「无」。\n\n### 4.3 证照摘要\n\n- 检验有效期:展示到期日与剩余/过期标签\n- 未上传时可点击 → Toast 跳转证照管理编辑(原型演示)\n\n---\n\n## 5. 型号参数 Tab\n\n- 展示车辆型号技术参数(只读)\n- 保养项目表:养护项目、保养公里/时间周期、工时费、材料费、合计\n\n---\n\n## 6. 证照信息 Tab\n\n- 分组:行驶证、道路运输证、上海牌专项证件等(按车牌规则显隐)\n- 每证展示:证件号、有效期、余额(如有)、照片网格\n- 照片可点击全屏预览;证件状态色点图例\n- 数据来自 `utils/vehicleLicenses` 与车辆台账关联\n\n---\n\n## 7. 保险记录 Tab\n\n### 7.1 险种概览卡片\n\n交强险、商业险、承运人责任险等;点击切换下方历史表。\n\n- 未购买险种卡片可点击 → Toast 跳转保险采购(原型演示)\n- 展示状态 Tag 与到期剩余天数\n\n### 7.2 采购历史表\n\n| 列 | 说明 |\n|---|---|\n| 保险导入时间、操作人、类型、保单号 | 只读 |\n| 保险状态 | Tag |\n| 保险公司、付款时间、到期日期 | 只读 |\n| 保单文件 | 有附件时「预览」「下载」可点击 → Toast 演示 |\n\n---\n\n## 8. 租赁记录 Tab\n\n### 8.1 筛选\n\n合同编码、项目名称、客户名称、业务类型。\n\n### 8.2 表格与跳转\n\n| 列 | 可点击 | 跳转目标(原型 Toast) |\n|---|---|---|\n| 合同编码 | 是 | 租赁合同详情 |\n| 项目名称 | 有值时 | 租赁合同详情 |\n| 交车日期 | 有值时 | 交车管理详情 |\n| 还车日期 | 有值时 | 还车管理详情 |\n\n---\n\n## 9. 事故记录 Tab\n\n### 9.1 筛选\n\n事故时间(日期范围)、客户名称、事故等级、事故状态。\n\n### 9.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 事故编码 | 是 | 事故管理详情页 |\n| 其余列 | 否 | — |\n\n---\n\n## 10. 故障记录 Tab\n\n### 10.1 筛选\n\n上报时间(日期范围)、故障编号、故障等级、故障类型、解决情况、AI 匹配状态。\n\n### 10.2 表格\n\n| 列/操作 | 可点击 | 行为 |\n|---|---|---|\n| 故障编号 | 是 | Toast 跳转故障管理详情 |\n| 操作「查看」 | 是 | 同上 |\n| 操作「编辑」 | 是 | Toast 编辑故障记录演示 |\n\n---\n\n## 11. 违章记录 Tab\n\n### 11.1 筛选\n\n违法时间、客户名称、缴费状态、是否处理。\n\n### 11.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 违法行为 | 是 | 违章管理详情页(按违法时间标识) |\n| 其余列 | 否 | — |\n\n---\n\n## 12. 异动记录 Tab\n\n### 12.1 筛选\n\n异动开始日期、异动目的地、异动类型。\n\n### 12.2 表格\n\n宽表展示异动起止里程/电量/氢量等;**异动开始日期**可点击 → Toast 跳转异动管理详情。\n\n---\n\n## 13. 调拨记录 Tab\n\n### 13.1 筛选\n\n调拨日期、调出人、接收人。\n\n### 13.2 表格\n\n**调拨日期**可点击 → Toast 跳转车辆调拨详情。\n\n---\n\n## 14. 年审记录 Tab\n\n对齐「年审管理」历史列表字段(不含车牌/品牌/型号,因已在详情页上下文)。\n\n### 14.1 筛选\n\n| 字段 | 说明 |\n|---|---|\n| 完成时间 | 按办结日期范围 |\n| 检验有效期 | 按检验有效期至范围 |\n| 办理人 | 下拉选择 |\n\n### 14.2 表格列\n\n| 列 | 可点击 | 说明 |\n|---|---|---|\n| 检验有效期至 | **是** | Toast 跳转年审管理详情(原型演示) |\n| 检测服务站名称 | 否 | 无数据显示「—」 |\n| 检测费用 | 否 | 金额两位小数,无数据显示「—」 |\n| 二保服务站名称 | 否 | 同上 |\n| 二保费用 | 否 | 同上 |\n| 整备服务站名称 | 否 | 同上 |\n| 整备费用 | 否 | 同上 |\n| 办理人 | 否 | — |\n| 完成时间 | 否 | 办结时间,默认倒序 |\n\n### 14.3 数据\n\n- 来源:`data/annual-review-records.json`,按 `plateNo` 过滤\n- 样例车 `粤AGP5156` 含 3 条已完成年审记录\n\n---\n\n## 15. 跨模块跳转 Toast 汇总\n\n| 场景 | 文案模板 |\n|---|---|\n| 证照编辑 | 即将跳转至证照管理编辑页(原型演示):{车牌} |\n| 保险采购 | 即将跳转至保险采购编辑页(原型演示):{车牌} · {险种} |\n| 保单预览 | 预览保单文件(原型演示):{文件名} |\n| 保单下载 | 已开始下载:{文件名}(原型演示) |\n| 租赁合同 | 即将跳转至租赁合同详情(原型演示):{车牌} · {合同编码} |\n| 交车记录 | 即将跳转至交车管理详情(原型演示):{车牌} · {合同} · {交车日期} |\n| 还车记录 | 即将跳转至还车管理详情(原型演示):{车牌} · {合同} · {还车日期} |\n| 事故详情 | 即将跳转至事故管理详情页(原型演示):{事故编码} |\n| 故障详情 | 即将跳转至故障管理详情页(原型演示):{故障编号} |\n| 故障编辑 | 编辑故障记录(原型演示):{车牌} · {故障编号} |\n| 违章详情 | 即将跳转至违章管理详情页(原型演示):{违法时间} |\n| 异动详情 | 即将跳转至异动管理详情页(原型演示):{异动开始日期} |\n| 调拨详情 | 即将跳转至车辆调拨详情页(原型演示):{调拨日期} |\n| 年审详情 | 即将跳转至年审管理详情页(原型演示):{检验有效期至} |\n| 字段保存 | 车辆信息已更新 / 运维负责人已更新 / 运营城市已更新 |\n\n---\n\n## 16. 原型局限\n\n1. 各业务记录为前端 JSON 种子,不与外部年审/事故等模块真实联动。\n2. 详情 Tab 间切换不保留各 Tab 内筛选/分页状态(刷新组件即重置)。\n3. 行内编辑与弹窗保存仅更新内存,刷新页面后丢失。\n\n---\n\n## 17. 验收要点\n\n- [ ] 返回列表保留列表态\n- [ ] 11 个 Tab 均可切换且内容正确\n- [ ] 各记录 Tab 筛选查询/重置/分页正常\n- [ ] 所有标注可点击字段触发对应 Toast\n- [ ] 年审记录含检测服务站/检测费用列,检验有效期至可点击\n- [ ] 右上角「查看需求说明」展示本文档(详情页 PRD),非列表页 PRD\n- [ ] 「PRD文档MD文件下载」「下载测试用例」在详情页可用且行为正确\n" - } - ] - }, - { - "type": "folder", - "id": "vm-doc-list", - "title": "列表页模块", - "defaultExpanded": true, - "children": [ - { - "type": "route", - "id": "vm-route-list", - "title": "打开列表页", - "route": "vehicle-management-clay:list", - "payload": { - "pageId": "list" - } - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-doc-toolbar", - "title": "测试用例下载", - "markdown": "## 下载测试用例\n\n右上角工具栏,列表页与详情页均可用;下载含列表/详情两个 Sheet 的 Excel。" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-filter", - "title": "筛选条件", - "markdown": "## 4. 筛选区\n\n### 4.1 字段清单\n\n默认展示前 4 项,点击「展开更多筛选项」显示全部。\n\n| 字段 | 控件 | 选项来源 | 匹配规则 |\n|---|---|---|---|\n| 车牌号 | 多行弹层输入 | 用户输入 | 确认后精确匹配(归一化:去空格、大写) |\n| 运营城市 | 省-市级联多选 | 种子 + 停车场映射 + 台账 location | 与车辆运营城市(省-市格式)包含关系 |\n| 品牌/型号/客户/部门/项目/合同/所有权/运营公司/租赁公司/停车场 | 可搜索单选 | 当前台账去重排序 | 精确相等 |\n| 项目类型 | 可搜索单选 | 租赁、自营 | 与创建合同时合同类型相等 |\n| 运营状态 | 可搜索单选 | 租赁、自营、可运营、待运营、退出运营 | 精确相等 |\n| 车辆来源 | 可搜索单选 | 自有、外租、挂靠 | 精确相等 |\n\n### 4.2 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入「已应用条件」,列表与 KPI 计数基于全量台账;页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n筛选与 KPI Tab **叠加**:先按 KPI 分类过滤,再按筛选条件过滤。\n\n\n---\n\n### 原型实现补充\n\n- **区域**:华东 / 华南 / 华中 / 华北 / 东北 / 西南 / 西北;按运营城市所在省份映射过滤" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-all", - "title": "所有营运车辆", - "markdown": "## 当前卡片口径\n\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-lease", - "title": "租赁车辆", - "markdown": "## 当前卡片口径\n\n| 租赁车辆 | `operateStatus === '租赁'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-self", - "title": "自营车辆", - "markdown": "## 当前卡片口径\n\n| 自营车辆 | `operateStatus === '自营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-stock", - "title": "库存车辆", - "markdown": "## 当前卡片口径\n\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-nonOperating", - "title": "非运营车辆", - "markdown": "## 当前卡片口径\n\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-exit", - "title": "退出运营车辆", - "markdown": "## 当前卡片口径\n\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-licenseAbnormal", - "title": "证照异常车辆", - "markdown": "## 当前卡片口径\n\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-kpi-insuranceAbnormal", - "title": "保险异常车辆", - "markdown": "## 当前卡片口径\n\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-table-toolbar", - "title": "导出与批量导入", - "markdown": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-vehicle-table", - "title": "台账列表", - "markdown": "## 7. 列表字段说明\n\n### 7.1 车辆信息(冻结列)\n\n- 车牌:链接色,点击进入**车辆详情页**\n- VIN、品牌·型号:只读展示\n\n### 7.2 运营城市\n\n- 展示:省-市(由 `location` 解析)\n- 标签:车机 / GPS / 人工(来源 `operateCitySource` 或推导)\n- **可编辑**:仅来源为「人工」时可点击修改 → 运营城市修改弹窗\n- 车机/GPS 来源:不可改,hover 提示原因\n\n### 7.3 所属项目\n\n仅**租赁/自营**车辆展示项目名称、合同编码、客户名称;否则三行均为空。\n\n- 项目名称、合同编码:有值时可点击 → Toast 跳转合同详情(原型演示)\n\n### 7.4 里程数\n\n- 在线车辆:显示里程 + 「车机」标签,里程按车机数据格式化\n- 离线:仅里程或空\n\n### 7.5 证照 / 保险\n\n| 列 | 有数据 | 无数据/异常 |\n|---|---|---|\n| 检验有效期 | 到期日 + 剩余/已过期天数标签 | 「未上传」可点击 + 「禁止交车」标签 |\n| 保险状态 | 正常/异常 Tag;异常时 hover 展示具体到期险种 | — |\n| 交强险/商业险到期 | 到期日 + 剩余天数 | 「未购买」可点击 + 「禁止交车」 |\n\n未上传/未购买点击 → Toast 跳转证照管理或保险采购(原型演示)。\n\n### 7.6 上次交车 / 还车情况\n\n三行纯数值(无行内标题):里程(km)、人员、时间(至分钟)。**有数据时**可点击。\n\n| 列 | 点击行为 |\n|---|---|\n| 上次交车情况 | Toast:`即将跳转至交车管理(原型演示):{车牌}` |\n| 上次还车情况 | Toast:`即将跳转至还车管理(原型演示):{车牌}` |\n\n### 7.7 三方租赁公司\n\n仅外租车辆显示 `leaseCompany`,否则空白。\n\n### 7.8 当前位置\n\n- 在线状态圆点 + 文案\n- 详细地址:有 GPS 坐标时可点击 → 地图弹窗(OpenStreetMap 嵌入)\n- GPS 时间:精确到秒\n\n### 7.9 运维负责人\n\n可编辑时显示为链接;无负责人显示「设置负责人」→ 运维负责人弹窗。\n\n### 7.10 操作列(冻结)\n\n| 操作 | 条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 进入车辆详情页 |\n| 更多 | 管理员或有运维权限 | 下拉:编辑(管理员)、修改运维负责人 |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-import-modal", - "title": "批量导入", - "markdown": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-edit-modal", - "title": "编辑车辆", - "markdown": "### 8.3 编辑车辆(管理员)\n\n- 可搜索选择器字段与筛选区一致规范\n- 保存时运营状态、车辆状态仍为必填(按钮禁用),但标签无红色 *\n- Toast「车辆信息已更新」" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-ops-modal", - "title": "运维负责人", - "markdown": "### 8.1 设置运维负责人\n\n- 标题下胶囊:车牌 | 运维区域(省-市)\n- 多选搜索选择器,候选人按车辆区域过滤\n- **保存**:写回 `opsManagers`,Toast「运维负责人已更新」\n- **取消**:不保存\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联编辑面板(非弹窗)" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-operate-city-modal", - "title": "修改运营城市", - "markdown": "### 8.2 修改运营城市\n\n- 省-市选择,保存后 `location` 更新且 `operateCitySource` 置为「人工」\n- Toast「运营城市已更新」\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联选择器(非弹窗)" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-location-map-modal", - "title": "车辆定位地图", - "markdown": "### 8.4 地图\n\n展示坐标点与 GPS 时间;关闭不保存。\n\n---" - } - ] - }, - { - "type": "folder", - "id": "vm-doc-detail", - "title": "详情页模块", - "defaultExpanded": true, - "children": [ - { - "type": "route", - "id": "vm-route-detail", - "title": "打开详情页", - "route": "vehicle-management-clay:detail", - "payload": { - "pageId": "detail" - } - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-doc-toolbar", - "title": "测试用例下载", - "markdown": "## 下载测试用例\n\n右上角工具栏,列表页与详情页均可用;下载含列表/详情两个 Sheet 的 Excel。" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-hero", - "title": "车辆概要", - "markdown": "### 2.1 顶栏\n\n| 元素 | 行为 |\n|---|---|\n| 返回列表 | 回到列表页,保留列表筛选/KPI/分页状态 |\n| 查看需求说明 | 打开详情页 PRD(本文档) |\n| PRD文档MD文件下载 | 下载列表页、详情页两份 Markdown PRD |\n| 下载测试用例 | 下载 Excel,含列表页与详情页测试用例两个 Sheet |\n\n---\n\n### 2.2 车辆概要区\n\n- 车牌号、品牌·型号、VIN\n- 在线状态(在线/离线圆点)\n- 运营城市文案、GPS 时间\n\n---\n\n### 原型实现补充\n\n- 顶栏仅保留「返回列表」与「下载测试用例」" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-stats", - "title": "统计条", - "markdown": "### 2.3 统计条(4 项)\n\n| 字段 | 可编辑 | 行为 |\n|---|---|---|\n| 运营城市 | 仅来源为「人工」时可改 | 点击 → 运营城市修改弹窗;展示车机/GPS/人工来源标签 |\n| 里程数 | 否 | 在线时展示车机标签 |\n| 停放区域 | 否 | 只读 |\n| 运维负责人 | 有运维权限时可改 | 点击铅笔图标 → 运维负责人弹窗 |\n\n\n---\n\n### 原型实现补充\n\n- **运营城市**、**运维负责人**:详情页使用内联编辑面板(非弹窗)" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tabs", - "title": "Tab 导航", - "markdown": "## 3. Tab 总览\n\n| 序号 | Tab | 说明 |\n|---|---|---|\n| 1 | 基本信息 | 档案、项目、证照摘要等,部分字段行内可编辑 |\n| 2 | 型号参数 | 车型参数与保养项目表 |\n| 3 | 证照信息 | 行驶证、营运证等分组展示与照片预览 |\n| 4 | 保险记录 | 险种概览卡片 + 采购历史表 |\n| 5 | 租赁记录 | 合同/交还车历史 |\n| 6 | 事故记录 | 筛选 + 事故列表 |\n| 7 | 故障记录 | 筛选 + 故障列表 |\n| 8 | 违章记录 | 筛选 + 违章列表 |\n| 9 | 异动记录 | 筛选 + 异动列表 |\n| 10 | 调拨记录 | 筛选 + 调拨列表 |\n| 11 | 年审记录 | 筛选 + 已完成年审历史(对齐年审管理历史列表) |\n\n各业务记录 Tab 通用规则:\n\n- 数据按当前车辆 `plateNo` 从对应 JSON 种子文件过滤\n- 筛选区:**查询**生效、**重置**清空,修改未查询不影响列表\n- 日期范围使用双月历弹层组件\n- 表格左对齐;金额/数字列使用等宽数字\n- 底栏:共 N 条 + 紧凑分页(默认 10 条/页)\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-basic", - "title": "基本信息 Tab", - "markdown": "## 4. 基本信息 Tab\n\n### 4.1 档案信息\n\n| 字段 | 可编辑 |\n|---|---|\n| 出厂年份 | 是(年份选择) |\n| 采购入库日期 | 是(日期选择) |\n| 运营状态、车辆来源、停车场等 | 只读展示 |\n\n保存后 Toast「车辆信息已更新」,写回内存台账。\n\n### 4.2 项目信息\n\n租赁/自营车辆展示项目名称、合同编码、客户名称;否则显示「无」。\n\n### 4.3 证照摘要\n\n- 检验有效期:展示到期日与剩余/过期标签\n- 未上传时可点击 → Toast 跳转证照管理编辑(原型演示)\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-model", - "title": "型号参数 Tab", - "markdown": "## 5. 型号参数 Tab\n\n- 展示车辆型号技术参数(只读)\n- 保养项目表:养护项目、保养公里/时间周期、工时费、材料费、合计\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-license", - "title": "证照信息 Tab", - "markdown": "## 6. 证照信息 Tab\n\n- 分组:行驶证、道路运输证、上海牌专项证件等(按车牌规则显隐)\n- 每证展示:证件号、有效期、余额(如有)、照片网格\n- 照片可点击全屏预览;证件状态色点图例\n- 数据来自 `utils/vehicleLicenses` 与车辆台账关联\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-insurance", - "title": "保险记录 Tab", - "markdown": "## 7. 保险记录 Tab\n\n### 7.1 险种概览卡片\n\n交强险、商业险、承运人责任险等;点击切换下方历史表。\n\n- 未购买险种卡片可点击 → Toast 跳转保险采购(原型演示)\n- 展示状态 Tag 与到期剩余天数\n\n### 7.2 采购历史表\n\n| 列 | 说明 |\n|---|---|\n| 保险导入时间、操作人、类型、保单号 | 只读 |\n| 保险状态 | Tag |\n| 保险公司、付款时间、到期日期 | 只读 |\n| 保单文件 | 有附件时「预览」「下载」可点击 → Toast 演示 |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-lease", - "title": "租赁记录 Tab", - "markdown": "## 8. 租赁记录 Tab\n\n### 8.1 筛选\n\n合同编码、项目名称、客户名称、业务类型。\n\n### 8.2 表格与跳转\n\n| 列 | 可点击 | 跳转目标(原型 Toast) |\n|---|---|---|\n| 合同编码 | 是 | 租赁合同详情 |\n| 项目名称 | 有值时 | 租赁合同详情 |\n| 交车日期 | 有值时 | 交车管理详情 |\n| 还车日期 | 有值时 | 还车管理详情 |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-accident", - "title": "事故记录 Tab", - "markdown": "## 9. 事故记录 Tab\n\n### 9.1 筛选\n\n事故时间(日期范围)、客户名称、事故等级、事故状态。\n\n### 9.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 事故编码 | 是 | 事故管理详情页 |\n| 其余列 | 否 | — |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-fault", - "title": "故障记录 Tab", - "markdown": "## 10. 故障记录 Tab\n\n### 10.1 筛选\n\n上报时间(日期范围)、故障编号、故障等级、故障类型、解决情况、AI 匹配状态。\n\n### 10.2 表格\n\n| 列/操作 | 可点击 | 行为 |\n|---|---|---|\n| 故障编号 | 是 | Toast 跳转故障管理详情 |\n| 操作「查看」 | 是 | 同上 |\n| 操作「编辑」 | 是 | Toast 编辑故障记录演示 |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-violation", - "title": "违章记录 Tab", - "markdown": "## 11. 违章记录 Tab\n\n### 11.1 筛选\n\n违法时间、客户名称、缴费状态、是否处理。\n\n### 11.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 违法行为 | 是 | 违章管理详情页(按违法时间标识) |\n| 其余列 | 否 | — |\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-movement", - "title": "异动记录 Tab", - "markdown": "## 12. 异动记录 Tab\n\n### 12.1 筛选\n\n异动开始日期、异动目的地、异动类型。\n\n### 12.2 表格\n\n宽表展示异动起止里程/电量/氢量等;**异动开始日期**可点击 → Toast 跳转异动管理详情。\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-transfer", - "title": "调拨记录 Tab", - "markdown": "## 13. 调拨记录 Tab\n\n### 13.1 筛选\n\n调拨日期、调出人、接收人。\n\n### 13.2 表格\n\n**调拨日期**可点击 → Toast 跳转车辆调拨详情。\n\n---" - }, - { - "type": "markdown", - "id": "vm-doc-node-vm-detail-tab-annual", - "title": "年审记录 Tab", - "markdown": "## 14. 年审记录 Tab\n\n对齐「年审管理」历史列表字段(不含车牌/品牌/型号,因已在详情页上下文)。\n\n### 14.1 筛选\n\n| 字段 | 说明 |\n|---|---|\n| 完成时间 | 按办结日期范围 |\n| 检验有效期 | 按检验有效期至范围 |\n| 办理人 | 下拉选择 |\n\n### 14.2 表格列\n\n| 列 | 可点击 | 说明 |\n|---|---|---|\n| 检验有效期至 | **是** | Toast 跳转年审管理详情(原型演示) |\n| 检测服务站名称 | 否 | 无数据显示「—」 |\n| 检测费用 | 否 | 金额两位小数,无数据显示「—」 |\n| 二保服务站名称 | 否 | 同上 |\n| 二保费用 | 否 | 同上 |\n| 整备服务站名称 | 否 | 同上 |\n| 整备费用 | 否 | 同上 |\n| 办理人 | 否 | — |\n| 完成时间 | 否 | 办结时间,默认倒序 |\n\n### 14.3 数据\n\n- 来源:`data/annual-review-records.json`,按 `plateNo` 过滤\n- 样例车 `粤AGP5156` 含 3 条已完成年审记录\n\n---" - } - ] - } - ] - } - ] - } -} diff --git a/src/prototypes/vehicle-management-clay/index.tsx b/src/prototypes/vehicle-management-clay/index.tsx deleted file mode 100644 index cc393c1..0000000 --- a/src/prototypes/vehicle-management-clay/index.tsx +++ /dev/null @@ -1,395 +0,0 @@ -/** - * @name 车辆管理(Clay 主题备份) - * 设计基底:src/themes/clay/DESIGN.md · 功能与标注同 vehicle-management 原版 - */ - -import '../vehicle-management/style.css'; -import './styles/clay-theme.css'; -import React, { useCallback, useMemo, useState } from 'react'; -import { Download, FileUp } from 'lucide-react'; -import { - AnnotationViewer, - type AnnotationDirectoryRouteNode, - type AnnotationSourceDocument, - type AnnotationViewerOptions, -} from '@axhub/annotation'; -import seedVehicles from '../vehicle-management/data/vehicles.json'; -import seedInsurancePurchases from '../vehicle-management/data/insurance-purchases.json'; -import { FilterPanel } from '../vehicle-management/components/FilterPanel'; -import { ImportModal } from '../vehicle-management/components/ImportModal'; -import { KpiCards } from '../vehicle-management/components/KpiCards'; -import { OpsManagerModal } from '../vehicle-management/components/OpsManagerModal'; -import { OperateCityEditModal } from '../vehicle-management/components/OperateCityEditModal'; -import { LocationMapModal } from '../vehicle-management/components/LocationMapModal'; -import { VehicleEditModal } from '../vehicle-management/components/VehicleEditModal'; -import { PageDocToolbar } from '../vehicle-management/components/PageDocToolbar'; -import { VehicleTable } from '../vehicle-management/components/VehicleTable'; -import { TablePagination } from '../vehicle-management/components/TablePagination'; -import { DetailPage } from '../vehicle-management/pages/DetailPage'; -import { - EMPTY_FILTERS, - type KpiKey, - type VehicleFilters, - type VehicleRecord, -} from '../vehicle-management/types'; -import type { InsurancePurchaseRecord } from '../vehicle-management/types'; -import { applyFilters } from '../vehicle-management/utils/filters'; -import { buildInsuranceExpireMap } from '../vehicle-management/utils/insurance'; -import type { LeaseRecordRow } from '../vehicle-management/utils/leaseRecords'; -import type { AccidentRecordRow } from '../vehicle-management/utils/accidentRecords'; -import type { ViolationRecordRow } from '../vehicle-management/utils/violationRecords'; -import type { MovementRecordRow } from '../vehicle-management/utils/movementRecords'; -import type { TransferRecordRow } from '../vehicle-management/utils/transferRecords'; -import type { AnnualReviewRecordRow } from '../vehicle-management/utils/annualReviewRecords'; -import { downloadVehicleTestCasesExcel } from '../vehicle-management/utils/testCaseExport'; -import { countKpi } from '../vehicle-management/utils/vehicle'; -import annotationSourceDocument from './annotation-source.json'; - -const CAN_EDIT_OPS_MANAGER = true; -const IS_ADMIN = true; -const DEFAULT_PAGE_SIZE = 10; - -function ClayBackupBanner() { - return ( -
-
- Clay Design System -

车辆管理 · Clay 主题备份页

-

- 奶油色画布、12px 控件圆角、24px 卡片圆角与饱和 KPI 色卡;业务逻辑与标注与原版一致。 -

-
- - 查看原版页面 - -
- ); -} - -export default function VehicleManagementClayApp() { - const [records, setRecords] = useState(seedVehicles as VehicleRecord[]); - const [pendingFilters, setPendingFilters] = useState(EMPTY_FILTERS); - const [appliedFilters, setAppliedFilters] = useState(EMPTY_FILTERS); - const [kpiTab, setKpiTab] = useState('all'); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - const [detailRecord, setDetailRecord] = useState(null); - const [importOpen, setImportOpen] = useState(false); - const [opsRecord, setOpsRecord] = useState(null); - const [operateCityRecord, setOperateCityRecord] = useState(null); - const [locationMapRecord, setLocationMapRecord] = useState(null); - const [editRecord, setEditRecord] = useState(null); - const [toast, setToast] = useState(''); - - const showToast = useCallback((msg: string) => { - setToast(msg); - window.setTimeout(() => setToast(''), 3000); - }, []); - - const insuranceExpireMap = useMemo( - () => buildInsuranceExpireMap(seedInsurancePurchases as InsurancePurchaseRecord[]), - [], - ); - - const kpiCounts = useMemo(() => ({ - all: countKpi(records, 'all'), - lease: countKpi(records, 'lease'), - self: countKpi(records, 'self'), - stock: countKpi(records, 'stock'), - nonOperating: countKpi(records, 'nonOperating'), - exit: countKpi(records, 'exit'), - licenseAbnormal: countKpi(records, 'licenseAbnormal'), - insuranceAbnormal: countKpi(records, 'insuranceAbnormal'), - }), [records]); - - const filtered = useMemo( - () => applyFilters(records, appliedFilters, kpiTab), - [records, appliedFilters, kpiTab], - ); - - const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); - const paged = useMemo(() => { - const start = (page - 1) * pageSize; - return filtered.slice(start, start + pageSize); - }, [filtered, page, pageSize]); - - const handlePageSizeChange = (size: number) => { - setPageSize(size); - setPage(1); - }; - - const handleSearch = () => { - setAppliedFilters(pendingFilters); - setPage(1); - }; - - const handleReset = () => { - setPendingFilters(EMPTY_FILTERS); - setAppliedFilters(EMPTY_FILTERS); - setPage(1); - }; - - const handleExport = () => { - showToast(`导出功能(原型演示):当前列表共 ${filtered.length} 条`); - }; - - const handleImport = (file: File) => { - showToast(`已接收文件 ${file.name},导入校验为原型演示`); - setImportOpen(false); - }; - - const handleSaveOps = (managers: string[]) => { - if (!opsRecord) return; - setRecords((prev) => prev.map((r) => ( - r.id === opsRecord.id ? { ...r, opsManagers: managers } : r - ))); - setOpsRecord(null); - showToast('运维负责人已更新'); - }; - - const handleSaveOperateCity = (location: string) => { - if (!operateCityRecord) return; - setRecords((prev) => prev.map((r) => ( - r.id === operateCityRecord.id - ? { ...r, location, operateCitySource: '人工' as const } - : r - ))); - setOperateCityRecord(null); - showToast('运营城市已更新'); - }; - - const handleOpenContract = (_record: VehicleRecord, contractNo: string) => { - showToast(`即将跳转至合同详情(原型演示):${contractNo}`); - }; - - const handleOpenLeaseContract = (record: VehicleRecord, contractNo: string) => { - showToast(`即将跳转至租赁合同详情(原型演示):${record.plateNo} · ${contractNo}`); - }; - - const handleOpenDeliveryRecord = (record: VehicleRecord, row: LeaseRecordRow) => { - showToast(`即将跳转至交车管理详情(原型演示):${record.plateNo} · ${row.contractNo} · ${row.deliveryDate}`); - }; - - const handleOpenReturnRecord = (record: VehicleRecord, row: LeaseRecordRow) => { - showToast(`即将跳转至还车管理详情(原型演示):${record.plateNo} · ${row.contractNo} · ${row.returnDate}`); - }; - - const handleOpenLicenseEdit = (record: VehicleRecord) => { - showToast(`即将跳转至证照管理编辑页(原型演示):${record.plateNo}`); - }; - - const handleOpenInsurancePurchase = (record: VehicleRecord, insuranceLabel: string) => { - showToast(`即将跳转至保险采购编辑页(原型演示):${record.plateNo} · ${insuranceLabel}`); - }; - - const handleViewFaultDetail = (record: VehicleRecord, row: { faultNo: string }) => { - showToast(`即将跳转至故障管理详情页(原型演示):${row.faultNo}`); - }; - - const handleEditFaultRecord = (record: VehicleRecord, row: { faultNo: string }) => { - showToast(`编辑故障记录(原型演示):${record.plateNo} · ${row.faultNo}`); - }; - - const handleOpenAccidentDetail = (record: VehicleRecord, row: AccidentRecordRow) => { - showToast(`即将跳转至事故管理详情页(原型演示):${row.accidentCode}`); - }; - - const handleOpenViolationDetail = (_record: VehicleRecord, row: ViolationRecordRow) => { - showToast(`即将跳转至违章管理详情页(原型演示):${row.violationTime}`); - }; - - const handleOpenMovementDetail = (_record: VehicleRecord, row: MovementRecordRow) => { - showToast(`即将跳转至异动管理详情页(原型演示):${row.startDate}`); - }; - - const handleOpenTransferDetail = (_record: VehicleRecord, row: TransferRecordRow) => { - showToast(`即将跳转至车辆调拨详情页(原型演示):${row.transferDate}`); - }; - - const handleOpenAnnualReviewDetail = (_record: VehicleRecord, row: AnnualReviewRecordRow) => { - showToast(`即将跳转至年审管理详情页(原型演示):${row.inspectionValidUntil}`); - }; - - const handlePreviewPolicyFile = (fileName: string) => { - showToast(`预览保单文件(原型演示):${fileName}`); - }; - - const handleDownloadPolicyFile = (fileName: string) => { - showToast(`已开始下载:${fileName}(原型演示)`); - }; - - const handleOpenDeliveryManagement = (record: VehicleRecord) => { - showToast(`即将跳转至交车管理(原型演示):${record.plateNo}`); - }; - - const handleOpenReturnManagement = (record: VehicleRecord) => { - showToast(`即将跳转至还车管理(原型演示):${record.plateNo}`); - }; - - const handleSaveEdit = (record: VehicleRecord) => { - setRecords((prev) => prev.map((r) => (r.id === record.id ? record : r))); - setEditRecord(null); - showToast('车辆信息已更新'); - }; - - const handleDetailFieldSave = (recordId: string, patch: Partial) => { - setRecords((prev) => prev.map((r) => (r.id === recordId ? { ...r, ...patch } : r))); - showToast('车辆信息已更新'); - }; - - const handleDownloadTestCases = () => { - downloadVehicleTestCasesExcel(showToast); - }; - - const docToolbar = ( - - ); - - const activeDetail = detailRecord - ? records.find((r) => r.id === detailRecord.id) ?? detailRecord - : null; - - const handleDirectoryRoute = useCallback((node: AnnotationDirectoryRouteNode) => { - const payload = node.payload as { pageId?: string } | undefined; - const pageId = typeof payload?.pageId === 'string' ? payload.pageId : ''; - if (pageId === 'list') { - setDetailRecord(null); - return; - } - if (pageId === 'detail') { - setDetailRecord((current) => current ?? filtered[0] ?? records[0] ?? null); - } - }, [filtered, records]); - - const annotationOptions = useMemo( - () => ({ - showToolbar: true, - showThemeToggle: true, - showColorFilter: true, - emptyWhenNoData: false, - toolbarEdge: 'right', - currentPageId: activeDetail ? 'detail' : 'list', - onDirectoryRoute: handleDirectoryRoute, - }), - [activeDetail, handleDirectoryRoute], - ); - - return ( - <> -
- {docToolbar} - {!activeDetail ? : null} - {activeDetail ? ( - setDetailRecord(null)} - canEditOpsManager={CAN_EDIT_OPS_MANAGER} - onOpenLicenseEdit={handleOpenLicenseEdit} - onSaveField={handleDetailFieldSave} - onOpenLeaseContract={handleOpenLeaseContract} - onOpenDeliveryRecord={handleOpenDeliveryRecord} - onOpenReturnRecord={handleOpenReturnRecord} - onOpenInsurancePurchase={handleOpenInsurancePurchase} - onViewFaultDetail={handleViewFaultDetail} - onEditFaultRecord={handleEditFaultRecord} - onOpenAccidentDetail={handleOpenAccidentDetail} - onOpenViolationDetail={handleOpenViolationDetail} - onOpenMovementDetail={handleOpenMovementDetail} - onOpenTransferDetail={handleOpenTransferDetail} - onOpenAnnualReviewDetail={handleOpenAnnualReviewDetail} - onPreviewPolicyFile={handlePreviewPolicyFile} - onDownloadPolicyFile={handleDownloadPolicyFile} - /> - ) : ( - <> - setPendingFilters((prev) => ({ ...prev, ...patch }))} - onReset={handleReset} - onSearch={handleSearch} - /> - - { setKpiTab(key); setPage(1); }} /> - -
-
-
- - -
-
- -
- -
- -
-
-
- - )} - - setImportOpen(false)} onImport={handleImport} /> - setOpsRecord(null)} - onSave={handleSaveOps} - /> - setOperateCityRecord(null)} - onSave={handleSaveOperateCity} - /> - setLocationMapRecord(null)} - /> - setEditRecord(null)} - onSave={handleSaveEdit} - /> - - {toast &&
{toast}
} -
- - - ); -} diff --git a/src/prototypes/vehicle-management-clay/styles/clay-theme.css b/src/prototypes/vehicle-management-clay/styles/clay-theme.css deleted file mode 100644 index 65cbcb7..0000000 --- a/src/prototypes/vehicle-management-clay/styles/clay-theme.css +++ /dev/null @@ -1,391 +0,0 @@ -/** - * 车辆管理 · Clay 主题备份 - * 设计基底:src/themes/clay/DESIGN.md - */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); - -.clay-vm-page { - /* Clay tokens → vm/ln aliases */ - --clay-primary: #0a0a0a; - --clay-primary-active: #1f1f1f; - --clay-ink: #0a0a0a; - --clay-body: #3a3a3a; - --clay-muted: #6a6a6a; - --clay-muted-soft: #9a9a9a; - --clay-hairline: #e5e5e5; - --clay-canvas: #fffaf0; - --clay-surface-soft: #faf5e8; - --clay-surface-card: #f5f0e0; - --clay-surface-strong: #ebe6d6; - --clay-brand-pink: #ff4d8b; - --clay-brand-teal: #1a3a3a; - --clay-brand-lavender: #b8a4ed; - --clay-brand-peach: #ffb084; - --clay-brand-ochre: #e8b94a; - --clay-brand-mint: #a4d4c5; - --clay-brand-coral: #ff6b5a; - --clay-on-dark: #ffffff; - --clay-radius-control: 12px; - --clay-radius-card: 24px; - --clay-radius-pill: 9999px; - - --ln-primary: var(--clay-primary); - --ln-primary-hover: var(--clay-primary-active); - --ln-primary-focus: var(--clay-primary-active); - --ln-primary-active: var(--clay-primary-active); - --ln-primary-soft: rgba(10, 10, 10, 0.06); - --ln-primary-soft-strong: rgba(10, 10, 10, 0.1); - --ln-ink: var(--clay-ink); - --ln-body: var(--clay-body); - --ln-body-strong: #1a1a1a; - --ln-muted: var(--clay-muted); - --ln-muted-soft: var(--clay-muted-soft); - --ln-hairline: var(--clay-hairline); - --ln-hairline-strong: #d4d4d4; - --ln-canvas: var(--clay-canvas); - --ln-canvas-parchment: var(--clay-canvas); - --ln-surface-pearl: var(--clay-surface-soft); - --ln-surface-card: var(--clay-surface-card); - --ln-surface-strong: var(--clay-surface-strong); - --ln-on-primary: var(--clay-on-dark); - --ln-success: #22c55e; - --ln-warning: #f59e0b; - --ln-error: #ef4444; - --ln-radius-xs: 6px; - --ln-radius-sm: 8px; - --ln-radius-control: var(--clay-radius-control); - --ln-radius-card: var(--clay-radius-card); - --ln-radius-xl: var(--clay-radius-card); - --ln-radius-pill: var(--clay-radius-pill); - --ln-shadow-soft: none; - --ln-shadow-hover: 0 8px 24px rgba(10, 10, 10, 0.08); - --ln-shadow-float: 0 12px 32px rgba(10, 10, 10, 0.1); - --ln-canvas-soft: var(--clay-surface-soft); - --ln-link: var(--clay-ink); - - --vm-primary: var(--clay-primary); - --vm-primary-dark: var(--clay-primary-active); - --vm-primary-soft: var(--ln-primary-soft); - --vm-bg: var(--clay-canvas); - --vm-surface: var(--clay-surface-card); - --vm-border: var(--clay-hairline); - --vm-text: var(--clay-ink); - --vm-muted: var(--clay-muted); - --vm-link: var(--clay-ink); - --vm-btn-height: 44px; - --vm-control-height: 44px; - --vm-focus-border-color: var(--clay-ink); - --vm-font: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; - - background: var(--clay-canvas); - color: var(--clay-body); - letter-spacing: 0; -} - -.clay-vm-page .clay-backup-banner { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - gap: 12px 16px; - margin-bottom: 24px; - padding: 14px 20px; - border: 1px solid var(--clay-hairline); - border-radius: var(--clay-radius-card); - background: linear-gradient(135deg, var(--clay-surface-soft) 0%, var(--clay-surface-card) 100%); -} - -.clay-vm-page .clay-backup-banner-main { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; -} - -.clay-vm-page .clay-backup-eyebrow { - font-size: 12px; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--clay-muted); -} - -.clay-vm-page .clay-backup-title { - margin: 0; - font-size: 1.125rem; - font-weight: 600; - letter-spacing: -0.02em; - color: var(--clay-ink); -} - -.clay-vm-page .clay-backup-desc { - margin: 0; - font-size: 0.875rem; - line-height: 1.5; - color: var(--clay-muted); -} - -.clay-vm-page .clay-backup-link { - display: inline-flex; - align-items: center; - min-height: 44px; - padding: 0 16px; - border: 1px solid var(--clay-hairline); - border-radius: var(--clay-radius-control); - background: var(--clay-canvas); - color: var(--clay-ink); - font-size: 0.875rem; - font-weight: 600; - text-decoration: none; - cursor: pointer; - white-space: nowrap; -} - -.clay-vm-page .clay-backup-link:hover { - border-color: var(--clay-ink); - background: var(--clay-surface-soft); -} - -.clay-vm-page .clay-backup-link:focus-visible { - outline: 2px solid var(--clay-ink); - outline-offset: 2px; -} - -/* Filter */ -.clay-vm-page .vm-filter-card { - border-radius: var(--clay-radius-card); - border-color: var(--clay-hairline); - background: var(--clay-surface-card); - box-shadow: none; -} - -.clay-vm-page .vm-filter-title { - font-weight: 600; - letter-spacing: -0.02em; - color: var(--clay-ink); -} - -.clay-vm-page .vm-input, -.clay-vm-page .vm-filter-picker-control, -.clay-vm-page .vm-plate-trigger, -.clay-vm-page .vm-filter-card .vm-input:not(textarea) { - min-height: 44px; - height: 44px; - border-radius: var(--clay-radius-control); - background: var(--clay-canvas); -} - -/* Buttons */ -.clay-vm-page .vm-btn { - min-height: 44px; - height: 44px; - border-radius: var(--clay-radius-control); - font-weight: 600; -} - -.clay-vm-page .vm-btn-primary { - background: var(--clay-primary); - color: var(--clay-on-dark); - border-color: var(--clay-primary); -} - -.clay-vm-page .vm-btn-primary:hover:not(:disabled) { - background: var(--clay-primary-active); - border-color: var(--clay-primary-active); -} - -.clay-vm-page .vm-btn-secondary, -.clay-vm-page .vm-btn-ghost { - background: var(--clay-canvas); - border-color: var(--clay-hairline); - color: var(--clay-ink); -} - -.clay-vm-page .vm-btn-link { - color: var(--clay-ink); - font-weight: 600; -} - -/* KPI — Clay saturated card cycle */ -.clay-vm-page .vm-kpi-row { - gap: 12px; -} - -.clay-vm-page .vm-kpi-card { - border: none; - border-radius: var(--clay-radius-card); - min-height: 96px; - padding: 20px 36px 20px 20px; - box-shadow: none; -} - -.clay-vm-page .vm-kpi-card:hover { - box-shadow: var(--ln-shadow-hover); -} - -.clay-vm-page .vm-kpi-card.active { - box-shadow: 0 0 0 2px var(--clay-ink); -} - -.clay-vm-page .vm-kpi-card:nth-child(1) { background: var(--clay-surface-card); color: var(--clay-ink); } -.clay-vm-page .vm-kpi-card:nth-child(2) { background: var(--clay-brand-pink); color: var(--clay-on-dark); } -.clay-vm-page .vm-kpi-card:nth-child(3) { background: var(--clay-brand-teal); color: var(--clay-on-dark); } -.clay-vm-page .vm-kpi-card:nth-child(4) { background: var(--clay-brand-lavender); color: var(--clay-ink); } -.clay-vm-page .vm-kpi-card:nth-child(5) { background: var(--clay-brand-peach); color: var(--clay-ink); } -.clay-vm-page .vm-kpi-card:nth-child(6) { background: var(--clay-brand-ochre); color: var(--clay-ink); } -.clay-vm-page .vm-kpi-card:nth-child(7) { background: var(--clay-brand-coral); color: var(--clay-on-dark); } -.clay-vm-page .vm-kpi-card:nth-child(8) { background: var(--clay-brand-mint); color: var(--clay-ink); } - -.clay-vm-page .vm-kpi-card:nth-child(1) .vm-kpi-eyebrow, -.clay-vm-page .vm-kpi-card:nth-child(4) .vm-kpi-eyebrow, -.clay-vm-page .vm-kpi-card:nth-child(5) .vm-kpi-eyebrow, -.clay-vm-page .vm-kpi-card:nth-child(6) .vm-kpi-eyebrow, -.clay-vm-page .vm-kpi-card:nth-child(8) .vm-kpi-eyebrow { color: var(--clay-muted); } - -.clay-vm-page .vm-kpi-card:nth-child(2) .vm-kpi-eyebrow, -.clay-vm-page .vm-kpi-card:nth-child(3) .vm-kpi-eyebrow, -.clay-vm-page .vm-kpi-card:nth-child(7) .vm-kpi-eyebrow { color: rgba(255, 255, 255, 0.82); } - -.clay-vm-page .vm-kpi-card:nth-child(1) .vm-kpi-val, -.clay-vm-page .vm-kpi-card:nth-child(4) .vm-kpi-val, -.clay-vm-page .vm-kpi-card:nth-child(5) .vm-kpi-val, -.clay-vm-page .vm-kpi-card:nth-child(6) .vm-kpi-val, -.clay-vm-page .vm-kpi-card:nth-child(8) .vm-kpi-val { color: var(--clay-ink); } - -.clay-vm-page .vm-kpi-card:nth-child(2) .vm-kpi-val, -.clay-vm-page .vm-kpi-card:nth-child(3) .vm-kpi-val, -.clay-vm-page .vm-kpi-card:nth-child(7) .vm-kpi-val { color: var(--clay-on-dark); } - -.clay-vm-page .vm-kpi-card.active .vm-kpi-val { color: inherit; } - -.clay-vm-page .vm-kpi-icon { - width: 40px; - height: 40px; - border-radius: var(--clay-radius-control); - background: rgba(255, 255, 255, 0.22); - color: inherit; -} - -.clay-vm-page .vm-kpi-card:nth-child(1) .vm-kpi-icon { - background: rgba(10, 10, 10, 0.06); - color: var(--clay-ink); -} - -.clay-vm-page .vm-kpi-tip { - color: inherit; - opacity: 0.72; -} - -/* Table */ -.clay-vm-page .vm-table-card { - border-radius: var(--clay-radius-card); - border-color: var(--clay-hairline); - background: var(--clay-canvas); - box-shadow: none; -} - -.clay-vm-page .vm-table th { - background: var(--clay-surface-soft); - color: var(--clay-muted); - font-weight: 600; -} - -.clay-vm-page .vm-table tbody tr:hover td { - background: var(--clay-surface-soft); -} - -.clay-vm-page .vm-plate-link, -.clay-vm-page .vm-cell-link { - color: var(--clay-ink); - font-weight: 600; -} - -.clay-vm-page .vm-plate-link:hover, -.clay-vm-page .vm-cell-link:hover { - color: var(--clay-primary-active); -} - -/* Modals & toast */ -.clay-vm-page .vm-modal { - border-radius: var(--clay-radius-card); - background: var(--clay-canvas); -} - -.clay-vm-page .vm-toast { - border-radius: var(--clay-radius-control); - background: var(--clay-primary); - color: var(--clay-on-dark); -} - -.clay-vm-page .vm-doc-toolbar-btn { - min-height: 44px; - border-radius: var(--clay-radius-control); - background: var(--clay-canvas); -} - -/* Detail page */ -.clay-vm-page .vm-detail-card { - border-radius: var(--clay-radius-card); - border-color: var(--clay-hairline); - background: var(--clay-canvas); - box-shadow: none; -} - -.clay-vm-page .vm-detail-plate { - color: var(--clay-ink); - letter-spacing: -0.02em; -} - -.clay-vm-page .vm-detail-stats { - border-color: var(--clay-hairline); - background: var(--clay-surface-soft); - border-radius: var(--clay-radius-control); -} - -.clay-vm-page .vm-detail-stat { - border-color: var(--clay-hairline); -} - -.clay-vm-page .vm-tabs { - border-color: var(--clay-hairline); -} - -.clay-vm-page .vm-tab { - min-height: 44px; - color: var(--clay-muted); - font-weight: 600; -} - -.clay-vm-page .vm-tab.active { - color: var(--clay-ink); - border-color: var(--clay-ink); -} - -.clay-vm-page .vm-btn-back { - min-height: 44px; - border-radius: var(--clay-radius-control); - color: var(--clay-ink); -} - -@media (max-width: 1280px) { - .clay-vm-page .vm-kpi-row { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } -} - -@media (max-width: 768px) { - .clay-vm-page .vm-kpi-row { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .clay-vm-page { - padding: 16px; - } -} - -@media (prefers-reduced-motion: reduce) { - .clay-vm-page .vm-kpi-card, - .clay-vm-page .vm-btn { - transition: none; - } -}
onToggleAll(e.target.checked)} aria-label="全选" /> - - +
+ handleSort(col.key)} + /> + handleResizeStart(col.key, e)} + /> +
+
操作
- {showReceiptLink && ( - <> - - | - - )} - {needsMaintenanceLinkWarning(row, maintenanceOrders) && ( - <> - - | - - )} - {needsAccidentLinkWarning(row, accidentRecords) && ( - <> - - | - - )} - - | - + )} +