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 && (
-
+ );
+}
+
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) => (
+
+ ))}
+
+
|
onToggleAll(e.target.checked)} aria-label="全选" />
|
- {LEASE_TABLE_COLUMNS.map((col) => (
-
-
- |
- ))}
+ {LEASE_TABLE_COLUMNS.map((col) => {
+ const sortable = col.sortable !== false && col.key !== '_index';
+ return (
+
+
+ handleSort(col.key)}
+ />
+ handleResizeStart(col.key, e)}
+ />
+
+ |
+ );
+ })}
操作 |
- {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({
))}
- {showReceiptLink && (
- <>
- onLinkReceipt(row)}>
- 关联收款记录
+
+ {showReceiptLink && (
+ onLinkReceipt(row)}>
+ 关联收款
- |
- >
- )}
- {needsMaintenanceLinkWarning(row, maintenanceOrders) && (
- <>
- onLinkMaintenance(row)}>
- 关联维保
+ )}
+ {canEdit && (
+ onEdit(row)}>
+ 编辑
- |
- >
- )}
- {needsAccidentLinkWarning(row, accidentRecords) && (
- <>
- onLinkAccident(row)}>
- 关联事故
+ )}
+ {isDraft && canDelete && (
+ onDelete(row)}>
+ 删除
- |
- >
- )}
- onEdit(row)}>
- 编辑
-
- |
- onDelete(row)}>
- 删除
-
+ )}
+
|
);
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() {
仅显示未收款数据
-
showToast('批量导入(原型演示)')}>
+ showToast('批量导入(原型演示)')}>
批量导入
- showToast(`导出 ${listFiltered.length} 条(原型演示)`)}>
+ showToast(`导出 ${listFiltered.length} 条(原型演示)`)}>
导出
@@ -293,8 +301,14 @@ export default function LeaseBusinessLedgerApp() {
showToast(`提交收款审批:已选 ${selectedIds.size} 条(原型演示)`)}
+ className="vm-btn vm-btn-primary ldb-toolbar-btn"
+ onClick={() => {
+ if (selectedIds.size === 0) {
+ showToast('请先勾选需要提交审批的记录');
+ return;
+ }
+ setSubmitApprovalOpen(true);
+ }}
>
提交收款审批
@@ -305,6 +319,7 @@ export default function LeaseBusinessLedgerApp() {
{
- if (!canStaffDeleteRow(row)) {
- showToast('已确认记录仅业务服务组主管可删除');
- return;
- }
- showToast(`删除:${row.plateNo}(原型演示)`);
+ if (!canStaffDeleteRow(row)) return;
+ setDeleteTarget(row);
}}
onLinkReceipt={(row) => setReceiptRow(row)}
onLinkMaintenance={(row) => setMaintenanceRow(row)}
@@ -379,6 +391,47 @@ export default function LeaseBusinessLedgerApp() {
onCancel={() => setConfirmOpen(false)}
onConfirm={handleConfirmMark}
/>
+ setDeleteTarget(null)}
+ onConfirm={() => {
+ if (!deleteTarget) return;
+ setRecords((prev) => prev.filter((row) => row.id !== deleteTarget.id));
+ setSelectedIds((prev) => {
+ const next = new Set(prev);
+ next.delete(deleteTarget.id);
+ return next;
+ });
+ setDeleteTarget(null);
+ showToast(`已删除 ${deleteTarget.plateNo}(原型)`);
+ }}
+ />
+ {submitApprovalOpen && (
+ setSubmitApprovalOpen(false)}>
+
e.stopPropagation()}>
+
+
+
确认将所选 {selectedIds.size} 条记录提交收款审批?
+
+
+
+
+ )}
): 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(true)}>
-
- 批量导入
-
-
-
-
-
-
- >
- )}
-
- 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;
- }
-}