diff --git a/.axhub/make/axhub.config.json b/.axhub/make/axhub.config.json
index b1ee2ea..687d7e0 100644
--- a/.axhub/make/axhub.config.json
+++ b/.axhub/make/axhub.config.json
@@ -14,5 +14,39 @@
"remote": {
"url": "https://gitea.lnh2e.com/wangmian/OneOS1.2.git"
}
+ },
+ "cloudPublishing": {
+ "vercel": {
+ "token": "",
+ "projectName": "",
+ "teamId": ""
+ },
+ "cloudflarePages": {
+ "apiToken": "",
+ "accountId": "",
+ "projectName": "",
+ "productionBranch": "main"
+ },
+ "s3": {
+ "accessKeyId": "",
+ "secretAccessKey": "",
+ "region": "cn-hangzhou",
+ "bucket": "model-lnoneos",
+ "prefix": "",
+ "baseUrl": "http://prototype.lnoneos.com/",
+ "endpoint": ""
+ },
+ "githubPages": {
+ "repository": "",
+ "branch": "gh-pages",
+ "sourceDirectory": "/"
+ },
+ "publishSettings": {
+ "includeSource": true,
+ "visibleTargets": [
+ "axhub",
+ "s3"
+ ]
+ }
}
}
\ No newline at end of file
diff --git a/.axhub/make/sidebar-tree.json b/.axhub/make/sidebar-tree.json
index dc683eb..9094c09 100644
--- a/.axhub/make/sidebar-tree.json
+++ b/.axhub/make/sidebar-tree.json
@@ -1,6 +1,6 @@
{
"version": 1,
- "updatedAt": "2026-06-30T08:53:12.196Z",
+ "updatedAt": "2026-06-30T10:18:56.724Z",
"prototypes": [
{
"id": "item:prototypes:insurance-procurement",
@@ -945,4 +945,4 @@
"templates": [],
"components": [],
"canvas": []
-}
\ No newline at end of file
+}
diff --git a/.local/extract-annotation-source/annotation-nodes.json b/.local/extract-annotation-source/annotation-nodes.json
new file mode 100644
index 0000000..eaa9a4b
--- /dev/null
+++ b/.local/extract-annotation-source/annotation-nodes.json
@@ -0,0 +1,287 @@
+[
+ {
+ "id": "ct-list-filter",
+ "index": 1,
+ "title": "筛选条件",
+ "pageId": "list",
+ "hasMarkdown": true,
+ "annotationText": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 文本输入 | 支持回车触发查询 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 适用品牌 | 下拉 | 选项来自车型主数据品牌列表 |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认",
+ "aiPrompt": "列表页筛选区字段与查询逻辑。",
+ "color": "#2563eb",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-list-filter\"]"
+ ],
+ "fingerprint": "ct-list-filter",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-list-toolbar",
+ "index": 2,
+ "title": "新增模板",
+ "pageId": "list",
+ "hasMarkdown": false,
+ "annotationText": "进入新增页,默认从「合同主体」章节开始编辑。",
+ "aiPrompt": "列表工具栏新增入口。",
+ "color": "#32a06e",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-list-toolbar\"]"
+ ],
+ "fingerprint": "ct-list-toolbar",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-list-table",
+ "index": 3,
+ "title": "Word 模板列表",
+ "pageId": "list",
+ "hasMarkdown": true,
+ "annotationText": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同类型 | `contractType` | 展示用户自定义的合同类型文案;已发布模板在类型后以 Tag 显示「启用」或「停用」 |\n| 版本号 | `versionNo` | 草稿显示「—」 |\n| 状态 | `status` | 已发布 / 草稿 Tag |\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**多套模板并存**:同一合同类型可维护多套模板;租赁合同创建时先选合同类型,再自动套用该类型下已启用的标准模板(`isDefault` 优先)。\n\n### 7.6 子表(展开行)\n\n主表每行可展开,展示该 Word 文档下 **9 个章节** 的摘要。",
+ "aiPrompt": "主表与子表嵌套列表结构。",
+ "color": "#0f766e",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-list-table\"]"
+ ],
+ "fingerprint": "ct-list-table",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-editor-topbar",
+ "index": 4,
+ "title": "编辑页顶栏",
+ "pageId": "editor",
+ "hasMarkdown": true,
+ "annotationText": "### 8.2 顶栏\n\n| 元素 | 行为 |\n|---|---|\n| ← 返回列表 | 回到列表页,不自动保存 |\n| 保存 | 见 §6.2 |\n| 保存并发布 | 见 §6.2 |\n\n### 6.2 保存与发布\n\n| 操作 | 新增模式 | 编辑草稿 | 编辑已发布 |\n|---|---|---|---|\n| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**降为草稿**(清空版本号) |\n| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空,且须已填写合同类型(见 §8.4)。",
+ "aiPrompt": "返回、保存草稿、保存并发布。",
+ "color": "#64748b",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-editor-topbar\"]"
+ ],
+ "fingerprint": "ct-editor-topbar",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-section-tabs",
+ "index": 5,
+ "title": "章节 Tab",
+ "pageId": "editor",
+ "hasMarkdown": true,
+ "annotationText": "### 8.3 章节 Tab\n\n- 共 9 个章节,**全部在横向滚动条中展示**(带序号 1–9),当前章节自动滚入视野。\n- 右上角 **章节列表** 可搜索并跳转(适合长标题章节)。\n- 当前章节全称在 Tab 下方展示,避免标题被截断后找不到位置。\n- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。\n- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**;其他 Tab 仅展示该章节切片。",
+ "aiPrompt": "9 个合同章节切换。",
+ "color": "#2563eb",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-section-tabs\"]"
+ ],
+ "fingerprint": "ct-section-tabs",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-contract-type",
+ "index": 6,
+ "title": "合同类型",
+ "pageId": "editor",
+ "hasMarkdown": true,
+ "annotationText": "### 8.4 合同类型(仅合同主体 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 合同类型输入框 | **必填**;用户自行填写,如「正式合同」「现代18吨试用合同」 |\n| 展示位置 | 章节 Tab 下方配置卡片,与附件章节的「适用车型」卡片交互一致 |\n| 用途 | 同步至列表页「合同类型」列;租赁合同创建时按类型匹配标准模板 |\n\n**规则**:\n\n- 仅当选中 **合同主体** 章节 Tab 时显示本卡片。\n- 未填写时无法保存或发布,并提示「请输入合同类型」。\n- 切换至租赁订单等附件章节时,显示「适用车型」卡片,本卡片隐藏。\n- 保存/发布时写入模板 `contractType` 字段。",
+ "aiPrompt": "合同主体章节下的合同类型配置。",
+ "color": "#7c3aed",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-contract-type\"]"
+ ],
+ "fingerprint": "ct-contract-type",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-vehicle-binding",
+ "index": 7,
+ "title": "适用车型绑定",
+ "pageId": "editor",
+ "hasMarkdown": true,
+ "annotationText": "### 8.5 适用车型绑定(附件章节 Tab:租赁订单~安全责任协议)\n\n| 元素 | 说明 |\n|---|---|\n| 品牌型号选择器 | 点击输入框弹出卡片;支持搜索筛选 |\n| 已选区 | 弹层内展示已选「品牌 · 型号」,可逐项移除 |\n| 品牌列 | 左侧列表切换品牌 |\n| 型号区 | 右侧多选型号(点击切换选中) |\n\n**规则**:\n\n- 绑定保存在 `sectionVehicleBindings[当前章节 key]`。\n- 切换章节 Tab 时加载对应章节的绑定。\n- 未绑定任何车型时表示全部车型适用。",
+ "aiPrompt": "附件章节适用车型配置。",
+ "color": "#059669",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-vehicle-binding\"]"
+ ],
+ "fingerprint": "ct-vehicle-binding",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-editor-panel",
+ "index": 8,
+ "title": "编辑区",
+ "pageId": "editor",
+ "hasMarkdown": true,
+ "annotationText": "### 8.6 编辑区(左侧)\n\n#### 8.6.1 面板头操作\n\n| 按钮 | 说明 |\n|---|---|\n| { } 模板占位符 | 弹层展示变量分组,点击插入光标处 |\n| 导入 Word 模板 | 接受 `.doc` / `.docx`;导入后按 §9 拆分章节 |\n\n#### 8.6.2 富文本工具栏(节选)\n\n| 按钮 | 说明 |\n|---|---|\n| **红线** | 在章节中选中内容后标记;新增租赁合同时**修改红线内文字**将自动变为非标合同,**不修改**则仍为标准流程 |\n| **取消**(红线旁) | 选中已标红线内容后点击,取消红线;取消后修改该部分不再触发非标流程 |\n| **条件** | 在**合同主体及租赁订单~安全责任协议**各章节圈定具体内容,使**部分条款**仅对指定车型生效(创建合同时按订单车辆命中显隐) |\n| **取消**(条件旁) | 选中已设条件的条款后点击,解除该部分的车型限定 |\n| **锁定** | 选中章节内内容后标记;新增租赁合同时该部分**无法被篡改** |\n| **解除**(锁定旁) | 选中已锁定内容后点击,解除锁定 |\n\n**叠加规则**:红线、条件与锁定可作用于**同一段内容**,互不排斥。\n\n**与章节「适用车型」的区别**:\n\n- **适用车型**(附件章节 Tab 上方卡片):控制整章是否在创建合同时对某车型**可见/不可见**。\n- **条件**(工具栏):仅作用于章节内的**部分段落**,不影响整章显隐。\n\n#### 8.6.3 条件条款交互\n\n**适用范围**:合同主体、租赁订单、车辆产生的费用、安全责任告知书、重型普通货运车辆租赁动态监管责任告知与确认书、安全责任承诺书、车辆使用规范和驾驶员要求、授权委托书、安全责任协议。\n\n1. 选中段落或文字;\n2. 点击「条件」并绑定「品牌 · 型号」;\n3. 创建合同时:订单车辆任意一辆命中 → 段落纳入正文,否则隐藏。",
+ "aiPrompt": "Word 版式分页编辑区。",
+ "color": "#7c3aed",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-editor-panel\"]"
+ ],
+ "fingerprint": "ct-editor-panel",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-rte-risk-redline",
+ "index": 9,
+ "title": "红线",
+ "pageId": "editor",
+ "hasMarkdown": false,
+ "annotationText": "红线功能主要用于在章节中选择特定内容,点击红线将其设置为篡改后自动变成非标合同。不修改红线内内容不会导致流程变为非标。",
+ "aiPrompt": "风控红线条款标记。",
+ "color": "#dc2626",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-rte-risk-redline\"]"
+ ],
+ "fingerprint": "ct-rte-risk-redline",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-rte-unrisk-redline",
+ "index": 10,
+ "title": "取消红线",
+ "pageId": "editor",
+ "hasMarkdown": false,
+ "annotationText": "取消功能主要用于取消被选中的部分的红线内容。取消后,在新增租赁合同时修改该部分内容,不会导致流程变为非标准流程。",
+ "aiPrompt": "取消风控红线标记。",
+ "color": "#94a3b8",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-rte-unrisk-redline\"]"
+ ],
+ "fingerprint": "ct-rte-unrisk-redline",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-rte-vehicle-clause",
+ "index": 11,
+ "title": "条件",
+ "pageId": "editor",
+ "hasMarkdown": false,
+ "annotationText": "条件可在合同主体及租赁订单~安全责任协议全部章节使用:圈定章节内具体内容,使部分条款仅对指定车型生效。与各章节「适用车型」(控制整章是否可见)不同,条件只作用于章节内部分段落。",
+ "aiPrompt": "条件条款(段落级车型显隐)。",
+ "color": "#2563eb",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-rte-vehicle-clause\"]"
+ ],
+ "fingerprint": "ct-rte-vehicle-clause",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-rte-unvehicle-clause",
+ "index": 12,
+ "title": "取消条件",
+ "pageId": "editor",
+ "hasMarkdown": false,
+ "annotationText": "当条款被设置条件后,可以通过选中特定条款内容,点击取消来取消该部分内容的车型限定。",
+ "aiPrompt": "取消条件条款标记。",
+ "color": "#64748b",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-rte-unvehicle-clause\"]"
+ ],
+ "fingerprint": "ct-rte-unvehicle-clause",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-rte-inline-lock",
+ "index": 13,
+ "title": "锁定",
+ "pageId": "editor",
+ "hasMarkdown": false,
+ "annotationText": "锁定主要用于选中章节内的内容;锁定之后该部分内容无法在新增租赁合同时被篡改。红线、条件以及锁定可以在同一段内容同时生效。",
+ "aiPrompt": "内容锁定区标记。",
+ "color": "#b45309",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-rte-inline-lock\"]"
+ ],
+ "fingerprint": "ct-rte-inline-lock",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-rte-uninline-lock",
+ "index": 14,
+ "title": "解除锁定",
+ "pageId": "editor",
+ "hasMarkdown": false,
+ "annotationText": "选中已锁定内容后点击解除;解除后新增租赁合同时可再次编辑该部分。",
+ "aiPrompt": "解除内容锁定标记。",
+ "color": "#94a3b8",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-rte-uninline-lock\"]"
+ ],
+ "fingerprint": "ct-rte-uninline-lock",
+ "path": []
+ }
+ },
+ {
+ "id": "ct-preview-panel",
+ "index": 15,
+ "title": "实时预览",
+ "pageId": "editor",
+ "hasMarkdown": true,
+ "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- 甲方相关变量按预览所选出租方公司填充",
+ "aiPrompt": "A4 分页预览与模拟车型。",
+ "color": "#0ea5e9",
+ "images": [],
+ "controls": [],
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ct-preview-panel\"]"
+ ],
+ "fingerprint": "ct-preview-panel",
+ "path": []
+ }
+ }
+]
\ No newline at end of file
diff --git a/.local/extract-annotation-source/markdown-nodes.json b/.local/extract-annotation-source/markdown-nodes.json
new file mode 100644
index 0000000..855bd89
--- /dev/null
+++ b/.local/extract-annotation-source/markdown-nodes.json
@@ -0,0 +1,57 @@
+[
+ {
+ "id": "ct-doc-overview",
+ "title": "模块总览",
+ "markdown": "## 模块定位\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| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |\n| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |\n| 支撑业务差异化 | 附件章节按车型绑定;各章节均支持条件条款 |\n| 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本 |\n| 标准模板唯一 | 同一合同分类同时仅一套标准模板生效 |"
+ },
+ {
+ "id": "ct-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. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。\n\n---\n\n## 3. 名词解释\n\n| 名词 | 定义 |\n|---|---|\n| Word 文档 / 模板记录 | 列表主表一行,对应一份完整合同文件 |\n| 章节 | 合同内的逻辑切片,共 9 个(1 个主体 + 8 个附件) |\n| 草稿 | `status = draft`,无版本号 |\n| 已发布 | `status = published`,有版本号 `vX.XX` |\n| 标准模板 | `isDefault = true` 且已发布的模板;同分类唯一 |\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 ├── 条件条款说明(各章节 Tab 均可使用工具栏「条件」)\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 isDefault: boolean; // 是否标准模板\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 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| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。\n\n### 6.4 删除规则\n\n- 仅 **非标准模板** 可删除。\n- 删除前二次确认;删除后不可恢复。\n\n### 6.5 列表排序\n\n1. 标准模板置顶;\n2. 其余按 `updateTime` 降序。\n\n### 6.6 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 模糊匹配 `fileName`、`versionName`、`versionNo`、`creator`、`updater` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 |\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| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |\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\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` 必须在每次保存时更新。\n\n---\n\n## 14. 接口需求(建议)\n\n### 14.1 列表查询\n\n```\nGET /api/contract-templates?category=lease&keyword=&status=&creator=&vehicleBrand=&page=1&pageSize=10\n```\n\n响应需包含主表字段;章节摘要可列表内嵌或展开时按需加载。\n\n### 14.2 详情\n\n```\nGET /api/contract-templates/{id}\n```\n\n返回完整 `sectionContents`、`sectionVehicleBindings`。\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}/set-default\n```\n\n服务端事务:同 category 旧标准 `isDefault=false`,新标准 `isDefault=true`。\n\n### 14.5 删除\n\n```\nDELETE /api/contract-templates/{id}\n```\n\n拒绝删除 `isDefault=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- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表\n- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态\n- [ ] 标准模板金色标签置顶,且不可删除\n- [ ] 切换标准模板时旧标准自动禁用\n\n### 16.2 编辑页\n\n- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多\n- [ ] 导入 Word 后 9 章节正确拆分\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\n---\n\n## 17. 原型实现映射(供研发对照)\n\n| 能力 | 原型文件 / 符号 |\n|---|---|\n| 主逻辑 | `src/prototypes/contract-template-management/ContractTemplate.jsx` |\n| 入口与标注 | `src/prototypes/contract-template-management/index.tsx` |\n| 标注数据 | `src/prototypes/contract-template-management/annotation-source.json` |\n| 占位符与出租方 | `src/prototypes/contract-template-management/contract-template-vars.js` |\n| 样式 | `src/prototypes/contract-template-management/styles/` |\n\n---\n\n## 18. 附录:标准模板确认弹窗(原文案)\n\n**标题**:确认设为标准模板\n\n**正文**:\n\n> 同一分类同时只能有一套标准模板生效。确认后,客服草拟新合同时将默认使用新标准模板。\n>\n> (若存在旧标准)将先禁用当前标准模板「{旧标准名称}」,再设置「{新标准名称}」为新的标准模板。\n\n**按钮**:取消 / 确认设置\n\n---\n\n## 19. 修订记录\n\n| 版本 | 日期 | 作者 | 说明 |\n|---|---|---|---|\n| v1.0 | 2026-06-25 | 产品 | 基于原型与需求讨论首版;明确 Word 文档级模型、嵌套列表、标准模板唯一、移除适用型号筛选与备注列 |"
+ },
+ {
+ "id": "ct-doc-prd-overview",
+ "title": "背景 · 数据模型 · 业务规则",
+ "markdown": "# 合同模板管理 · 全局说明\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. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。\n\n---\n\n## 3. 名词解释\n\n| 名词 | 定义 |\n|---|---|\n| Word 文档 / 模板记录 | 列表主表一行,对应一份完整合同文件 |\n| 章节 | 合同内的逻辑切片,共 9 个(1 个主体 + 8 个附件) |\n| 草稿 | `status = draft`,无版本号 |\n| 已发布 | `status = published`,有版本号 `vX.XX` |\n| 标准模板 | `isDefault = true` 且已发布的模板;同分类唯一 |\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 ├── 条件条款说明(各章节 Tab 均可使用工具栏「条件」)\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 isDefault: boolean; // 是否标准模板\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 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| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。\n\n### 6.4 删除规则\n\n- 仅 **非标准模板** 可删除。\n- 删除前二次确认;删除后不可恢复。\n\n### 6.5 列表排序\n\n1. 标准模板置顶;\n2. 其余按 `updateTime` 降序。\n\n### 6.6 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 模糊匹配 `fileName`、`versionName`、`versionNo`、`creator`、`updater` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 |\n\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 isDefault: boolean; // 是否标准模板\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 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| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。\n\n### 6.4 删除规则\n\n- 仅 **非标准模板** 可删除。\n- 删除前二次确认;删除后不可恢复。\n\n### 6.5 列表排序\n\n1. 标准模板置顶;\n2. 其余按 `updateTime` 降序。\n\n### 6.6 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 模糊匹配 `fileName`、`versionName`、`versionNo`、`creator`、`updater` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 |\n\n> **已移除**:筛选项「适用型号」、主表列「备注」(产品确认不再展示)。\n\n---"
+ },
+ {
+ "id": "ct-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| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |\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": "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\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---\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---\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---"
+ },
+ {
+ "id": "ct-doc-prd-acceptance",
+ "title": "验收标准",
+ "markdown": "## 16. 验收标准\n\n### 16.1 列表页\n\n- [ ] 筛选项包含:合同名称、状态、创建人、适用品牌;**不含**适用型号、备注列\n- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表\n- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态\n- [ ] 标准模板金色标签置顶,且不可删除\n- [ ] 切换标准模板时旧标准自动禁用\n\n### 16.2 编辑页\n\n- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多\n- [ ] 导入 Word 后 9 章节正确拆分\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\n---"
+ },
+ {
+ "id": "ct-doc-node-filter",
+ "title": "筛选条件",
+ "markdown": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 文本输入 | 支持回车触发查询 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 适用品牌 | 下拉 | 选项来自车型主数据品牌列表 |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认"
+ },
+ {
+ "id": "ct-doc-node-table",
+ "title": "嵌套列表",
+ "markdown": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同名称 | `fileName` | 超长省略;标准模板显示金色标签「标准合同」 |\n| 版本号 | `versionNo` | 草稿显示「—」 |\n| 状态 | `status` | 已发布 / 草稿 Tag |\n| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |\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### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。"
+ },
+ {
+ "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. 安全责任协议"
+ },
+ {
+ "id": "ct-doc-node-workspace",
+ "title": "编辑与预览",
+ "markdown": "## 编辑与预览\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\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- 甲方相关变量按预览所选出租方公司填充"
+ },
+ {
+ "id": "ct-doc-annotation-hint",
+ "title": "标注查看提示",
+ "markdown": "## 标注查看提示\n\n右侧标注工具栏可浏览目录、切换主题与颜色筛选。列表页与编辑页标注按当前页面自动显隐。\n\n**PRD 同步**:目录「PRD 全文」已与 `src/resources/contract-template-management/PRD.md` 对齐;修改 PRD 后需重新同步至 `annotation-source.json`。"
+ }
+]
\ No newline at end of file
diff --git a/.local/extract-annotation-source/output.json b/.local/extract-annotation-source/output.json
index 70fa74f..74fac35 100644
--- a/.local/extract-annotation-source/output.json
+++ b/.local/extract-annotation-source/output.json
@@ -1,110 +1,315 @@
{
- "title": "小羚羚小程序 - 原型预览(开发)",
+ "title": "合同模板管理 - 原型预览(开发)",
"errors": [],
"source": {
"directory": {
"nodes": [
{
"type": "folder",
- "id": "directory-main",
- "title": "主流程",
+ "id": "oneos-project-nav",
+ "title": "ONE-OS 原型导航",
"defaultExpanded": true,
"children": [
{
- "type": "route",
- "id": "route-todo",
- "title": "工作台",
- "route": "todo"
+ "type": "link",
+ "id": "nav-link-insurance-procurement",
+ "title": "保险采购",
+ "href": "/prototypes/insurance-procurement",
+ "target": "self"
},
{
- "type": "route",
- "id": "route-business",
- "title": "业务",
- "route": "business"
+ "type": "link",
+ "id": "nav-link-oneos-weekly-report",
+ "title": "数智部周报",
+ "href": "/prototypes/oneos-weekly-report",
+ "target": "self"
},
{
- "type": "route",
- "id": "route-map",
- "title": "地图",
- "route": "map"
- },
- {
- "type": "route",
- "id": "route-mine",
- "title": "我的",
- "route": "mine"
- }
- ]
- },
- {
- "type": "folder",
- "id": "directory-modules",
- "title": "业务模块",
- "defaultExpanded": true,
- "children": [
- {
- "type": "route",
- "id": "route-audit",
- "title": "审批中心",
- "route": "audit"
- },
- {
- "type": "route",
- "id": "route-audit-return",
- "title": "还车应结款(说明)",
- "route": "audit-return"
- },
- {
- "type": "route",
- "id": "route-delivery",
- "title": "交车",
- "route": "delivery"
- },
- {
- "type": "route",
- "id": "route-inspection",
- "title": "年审",
- "route": "inspection"
- },
- {
- "type": "route",
- "id": "route-vehicle",
+ "type": "link",
+ "id": "nav-link-vehicle-management",
"title": "车辆管理",
- "route": "vehicle"
+ "href": "/prototypes/vehicle-management",
+ "target": "self"
},
{
- "type": "route",
- "id": "route-third-return",
- "title": "三方退车",
- "route": "thirdReturn"
+ "type": "link",
+ "id": "nav-link-contract-template-management",
+ "title": "合同模板管理(当前)",
+ "href": "/prototypes/contract-template-management",
+ "target": "self"
},
{
- "type": "route",
- "id": "route-replace",
- "title": "替换车",
- "route": "replace"
+ "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-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",
+ "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": "directory-docs",
- "title": "文档",
+ "id": "ct-doc-root",
+ "title": "合同模板管理说明",
"defaultExpanded": true,
"children": [
{
"type": "markdown",
- "id": "prd-overview",
- "title": "PRD 00|小程序总览",
- "markdownPath": "src/resources/xll-miniapp/PRD-00-小程序总览.md",
- "markdown": "# 小羚羚小程序 · 总览\n\n氢能车辆运营移动端原型,面向一线运维与管理人员。**原型无需登录**,默认进入工作台。\n\n## 核心路径\n\n1. 工作台处理待办\n2. 业务 → 进入各运维 / 审批模块(独立原型页)\n3. 审批中心 → 本页内跳转**本期已接入**的审批工作流(提车应收款、还车应结款及子类型、租赁账单、车辆调拨、替换车申请等)\n\n## 页面导航\n\n左侧预览列表可切换 10 个功能页;各业务模块内列表与表单在同一页内跳转。\n\n## 最新改版\n\n审批中心与还车应结款多部门子流程见 **PRD 01|审批中心改版说明**(V1.4)。\n\n### 审批中心要点(与标注 `page-audit` 一致)\n\n- 搜索:仅按卡片加粗标题(工作流标题)模糊匹配\n- 更多筛选:发起人(滚轮选择)、发起时间(年/月/日滚轮,起止日期)\n- 底部抽屉与选择器:限定在手机预览框内,模拟小程序交互\n- 还车应结款:主流程以车牌为主标题;四部门子流程以「还车应结款-部门名」为主标题;四部门全过后须业务管理组发起主流程\n\n## 需求文档与标注\n\n- **标注目录**:PRD 00 / PRD 01 及页面说明(`page-audit` 等)\n- **导航栏**:不设「需求说明」入口;以标注工具与目录为主\n- **业务评审**:`.spec/prototype-review.md`\n"
+ "id": "ct-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`"
+ },
+ {
+ "type": "folder",
+ "id": "ct-doc-prd",
+ "title": "PRD 全文",
+ "defaultExpanded": true,
+ "children": [
+ {
+ "type": "markdown",
+ "id": "ct-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"
+ },
+ {
+ "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",
+ "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---"
+ },
+ {
+ "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---"
+ },
+ {
+ "type": "markdown",
+ "id": "ct-doc-prd-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---"
+ }
+ ]
+ },
+ {
+ "type": "folder",
+ "id": "ct-doc-list",
+ "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",
+ "title": "筛选条件",
+ "markdown": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 可搜索下拉 | 选项来自现有模板名称;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\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---"
+ }
+ ]
+ },
+ {
+ "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": "prd-approval-v11",
- "title": "PRD 01|审批中心改版说明",
- "markdownPath": "src/resources/xll-miniapp/PRD-01-审批中心改版说明.md",
- "markdown": "# 小羚羚小程序 · 审批中心改版说明\n\n**文档版本**:V1.4 \n**更新日期**:2026-06-29 \n**适用范围**:审批中心、还车应结款、提车应收款及相关导航结构\n\n---\n\n## 一、改版背景\n\n本次迭代聚焦审批体验与还车应结款多部门子流程,并调整原型信息架构:去除登录门槛、按功能拆分为独立预览页,便于评审与交付。\n\n---\n\n## 二、信息架构与导航\n\n### 2.1 去除登录\n\n- 原型默认进入「工作台 · 待办」,无需登录页。\n- 左侧预览列表可切换 **10 个功能页**(hash 路由)。\n- 源码中仍保留登录页组件与样式,**不在默认导航与预览目录中暴露**;评审以「免登录直达工作台」为准。\n\n### 2.2 页面清单\n\n| 路由 | 页面 | 说明 |\n| --- | --- | --- |\n| `todo` | 工作台 | 待办聚合,默认入口 |\n| `business` | 业务 | 运维 / 审批 / 数据可视化入口 |\n| `map` | 地图 | 氢能车与加氢站 |\n| `mine` | 我的 | 个人信息 |\n| `audit` | 审批中心 | 列表 + 本页内跳转各审批详情 |\n| `audit-return` | 还车应结款(说明) | 还车应结款专项说明页 |\n| `delivery` | 交车 | 列表与办理表单同页切换 |\n| `inspection` | 年审 | 列表与办理表单同页切换 |\n| `vehicle` | 车辆管理 | 列表与详情同页切换 |\n| `third-return` | 三方退车 | 列表与办理同页切换 |\n| `replace` | 替换车 | 列表与详情同页切换 |\n\n### 2.3 核心路径\n\n1. 工作台处理待办 → 「去处理」跳转对应业务页 \n2. 业务 → 进入各运维 / 审批模块(独立原型页) \n3. 审批中心 → 本页内打开**本期已接入**的审批工作流详情(见 §3.5)\n\n### 2.4 需求文档入口\n\n- 导航栏**不设**「需求说明」按钮(按批注已移除该入口)。\n- 需求与 PRD 通过**左侧标注目录**(「PRD 00|小程序总览」「PRD 01|审批中心改版说明」)及页面标注节点(如 `page-audit`)查看。\n\n---\n\n## 三、审批中心列表\n\n### 3.1 Tab\n\n四类:**我发起的**、**我的待办**、**我的已办**、**抄送我的**。\n\n### 3.2 筛选与搜索\n\n- **流程类型 Chip**:全部、合同审批、提车应收款、租赁账单、**还车应结款**(含四部门子类型)、车辆调拨等;「更多类型」抽屉展示全部流程类型。\n- 「还车应结款」Chip 同时匹配主流程与四个部门子流程。\n- **更多筛选**(搜索框右侧筛选按钮):**发起人**、**发起时间**(开始至结束,按日期区间过滤)。\n- **搜索**:按**卡片加粗标题(工作流标题)**模糊匹配;不按单据号、摘要等多字段联合检索。示例:提车应收款搜**客户全称**;还车应结款主流程搜**车牌号**;还车四部门子流程搜**「还车应结款-部门名」**。\n\n> **决策说明**(与 `page-audit` 标注一致):发起人、发起时间从搜索中拆出,归入「更多筛选」,搜索仅面向列表主识别字段。\n\n### 3.3 列表卡片字段\n\n以提车应收款为例:\n\n| 区域 | 说明 |\n| --- | --- |\n| 左上角标签 | 流程类型,如「提车应收款」 |\n| 右上角状态 | 审批中 + 当前节点审批人(最多 2 人,超出显示 +N) |\n| 加粗标题 | 工作流标题(见 §3.4 标题规则) |\n| 副标题 | 合同项目名称等 |\n| 元信息 | 发起人、实收金额(提车:选中车辆租金 + 服务费 + 保证金合计,**不含氢费预付款**) |\n| 底部 | 发起时间精确至分钟;待办卡片可点「去审批」或整卡进入办理页 |\n\n**还车应结款**:主流程以车牌为主标题并展示应退/应补;四部门子流程主标题为「还车应结款-部门名」,不展示汇总金额。四部门子流程全部通过后,须由**业务管理组主动发起**主流程审批,任务才会进入审批列表。\n\n### 3.4 列表卡片标题规则\n\n| 类型 | 主标题(加粗) | 辅信息 | 金额 |\n| --- | --- | --- | --- |\n| 主流程「还车应结款」 | **车牌号** | 客户、项目等 | 展示应退 / 应补 |\n| 四部门子流程 | **还车应结款-部门名** | 车牌、客户等 | 不展示汇总金额 |\n| 提车应收款 | **客户全称** | 合同项目 | 实收金额(见 §3.3) |\n| 租赁账单 / 替换车申请 | **客户全称** | — | 按业务 |\n| 车辆调拨 | **调拨路线摘要** | — | 按业务 |\n| 其他流程 | 摘要或单据号 | — | 按业务 |\n\n部门子流程包括:**还车应结款-业务管理组**、**还车应结款-能源服务组**、**还车应结款-运维组**、**还车应结款-安全部**。\n\n### 3.5 跳转与本期原型范围\n\n**已接入详情页(页内跳转)**:提车应收款、还车应结款(含四部门子类型)、租赁账单、车辆调拨、替换车申请。待办 Tab 展示底部操作栏;其余 Tab 为只读查看态。\n\n**列表占位(点击 Toast 提示,暂无办理页)**:合同审批、氢费对账单(对站/对客)、车辆异动等。演示时可在列表中看到待办,但无法进入完整办理流程。\n\n**审批操作弹窗**:待办详情底部「通过」「驳回」「终止」「评论」均从手机屏幕底部滑出半屏表单(限定在手机预览框内);通过/驳回/终止除标题外字段一致(通知方式、附件上传、抄送人、审批意见)。「通过」标题为「审批通过」。\n\n### 3.6 更多筛选交互(小程序式)\n\n| 能力 | 规则 |\n| --- | --- |\n| 底部抽屉 | 「更多筛选」及同类 `.xll-mod-sheet-*` 抽屉从**手机预览框底部**滑出,遮罩与面板相对当前页面模块定位,**不铺满浏览器全屏** |\n| 发起人 | 表单行展示「请选择 ›」;点击后弹出底部**滚轮选择器**,选项来自 mock 发起人列表,首项「不限」 |\n| 发起时间 | 「开始日期 至 结束日期」两行可点;分别弹出**年 / 月 / 日**三列滚轮,工具栏「取消 / 确定」 |\n| 嵌套层级 | 滚轮选择器叠在筛选抽屉之上(`z-index` 更高),同样限定在手机预览框内;选中高亮条在文字下层,不遮挡选项文字 |\n\n---\n\n## 四、还车应结款审批\n\n### 4.1 流程结构\n\n**主工作流**:`还车应结款` — 汇总四部门费用,判定保证金抵扣后的应退/应补。\n\n**四个子流程**(各部门独立提交审批):\n\n| 子流程 | 提交部门 |\n| --- | --- |\n| 还车应结款-业务管理组 | 业务管理组 |\n| 还车应结款-能源服务组 | 能源服务组 |\n| 还车应结款-运维组 | 运维组 |\n| 还车应结款-安全部 | 安全部 |\n\n**提交规则**:\n\n1. 四个子流程分别由对应部门填报费用并**独立提交审批**;\n2. 四个子流程**全部审批通过**后,**不会自动生成**主流程「还车应结款」审批任务;\n3. 须由**业务管理组**在还车结算侧**主动发起**主流程审批,发起后任务进入审批列表;\n4. 主流程进入列表后,由财务等环节整体审批;详情页展示四部门子流程通过状态;\n5. 四部门未全部通过前,主流程不可发起;若已进入主流程详情,「通过」操作在四部门未全过时仍被拦截。\n\n#### 原型 mock 场景(开发 / 测试对照)\n\n| 场景 | 车牌 | 单据号 | 说明 |\n| --- | --- | --- | --- |\n| A | 粤B58888F | HC-2026-0418 | 四部门已通过 → 业务管理组已发起 → 主流程在「我的待办」(财务审核) |\n| B | 浙F06901F | HC-2026-0625 | 四部门子流程均在待办(审批中);主流程未发起 |\n| C | 苏E12345F | HC-2026-0610 | 四部门已通过 → 业务管理组**尚未发起** → 主流程不出现在列表 |\n\n> 主流程发起人 mock 为业务管理组人员(如陈高伟);发起时间晚于四部门子流程全部通过时间。\n\n### 4.2 Hero 区规则\n\n| 类型 | Hero 主信息 | 是否展示应退/应补 |\n| --- | --- | --- |\n| 主流程「还车应结款」 | 应退还总额 / 应补缴总额;保证金、待结算总额、车辆实际租金;「结算明细」抽屉;部门子流程进度 | **是** |\n| 业务管理组子流程 | 「还车应结款-业务管理组」+ **车牌重点样式** | 否 |\n| 能源服务组子流程 | 「还车应结款-能源服务组」+ 车牌重点样式 | 否 |\n| 运维组子流程 | 「还车应结款-运维组」+ 车牌重点样式 | 否 |\n| 安全部子流程 | 「还车应结款-安全部」+ 车牌重点样式 | 否 |\n\n**车牌重点样式**:Hero 内展示「车牌号」标签 + 加粗胶囊样式车牌,便于子流程快速识别车辆。\n\n子流程 Hero **不展示**保证金、待结算总额、应退/应补等汇总类金额。\n\n### 4.3 分组费用明细\n\n#### 业务管理组\n\n- 固定费项表格(如 ETC 费用、停车费等)。\n- **不含**违章处理违约金、保险上浮(归属安全部)。\n- 底部展示本部门应结算总额。\n\n#### 能源服务组\n\n- 氢量差补缴、交/还车氢量、退还单价、预付款退费等。\n\n#### 运维组\n\n- 费项:清洗、保养、维修、车损、工具/证件/广告丢失、送接车服务、轮胎磨损等。\n- 表格列:**序号 / 费用项 / 金额 / 无忧包减免**(横向滚动,减免为输入框样式只读展示)。\n- **应结算 = 金额 − 无忧包减免**(按行合计后汇总)。\n\n#### 安全部\n\n- **固定费项三行**(由安全部填报):\n 1. 违章处理违约金 \n 2. 保险上浮 \n 3. 其他违规费用 \n- 保留违章统计区:违章次数、已缴/未缴金额等。\n- 应结算总额为上述三行费用合计。\n\n#### 部门结算卡片状态标签\n\n各分组费用卡片右上角展示本部门子流程提交状态:\n\n| 状态 | 展示文案 | 触发条件 |\n| --- | --- | --- |\n| 待提交 | 待提交 | 表单未提交、子流程未发起 |\n| 审批中 | 审批中 | 用户已提交子流程审批 |\n| 已提交 | 已提交 | 该部门子流程全部审批完成 |\n| 已驳回 | 已驳回 | 子流程审批被驳回 |\n\n**主流程详情**:四张分组卡片右上角均展示「已提交」(四部门已全部完成子流程审批,且业务管理组已发起主流程后才可进入主流程详情)。\n\n### 4.4 审批操作\n\n- **审批情况**:纵向时间轴,节点展示审批人头像、状态 Tag、时间与评论气泡。\n- **待办态底部**:评论、终止、驳回、通过(规则同提车应收款)。\n- **查看态**:隐藏操作栏,仅浏览。\n\n---\n\n## 五、提车应收款审批\n\n### 5.1 页面目标\n\n审批人核对实收金额、车辆明细、氢费预付款(如有)及开票信息。\n\n### 5.2 布局细节\n\n- **车辆明细**:区块标题行(`.tc-section-head`)与首张车辆卡片之间保留 **12px** 上间距,避免标题分隔线与首卡紧贴。\n- **审批情况**:时间轴支持评论展示;待办态底部操作同还车应结款。\n\n### 5.3 实收金额口径\n\n| 位置 | 规则 |\n| --- | --- |\n| 列表卡片 | 选中车辆 **租金 + 服务费 + 保证金 − 减免** 合计(**不含氢费预付款**),由车辆明细动态汇总 |\n| 详情 Hero | 与列表卡片**同一口径**,展示实收款总额 |\n\n> **原型缺口**:当前列表已按 §3.3 动态计算,部分 mock 详情 Hero 仍使用静态 `actualAmount` 字段,评审时需对齐(见 `.spec/prototype-review.md`)。\n\n---\n\n## 六、验收要点\n\n请在预览中逐项确认(可提供截图或页面路径反馈):\n\n- [ ] 打开原型无需登录,默认进入工作台 \n- [ ] 左侧可切换 10 个功能页 \n- [ ] 导航栏无「需求说明」按钮;PRD 可从标注目录打开 \n- [ ] 审批中心「还车应结款」Chip 可筛出主流程 + 四部门子流程 \n- [ ] **更多筛选**抽屉在手机预览框内弹出(非全浏览器宽) \n- [ ] **更多筛选**:发起人、发起时间为滚轮选择器(非原生输入框) \n- [ ] **搜索**仅匹配卡片加粗标题(工作流标题) \n- [ ] 子流程列表卡片主标题为「还车应结款-部门名」;主流程卡片以车牌为主标题并展示应退/应补 \n- [ ] 四部门子流程详情 Hero 为部门标题 + 车牌重点样式,无汇总金额 \n- [ ] 主流程还车应结款 Hero 展示应退/应补、结算明细与四部门子流程进度 \n- [ ] 四部门子流程未全部通过时,主流程不可发起;四部门全过但业务管理组未发起时,主流程不出现在列表 \n- [ ] 业务管理组无违章违约金、保险上浮;安全部固定三行费项 \n- [ ] 运维组费用表含「无忧包减免」列,应结算 = 金额 − 减免 \n- [ ] 提车应收款车辆明细首卡与标题间距 12px \n- [ ] 提车列表实收金额与详情 Hero 口径一致(不含氢费预付款) \n- [ ] 审批时间轴展示头像、状态与评论;待办态可操作通过/驳回/终止/评论 \n\n**预览地址示例**:`http://localhost:51721/prototypes/xll-miniapp#page=audit`\n\n---\n\n## 七、关联文档\n\n| 文档 | 位置 |\n| --- | --- |\n| 小程序总览 | 标注目录「PRD 00|小程序总览」 |\n| 原型评审结论 | `src/prototypes/xll-miniapp/.spec/prototype-review.md` |\n| 页面标注 | `page-audit`、`page-audit-return`、`page-audit-pickup` 等 |\n"
+ "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`。"
}
]
}
@@ -112,294 +317,318 @@
},
"nodes": [
{
- "id": "page-todo",
+ "id": "ct-list-filter",
"index": 1,
- "title": "工作台待办",
- "pageId": "todo",
+ "title": "筛选条件",
+ "pageId": "list",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-todo\"]"
- ]
+ "[data-annotation-id=\"ct-list-filter\"]"
+ ],
+ "fingerprint": "ct-list-filter",
+ "path": []
},
- "aiPrompt": "说明待办任务聚合规则与去处理跳转。",
- "annotationText": "# 工作台 · 待办\n\n聚合交车、还车、年审、调拨、异动等待处理任务。\n\n- 卡片按任务类型色条区分\n- 「去处理」根据类型跳转对应业务页或本页任务办理\n- 原型入口默认打开本页,无需登录",
+ "aiPrompt": "列表页筛选区字段与查询逻辑。",
+ "annotationText": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 可搜索下拉 | 选项来自现有模板名称;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认",
"hasMarkdown": true,
- "color": "#7AB929",
+ "color": "#2563eb",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "main-tabbar",
+ "id": "ct-list-toolbar",
"index": 2,
- "title": "底部主导航",
- "pageId": [
- "todo",
- "business",
- "map",
- "mine"
- ],
+ "title": "新增模板",
+ "pageId": "list",
"locator": {
"selectors": [
- "[data-annotation-id=\"main-tabbar\"]"
- ]
+ "[data-annotation-id=\"ct-list-toolbar\"]"
+ ],
+ "fingerprint": "ct-list-toolbar",
+ "path": []
},
- "aiPrompt": "说明四个主 Tab 的信息架构。",
- "annotationText": "# 底部主导航\n\n| Tab | 说明 |\n| --- | --- |\n| 工作台 | 待办任务列表 |\n| 业务 | 运维与审批入口 |\n| 地图 | 车辆 / 加氢站 |\n| 我的 | 账号信息 |",
- "hasMarkdown": true,
- "color": "#2563EB",
+ "aiPrompt": "列表工具栏新增入口。",
+ "annotationText": "进入新增页,默认从「合同主体」章节开始编辑。",
+ "hasMarkdown": false,
+ "color": "#32a06e",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-business",
+ "id": "ct-list-table",
"index": 3,
- "title": "业务入口",
- "pageId": "business",
+ "title": "Word 模板列表",
+ "pageId": "list",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-business\"]"
- ]
+ "[data-annotation-id=\"ct-list-table\"]"
+ ],
+ "fingerprint": "ct-list-table",
+ "path": []
},
- "aiPrompt": "说明运维管理、审批管理与数据可视化分区。",
- "annotationText": "# 业务页\n\n分三大区:运维管理、审批管理、数据可视化。\n\n点击模块图标跳转到对应独立原型页(hash 路由),各页内列表与表单可直接跳转。",
+ "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": "#F97316",
+ "color": "#0f766e",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-audit",
+ "id": "ct-editor-topbar",
"index": 4,
- "title": "审批中心列表",
- "pageId": "audit",
+ "title": "编辑页顶栏",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-audit\"]"
- ]
+ "[data-annotation-id=\"ct-editor-topbar\"]"
+ ],
+ "fingerprint": "ct-editor-topbar",
+ "path": []
},
- "aiPrompt": "说明审批中心 Tab、筛选与还车应结款类目;列表内可跳转各审批流详情。",
- "annotationText": "# 审批中心\n\n四类列表:我发起的、我的待办、我的已办、抄送我的。\n\n### 筛选与搜索\n\n- **流程类型 Chip**:全部、合同审批、提车应收款、租赁账单、还车应结款(含四部门子类型)、车辆调拨;「更多类型」抽屉展示全部流程类型。\n- **更多筛选**(搜索框右侧筛选按钮):发起人、发起时间(开始至结束)。\n- **搜索**:按卡片加粗标题(工作流标题)模糊匹配,如提车应收款为**客户全称**、还车应结款为**车牌号**。\n\n### 更多筛选交互(小程序式)\n\n| 能力 | 说明 |\n| --- | --- |\n| 底部抽屉 | 「更多筛选」限定在**手机预览框内**弹出,不铺满浏览器(`.xll-mod-sheet-*`) |\n| 发起人 | 点击「请选择 ›」→ 底部滚轮;首项「不限」 |\n| 发起时间 | 「开始日期 至 结束日期」→ 年/月/日三列滚轮,取消/确定 |\n| 嵌套选择器 | 滚轮叠在筛选抽屉之上,高亮条在文字下层 |\n\n### 列表卡片(以提车应收款为例)\n\n| 区域 | 说明 |\n| --- | --- |\n| 左上角标签 | 流程类型,如「提车应收款」 |\n| 右上角状态 | 审批中 + 当前节点审批人(最多 2 人,超出显示 +N) |\n| 加粗标题 | 客户名称全称 |\n| 副标题 | 合同项目名称 |\n| 元信息 | 发起人、实收金额(选中车辆租金+服务费+保证金合计) |\n| 底部 | 发起时间精确至分钟;待办卡片可点「去审批」或整卡进入办理页 |\n\n**还车应结款**:主流程以车牌为主标题;子流程主标题为「还车应结款-部门名」。四部门子流程全部通过后,须由**业务管理组主动发起**主流程审批,任务才会进入审批列表。\n\n### 需求文档入口\n\n导航栏**不设**「需求说明」按钮;PRD 见标注目录「PRD 01|审批中心改版说明」。\n\n### 审批操作弹窗\n\n待办详情底部「通过」「驳回」「终止」「评论」均从手机屏幕底部滑出半屏表单(限定在手机预览框内,非全浏览器宽);通过/驳回/终止除标题外字段一致(通知方式、附件上传、抄送人、审批意见)。「通过」标题为「审批通过」。",
+ "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": "#8B5CF6",
+ "color": "#64748b",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1782705600000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-audit-return",
+ "id": "ct-section-tabs",
"index": 5,
- "title": "还车应结款审批",
- "pageId": "audit-return",
+ "title": "章节 Tab",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-audit-return\"]"
- ]
+ "[data-annotation-id=\"ct-section-tabs\"]"
+ ],
+ "fingerprint": "ct-section-tabs",
+ "path": []
},
- "aiPrompt": "说明还车应结款汇总审批与四部门子流程差异。",
- "annotationText": "# 还车应结款\n\n**主工作流**「还车应结款」汇总四部门费用与应退/应补总额。\n\n**四个子流程**(各部门独立提交):还车应结款-业务管理组、还车应结款-能源服务组、还车应结款-运维组、还车应结款-安全部。\n\n**主流程发起规则**:四部门子流程全部审批通过后,**不会自动生成**主流程任务;须由**业务管理组主动发起**主流程审批,发起后任务才会出现在审批列表。\n\n子流程 Hero 统一为「还车应结款-部门名」+ 车牌重点样式,仅展示本部门费用。\n\n### 原型 mock 场景\n\n| 场景 | 车牌 | 说明 |\n| --- | --- | --- |\n| A | 粤B58888F | 四部门已通过 → 业务管理组已发起 → 主流程在待办 |\n| B | 浙F06901F | 四部门子流程均在待办 |\n| C | 苏E12345F | 四部门已通过 → 业务管理组尚未发起 → 主流程不在列表 |\n\n### 部门结算卡片状态标签\n\n各分组费用卡片右上角展示本部门子流程提交状态:\n\n| 状态 | 展示文案 | 触发条件 |\n| --- | --- | --- |\n| 待提交 | 待提交 | 表单未提交、子流程未发起 |\n| 审批中 | 审批中 | 用户已提交子流程审批 |\n| 已提交 | 已提交 | 该部门子流程全部审批完成 |\n| 已驳回 | 已驳回 | 子流程审批被驳回 |\n\n**主流程详情**:四张分组卡片右上角均展示「已提交」(四部门已全部完成子流程审批,且业务管理组已发起主流程后才可进入)。\n\n| 部门 | 费用要点 |\n| --- | --- |\n| 业务管理组 | ETC、停车费等 |\n| 能源服务组 | 氢量差补缴、预付款退费等 |\n| 运维组 | 含「无忧包减免」列,应结算 = 金额 − 减免 |\n| 安全部 | 违章违约金、保险上浮、其他违规费用 |\n\n详见「PRD 01|审批中心改版说明」。",
+ "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": "#8B5CF6",
+ "color": "#2563eb",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1782705600000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1782820000000,
"controls": []
},
{
- "id": "hc-settle-group-status",
+ "id": "ct-contract-type",
"index": 6,
- "title": "部门结算状态标签",
- "pageId": "audit-return",
+ "title": "合同类型",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"hc-settle-group-status\"]"
- ]
+ "[data-annotation-id=\"ct-contract-type\"]"
+ ],
+ "fingerprint": "ct-contract-type",
+ "path": []
},
- "aiPrompt": "说明四部门结算卡片右上角状态标签的四种展示规则。",
- "annotationText": "1. 表单未提交前展示「待提交」\n2. 用户提交子流程审批后展示「审批中」\n3. 该部门子流程全部审批完成后展示「已提交」\n4. 审批被驳回时展示「已驳回」\n\n**主流程**:四部门子流程全部通过后,须由**业务管理组主动发起**主流程审批,发起后任务才会出现在审批列表;主流程详情页四张分组卡片右上角均展示「已提交」(绿色)。\n\n**子流程详情**:按各部门实际审批状态展示对应标签。",
- "hasMarkdown": false,
- "color": "#8B5CF6",
+ "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": 1782792000000,
- "updatedAt": 1782752350053,
+ "createdAt": 1740508800000,
+ "updatedAt": 1782820000000,
"controls": []
},
{
- "id": "tc-btn-approve",
+ "id": "ct-vehicle-binding",
"index": 7,
- "title": "审批通过按钮",
- "pageId": "audit",
+ "title": "适用车型绑定",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"tc-btn-approve\"]",
- ".tc-btn-approve"
- ]
+ "[data-annotation-id=\"ct-vehicle-binding\"]"
+ ],
+ "fingerprint": "ct-vehicle-binding",
+ "path": []
},
- "aiPrompt": "说明审批中心各流程详情底部「通过」按钮的弹窗交互。",
- "annotationText": "点击「通过」弹出底部半屏表单(标题为「审批通过」),表单字段与「驳回」「终止」一致:通知方式、附件上传、抄送人、审批意见;底部为「取消」「确认」。弹窗限定在手机预览区域内自底部滑出。",
- "hasMarkdown": false,
- "color": "#7AB929",
+ "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": 1782878400000,
- "updatedAt": 1782878400000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-audit-pickup",
+ "id": "ct-editor-panel",
"index": 8,
- "title": "提车应收款审批",
- "pageId": "audit-pickup",
+ "title": "编辑区",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-audit-pickup\"]"
- ]
+ "[data-annotation-id=\"ct-editor-panel\"]"
+ ],
+ "fingerprint": "ct-editor-panel",
+ "path": []
},
- "aiPrompt": "说明提车应收款 Hero、车辆明细间距与审批操作。",
- "annotationText": "# 提车应收款\n\nHero 展示实收款总额与客户/合同/项目信息。\n\n**车辆明细**:区块标题与首张车辆卡片之间保留 12px 上间距。\n\n审批时间轴支持评论;待办态底部评论/终止/驳回/通过。",
+ "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": "#F97316",
+ "color": "#7c3aed",
"images": [],
- "createdAt": 1782705600000,
- "updatedAt": 1782705600000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-delivery",
+ "id": "ct-rte-risk-redline",
"index": 9,
- "title": "交车",
- "pageId": "delivery",
+ "title": "红线",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-delivery\"]"
- ]
+ "[data-annotation-id=\"ct-rte-risk-redline\"]"
+ ],
+ "fingerprint": "ct-rte-risk-redline",
+ "path": []
},
- "aiPrompt": "说明交车列表与多步骤办理表单的内页跳转。",
- "annotationText": "# 交车\n\n列表与多步骤交车办理在同一原型页内切换。",
- "hasMarkdown": true,
- "color": "#7AB929",
+ "aiPrompt": "风控红线条款标记。",
+ "annotationText": "红线功能主要用于在章节中选择特定内容,点击红线将其设置为篡改后自动变成非标合同。不修改红线内内容不会导致流程变为非标。",
+ "hasMarkdown": false,
+ "color": "#dc2626",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-inspection",
+ "id": "ct-rte-unrisk-redline",
"index": 10,
- "title": "年审",
- "pageId": "inspection",
+ "title": "取消红线",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-inspection\"]"
- ]
+ "[data-annotation-id=\"ct-rte-unrisk-redline\"]"
+ ],
+ "fingerprint": "ct-rte-unrisk-redline",
+ "path": []
},
- "aiPrompt": "说明年审列表与检测表单的内页跳转。",
- "annotationText": "# 年审\n\n待办列表与检测表单在同一原型页内切换。",
- "hasMarkdown": true,
- "color": "#FF7D00",
+ "aiPrompt": "取消风控红线标记。",
+ "annotationText": "取消功能主要用于取消被选中的部分的红线内容。取消后,在新增租赁合同时修改该部分内容,不会导致流程变为非标准流程。",
+ "hasMarkdown": false,
+ "color": "#94a3b8",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-vehicle",
+ "id": "ct-rte-vehicle-clause",
"index": 11,
- "title": "车辆管理",
- "pageId": "vehicle",
+ "title": "条件",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-vehicle\"]"
- ]
+ "[data-annotation-id=\"ct-rte-vehicle-clause\"]"
+ ],
+ "fingerprint": "ct-rte-vehicle-clause",
+ "path": []
},
- "aiPrompt": "说明车辆列表与详情页的内页跳转。",
- "annotationText": "# 车辆管理\n\n车辆列表与详情在同一原型页内切换。",
- "hasMarkdown": true,
- "color": "#2563EB",
+ "aiPrompt": "条件条款(段落级车型显隐)。",
+ "annotationText": "条件可在合同主体及租赁订单~安全责任协议全部章节使用:圈定章节内具体内容,使部分条款仅对指定车型生效。与各章节「适用车型」(控制整章是否可见)不同,条件只作用于章节内部分段落。",
+ "hasMarkdown": false,
+ "color": "#2563eb",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-third-return",
+ "id": "ct-rte-unvehicle-clause",
"index": 12,
- "title": "三方退车",
- "pageId": "third-return",
+ "title": "取消条件",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-third-return\"]"
- ]
+ "[data-annotation-id=\"ct-rte-unvehicle-clause\"]"
+ ],
+ "fingerprint": "ct-rte-unvehicle-clause",
+ "path": []
},
- "aiPrompt": "说明三方退车列表与办理表单的内页跳转。",
- "annotationText": "# 三方退车\n\n退车任务列表与办理表单在同一原型页内切换。",
- "hasMarkdown": true,
- "color": "#EA580C",
+ "aiPrompt": "取消条件条款标记。",
+ "annotationText": "当条款被设置条件后,可以通过选中特定条款内容,点击取消来取消该部分内容的车型限定。",
+ "hasMarkdown": false,
+ "color": "#64748b",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-replace",
+ "id": "ct-rte-inline-lock",
"index": 13,
- "title": "替换车",
- "pageId": "replace",
+ "title": "锁定",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-replace\"]"
- ]
+ "[data-annotation-id=\"ct-rte-inline-lock\"]"
+ ],
+ "fingerprint": "ct-rte-inline-lock",
+ "path": []
},
- "aiPrompt": "说明替换车列表与申请/审批详情的内页跳转。",
- "annotationText": "# 替换车\n\n申请列表与详情/办理在同一原型页内切换。",
- "hasMarkdown": true,
- "color": "#14B8A6",
+ "aiPrompt": "内容锁定区标记。",
+ "annotationText": "锁定主要用于选中章节内的内容;锁定之后该部分内容无法在新增租赁合同时被篡改。红线、条件以及锁定可以在同一段内容同时生效。",
+ "hasMarkdown": false,
+ "color": "#b45309",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-map",
+ "id": "ct-rte-uninline-lock",
"index": 14,
- "title": "地图",
- "pageId": "map",
+ "title": "解除锁定",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-map\"]"
- ]
+ "[data-annotation-id=\"ct-rte-uninline-lock\"]"
+ ],
+ "fingerprint": "ct-rte-uninline-lock",
+ "path": []
},
- "aiPrompt": "说明氢能车与加氢站地图切换。",
- "annotationText": "# 地图\n\n氢能车 Tab 展示车辆实时位置;加氢站 Tab 展示站点 POI。\n\n支持车牌搜索与全图视野(原型提示)。",
- "hasMarkdown": true,
- "color": "#0EA5E9",
+ "aiPrompt": "解除内容锁定标记。",
+ "annotationText": "选中已锁定内容后点击解除;解除后新增租赁合同时可再次编辑该部分。",
+ "hasMarkdown": false,
+ "color": "#94a3b8",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
},
{
- "id": "page-mine",
+ "id": "ct-preview-panel",
"index": 15,
- "title": "我的",
- "pageId": "mine",
+ "title": "实时预览",
+ "pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"page-mine\"]"
- ]
+ "[data-annotation-id=\"ct-preview-panel\"]"
+ ],
+ "fingerprint": "ct-preview-panel",
+ "path": []
},
- "aiPrompt": "说明个人中心信息展示。",
- "annotationText": "# 我的\n\n展示当前用户头像、姓名、岗位与联系方式。",
+ "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": "#4E5969",
+ "color": "#0ea5e9",
"images": [],
- "createdAt": 1779667200000,
- "updatedAt": 1779667200000,
+ "createdAt": 1740508800000,
+ "updatedAt": 1740508800000,
"controls": []
}
]
diff --git a/package.json b/package.json
index 78fd2cf..d7e59d6 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"dev": "node scripts/sync-vendor-if-present.mjs && vite",
"start": "npm run dev",
"metadata:sync": "node scripts/sync-project-metadata.mjs",
+ "project-nav:sync": "node scripts/sync-project-prototype-directory.mjs",
"oneos-web:sync": "node scripts/sync-oneos-web-prototypes.mjs && node scripts/scan-entries.js && npm run metadata:sync",
"build": "node scripts/sync-vendor-if-present.mjs && node scripts/scan-entries.js && node scripts/build-all.js",
"preview": "vite preview",
diff --git a/scripts/sync-project-prototype-directory.mjs b/scripts/sync-project-prototype-directory.mjs
new file mode 100644
index 0000000..6de722d
--- /dev/null
+++ b/scripts/sync-project-prototype-directory.mjs
@@ -0,0 +1,188 @@
+/**
+ * 将 .axhub/make/sidebar-tree.json 左侧菜单同步到各原型 annotation-source.json 的「原型目录」。
+ *
+ * 用法:
+ * node scripts/sync-project-prototype-directory.mjs
+ * node scripts/sync-project-prototype-directory.mjs --prototype vehicle-management
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const NAV_FOLDER_ID = 'oneos-project-nav';
+
+function parseArgs(argv) {
+ const args = { prototype: '', projectRoot: '' };
+ for (let i = 0; i < argv.length; i += 1) {
+ const arg = argv[i];
+ if (arg === '--prototype' && argv[i + 1]) {
+ args.prototype = argv[++i].trim();
+ } else if (arg === '--project-root' && argv[i + 1]) {
+ args.projectRoot = path.resolve(argv[++i].trim());
+ }
+ }
+ return args;
+}
+
+function readJson(filePath) {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+}
+
+function writeJson(filePath, value) {
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
+}
+
+function prototypeIdFromItemKey(itemKey) {
+ const normalized = String(itemKey || '').trim().replace(/^\/+/u, '');
+ const match = normalized.match(/^prototypes\/(.+)$/u);
+ return match ? match[1] : '';
+}
+
+function navLinkId(prototypeId) {
+ return `nav-link-${prototypeId.replace(/[^a-zA-Z0-9]+/gu, '-').replace(/^-+|-+$/gu, '').toLowerCase()}`;
+}
+
+function createLinkNode(sidebarItem, currentPrototypeId) {
+ const prototypeId = prototypeIdFromItemKey(sidebarItem.itemKey);
+ if (!prototypeId) return null;
+ const isCurrent = prototypeId === currentPrototypeId;
+ return {
+ type: 'link',
+ id: navLinkId(prototypeId),
+ title: isCurrent ? `${sidebarItem.title}(当前)` : sidebarItem.title,
+ href: `/prototypes/${prototypeId}`,
+ target: 'self',
+ };
+}
+
+function sidebarItemsToDirectoryChildren(sidebarItems, currentPrototypeId) {
+ const children = [];
+ for (const item of sidebarItems || []) {
+ if (item?.kind === 'item') {
+ const link = createLinkNode(item, currentPrototypeId);
+ if (link) children.push(link);
+ continue;
+ }
+ if (item?.kind !== 'folder' || !Array.isArray(item.children)) {
+ continue;
+ }
+ const folderChildren = item.children
+ .filter((child) => child?.kind === 'item')
+ .map((child) => createLinkNode(child, currentPrototypeId))
+ .filter(Boolean);
+ if (!folderChildren.length) continue;
+ children.push({
+ type: 'folder',
+ id: item.id ? `nav-${item.id}` : `nav-folder-${children.length + 1}`,
+ title: item.title || '分组',
+ defaultExpanded: item.defaultExpanded !== false,
+ children: folderChildren,
+ });
+ }
+ return children;
+}
+
+function loadSidebarPrototypes(projectRoot) {
+ const sidebarPath = path.join(projectRoot, '.axhub/make/sidebar-tree.json');
+ if (!fs.existsSync(sidebarPath)) {
+ throw new Error(`缺少侧边栏配置:${sidebarPath}`);
+ }
+ const sidebar = readJson(sidebarPath);
+ return Array.isArray(sidebar.prototypes) ? sidebar.prototypes : [];
+}
+
+function listPrototypeIdsWithAnnotation(projectRoot) {
+ const prototypesRoot = path.join(projectRoot, 'src/prototypes');
+ if (!fs.existsSync(prototypesRoot)) return [];
+ return fs.readdirSync(prototypesRoot, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory()
+ && fs.existsSync(path.join(prototypesRoot, entry.name, 'annotation-source.json')))
+ .map((entry) => entry.name)
+ .sort((a, b) => a.localeCompare(b, 'zh-CN'));
+}
+
+function fixPublishedLinks(nodes, prototypeId) {
+ const walk = (items) => {
+ for (const node of items || []) {
+ if (node?.type === 'link' && typeof node.href === 'string') {
+ const href = node.href.trim();
+ const selfMatch = href.match(new RegExp(`^/prototypes/${prototypeId.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}(?:[#?].*)?$`, 'u'));
+ if (selfMatch) {
+ node.href = './index.html';
+ node.target = node.target || 'self';
+ } else {
+ const otherMatch = href.match(/^\/prototypes\/([^/?#]+)(?:[#?].*)?$/u);
+ if (otherMatch) {
+ node.href = `../${otherMatch[1]}/index.html`;
+ node.target = node.target || 'self';
+ }
+ }
+ }
+ if (node?.type === 'folder' && Array.isArray(node.children)) {
+ walk(node.children);
+ }
+ }
+ };
+ walk(nodes);
+}
+
+function syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot) {
+ const annotationPath = path.join(projectRoot, 'src/prototypes', prototypeId, 'annotation-source.json');
+ if (!fs.existsSync(annotationPath)) {
+ console.log(`[skip] ${prototypeId}: 无 annotation-source.json`);
+ return false;
+ }
+
+ const annotation = readJson(annotationPath);
+ const navChildren = sidebarItemsToDirectoryChildren(sidebarItems, prototypeId);
+ const navFolder = {
+ type: 'folder',
+ id: NAV_FOLDER_ID,
+ title: 'ONE-OS 原型导航',
+ defaultExpanded: true,
+ children: navChildren,
+ };
+
+ const existingNodes = Array.isArray(annotation.directory?.nodes) ? annotation.directory.nodes : [];
+ const preservedNodes = existingNodes.filter((node) => node?.id !== NAV_FOLDER_ID);
+ fixPublishedLinks(preservedNodes, prototypeId);
+
+ annotation.directory = {
+ nodes: [navFolder, ...preservedNodes],
+ };
+ annotation.data = annotation.data || {};
+ annotation.data.updatedAt = Date.now();
+
+ writeJson(annotationPath, annotation);
+ console.log(`[sync] ${prototypeId}: 已写入 ${navChildren.length} 个顶级目录项(含分组)`);
+ return true;
+}
+
+function main() {
+ const args = parseArgs(process.argv.slice(2));
+ const projectRoot = args.projectRoot || path.resolve(__dirname, '..');
+ const sidebarItems = loadSidebarPrototypes(projectRoot);
+ const targets = args.prototype
+ ? [args.prototype]
+ : listPrototypeIdsWithAnnotation(projectRoot);
+
+ if (!sidebarItems.length) {
+ console.error('sidebar-tree.json 中未找到 prototypes 菜单。');
+ process.exit(1);
+ }
+ if (!targets.length) {
+ console.log('未找到带 annotation-source.json 的原型。');
+ process.exit(0);
+ }
+
+ let changed = 0;
+ for (const prototypeId of targets) {
+ if (syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot)) {
+ changed += 1;
+ }
+ }
+ console.log(`完成:处理 ${targets.length} 个原型,更新 ${changed} 个 annotation-source.json`);
+}
+
+main();
diff --git a/src/common/useHashPage.ts b/src/common/useHashPage.ts
index c28299f..626636d 100644
--- a/src/common/useHashPage.ts
+++ b/src/common/useHashPage.ts
@@ -110,6 +110,19 @@ function notifyHostPrototypeRouteInfo(
}, '*');
}
+/** 单页原型挂载时调用,清除 Make 侧栏中残留的多页子路由(如从「工作台」切过来后仍显示子页) */
+export function clearHostPrototypeRouteInfo() {
+ if (typeof window === 'undefined' || window.parent === window) {
+ return;
+ }
+ window.parent.postMessage({
+ type: 'AXHUB_PROTOTYPE_ROUTE_INFO',
+ pages: [],
+ defaultPageId: '',
+ activePageId: '',
+ }, '*');
+}
+
export function useHashPage(routeOrDefault: HashPageRoute | string = 'home') {
const route = normalizeRouteInput(routeOrDefault);
const { pages, defaultPageId } = route;
diff --git a/src/prototypes/contract-template-management/.spec/prototype-review.md b/src/prototypes/contract-template-management/.spec/prototype-review.md
new file mode 100644
index 0000000..e857e72
--- /dev/null
+++ b/src/prototypes/contract-template-management/.spec/prototype-review.md
@@ -0,0 +1,54 @@
+# Prototype Review
+
+- 审查目标:`src/prototypes/contract-template-management`
+- 用户资料/参考资料:
+ - 本轮用户任务:处理评审结论中 P1–P3 共 5 条问题
+ - `src/resources/contract-template-management/PRD.md`(v1.2)
+ - `src/prototypes/contract-template-management/annotation-source.json`
+ - `contract-template-seed.js`、`contract-template-section-html.js`、`contract-template-catalog.js`、`contract-template-store.js`
+ - 关联下游:`lease-contract-preview-build.js`、`LeaseContractCreate.jsx`
+- 生成时间:2026-06-29(复审)
+
+## 总体点评
+
+上一轮评审中的 **5 条优先级问题均已落地**。合同模板管理原型现已具备:
+
+- 租赁创建预览优先消费 store 中 `sectionContents`(静态 HTML 仅兜底)
+- 冷启动种子:直接进入租赁创建即可选用已启用模板
+- PRD / 标注与实现对齐(`contractType`、列表筛选、子表列、章节 Tab 交互)
+- `localStorage` 持久化(兼写 `sessionStorage`)
+- 移除 `isDefault` /「标准模板」残留逻辑
+
+原型可作为研发与测试的**端到端演示基线**;正式环境仍需 API 持久化、权限与异常分支。
+
+## P0-P3 优先级问题(复审)
+
+| 级别 | 问题 | 状态 |
+|---|---|---|
+| P1 | 租赁预览未消费 store 章节正文 | **已修复** — `getContractTemplateBaseHtml` 优先 `mergeMonolithicHtml(doc.sectionContents)` |
+| P1 | PRD / 标注与实现字段口径不一致 | **已修复** — PRD v1.2 + `build-annotation-source.mjs` 同步 |
+| P1 | 冷启动租赁创建无模板 | **已修复** — `catalog.js` 调用 `ensureContractTemplateStore(buildInitialWordTemplates)` |
+| P2 | 仅 sessionStorage 持久化 | **已缓解** — store 读写 `localStorage` 优先 |
+| P3 | `isDefault` / 标准模板残留 | **已修复** — 种子、store 默认逻辑、编辑区文案已清理 |
+
+## 完整性与项目对齐
+
+| 维度 | 评估 |
+|---|---|
+| 跨模块正文 | 模板管理编辑 ↔ 租赁创建预览:**已贯通**(同会话 / localStorage 内) |
+| 冷启动 | 无需先打开模板管理:**已支持** |
+| 资料一致性 | PRD、标注、列表筛选/列、子表、§8.3 Tab:**已对齐** |
+| 正式环境差距 | API 持久化、角色权限、Word 解析失败等异常分支仍待研发 |
+
+## 验收建议(请用户在预览中确认)
+
+1. **租赁预览贯通**:在模板管理编辑某章节并保存 → 打开租赁创建 → 选用同一模板 → 预览正文应反映修改。
+2. **冷启动**:清空站点 localStorage 后**直接**打开租赁创建 → 应能选择已启用模板(如「正式合同」「试用合同」)。
+3. **列表筛选**:列表页「合同类型」筛选与主表「合同类型」列一致。
+4. **持久化**:刷新浏览器后模板停用/发布状态应保留(localStorage)。
+
+## 修订记录
+
+| 日期 | 说明 |
+|---|---|
+| 2026-06-29 | 复审:5 条评审问题全部处理完毕 |
diff --git a/src/prototypes/contract-template-management/.spec/ui-review.md b/src/prototypes/contract-template-management/.spec/ui-review.md
new file mode 100644
index 0000000..b3e919e
--- /dev/null
+++ b/src/prototypes/contract-template-management/.spec/ui-review.md
@@ -0,0 +1,89 @@
+# UI Review
+
+- 审查目标:`src/prototypes/contract-template-management`
+- 使用设计依据:`src/themes/linear/DESIGN.md`(实现侧通过 `vehicle-management/style.css` 声明为 Linear **light / inverse** 模式,主色覆写为 ONE-OS 翡翠绿 `--ln-primary: #32a06e`;与 Linear 原文档薰衣草色 `#5e6ad2` 存在有意的品牌偏移)
+- 生成时间:2026-06-29
+
+## 总体点评
+
+合同模板管理在整体视觉上与车辆管理等 ONE-OS 中后台模块保持一致:浅灰画布(`--ln-canvas-parchment`)、白卡片、8px 圆角控件、Inter 字体与翡翠绿主色,表格/筛选区复用 `.vm-filter-card`、`.vm-table-card` 模式,信息密度适合法务日常维护任务。编辑页双栏(Word 编辑 + A4 预览)、章节 Tab、预览工具栏的 `aria-label` / `role` 标注较完整,禁用态操作按钮与 `focus-visible` 内描边也体现了一定的可访问性意识。
+
+主要风险集中在**列表操作区触控尺寸**、**宽表横向滚动**以及**多操作并列带来的扫描成本**。Impeccable detector 另提示条件说明卡片的紫色左边框(`#7c3aed`)与全站绿色主色体系不一致,属于品牌表达层面的杂音。整体未达到 Linear 营销站那种极简留白,但作为 dense admin 工具界面,层次与组件复用是成立且可继续打磨的。
+
+## P0-P3 优先级问题
+
+### P1 - 列表行内操作按钮触控高度不足 44px
+
+- 证据:`.ct-action-btn.ant-btn-link` 使用 `min-height: var(--vm-btn-height)`(36px,`vehicle-management/style.css`);对比同模块预览翻页按钮已设 `min-width/min-height: 44px`(`contract-template.css` 约 1563 行)。列表操作列含「启用/停用」「版本日志」「编辑」「删除」等链接按钮。
+- 影响:在触控屏或高精度鼠标场景下,误触率上升;不符合常见 WCAG 2.5.5 目标尺寸建议,与 DESIGN 中间距/控件可点性预期不符。
+- 修复方向:将行内操作 `min-height` 提升至 44px,或改为「更多」下拉 + 主操作外露;保持 `cursor-pointer` 与禁用态对比度(当前禁用灰 `#94A3B8` 已处理)。
+
+### P1 - 主表固定最小宽度导致中小屏强依赖横向滚动
+
+- 证据:`.ct-data-table--nested` 设 `--ct-list-table-width: 1232px` 且 `min-width` 强制(`contract-template.css` 72–77 行);`LIST_TABLE_SCROLL_X` 合计约 1332px(含操作列 300px,`ContractTemplate.jsx`)。右侧还有 `@axhub/annotation` 工具栏占用宽度。
+- 影响:13 寸笔记本或分屏预览时,用户需频繁横向拖动才能看到「操作」列;筛选结果核对与批量处理效率下降。
+- 修复方向:≥1280px 保持全列;`<1200px` 将「启用状态」与次要时间列折叠为 expandable 摘要,或操作列收进「更多」菜单;与 Linear inverse 表格「密度可伸缩」原则对齐。
+
+### P2 - 操作列 4~5 个并列链接,视觉密度与扫读成本高
+
+- 证据:已发布行同时展示「停用/启用」「版本日志」「编辑」「删除」(`getListColumns` 操作列宽 300px);多行换行时(`Space wrap`)行高不齐。
+- 影响:法务需逐行扫描多个同色链接,难以快速区分主操作与危险操作;与 DESIGN 中 button-primary / button-secondary 层级未充分映射到表格场景。
+- 修复方向:主操作保留 1~2 个(如「编辑」);其余收入「更多」`Dropdown`;危险操作「删除」保持 `danger` 但与次要操作拉开间距或使用图标+文字。
+
+### P2 - 条件条款说明卡使用紫色左边框,偏离主色体系
+
+- 证据:detector 命中 `border-left: 3px solid #7c3aed`(`ContractTemplate.jsx` 内联样式与 `contract-template.css` 约 1390 行);全站主色为 `--ln-primary #32a06e`,Linear DESIGN 强调单一 chromatic accent。
+- 影响:编辑页唯一紫色强调与表格/按钮绿色系统冲突,削弱品牌一致性,易被识别为「拼贴式」装饰。
+- 修复方向:改为 `border-left-color: var(--ln-primary)` 或 `var(--ln-primary-soft)` 背景 + 无侧栏;与 `.ct-main-clause-tip` 信息层级一致即可。
+
+### P3 - 字号阶梯偏平,表头与正文层次不够清晰
+
+- 证据:Impeccable detector 报告 11–20px 多档字号、比例约 1.8:1(`flat-type-hierarchy`);表头 `0.75rem`(12px)/`--ln-muted`,正文 `0.8125rem`(13px)/`--ln-body`,差距仅 1px。
+- 影响:长列表扫读时列标题与单元格内容对比偏弱;不影响完成任务,但略损专业感。
+- 修复方向:收敛为 3 档:表头 12px medium + `--ln-muted-80`、正文 14px、辅助 12px;确保表头对比度 ≥4.5:1。
+
+## 核心元件
+
+### 筛选区(`ct-list-filter`)
+
+- **符合**:四字段栅格(合同名称可搜索 Select、状态、创建人、启用状态)+ 查询/重置,与 `vm-filter-card` 模式一致;字段带 `aria-label`。
+- **调整**:筛选项增至 4 个后,窄屏下建议 `@media (max-width: 992px)` 改为两列栅格(当前 filter grid 响应式依赖 vehicle-management 全局样式,需确认 4 字段是否换行整齐)。
+
+### 列表表格(`ct-list-table`)
+
+- **符合**:表头浅灰底、hairline 分隔、hover 行背景、展开行嵌套章节子表;状态/启用 Tag 使用 Ant Design semantic 色。
+- **调整**:操作列与总宽度过宽(见 P1/P2);禁用编辑/删除的 link 按钮样式已灰化,但缺少 `title` 提示「请先停用」可帮助理解禁用原因。
+
+### 编辑页顶栏与章节 Tab(`ct-editor-topbar` / `ct-section-tabs`)
+
+- **符合**:返回 + 保存/保存并发布 CTA 层级清晰;`ct-btn-cta` 使用主色实心按钮;章节 Tab 带 `role="tablist"`、`aria-selected`,「更多」章节有 `aria-expanded`。
+- **调整**:顶栏在窄屏未 sticky;长章节名在「更多」Tab 内 ellipsis 至 140px(`max-width: 992px`),可考虑 tooltip 展示全名。
+
+### 编辑区 + 预览区(`ct-editor-panel` / `ct-preview-panel`)
+
+- **符合**:左右分栏、A4 预览分页/连续模式、缩放与翻页控件具备 44px 触控与 `prefers-reduced-motion`;富文本工具栏 `focus-visible` 内描边符合 `--vm-focus-border-color`。
+- **保留亮点**:预览工具栏 `role="toolbar"` 与分组 `aria-label` 是全场最佳 a11y 实践,可复用到列表操作区。
+- **调整**:编辑区 Word 画布 `210mm` 固定宽,小屏下横向滚动不可避免,需在容器上提示「可横向滑动查看版式」。
+
+### 弹窗(删除 / 停用 / 未保存离开 / 版本日志)
+
+- **符合**:`ct-modal-wrap` 统一包裹;停用与离开编辑有明确二次确认文案。
+- **调整**:版本日志 Modal 宽 720px、快照预览 900px,小屏应验证 `max-width: calc(100vw - 32px)` 是否生效。
+
+## 响应式与可访问性
+
+| 维度 | 结论 |
+|---|---|
+| 桌面(≥1280px) | 编辑双栏与预览体验完整;列表需横向滚动但可接受 |
+| 平板/窄屏(≤992px) | 编辑列 `max-height: none` 已放开;表格仍强制 min-width,体验明显降级 |
+| 键盘 | 章节 Tab、预览控件、部分按钮有 `focus-visible`;行内 link 操作依赖 Tab 顺序,密集时冗长 |
+| 语义 | 筛选、章节、预览工具栏标注较好;表格操作按钮未统一 `aria-label`(仅靠文字) |
+| 对比度 | 正文 `#52525b` on 白 generally OK;表头 `#71717a` on `#f6f7f7` 建议复核 AA |
+| 动效 | 多处 `@media (prefers-reduced-motion: reduce)` 已覆盖工具栏与章节 Tab |
+
+## 证据与评估说明
+
+- **浏览器/截图**:未启动本地预览与浏览器截图;结论来自源码、`contract-template.css`、`index.tsx` ConfigProvider token 及 detector 输出。
+- **Scanner**:已运行 `impeccable/scripts/detect.mjs --json`(exit 0,含 side-tab、overused-font Inter、flat-type-hierarchy、em-dash-overuse 等 warning);已筛入 P2/P3 相关项,em-dash 主要来自合同正文 HTML 种子,非 UI chrome。
+- **设计依据说明**:项目根 `DESIGN.md` 为 Vercel 营销规范,与本原型实现不一致;审查采用实现声明的 `src/themes/linear/DESIGN.md` + ONE-OS token 覆写作为对齐基准。
+- **独立评估**:`degraded`(单轮完成设计阅读 + 源码 + detector;未拆分子代理,未做浏览器实机检查)。
diff --git a/src/prototypes/contract-template-management/ContractTemplate.jsx b/src/prototypes/contract-template-management/ContractTemplate.jsx
index 5613a04..03920b7 100644
--- a/src/prototypes/contract-template-management/ContractTemplate.jsx
+++ b/src/prototypes/contract-template-management/ContractTemplate.jsx
@@ -1,6 +1,26 @@
import { V266_LEASE_DOCUMENT_HTML } from './v266-lease-document.js';
import { TRIAL_AGREEMENT_DOCUMENT_HTML } from './trial-agreement-document.js';
import { TRIAL_AGREEMENT_18T_DOCUMENT_HTML } from './trial-agreement-18t-document.js';
+import {
+ ensureContractTemplateStore,
+ getContractTemplates,
+ setContractTemplates,
+ subscribeContractTemplates,
+} from './contract-template-store.js';
+import { buildInitialWordTemplates } from './contract-template-seed.js';
+import {
+ getContractTypeLabel,
+ normalizeContractType,
+ resolveContractType,
+} from './contract-template-types.js';
+import { LEASE_VEHICLE_BRAND_MODEL_CATALOG } from '../lease-contract-management/lease-order-vars.js';
+import VehicleBrandModelPicker from './VehicleBrandModelPicker.jsx';
+import {
+ vehicleBindingKey,
+ parseVehicleBindingKey,
+ vehicleMatchesBindings,
+ formatVehicleBindings,
+} from './vehicle-binding-utils.js';
var DEFAULT_LESSOR_COMPANIES = [
@@ -649,9 +669,14 @@ const Component = function (props) {
{ key: 'authorization', label: '授权委托书', markers: ['font: 28.0px Times; color: #000000">授权委托书', '授权委托书致:', '授权委托书
致:'] },
{ key: 'safety_agreement', label: '安全责任协议', markers: ['data-risk-redline="1">安全责任协议
', '安全责任协议
甲方(出租方):', '安全责任协议
'] }
];
- var SECTION_VISIBLE_TAB_COUNT = 4;
var LIST_EXPAND_COL_WIDTH = 48;
- var LIST_TABLE_SCROLL_X = LIST_EXPAND_COL_WIDTH + 280 + 96 + 88 + 150 + 110 + 150 + 110 + 200;
+
+ function getActiveSectionTabIndex() {
+ for (var i = 0; i < DOCUMENT_SECTION_TABS.length; i++) {
+ if (DOCUMENT_SECTION_TABS[i].key === activeSectionKey) return i;
+ }
+ return 0;
+ }
function escapeAttr(value) {
return String(value || '').replace(/&/g, '&').replace(/"/g, '"');
@@ -844,43 +869,12 @@ const Component = function (props) {
return tab ? tab.label : key;
}
- var VEHICLE_BRAND_MODEL_CATALOG = [
- { brand: '现代', models: ['帕力安牌4.5吨冷链车', '18吨氢燃料电池车', '9.6米厢式货车'] },
- { brand: '苏龙', models: ['9.6米氢燃料电池车'] },
- { brand: '飞驰', models: ['集卡头', '半挂车'] },
- { brand: '跃进', models: ['4.2米冷链车'] },
- { brand: '宇通', models: ['氢燃料电池客车'] }
- ];
+ var VEHICLE_BRAND_MODEL_CATALOG = LEASE_VEHICLE_BRAND_MODEL_CATALOG;
function sectionAllowsVehicleBinding(sectionKey) {
return !!sectionKey && sectionKey !== 'main';
}
- function vehicleBindingKey(binding) {
- return String(binding.brand || '') + '|' + String(binding.model || '');
- }
-
- function parseVehicleBindingKey(key) {
- var parts = String(key || '').split('|');
- return { brand: parts[0] || '', model: parts[1] || '' };
- }
-
- function vehicleMatchesBindings(brand, model, bindings) {
- var list = bindings || [];
- if (!list.length) return true;
- return list.some(function (item) {
- return item.brand === brand && item.model === model;
- });
- }
-
- function formatVehicleBindings(bindings) {
- var list = bindings || [];
- if (!list.length) return '全部车型';
- return list.map(function (item) {
- return item.brand + ' · ' + item.model;
- }).join(';');
- }
-
function serializeVehicleClauseBindings(bindings) {
return escapeAttr(JSON.stringify(bindings || []));
}
@@ -902,7 +896,7 @@ const Component = function (props) {
}
function sectionAllowsMainVehicleClauses(sectionKey) {
- return sectionKey === 'main';
+ return !!sectionKey && (sectionKey === 'main' || sectionAllowsVehicleBinding(sectionKey));
}
function vehicleClauseMatchesPreview(bindings, selectedKeys) {
@@ -926,12 +920,19 @@ const Component = function (props) {
}
function createEmptyListFilter() {
- return { keyword: '', status: '', creator: '', vehicleBrand: '', vehicleModel: '' };
+ return { keyword: '', status: '', creator: '', enabled: '' };
}
function hasActiveListFilters(filters) {
if (!filters) return false;
- return !!(filters.keyword || filters.status || filters.creator || filters.vehicleBrand || filters.vehicleModel);
+ return !!(filters.keyword || filters.status || filters.creator || filters.enabled);
+ }
+
+ function filterContractNameOption(input, option) {
+ var text = String((option && option.label) || '').toLowerCase();
+ var query = String(input || '').trim().toLowerCase();
+ if (!query) return true;
+ return text.indexOf(query) >= 0;
}
function recordMatchesVehicleFilter(record, brand, model) {
@@ -964,9 +965,6 @@ const Component = function (props) {
}
if (filters.status && record.status !== filters.status) return false;
if (filters.creator && record.creator !== filters.creator) return false;
- if (sectionAllowsVehicleBinding(sectionKey)) {
- if (!recordMatchesVehicleFilter(record, filters.vehicleBrand, filters.vehicleModel)) return false;
- }
return true;
}
@@ -1010,89 +1008,19 @@ const Component = function (props) {
function createWordDocument(params) {
return Object.assign({
category: 'lease',
+ contractType: '',
fileName: '未命名合同.docx',
versionName: '未命名合同',
versionNo: null,
status: 'draft',
- isDefault: false,
+ enabled: false,
+ versionLogs: [],
sectionContents: emptySectionMap(),
sectionVehicleBindings: {},
remark: ''
}, params);
}
- function buildInitialWordTemplates() {
- var v266 = getLeaseDocumentSectionMap();
- var trialBase = getTrialAgreementSectionMap('商用车试用协议.docx');
- var trial18t = getTrialAgreementSectionMap('商用车试用协议-现代18吨.docx');
- var model18t = [{ brand: '现代', model: '18吨氢燃料电池车' }];
- var model45t = [{ brand: '现代', model: '帕力安牌4.5吨冷链车' }];
- var model96 = [{ brand: '苏龙', model: '9.6米氢燃料电池车' }];
- var fullSections = Object.assign(emptySectionMap(), v266);
- return [
- createWordDocument({
- id: 'doc-1',
- fileName: '现代18吨车型-数智中心版.docx',
- versionName: '现代18吨车型-数智中心版',
- versionNo: 'v1.00',
- status: 'published',
- isDefault: true,
- sectionContents: fullSections,
- sectionVehicleBindings: {
- lease_order: model18t,
- dynamic_supervision: model96,
- vehicle_spec: model45t
- },
- creator: '法务-王静', createTime: '2026-05-25 09:13', updater: '法务-王静', updateTime: '2026-05-25 09:13',
- remark: 'V26.6 现代18吨数智中心版标准合同;创建合同时将始终包含'
- }),
- createWordDocument({
- id: 'doc-2',
- fileName: '2026年标准商用车租赁合同.docx',
- versionName: '2026年标准商用车租赁合同',
- versionNo: 'v1.01',
- status: 'published',
- sectionContents: fullSections,
- sectionVehicleBindings: {},
- creator: '法务-王静', createTime: '2026-03-15 14:30', updater: '法务-王静', updateTime: '2026-03-15 14:30',
- remark: '通用主体条款'
- }),
- createWordDocument({
- id: 'doc-3',
- fileName: '2026年二季度草案.docx',
- versionName: '2026年二季度草案',
- versionNo: null,
- status: 'draft',
- sectionContents: fullSections,
- sectionVehicleBindings: {},
- creator: '法务-李明', createTime: '2026-06-01 09:15', updater: '法务-李明', updateTime: '2026-06-01 09:15',
- remark: '待法务审核发布'
- }),
- createWordDocument({
- id: 'doc-4',
- fileName: '商用车试用协议.docx',
- versionName: '商用车试用协议',
- versionNo: 'v1.00',
- status: 'published',
- sectionContents: Object.assign(emptySectionMap(), trialBase),
- sectionVehicleBindings: {},
- creator: '法务-王静', createTime: '2026-06-20 10:00', updater: '法务-王静', updateTime: '2026-06-20 10:00',
- remark: '通用商用车试用协议范本'
- }),
- createWordDocument({
- id: 'doc-5',
- fileName: '商用车试用协议-现代18吨.docx',
- versionName: '商用车试用协议-现代18吨',
- versionNo: 'v1.00',
- status: 'published',
- sectionContents: Object.assign(emptySectionMap(), trial18t),
- sectionVehicleBindings: {},
- creator: '法务-李明', createTime: '2026-06-25 14:20', updater: '法务-李明', updateTime: '2026-06-25 14:20',
- remark: '现代18吨车型试用协议;含完整安全附件'
- })
- ];
- }
-
function resolveRecordContentHtml(record) {
if (!record) return '';
if (record.sectionContents) {
@@ -1122,9 +1050,7 @@ const Component = function (props) {
hasContent: hasContent,
vehicleBindings: bindings,
riskClauseCount: hasContent ? countRiskRedlineRegions(html) : 0,
- vehicleClauseCount: hasContent && sectionAllowsMainVehicleClauses(tab.key)
- ? countVehicleClauseRegions(html)
- : 0
+ vehicleClauseCount: hasContent ? countVehicleClauseRegions(html) : 0
};
});
}
@@ -1145,31 +1071,25 @@ const Component = function (props) {
function documentMatchesListFilters(doc, filters) {
if (!filters) return true;
- var keyword = String(filters.keyword || '').trim().toLowerCase();
+ var keyword = String(filters.keyword || '').trim();
if (keyword) {
- var haystack = [
- doc.versionName,
- doc.fileName,
- doc.versionNo,
- doc.remark,
- doc.creator,
- doc.updater
- ].join(' ').toLowerCase();
- if (haystack.indexOf(keyword) < 0) return false;
+ var contractType = String(resolveContractType(doc) || '');
+ if (contractType !== keyword) return false;
}
if (filters.status && doc.status !== filters.status) return false;
if (filters.creator && doc.creator !== filters.creator) return false;
- if (filters.vehicleBrand || filters.vehicleModel) {
- if (!documentHasVehicleBindingMatch(doc, filters.vehicleBrand, filters.vehicleModel)) return false;
+ if (filters.enabled === 'enabled') {
+ if (!isPublishedTemplate(doc) || doc.enabled === false) return false;
+ }
+ if (filters.enabled === 'disabled') {
+ if (!isPublishedTemplate(doc) || doc.enabled !== false) return false;
}
return true;
}
function getDefaultSectionVersion(sectionKey, list) {
var source = list || templates;
- var defaultDoc = source.find(function (item) {
- return item.status === 'published' && item.isDefault;
- }) || source.find(function (item) { return item.status === 'published'; });
+ var defaultDoc = source.find(function (item) { return item.status === 'published'; });
if (!defaultDoc || !defaultDoc.sectionContents) return null;
var html = defaultDoc.sectionContents[sectionKey] || '';
if (!html && sectionKey !== 'main') return null;
@@ -1262,9 +1182,20 @@ const Component = function (props) {
return base;
}
- var templatesState = useState(buildInitialWordTemplates);
+ ensureContractTemplateStore(buildInitialWordTemplates);
+ var templatesState = useState(getContractTemplates);
var templates = templatesState[0];
- var setTemplates = templatesState[1];
+ useEffect(function () {
+ if (!getContractTemplates().length) {
+ ensureContractTemplateStore(buildInitialWordTemplates);
+ }
+ return subscribeContractTemplates(function (next) {
+ templatesState[1](next);
+ });
+ }, []);
+ function setTemplates(updater) {
+ setContractTemplates(updater);
+ }
var categoryFilterState = useState('lease');
var categoryFilter = categoryFilterState[0];
@@ -1278,6 +1209,22 @@ const Component = function (props) {
if (props && props.onPageViewChange) props.onPageViewChange(pageView);
}, [pageView]);
+ useEffect(function () {
+ if (pageView !== 'editor') return;
+ var root = sectionTabsScrollRef.current;
+ if (!root) return;
+ var active = root.querySelector('.ct-section-tab.is-active');
+ if (!active || typeof active.scrollIntoView !== 'function') return;
+ var reduceMotion = typeof window !== 'undefined'
+ && window.matchMedia
+ && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ active.scrollIntoView({
+ inline: 'center',
+ block: 'nearest',
+ behavior: reduceMotion ? 'auto' : 'smooth',
+ });
+ }, [activeSectionKey, pageView]);
+
var editorModeState = useState('create');
var editorMode = editorModeState[0];
var setEditorMode = editorModeState[1];
@@ -1286,6 +1233,10 @@ const Component = function (props) {
var editingRecord = editingRecordState[0];
var setEditingRecord = editingRecordState[1];
+ var editorContractTypeState = useState('');
+ var editorContractType = editorContractTypeState[0];
+ var setEditorContractType = editorContractTypeState[1];
+
var lastImportFileNameState = useState('');
var importChangedInSessionState = useState(false);
var formBlocksState = useState([]);
@@ -1293,8 +1244,13 @@ const Component = function (props) {
var deleteModalState = useState(false);
var deleteTargetState = useState(null);
- var defaultConfirmModalState = useState(false);
- var defaultConfirmTargetState = useState(null);
+ var disableModalState = useState(false);
+ var disableTargetState = useState(null);
+ var versionLogModalState = useState(false);
+ var versionLogTargetState = useState(null);
+ var snapshotViewState = useState(null);
+ var editorDirtyState = useState(false);
+ var leaveEditorModalState = useState(false);
var editorSyncKeyState = useState(0);
var editorInsertHandlerRef = useRef(null);
var registerEditorInsertRef = useRef(function (fn) { editorInsertHandlerRef.current = fn; });
@@ -1323,7 +1279,6 @@ const Component = function (props) {
var editorSectionVehicleBindings = editorSectionVehicleBindingsState[0];
var setEditorSectionVehicleBindings = editorSectionVehicleBindingsState[1];
var vehicleBindingBrandState = useState('现代');
- var vehicleBindingModelState = useState('');
var listFilterDraftState = useState(createEmptyListFilter);
var listFilterDraft = listFilterDraftState[0];
var setListFilterDraft = listFilterDraftState[1];
@@ -1333,6 +1288,7 @@ const Component = function (props) {
var editorScrollElRef = useRef(null);
var previewScrollElRef = useRef(null);
var scrollSyncLockRef = useRef(false);
+ var sectionTabsScrollRef = useRef(null);
var workspaceZoomState = useState(100);
var workspaceZoom = workspaceZoomState[0];
var setWorkspaceZoom = workspaceZoomState[1];
@@ -1424,16 +1380,16 @@ const Component = function (props) {
'.ct-inline-lock{background:#fef3c7;border-bottom:1px dashed #f59e0b;padding:0 2px;border-radius:2px}' +
'.ct-preview-doc .ct-inline-lock{background:#f8fafc;border-left:2px solid #94a3b8;padding:2px 4px}' +
'.ct-rich-editor .ct-risk-redline,.ct-preview-doc .ct-risk-redline,.ct-preview-page__body .ct-risk-redline,.ct-word-doc .ct-risk-redline{background:rgba(254,226,226,0.4);border:1px dashed #f87171;border-radius:3px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}' +
- '.ct-vehicle-clause{position:relative;margin:4px 0;padding:4px 6px 4px 10px;border-left:3px solid #7c3aed;background:rgba(237,233,254,0.45);border-radius:0 4px 4px 0}' +
- '.ct-vehicle-clause__footer{display:flex;align-items:center;flex-wrap:wrap;gap:6px 10px;margin-top:6px;padding-top:6px;border-top:1px dashed #c4b5fd;font-size:11px;line-height:1.4;color:#5b21b6;user-select:none}' +
+ '.ct-vehicle-clause{position:relative;margin:4px 0;padding:4px 6px 4px 10px;border-left:3px solid var(--ln-primary);background:var(--ln-primary-soft);border-radius:0 4px 4px 0}' +
+ '.ct-vehicle-clause__footer{display:flex;align-items:center;flex-wrap:wrap;gap:6px 10px;margin-top:6px;padding-top:6px;border-top:1px dashed color-mix(in srgb,var(--ln-primary) 35%,var(--ln-hairline));font-size:11px;line-height:1.4;color:var(--ln-primary-focus);user-select:none}' +
'.ct-vehicle-clause__label{font-weight:600}' +
- '.ct-vehicle-clause__hint{color:#7c3aed;opacity:.85}' +
+ '.ct-vehicle-clause__hint{color:var(--ln-primary);opacity:.9}' +
'.ct-preview-sim-vehicles{display:flex;align-items:center;gap:8px;flex-wrap:wrap;max-width:100%}' +
'.ct-preview-sim-vehicles__label{font-size:12px;color:#64748B;white-space:nowrap}' +
'.ct-preview-sim-vehicles__select{min-width:200px;max-width:360px;flex:1}' +
- '.ct-preview-sim-vehicles__meta{font-size:12px;color:#7c3aed;white-space:nowrap}' +
+ '.ct-preview-sim-vehicles__meta{font-size:12px;color:var(--ln-primary);white-space:nowrap}' +
'.ct-vc-range-marker{display:inline-block;width:0;height:0;overflow:hidden;font-size:0;line-height:0;opacity:0;vertical-align:baseline;user-select:none;pointer-events:none}' +
- '.ct-vc-placeholder{display:inline;padding:0 4px;border-radius:4px;background:#ede9fe;color:#6d28d9;font-size:12px;line-height:1.6;user-select:none;white-space:nowrap}' +
+ '.ct-vc-placeholder{display:inline;padding:0 4px;border-radius:4px;background:var(--ln-primary-soft);color:var(--ln-primary-focus);font-size:12px;line-height:1.6;user-select:none;white-space:nowrap}' +
'.ct-rte-btn--text{width:auto;padding:0 10px;font-size:12px;font-weight:600;color:#dc2626;border:1px solid #fecaca;background:#fef2f2;border-radius:4px;min-width:auto;height:28px;gap:4px}' +
'.ct-rte-btn--text:hover{background:#fee2e2;border-color:#fca5a5;color:#b91c1c}' +
'.ct-rte-btn--text.is-muted{color:#64748B;border-color:#E2E8F0;background:#F8FAFC;font-weight:500}' +
@@ -1554,7 +1510,10 @@ const Component = function (props) {
'.ct-placeholder-card__item-actions{display:flex;gap:4px;flex-shrink:0}' +
'.ct-placeholder-card__action{font-size:11px;color:#64748B;padding:2px 6px;border-radius:4px;border:1px solid #E2E8F0;background:#F8FAFC;cursor:pointer}' +
'.ct-placeholder-card__action:hover{color:var(--ln-primary);border-color:var(--ln-hairline-strong);background:var(--ln-primary-soft)}' +
- '.ct-editor-page{display:flex;flex-direction:column;flex:1;min-height:0;height:100%}'
+ '.ct-editor-page{display:flex;flex-direction:column;flex:1;min-height:0;height:100%}' +
+ '.ct-version-log-hint{font-size:12px;color:#64748B;margin-bottom:12px}' +
+ '.ct-snapshot-preview{max-height:60vh;overflow:auto;border:1px solid #E2E8F0;border-radius:8px;padding:16px;background:#fff}' +
+ '.ct-snapshot-meta{font-size:12px;color:#64748B;margin-bottom:12px;line-height:1.6}'
]).join('\n');
function statusTag(status) {
@@ -1563,6 +1522,81 @@ const Component = function (props) {
return React.createElement(Tag, null, '已归档');
}
+ function cloneJson(value) {
+ return JSON.parse(JSON.stringify(value || {}));
+ }
+
+ function isPublishedTemplate(record) {
+ return !!(record && record.status === 'published');
+ }
+
+ function isTemplateEnabled(record) {
+ return isPublishedTemplate(record) && record.enabled !== false;
+ }
+
+ function createVersionSnapshot(record) {
+ return {
+ fileName: record.fileName,
+ versionName: record.versionName,
+ versionNo: record.versionNo,
+ contractType: resolveContractType(record),
+ sectionContents: cloneJson(record.sectionContents),
+ sectionVehicleBindings: cloneJson(record.sectionVehicleBindings),
+ remark: record.remark || '',
+ archivedAt: formatNowTime()
+ };
+ }
+
+ function renderEnabledTag(record) {
+ if (!isPublishedTemplate(record)) return null;
+ return record.enabled !== false
+ ? React.createElement(Tag, { color: 'success' }, '启用')
+ : React.createElement(Tag, { color: 'default' }, '停用');
+ }
+
+ function getContractDisplayName(record) {
+ var raw = (record && (record.fileName || record.versionName)) || '';
+ return stripFileExtension(raw) || raw || '—';
+ }
+
+ function toggleTemplateEnabled(record, nextEnabled) {
+ if (!isPublishedTemplate(record)) return;
+ var now = formatNowTime();
+ setTemplates(function (prev) {
+ return prev.map(function (t) {
+ if (t.id !== record.id) return t;
+ return Object.assign({}, t, {
+ enabled: nextEnabled,
+ updater: CURRENT_USER,
+ updateTime: now
+ });
+ });
+ });
+ message.success(nextEnabled ? '模板已启用' : '模板已停用,可进行编辑');
+ }
+
+ function confirmDisableTemplate(record) {
+ disableTargetState[1](record);
+ disableModalState[1](true);
+ }
+
+ function handleConfirmDisable() {
+ var target = disableTargetState[0];
+ if (!target) return;
+ toggleTemplateEnabled(target, false);
+ disableModalState[1](false);
+ disableTargetState[1](null);
+ }
+
+ function openVersionLog(record) {
+ versionLogTargetState[1](record);
+ versionLogModalState[1](true);
+ }
+
+ function openSnapshotView(logEntry) {
+ snapshotViewState[1](logEntry);
+ }
+
function loadDocumentFromBlocks(blocks) {
var html = mergeBlocksToDocument(blocks);
formDocumentHtmlState[1](html);
@@ -1600,6 +1634,14 @@ const Component = function (props) {
return chunk;
}
+ function markEditorDirty() {
+ editorDirtyState[1](true);
+ }
+
+ function clearEditorDirty() {
+ editorDirtyState[1](false);
+ }
+
function syncActiveSectionHtml(html) {
if (shouldShowFullWordDocument(activeSectionKey) && isWordFormattedDoc(html)) {
syncDocumentHtml(html);
@@ -1611,6 +1653,7 @@ const Component = function (props) {
var merged = mergeSectionsToDocument(next);
formDocumentHtmlState[1](merged);
formBlocksState[1](documentToBlocks(merged));
+ markEditorDirty();
}
function parseVersionNo(versionNo) {
@@ -1658,8 +1701,11 @@ const Component = function (props) {
return '合同 ' + versionNo;
}
- function buildWordDocumentItem(publish, sectionContents, sectionVehicleBindings, importName, editing) {
+ function buildWordDocumentItem(publish, sectionContents, sectionVehicleBindings, importName, editing, contractType) {
var now = formatNowTime();
+ var resolvedContractType = normalizeContractType(
+ contractType || (editing && resolveContractType(editing)) || ''
+ );
var fileName = importName
? (/\./.test(importName) ? importName : importName + '.docx')
: (editing && editing.fileName) || '未命名合同.docx';
@@ -1671,11 +1717,13 @@ const Component = function (props) {
return {
id: 'doc-' + Date.now(),
category: categoryFilter,
+ contractType: resolvedContractType,
fileName: fileName,
versionNo: versionNo,
versionName: buildAutoVersionName(versionNo, importName, versionName),
status: 'published',
- isDefault: false,
+ enabled: true,
+ versionLogs: [],
sectionContents: contentsMap,
sectionVehicleBindings: bindingsMap,
creator: CURRENT_USER,
@@ -1688,11 +1736,11 @@ const Component = function (props) {
return {
id: 'doc-' + Date.now(),
category: categoryFilter,
+ contractType: resolvedContractType,
fileName: fileName,
versionNo: null,
versionName: buildAutoVersionName(null, importName, versionName),
status: 'draft',
- isDefault: false,
sectionContents: contentsMap,
sectionVehicleBindings: bindingsMap,
creator: CURRENT_USER,
@@ -1718,7 +1766,6 @@ const Component = function (props) {
var bindings = source[sectionKey] || [];
setFormVehicleBindings(JSON.parse(JSON.stringify(bindings)));
vehicleBindingBrandState[1](bindings[0] ? bindings[0].brand : '现代');
- vehicleBindingModelState[1]('');
}
function collectEditorSectionContents() {
@@ -1743,6 +1790,7 @@ const Component = function (props) {
setActiveSectionKey(sectionKey);
setEditorMode('create');
setEditingRecord(null);
+ setEditorContractType('');
setEditorSectionKey(sectionKey);
lastImportFileNameState[1]('');
importChangedInSessionState[1](false);
@@ -1750,18 +1798,20 @@ const Component = function (props) {
formBlocksState[1]([]);
setFormVehicleBindings([]);
vehicleBindingBrandState[1]('现代');
- vehicleBindingModelState[1]('');
setPreviewSimVehicleKeys([]);
setSectionContents(emptySectionMap());
setEditorSectionVehicleBindings({});
editorSyncKeyState[1](function (v) { return v + 1; });
+ clearEditorDirty();
setPageView('editor');
}
function openEdit(record) {
+ if (isPublishedTemplate(record) && isTemplateEnabled(record)) return;
var sectionKey = 'main';
setEditorMode('edit');
setEditingRecord(record);
+ setEditorContractType(resolveContractType(record));
setEditorSectionKey(sectionKey);
setActiveSectionKey(sectionKey);
lastImportFileNameState[1](record.fileName || '');
@@ -1776,6 +1826,7 @@ const Component = function (props) {
loadSectionVehicleBindings(sectionKey, bindingsMap);
setPreviewSimVehicleKeys([vehicleBindingKey({ brand: '现代', model: '18吨氢燃料电池车' })]);
editorSyncKeyState[1](function (v) { return v + 1; });
+ clearEditorDirty();
setPageView('editor');
}
@@ -1791,6 +1842,7 @@ const Component = function (props) {
setSectionContents(splitHtmlIntoSections(fullHtml));
setActiveSectionKey(sectionKey);
editorSyncKeyState[1](function (v) { return v + 1; });
+ markEditorDirty();
message.success('已导入 Word 合同全文,编辑区将按原版式分页展示全部章节');
return false;
}
@@ -1805,15 +1857,18 @@ const Component = function (props) {
formDocumentHtmlState[1](merged);
setSectionContents(sections);
formBlocksState[1](documentToBlocks(merged));
+ markEditorDirty();
return;
}
formDocumentHtmlState[1](html);
+ markEditorDirty();
}
function syncDocumentHtml(html) {
formDocumentHtmlState[1](html);
formBlocksState[1](documentToBlocks(html));
setSectionContents(splitHtmlIntoSections(html));
+ markEditorDirty();
}
function handleSaveTemplate(publish) {
@@ -1823,20 +1878,55 @@ const Component = function (props) {
message.warning('请先编辑合同内容,或导入 Word 模板');
return;
}
+ if (!normalizeContractType(editorContractType)) {
+ message.warning('请输入合同类型');
+ return;
+ }
var now = formatNowTime();
var importName = lastImportFileNameState[0] || '';
var bindingsMap = collectEditorSectionVehicleBindings();
+ var savedContractType = normalizeContractType(editorContractType);
if (editorMode === 'create') {
- var created = buildWordDocumentItem(publish, sections, bindingsMap, importName, null);
+ var created = buildWordDocumentItem(publish, sections, bindingsMap, importName, null, savedContractType);
setTemplates(function (prev) { return prev.concat([created]); });
message.success(publish ? ('已发布,版本号 ' + created.versionNo) : '草稿已保存(未生成版本号)');
} else if (editingRecord) {
if (publish) {
if (editingRecord.status === 'published') {
- var nextVersion = buildWordDocumentItem(true, sections, bindingsMap, importName || editingRecord.fileName, editingRecord);
- setTemplates(function (prev) { return prev.concat([nextVersion]); });
- message.success('已发布新版本 ' + nextVersion.versionNo + ',原版本保留可回滚');
+ var snapshot = createVersionSnapshot(editingRecord);
+ var nextVersionNo = generateNextVersionNo();
+ var logEntry = {
+ id: 'log-' + Date.now(),
+ versionNo: editingRecord.versionNo,
+ action: '发布变更',
+ operator: CURRENT_USER,
+ time: now,
+ remark: '编辑前自动归档',
+ snapshot: snapshot
+ };
+ setTemplates(function (prev) {
+ return prev.map(function (t) {
+ if (t.id !== editingRecord.id) return t;
+ var fileName = importName
+ ? (/\./.test(importName) ? importName : importName + '.docx')
+ : t.fileName;
+ var logs = (t.versionLogs || []).concat([logEntry]);
+ return Object.assign({}, t, {
+ fileName: fileName,
+ contractType: savedContractType,
+ versionNo: nextVersionNo,
+ versionName: buildAutoVersionName(nextVersionNo, importName, stripFileExtension(fileName)),
+ sectionContents: sections,
+ sectionVehicleBindings: bindingsMap,
+ versionLogs: logs,
+ enabled: false,
+ updater: CURRENT_USER,
+ updateTime: now
+ });
+ });
+ });
+ message.success('已发布新版本 ' + nextVersionNo + ',旧版快照已写入版本日志');
} else {
var publishVersionNo = generateNextVersionNo();
setTemplates(function (prev) {
@@ -1847,9 +1937,12 @@ const Component = function (props) {
: t.fileName;
return Object.assign({}, t, {
fileName: fileName,
+ contractType: savedContractType,
versionNo: publishVersionNo,
versionName: buildAutoVersionName(publishVersionNo, importName, stripFileExtension(fileName)),
status: 'published',
+ enabled: true,
+ versionLogs: t.versionLogs || [],
sectionContents: sections,
sectionVehicleBindings: bindingsMap,
updater: CURRENT_USER,
@@ -1860,28 +1953,50 @@ const Component = function (props) {
message.success('已发布,版本号 ' + publishVersionNo);
}
} else {
- setTemplates(function (prev) {
- return prev.map(function (t) {
- if (t.id !== editingRecord.id) return t;
- var fileName = importName
- ? (/\./.test(importName) ? importName : importName + '.docx')
- : t.fileName;
- var patch = {
- fileName: fileName,
- sectionContents: sections,
- sectionVehicleBindings: bindingsMap,
- updater: CURRENT_USER,
- updateTime: now,
- status: 'draft',
- versionNo: null,
- versionName: buildAutoVersionName(null, importName, stripFileExtension(fileName))
- };
- return Object.assign({}, t, patch);
+ if (editingRecord.status === 'published') {
+ setTemplates(function (prev) {
+ return prev.map(function (t) {
+ if (t.id !== editingRecord.id) return t;
+ var fileName = importName
+ ? (/\./.test(importName) ? importName : importName + '.docx')
+ : t.fileName;
+ return Object.assign({}, t, {
+ fileName: fileName,
+ contractType: savedContractType,
+ sectionContents: sections,
+ sectionVehicleBindings: bindingsMap,
+ updater: CURRENT_USER,
+ updateTime: now
+ });
+ });
});
- });
- message.success('草稿已保存(未生成版本号)');
+ message.success('已保存修改,仍保持已发布与停用状态;发布后将生成新版本');
+ } else {
+ setTemplates(function (prev) {
+ return prev.map(function (t) {
+ if (t.id !== editingRecord.id) return t;
+ var fileName = importName
+ ? (/\./.test(importName) ? importName : importName + '.docx')
+ : t.fileName;
+ var patch = {
+ fileName: fileName,
+ contractType: savedContractType,
+ sectionContents: sections,
+ sectionVehicleBindings: bindingsMap,
+ updater: CURRENT_USER,
+ updateTime: now,
+ status: 'draft',
+ versionNo: null,
+ versionName: buildAutoVersionName(null, importName, stripFileExtension(fileName))
+ };
+ return Object.assign({}, t, patch);
+ });
+ });
+ message.success('草稿已保存(未生成版本号)');
+ }
}
}
+ clearEditorDirty();
backToList();
}
@@ -1890,12 +2005,20 @@ const Component = function (props) {
setEditorSectionKey(null);
placeholderPopoverOpenState[1](false);
editorInsertHandlerRef.current = null;
+ clearEditorDirty();
}
- function getActiveDefaultTemplate(category) {
- return templates.find(function (t) {
- return t.category === category && t.isDefault && t.status === 'published';
- }) || null;
+ function requestBackToList() {
+ if (pageView === 'editor' && editorDirtyState[0]) {
+ leaveEditorModalState[1](true);
+ return;
+ }
+ backToList();
+ }
+
+ function confirmLeaveEditor() {
+ leaveEditorModalState[1](false);
+ backToList();
}
function getTemplateDisplayName(record) {
@@ -1903,49 +2026,8 @@ const Component = function (props) {
return record.fileName || record.versionName || '未命名合同';
}
- function confirmSetDefault(record) {
- if (record.status !== 'published') {
- message.warning('仅已发布版本可设为标准模板');
- return;
- }
- if (record.isDefault) {
- message.info('该模板已是当前生效的标准模板');
- return;
- }
- defaultConfirmTargetState[1](record);
- defaultConfirmModalState[1](true);
- }
-
- function handleSetDefault() {
- var record = defaultConfirmTargetState[0];
- if (!record) return;
- var previousDefault = getActiveDefaultTemplate(record.category);
- setTemplates(function (prev) {
- return prev.map(function (t) {
- if (t.category !== record.category) return t;
- if (t.id === record.id) {
- return Object.assign({}, t, { isDefault: true });
- }
- if (t.isDefault) {
- return Object.assign({}, t, { isDefault: false });
- }
- return t;
- });
- });
- defaultConfirmModalState[1](false);
- defaultConfirmTargetState[1](null);
- if (previousDefault && previousDefault.id !== record.id) {
- message.success('已切换标准模板:已禁用「' + getTemplateDisplayName(previousDefault) + '」,当前生效标准为「' + getTemplateDisplayName(record) + '」');
- return;
- }
- message.success('已设为标准模板:「' + getTemplateDisplayName(record) + '」');
- }
-
function confirmDelete(record) {
- if (record.isDefault) {
- message.warning('当前为标准模板,无法删除。请先将其他模板设为标准模板,以禁用本模板的标准状态。');
- return;
- }
+ if (isPublishedTemplate(record) && isTemplateEnabled(record)) return;
deleteTargetState[1](record);
deleteModalState[1](true);
}
@@ -1953,10 +2035,6 @@ const Component = function (props) {
function handleDelete() {
var target = deleteTargetState[0];
if (!target) return;
- if (target.isDefault) {
- message.warning('标准模板无法删除');
- return;
- }
setTemplates(function (prev) { return prev.filter(function (t) { return t.id !== target.id; }); });
deleteModalState[1](false);
deleteTargetState[1](null);
@@ -3341,11 +3419,11 @@ const Component = function (props) {
span.className = 'ct-inline-lock';
span.setAttribute('data-inline-lock', '1');
span.setAttribute('data-lock-id', lockId);
- span.setAttribute('title', '内容锁定区');
+ span.setAttribute('title', '内容锁定区:新增租赁合同时不可篡改');
try {
range.surroundContents(span);
} catch (err) {
- document.execCommand('insertHTML', false, '' + sel.toString() + '');
+ document.execCommand('insertHTML', false, '' + sel.toString() + '');
}
syncHtml();
message.success('已设为内容锁定区');
@@ -3747,6 +3825,7 @@ const Component = function (props) {
className: 'ct-rte-btn ct-rte-btn--text'
+ (opts.muted ? ' is-muted' : '')
+ (opts.compact ? ' is-compact' : ''),
+ 'data-annotation-id': opts.annotationId || undefined,
'data-ct-action': isVehicleClause ? 'vehicle-clause' : undefined,
onMouseDown: function (e) {
e.preventDefault();
@@ -3810,14 +3889,6 @@ const Component = function (props) {
{ key: 'colR', label: '右侧插入列', onClick: function () { tableOp('addColRight'); } },
{ key: 'delTbl', label: '删除表格', onClick: function () { tableOp('deleteTable'); } }
] },
- { type: 'group', label: '合同', children: [
- { key: 'lock', label: '锁定选中内容', onClick: wrapSelectionAsLock },
- { key: 'unlock', label: '解除锁定', onClick: unwrapSelectionLock },
- { key: 'risk', label: '设为风控红线条款', onClick: wrapSelectionAsRiskRedline },
- { key: 'unrisk', label: '取消风控红线条款', onClick: unwrapSelectionRiskRedline },
- enableVehicleClause ? { key: 'vclause', label: '设为条件条款', onClick: beginVehicleClauseWrap } : null,
- enableVehicleClause ? { key: 'unvclause', label: '取消条件条款', onClick: unwrapSelectionVehicleClause } : null
- ].filter(Boolean) },
{ type: 'group', label: '视图', children: [
{ key: 'fullscreen', label: isFullscreen ? '退出全屏' : '全屏编辑', onClick: function () { setFullscreen(!isFullscreen); } },
{ key: 'source', label: isSourceMode ? '返回可视化' : 'HTML 源码', onClick: toggleSourceMode },
@@ -3856,11 +3927,14 @@ const Component = function (props) {
? rteBtn('插入分页符,将光标后的内容移至下一页', rteIcon('pageBreak'), insertWordPageBreak, { key: 'page-break' })
: null,
rteSep('s-risk'),
- rteTextBtn('红线', '设为风控红线条款(选中段落或文字后点击;创建合同时修改将触发非标审批)', wrapSelectionAsRiskRedline, { key: 'risk-redline', compact: true }),
- rteTextBtn('取消', '取消风控红线条款标记', unwrapSelectionRiskRedline, { key: 'unrisk-redline', muted: true, compact: true }),
+ rteTextBtn('红线', '在章节中选中内容后点击;新增租赁合同时若修改红线内文字,将自动变为非标合同。不修改红线内容则仍走标准流程', wrapSelectionAsRiskRedline, { key: 'risk-redline', compact: true, annotationId: 'ct-rte-risk-redline' }),
+ rteTextBtn('取消', '选中已标红线的内容后点击,取消红线标记;取消后新增租赁合同时修改该部分不会触发非标流程', unwrapSelectionRiskRedline, { key: 'unrisk-redline', muted: true, compact: true, annotationId: 'ct-rte-unrisk-redline' }),
enableVehicleClause ? rteSep('s-vclause') : null,
- enableVehicleClause ? rteTextBtn('条件', '设为条件条款(选中段落后指定适用车型;任意命中即显示)', handleVehicleClausePointerDown, { key: 'vehicle-clause', compact: true, vehicleClause: true }) : null,
- enableVehicleClause ? rteTextBtn('取消', '取消条件条款标记', unwrapSelectionVehicleClause, { key: 'unvehicle-clause', muted: true, compact: true }) : null
+ enableVehicleClause ? rteTextBtn('条件', '在合同各章节圈定具体内容,使部分条款仅对指定车型生效。与章节「适用车型」(控制整章是否可见)不同,条件只作用于章节内部分段落', handleVehicleClausePointerDown, { key: 'vehicle-clause', compact: true, vehicleClause: true, annotationId: 'ct-rte-vehicle-clause' }) : null,
+ enableVehicleClause ? rteTextBtn('取消', '条款设条件后,选中该条款内容并点击取消,可解除该部分的车型限定', unwrapSelectionVehicleClause, { key: 'unvehicle-clause', muted: true, compact: true, annotationId: 'ct-rte-unvehicle-clause' }) : null,
+ rteSep('s-lock'),
+ rteTextBtn('锁定', '选中章节内内容后点击;锁定后新增租赁合同时该部分无法被篡改。红线、条件与锁定可作用于同一段内容', wrapSelectionAsLock, { key: 'inline-lock', compact: true, annotationId: 'ct-rte-inline-lock' }),
+ rteTextBtn('解除', '选中已锁定内容后点击,解除锁定;解除后新增租赁合同时可再次编辑该部分', unwrapSelectionLock, { key: 'uninline-lock', muted: true, compact: true, annotationId: 'ct-rte-uninline-lock' })
);
var row2 = React.createElement('div', { className: 'ct-rich-toolbar__row ct-rich-toolbar__row--secondary' },
@@ -4019,7 +4093,6 @@ const Component = function (props) {
setListFilterDraft(function (prev) {
var next = Object.assign({}, prev);
next[field] = value;
- if (field === 'vehicleBrand') next.vehicleModel = '';
return next;
});
}
@@ -4033,40 +4106,104 @@ const Component = function (props) {
editorSyncKeyState[1](function (v) { return v + 1; });
}
- function renderSectionTabsEllipsisIcon() {
+ function renderSectionListIcon() {
return React.createElement('svg', {
- className: 'ct-section-tab__ellipsis-icon',
+ className: 'ct-section-nav__list-icon',
width: 16,
height: 16,
viewBox: '0 0 16 16',
'aria-hidden': true,
focusable: 'false'
},
- React.createElement('circle', { cx: 3, cy: 8, r: 1.25, fill: 'currentColor' }),
- React.createElement('circle', { cx: 8, cy: 8, r: 1.25, fill: 'currentColor' }),
- React.createElement('circle', { cx: 13, cy: 8, r: 1.25, fill: 'currentColor' })
+ React.createElement('path', {
+ d: 'M2.5 4.25h11M2.5 8h11M2.5 11.75h11',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 1.5,
+ strokeLinecap: 'round'
+ })
+ );
+ }
+
+ function renderPlaceholderToolIcon() {
+ return React.createElement('svg', {
+ className: 'ct-panel-tool-btn__icon',
+ width: 16,
+ height: 16,
+ viewBox: '0 0 16 16',
+ 'aria-hidden': true,
+ focusable: 'false'
+ },
+ React.createElement('path', {
+ d: 'M5.5 4.5h5M5.5 8h5M5.5 11.5h3',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 1.4,
+ strokeLinecap: 'round'
+ }),
+ React.createElement('path', {
+ d: 'M3.5 3.5h9v9H3.5z',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 1.4,
+ strokeLinejoin: 'round'
+ })
+ );
+ }
+
+ function renderImportWordToolIcon() {
+ return React.createElement('svg', {
+ className: 'ct-panel-tool-btn__icon',
+ width: 16,
+ height: 16,
+ viewBox: '0 0 16 16',
+ 'aria-hidden': true,
+ focusable: 'false'
+ },
+ React.createElement('path', {
+ d: 'M8.5 2.5 12.5 6.5H9.5a1 1 0 0 1-1-1V2.5z',
+ fill: 'currentColor',
+ opacity: 0.18
+ }),
+ React.createElement('path', {
+ d: 'M8.5 2.5 12.5 6.5H9.5a1 1 0 0 1-1-1V2.5zM4.5 3.5h4v3h3.5v6.5h-7.5V3.5z',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 1.4,
+ strokeLinejoin: 'round'
+ }),
+ React.createElement('path', {
+ d: 'M6.25 9h3.5M6.25 11h2.25',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 1.4,
+ strokeLinecap: 'round'
+ })
);
}
function renderSectionTabsMorePanel() {
var query = String(sectionMoreSearchState[0] || '').trim().toLowerCase();
- var filtered = DOCUMENT_SECTION_TABS.filter(function (tab) {
+ var filtered = DOCUMENT_SECTION_TABS.filter(function (tab, index) {
if (!query) return true;
- return String(tab.label || '').toLowerCase().indexOf(query) >= 0;
+ var label = String(tab.label || '').toLowerCase();
+ var order = String(index + 1);
+ return label.indexOf(query) >= 0 || order.indexOf(query) >= 0;
});
return React.createElement('div', { className: 'ct-section-tabs-more' },
React.createElement('p', { className: 'ct-section-tabs-more__title' }, '全部章节'),
React.createElement(Input, {
className: 'ct-section-tabs-more__search',
- placeholder: '请输入内容',
+ placeholder: '搜索章节名称或序号',
value: sectionMoreSearchState[0],
onChange: function (e) { sectionMoreSearchState[1](e.target.value); },
allowClear: true,
- 'aria-label': '搜索模板类型'
+ 'aria-label': '搜索章节'
}),
- React.createElement('div', { className: 'ct-section-tabs-more__list', role: 'tablist', 'aria-label': '全部模板类型' },
+ React.createElement('div', { className: 'ct-section-tabs-more__list', role: 'tablist', 'aria-label': '全部章节' },
filtered.length
? filtered.map(function (tab) {
+ var tabIndex = DOCUMENT_SECTION_TABS.findIndex(function (item) { return item.key === tab.key; });
return React.createElement('button', {
key: tab.key,
type: 'button',
@@ -4078,54 +4215,64 @@ const Component = function (props) {
sectionMoreOpenState[1](false);
sectionMoreSearchState[1]('');
}
- }, tab.label);
+ },
+ React.createElement('span', { className: 'ct-section-tabs-more__index' }, tabIndex + 1),
+ React.createElement('span', { className: 'ct-section-tabs-more__label' }, tab.label)
+ );
})
- : React.createElement('p', { className: 'ct-section-tabs-more__empty' }, '无匹配模板类型')
+ : React.createElement('p', { className: 'ct-section-tabs-more__empty' }, '无匹配章节')
)
);
}
function renderSectionTabs() {
- var visibleTabs = DOCUMENT_SECTION_TABS.slice(0, SECTION_VISIBLE_TAB_COUNT);
- var overflowTabs = DOCUMENT_SECTION_TABS.slice(SECTION_VISIBLE_TAB_COUNT);
- var overflowActive = overflowTabs.some(function (tab) { return tab.key === activeSectionKey; });
- var activeOverflowLabel = overflowActive ? getSectionTabLabel(activeSectionKey) : '';
- return React.createElement('div', { className: 'ct-section-tabs', role: 'tablist', 'aria-label': '合同章节' },
- visibleTabs.map(function (tab) {
- return React.createElement('button', {
- key: tab.key,
- type: 'button',
- role: 'tab',
- className: 'ct-section-tab' + (activeSectionKey === tab.key ? ' is-active' : ''),
- 'aria-selected': activeSectionKey === tab.key,
- onClick: function () { switchSectionTab(tab.key); }
- }, tab.label);
- }),
- overflowTabs.length
- ? React.createElement(Popover, {
- trigger: 'click',
- placement: 'bottomRight',
- open: sectionMoreOpenState[0],
- onOpenChange: function (open) {
- sectionMoreOpenState[1](open);
- if (!open) sectionMoreSearchState[1]('');
- },
- content: renderSectionTabsMorePanel(),
- overlayClassName: 'ct-section-tabs-more-popover',
- arrow: false
- },
- React.createElement('button', {
- type: 'button',
- role: 'tab',
- className: 'ct-section-tab ct-section-tab--more' + (overflowActive ? ' is-active' : ''),
- 'aria-selected': overflowActive,
- 'aria-haspopup': 'dialog',
- 'aria-expanded': sectionMoreOpenState[0],
- 'aria-label': overflowActive ? ('更多章节,当前:' + activeOverflowLabel) : '更多章节',
- title: overflowActive ? activeOverflowLabel : '展开更多章节'
- }, renderSectionTabsEllipsisIcon())
+ var activeIndex = getActiveSectionTabIndex();
+ var activeTab = DOCUMENT_SECTION_TABS[activeIndex] || DOCUMENT_SECTION_TABS[0];
+ return React.createElement('div', { className: 'ct-section-nav' },
+ React.createElement('div', { className: 'ct-section-tabs-scroll', ref: sectionTabsScrollRef },
+ React.createElement('div', { className: 'ct-section-tabs', role: 'tablist', 'aria-label': '合同章节' },
+ DOCUMENT_SECTION_TABS.map(function (tab, index) {
+ var isActive = activeSectionKey === tab.key;
+ return React.createElement('button', {
+ key: tab.key,
+ type: 'button',
+ role: 'tab',
+ className: 'ct-section-tab' + (isActive ? ' is-active' : ''),
+ 'aria-selected': isActive,
+ title: tab.label,
+ onClick: function () { switchSectionTab(tab.key); }
+ },
+ React.createElement('span', { className: 'ct-section-tab__index' }, index + 1),
+ React.createElement('span', { className: 'ct-section-tab__label' }, tab.label)
+ );
+ })
)
- : null
+ ),
+ React.createElement(Popover, {
+ trigger: 'click',
+ placement: 'bottomRight',
+ open: sectionMoreOpenState[0],
+ onOpenChange: function (open) {
+ sectionMoreOpenState[1](open);
+ if (!open) sectionMoreSearchState[1]('');
+ },
+ content: renderSectionTabsMorePanel(),
+ overlayClassName: 'ct-section-tabs-more-popover',
+ arrow: false
+ },
+ React.createElement('button', {
+ type: 'button',
+ className: 'ct-section-nav__list-btn' + (sectionMoreOpenState[0] ? ' is-open' : ''),
+ 'aria-haspopup': 'dialog',
+ 'aria-expanded': sectionMoreOpenState[0],
+ 'aria-label': '打开章节列表,当前:' + activeTab.label,
+ title: '章节列表'
+ },
+ renderSectionListIcon(),
+ React.createElement('span', { className: 'ct-section-nav__list-btn-text' }, '章节列表')
+ )
+ ),
+ React.createElement('p', { className: 'ct-section-nav__current', title: activeTab.label }, activeTab.label)
);
}
@@ -4203,11 +4350,25 @@ const Component = function (props) {
);
}
- function getVehicleModelOptions(brand) {
- var entry = VEHICLE_BRAND_MODEL_CATALOG.find(function (item) { return item.brand === brand; });
- return entry ? entry.models.slice() : [];
+ function handleVehicleBindingsChange(nextBindings) {
+ setFormVehicleBindings(nextBindings || []);
+ markEditorDirty();
}
+ function renderVehicleBindingEditor() {
+ var sectionKey = getCurrentSectionKey();
+ if (!sectionAllowsVehicleBinding(sectionKey)) return null;
+ return React.createElement('section', { className: 'ct-vehicle-binding-card vm-filter-card', 'aria-label': '适用车型', 'data-annotation-id': 'ct-vehicle-binding' },
+ React.createElement('div', { className: 'ct-vehicle-binding-card__head' },
+ React.createElement('h3', { className: 'ct-vehicle-binding-card__title' }, '适用车型'),
+ React.createElement('p', { className: 'ct-vehicle-binding-card__desc' }, '不绑定则适用于全部车型;绑定后仅在创建合同时匹配到对应品牌、型号的车辆才纳入该条款。')
+ ),
+ React.createElement(VehicleBrandModelPicker, {
+ value: formVehicleBindings,
+ onChange: handleVehicleBindingsChange,
+ })
+ );
+ }
function getVehiclePreviewOptions() {
var options = [];
VEHICLE_BRAND_MODEL_CATALOG.forEach(function (item) {
@@ -4221,79 +4382,39 @@ const Component = function (props) {
return options;
}
- function addVehicleBinding() {
- var brand = vehicleBindingBrandState[0];
- var model = vehicleBindingModelState[0];
- if (!brand || !model) {
- message.warning('请选择品牌和型号');
- return;
- }
- var key = vehicleBindingKey({ brand: brand, model: model });
- var exists = (formVehicleBindings || []).some(function (item) {
- return vehicleBindingKey(item) === key;
- });
- if (exists) {
- message.info('该车型已添加');
- return;
- }
- setFormVehicleBindings(function (prev) {
- return (prev || []).concat([{ brand: brand, model: model }]);
- });
- }
-
- function removeVehicleBinding(index) {
- setFormVehicleBindings(function (prev) {
- return (prev || []).filter(function (_, idx) { return idx !== index; });
- });
- }
-
- function renderVehicleBindingEditor() {
+ function renderContractTypeEditor() {
var sectionKey = getCurrentSectionKey();
- if (!sectionAllowsVehicleBinding(sectionKey)) return null;
- var brand = vehicleBindingBrandState[0];
- var models = getVehicleModelOptions(brand);
- return React.createElement('section', { className: 'ct-vehicle-binding-card vm-filter-card', 'aria-label': '适用车型', 'data-annotation-id': 'ct-vehicle-binding' },
+ if (sectionKey !== 'main') return null;
+ return React.createElement('section', {
+ className: 'ct-contract-type-card ct-vehicle-binding-card vm-filter-card',
+ 'aria-label': '合同类型',
+ 'data-annotation-id': 'ct-contract-type',
+ },
React.createElement('div', { className: 'ct-vehicle-binding-card__head' },
- React.createElement('h3', { className: 'ct-vehicle-binding-card__title' }, '适用车型'),
- React.createElement('p', { className: 'ct-vehicle-binding-card__desc' }, '不绑定则适用于全部车型;绑定后仅在创建合同时匹配到对应品牌、型号的车辆才纳入该条款。')
- ),
- React.createElement('div', { className: 'ct-vehicle-binding-card__form' },
- React.createElement(Select, {
- className: 'ct-vehicle-binding-brand',
- value: brand,
- onChange: function (value) {
- vehicleBindingBrandState[1](value);
- vehicleBindingModelState[1]('');
- },
- options: VEHICLE_BRAND_MODEL_CATALOG.map(function (item) {
- return { value: item.brand, label: item.brand };
- })
- }),
- React.createElement(Select, {
- className: 'ct-vehicle-binding-model',
- value: vehicleBindingModelState[0] || undefined,
- placeholder: '选择型号',
- onChange: vehicleBindingModelState[1],
- options: models.map(function (model) {
- return { value: model, label: model };
- })
- }),
- React.createElement(Button, { onClick: addVehicleBinding }, '添加车型')
- ),
- (formVehicleBindings && formVehicleBindings.length)
- ? React.createElement('div', { className: 'ct-vehicle-binding-tags' },
- formVehicleBindings.map(function (item, index) {
- return React.createElement(Tag, {
- key: vehicleBindingKey(item) + '-' + index,
- closable: true,
- onClose: function (e) {
- if (e && e.preventDefault) e.preventDefault();
- removeVehicleBinding(index);
- }
- }, item.brand + ' · ' + item.model);
- })
+ React.createElement('h3', { className: 'ct-vehicle-binding-card__title' },
+ React.createElement('span', { className: 'ct-form-field__required', 'aria-hidden': 'true' }, '*'),
+ '合同类型'
+ ),
+ React.createElement('p', { className: 'ct-vehicle-binding-card__desc' },
+ '必填。用于列表展示,并供租赁合同创建时按类型筛选模板;请自行填写,如「正式合同」「现代18吨试用合同」。'
)
- : React.createElement('p', { className: 'ct-vehicle-binding-empty' }, '当前未绑定车型,表示全部车型适用')
+ ),
+ React.createElement('div', { className: 'ct-contract-type-card__form' },
+ React.createElement(Input, {
+ className: 'ct-editor-contract-type-input',
+ value: editorContractType,
+ onChange: function (e) {
+ setEditorContractType(e.target.value);
+ markEditorDirty();
+ },
+ placeholder: '请输入合同类型,如:正式合同',
+ maxLength: 32,
+ allowClear: true,
+ required: true,
+ 'aria-required': 'true',
+ 'aria-label': '合同类型',
+ })
+ )
);
}
@@ -4310,8 +4431,7 @@ const Component = function (props) {
React.createElement('th', { scope: 'col' }, '章节'),
React.createElement('th', { scope: 'col' }, '适用车型'),
React.createElement('th', { scope: 'col', className: 'ct-chapter-native-table__num' }, '风控条款数'),
- React.createElement('th', { scope: 'col', className: 'ct-chapter-native-table__num' }, '条件条款数'),
- React.createElement('th', { scope: 'col', className: 'ct-chapter-native-table__status' }, '内容状态')
+ React.createElement('th', { scope: 'col', className: 'ct-chapter-native-table__num' }, '条件条款数')
)
),
React.createElement('tbody', null,
@@ -4325,12 +4445,7 @@ const Component = function (props) {
React.createElement('span', { className: 'ct-vehicle-bindings-cell' }, vehicleText)
),
React.createElement('td', { className: 'ct-chapter-native-table__num' }, renderClauseCountCell(row.riskClauseCount)),
- React.createElement('td', { className: 'ct-chapter-native-table__num' }, renderClauseCountCell(row.vehicleClauseCount)),
- React.createElement('td', { className: 'ct-chapter-native-table__status' },
- row.hasContent
- ? React.createElement(Tag, { color: 'success' }, '已配置')
- : React.createElement(Tag, null, '未配置')
- )
+ React.createElement('td', { className: 'ct-chapter-native-table__num' }, renderClauseCountCell(row.vehicleClauseCount))
);
})
)
@@ -4338,13 +4453,70 @@ const Component = function (props) {
);
}
+ function renderTemplateRowActions(record) {
+ var published = isPublishedTemplate(record);
+ var enabled = isTemplateEnabled(record);
+ var actionLocked = published && enabled;
+ var lockTitle = '启用中的模板不可操作,请先停用';
+ var menuItems = [];
+
+ if (published) {
+ menuItems.push({
+ key: 'version-log',
+ label: '版本日志'
+ });
+ }
+ menuItems.push({
+ key: 'delete',
+ label: '删除',
+ disabled: actionLocked,
+ danger: true
+ });
+
+ function onMenuClick(info) {
+ if (info.key === 'version-log') openVersionLog(record);
+ if (info.key === 'delete' && !actionLocked) confirmDelete(record);
+ }
+
+ return React.createElement('div', { className: 'ct-row-actions' },
+ published
+ ? (enabled
+ ? React.createElement(Button, {
+ type: 'link', size: 'small', className: 'ct-action-btn', key: 'disable',
+ onClick: function () { confirmDisableTemplate(record); }
+ }, '停用')
+ : React.createElement(Button, {
+ type: 'link', size: 'small', className: 'ct-action-btn', key: 'enable',
+ onClick: function () { toggleTemplateEnabled(record, true); }
+ }, '启用'))
+ : null,
+ React.createElement(Button, {
+ type: 'link', size: 'small', className: 'ct-action-btn', key: 'edit',
+ disabled: actionLocked,
+ title: actionLocked ? lockTitle : undefined,
+ onClick: actionLocked ? undefined : function () { openEdit(record); }
+ }, '编辑'),
+ menuItems.length
+ ? React.createElement(Dropdown, {
+ key: 'more',
+ trigger: ['click'],
+ menu: { items: menuItems, onClick: onMenuClick }
+ }, React.createElement(Button, {
+ type: 'link', size: 'small', className: 'ct-action-btn ct-row-actions__more',
+ 'aria-label': '更多操作'
+ }, '更多'))
+ : null
+ );
+ }
+
function getListColumns() {
var cols = [
- { title: '合同名称', dataIndex: 'fileName', key: 'fileName', width: 280, ellipsis: true,
- render: function (v, r) {
+ { title: '合同类型', dataIndex: 'contractType', key: 'contractType', width: 160, ellipsis: true,
+ render: function (_, r) {
+ var typeLabel = getContractTypeLabel(resolveContractType(r));
return React.createElement('div', { className: 'ct-contract-name-cell' },
- React.createElement('span', { className: 'ct-contract-name-cell__name', title: v || '' }, v || '—'),
- r.isDefault ? React.createElement(Tag, { color: 'gold' }, '标准合同') : null
+ React.createElement('span', { className: 'ct-contract-name-cell__name', title: typeLabel }, typeLabel),
+ renderEnabledTag(r)
);
}
},
@@ -4354,28 +4526,13 @@ const Component = function (props) {
{ title: '状态', dataIndex: 'status', key: 'status', width: 88, align: 'center', render: statusTag }
];
cols = cols.concat([
- { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 150 },
- { title: '创建人', dataIndex: 'creator', key: 'creator', width: 110, render: formatPersonDisplayName },
- { title: '最后更新时间', dataIndex: 'updateTime', key: 'updateTime', width: 150 },
- { title: '更新人', dataIndex: 'updater', key: 'updater', width: 110, render: formatPersonDisplayName },
+ { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 140, className: 'ct-col-optional' },
+ { title: '创建人', dataIndex: 'creator', key: 'creator', width: 100, className: 'ct-col-optional', render: formatPersonDisplayName },
+ { title: '最后更新时间', dataIndex: 'updateTime', key: 'updateTime', width: 140, className: 'ct-col-optional' },
+ { title: '更新人', dataIndex: 'updater', key: 'updater', width: 100, className: 'ct-col-optional', render: formatPersonDisplayName },
{
- title: '操作', key: 'action', width: 200,
- render: function (_, record) {
- var actions = [
- React.createElement(Button, { type: 'link', size: 'small', className: 'ct-action-btn', key: 'edit', onClick: function () { openEdit(record); } }, '编辑')
- ];
- if (record.status === 'published' && !record.isDefault) {
- actions.push(
- React.createElement(Button, { type: 'link', size: 'small', className: 'ct-action-btn', key: 'default', onClick: function () { confirmSetDefault(record); } }, '设为标准模板')
- );
- }
- if (!record.isDefault) {
- actions.push(
- React.createElement(Button, { type: 'link', size: 'small', className: 'ct-action-btn', key: 'delete', danger: true, onClick: function () { confirmDelete(record); } }, '删除')
- );
- }
- return React.createElement(Space, { size: 4, wrap: true }, actions);
- }
+ title: '操作', key: 'action', width: 200, fixed: 'right',
+ render: function (_, record) { return renderTemplateRowActions(record); }
}
]);
return cols;
@@ -4396,7 +4553,6 @@ const Component = function (props) {
tableLayout: 'fixed',
rowClassName: function () { return 'ct-row-hover'; },
pagination: { pageSize: 10, showTotal: function (t) { return '共 ' + t + ' 条'; }, showSizeChanger: false },
- scroll: { x: LIST_TABLE_SCROLL_X },
locale: { emptyText: emptyText },
expandable: {
columnWidth: LIST_EXPAND_COL_WIDTH,
@@ -4420,7 +4576,6 @@ const Component = function (props) {
return doc.category === categoryFilter
&& documentMatchesListFilters(doc, listFilterApplied);
}).sort(function (a, b) {
- if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
return String(b.updateTime).localeCompare(String(a.updateTime));
});
}
@@ -4435,19 +4590,36 @@ const Component = function (props) {
});
}
+ function getContractTypeFilterOptions() {
+ var seen = {};
+ var options = [];
+ templates.forEach(function (doc) {
+ if (doc.category !== categoryFilter) return;
+ var type = resolveContractType(doc);
+ if (!type || seen[type]) return;
+ seen[type] = true;
+ options.push({ value: type, label: type });
+ });
+ return options.sort(function (a, b) {
+ return String(a.label).localeCompare(String(b.label), 'zh-CN');
+ });
+ }
+
function renderListFilterPanel() {
- var showVehicle = true;
var fields = [
React.createElement('label', { key: 'keyword', className: 'vm-filter-field' },
- React.createElement('span', null, '合同名称'),
- React.createElement(Input, {
- className: 'vm-input',
- placeholder: '请输入合同名称',
- value: listFilterDraft.keyword,
+ React.createElement('span', null, '合同类型'),
+ React.createElement(Select, {
+ className: 'ct-list-filter-select',
+ value: listFilterDraft.keyword || undefined,
+ placeholder: '请选择或搜索合同类型',
allowClear: true,
- onChange: function (e) { updateListFilterDraft('keyword', e.target.value); },
- onPressEnter: applyListFilters,
- 'aria-label': '合同名称'
+ showSearch: true,
+ optionFilterProp: 'label',
+ filterOption: filterContractNameOption,
+ onChange: function (v) { updateListFilterDraft('keyword', v || ''); },
+ options: getContractTypeFilterOptions(),
+ 'aria-label': '合同类型'
})
),
React.createElement('label', { key: 'status', className: 'vm-filter-field' },
@@ -4474,25 +4646,22 @@ const Component = function (props) {
onChange: function (v) { updateListFilterDraft('creator', v || ''); },
options: getCreatorFilterOptions()
})
+ ),
+ React.createElement('label', { key: 'enabled', className: 'vm-filter-field' },
+ React.createElement('span', null, '启用状态'),
+ React.createElement(Select, {
+ className: 'ct-list-filter-select',
+ value: listFilterDraft.enabled || undefined,
+ placeholder: '全部',
+ allowClear: true,
+ onChange: function (v) { updateListFilterDraft('enabled', v || ''); },
+ options: [
+ { value: 'enabled', label: '启用' },
+ { value: 'disabled', label: '停用' }
+ ]
+ })
)
];
- if (showVehicle) {
- fields.push(
- React.createElement('label', { key: 'vehicleBrand', className: 'vm-filter-field' },
- React.createElement('span', null, '适用品牌'),
- React.createElement(Select, {
- className: 'ct-list-filter-select',
- value: listFilterDraft.vehicleBrand || undefined,
- placeholder: '全部品牌',
- allowClear: true,
- onChange: function (v) { updateListFilterDraft('vehicleBrand', v || ''); },
- options: VEHICLE_BRAND_MODEL_CATALOG.map(function (item) {
- return { value: item.brand, label: item.brand };
- })
- })
- )
- );
- }
return React.createElement('section', { className: 'vm-filter-card', 'aria-label': '筛选条件', 'data-annotation-id': 'ct-list-filter' },
React.createElement('header', { className: 'vm-filter-header' },
React.createElement('h2', { className: 'vm-filter-title' }, '筛选条件')
@@ -4509,15 +4678,15 @@ const Component = function (props) {
var sectionKey = getCurrentSectionKey();
var activeSectionHtml = getEditorDisplayHtml();
var hasContent = hasPreviewContent(activeSectionHtml);
- var isMainSection = sectionAllowsMainVehicleClauses(sectionKey);
- var clauseTotal = isMainSection ? countVehicleClauseRegions(activeSectionHtml) : 0;
- var clauseMatched = isMainSection ? countMatchedVehicleClauses(activeSectionHtml, previewSimVehicleKeys) : 0;
- var previewOptions = isMainSection ? { simVehicleKeys: previewSimVehicleKeys } : null;
+ var supportsVehicleClause = sectionAllowsMainVehicleClauses(sectionKey);
+ var clauseTotal = supportsVehicleClause ? countVehicleClauseRegions(activeSectionHtml) : 0;
+ var clauseMatched = supportsVehicleClause ? countMatchedVehicleClauses(activeSectionHtml, previewSimVehicleKeys) : 0;
+ var previewOptions = supportsVehicleClause ? { simVehicleKeys: previewSimVehicleKeys } : null;
var previewPanelClass = 'ct-preview-panel'
+ (!hasContent ? ' ct-preview-panel--empty' : '')
+ (hasContent && isWordFormattedDoc(activeSectionHtml) ? ' ct-preview-panel--word' : '');
var previewBadge = '锁定 ' + countLockRegions(activeSectionHtml) + ' 处 · 风控红线 ' + countRiskRedlineRegions(activeSectionHtml) + ' 处';
- if (isMainSection) {
+ if (supportsVehicleClause) {
previewBadge += ' · 条件条款 ' + clauseTotal + ' 处';
if (previewSimVehicleKeys.length) previewBadge += '(预览命中 ' + clauseMatched + ')';
}
@@ -4526,8 +4695,13 @@ const Component = function (props) {
React.createElement(Col, { xs: 24, lg: 12 },
React.createElement('section', { className: 'ct-editor-column-card', 'aria-label': '编辑区', 'data-annotation-id': 'ct-editor-panel' },
React.createElement('div', { className: 'ct-panel-head' },
- React.createElement('div', { className: 'ct-panel-head__title' }, '编辑区',
- React.createElement('span', { className: 'ct-panel-head__badge' }, getSectionTabLabel(sectionKey))
+ React.createElement('div', { className: 'ct-panel-head__title' },
+ React.createElement('span', null, '编辑区',
+ React.createElement('span', { className: 'ct-panel-head__badge' }, getSectionTabLabel(sectionKey))
+ ),
+ hasContent && isWordFormattedDoc(activeSectionHtml)
+ ? React.createElement('span', { className: 'ct-editor-layout-hint' }, 'A4 版式较宽时可横向滑动查看')
+ : null
),
React.createElement('div', { className: 'ct-panel-head__actions' },
React.createElement(Popover, {
@@ -4540,18 +4714,18 @@ const Component = function (props) {
overlayInnerStyle: { padding: '12px 14px' }
},
React.createElement(Button, {
- className: 'vm-btn ct-placeholder-trigger' + (placeholderPopoverOpenState[0] ? ' vm-btn-primary ct-btn-cta' : ' vm-btn-secondary'),
- type: placeholderPopoverOpenState[0] ? 'primary' : 'default'
- }, '{ } 模板占位符')
+ className: 'ct-panel-tool-btn ct-panel-tool-btn--ghost' + (placeholderPopoverOpenState[0] ? ' is-active' : ''),
+ type: 'default'
+ }, renderPlaceholderToolIcon(), '模板占位符')
),
React.createElement(Upload, {
accept: '.doc,.docx',
showUploadList: false,
beforeUpload: handleImportWord
}, React.createElement(Button, {
- className: 'vm-btn ' + (!hasContent && editorMode === 'create' ? 'vm-btn-primary ct-btn-cta' : 'vm-btn-secondary'),
- type: !hasContent && editorMode === 'create' ? 'primary' : 'default'
- }, '导入 Word 模板'))
+ className: 'ct-panel-tool-btn' + (!hasContent && editorMode === 'create' ? ' ct-panel-tool-btn--accent' : ' ct-panel-tool-btn--ghost'),
+ type: 'default'
+ }, renderImportWordToolIcon(), '导入 Word 模板'))
)
),
React.createElement('div', { key: 'editor-panel-' + editorSyncKeyState[0] + '-' + getCurrentSectionKey(), className: 'ct-editor-panel' }, renderUnifiedEditor())
@@ -4563,7 +4737,7 @@ const Component = function (props) {
React.createElement('div', { className: 'ct-panel-head__title' }, '实时预览',
React.createElement('span', { className: 'ct-panel-head__badge' }, previewBadge)
),
- isMainSection && hasContent
+ supportsVehicleClause && hasContent
? React.createElement('div', { className: 'ct-panel-head__actions' },
React.createElement('div', { className: 'ct-preview-sim-vehicles' },
React.createElement('span', { className: 'ct-preview-sim-vehicles__label' }, '模拟车型'),
@@ -4595,23 +4769,10 @@ const Component = function (props) {
);
}
- function renderMainVehicleClauseTip() {
- var sectionKey = getCurrentSectionKey();
- if (!sectionAllowsMainVehicleClauses(sectionKey)) return null;
- return React.createElement('section', { className: 'ct-vehicle-binding-card vm-filter-card ct-main-clause-tip', 'aria-label': '条件条款说明', 'data-annotation-id': 'ct-main-clause-tip' },
- React.createElement('div', { className: 'ct-vehicle-binding-card__head' },
- React.createElement('h3', { className: 'ct-vehicle-binding-card__title' }, '合同主体 · 条件条款'),
- React.createElement('p', { className: 'ct-vehicle-binding-card__desc' },
- '在编辑区选中段落后,点击工具栏「条件」并指定适用车型。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。'
- )
- )
- );
- }
-
function renderEditorPage() {
return React.createElement('div', { className: 'ct-editor-page' },
React.createElement('div', { className: 'ct-editor-page__topbar vm-detail-topbar', 'data-annotation-id': 'ct-editor-topbar' },
- React.createElement(Button, { className: 'ct-editor-page__back-btn vm-btn vm-btn-back', onClick: backToList }, '← 返回列表'),
+ React.createElement(Button, { className: 'ct-editor-page__back-btn vm-btn vm-btn-back', onClick: requestBackToList }, '← 返回列表'),
React.createElement('div', { className: 'ct-editor-page__topbar-actions' },
React.createElement(Button, {
className: 'vm-btn vm-btn-secondary',
@@ -4627,7 +4788,7 @@ const Component = function (props) {
React.createElement('nav', { className: 'ct-editor-page__sections', 'aria-label': '合同章节导航', 'data-annotation-id': 'ct-section-tabs' },
renderSectionTabs()
),
- renderMainVehicleClauseTip(),
+ renderContractTypeEditor(),
renderVehicleBindingEditor(),
React.createElement('div', { className: 'ct-editor-page__body' }, renderEditorWorkspace())
);
@@ -4652,41 +4813,15 @@ const Component = function (props) {
if (props && props.navigationRef) {
props.navigationRef.current = {
openCreate: openCreate,
- backToList: backToList
+ backToList: requestBackToList
};
}
return React.createElement(App, null,
React.createElement('style', null, pageStyle),
- React.createElement('div', { className: 'vm-page ct-page' + (pageView === 'editor' ? ' ct-editor-shell' : '') },
+ React.createElement('div', { className: 'vm-page ct-page' + (pageView === 'editor' ? ' ct-editor-shell' : ' ct-list-shell') },
pageView === 'editor' ? renderEditorPage() : renderListPage(),
- React.createElement(Modal, {
- wrapClassName: 'ct-modal-wrap',
- title: '确认设为标准模板',
- open: defaultConfirmModalState[0],
- onOk: handleSetDefault,
- onCancel: function () { defaultConfirmModalState[1](false); defaultConfirmTargetState[1](null); },
- okText: '确认设置',
- cancelText: '取消'
- }, defaultConfirmTargetState[0]
- ? (function () {
- var target = defaultConfirmTargetState[0];
- var currentDefault = getActiveDefaultTemplate(target.category);
- var switching = currentDefault && currentDefault.id !== target.id;
- return React.createElement('div', { style: { lineHeight: 1.6 } },
- '同一分类同时只能有一套标准模板生效。确认后,客服草拟新合同时将默认使用新标准模板。',
- switching
- ? React.createElement('div', { style: { marginTop: 12, fontSize: 13, color: '#64748B' } },
- '将先禁用当前标准模板「', getTemplateDisplayName(currentDefault), '」,再设置「', getTemplateDisplayName(target), '」为新的标准模板。'
- )
- : React.createElement('div', { style: { marginTop: 12, fontSize: 13, color: '#64748B' } },
- '将「', target.versionNo || '草稿', ' · ', getTemplateDisplayName(target), '」设为标准模板。'
- )
- );
- })()
- : null),
-
React.createElement(Modal, {
wrapClassName: 'ct-modal-wrap',
title: '确认删除',
@@ -4700,6 +4835,101 @@ const Component = function (props) {
: null
),
+ React.createElement(Modal, {
+ wrapClassName: 'ct-modal-wrap',
+ title: '确认停用',
+ open: disableModalState[0],
+ onOk: handleConfirmDisable,
+ onCancel: function () { disableModalState[1](false); disableTargetState[1](null); },
+ okText: '确定停用',
+ cancelText: '取消'
+ }, disableTargetState[0]
+ ? React.createElement('div', null,
+ '停用「', getTemplateDisplayName(disableTargetState[0]), '」后,租赁合同创建时将无法选用该模板,是否确定?'
+ )
+ : null
+ ),
+
+ React.createElement(Modal, {
+ wrapClassName: 'ct-modal-wrap',
+ title: '未保存的修改',
+ open: leaveEditorModalState[0],
+ onOk: confirmLeaveEditor,
+ onCancel: function () { leaveEditorModalState[1](false); },
+ okText: '离开且不保存',
+ cancelText: '继续编辑'
+ }, React.createElement('div', null, '当前有未保存的修改,离开后将丢失。确定返回列表吗?')),
+
+ React.createElement(Modal, {
+ wrapClassName: 'ct-modal-wrap',
+ title: versionLogTargetState[0]
+ ? ('版本日志 · ' + getTemplateDisplayName(versionLogTargetState[0]))
+ : '版本日志',
+ open: versionLogModalState[0],
+ width: 720,
+ footer: null,
+ onCancel: function () { versionLogModalState[1](false); versionLogTargetState[1](null); }
+ }, versionLogTargetState[0]
+ ? React.createElement('div', null,
+ React.createElement('div', { className: 'ct-version-log-hint' },
+ '每次发布变更前,系统会自动将旧版本内容归档为快照,便于回溯比对。'
+ ),
+ React.createElement(Table, {
+ size: 'small',
+ rowKey: 'id',
+ pagination: false,
+ dataSource: (versionLogTargetState[0].versionLogs || []).slice().reverse(),
+ locale: { emptyText: '暂无版本日志' },
+ columns: [
+ { title: '版本号', dataIndex: 'versionNo', key: 'versionNo', width: 88 },
+ { title: '操作', dataIndex: 'action', key: 'action', width: 96 },
+ { title: '操作人', dataIndex: 'operator', key: 'operator', width: 100, render: formatPersonDisplayName },
+ { title: '时间', dataIndex: 'time', key: 'time', width: 140 },
+ { title: '说明', dataIndex: 'remark', key: 'remark', ellipsis: true },
+ {
+ title: '快照', key: 'snapshot', width: 88, align: 'center',
+ render: function (_, logEntry) {
+ return React.createElement(Button, {
+ type: 'link', size: 'small',
+ disabled: !logEntry.snapshot,
+ onClick: function () { openSnapshotView(logEntry); }
+ }, '查看');
+ }
+ }
+ ]
+ })
+ )
+ : null
+ ),
+
+ React.createElement(Modal, {
+ wrapClassName: 'ct-modal-wrap',
+ title: snapshotViewState[0]
+ ? ('版本快照 · ' + (snapshotViewState[0].versionNo || '—'))
+ : '版本快照',
+ open: !!snapshotViewState[0],
+ width: 900,
+ footer: null,
+ onCancel: function () { snapshotViewState[1](null); }
+ }, snapshotViewState[0] && snapshotViewState[0].snapshot
+ ? (function () {
+ var snap = snapshotViewState[0].snapshot;
+ var previewHtml = mergeMonolithicHtml(Object.assign(emptySectionMap(), snap.sectionContents || {}));
+ return React.createElement('div', null,
+ React.createElement('div', { className: 'ct-snapshot-meta' },
+ React.createElement('div', null, '合同名称:', snap.fileName || snap.versionName || '—'),
+ React.createElement('div', null, '版本号:', snap.versionNo || '—'),
+ React.createElement('div', null, '归档时间:', snap.archivedAt || snapshotViewState[0].time || '—')
+ ),
+ React.createElement('div', {
+ className: 'ct-snapshot-preview ct-word-doc',
+ dangerouslySetInnerHTML: { __html: previewHtml || '(无正文快照)
' }
+ })
+ );
+ })()
+ : React.createElement('div', null, '暂无快照内容')
+ ),
+
)
);
};
diff --git a/src/prototypes/contract-template-management/VehicleBrandModelPicker.jsx b/src/prototypes/contract-template-management/VehicleBrandModelPicker.jsx
new file mode 100644
index 0000000..7cc203d
--- /dev/null
+++ b/src/prototypes/contract-template-management/VehicleBrandModelPicker.jsx
@@ -0,0 +1,352 @@
+import React, { useState } from 'react';
+import { Popover } from 'antd';
+import {
+ LEASE_VEHICLE_BRAND_MODEL_CATALOG,
+ formatBrandModelPair,
+} from '../lease-contract-management/lease-order-vars.js';
+import {
+ VEHICLE_BINDING_ALL_BRAND,
+ isAllVehicleBindings,
+ vehicleBindingKey,
+} from './vehicle-binding-utils.js';
+
+function SearchIcon() {
+ return React.createElement('svg', {
+ width: 16,
+ height: 16,
+ viewBox: '0 0 24 24',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 2,
+ 'aria-hidden': true,
+ },
+ React.createElement('circle', { cx: 11, cy: 11, r: 7 }),
+ React.createElement('path', { d: 'M20 20l-3-3' })
+ );
+}
+
+function CloseIcon() {
+ return React.createElement('svg', {
+ width: 14,
+ height: 14,
+ viewBox: '0 0 24 24',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 2,
+ 'aria-hidden': true,
+ },
+ React.createElement('path', { d: 'M18 6L6 18M6 6l12 12' })
+ );
+}
+
+function ChevronDownIcon() {
+ return React.createElement('svg', {
+ width: 14,
+ height: 14,
+ viewBox: '0 0 24 24',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 2,
+ 'aria-hidden': true,
+ },
+ React.createElement('path', { d: 'M6 9l6 6 6-6' })
+ );
+}
+
+export default function VehicleBrandModelPicker(props) {
+ var value = props.value || [];
+ var onChange = props.onChange;
+ var openState = useState(false);
+ var activeBrandState = useState(VEHICLE_BINDING_ALL_BRAND);
+ var queryState = useState('');
+ var open = openState[0];
+ var setOpen = openState[1];
+ var activeBrand = activeBrandState[0];
+ var setActiveBrand = activeBrandState[1];
+ var query = queryState[0];
+ var setQuery = queryState[1];
+ var searchText = query.trim();
+ var isSearchMode = searchText.length > 0;
+ var isAllMode = isAllVehicleBindings(value);
+ var isBrowsingAllTab = !isSearchMode && activeBrand === VEHICLE_BINDING_ALL_BRAND;
+ var resolvedBrand = (activeBrand && activeBrand !== VEHICLE_BINDING_ALL_BRAND)
+ ? activeBrand
+ : (value.length > 0
+ ? value[0].brand
+ : LEASE_VEHICLE_BRAND_MODEL_CATALOG[0].brand);
+ var searchLower = searchText.toLowerCase();
+
+ var displayModels = isSearchMode
+ ? LEASE_VEHICLE_BRAND_MODEL_CATALOG.reduce(function (list, item) {
+ return list.concat(item.models.map(function (model) {
+ return { brand: item.brand, model: model };
+ }));
+ }, []).filter(function (item) {
+ var label = formatBrandModelPair(item.brand, item.model);
+ return label.toLowerCase().indexOf(searchLower) >= 0
+ || item.brand.toLowerCase().indexOf(searchLower) >= 0;
+ })
+ : (function () {
+ var group = LEASE_VEHICLE_BRAND_MODEL_CATALOG.find(function (node) {
+ return node.brand === resolvedBrand;
+ });
+ if (!group) return [];
+ return group.models.map(function (model) {
+ return { brand: group.brand, model: model };
+ });
+ })();
+
+ function handleOpenChange(nextOpen) {
+ setOpen(nextOpen);
+ if (nextOpen) {
+ if (isAllVehicleBindings(value)) {
+ setActiveBrand(VEHICLE_BINDING_ALL_BRAND);
+ } else if (value.length > 0) {
+ setActiveBrand(value[0].brand);
+ } else {
+ setActiveBrand(VEHICLE_BINDING_ALL_BRAND);
+ }
+ } else {
+ setQuery('');
+ }
+ }
+
+ function selectAllBrands() {
+ onChange([]);
+ setActiveBrand(VEHICLE_BINDING_ALL_BRAND);
+ setQuery('');
+ }
+
+ function isSelected(brand, model) {
+ var key = vehicleBindingKey({ brand: brand, model: model });
+ return value.some(function (item) {
+ return vehicleBindingKey(item) === key;
+ });
+ }
+
+ function togglePair(brand, model) {
+ var key = vehicleBindingKey({ brand: brand, model: model });
+ if (isSelected(brand, model)) {
+ var next = value.filter(function (item) {
+ return vehicleBindingKey(item) !== key;
+ });
+ onChange(next);
+ if (!next.length) {
+ setActiveBrand(VEHICLE_BINDING_ALL_BRAND);
+ }
+ return;
+ }
+ onChange(isAllMode ? [{ brand: brand, model: model }] : value.concat([{ brand: brand, model: model }]));
+ setActiveBrand(brand);
+ }
+
+ function removeBinding(binding) {
+ var key = vehicleBindingKey(binding);
+ var next = value.filter(function (item) {
+ return vehicleBindingKey(item) !== key;
+ });
+ onChange(next);
+ if (!next.length) {
+ setActiveBrand(VEHICLE_BINDING_ALL_BRAND);
+ }
+ }
+
+ function clearAll(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ selectAllBrands();
+ }
+
+ var panelContent = React.createElement('div', {
+ className: 'vm-operate-city-edit-body vm-operate-city-picker-panel ct-vehicle-brand-model-panel',
+ },
+ React.createElement('label', { className: 'vm-operate-city-edit-search' },
+ React.createElement('span', { className: 'vm-operate-city-edit-search-icon', 'aria-hidden': true },
+ React.createElement(SearchIcon)
+ ),
+ React.createElement('input', {
+ type: 'text',
+ className: 'vm-operate-city-edit-search-input',
+ value: query,
+ placeholder: '输入品牌或型号名称快速筛选',
+ 'aria-label': '筛选品牌型号',
+ autoComplete: 'off',
+ onChange: function (e) { setQuery(e.target.value); },
+ }),
+ query
+ ? React.createElement('button', {
+ type: 'button',
+ className: 'vm-operate-city-edit-search-clear',
+ onClick: function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ setQuery('');
+ },
+ 'aria-label': '清空搜索',
+ }, React.createElement(CloseIcon))
+ : null
+ ),
+ value.length > 0
+ ? React.createElement('div', { className: 'vm-operate-city-edit-selected', 'aria-live': 'polite' },
+ React.createElement('span', { className: 'vm-operate-city-edit-selected-label' }, '已选'),
+ React.createElement('div', { className: 'ct-vehicle-brand-model-panel__selected-list' },
+ value.map(function (item) {
+ var label = formatBrandModelPair(item.brand, item.model);
+ return React.createElement('span', {
+ key: vehicleBindingKey(item),
+ className: 'ct-vehicle-brand-model-panel__selected-tag',
+ },
+ label,
+ React.createElement('button', {
+ type: 'button',
+ 'aria-label': '移除' + label,
+ onMouseDown: function (e) { e.preventDefault(); },
+ onClick: function () { removeBinding(item); },
+ }, React.createElement(CloseIcon))
+ );
+ })
+ )
+ )
+ : isAllMode
+ ? React.createElement('div', { className: 'vm-operate-city-edit-selected', 'aria-live': 'polite' },
+ React.createElement('span', { className: 'vm-operate-city-edit-selected-label' }, '已选'),
+ React.createElement('div', { className: 'ct-vehicle-brand-model-panel__selected-list' },
+ React.createElement('span', {
+ className: 'ct-vehicle-brand-model-panel__selected-tag ct-vehicle-brand-model-panel__selected-tag--all',
+ }, '全部')
+ )
+ )
+ : null,
+ React.createElement('div', {
+ className: 'vm-operate-city-edit-main' + (isSearchMode ? ' is-search' : ''),
+ },
+ !isSearchMode
+ ? React.createElement('div', { className: 'vm-operate-city-edit-step vm-operate-city-edit-step--province' },
+ React.createElement('span', { className: 'vm-operate-city-step-label' }, '品牌'),
+ React.createElement('div', {
+ className: 'vm-operate-city-edit-chips',
+ role: 'tablist',
+ 'aria-label': '品牌列表',
+ },
+ React.createElement('button', {
+ key: VEHICLE_BINDING_ALL_BRAND,
+ type: 'button',
+ role: 'tab',
+ 'aria-selected': isBrowsingAllTab,
+ className: 'vm-operate-city-chip ct-vehicle-brand-model-picker__all-brand'
+ + (isBrowsingAllTab ? ' active' : ''),
+ onClick: selectAllBrands,
+ }, '全部'),
+ LEASE_VEHICLE_BRAND_MODEL_CATALOG.map(function (item) {
+ var active = activeBrand === item.brand;
+ return React.createElement('button', {
+ key: item.brand,
+ type: 'button',
+ role: 'tab',
+ 'aria-selected': active,
+ className: 'vm-operate-city-chip' + (active ? ' active' : ''),
+ onClick: function () {
+ setActiveBrand(item.brand);
+ setQuery('');
+ },
+ }, item.brand);
+ })
+ )
+ )
+ : null,
+ React.createElement('div', { className: 'vm-operate-city-edit-step vm-operate-city-edit-step--city' },
+ React.createElement('span', { className: 'vm-operate-city-step-label' },
+ isSearchMode ? ('搜索结果(' + displayModels.length + ')') : '型号',
+ ),
+ React.createElement('div', {
+ className: 'vm-operate-city-edit-chips',
+ role: 'listbox',
+ 'aria-label': '型号列表',
+ 'aria-multiselectable': true,
+ },
+ isBrowsingAllTab && isAllMode
+ ? React.createElement('p', { className: 'vm-operate-city-empty ct-vehicle-brand-model-picker__all-hint' },
+ '当前章节适用于全部品牌、全部型号;如需限定车型,请先选择左侧品牌'
+ )
+ : displayModels.length === 0
+ ? React.createElement('p', { className: 'vm-operate-city-empty' },
+ isSearchMode ? '未找到匹配型号,请换个关键词' : '暂无型号'
+ )
+ : displayModels.map(function (item) {
+ var checked = isSelected(item.brand, item.model);
+ return React.createElement('button', {
+ key: vehicleBindingKey(item),
+ type: 'button',
+ role: 'option',
+ 'aria-selected': checked,
+ className: 'vm-operate-city-chip' + (checked ? ' active' : ''),
+ onClick: function () { togglePair(item.brand, item.model); },
+ }, item.model);
+ })
+ )
+ )
+ )
+ );
+
+ return React.createElement('div', { className: 'vm-operate-city-field ct-vehicle-brand-model-picker' },
+ React.createElement(Popover, {
+ trigger: 'click',
+ placement: 'bottomLeft',
+ open: open,
+ onOpenChange: handleOpenChange,
+ overlayClassName: 'ct-vehicle-brand-model-popover',
+ getPopupContainer: function () { return document.body; },
+ content: panelContent,
+ },
+ React.createElement('div', {
+ className: 'vm-filter-picker-control ct-vehicle-brand-model-picker__trigger' + (open ? ' open' : ''),
+ role: 'combobox',
+ 'aria-label': '品牌型号',
+ 'aria-expanded': open,
+ tabIndex: 0,
+ onKeyDown: function (e) {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setOpen(true);
+ }
+ },
+ },
+ isAllMode
+ ? React.createElement('span', {
+ className: 'ct-vehicle-brand-model-picker__summary-tag ct-vehicle-brand-model-picker__summary-tag--all',
+ title: '全部品牌、全部型号',
+ }, '全部')
+ : value.length > 0
+ ? React.createElement('div', { className: 'ct-vehicle-brand-model-picker__summary-tags' },
+ value.map(function (item) {
+ var label = formatBrandModelPair(item.brand, item.model);
+ return React.createElement('span', {
+ key: vehicleBindingKey(item),
+ className: 'ct-vehicle-brand-model-picker__summary-tag',
+ title: label,
+ },
+ React.createElement('span', { className: 'ct-vehicle-brand-model-picker__summary-tag-brand' }, item.brand),
+ React.createElement('span', { className: 'ct-vehicle-brand-model-picker__summary-tag-sep', 'aria-hidden': true }, '·'),
+ React.createElement('span', { className: 'ct-vehicle-brand-model-picker__summary-tag-model' }, item.model)
+ );
+ })
+ )
+ : React.createElement('span', {
+ className: 'vm-filter-picker-input ct-vehicle-brand-model-picker__summary is-placeholder',
+ }, '请选择品牌 / 型号'),
+ !isAllMode && value.length > 0
+ ? React.createElement('button', {
+ type: 'button',
+ className: 'vm-filter-picker-clear',
+ onClick: clearAll,
+ 'aria-label': '清空品牌型号',
+ tabIndex: -1,
+ }, React.createElement(CloseIcon))
+ : null,
+ React.createElement('span', { className: 'vm-filter-picker-chevron' },
+ React.createElement(ChevronDownIcon)
+ )
+ )
+ )
+ );
+}
diff --git a/src/prototypes/contract-template-management/annotation-source.json b/src/prototypes/contract-template-management/annotation-source.json
index 2c4a210..3be4d06 100644
--- a/src/prototypes/contract-template-management/annotation-source.json
+++ b/src/prototypes/contract-template-management/annotation-source.json
@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "contract-template-management",
"pageId": "list",
- "updatedAt": 1782493420078,
+ "updatedAt": 1782832088411,
"nodes": [
{
"id": "ct-list-filter",
@@ -20,7 +20,7 @@
"path": []
},
"aiPrompt": "列表页筛选区字段与查询逻辑。",
- "annotationText": "支持按合同名称、状态、创建人、适用品牌筛选。",
+ "annotationText": "支持按合同名称(可搜索下拉)、状态、创建人、启用状态筛选。",
"hasMarkdown": true,
"color": "#2563eb",
"images": [],
@@ -100,32 +100,32 @@
"path": []
},
"aiPrompt": "9 个合同章节切换。",
- "annotationText": "导入 Word 后按附件标题自动拆分;切换 Tab 同步编辑区与预览区。",
+ "annotationText": "横向滚动查看全部 9 章;可通过章节列表搜索并跳转。",
"hasMarkdown": true,
"color": "#2563eb",
"images": [],
"createdAt": 1740508800000,
- "updatedAt": 1740508800000
+ "updatedAt": 1782820000000
},
{
- "id": "ct-main-clause-tip",
- "index": 6,
- "title": "条件条款说明",
+ "id": "ct-contract-type",
+ "index": 6.5,
+ "title": "合同类型",
"pageId": "editor",
"locator": {
"selectors": [
- "[data-annotation-id=\"ct-main-clause-tip\"]"
+ "[data-annotation-id=\"ct-contract-type\"]"
],
- "fingerprint": "ct-main-clause-tip",
+ "fingerprint": "ct-contract-type",
"path": []
},
- "aiPrompt": "合同主体章节条件条款使用说明。",
- "annotationText": "选中段落后点工具栏「条件」,绑定适用车型;预览区可模拟车型验证显隐。",
- "hasMarkdown": false,
- "color": "#d97706",
+ "aiPrompt": "合同主体章节下的合同类型配置。",
+ "annotationText": "仅在「合同主体」章节 Tab 下展示;合同类型为必填,用户自行填写后同步至列表页。",
+ "hasMarkdown": true,
+ "color": "#7c3aed",
"images": [],
"createdAt": 1740508800000,
- "updatedAt": 1740508800000
+ "updatedAt": 1782820000000
},
{
"id": "ct-vehicle-binding",
@@ -140,7 +140,7 @@
"path": []
},
"aiPrompt": "附件章节适用车型配置。",
- "annotationText": "非合同主体章节可绑定品牌/型号;空表示全部车型适用。",
+ "annotationText": "点击「请选择品牌 / 型号」弹出级联多选卡片;左侧品牌、右侧型号多选。未选表示全部车型适用。",
"hasMarkdown": true,
"color": "#059669",
"images": [],
@@ -160,16 +160,136 @@
"path": []
},
"aiPrompt": "Word 版式分页编辑区。",
- "annotationText": "支持导入 Word、模板占位符、条件条款与锁定区编辑。",
+ "annotationText": "支持导入 Word、模板占位符、红线/条件条款与锁定区编辑。",
"hasMarkdown": true,
"color": "#7c3aed",
"images": [],
"createdAt": 1740508800000,
"updatedAt": 1740508800000
},
+ {
+ "id": "ct-rte-risk-redline",
+ "index": 10,
+ "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
+ },
+ {
+ "id": "ct-rte-unrisk-redline",
+ "index": 11,
+ "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
+ },
+ {
+ "id": "ct-rte-vehicle-clause",
+ "index": 12,
+ "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
+ },
+ {
+ "id": "ct-rte-unvehicle-clause",
+ "index": 13,
+ "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
+ },
+ {
+ "id": "ct-rte-inline-lock",
+ "index": 14,
+ "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
+ },
+ {
+ "id": "ct-rte-uninline-lock",
+ "index": 15,
+ "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
+ },
{
"id": "ct-preview-panel",
- "index": 9,
+ "index": 16,
"title": "实时预览",
"pageId": "editor",
"locator": {
@@ -180,28 +300,224 @@
"path": []
},
"aiPrompt": "A4 分页预览与模拟车型。",
- "annotationText": "支持连续预览/单页预览;合同主体可切换模拟车型查看条件条款命中。",
+ "annotationText": "支持连续预览/单页预览;各章节可切换模拟车型查看条件条款命中。",
"hasMarkdown": true,
"color": "#0ea5e9",
"images": [],
"createdAt": 1740508800000,
"updatedAt": 1740508800000
}
- ]
+ ],
+ "annotationsByElementId": {
+ "ct-list-filter": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 可搜索下拉 | 选项来自现有模板名称;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅已发布) |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认",
+ "ct-list-table": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同类型 | `contractType` | 展示用户自定义的合同类型文案 |\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展示 9 个章节摘要:章节名、适用车型、风控条款数、条件条款数。"
+ }
},
"markdownMap": {
- "ct-list-filter": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 文本输入 | 支持回车触发查询 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 适用品牌 | 下拉 | 选项来自车型主数据品牌列表 |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认",
- "ct-list-table": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同名称 | `fileName` | 超长省略;标准模板显示金色标签「标准合同」 |\n| 版本号 | `versionNo` | 草稿显示「—」 |\n| 状态 | `status` | 已发布 / 草稿 Tag |\n| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |\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| 条件条款数 | 仅合同主体统计条件条款数;其他章节显示 `0` |\n| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。",
- "ct-editor-topbar": "### 8.2 顶栏\n\n| 按钮 | 行为 |\n|---|---|\n| ← 返回列表 | 回到列表页,不自动保存 |\n| 保存 | 见 §6.2 |\n| 保存并发布 | 见 §6.2 |\n\n### 6.2 保存与发布\n\n| 操作 | 新增模式 | 编辑草稿 | 编辑已发布 |\n|---|---|---|---|\n| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**降为草稿**(清空版本号) |\n| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。",
- "ct-section-tabs": "### 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. 安全责任协议",
+ "ct-list-filter": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同类型 | 可搜索下拉 | 选项来自现有模板 `contractType` 去重;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认",
+ "ct-list-table": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同类型 | `contractType` | 展示用户自定义的合同类型文案 |\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---",
+ "ct-editor-topbar": "### 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**离开编辑**:有未保存修改时返回列表需二次确认。",
+ "ct-contract-type": "### 8.4 合同类型(仅合同主体 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 合同类型输入框 | **必填**;用户自行填写,如「正式合同」「现代18吨试用合同」 |\n| 展示位置 | 章节 Tab 下方配置卡片,与附件章节的「适用车型」卡片交互一致 |\n| 用途 | 同步至列表页「合同类型」列;租赁合同创建时按类型匹配标准模板 |\n\n**规则**:\n\n- 仅当选中 **合同主体** 章节 Tab 时显示本卡片。\n- 未填写时无法保存或发布,并提示「请输入合同类型」。\n- 切换至租赁订单等附件章节时,显示「适用车型」卡片,本卡片隐藏。\n- 保存/发布时写入模板 `contractType` 字段。",
+ "ct-section-tabs": "### 8.3 章节 Tab\n\n- 共 9 个章节;**横向滚动 Tab 条**展示全部章节,右侧提供「章节列表」按钮打开可搜索的章节面板(便于长标题章节快速定位)。\n- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。\n- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**。\n- **其他 Tab**:仅展示该章节切片内容。\n\n章节顺序:\n\n1. 合同主体 \n2. 租赁订单 \n3. 车辆产生的费用 \n4. 安全责任告知书 \n5. 重型普通货运车辆租赁动态监管责任告知与确认书 \n6. 安全责任承诺书 \n7. 车辆使用规范和驾驶员要求 \n8. 授权委托书 \n9. 安全责任协议",
"ct-vehicle-binding": "### 8.5 适用车型绑定(仅附件章节 Tab)\n\n| 元素 | 说明 |\n|---|---|\n| 品牌下拉 | 来自车型主数据 |\n| 型号下拉 | 随品牌联动 |\n| 添加车型 | 追加到当前章节绑定列表 |\n| 已绑定 Tag | 可删除单项 |\n| 空状态文案 | 「当前未绑定车型,表示全部车型适用」 |\n\n**规则**:\n\n- 绑定保存在 `sectionVehicleBindings[当前章节 key]`。\n- 切换章节 Tab 时加载对应章节的绑定到编辑表单。\n- 保存时将各章节绑定一并写入文档记录。",
- "ct-editor-panel": "### 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\n1. 用户选中段落或文字;\n2. 点击工具栏「条件」;\n3. 为选中内容绑定一个或多个「品牌 · 型号」;\n4. 生成 `.ct-vehicle-clause` 包裹结构,并写入 `data-vehicle-bindings`;\n5. 创建合同时:订单车辆 **任意一辆** 命中绑定车型 → 段落纳入正文;否则隐藏。",
- "ct-preview-panel": "### 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- 甲方相关变量按预览所选出租方公司填充",
- "ct-main-clause-tip": "### 8.4 条件条款说明(仅合同主体 Tab)\n\n展示说明卡片,文案要点:\n\n> 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。\n\n#### 8.6.3 条件条款交互(合同主体)\n\n1. 用户选中段落或文字;\n2. 点击工具栏「条件」;\n3. 为选中内容绑定一个或多个「品牌 · 型号」;\n4. 生成 `.ct-vehicle-clause` 包裹结构,并写入 `data-vehicle-bindings`;\n5. 创建合同时:订单车辆 **任意一辆** 命中绑定车型 → 段落纳入正文;否则隐藏。"
+ "ct-editor-panel": "### 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. 创建合同时:订单车辆 **任意一辆** 命中绑定车型 → 段落纳入正文;否则隐藏。",
+ "ct-preview-panel": "### 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- 甲方相关变量按预览所选出租方公司填充"
},
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "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": "ct-doc-root",
@@ -212,14 +528,7 @@
"type": "markdown",
"id": "ct-doc-overview",
"title": "模块总览",
- "markdown": "## 模块定位\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| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |\n| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |\n| 支撑业务差异化 | 附件章节按车型绑定;合同主体支持条件条款 |\n| 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本 |\n| 标准模板唯一 | 同一合同分类同时仅一套标准模板生效 |"
- },
- {
- "type": "link",
- "id": "ct-link-vehicle",
- "title": "车辆管理原型",
- "href": "/prototypes/vehicle-management",
- "target": "self"
+ "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`"
},
{
"type": "folder",
@@ -231,31 +540,32 @@
"type": "markdown",
"id": "ct-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. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。\n\n---\n\n## 3. 名词解释\n\n| 名词 | 定义 |\n|---|---|\n| Word 文档 / 模板记录 | 列表主表一行,对应一份完整合同文件 |\n| 章节 | 合同内的逻辑切片,共 9 个(1 个主体 + 8 个附件) |\n| 草稿 | `status = draft`,无版本号 |\n| 已发布 | `status = published`,有版本号 `vX.XX` |\n| 标准模板 | `isDefault = true` 且已发布的模板;同分类唯一 |\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 ├── 条件条款说明(合同主体 Tab 时显示)\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 isDefault: boolean; // 是否标准模板\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 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| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。\n\n### 6.4 删除规则\n\n- 仅 **非标准模板** 可删除。\n- 删除前二次确认;删除后不可恢复。\n\n### 6.5 列表排序\n\n1. 标准模板置顶;\n2. 其余按 `updateTime` 降序。\n\n### 6.6 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 模糊匹配 `fileName`、`versionName`、`versionNo`、`creator`、`updater` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 |\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| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |\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| 条件条款数 | 仅合同主体统计条件条款数;其他章节显示 `0` |\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 条件条款说明(仅合同主体 Tab)\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\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` 必须在每次保存时更新。\n\n---\n\n## 14. 接口需求(建议)\n\n### 14.1 列表查询\n\n```\nGET /api/contract-templates?category=lease&keyword=&status=&creator=&vehicleBrand=&page=1&pageSize=10\n```\n\n响应需包含主表字段;章节摘要可列表内嵌或展开时按需加载。\n\n### 14.2 详情\n\n```\nGET /api/contract-templates/{id}\n```\n\n返回完整 `sectionContents`、`sectionVehicleBindings`。\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}/set-default\n```\n\n服务端事务:同 category 旧标准 `isDefault=false`,新标准 `isDefault=true`。\n\n### 14.5 删除\n\n```\nDELETE /api/contract-templates/{id}\n```\n\n拒绝删除 `isDefault=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- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表\n- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态\n- [ ] 标准模板金色标签置顶,且不可删除\n- [ ] 切换标准模板时旧标准自动禁用\n\n### 16.2 编辑页\n\n- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多\n- [ ] 导入 Word 后 9 章节正确拆分\n- [ ] 附件章节可绑定车型;合同主体可设条件条款\n- [ ] 预览支持连续/单页切换;合同主体可模拟车型验证条件条款\n- [ ] 保存草稿无版本号;发布生成版本号\n- [ ] 编辑已发布并发布时产生新版本,旧版本保留\n\n### 16.3 数据一致性\n\n- [ ] 列表子表「风控条款数」与编辑区统计一致\n- [ ] 列表子表「条件条款数」仅合同主体可大于 0\n- [ ] 筛选「适用品牌」与子表车型绑定逻辑一致\n\n### 16.4 标注与文档(原型)\n\n- [ ] 原型已接入 `@axhub/annotation`,关键区域有 `data-annotation-id`\n- [ ] 目录路由可切换列表页 / 新增页\n\n---\n\n## 17. 原型实现映射(供研发对照)\n\n| 能力 | 原型文件 / 符号 |\n|---|---|\n| 主逻辑 | `src/prototypes/contract-template-management/ContractTemplate.jsx` |\n| 入口与标注 | `src/prototypes/contract-template-management/index.tsx` |\n| 标注数据 | `src/prototypes/contract-template-management/annotation-source.json` |\n| 占位符与出租方 | `src/prototypes/contract-template-management/contract-template-vars.js` |\n| 样式 | `src/prototypes/contract-template-management/styles/` |\n\n---\n\n## 18. 附录:标准模板确认弹窗(原文案)\n\n**标题**:确认设为标准模板\n\n**正文**:\n\n> 同一分类同时只能有一套标准模板生效。确认后,客服草拟新合同时将默认使用新标准模板。\n>\n> (若存在旧标准)将先禁用当前标准模板「{旧标准名称}」,再设置「{新标准名称}」为新的标准模板。\n\n**按钮**:取消 / 确认设置\n\n---\n\n## 19. 修订记录\n\n| 版本 | 日期 | 作者 | 说明 |\n|---|---|---|---|\n| v1.0 | 2026-06-25 | 产品 | 基于原型与需求讨论首版;明确 Word 文档级模型、嵌套列表、标准模板唯一、移除适用型号筛选与备注列 |"
+ "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 contractType: string; // 用户自定义合同类型,如「正式合同」「试用合同」\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| 合同类型 | 下拉选择(支持输入模糊搜索候选项);精确匹配 `contractType` |\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| 合同类型 | 可搜索下拉 | 选项来自现有模板 `contractType` 去重;支持输入模糊搜索 |\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| 合同类型 | `contractType` | 展示用户自定义的合同类型文案 |\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## 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 个章节;**横向滚动 Tab 条**展示全部章节,右侧提供「章节列表」按钮打开可搜索的章节面板(便于长标题章节快速定位)。\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 仅在编辑页展示;横向滚动 + 章节列表面板\n- [ ] 导入 Word 后 9 章节正确拆分\n- [ ] 附件章节可绑定车型;各章节均可设条件条款\n- [ ] 预览支持连续/单页切换;各章节可模拟车型验证条件条款\n- [ ] 保存草稿无版本号;发布生成版本号\n- [ ] 编辑已发布(停用后)并发布时版本号递增,旧版写入版本日志\n- [ ] 编辑已发布(停用后)点「保存」不改变发布态与版本号\n- [ ] 有未保存修改时返回列表需确认\n\n### 16.3 跨模块与数据一致性\n\n- [ ] 模板管理停用后,租赁合同创建页不可再选用该模板\n- [ ] 租赁创建预览读取 store 中模板 `sectionContents`(与模板管理编辑一致)\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| 章节 HTML 合并 | `src/prototypes/contract-template-management/contract-template-section-html.js` |\n| 冷启动种子 | `src/prototypes/contract-template-management/contract-template-seed.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> **原型限制**:模板数据经 `localStorage`(兼写 `sessionStorage`)持久化;冷启动由 `contract-template-seed.js` 注入种子,无需先打开模板管理页即可在租赁创建选用已启用模板。\n\n---\n\n## 18. 附录:停用确认弹窗(原文案)\n\n**标题**:确认停用\n\n**正文**:\n\n> 停用后,该模板将不可在租赁合同创建时选用,是否确定?\n\n**按钮**:取消 / 确定停用\n\n---\n\n## 19. 修订记录\n\n| 版本 | 日期 | 作者 | 说明 |\n|---|---|---|---|\n| v1.2 | 2026-06-29 | 产品 | 租赁预览贯通 store;冷启动种子;列表按合同类型筛选;移除 isDefault;localStorage 持久化;PRD 与实现对齐 |\n| v1.1 | 2026-06-29 | 产品 | 移除标准模板模型,改为启用/停用 + 版本日志;列表筛选与操作同步;各章节支持条件条款 |\n| v1.0 | 2026-06-25 | 产品 | 首版:Word 文档级模型、嵌套列表、章节拆分与条件条款 |\n",
+ "markdownPath": "src/resources/contract-template-management/PRD.md"
},
{
"type": "markdown",
"id": "ct-doc-prd-overview",
"title": "背景 · 数据模型 · 业务规则",
- "markdown": "# 合同模板管理 · 全局说明\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. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。\n\n---\n\n## 3. 名词解释\n\n| 名词 | 定义 |\n|---|---|\n| Word 文档 / 模板记录 | 列表主表一行,对应一份完整合同文件 |\n| 章节 | 合同内的逻辑切片,共 9 个(1 个主体 + 8 个附件) |\n| 草稿 | `status = draft`,无版本号 |\n| 已发布 | `status = published`,有版本号 `vX.XX` |\n| 标准模板 | `isDefault = true` 且已发布的模板;同分类唯一 |\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 ├── 条件条款说明(合同主体 Tab 时显示)\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 isDefault: boolean; // 是否标准模板\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 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| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。\n\n### 6.4 删除规则\n\n- 仅 **非标准模板** 可删除。\n- 删除前二次确认;删除后不可恢复。\n\n### 6.5 列表排序\n\n1. 标准模板置顶;\n2. 其余按 `updateTime` 降序。\n\n### 6.6 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 模糊匹配 `fileName`、`versionName`、`versionNo`、`creator`、`updater` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 |\n\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 isDefault: boolean; // 是否标准模板\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 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| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |\n\n**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。\n\n### 6.4 删除规则\n\n- 仅 **非标准模板** 可删除。\n- 删除前二次确认;删除后不可恢复。\n\n### 6.5 列表排序\n\n1. 标准模板置顶;\n2. 其余按 `updateTime` 降序。\n\n### 6.6 筛选匹配规则\n\n| 字段 | 匹配逻辑 |\n|---|---|\n| 合同名称 | 模糊匹配 `fileName`、`versionName`、`versionNo`、`creator`、`updater` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 |\n\n> **已移除**:筛选项「适用型号」、主表列「备注」(产品确认不再展示)。\n\n---"
+ "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 contractType: string; // 用户自定义合同类型,如「正式合同」「试用合同」\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| 合同类型 | 下拉选择(支持输入模糊搜索候选项);精确匹配 `contractType` |\n| 状态 | 精确匹配 `draft` / `published` |\n| 创建人 | 精确匹配 `creator` |\n| 启用状态 | `enabled`:仅已发布且启用;`disabled`:仅已发布且停用 |\n\n> **已移除**:筛选项「适用品牌/适用车型」、主表列「备注」、「设为标准模板」能力。\n\n---"
},
{
"type": "markdown",
"id": "ct-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| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |\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| 条件条款数 | 仅合同主体统计条件条款数;其他章节显示 `0` |\n| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |\n\n---"
+ "markdown": "# 合同模板管理 · 列表页 PRD\n\n## 7. 列表页需求\n\n### 7.1 页面结构\n\n筛选区 + 工具栏 + 嵌套表格。**列表页不展示章节 Tab。**\n\n### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同类型 | 可搜索下拉 | 选项来自现有模板 `contractType` 去重;支持输入模糊搜索 |\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| 合同类型 | `contractType` | 展示用户自定义的合同类型文案 |\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---"
},
{
"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 条件条款说明(仅合同主体 Tab)\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\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---\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---\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---"
+ "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 个章节;**横向滚动 Tab 条**展示全部章节,右侧提供「章节列表」按钮打开可搜索的章节面板(便于长标题章节快速定位)。\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---"
},
{
"type": "markdown",
"id": "ct-doc-prd-acceptance",
"title": "验收标准",
- "markdown": "## 16. 验收标准\n\n### 16.1 列表页\n\n- [ ] 筛选项包含:合同名称、状态、创建人、适用品牌;**不含**适用型号、备注列\n- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表\n- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态\n- [ ] 标准模板金色标签置顶,且不可删除\n- [ ] 切换标准模板时旧标准自动禁用\n\n### 16.2 编辑页\n\n- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多\n- [ ] 导入 Word 后 9 章节正确拆分\n- [ ] 附件章节可绑定车型;合同主体可设条件条款\n- [ ] 预览支持连续/单页切换;合同主体可模拟车型验证条件条款\n- [ ] 保存草稿无版本号;发布生成版本号\n- [ ] 编辑已发布并发布时产生新版本,旧版本保留\n\n### 16.3 数据一致性\n\n- [ ] 列表子表「风控条款数」与编辑区统计一致\n- [ ] 列表子表「条件条款数」仅合同主体可大于 0\n- [ ] 筛选「适用品牌」与子表车型绑定逻辑一致\n\n### 16.4 标注与文档(原型)\n\n- [ ] 原型已接入 `@axhub/annotation`,关键区域有 `data-annotation-id`\n- [ ] 目录路由可切换列表页 / 新增页\n\n---"
+ "markdown": "## 16. 验收标准\n\n### 16.1 列表页\n\n- [ ] 筛选项包含:合同类型(可搜索下拉)、状态、创建人、启用状态\n- [ ] 主表含合同类型、启用状态列;操作含启用/停用、版本日志、编辑、删除\n- [ ] 启用中模板编辑/删除按钮禁用;停用需二次确认\n- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表\n- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数\n\n### 16.2 编辑页\n\n- [ ] 章节 Tab 仅在编辑页展示;横向滚动 + 章节列表面板\n- [ ] 导入 Word 后 9 章节正确拆分\n- [ ] 附件章节可绑定车型;各章节均可设条件条款\n- [ ] 预览支持连续/单页切换;各章节可模拟车型验证条件条款\n- [ ] 保存草稿无版本号;发布生成版本号\n- [ ] 编辑已发布(停用后)并发布时版本号递增,旧版写入版本日志\n- [ ] 编辑已发布(停用后)点「保存」不改变发布态与版本号\n- [ ] 有未保存修改时返回列表需确认\n\n### 16.3 跨模块与数据一致性\n\n- [ ] 模板管理停用后,租赁合同创建页不可再选用该模板\n- [ ] 租赁创建预览读取 store 中模板 `sectionContents`(与模板管理编辑一致)\n- [ ] 列表子表「风控条款数」与编辑区统计一致\n- [ ] 列表子表「条件条款数」与各章节实际标记数一致\n\n### 16.4 标注与文档(原型)\n\n- [ ] 原型已接入 `@axhub/annotation`,关键区域有 `data-annotation-id`\n- [ ] 目录路由可切换列表页 / 新增页\n- [ ] `PRD.md` 与 `annotation-source.json` 与当前实现一致\n\n---"
}
]
},
@@ -278,13 +588,13 @@
"type": "markdown",
"id": "ct-doc-node-filter",
"title": "筛选条件",
- "markdown": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同名称 | 文本输入 | 支持回车触发查询 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 适用品牌 | 下拉 | 选项来自车型主数据品牌列表 |\n\n**按钮**:\n\n- **查询**:应用筛选条件\n- **重置**:清空筛选恢复默认"
+ "markdown": "### 7.2 筛选区\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同类型 | 可搜索下拉 | 选项来自现有模板 `contractType` 去重;支持输入模糊搜索 |\n| 状态 | 下拉 | 全部 / 已发布 / 草稿 |\n| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |\n| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |\n\n**按钮**:\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| 创建时间 | `createTime` | — |\n| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |\n| 最后更新时间 | `updateTime` | — |\n| 更新人 | `updater` | 同创建人展示规则 |\n| 操作 | — | 见 7.5 |\n\n**分页**:每页 10 条,展示总条数。\n\n### 7.5 主表操作\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |\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| 条件条款数 | 仅合同主体统计条件条款数;其他章节显示 `0` |\n| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |\n\n### 6.3 标准模板规则\n\n1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。\n2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。\n3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。\n4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。\n5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。"
+ "markdown": "### 7.4 主表字段\n\n| 列名 | 字段 | 展示规则 |\n|---|---|---|\n| (展开列) | — | 宽 48px |\n| 合同类型 | `contractType` | 展示用户自定义的合同类型文案 |\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---"
}
]
},
@@ -307,13 +617,13 @@
"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. 安全责任协议"
+ "markdown": "### 8.3 章节 Tab\n\n- 共 9 个章节;**横向滚动 Tab 条**展示全部章节,右侧提供「章节列表」按钮打开可搜索的章节面板(便于长标题章节快速定位)。\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": "## 编辑与预览\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\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- 甲方相关变量按预览所选出租方公司填充"
+ "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- 甲方相关变量按预览所选出租方公司填充"
}
]
},
@@ -321,7 +631,7 @@
"type": "markdown",
"id": "ct-doc-annotation-hint",
"title": "标注查看提示",
- "markdown": "## 标注查看提示\n\n右侧标注工具栏可浏览目录、切换主题与颜色筛选。列表页与编辑页标注按当前页面自动显隐。\n\n**PRD 同步**:目录「PRD 全文」已与 `src/resources/contract-template-management/PRD.md` 对齐;修改 PRD 后需重新同步至 `annotation-source.json`。"
+ "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`。"
}
]
}
diff --git a/src/prototypes/contract-template-management/contract-template-catalog.js b/src/prototypes/contract-template-management/contract-template-catalog.js
new file mode 100644
index 0000000..10954ab
--- /dev/null
+++ b/src/prototypes/contract-template-management/contract-template-catalog.js
@@ -0,0 +1,75 @@
+import { V266_LEASE_DOCUMENT_HTML } from './v266-lease-document.js';
+import { TRIAL_AGREEMENT_DOCUMENT_HTML } from './trial-agreement-document.js';
+import { TRIAL_AGREEMENT_18T_DOCUMENT_HTML } from './trial-agreement-18t-document.js';
+import { mergeMonolithicHtml } from './contract-template-section-html.js';
+import { buildInitialWordTemplates } from './contract-template-seed.js';
+import {
+ ensureContractTemplateStore,
+ getPublishedEnabledLeaseOptions,
+ getPublishedContractTypeOptions,
+ getTemplateDocumentById,
+ getDefaultTemplateIdForContractType,
+ subscribeContractTemplates,
+ mapTemplateToLeaseOption,
+ restoreContractTemplatesFromSession,
+} from './contract-template-store.js';
+
+export {
+ getContractTypeLabel,
+ normalizeContractType,
+ resolveContractType,
+} from './contract-template-types.js';
+
+restoreContractTemplatesFromSession();
+ensureContractTemplateStore(buildInitialWordTemplates);
+
+export function getPublishedContractTemplateOptions(category) {
+ return getPublishedEnabledLeaseOptions(category);
+}
+
+export function getPublishedContractTypeChoices(category) {
+ return getPublishedContractTypeOptions(category);
+}
+
+export function subscribePublishedContractTemplateOptions(listener) {
+ return subscribeContractTemplates(listener);
+}
+
+export function getContractTemplateOption(templateId) {
+ var doc = getTemplateDocumentById(templateId);
+ if (!doc || doc.status !== 'published' || doc.enabled === false) return null;
+ return mapTemplateToLeaseOption(doc);
+}
+
+function getStaticTemplateBaseHtml(templateId) {
+ switch (templateId) {
+ case 'doc-4':
+ return TRIAL_AGREEMENT_DOCUMENT_HTML || '';
+ case 'doc-5':
+ return TRIAL_AGREEMENT_18T_DOCUMENT_HTML || '';
+ case 'doc-1':
+ case 'doc-2':
+ default:
+ return V266_LEASE_DOCUMENT_HTML || '';
+ }
+}
+
+/** 优先读取 store 中模板的 sectionContents,静态 HTML 仅作兜底 */
+export function getContractTemplateBaseHtml(templateId) {
+ var doc = getTemplateDocumentById(templateId);
+ if (doc && doc.sectionContents) {
+ var merged = mergeMonolithicHtml(doc.sectionContents);
+ if (merged && String(merged).trim()) return merged;
+ }
+ return getStaticTemplateBaseHtml(templateId);
+}
+
+export function getDefaultContractTemplateId(contractType, category) {
+ return getDefaultTemplateIdForContractType(contractType, category);
+}
+
+export function getContractTemplatePreviewBadge(templateId) {
+ var option = getContractTemplateOption(templateId);
+ if (!option) return '请先选择合同模板';
+ return [option.contractTypeLabel, option.fileName || option.title].filter(Boolean).join(' · ');
+}
diff --git a/src/prototypes/contract-template-management/contract-template-section-filter.js b/src/prototypes/contract-template-management/contract-template-section-filter.js
new file mode 100644
index 0000000..d0ca8e2
--- /dev/null
+++ b/src/prototypes/contract-template-management/contract-template-section-filter.js
@@ -0,0 +1,95 @@
+/**
+ * 按章节适用车型绑定,从整份合同 HTML 中移除未命中的附件章节。
+ */
+import { sectionBindingsMatchOrder } from './vehicle-binding-utils.js';
+
+var ATTACHMENT_SECTIONS = [
+ { key: 'lease_order', markers: ['1:租赁订单 ', '附件 1:租赁订单', '附件1:租赁订单', '1:租赁订单'] },
+ { key: 'vehicle_cost', markers: ['附件2:车辆产生的费用', '附件2:车辆产生的费用', '附件2:车辆产生的费用'] },
+ { key: 'safety_notice', markers: ['附件3:安全责任告知书', '附件3:安全责任告知书', '附件3:安全责任告知书'] },
+ { key: 'dynamic_supervision', markers: ['附件4:重型普通货运车辆租赁动态监管责任告知与确认书', '附件4:'] },
+ { key: 'safety_commitment', markers: ['附件5:', '安全责任承诺书为严格落实', '附件5:', '附件5:'] },
+ { key: 'vehicle_spec', markers: ['附件6:车辆使用规范和驾驶员要求', '附件6:车辆使用规范和驾驶员要求', '附件6:'] },
+ { key: 'authorization', markers: ['font: 28.0px Times; color: #000000">授权委托书', '授权委托书
致:', '授权委托书
致:'] },
+ { key: 'safety_agreement', markers: ['data-risk-redline="1">安全责任协议
', '安全责任协议
甲方(出租方):', '安全责任协议
'] },
+];
+
+function isValidSectionMarkerMatch(source, index, marker) {
+ if (index < 0) return false;
+ var text = String(source || '');
+ var before = text.slice(Math.max(0, index - 8), index);
+ var after = text.slice(index, index + String(marker || '').length + 12);
+ if (/[((]$/.test(before)) return false;
+ if (/、$/.test(before)) return false;
+ if (/^《/.test(after.slice(String(marker).length))) return false;
+ var nearby = text.slice(Math.max(0, index - 24), index + String(marker).length + 24);
+ if (/详见.{0,20}附件/.test(nearby)) return false;
+ if (/以附件/.test(nearby)) return false;
+ if (String(marker).length <= 10 && !//.test(nearby)) return false;
+ return true;
+}
+
+function findFirstMarker(source, markers) {
+ var text = String(source || '');
+ var best = -1;
+ (markers || []).forEach(function (marker) {
+ var from = 0;
+ while (from < text.length) {
+ var idx = text.indexOf(marker, from);
+ if (idx < 0) break;
+ if (isValidSectionMarkerMatch(text, idx, marker)) {
+ if (best < 0 || idx < best) best = idx;
+ break;
+ }
+ from = idx + 1;
+ }
+ });
+ return best;
+}
+
+function collectSectionRanges(html) {
+ var source = String(html || '');
+ var points = ATTACHMENT_SECTIONS.map(function (section) {
+ return {
+ key: section.key,
+ index: findFirstMarker(source, section.markers),
+ };
+ }).filter(function (point) { return point.index >= 0; });
+
+ points.sort(function (a, b) { return a.index - b.index; });
+
+ return points.map(function (point, idx) {
+ var next = points[idx + 1];
+ return {
+ key: point.key,
+ start: point.index,
+ end: next ? next.index : source.length,
+ };
+ });
+}
+
+export function applyAttachmentSectionVehicleFilter(html, sectionVehicleBindings, vehicleKeys) {
+ var source = String(html || '');
+ if (!source) return source;
+
+ var bindingsMap = sectionVehicleBindings || {};
+ var ranges = collectSectionRanges(source);
+ if (!ranges.length) return source;
+
+ var removeMask = new Array(source.length);
+ ranges.forEach(function (range) {
+ var bindings = bindingsMap[range.key] || [];
+ if (sectionBindingsMatchOrder(bindings, vehicleKeys)) return;
+ for (var i = range.start; i < range.end; i++) {
+ removeMask[i] = true;
+ }
+ });
+
+ if (!removeMask.some(Boolean)) return source;
+
+ var out = '';
+ for (var pos = 0; pos < source.length; pos++) {
+ if (!removeMask[pos]) out += source.charAt(pos);
+ }
+ return out;
+}
diff --git a/src/prototypes/contract-template-management/contract-template-section-html.js b/src/prototypes/contract-template-management/contract-template-section-html.js
new file mode 100644
index 0000000..c6d9dca
--- /dev/null
+++ b/src/prototypes/contract-template-management/contract-template-section-html.js
@@ -0,0 +1,102 @@
+/**
+ * 章节 HTML 合并 / 拆分(合同模板管理 ↔ 租赁合同预览共用)
+ */
+
+export var DOCUMENT_SECTION_TABS = [
+ { key: 'main', label: '合同主体' },
+ { key: 'lease_order', label: '租赁订单', markers: ['1:租赁订单 ', '附件 1:租赁订单', '附件1:租赁订单'] },
+ { key: 'vehicle_cost', label: '车辆产生的费用', markers: ['附件2:车辆产生的费用', '附件2:车辆产生的费用', '附件2:车辆产生的费用'] },
+ { key: 'safety_notice', label: '安全责任告知书', markers: ['附件3:安全责任告知书', '附件3:安全责任告知书', '附件3:安全责任告知书'] },
+ { key: 'dynamic_supervision', label: '重型普通货运车辆租赁动态监管责任告知与确认书', markers: ['附件4:重型普通货运车辆租赁动态监管责任告知与确认书', '附件4:'] },
+ { key: 'safety_commitment', label: '安全责任承诺书', markers: ['附件5:', '安全责任承诺书为严格落实', '附件5:', '附件5:'] },
+ { key: 'vehicle_spec', label: '车辆使用规范和驾驶员要求', markers: ['附件6:车辆使用规范和驾驶员要求', '附件6:车辆使用规范和驾驶员要求', '附件6:'] },
+ { key: 'authorization', label: '授权委托书', markers: ['font: 28.0px Times; color: #000000">授权委托书', '授权委托书
致:', '授权委托书
致:'] },
+ { key: 'safety_agreement', label: '安全责任协议', markers: ['data-risk-redline="1">安全责任协议
', '安全责任协议
甲方(出租方):', '安全责任协议
'] },
+];
+
+export function emptySectionMap() {
+ var map = {};
+ DOCUMENT_SECTION_TABS.forEach(function (tab) { map[tab.key] = ''; });
+ return map;
+}
+
+export function mergeMonolithicHtml(sectionMap) {
+ var map = sectionMap || {};
+ var out = String(map.main || '');
+ DOCUMENT_SECTION_TABS.slice(1).forEach(function (tab) {
+ var chunk = map[tab.key];
+ if (chunk) out += String(chunk);
+ });
+ return out;
+}
+
+function isValidSectionMarkerMatch(source, index, marker) {
+ if (index < 0) return false;
+ var text = String(source || '');
+ var before = text.slice(Math.max(0, index - 8), index);
+ var after = text.slice(index, index + String(marker || '').length + 12);
+ if (/[((]$/.test(before)) return false;
+ if (/、$/.test(before)) return false;
+ if (/^《/.test(after.slice(String(marker).length))) return false;
+ var nearby = text.slice(Math.max(0, index - 24), index + String(marker).length + 24);
+ if (/详见.{0,20}附件/.test(nearby)) return false;
+ if (/以附件/.test(nearby)) return false;
+ if (String(marker).length <= 10 && !//.test(nearby)) return false;
+ return true;
+}
+
+function findFirstMarker(source, markers) {
+ var text = String(source || '');
+ var best = -1;
+ (markers || []).forEach(function (marker) {
+ var from = 0;
+ while (from < text.length) {
+ var idx = text.indexOf(marker, from);
+ if (idx < 0) break;
+ if (isValidSectionMarkerMatch(text, idx, marker)) {
+ if (best < 0 || idx < best) best = idx;
+ break;
+ }
+ from = idx + 1;
+ }
+ });
+ return best;
+}
+
+function findSafeHtmlSplitIndex(source, index) {
+ var text = String(source || '');
+ var candidates = ['', '', ''];
+ var best = index;
+ candidates.forEach(function (tag) {
+ var idx = text.lastIndexOf(tag, index);
+ if (idx >= 0) {
+ var end = idx + tag.length;
+ if (end > best) best = end;
+ }
+ });
+ return best > 0 ? best : index;
+}
+
+/** 将整份 Word HTML 按附件标记拆为章节 map(种子数据 / 导入用) */
+export function splitMonolithicHtml(html) {
+ var source = String(html || '');
+ var result = emptySectionMap();
+ if (!source.trim()) return result;
+ var splitPoints = [];
+ DOCUMENT_SECTION_TABS.slice(1).forEach(function (tab) {
+ var idx = findFirstMarker(source, tab.markers || [tab.label]);
+ if (idx >= 0) splitPoints.push({ key: tab.key, index: idx });
+ });
+ splitPoints.sort(function (a, b) { return a.index - b.index; });
+ var deduped = [];
+ splitPoints.forEach(function (point) {
+ if (!deduped.length || deduped[deduped.length - 1].index !== point.index) deduped.push(point);
+ });
+ result.main = deduped.length ? source.slice(0, findSafeHtmlSplitIndex(source, deduped[0].index)) : source;
+ for (var i = 0; i < deduped.length; i++) {
+ var start = deduped[i].index;
+ var end = i + 1 < deduped.length ? findSafeHtmlSplitIndex(source, deduped[i + 1].index) : source.length;
+ result[deduped[i].key] = source.slice(start, end);
+ }
+ return result;
+}
diff --git a/src/prototypes/contract-template-management/contract-template-seed.js b/src/prototypes/contract-template-management/contract-template-seed.js
new file mode 100644
index 0000000..9793244
--- /dev/null
+++ b/src/prototypes/contract-template-management/contract-template-seed.js
@@ -0,0 +1,164 @@
+import { V266_LEASE_DOCUMENT_HTML } from './v266-lease-document.js';
+import { TRIAL_AGREEMENT_DOCUMENT_HTML } from './trial-agreement-document.js';
+import { TRIAL_AGREEMENT_18T_DOCUMENT_HTML } from './trial-agreement-18t-document.js';
+import { emptySectionMap, splitMonolithicHtml } from './contract-template-section-html.js';
+
+function getLeaseDocumentSectionMap() {
+ if (typeof V266_LEASE_DOCUMENT_HTML !== 'undefined' && V266_LEASE_DOCUMENT_HTML) {
+ return splitMonolithicHtml(V266_LEASE_DOCUMENT_HTML);
+ }
+ return emptySectionMap();
+}
+
+function isTrialAgreementFileName(name) {
+ return /商用车试用协议/i.test(String(name || ''));
+}
+
+function isTrialAgreement18tFileName(name) {
+ return /商用车试用协议.*现代18吨|现代18吨.*商用车试用协议/i.test(String(name || ''));
+}
+
+function getTrialAgreementHtml(fileName) {
+ if (isTrialAgreement18tFileName(fileName)) {
+ return typeof TRIAL_AGREEMENT_18T_DOCUMENT_HTML !== 'undefined' ? TRIAL_AGREEMENT_18T_DOCUMENT_HTML : '';
+ }
+ if (isTrialAgreementFileName(fileName)) {
+ return typeof TRIAL_AGREEMENT_DOCUMENT_HTML !== 'undefined' ? TRIAL_AGREEMENT_DOCUMENT_HTML : '';
+ }
+ return '';
+}
+
+function getTrialAgreementSectionMap(fileName) {
+ var html = getTrialAgreementHtml(fileName);
+ if (!html) return emptySectionMap();
+ return splitMonolithicHtml(html);
+}
+
+function createWordDocument(params) {
+ return Object.assign({
+ category: 'lease',
+ contractType: '',
+ fileName: '未命名合同.docx',
+ versionName: '未命名合同',
+ versionNo: null,
+ status: 'draft',
+ enabled: false,
+ versionLogs: [],
+ sectionContents: emptySectionMap(),
+ sectionVehicleBindings: {},
+ remark: '',
+ }, params || {});
+}
+
+/** 冷启动种子:未打开模板管理页时租赁创建等场景仍可列出已启用模板 */
+export function buildInitialWordTemplates() {
+ var v266 = getLeaseDocumentSectionMap();
+ var trialBase = getTrialAgreementSectionMap('商用车试用协议.docx');
+ var trial18t = getTrialAgreementSectionMap('商用车试用协议-现代18吨.docx');
+ var model18t = [{ brand: '现代', model: '18吨氢燃料电池车' }];
+ var model45t = [{ brand: '现代', model: '帕力安牌4.5吨冷链车' }];
+ var model96 = [{ brand: '苏龙', model: '9.6米氢燃料电池车' }];
+ var fullSections = Object.assign(emptySectionMap(), v266);
+ return [
+ createWordDocument({
+ id: 'doc-1',
+ contractType: '现代18吨正式合同',
+ fileName: '现代18吨车型-数智中心版.docx',
+ versionName: '现代18吨车型-数智中心版',
+ versionNo: 'v1.00',
+ status: 'published',
+ enabled: true,
+ versionLogs: [{
+ id: 'log-doc-1-v0',
+ versionNo: 'v0.99',
+ action: '发布变更',
+ operator: '法务-王静',
+ time: '2026-05-20 16:40',
+ remark: '数智中心版条款调整前的归档快照',
+ snapshot: {
+ fileName: '现代18吨车型-数智中心版.docx',
+ versionName: '现代18吨车型-数智中心版',
+ versionNo: 'v0.99',
+ sectionContents: Object.assign(emptySectionMap(), v266),
+ sectionVehicleBindings: { lease_order: model18t },
+ remark: '历史快照(原型)',
+ archivedAt: '2026-05-20 16:40',
+ },
+ }],
+ sectionContents: fullSections,
+ sectionVehicleBindings: {
+ lease_order: model18t,
+ dynamic_supervision: model96,
+ vehicle_spec: model45t,
+ },
+ creator: '法务-王静',
+ createTime: '2026-05-25 09:13',
+ updater: '法务-王静',
+ updateTime: '2026-05-25 09:13',
+ remark: 'V26.6 现代18吨数智中心版标准合同',
+ }),
+ createWordDocument({
+ id: 'doc-2',
+ contractType: '正式合同',
+ fileName: '2026年标准商用车租赁合同.docx',
+ versionName: '2026年标准商用车租赁合同',
+ versionNo: 'v1.01',
+ status: 'published',
+ enabled: true,
+ sectionContents: fullSections,
+ sectionVehicleBindings: {},
+ creator: '法务-王静',
+ createTime: '2026-03-15 14:30',
+ updater: '法务-王静',
+ updateTime: '2026-03-15 14:30',
+ remark: '通用主体条款',
+ }),
+ createWordDocument({
+ id: 'doc-3',
+ contractType: '正式合同',
+ fileName: '2026年二季度草案.docx',
+ versionName: '2026年二季度草案',
+ versionNo: null,
+ status: 'draft',
+ sectionContents: fullSections,
+ sectionVehicleBindings: {},
+ creator: '法务-李明',
+ createTime: '2026-06-01 09:15',
+ updater: '法务-李明',
+ updateTime: '2026-06-01 09:15',
+ remark: '待法务审核发布',
+ }),
+ createWordDocument({
+ id: 'doc-4',
+ contractType: '试用合同',
+ fileName: '商用车试用协议.docx',
+ versionName: '商用车试用协议',
+ versionNo: 'v1.00',
+ status: 'published',
+ enabled: true,
+ sectionContents: Object.assign(emptySectionMap(), trialBase),
+ sectionVehicleBindings: {},
+ creator: '法务-王静',
+ createTime: '2026-06-20 10:00',
+ updater: '法务-王静',
+ updateTime: '2026-06-20 10:00',
+ remark: '通用商用车试用协议范本',
+ }),
+ createWordDocument({
+ id: 'doc-5',
+ contractType: '现代18吨试用合同',
+ fileName: '商用车试用协议-现代18吨.docx',
+ versionName: '商用车试用协议-现代18吨',
+ versionNo: 'v1.00',
+ status: 'published',
+ enabled: false,
+ sectionContents: Object.assign(emptySectionMap(), trial18t),
+ sectionVehicleBindings: {},
+ creator: '法务-李明',
+ createTime: '2026-06-25 14:20',
+ updater: '法务-李明',
+ updateTime: '2026-06-25 14:20',
+ remark: '现代18吨车型试用协议;含完整安全附件',
+ }),
+ ];
+}
diff --git a/src/prototypes/contract-template-management/contract-template-store.js b/src/prototypes/contract-template-management/contract-template-store.js
new file mode 100644
index 0000000..ffbdfd3
--- /dev/null
+++ b/src/prototypes/contract-template-management/contract-template-store.js
@@ -0,0 +1,205 @@
+/**
+ * 合同模板跨原型共享状态(合同模板管理 ↔ 租赁合同创建)
+ * 原型级 store;localStorage 优先持久化,sessionStorage 作兼容回退。
+ */
+
+import {
+ getContractTypeLabel,
+ normalizeContractType,
+ resolveContractType,
+} from './contract-template-types.js';
+
+var STORAGE_KEY = 'oneos_contract_templates_v1';
+var templates = null;
+var listeners = new Set();
+
+function readFromStorage() {
+ if (typeof localStorage !== 'undefined') {
+ try {
+ var localRaw = localStorage.getItem(STORAGE_KEY);
+ if (localRaw) {
+ var localParsed = JSON.parse(localRaw);
+ if (Array.isArray(localParsed) && localParsed.length) return localParsed;
+ }
+ } catch (err) {
+ /* 忽略 */
+ }
+ }
+ if (typeof sessionStorage === 'undefined') return null;
+ try {
+ var raw = sessionStorage.getItem(STORAGE_KEY);
+ if (!raw) return null;
+ var parsed = JSON.parse(raw);
+ return Array.isArray(parsed) ? parsed : null;
+ } catch (err) {
+ return null;
+ }
+}
+
+function writeToStorage() {
+ if (!templates) return;
+ var payload = JSON.stringify(templates);
+ if (typeof localStorage !== 'undefined') {
+ try {
+ localStorage.setItem(STORAGE_KEY, payload);
+ } catch (err) {
+ /* 原型忽略存储失败 */
+ }
+ }
+ if (typeof sessionStorage !== 'undefined') {
+ try {
+ sessionStorage.setItem(STORAGE_KEY, payload);
+ } catch (err) {
+ /* 原型忽略存储失败 */
+ }
+ }
+}
+
+function notify() {
+ var snapshot = templates ? templates.slice() : [];
+ listeners.forEach(function (fn) {
+ try {
+ fn(snapshot);
+ } catch (err) {
+ console.error('[contract-template-store] listener error', err);
+ }
+ });
+}
+
+export function restoreContractTemplatesFromSession() {
+ if (templates) return templates;
+ var restored = readFromStorage();
+ if (restored) {
+ templates = restored;
+ notify();
+ }
+ return templates || [];
+}
+
+export function ensureContractTemplateStore(seedFactory) {
+ if (!templates) {
+ templates = readFromStorage();
+ }
+ if (!templates && typeof seedFactory === 'function') {
+ templates = seedFactory();
+ writeToStorage();
+ notify();
+ }
+ return templates || [];
+}
+
+export function getContractTemplates() {
+ return templates ? templates.slice() : [];
+}
+
+export function setContractTemplates(updater) {
+ var current = templates ? templates.slice() : [];
+ var next = typeof updater === 'function' ? updater(current) : updater;
+ templates = Array.isArray(next) ? next : current;
+ writeToStorage();
+ notify();
+ return templates;
+}
+
+export function subscribeContractTemplates(listener) {
+ if (typeof listener !== 'function') return function () {};
+ listeners.add(listener);
+ return function () {
+ listeners.delete(listener);
+ };
+}
+
+var VEHICLE_HINT_BY_ID = {
+ 'doc-1': '现代 · 18吨氢燃料电池车',
+ 'doc-2': '通用车型',
+ 'doc-4': '通用车型',
+ 'doc-5': '现代 · 18吨',
+};
+
+function inferKind(doc) {
+ return resolveContractType(doc);
+}
+
+function inferVehicleHint(doc) {
+ if (doc.id && VEHICLE_HINT_BY_ID[doc.id]) return VEHICLE_HINT_BY_ID[doc.id];
+ return '通用车型';
+}
+
+export function mapTemplateToLeaseOption(doc) {
+ var contractType = resolveContractType(doc);
+ return {
+ id: doc.id,
+ title: doc.versionName || stripExtension(doc.fileName) || doc.id,
+ fileName: doc.fileName || doc.versionName || '',
+ versionNo: doc.versionNo || '—',
+ contractType: contractType,
+ contractTypeLabel: getContractTypeLabel(contractType),
+ kind: inferKind(doc),
+ vehicleHint: inferVehicleHint(doc),
+ };
+}
+
+function stripExtension(name) {
+ return String(name || '').replace(/\.[^.]+$/, '');
+}
+
+/** 已发布且启用、可供租赁合同创建选用的模板 */
+export function getPublishedEnabledLeaseOptions(category) {
+ var cat = category || 'lease';
+ return (templates || [])
+ .filter(function (doc) {
+ return doc.category === cat
+ && doc.status === 'published'
+ && doc.enabled !== false;
+ })
+ .map(mapTemplateToLeaseOption)
+ .sort(function (a, b) {
+ return String(a.fileName).localeCompare(String(b.fileName), 'zh-CN');
+ });
+}
+
+/** 已发布且启用模板中去重后的合同类型(供租赁合同创建选用) */
+export function getPublishedContractTypeOptions(category) {
+ var cat = category || 'lease';
+ var seen = {};
+ var types = [];
+ (templates || []).forEach(function (doc) {
+ if (doc.category !== cat || doc.status !== 'published' || doc.enabled === false) return;
+ var type = resolveContractType(doc);
+ if (!type || seen[type]) return;
+ seen[type] = true;
+ types.push({ value: type, label: type });
+ });
+ return types.sort(function (a, b) {
+ return a.label.localeCompare(b.label, 'zh-CN');
+ });
+}
+
+export function getTemplateDocumentById(templateId) {
+ if (!templateId || !templates) return null;
+ return templates.find(function (doc) { return doc.id === templateId; }) || null;
+}
+
+export function getTemplateSectionVehicleBindings(templateId) {
+ var doc = getTemplateDocumentById(templateId);
+ if (!doc || !doc.sectionVehicleBindings) return {};
+ return doc.sectionVehicleBindings;
+}
+
+/** 某合同类型下已发布且启用的模板(取最近更新的一条) */
+export function getDefaultTemplateIdForContractType(contractType, category) {
+ var normalizedType = normalizeContractType(contractType);
+ if (!normalizedType) return '';
+ var cat = category || 'lease';
+ var matches = (templates || []).filter(function (doc) {
+ return doc.category === cat
+ && doc.status === 'published'
+ && doc.enabled !== false
+ && resolveContractType(doc) === normalizedType;
+ });
+ if (!matches.length) return '';
+ matches.sort(function (a, b) {
+ return String(b.updateTime || '').localeCompare(String(a.updateTime || ''), 'zh-CN');
+ });
+ return matches[0].id;
+}
diff --git a/src/prototypes/contract-template-management/contract-template-types.js b/src/prototypes/contract-template-management/contract-template-types.js
new file mode 100644
index 0000000..b041dc0
--- /dev/null
+++ b/src/prototypes/contract-template-management/contract-template-types.js
@@ -0,0 +1,23 @@
+/**
+ * 合同类型 — 用户自定义文本,合同模板管理与租赁合同创建共用
+ */
+
+export function normalizeContractType(value) {
+ return String(value || '').trim();
+}
+
+export function getContractTypeLabel(value) {
+ var text = normalizeContractType(value);
+ return text || '—';
+}
+
+/** 无 contractType 字段时,按文件名推断(兼容旧数据) */
+export function inferContractTypeFromDoc(doc) {
+ if (doc && doc.contractType) return normalizeContractType(doc.contractType);
+ var name = String((doc && (doc.fileName || doc.versionName)) || '');
+ return /试用/.test(name) ? '试用合同' : '正式合同';
+}
+
+export function resolveContractType(doc) {
+ return inferContractTypeFromDoc(doc);
+}
diff --git a/src/prototypes/contract-template-management/index.tsx b/src/prototypes/contract-template-management/index.tsx
index 2c6dff3..4afcc30 100644
--- a/src/prototypes/contract-template-management/index.tsx
+++ b/src/prototypes/contract-template-management/index.tsx
@@ -1,7 +1,8 @@
/**
* @name 合同模板管理
*/
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { clearHostPrototypeRouteInfo } from '../../common/useHashPage';
import * as antd from 'antd';
import { ConfigProvider } from 'antd';
import {
@@ -72,6 +73,10 @@ export default function ContractTemplateManagementPage() {
const [annotationPageId, setAnnotationPageId] = useState('list');
const navigationRef = useRef({});
+ useEffect(() => {
+ clearHostPrototypeRouteInfo();
+ }, []);
+
const handleDirectoryRoute = useCallback((node: AnnotationDirectoryRouteNode) => {
const payload = node.payload as { pageId?: string } | undefined;
const pageId: ContractPageId = payload?.pageId === 'editor' ? 'editor' : 'list';
diff --git a/src/prototypes/contract-template-management/scripts/build-annotation-source.mjs b/src/prototypes/contract-template-management/scripts/build-annotation-source.mjs
new file mode 100644
index 0000000..1df0f02
--- /dev/null
+++ b/src/prototypes/contract-template-management/scripts/build-annotation-source.mjs
@@ -0,0 +1,104 @@
+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 prdPath = path.resolve(protoRoot, '../../resources/contract-template-management/PRD.md');
+const sourcePath = path.join(protoRoot, 'annotation-source.json');
+
+const fullPrd = fs.readFileSync(prdPath, 'utf8');
+const source = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
+
+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 combineSections(headings) {
+ return headings.map((h) => extractSection(fullPrd, h)).filter(Boolean).join('\n\n---\n\n');
+}
+
+const overviewMd = `## 模块定位
+
+| 项 | 说明 |
+|---|---|
+| 模块名称 | 合同模板管理 |
+| 目标用户 | 法务人员 |
+| 核心任务 | 维护 Word 版商用车租赁合同模板,按章节编辑,发布版本并控制启用态 |
+| 数据模型 | 一份 Word 文件 = 一条记录,内含 9 个章节内容 |
+| PRD 文档 | 见目录「PRD 全文 → PRD 完整文档」 |
+| 资源文件 | \`src/resources/contract-template-management/PRD.md\` |
+
+---
+
+### 产品目标(摘要)
+
+| 目标 | 说明 |
+|---|---|
+| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |
+| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |
+| 支撑业务差异化 | 附件章节按车型绑定;各章节均支持条件条款 |
+| 版本可追溯 | 草稿 / 已发布分离;发布变更写入版本日志 |
+| 多套模板并存 | 租赁创建时由用户选择已启用模板 |
+
+**同步命令**:\`node src/prototypes/contract-template-management/scripts/build-annotation-source.mjs\``;
+
+const DOC_MARKDOWN = {
+ 'ct-doc-overview': overviewMd,
+ 'ct-doc-prd-full': fullPrd,
+ 'ct-doc-prd-overview': combineSections(['## 1. 背景与目标', '## 2. 用户与场景', '## 3. 名词解释', '## 4. 信息架构', '## 5. 数据模型', '## 6. 业务规则(全局)']),
+ 'ct-doc-prd-list': `# 合同模板管理 · 列表页 PRD\n\n${extractSection(fullPrd, '## 7. 列表页需求') ?? ''}`,
+ 'ct-doc-prd-editor': `# 合同模板管理 · 编辑/新增页 PRD\n\n${extractSection(fullPrd, '## 8. 编辑 / 新增页需求') ?? ''}`,
+ 'ct-doc-prd-acceptance': extractSection(fullPrd, '## 16. 验收标准') ?? '## 16. 验收标准',
+ 'ct-doc-node-filter': extractSection(fullPrd, '### 7.2 筛选区') ?? '',
+ 'ct-doc-node-table': combineSections(['### 7.4 主表字段', '### 7.5 主表操作', '### 7.6 子表(展开行)']),
+ 'ct-doc-node-tabs': combineSections(['### 8.3 章节 Tab', '### 8.4 条件条款说明(合同主体及租赁订单~安全责任协议)', '### 8.5 适用车型绑定(仅附件章节 Tab)']),
+ 'ct-doc-node-workspace': combineSections(['### 8.6 编辑区(左侧)', '### 8.7 实时预览(右侧)']),
+ 'ct-doc-annotation-hint': '## 标注查看提示\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`。',
+};
+
+const MARKDOWN_MAP = {
+ 'ct-list-filter': `### 7.2 筛选区\n\n${extractSection(fullPrd, '### 7.2 筛选区')?.replace(/^### 7\.2 筛选区\n*/m, '') ?? ''}`.trim(),
+ 'ct-list-table': combineSections(['### 7.4 主表字段', '### 7.5 主表操作', '### 7.6 子表(展开行)']),
+ 'ct-editor-topbar': combineSections(['### 8.2 顶栏', '### 6.2 保存与发布']),
+ 'ct-section-tabs': extractSection(fullPrd, '### 8.3 章节 Tab') ?? '',
+ 'ct-contract-type': source.markdownMap?.['ct-contract-type'] ?? '',
+ 'ct-vehicle-binding': extractSection(fullPrd, '### 8.5 适用车型绑定(仅附件章节 Tab)') ?? source.markdownMap?.['ct-vehicle-binding'] ?? '',
+ 'ct-editor-panel': combineSections(['### 8.6 编辑区(左侧)']),
+ 'ct-preview-panel': extractSection(fullPrd, '### 8.7 实时预览(右侧)') ?? '',
+};
+
+function patchDirectoryMarkdown(nodes) {
+ for (const node of nodes ?? []) {
+ if (node.type === 'markdown' && DOC_MARKDOWN[node.id]) {
+ node.markdown = DOC_MARKDOWN[node.id];
+ if (node.id === 'ct-doc-prd-full') {
+ node.markdownPath = 'src/resources/contract-template-management/PRD.md';
+ }
+ }
+ if (node.children) patchDirectoryMarkdown(node.children);
+ }
+}
+
+const ctDocRoot = source.directory?.nodes?.find((n) => n.id === 'ct-doc-root');
+if (ctDocRoot) patchDirectoryMarkdown(ctDocRoot.children);
+
+source.markdownMap = { ...source.markdownMap, ...MARKDOWN_MAP };
+source.data.updatedAt = Date.now();
+
+fs.writeFileSync(sourcePath, `${JSON.stringify(source, null, 2)}\n`);
+console.log(`Synced annotation-source.json from PRD.md (${fullPrd.length} chars).`);
diff --git a/src/prototypes/contract-template-management/styles/contract-template.css b/src/prototypes/contract-template-management/styles/contract-template.css
index de46621..0a88231 100644
--- a/src/prototypes/contract-template-management/styles/contract-template.css
+++ b/src/prototypes/contract-template-management/styles/contract-template.css
@@ -9,16 +9,16 @@
.vm-page.ct-page .ct-data-table.ant-table-wrapper .ant-table-thead > tr > th {
background: var(--ln-canvas-soft) !important;
- color: var(--ln-muted) !important;
- font-size: 0.75rem !important;
- font-weight: 500 !important;
+ color: var(--ln-muted-80) !important;
+ font-size: 12px !important;
+ font-weight: 600 !important;
letter-spacing: 0 !important;
border-bottom: 1px solid var(--ln-hairline) !important;
padding: 8px 12px !important;
}
.vm-page.ct-page .ct-data-table.ant-table-wrapper .ant-table-tbody > tr > td {
- font-size: 0.8125rem !important;
+ font-size: 0.875rem !important;
color: var(--ln-body) !important;
border-bottom: 1px solid var(--ln-hairline) !important;
padding: 8px 12px !important;
@@ -78,7 +78,50 @@
}
.vm-page.ct-page .ct-data-table--nested {
- --ct-list-table-width: 1232px;
+ --ct-list-table-width: 1100px;
+}
+
+/* 列表页全宽:表格与卡片铺满内容区(预留右侧标注工具栏空间) */
+.vm-page.ct-page.ct-list-shell {
+ width: 100%;
+ max-width: none;
+ box-sizing: border-box;
+ padding-right: max(24px, 88px);
+}
+
+.vm-page.ct-page.ct-list-shell .vm-filter-card,
+.vm-page.ct-page.ct-list-shell .vm-table-section,
+.vm-page.ct-page.ct-list-shell .vm-table-card,
+.vm-page.ct-page.ct-list-shell .vm-table-card.ct-list-table-card,
+.vm-page.ct-page.ct-list-shell .ct-data-table.ant-table-wrapper {
+ width: 100%;
+ max-width: 100%;
+}
+
+.vm-page.ct-page.ct-list-shell .ct-data-table--nested {
+ --ct-list-table-width: 100%;
+}
+
+.vm-page.ct-page.ct-list-shell .ct-data-table--nested .ant-table-content > table,
+.vm-page.ct-page.ct-list-shell .ct-data-table--nested .ant-table-header table,
+.vm-page.ct-page.ct-list-shell .ct-data-table--nested .ant-table-body table {
+ width: 100% !important;
+ min-width: 100% !important;
+ table-layout: fixed !important;
+}
+
+@media (max-width: 1280px) {
+ .vm-page.ct-page .ct-data-table--nested {
+ --ct-list-table-width: 920px;
+ }
+
+ .vm-page.ct-page.ct-list-shell .ct-data-table--nested {
+ --ct-list-table-width: 100%;
+ }
+
+ .vm-page.ct-page .ct-data-table.ant-table-wrapper .ct-col-optional {
+ display: none !important;
+ }
}
.vm-page.ct-page .ct-data-table--nested .ant-table-thead > tr > th.ant-table-row-expand-icon-cell,
@@ -159,6 +202,19 @@
margin-bottom: 16px;
}
+.ct-modal-wrap .ant-modal {
+ max-width: calc(100vw - 32px) !important;
+}
+
+.vm-page.ct-page .ct-editor-layout-hint {
+ flex-basis: 100%;
+ display: block;
+ margin-top: 2px;
+ font-size: 12px;
+ font-weight: 400;
+ color: var(--ln-muted);
+}
+
.vm-page.ct-page .vm-table-section .vm-table-toolbar {
display: flex;
justify-content: flex-end;
@@ -223,7 +279,7 @@
}
.ct-section-tabs-more-popover .ct-section-tabs-more {
- width: min(320px, calc(100vw - 32px));
+ width: min(380px, calc(100vw - 32px));
padding: 12px 0 8px;
}
@@ -251,7 +307,8 @@
.ct-section-tabs-more-popover .ct-section-tabs-more__item {
display: flex;
- align-items: center;
+ align-items: flex-start;
+ gap: 10px;
flex: 0 0 auto;
width: 100%;
box-sizing: border-box;
@@ -265,12 +322,37 @@
font-size: 0.875rem;
line-height: 1.45;
text-align: left;
- white-space: normal;
- word-break: break-word;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
+.ct-section-tabs-more-popover .ct-section-tabs-more__index {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 24px;
+ height: 24px;
+ border-radius: 999px;
+ background: var(--ln-surface-strong, #f1f5f4);
+ color: var(--ln-muted, #64748b);
+ font-size: 0.75rem;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+
+.ct-section-tabs-more-popover .ct-section-tabs-more__item.is-active .ct-section-tabs-more__index {
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 18%, #fff);
+ color: var(--ln-primary-focus, #2a8a5e);
+}
+
+.ct-section-tabs-more-popover .ct-section-tabs-more__label {
+ flex: 1;
+ min-width: 0;
+ white-space: normal;
+ word-break: break-word;
+}
+
.ct-section-tabs-more-popover .ct-section-tabs-more__item:hover {
background: var(--ln-surface-strong, #f1f5f4);
color: var(--ln-ink, #0f172a);
@@ -417,17 +499,42 @@
========================================================================== */
.vm-page.ct-page .ct-action-btn.ant-btn-link {
- min-height: var(--vm-btn-height);
- padding: 0 8px !important;
+ min-height: 44px;
+ padding: 0 10px !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
color: var(--ln-primary) !important;
+ cursor: pointer;
+}
+
+.vm-page.ct-page .ct-row-actions {
+ display: inline-flex;
+ align-items: center;
+ flex-wrap: nowrap;
+ gap: 2px;
+ max-width: 100%;
+}
+
+.vm-page.ct-page .ct-row-actions__more.ant-btn-link {
+ padding: 0 8px !important;
}
.vm-page.ct-page .ct-action-btn.ant-btn-link:hover:not(:disabled) {
color: var(--ln-primary-hover) !important;
}
+.vm-page.ct-page .ct-action-btn.ant-btn-link:disabled,
+.vm-page.ct-page .ct-action-btn.ant-btn-link.ant-btn-disabled {
+ color: #94A3B8 !important;
+ cursor: not-allowed !important;
+ opacity: 0.55;
+}
+
+.vm-page.ct-page .ct-action-btn.ant-btn-link.ant-btn-dangerous:disabled,
+.vm-page.ct-page .ct-action-btn.ant-btn-link.ant-btn-dangerous.ant-btn-disabled {
+ color: #94A3B8 !important;
+}
+
.vm-page.ct-page .ct-btn-cta.ant-btn-primary {
background: var(--ln-primary) !important;
border-color: var(--ln-primary) !important;
@@ -461,6 +568,7 @@
gap: 8px;
flex: 1;
min-width: 0;
+ flex-wrap: wrap;
}
.vm-page.ct-page .ct-panel-head__badge {
@@ -716,22 +824,101 @@
padding: 0 16px;
}
+.vm-page.ct-page .ct-section-nav {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ grid-template-rows: auto auto;
+ gap: 8px 10px;
+ align-items: center;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--ln-hairline);
+}
+
+.vm-page.ct-page .ct-section-tabs-scroll {
+ grid-column: 1;
+ grid-row: 1;
+ min-width: 0;
+ overflow-x: auto;
+ overflow-y: hidden;
+ -webkit-overflow-scrolling: touch;
+ scroll-behavior: smooth;
+ scrollbar-width: thin;
+}
+
+.vm-page.ct-page .ct-section-tabs-scroll::-webkit-scrollbar {
+ height: 6px;
+}
+
+.vm-page.ct-page .ct-section-tabs-scroll::-webkit-scrollbar-thumb {
+ background: color-mix(in srgb, var(--ln-muted) 35%, transparent);
+ border-radius: 999px;
+}
+
+.vm-page.ct-page .ct-section-nav__list-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ min-height: 36px;
+ padding: 0 12px;
+ border: 1px solid var(--ln-hairline);
+ border-radius: var(--ln-radius-control);
+ background: var(--ln-surface-card, #fff);
+ color: var(--ln-body);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+ grid-column: 2;
+ grid-row: 1;
+}
+
+.vm-page.ct-page .ct-section-nav__list-btn:hover,
+.vm-page.ct-page .ct-section-nav__list-btn.is-open {
+ border-color: color-mix(in srgb, var(--ln-primary) 35%, var(--ln-hairline));
+ background: var(--ln-primary-soft);
+ color: var(--ln-primary-focus);
+}
+
+.vm-page.ct-page .ct-section-nav__list-btn:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 1px var(--vm-focus-border-color);
+}
+
+.vm-page.ct-page .ct-section-nav__list-icon {
+ flex-shrink: 0;
+}
+
+.vm-page.ct-page .ct-section-nav__current {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ margin: 0;
+ padding: 0 2px;
+ color: var(--ln-ink);
+ font-size: 0.875rem;
+ font-weight: 600;
+ line-height: 1.45;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.vm-page.ct-page .ct-section-tabs {
display: flex;
align-items: center;
- flex-wrap: wrap;
- gap: 8px;
- padding-bottom: 10px;
- border-bottom: 1px solid var(--ln-hairline);
+ flex-wrap: nowrap;
+ gap: 6px;
+ padding: 2px 2px 4px;
}
.vm-page.ct-page .ct-section-tab {
display: inline-flex;
align-items: center;
gap: 6px;
- max-width: 100%;
+ flex-shrink: 0;
+ max-width: 180px;
min-height: 36px;
- padding: 6px 12px;
+ padding: 6px 10px;
border: 1px solid transparent;
border-radius: var(--ln-radius-control);
background: transparent;
@@ -743,6 +930,29 @@
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
+.vm-page.ct-page .ct-section-tab__index {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 4px;
+ border-radius: 999px;
+ background: var(--ln-surface-strong);
+ color: var(--ln-muted);
+ font-size: 0.6875rem;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+}
+
+.vm-page.ct-page .ct-section-tab__label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.vm-page.ct-page .ct-section-tab:hover {
color: var(--ln-ink);
background: var(--ln-surface-strong);
@@ -754,31 +964,52 @@
border-color: color-mix(in srgb, var(--ln-primary) 28%, var(--ln-hairline));
}
+.vm-page.ct-page .ct-section-tab.is-active .ct-section-tab__index {
+ background: color-mix(in srgb, var(--ln-primary) 18%, #fff);
+ color: var(--ln-primary-focus);
+}
+
.vm-page.ct-page .ct-section-tab:focus-visible {
outline: none;
box-shadow: inset 0 0 0 1px var(--vm-focus-border-color);
}
-.vm-page.ct-page .ct-section-tab--more .ct-section-tab__current {
- max-width: 220px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--ln-muted);
- font-weight: 400;
-}
-
-.vm-page.ct-page .ct-section-tab--more.is-active .ct-section-tab__current {
- color: var(--ln-primary-focus);
-}
-
@media (max-width: 992px) {
- .vm-page.ct-page .ct-section-tab {
- min-height: 44px;
+ .vm-page.ct-page .ct-section-nav {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto auto auto;
}
- .vm-page.ct-page .ct-section-tab--more .ct-section-tab__current {
- max-width: 140px;
+ .vm-page.ct-page .ct-section-tabs-scroll {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .vm-page.ct-page .ct-section-nav__list-btn {
+ grid-column: 1;
+ grid-row: 2;
+ width: 100%;
+ }
+
+ .vm-page.ct-page .ct-section-nav__current {
+ grid-row: 3;
+ display: block;
+ }
+
+ .vm-page.ct-page .ct-section-tab,
+ .vm-page.ct-page .ct-section-nav__list-btn {
+ min-height: 44px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .vm-page.ct-page .ct-section-tabs-scroll {
+ scroll-behavior: auto;
+ }
+
+ .vm-page.ct-page .ct-section-tab,
+ .vm-page.ct-page .ct-section-nav__list-btn {
+ transition: none;
}
}
@@ -835,7 +1066,10 @@
}
.vm-page.ct-page .ct-vehicle-binding-card + .ct-editor-page__body,
-.vm-page.ct-page .ct-editor-page > .ct-vehicle-binding-card {
+.vm-page.ct-page .ct-contract-type-card + .ct-editor-page__body,
+.vm-page.ct-page .ct-contract-type-card + .ct-vehicle-binding-card,
+.vm-page.ct-page .ct-editor-page > .ct-vehicle-binding-card,
+.vm-page.ct-page .ct-editor-page > .ct-contract-type-card {
flex-shrink: 0;
}
@@ -848,6 +1082,18 @@
flex-wrap: wrap;
}
+.vm-page.ct-page .ct-editor-page__topbar-meta {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ min-width: 0;
+}
+
+.vm-page.ct-page .ct-editor-page__topbar-meta .ct-editor-category-field {
+ align-items: flex-start;
+}
+
.vm-page.ct-page .ct-editor-page__topbar-actions {
display: flex;
align-items: center;
@@ -965,7 +1211,7 @@
color: var(--ln-muted);
}
-.vm-page.ct-page .ct-editor-category-select {
+.vm-page.ct-page .ct-editor-contract-type-input {
width: 220px;
}
@@ -998,7 +1244,6 @@
align-items: center;
justify-content: space-between;
gap: 12px;
- min-height: var(--vm-btn-height, 36px);
margin-bottom: 12px;
padding: 0 0 10px;
border-bottom: 1px solid var(--ln-hairline);
@@ -1006,6 +1251,81 @@
box-sizing: border-box;
}
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn.ant-btn,
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ant-upload .ct-panel-tool-btn.ant-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ min-height: 32px;
+ height: 32px;
+ padding: 0 12px;
+ border-radius: 8px;
+ font-size: 0.8125rem;
+ font-weight: 500;
+ line-height: 1.2;
+ box-shadow: none;
+ transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn__icon {
+ flex-shrink: 0;
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn--ghost.ant-btn {
+ background: var(--ln-surface-card);
+ color: var(--ln-body);
+ border: 1px solid var(--ln-hairline);
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn--ghost.ant-btn:hover:not(:disabled) {
+ background: var(--ln-canvas-soft);
+ border-color: var(--ln-hairline-strong);
+ color: var(--ln-ink);
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn--accent.ant-btn {
+ background: var(--ln-primary-soft);
+ color: var(--ln-primary-focus);
+ border: 1px solid color-mix(in srgb, var(--ln-primary) 35%, var(--ln-hairline));
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn--accent.ant-btn:hover:not(:disabled) {
+ background: color-mix(in srgb, var(--ln-primary) 14%, #fff);
+ border-color: var(--ln-primary);
+ color: var(--ln-primary-focus);
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn.is-active.ant-btn {
+ background: var(--ln-primary-soft);
+ color: var(--ln-primary-focus);
+ border-color: color-mix(in srgb, var(--ln-primary) 45%, var(--ln-hairline));
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn.ant-btn:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 1px var(--vm-focus-border-color);
+}
+
+.vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ant-upload {
+ display: inline-flex;
+}
+
+@media (max-width: 992px) {
+ .vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn.ant-btn,
+ .vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ant-upload .ct-panel-tool-btn.ant-btn {
+ min-height: 44px;
+ height: 44px;
+ padding: 0 14px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .vm-page.ct-page .ct-editor-workspace .ct-panel-head__actions .ct-panel-tool-btn.ant-btn {
+ transition: none;
+ }
+}
+
.vm-page.ct-page .ct-editor-workspace .ct-panel-head__title {
display: flex;
align-items: center;
@@ -1038,7 +1358,7 @@
display: flex;
align-items: center;
gap: 8px;
- min-height: var(--vm-btn-height, 36px);
+ min-height: 32px;
}
.vm-page.ct-page .ct-editor-workspace .ct-editor-column-card .ct-editor-panel,
@@ -1373,11 +1693,6 @@
width: 100%;
}
-.ct-main-clause-tip {
- margin-bottom: 12px;
- border-left: 3px solid #7c3aed;
-}
-
.ct-preview-doc .ct-vehicle-clause,
.ct-rich-editor .ct-vehicle-clause {
box-decoration-break: clone;
@@ -1725,6 +2040,20 @@
line-height: 1.5;
}
+.vm-page.ct-page .ct-form-field__required {
+ color: #dc2626;
+ margin-right: 4px;
+ font-weight: 600;
+}
+
+.vm-page.ct-page .ct-contract-type-card__form {
+ max-width: 360px;
+}
+
+.vm-page.ct-page .ct-contract-type-card__form .ct-editor-contract-type-input {
+ width: 100%;
+}
+
.vm-page.ct-page .ct-vehicle-binding-card {
margin-bottom: 16px;
padding: 16px 20px;
@@ -1756,6 +2085,192 @@
margin-bottom: 12px;
}
+.vm-page.ct-page .ct-vehicle-brand-model-picker {
+ width: 100%;
+ max-width: 480px;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__trigger {
+ width: 100%;
+ min-height: var(--vm-control-height, 32px);
+ height: var(--vm-control-height, 32px);
+ cursor: pointer;
+ align-items: center;
+ border-color: var(--ln-hairline);
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__trigger:has(.ct-vehicle-brand-model-picker__summary-tags),
+.vm-page.ct-page .ct-vehicle-brand-model-picker__trigger:has(.ct-vehicle-brand-model-picker__summary-tag--all) {
+ height: auto;
+ min-height: var(--vm-control-height, 32px);
+ padding-top: 6px;
+ padding-bottom: 6px;
+ align-items: center;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__summary-tags {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+ min-width: 0;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__summary-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ max-width: 100%;
+ padding: 2px 10px;
+ border-radius: 9999px;
+ background: color-mix(in srgb, var(--ln-primary) 12%, var(--ln-surface-card));
+ border: 1px solid color-mix(in srgb, var(--ln-primary) 30%, transparent);
+ font-size: 0.8125rem;
+ font-weight: 600;
+ line-height: 1.35;
+ color: var(--ln-primary-focus);
+ white-space: nowrap;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__summary-tag-brand,
+.vm-page.ct-page .ct-vehicle-brand-model-picker__summary-tag-model {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__summary-tag-sep {
+ flex-shrink: 0;
+ color: color-mix(in srgb, var(--ln-primary-focus) 70%, var(--ln-muted));
+ font-weight: 700;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__summary-tag--all,
+.vm-page.ct-page .ct-vehicle-brand-model-panel__selected-tag--all {
+ background: color-mix(in srgb, var(--ln-primary) 16%, var(--ln-surface-card));
+ border-color: color-mix(in srgb, var(--ln-primary) 38%, transparent);
+ color: var(--ln-primary-focus);
+ font-weight: 700;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__all-brand {
+ font-weight: 700;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__all-hint {
+ width: 100%;
+ margin: 0;
+ color: var(--ln-muted);
+ font-size: 0.8125rem;
+ line-height: 1.5;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-picker__summary.is-placeholder {
+ color: var(--ln-muted-soft);
+ display: flex;
+ align-items: center;
+ align-self: center;
+ height: auto;
+ line-height: 1.5715;
+}
+
+.ct-vehicle-brand-model-popover .ant-popover-inner {
+ padding: 0;
+}
+
+.ct-vehicle-brand-model-popover .ct-vehicle-brand-model-panel {
+ width: min(560px, calc(100vw - 32px));
+ max-height: min(420px, calc(100vh - 120px));
+ padding: 12px;
+ overflow: hidden;
+ border: none;
+ background: transparent;
+}
+
+.ct-vehicle-brand-model-popover .vm-operate-city-edit-selected {
+ align-items: flex-start;
+}
+
+.ct-vehicle-brand-model-popover .vm-operate-city-edit-main {
+ min-height: 220px;
+}
+
+@media (max-width: 640px) {
+ .ct-vehicle-brand-model-popover .vm-operate-city-edit-main {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .ct-vehicle-brand-model-popover .vm-operate-city-edit-step--province {
+ padding-right: 0;
+ border-right: none;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--ln-hairline);
+ }
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-panel {
+ width: 100%;
+ max-width: 100%;
+ gap: 12px;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-panel .vm-operate-city-edit-main {
+ min-height: 220px;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-panel__selected-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ flex: 1;
+ min-width: 0;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-panel__selected-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border-radius: 9999px;
+ background: var(--ln-surface-card);
+ border: 1px solid color-mix(in srgb, var(--ln-primary) 22%, transparent);
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: var(--ln-ink);
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-panel__selected-tag button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ border: none;
+ border-radius: 9999px;
+ background: transparent;
+ color: var(--ln-muted);
+ cursor: pointer;
+}
+
+.vm-page.ct-page .ct-vehicle-brand-model-panel__selected-tag button:hover {
+ background: var(--ln-hairline);
+ color: var(--ln-ink);
+}
+
+@media (max-width: 640px) {
+ .vm-page.ct-page .ct-vehicle-brand-model-panel .vm-operate-city-edit-main {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .vm-page.ct-page .ct-vehicle-brand-model-panel .vm-operate-city-edit-step--province {
+ padding-right: 0;
+ border-right: none;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--ln-hairline);
+ }
+}
+
.vm-page.ct-page .ct-vehicle-binding-brand {
min-width: 120px;
}
@@ -1897,8 +2412,8 @@
padding: 14px 16px;
margin-bottom: 16px;
border-radius: 10px;
- background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
- border: 1px solid #ddd6fe;
+ background: linear-gradient(135deg, var(--ln-primary-soft) 0%, rgba(50, 160, 110, 0.06) 100%);
+ border: 1px solid color-mix(in srgb, var(--ln-primary) 22%, var(--ln-hairline));
}
.ct-vc-modal__hero-icon {
@@ -1909,23 +2424,23 @@
align-items: center;
justify-content: center;
border-radius: 10px;
- background: #7c3aed;
+ background: var(--ln-primary);
color: #fff;
}
.ct-vc-modal__hero-title {
margin: 0 0 6px;
- font-size: 0.8125rem;
+ font-size: 0.875rem;
font-weight: 600;
- color: #5b21b6;
+ color: var(--ln-ink);
}
.ct-vc-modal__hero-list {
margin: 0;
padding-left: 1.1em;
- font-size: 0.8125rem;
+ font-size: 0.875rem;
line-height: 1.55;
- color: #4c1d95;
+ color: var(--ln-body);
}
.ct-vc-modal__hero-list li + li {
@@ -2021,8 +2536,8 @@
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
- background: #ede9fe;
- color: #6d28d9;
+ background: var(--ln-primary-soft);
+ color: var(--ln-primary-focus);
}
.ct-vc-modal__chips {
@@ -2032,8 +2547,8 @@
min-height: 56px;
padding: 12px;
border-radius: 10px;
- border: 1px dashed #c4b5fd;
- background: #faf5ff;
+ border: 1px dashed color-mix(in srgb, var(--ln-primary) 30%, var(--ln-hairline));
+ background: var(--ln-canvas-soft);
}
.ct-vc-modal__chips.is-empty {
@@ -2043,9 +2558,8 @@
.ct-vc-modal__empty-hint {
margin: 0;
- font-size: 0.8125rem;
- color: #7c3aed;
- opacity: 0.8;
+ font-size: 0.875rem;
+ color: var(--ln-muted);
}
.ct-vc-modal__chip.ant-tag {
diff --git a/src/prototypes/contract-template-management/vehicle-binding-utils.js b/src/prototypes/contract-template-management/vehicle-binding-utils.js
new file mode 100644
index 0000000..18e1c16
--- /dev/null
+++ b/src/prototypes/contract-template-management/vehicle-binding-utils.js
@@ -0,0 +1,58 @@
+/**
+ * 章节适用车型绑定:空列表或显式「全部」表示全部品牌型号适用。
+ */
+
+export var VEHICLE_BINDING_ALL_BRAND = '__all__';
+export var VEHICLE_BINDING_ALL_MODEL = '__all__';
+
+export function vehicleBindingKey(binding) {
+ return String(binding.brand || '') + '|' + String(binding.model || '');
+}
+
+export function parseVehicleBindingKey(key) {
+ var parts = String(key || '').split('|');
+ return { brand: parts[0] || '', model: parts[1] || '' };
+}
+
+export function isAllVehicleBindings(bindings) {
+ var list = bindings || [];
+ if (!list.length) return true;
+ if (list.length !== 1) return false;
+ var item = list[0] || {};
+ return item.brand === VEHICLE_BINDING_ALL_BRAND && item.model === VEHICLE_BINDING_ALL_MODEL;
+}
+
+export function isSpecificVehicleBindings(bindings) {
+ return !isAllVehicleBindings(bindings);
+}
+
+export function normalizeAllVehicleBindings() {
+ return [];
+}
+
+export function vehicleMatchesBindings(brand, model, bindings) {
+ if (isAllVehicleBindings(bindings)) return true;
+ var list = bindings || [];
+ return list.some(function (item) {
+ return item.brand === brand && item.model === model;
+ });
+}
+
+/** 订单车辆 keys 是否命中章节绑定(任意一辆命中即可;未选车辆时仅「全部」章节可见) */
+export function sectionBindingsMatchOrder(bindings, vehicleKeys) {
+ if (isAllVehicleBindings(bindings)) return true;
+ var keys = vehicleKeys || [];
+ if (!keys.length) return false;
+ return keys.some(function (key) {
+ var pair = parseVehicleBindingKey(key);
+ return vehicleMatchesBindings(pair.brand, pair.model, bindings);
+ });
+}
+
+export function formatVehicleBindings(bindings) {
+ if (isAllVehicleBindings(bindings)) return '全部车型';
+ var list = bindings || [];
+ return list.map(function (item) {
+ return item.brand + ' · ' + item.model;
+ }).join(';');
+}
diff --git a/src/prototypes/customer-management/annotation-source.json b/src/prototypes/customer-management/annotation-source.json
index 13f2dae..4a36dfa 100644
--- a/src/prototypes/customer-management/annotation-source.json
+++ b/src/prototypes/customer-management/annotation-source.json
@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "customer-management",
"pageId": "list",
- "updatedAt": 1782697342692,
+ "updatedAt": 1782810809219,
"nodes": [
{
"id": "cm-list-filter",
@@ -161,6 +161,198 @@
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "title": "ONE-OS Web 端",
+ "defaultExpanded": true,
+ "children": [
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-workbench",
+ "title": "工作台",
+ "href": "/prototypes/oneos-web-workbench",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-login",
+ "title": "登录",
+ "href": "/prototypes/oneos-web-login",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-vehicle-asset",
+ "title": "车辆管理",
+ "href": "/prototypes/oneos-web-vehicle-asset",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-contract-template",
+ "title": "合同模板管理",
+ "href": "/prototypes/oneos-web-contract-template",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-help-center",
+ "title": "帮助中心",
+ "href": "/prototypes/oneos-web-help-center",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-finance",
+ "title": "财务管理",
+ "href": "/prototypes/oneos-web-finance",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-procurement",
+ "title": "采购管理",
+ "href": "/prototypes/oneos-web-procurement",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-lease-contract",
+ "title": "车辆租赁合同",
+ "href": "/prototypes/oneos-web-lease-contract",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-h2-station",
+ "title": "加氢站管理",
+ "href": "/prototypes/oneos-web-h2-station",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-data-analysis",
+ "title": "数据分析",
+ "href": "/prototypes/oneos-web-data-analysis",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-ledger-data",
+ "title": "台账数据",
+ "href": "/prototypes/oneos-web-ledger-data",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-business",
+ "title": "业务管理",
+ "href": "/prototypes/oneos-web-business",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-ops",
+ "title": "运维管理",
+ "href": "/prototypes/oneos-web-ops",
+ "target": "self"
+ },
+ {
+ "type": "link",
+ "id": "nav-link-oneos-web-requirements",
+ "title": "需求说明",
+ "href": "/prototypes/oneos-web-requirements",
+ "target": "self"
+ }
+ ]
+ }
+ ]
+ },
{
"type": "folder",
"id": "cm-doc-root",
@@ -177,7 +369,7 @@
"type": "link",
"id": "cm-link-self",
"title": "客户管理原型",
- "href": "/prototypes/customer-management",
+ "href": "./index.html",
"target": "self"
},
{
diff --git a/src/prototypes/insurance-procurement/annotation-source.json b/src/prototypes/insurance-procurement/annotation-source.json
index 9605c96..d8c753e 100644
--- a/src/prototypes/insurance-procurement/annotation-source.json
+++ b/src/prototypes/insurance-procurement/annotation-source.json
@@ -5,14 +5,20 @@
"version": 2,
"prototypeName": "insurance-procurement",
"pageId": "list",
- "updatedAt": 1740009600000,
+ "updatedAt": 1782810809220,
"nodes": [
{
"id": "ipc-header",
"index": 1,
"title": "页面标题与模块边界",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-header\"]"], "fingerprint": "header|ipc", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-header\"]"
+ ],
+ "fingerprint": "header|ipc",
+ "path": []
+ },
"aiPrompt": "说明保险采购模块双业务线边界。",
"annotationText": "保单管理与比价单相互独立,不自动双向同步。",
"hasMarkdown": true,
@@ -26,7 +32,13 @@
"index": 2,
"title": "保单管理 · 筛选条件",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-filter\"]"], "fingerprint": "card|filter", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-filter\"]"
+ ],
+ "fingerprint": "card|filter",
+ "path": []
+ },
"aiPrompt": "列表筛选字段与查询逻辑。",
"annotationText": "",
"hasMarkdown": true,
@@ -40,7 +52,13 @@
"index": 3,
"title": "台账车辆总数",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-kpi-total\"]"], "fingerprint": "kpi|total", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-kpi-total\"]"
+ ],
+ "fingerprint": "kpi|total",
+ "path": []
+ },
"aiPrompt": "KPI 点击筛选行为。",
"annotationText": "纳入保险采购台账管理的车辆总数(一车一档)。点击可筛选列表。",
"hasMarkdown": false,
@@ -54,7 +72,13 @@
"index": 4,
"title": "保险状态正常",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-kpi-normal\"]"], "fingerprint": "kpi|normal", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-kpi-normal\"]"
+ ],
+ "fingerprint": "kpi|normal",
+ "path": []
+ },
"aiPrompt": "与车辆管理保险状态联动口径。",
"annotationText": "交强险、商业险均已购买且在有效期内,与车辆管理「保险状态=正常」一致。",
"hasMarkdown": false,
@@ -68,7 +92,13 @@
"index": 5,
"title": "险种临期预警",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-kpi-warning\"]"], "fingerprint": "kpi|warning", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-kpi-warning\"]"
+ ],
+ "fingerprint": "kpi|warning",
+ "path": []
+ },
"aiPrompt": "临期预警弹窗入口。",
"annotationText": "任一类险种止期 ≤ 30 天。点击打开临期预警弹窗,可按险种筛选。",
"hasMarkdown": false,
@@ -82,7 +112,13 @@
"index": 6,
"title": "核心险种逾期",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-kpi-expired\"]"], "fingerprint": "kpi|expired", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-kpi-expired\"]"
+ ],
+ "fingerprint": "kpi|expired",
+ "path": []
+ },
"aiPrompt": "交车合规禁止规则。",
"annotationText": "交强险或商业险已到期 → 车辆管理保险状态异常,禁止交车。",
"hasMarkdown": false,
@@ -96,7 +132,13 @@
"index": 7,
"title": "核心险种待购",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-kpi-unuploaded\"]"], "fingerprint": "kpi|unuploaded", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-kpi-unuploaded\"]"
+ ],
+ "fingerprint": "kpi|unuploaded",
+ "path": []
+ },
"aiPrompt": "未购买核心险种规则。",
"annotationText": "交强险或商业险未录入/未购买 → 保险状态异常,禁止交车。",
"hasMarkdown": false,
@@ -110,7 +152,13 @@
"index": 8,
"title": "比价单管理",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-btn-compare-mgmt\"]"], "fingerprint": "btn|compare-mgmt", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-btn-compare-mgmt\"]"
+ ],
+ "fingerprint": "btn|compare-mgmt",
+ "path": []
+ },
"aiPrompt": "比价单独立业务线入口。",
"annotationText": "",
"hasMarkdown": true,
@@ -124,7 +172,13 @@
"index": 9,
"title": "保单批量识别",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-btn-policy-ocr\"]"], "fingerprint": "btn|ocr", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-btn-policy-ocr\"]"
+ ],
+ "fingerprint": "btn|ocr",
+ "path": []
+ },
"aiPrompt": "OCR 录入通道与确认规则。",
"annotationText": "",
"hasMarkdown": true,
@@ -138,7 +192,13 @@
"index": 10,
"title": "识别任务记录",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-btn-recogn-tasks\"]"], "fingerprint": "btn|tasks", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-btn-recogn-tasks\"]"
+ ],
+ "fingerprint": "btn|tasks",
+ "path": []
+ },
"aiPrompt": "识别任务列表与确认进度。",
"annotationText": "查看 OCR/导入任务状态、识别进度与「确认识别结果」入口。",
"hasMarkdown": false,
@@ -152,7 +212,13 @@
"index": 11,
"title": "批量导入",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-btn-batch-import\"]"], "fingerprint": "btn|import", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-btn-batch-import\"]"
+ ],
+ "fingerprint": "btn|import",
+ "path": []
+ },
"aiPrompt": "Excel 批量导入规则。",
"annotationText": "仅支持新保/续保类业务行;停保/复驶/退保行须自动跳过。",
"hasMarkdown": false,
@@ -166,7 +232,13 @@
"index": 12,
"title": "新增保单",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-btn-policy-add\"]"], "fingerprint": "btn|add", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-btn-policy-add\"]"
+ ],
+ "fingerprint": "btn|add",
+ "path": []
+ },
"aiPrompt": "手工逐条录入入口。",
"annotationText": "手工新增单条保单记录,支持承保险种明细等完整字段(OCR 确认页不展示部分字段)。",
"hasMarkdown": false,
@@ -180,7 +252,13 @@
"index": 13,
"title": "保险台账列表",
"pageId": "list",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-vehicle-table\"]"], "fingerprint": "table|ledger", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-vehicle-table\"]"
+ ],
+ "fingerprint": "table|ledger",
+ "path": []
+ },
"aiPrompt": "列表列含义与操作列。",
"annotationText": "",
"hasMarkdown": true,
@@ -193,7 +271,13 @@
"id": "ipc-vehicle-mgmt",
"index": 14,
"title": "车辆保险档案",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-vehicle-mgmt\"]"], "fingerprint": "modal|vehicle-mgmt", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-vehicle-mgmt\"]"
+ ],
+ "fingerprint": "modal|vehicle-mgmt",
+ "path": []
+ },
"aiPrompt": "管理弹窗结构与 Tab。",
"annotationText": "",
"hasMarkdown": true,
@@ -206,7 +290,13 @@
"id": "ipc-vehicle-mgmt-tabs",
"index": 15,
"title": "险种 Tab 与全周期记录",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-vehicle-mgmt-tabs\"]"], "fingerprint": "tabs|vehicle-mgmt", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-vehicle-mgmt-tabs\"]"
+ ],
+ "fingerprint": "tabs|vehicle-mgmt",
+ "path": []
+ },
"aiPrompt": "全周期时间轴与分险种历史表。",
"annotationText": "",
"hasMarkdown": true,
@@ -219,7 +309,13 @@
"id": "ipc-compare-mgmt",
"index": 16,
"title": "比价单管理弹窗",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-compare-mgmt\"]"], "fingerprint": "modal|compare-mgmt", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-compare-mgmt\"]"
+ ],
+ "fingerprint": "modal|compare-mgmt",
+ "path": []
+ },
"aiPrompt": "比价单批次列表与付费预警。",
"annotationText": "",
"hasMarkdown": true,
@@ -232,7 +328,13 @@
"id": "ipc-compare-pay-alerts",
"index": 17,
"title": "最晚付费临期/超期",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-compare-pay-alerts\"]"], "fingerprint": "compare|pay-alert", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-compare-pay-alerts\"]"
+ ],
+ "fingerprint": "compare|pay-alert",
+ "path": []
+ },
"aiPrompt": "最晚付费日预警规则。",
"annotationText": "最晚付费日期 ≤ 3 天视为临期;已过期为超期。用于采购前提醒,不写入保单台账。",
"hasMarkdown": false,
@@ -245,7 +347,13 @@
"id": "ipc-compare-editor",
"index": 18,
"title": "比价单编辑",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-compare-editor\"]"], "fingerprint": "modal|compare-editor", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-compare-editor\"]"
+ ],
+ "fingerprint": "modal|compare-editor",
+ "path": []
+ },
"aiPrompt": "报价、最终比价、采购审批提交流程。",
"annotationText": "",
"hasMarkdown": true,
@@ -258,7 +366,13 @@
"id": "ipc-compare-procurement",
"index": 19,
"title": "提交采购申请",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-compare-procurement\"]"], "fingerprint": "btn|procurement", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-compare-procurement\"]"
+ ],
+ "fingerprint": "btn|procurement",
+ "path": []
+ },
"aiPrompt": "采购状态与工作流回写。",
"annotationText": "",
"hasMarkdown": true,
@@ -271,7 +385,13 @@
"id": "ipc-policy-recogn",
"index": 20,
"title": "保单识别/导入",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-policy-recogn\"]"], "fingerprint": "modal|recogn", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-policy-recogn\"]"
+ ],
+ "fingerprint": "modal|recogn",
+ "path": []
+ },
"aiPrompt": "OCR 与导入确认页字段差异。",
"annotationText": "",
"hasMarkdown": true,
@@ -284,7 +404,13 @@
"id": "ipc-policy-add",
"index": 21,
"title": "新增保单表单",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-policy-add\"]"], "fingerprint": "modal|policy-add", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-policy-add\"]"
+ ],
+ "fingerprint": "modal|policy-add",
+ "path": []
+ },
"aiPrompt": "手工录入字段与附件。",
"annotationText": "支持完整保单要素、承保险种明细与附件;落库后更新对应险种台账。",
"hasMarkdown": false,
@@ -297,7 +423,13 @@
"id": "ipc-insurance-alert",
"index": 22,
"title": "险种临期/逾期预警弹窗",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-insurance-alert\"]"], "fingerprint": "modal|alert", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-insurance-alert\"]"
+ ],
+ "fingerprint": "modal|alert",
+ "path": []
+ },
"aiPrompt": "预警列表与比价采购状态展示。",
"annotationText": "",
"hasMarkdown": true,
@@ -310,7 +442,13 @@
"id": "ipc-policy-biz",
"index": 23,
"title": "停保 / 复驶 / 退保",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-policy-biz\"]"], "fingerprint": "modal|policy-biz", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-policy-biz\"]"
+ ],
+ "fingerprint": "modal|policy-biz",
+ "path": []
+ },
"aiPrompt": "业务变更表单与附件要求。",
"annotationText": "",
"hasMarkdown": true,
@@ -323,7 +461,13 @@
"id": "ipc-recogn-tasks",
"index": 24,
"title": "识别任务列表",
- "locator": { "selectors": ["[data-annotation-id=\"ipc-recogn-tasks\"]"], "fingerprint": "modal|recogn-tasks", "path": [] },
+ "locator": {
+ "selectors": [
+ "[data-annotation-id=\"ipc-recogn-tasks\"]"
+ ],
+ "fingerprint": "modal|recogn-tasks",
+ "path": []
+ },
"aiPrompt": "任务状态机与确认进度。",
"annotationText": "任务含识别中/已完成;确认识别结果后写入台账。",
"hasMarkdown": false,
@@ -352,6 +496,198 @@
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "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": "ipc-doc-root",
@@ -368,7 +704,7 @@
"type": "link",
"id": "ipc-link-vehicle",
"title": "车辆管理原型",
- "href": "/prototypes/vehicle-management",
+ "href": "../vehicle-management/index.html",
"target": "self"
},
{
diff --git a/src/prototypes/lease-business-ledger/annotation-source.json b/src/prototypes/lease-business-ledger/annotation-source.json
index e4013d9..1a6af83 100644
--- a/src/prototypes/lease-business-ledger/annotation-source.json
+++ b/src/prototypes/lease-business-ledger/annotation-source.json
@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "lease-business-ledger",
"pageId": "list",
- "updatedAt": 1782712021271,
+ "updatedAt": 1782810809220,
"nodes": [
{
"id": "lbl-filter",
@@ -872,6 +872,198 @@
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "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": "lbl-doc-root",
@@ -888,7 +1080,7 @@
"type": "link",
"id": "lbl-link-self",
"title": "租赁业务台账原型",
- "href": "/prototypes/lease-business-ledger",
+ "href": "./index.html",
"target": "self"
},
{
diff --git a/src/prototypes/lease-contract-management/.spec/requirements-prd-create.md b/src/prototypes/lease-contract-management/.spec/requirements-prd-create.md
index 3ea0193..f74e7ae 100644
--- a/src/prototypes/lease-contract-management/.spec/requirements-prd-create.md
+++ b/src/prototypes/lease-contract-management/.spec/requirements-prd-create.md
@@ -85,6 +85,8 @@
支持新增行、删除行。
+**合同备注**:授权委托书区块最下方提供文本域,支持自定义输入,非必填;列表页不再展示备注列。
+
**完成度**:至少一行且三项均非空。
---
@@ -110,8 +112,8 @@
| 类型 | 触发条件 |
|---|---|
-| 标准合同审批 | 默认可编辑预览相对基线未改动风控红线条款 |
-| 非标准合同审批 | 预览中 `.ct-risk-redline[data-risk-redline="1"]` 正文相对基线有增删改 |
+| 标准合同 | 默认可编辑预览相对基线未改动风控红线条款 |
+| 非标准合同 | 预览中 `.ct-risk-redline[data-risk-redline="1"]` 正文相对基线有增删改 |
检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。
diff --git a/src/prototypes/lease-contract-management/.spec/requirements-prd-list.md b/src/prototypes/lease-contract-management/.spec/requirements-prd-list.md
index 7c77470..433706e 100644
--- a/src/prototypes/lease-contract-management/.spec/requirements-prd-list.md
+++ b/src/prototypes/lease-contract-management/.spec/requirements-prd-list.md
@@ -28,7 +28,7 @@
| 业务部门 | 多选下拉 | 业务相关部门 |
| 业务负责人 | 多选下拉 | 业务组人员 |
| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |
-| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |
+| 审批类型 | 多选下拉 | 全部 / 标准合同 / 非标准合同 |
| 创建人 | 多选下拉 | 业务相关人员 |
| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |
@@ -90,17 +90,16 @@
| 列 | 说明 |
|---|---|
+| 审批类型 | 标准合同 / 非标准合同;位于审批状态左侧 |
| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |
| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |
| 签约公司 | 创建时所选甲方签约主体简称 |
| 业务部门 / 业务负责人 | 创建时选取 |
| 合同类型 | 正式合同 / 试用合同 |
-| 合同审批类型 | 标准合同审批 / 非标准合同审批 |
-| 合同结束日期 | YYYY-MM-DD |
| 客户联系人 / 联系电话 | 来自客户档案 |
+| 被授权人 | 显示人数;点击弹出卡片查看姓名、联系方式、身份证号 |
| 创建人 / 创建时间 | 精确至分钟 |
-| 更新人 / 最后更新时间 | 无则显示 `-` |
-| 备注 | 超长省略 |
+| 最后更新 | 更新人 / 最后更新时间,无则显示 `-` |
**分页**:每页 10 / 20 / 50 条可选。
diff --git a/src/prototypes/lease-contract-management/LeaseContractCreate.jsx b/src/prototypes/lease-contract-management/LeaseContractCreate.jsx
index 5211da1..aacc1e1 100644
--- a/src/prototypes/lease-contract-management/LeaseContractCreate.jsx
+++ b/src/prototypes/lease-contract-management/LeaseContractCreate.jsx
@@ -1,6 +1,13 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { Row, Col, message } from 'antd';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Row, Col, Select, message } from 'antd';
+import { Info, Check } from 'lucide-react';
import { DEFAULT_MILEAGE_STANDARD, DEFAULT_FEE_INFO } from '../contract-template-management/contract-template-vars.js';
+import {
+ getPublishedContractTemplateOptions,
+ subscribePublishedContractTemplateOptions,
+ getContractTemplatePreviewBadge,
+ getContractTypeLabel,
+} from '../contract-template-management/contract-template-catalog.js';
import { createDefaultLeaseOrderState, createDefaultPowerOfAttorneyState } from './lease-order-vars.js';
import { buildLeaseContractPreviewHtml } from './lease-contract-preview-build.js';
import {
@@ -12,15 +19,72 @@ import {
isMainContractFormComplete,
isLeaseOrderFormComplete,
isPowerOfAttorneyFormComplete,
+ isPowerOfAttorneySubmitReady,
isPowerOfAttorneyOptionalEmpty,
+ isSealTypeSelected,
+ isLeaseContractFormComplete,
} from './lease-contract-form-validation.js';
-import LeaseContractPreviewPanel from './LeaseContractPreviewPanel.jsx';
+import LeaseContractPreviewPanel, { PreviewEditableHint } from './LeaseContractPreviewPanel.jsx';
import LeaseContractEditorForm, {
LeaseContractLeaseOrderSection,
LeaseContractPowerOfAttorneySection,
} from './LeaseContractEditorForm.jsx';
+var SEAL_TYPE_OPTIONS = [
+ { value: 'contract', label: '合同章' },
+ { value: 'official', label: '公章' },
+ { value: 'legal_person', label: '法人章' },
+ { value: 'finance', label: '财务章' },
+ { value: 'invoice', label: '发票章' },
+];
+
export default function LeaseContractCreate({ onBack }) {
+ var contractTypeState = useState('');
+ var contractType = contractTypeState[0];
+ var setContractType = contractTypeState[1];
+ var contractTemplateIdState = useState('');
+ var contractTemplateId = contractTemplateIdState[0];
+ var setContractTemplateId = contractTemplateIdState[1];
+ var contractTemplateIdRef = useRef('');
+ useEffect(function () {
+ contractTemplateIdRef.current = contractTemplateId;
+ }, [contractTemplateId]);
+ var templateOptionsState = useState(function () { return getPublishedContractTemplateOptions(); });
+ var templateOptions = templateOptionsState[0];
+ var setTemplateOptions = templateOptionsState[1];
+ var templateSelectOptions = useMemo(function () {
+ return templateOptions.map(function (option) {
+ var typeLabel = getContractTypeLabel(option.contractType) || option.contractType || '—';
+ var fileLabel = option.fileName || option.title || option.id;
+ return {
+ value: option.id,
+ label: typeLabel + ' · ' + fileLabel,
+ contractType: option.contractType,
+ };
+ });
+ }, [templateOptions]);
+ function handleTemplateSelect(templateId) {
+ if (!templateId) {
+ setContractTemplateId('');
+ setContractType('');
+ return;
+ }
+ var match = templateOptions.find(function (item) { return item.id === templateId; });
+ setContractTemplateId(templateId);
+ setContractType(match ? match.contractType : '');
+ }
+ useEffect(function () {
+ return subscribePublishedContractTemplateOptions(function () {
+ var next = getPublishedContractTemplateOptions();
+ setTemplateOptions(next);
+ var currentId = contractTemplateIdRef.current;
+ if (currentId && !next.some(function (item) { return item.id === currentId; })) {
+ setContractTemplateId('');
+ setContractType('');
+ message.warning('所选模板已停用或不可用,请重新选择');
+ }
+ });
+ }, []);
var workspaceZoomState = useState(100);
var workspaceZoom = workspaceZoomState[0];
var setWorkspaceZoom = workspaceZoomState[1];
@@ -29,6 +93,8 @@ export default function LeaseContractCreate({ onBack }) {
var mileageState = useState(Object.assign({}, DEFAULT_MILEAGE_STANDARD));
var feeInfoState = useState(Object.assign({}, DEFAULT_FEE_INFO));
var contractCodeState = useState('');
+ var businessDeptState = useState('');
+ var businessOwnerState = useState('');
var leaseOrderState = useState(function () {
var initial = createDefaultLeaseOrderState();
return {
@@ -54,6 +120,10 @@ export default function LeaseContractCreate({ onBack }) {
var setFeeInfo = feeInfoState[1];
var contractCode = contractCodeState[0];
var setContractCode = contractCodeState[1];
+ var businessDept = businessDeptState[0];
+ var setBusinessDept = businessDeptState[1];
+ var businessOwner = businessOwnerState[0];
+ var setBusinessOwner = businessOwnerState[1];
var leaseOrder = leaseOrderState[0];
var setLeaseOrder = leaseOrderState[1];
var powerOfAttorneyState = useState(function () {
@@ -66,12 +136,20 @@ export default function LeaseContractCreate({ onBack }) {
});
var powerOfAttorney = powerOfAttorneyState[0];
var setPowerOfAttorney = powerOfAttorneyState[1];
+ var contractRemarkState = useState('');
+ var contractRemark = contractRemarkState[0];
+ var setContractRemark = contractRemarkState[1];
+ var sealTypesState = useState(['contract']);
+ var sealTypes = sealTypesState[0];
+ var setSealTypes = sealTypesState[1];
var contractApprovalTypeState = useState(STANDARD_CONTRACT_APPROVAL);
var contractApprovalType = contractApprovalTypeState[0];
var setContractApprovalType = contractApprovalTypeState[1];
var previewHtml = useMemo(function () {
+ if (!contractTemplateId) return '';
return buildLeaseContractPreviewHtml({
+ contractTemplateId: contractTemplateId,
lessorId: lessorId,
customerId: customerId,
mileage: mileage,
@@ -80,7 +158,7 @@ export default function LeaseContractCreate({ onBack }) {
leaseOrder: leaseOrder,
powerOfAttorney: powerOfAttorney,
});
- }, [lessorId, customerId, mileage, feeInfo, contractCode, leaseOrder, powerOfAttorney]);
+ }, [contractTemplateId, lessorId, customerId, mileage, feeInfo, contractCode, leaseOrder, powerOfAttorney]);
var handlePreviewContentChange = useCallback(function (mergedHtml) {
setContractApprovalType(resolveContractApprovalType(previewHtml, mergedHtml));
@@ -91,16 +169,20 @@ export default function LeaseContractCreate({ onBack }) {
}, [previewHtml]);
var isNonStandardApproval = contractApprovalType === NONSTANDARD_CONTRACT_APPROVAL;
+ var isTemplateSelected = Boolean(contractTemplateId);
+ var previewBadge = getContractTemplatePreviewBadge(contractTemplateId);
var isMainContractComplete = useMemo(function () {
return isMainContractFormComplete({
lessorId: lessorId,
customerId: customerId,
contractCode: contractCode,
+ businessDept: businessDept,
+ businessOwner: businessOwner,
mileage: mileage,
feeInfo: feeInfo,
});
- }, [lessorId, customerId, contractCode, mileage, feeInfo]);
+ }, [lessorId, customerId, contractCode, businessDept, businessOwner, mileage, feeInfo]);
var isLeaseOrderComplete = useMemo(function () {
return isLeaseOrderFormComplete({ leaseOrder: leaseOrder });
@@ -110,10 +192,182 @@ export default function LeaseContractCreate({ onBack }) {
return isPowerOfAttorneyFormComplete({ powerOfAttorney: powerOfAttorney });
}, [powerOfAttorney]);
+ var isPoaSubmitReady = useMemo(function () {
+ return isPowerOfAttorneySubmitReady({ powerOfAttorney: powerOfAttorney });
+ }, [powerOfAttorney]);
+
var isPowerOfAttorneyEmpty = useMemo(function () {
return isPowerOfAttorneyOptionalEmpty({ powerOfAttorney: powerOfAttorney });
}, [powerOfAttorney]);
+ var isSealTypeComplete = useMemo(function () {
+ return isSealTypeSelected({ sealTypes: sealTypes });
+ }, [sealTypes]);
+
+ var isFormSubmitReady = useMemo(function () {
+ return isLeaseContractFormComplete({
+ lessorId: lessorId,
+ customerId: customerId,
+ contractCode: contractCode,
+ mileage: mileage,
+ feeInfo: feeInfo,
+ leaseOrder: leaseOrder,
+ powerOfAttorney: powerOfAttorney,
+ sealTypes: sealTypes,
+ });
+ }, [lessorId, customerId, contractCode, mileage, feeInfo, leaseOrder, powerOfAttorney, sealTypes]);
+
+ function toggleSealType(value) {
+ setSealTypes(function (current) {
+ var index = current.indexOf(value);
+ if (index >= 0) {
+ var next = current.filter(function (item) { return item !== value; });
+ return next.length ? next : current;
+ }
+ return current.concat([value]);
+ });
+ }
+
+ function getSealTypeLabel(value) {
+ var match = SEAL_TYPE_OPTIONS.find(function (item) { return item.value === value; });
+ return match ? match.label : value;
+ }
+
+ var columnRef = useRef(null);
+ var sectionRefs = {
+ template: useRef(null),
+ main: useRef(null),
+ leaseOrder: useRef(null),
+ poa: useRef(null),
+ seal: useRef(null),
+ };
+
+ var createSteps = useMemo(function () {
+ return [
+ {
+ key: 'template',
+ label: '选择模板',
+ done: isTemplateSelected,
+ optional: false,
+ locked: false,
+ },
+ {
+ key: 'main',
+ label: '主体合同',
+ done: isMainContractComplete,
+ optional: false,
+ locked: !isTemplateSelected,
+ },
+ {
+ key: 'leaseOrder',
+ label: '租赁订单',
+ done: isLeaseOrderComplete,
+ optional: false,
+ locked: !isTemplateSelected,
+ },
+ {
+ key: 'poa',
+ label: '授权委托',
+ done: !isPowerOfAttorneyEmpty && isPowerOfAttorneyComplete,
+ optional: isPowerOfAttorneyEmpty,
+ locked: !isTemplateSelected,
+ },
+ {
+ key: 'seal',
+ label: '用章类型',
+ done: isSealTypeComplete,
+ optional: false,
+ locked: !isTemplateSelected,
+ },
+ ];
+ }, [
+ isTemplateSelected,
+ isMainContractComplete,
+ isLeaseOrderComplete,
+ isPowerOfAttorneyComplete,
+ isPowerOfAttorneyEmpty,
+ isSealTypeComplete,
+ ]);
+
+ var requiredDoneCount = useMemo(function () {
+ var count = 0;
+ if (isTemplateSelected) count += 1;
+ if (isMainContractComplete) count += 1;
+ if (isLeaseOrderComplete) count += 1;
+ if (isPowerOfAttorneyComplete) count += 1;
+ if (isSealTypeComplete) count += 1;
+ return count;
+ }, [isTemplateSelected, isMainContractComplete, isLeaseOrderComplete, isPowerOfAttorneyComplete, isSealTypeComplete]);
+
+ function scrollToCreateSection(stepKey) {
+ var target = sectionRefs[stepKey] && sectionRefs[stepKey].current;
+ if (!target) return;
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+
+ function handleStepNavClick(step) {
+ if (step.locked && step.key !== 'template') {
+ message.info('请先选择合同模板');
+ scrollToCreateSection('template');
+ return;
+ }
+ scrollToCreateSection(step.key);
+ }
+
+ function renderSectionLock(stepKey) {
+ if (isTemplateSelected) return null;
+ return React.createElement('div', {
+ className: 'lc-create-section__lock',
+ role: 'region',
+ 'aria-label': '需先选择合同模板',
+ },
+ React.createElement('p', { className: 'lc-create-section__lock-text' }, '请先完成第 1 步选择合同模板,再填写本节内容。'),
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-btn vm-btn-secondary lc-create-section__lock-btn',
+ onClick: function () { scrollToCreateSection('template'); },
+ }, '前往选择模板'),
+ );
+ }
+
+ function renderStepNav() {
+ return React.createElement('nav', {
+ className: 'lc-create-step-nav',
+ 'aria-label': '填写步骤',
+ 'data-annotation-id': 'lc-create-step-nav',
+ },
+ React.createElement('ol', { className: 'lc-create-step-nav__list' },
+ createSteps.map(function (step, index) {
+ var statusClass = step.done
+ ? 'lc-create-step-nav__item--done'
+ : (step.optional ? 'lc-create-step-nav__item--optional' : 'lc-create-step-nav__item--pending');
+ if (step.locked) statusClass += ' lc-create-step-nav__item--locked';
+ return React.createElement('li', { key: step.key, className: 'lc-create-step-nav__item ' + statusClass },
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-create-step-nav__btn',
+ onClick: function () { handleStepNavClick(step); },
+ 'aria-current': step.key === 'template' && !isTemplateSelected ? 'step' : undefined,
+ },
+ React.createElement('span', { className: 'lc-create-step-nav__index', 'aria-hidden': true },
+ step.done
+ ? React.createElement(Check, { size: 14, strokeWidth: 2.5, 'aria-hidden': true })
+ : String(index + 1),
+ ),
+ React.createElement('span', { className: 'lc-create-step-nav__label' }, step.label),
+ step.optional
+ ? React.createElement('span', { className: 'lc-create-step-nav__tag' }, '选填')
+ : null,
+ ),
+ index < createSteps.length - 1
+ ? React.createElement('span', { className: 'lc-create-step-nav__sep', 'aria-hidden': true })
+ : null,
+ );
+ }),
+ ),
+ );
+ }
+
return React.createElement('div', { className: 'vm-page lc-page lc-create-page ct-page' },
React.createElement('div', { className: 'lc-create-topbar' },
React.createElement('button', {
@@ -121,15 +375,89 @@ export default function LeaseContractCreate({ onBack }) {
className: 'vm-btn vm-btn-back',
onClick: onBack,
}, '返回列表'),
+ React.createElement('div', { className: 'lc-create-topbar__intro' },
+ React.createElement('h1', { className: 'lc-create-topbar__title' }, '新增租赁合同'),
+ React.createElement('p', { className: 'lc-create-topbar__meta', role: 'status' },
+ '必填进度 ',
+ React.createElement('strong', null, requiredDoneCount),
+ ' / 5',
+ isTemplateSelected
+ ? React.createElement('span', { className: 'lc-create-topbar__meta-divider', 'aria-hidden': true }, '·')
+ : null,
+ isTemplateSelected
+ ? React.createElement('span', { className: 'lc-create-topbar__meta-template' }, previewBadge)
+ : React.createElement('span', { className: 'lc-create-topbar__meta-hint' }, '请先选择合同模板'),
+ ),
+ ),
),
React.createElement('div', { className: 'ct-editor-page lc-create-editor-page' },
React.createElement('div', { className: 'ct-editor-workspace' },
React.createElement(Row, { gutter: [16, 16] },
React.createElement(Col, { xs: 24, lg: 12 },
- React.createElement('div', { className: 'lc-create-editor-column' },
- React.createElement('section', { className: 'ct-editor-column-card lc-main-contract-card', 'aria-label': '主体合同信息', 'data-annotation-id': 'lc-create-main-contract' },
+ React.createElement('div', { className: 'lc-create-editor-column', ref: columnRef },
+ renderStepNav(),
+ React.createElement('section', {
+ className: 'ct-editor-column-card lc-template-step-card lc-create-section',
+ 'aria-label': '选择合同类型',
+ 'data-annotation-id': 'lc-create-template',
+ id: 'lc-create-section-template',
+ ref: sectionRefs.template,
+ },
+ React.createElement('div', {
+ className: 'ct-panel-head lc-main-contract-card__head lc-template-step-card__head',
+ 'aria-describedby': 'lc-template-step-hint',
+ },
+ React.createElement('div', { className: 'lc-template-step-card__title-row' },
+ React.createElement('div', { className: 'ct-panel-head__title' }, '第 1 步:选择合同类型'),
+ React.createElement('span', {
+ className: 'lc-form-card__hint',
+ role: 'note',
+ id: 'lc-template-step-hint',
+ },
+ React.createElement(Info, {
+ size: 13,
+ className: 'lc-form-card__hint-icon',
+ 'aria-hidden': true,
+ }),
+ React.createElement('span', { className: 'lc-form-card__hint-text' },
+ '请选择模板管理中已发布且启用的合同模板,右侧将自动生成预览。',
+ ),
+ ),
+ ),
+ React.createElement('span', {
+ className: 'lc-form-completion-badge ' + (isTemplateSelected ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'),
+ role: 'status',
+ 'aria-live': 'polite',
+ }, isTemplateSelected ? '已选择' : '必选'),
+ ),
+ React.createElement('div', { className: 'ct-editor-panel lc-create-editor-panel lc-template-step-panel' },
+ React.createElement(Select, {
+ className: 'lc-template-select',
+ value: contractTemplateId || undefined,
+ placeholder: templateSelectOptions.length ? '请选择合同模板' : '暂无可用模板',
+ allowClear: true,
+ showSearch: true,
+ optionFilterProp: 'label',
+ disabled: !templateSelectOptions.length,
+ onChange: handleTemplateSelect,
+ options: templateSelectOptions,
+ 'aria-label': '合同模板',
+ }),
+ !templateSelectOptions.length
+ ? React.createElement('p', { className: 'lc-template-step-empty' }, '暂无已启用的合同模板,请先在合同模板管理中发布并启用。')
+ : null,
+ ),
+ ),
+ React.createElement('section', {
+ className: 'ct-editor-column-card lc-main-contract-card lc-create-section'
+ + (isTemplateSelected ? '' : ' lc-create-section--disabled'),
+ 'aria-label': '主体合同信息',
+ 'data-annotation-id': 'lc-create-main-contract',
+ id: 'lc-create-section-main',
+ ref: sectionRefs.main,
+ },
React.createElement('div', { className: 'ct-panel-head lc-main-contract-card__head' },
- React.createElement('div', { className: 'ct-panel-head__title' }, '主体合同信息'),
+ React.createElement('div', { className: 'ct-panel-head__title' }, '第 2 步:主体合同信息'),
React.createElement('span', {
className: 'lc-form-completion-badge ' + (isMainContractComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'),
role: 'status',
@@ -141,19 +469,33 @@ export default function LeaseContractCreate({ onBack }) {
lessorId: lessorId,
customerId: customerId,
contractCode: contractCode,
+ businessDept: businessDept,
+ businessOwner: businessOwner,
mileage: mileage,
feeInfo: feeInfo,
onLessorChange: setLessorId,
onCustomerChange: setCustomerId,
onContractCodeChange: setContractCode,
+ onBusinessAssignmentChange: function (dept, owner) {
+ setBusinessDept(dept);
+ setBusinessOwner(owner);
+ },
onMileageChange: setMileage,
onFeeInfoChange: setFeeInfo,
}),
),
+ renderSectionLock('main'),
),
- React.createElement('section', { className: 'ct-editor-column-card', 'aria-label': '附件1:租赁订单', 'data-annotation-id': 'lc-create-lease-order' },
+ React.createElement('section', {
+ className: 'ct-editor-column-card lc-create-section'
+ + (isTemplateSelected ? '' : ' lc-create-section--disabled'),
+ 'aria-label': '附件1:租赁订单',
+ 'data-annotation-id': 'lc-create-lease-order',
+ id: 'lc-create-section-lease-order',
+ ref: sectionRefs.leaseOrder,
+ },
React.createElement('div', { className: 'ct-panel-head' },
- React.createElement('div', { className: 'ct-panel-head__title' }, '附件1:租赁订单'),
+ React.createElement('div', { className: 'ct-panel-head__title' }, '第 3 步:附件1 · 租赁订单'),
React.createElement('span', {
className: 'lc-form-completion-badge ' + (isLeaseOrderComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'),
role: 'status',
@@ -166,15 +508,45 @@ export default function LeaseContractCreate({ onBack }) {
onLeaseOrderChange: setLeaseOrder,
}),
),
+ renderSectionLock('leaseOrder'),
),
- React.createElement('section', { className: 'ct-editor-column-card', 'aria-label': '授权委托书', 'data-annotation-id': 'lc-create-poa' },
- React.createElement('div', { className: 'ct-panel-head' },
- React.createElement('div', { className: 'ct-panel-head__title' }, '授权委托书'),
+ React.createElement('section', {
+ className: 'ct-editor-column-card lc-create-section'
+ + (isTemplateSelected ? '' : ' lc-create-section--disabled'),
+ 'aria-label': '授权委托书',
+ 'data-annotation-id': 'lc-create-poa',
+ id: 'lc-create-section-poa',
+ ref: sectionRefs.poa,
+ },
+ React.createElement('div', {
+ className: 'ct-panel-head lc-create-section__head lc-create-section-poa-head',
+ 'aria-describedby': 'lc-create-poa-hint',
+ },
+ React.createElement('div', { className: 'lc-template-step-card__title-row' },
+ React.createElement('div', { className: 'ct-panel-head__title' }, '第 4 步:授权委托书'),
+ React.createElement('span', {
+ className: 'lc-form-card__hint lc-create-poa-hint',
+ role: 'note',
+ id: 'lc-create-poa-hint',
+ 'data-annotation-id': 'lc-create-poa-hint',
+ },
+ React.createElement(Info, {
+ size: 13,
+ className: 'lc-form-card__hint-icon',
+ 'aria-hidden': true,
+ }),
+ React.createElement('span', { className: 'lc-form-card__hint-text' },
+ '添加授权人须同时上传授权委托书',
+ ),
+ ),
+ ),
React.createElement('span', {
className: 'lc-form-completion-badge '
+ (isPowerOfAttorneyEmpty
? 'lc-form-completion-badge--optional'
- : (isPowerOfAttorneyComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending')),
+ : (isPowerOfAttorneyComplete
+ ? 'lc-form-completion-badge--done'
+ : 'lc-form-completion-badge--pending')),
role: 'status',
'aria-live': 'polite',
}, isPowerOfAttorneyEmpty ? '选填' : (isPowerOfAttorneyComplete ? '已完成' : '未完成')),
@@ -183,8 +555,65 @@ export default function LeaseContractCreate({ onBack }) {
React.createElement(LeaseContractPowerOfAttorneySection, {
powerOfAttorney: powerOfAttorney,
onPowerOfAttorneyChange: setPowerOfAttorney,
+ contractRemark: contractRemark,
+ onContractRemarkChange: setContractRemark,
}),
),
+ renderSectionLock('poa'),
+ ),
+ React.createElement('section', {
+ className: 'ct-editor-column-card lc-create-section'
+ + (isTemplateSelected ? '' : ' lc-create-section--disabled'),
+ 'aria-label': '用章类型',
+ 'data-annotation-id': 'lc-create-seal',
+ id: 'lc-create-section-seal',
+ ref: sectionRefs.seal,
+ },
+ React.createElement('div', {
+ className: 'ct-panel-head lc-create-section__head lc-create-section-seal-head',
+ },
+ React.createElement('div', { className: 'lc-template-step-card__title-row' },
+ React.createElement('div', { className: 'ct-panel-head__title' }, '第 5 步:用章类型'),
+ sealTypes.length
+ ? React.createElement('span', {
+ className: 'lc-seal-type-head-tags',
+ 'aria-label': '已选用章',
+ },
+ sealTypes.map(function (value) {
+ return React.createElement('span', {
+ key: value,
+ className: 'lc-seal-type-head-tag',
+ }, getSealTypeLabel(value));
+ }),
+ )
+ : null,
+ ),
+ React.createElement('span', {
+ className: 'lc-form-completion-badge ' + (isSealTypeComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'),
+ role: 'status',
+ 'aria-live': 'polite',
+ }, isSealTypeComplete ? '已选择' : '必选'),
+ ),
+ React.createElement('div', { className: 'ct-editor-panel lc-create-editor-panel' },
+ React.createElement('div', {
+ className: 'lc-seal-type-chips',
+ role: 'group',
+ 'aria-label': '用章类型',
+ 'data-annotation-id': 'lc-create-seal-type',
+ },
+ SEAL_TYPE_OPTIONS.map(function (option) {
+ var active = sealTypes.indexOf(option.value) >= 0;
+ return React.createElement('button', {
+ key: option.value,
+ type: 'button',
+ className: 'lc-seal-type-chip' + (active ? ' is-active' : ''),
+ 'aria-pressed': active,
+ onClick: function () { toggleSealType(option.value); },
+ }, option.label);
+ }),
+ ),
+ ),
+ renderSectionLock('seal'),
),
),
),
@@ -194,9 +623,13 @@ export default function LeaseContractCreate({ onBack }) {
'aria-label': '实时预览',
'data-annotation-id': 'lc-create-preview',
},
- React.createElement('div', { className: 'ct-panel-head' },
- React.createElement('div', { className: 'ct-panel-head__title' }, '实时预览',
- React.createElement('span', { className: 'ct-panel-head__badge' }, 'V26.6 完整合同'),
+ React.createElement('div', {
+ className: 'ct-panel-head lc-create-preview-head',
+ 'aria-describedby': isTemplateSelected ? 'lc-preview-editable-hint' : undefined,
+ },
+ React.createElement('div', { className: 'lc-template-step-card__title-row' },
+ React.createElement('div', { className: 'ct-panel-head__title' }, '实时预览'),
+ isTemplateSelected ? React.createElement(PreviewEditableHint) : null,
),
React.createElement('div', { className: 'ct-panel-head__actions' },
React.createElement('span', {
@@ -208,13 +641,15 @@ export default function LeaseContractCreate({ onBack }) {
),
),
React.createElement('div', { className: 'ct-preview-panel ct-preview-panel--word' },
- React.createElement(LeaseContractPreviewPanel, {
- html: previewHtml,
- zoom: workspaceZoom,
- onZoomChange: setWorkspaceZoom,
- editable: true,
- onContentChange: handlePreviewContentChange,
- }),
+ isTemplateSelected
+ ? React.createElement(LeaseContractPreviewPanel, {
+ html: previewHtml,
+ zoom: workspaceZoom,
+ onZoomChange: setWorkspaceZoom,
+ editable: true,
+ onContentChange: handlePreviewContentChange,
+ })
+ : React.createElement('div', { className: 'lc-template-preview-empty' }, '请先在左侧选择合同模板,右侧将自动生成对应预览。'),
),
),
),
@@ -231,14 +666,31 @@ export default function LeaseContractCreate({ onBack }) {
type: 'button',
className: 'vm-btn vm-btn-secondary',
onClick: function () {
- message.success('已保存草稿(' + contractApprovalType + ',原型)');
+ if (!contractTemplateId) {
+ message.warning('请先选择合同模板');
+ return;
+ }
+ message.success('已保存草稿(' + getContractTypeLabel(contractType) + ' · ' + contractApprovalType + ',原型)');
},
}, '保存草稿'),
React.createElement('button', {
type: 'button',
className: 'vm-btn vm-btn-primary',
onClick: function () {
- message.info('提交审核:' + contractApprovalType + '(原型)');
+ if (!contractTemplateId) {
+ message.warning('请先选择合同模板');
+ return;
+ }
+ if (!isFormSubmitReady) {
+ if (!isPoaSubmitReady) {
+ message.warning('提交审核前须完成授权委托书填写');
+ scrollToCreateSection('poa');
+ return;
+ }
+ message.warning('请先完成左侧必填项');
+ return;
+ }
+ message.info('提交审核:' + getContractTypeLabel(contractType) + ' · ' + contractApprovalType + '(原型)');
},
}, '提交审核'),
),
diff --git a/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx b/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx
index 6259210..3b372a9 100644
--- a/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx
+++ b/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx
@@ -53,6 +53,13 @@ import {
formatKpiAmount,
getExtraServiceNames,
} from './lease-order-vars.js';
+import {
+ LEASE_BUSINESS_ORG_TREE,
+ flattenBusinessOwnerOptions,
+ matchBusinessOwnerOption,
+ formatBusinessAssignmentLabel,
+ getBusinessOwnersByDept,
+} from './lease-business-org-data.js';
import { getMatchedPrototypeClauses } from './lease-contract-vehicle-clause.js';
function FieldHint(props) {
@@ -63,6 +70,20 @@ function FieldHint(props) {
}, props.children);
}
+function aggregateOrderBrandModels(rows) {
+ var seen = {};
+ var result = [];
+ (rows || []).forEach(function (row) {
+ normalizeBrandModels(row).forEach(function (pair) {
+ var key = pair[0] + '\0' + pair[1];
+ if (seen[key]) return;
+ seen[key] = true;
+ result.push(pair);
+ });
+ });
+ return result;
+}
+
function BrandModelsClauseHint(props) {
var brandModels = normalizeBrandModels({ brandModels: props.brandModels });
if (!brandModels.length) return null;
@@ -286,9 +307,6 @@ function LeaseOrderServiceContentField(props) {
var query = queryState[0];
var setQuery = queryState[1];
var serviceNames = getExtraServiceNames(selected);
- var summary = serviceNames.length <= 2
- ? serviceNames.join('、')
- : (serviceNames.length > 0 ? '已选 ' + serviceNames.length + ' 项' : '');
var searchText = query.trim();
var isSearchMode = searchText.length > 0;
var resolvedCategory = activeCategory || LEASE_EXTRA_SERVICE_TREE[0].category;
@@ -459,7 +477,13 @@ function LeaseOrderServiceContentField(props) {
),
);
- return React.createElement('div', { className: 'vm-operate-city-field lc-lease-order-service-picker' },
+ return React.createElement('div', { className: 'lc-lease-order-service-field' },
+ selected.length > 0
+ ? React.createElement('p', {
+ className: 'lc-lease-order-service-field__summary',
+ 'aria-live': 'polite',
+ }, serviceNames.join('、'))
+ : null,
React.createElement(Popover, {
trigger: 'click',
placement: 'bottomLeft',
@@ -469,34 +493,200 @@ function LeaseOrderServiceContentField(props) {
getPopupContainer: function () { return document.body; },
content: popoverContent,
},
- React.createElement('div', {
- className: 'vm-filter-picker-control lc-lease-order-service-picker__trigger' + (open ? ' open' : ''),
- role: 'combobox',
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-lease-order-service-picker__btn',
'aria-label': '服务内容',
'aria-expanded': open,
- tabIndex: 0,
'data-annotation-id': 'lc-lease-order-service-content',
- onKeyDown: function (e) {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- setOpen(true);
- }
+ }, '选择服务项'),
+ ),
+ );
+}
+
+function BusinessDeptOwnerPickerField(props) {
+ var businessDept = props.businessDept || '';
+ var businessOwner = props.businessOwner || '';
+ var onChange = props.onChange;
+ var openState = React.useState(false);
+ var deptState = React.useState('');
+ var queryState = React.useState('');
+ var open = openState[0];
+ var setOpen = openState[1];
+ var activeDept = deptState[0];
+ var setActiveDept = deptState[1];
+ var query = queryState[0];
+ var setQuery = queryState[1];
+ var searchText = query.trim();
+ var isSearchMode = searchText.length > 0;
+ var resolvedDept = activeDept || businessDept || LEASE_BUSINESS_ORG_TREE[0].dept;
+ var displayOptions = isSearchMode
+ ? flattenBusinessOwnerOptions().filter(function (item) {
+ return matchBusinessOwnerOption(item, searchText);
+ })
+ : getBusinessOwnersByDept(resolvedDept).map(function (owner) {
+ return { dept: resolvedDept, owner: owner };
+ });
+
+ function resolveInitialDept() {
+ if (businessDept) return businessDept;
+ return LEASE_BUSINESS_ORG_TREE[0].dept;
+ }
+
+ function handleOpenChange(nextOpen) {
+ setOpen(nextOpen);
+ if (nextOpen) {
+ setActiveDept(resolveInitialDept());
+ } else {
+ setQuery('');
+ }
+ }
+
+ function selectOwner(dept, owner) {
+ onChange(dept, owner);
+ setActiveDept(dept);
+ setOpen(false);
+ setQuery('');
+ }
+
+ function clearSelection(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ onChange('', '');
+ setQuery('');
+ }
+
+ function clearSearch(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ setQuery('');
+ }
+
+ var summary = formatBusinessAssignmentLabel(businessDept, businessOwner);
+
+ var popoverContent = React.createElement('div', {
+ className: 'vm-operate-city-edit-body vm-operate-city-picker-panel lc-lease-order-service-popover__panel',
+ },
+ React.createElement('label', { className: 'vm-operate-city-edit-search' },
+ React.createElement(Search, { size: 16, 'aria-hidden': true, className: 'vm-operate-city-edit-search-icon' }),
+ React.createElement('input', {
+ type: 'text',
+ className: 'vm-operate-city-edit-search-input',
+ value: query,
+ placeholder: '输入业务员姓名快速筛选',
+ 'aria-label': '搜索业务员姓名',
+ autoComplete: 'off',
+ onChange: function (e) { setQuery(e.target.value); },
+ }),
+ query
+ ? React.createElement('button', {
+ type: 'button',
+ className: 'vm-operate-city-edit-search-clear',
+ onClick: clearSearch,
+ 'aria-label': '清空搜索',
+ }, React.createElement(X, { size: 14, 'aria-hidden': true }))
+ : null,
+ ),
+ summary
+ ? React.createElement('div', { className: 'vm-operate-city-edit-selected', 'aria-live': 'polite' },
+ React.createElement('span', { className: 'vm-operate-city-edit-selected-label' }, '已选'),
+ React.createElement('div', { className: 'lc-lease-order-service-popover__selected-tags' },
+ React.createElement('span', { className: 'lc-lease-order-service-popover__selected-tag' },
+ summary,
+ React.createElement('button', {
+ type: 'button',
+ 'aria-label': '清除业务部门及业务人员',
+ onMouseDown: function (e) { e.preventDefault(); },
+ onClick: clearSelection,
+ }, React.createElement(X, { size: 12, 'aria-hidden': true })),
+ ),
+ ),
+ )
+ : null,
+ React.createElement('div', {
+ className: 'vm-operate-city-edit-main' + (isSearchMode ? ' is-search' : ''),
+ },
+ !isSearchMode
+ ? React.createElement('div', {
+ className: 'vm-operate-city-edit-step vm-operate-city-edit-step--province',
},
+ React.createElement('span', { className: 'vm-operate-city-step-label' }, '业务部门'),
+ React.createElement('div', {
+ className: 'vm-operate-city-edit-chips',
+ role: 'tablist',
+ 'aria-label': '业务部门',
+ },
+ LEASE_BUSINESS_ORG_TREE.map(function (group) {
+ var active = resolvedDept === group.dept;
+ return React.createElement('button', {
+ key: group.dept,
+ type: 'button',
+ role: 'tab',
+ 'aria-selected': active,
+ className: 'vm-operate-city-chip' + (active ? ' active' : ''),
+ onClick: function () {
+ setActiveDept(group.dept);
+ setQuery('');
+ },
+ }, group.dept);
+ }),
+ ),
+ )
+ : null,
+ React.createElement('div', {
+ className: 'vm-operate-city-edit-step vm-operate-city-edit-step--city',
},
- React.createElement('span', {
- className: 'vm-filter-picker-input lc-lease-order-service-picker__summary'
- + (summary ? '' : ' is-placeholder'),
- }, summary || '请选择服务内容'),
- selected.length > 0
- ? React.createElement('button', {
- type: 'button',
- className: 'vm-filter-picker-clear',
- onClick: clearAll,
- 'aria-label': '清空服务内容',
- tabIndex: -1,
- }, React.createElement(X, { size: 14, 'aria-hidden': true }))
- : null,
- React.createElement(ChevronDown, { size: 14, className: 'vm-filter-picker-chevron', 'aria-hidden': true }),
+ React.createElement('span', { className: 'vm-operate-city-step-label' },
+ isSearchMode ? ('搜索结果(' + displayOptions.length + ')') : '业务人员',
+ ),
+ React.createElement('div', {
+ className: 'vm-operate-city-edit-chips',
+ role: 'listbox',
+ 'aria-label': '业务人员列表',
+ },
+ displayOptions.length === 0
+ ? React.createElement('p', { className: 'vm-operate-city-empty' },
+ isSearchMode ? '未找到匹配的业务人员,请换个关键词' : '该部门暂无可选业务人员',
+ )
+ : displayOptions.map(function (item) {
+ var checked = businessDept === item.dept && businessOwner === item.owner;
+ return React.createElement('button', {
+ key: item.dept + '::' + item.owner,
+ type: 'button',
+ role: 'option',
+ 'aria-selected': checked,
+ className: 'vm-operate-city-chip' + (checked ? ' active' : ''),
+ onClick: function () { selectOwner(item.dept, item.owner); },
+ }, item.owner);
+ }),
+ ),
+ ),
+ ),
+ );
+
+ return React.createElement('div', { className: 'lc-business-dept-picker' },
+ React.createElement(Popover, {
+ trigger: 'click',
+ placement: 'bottomLeft',
+ open: open,
+ onOpenChange: handleOpenChange,
+ overlayClassName: 'lc-lease-order-service-popover lc-business-dept-picker-popover',
+ getPopupContainer: function () { return document.body; },
+ content: popoverContent,
+ },
+ React.createElement('button', {
+ type: 'button',
+ className: 'vm-filter-picker-control lc-business-dept-picker__trigger' + (open ? ' open' : ''),
+ 'aria-label': '业务部门及业务人员',
+ 'aria-expanded': open,
+ 'data-annotation-id': 'lc-signing-business-assignment',
+ },
+ summary
+ ? React.createElement('span', { className: 'vm-filter-picker-input lc-business-dept-picker__summary' }, summary)
+ : React.createElement('span', {
+ className: 'vm-filter-picker-input lc-business-dept-picker__summary is-placeholder',
+ }, '请选择业务部门及业务人员'),
+ React.createElement(ChevronDown, { size: 16, className: 'vm-filter-picker-chevron', 'aria-hidden': true }),
),
),
);
@@ -977,11 +1167,14 @@ function LeaseOrderEditor(props) {
),
React.createElement('span', { className: 'lc-form-inline-sentence__text' }, '辆'),
),
- insuredCountEditable
- ? React.createElement(FieldHint, { annotationId: 'lc-lease-order-insured-count-hint' }, '车牌号为「以实际交付为准」时,可手动填写车辆数量(正整数)。')
- : null,
),
- React.createElement(FormField, { label: '保险金额/辆' },
+ React.createElement(FormField, {
+ label: '保险金额/辆',
+ labelHint: React.createElement(FormFieldLabelHint, {
+ annotationId: 'lc-lease-order-third-party-hint',
+ text: '仅记录保额并同步至右侧预览,不计入本卡片及其他费用计算。',
+ }),
+ },
React.createElement('div', { className: 'lc-form-inline-sentence' },
React.createElement('span', { className: 'lc-form-inline-sentence__text' }, '三者责任险'),
React.createElement(InputNumber, {
@@ -997,9 +1190,15 @@ function LeaseOrderEditor(props) {
}),
React.createElement('span', { className: 'lc-form-inline-sentence__text' }, '万元、车损'),
),
- React.createElement(FieldHint, { annotationId: 'lc-lease-order-third-party-hint' }, '三者责任险仅用于记录保额,不计入费用计算;填写后会同步显示在右侧附件一「三者责任险【】万元」中。'),
),
),
+ React.createElement('div', {
+ className: 'lc-lease-order-editor__clause-hint',
+ id: 'lc-brand-clause-hint-order',
+ 'data-annotation-id': 'lc-brand-clause-hint-order',
+ },
+ React.createElement(BrandModelsClauseHint, { brandModels: aggregateOrderBrandModels(rows) }),
+ ),
React.createElement('div', { className: 'lc-lease-order-editor__table-wrap', role: 'region', 'aria-label': '租赁订单车辆列表' },
React.createElement('table', { className: 'lc-lease-order-editor__table' },
React.createElement('thead', null,
@@ -1009,7 +1208,7 @@ function LeaseOrderEditor(props) {
React.createElement('th', { rowSpan: 2, scope: 'col', className: 'lc-lease-order-editor__th-subtotal' }, '租金及服务费小计(元/月/辆,含税)'),
React.createElement('th', { colSpan: 3, scope: 'colgroup', className: 'lc-lease-order-editor__th-group' }, '价格及服务内容'),
React.createElement('th', { rowSpan: 2, scope: 'col' }, '保证金(元/辆)'),
- React.createElement('th', { rowSpan: 2, scope: 'col', className: 'lc-lease-order-editor__th-period' }, '车辆租赁期限'),
+ React.createElement('th', { rowSpan: 2, scope: 'col', className: 'lc-lease-order-editor__th-period' }, '租赁期限(月)'),
React.createElement('th', { rowSpan: 2, scope: 'col', className: 'lc-lease-order-editor__actions-col' }, '操作'),
),
React.createElement('tr', { className: 'lc-lease-order-editor__head-row lc-lease-order-editor__head-row--sub' },
@@ -1025,9 +1224,6 @@ function LeaseOrderEditor(props) {
var plateNos = normalizePlateNos(row);
var vehicleCount = getRowVehicleCountForPricing(row, order);
var subtotal = calcRentServiceSubtotal(row.rent, row.serviceFee, vehicleCount);
- var leasePeriodValue = row.leasePeriodStart && row.leasePeriodEnd
- ? [dayjs(row.leasePeriodStart), dayjs(row.leasePeriodEnd)]
- : null;
return React.createElement('tr', { key: row.id },
React.createElement('td', { className: 'lc-lease-order-editor__brand-cell' },
React.createElement(LeaseOrderBrandModelField, {
@@ -1036,9 +1232,6 @@ function LeaseOrderEditor(props) {
updateRow(row.id, { brandModels: values || [] });
},
}),
- React.createElement('div', { id: 'lc-brand-clause-hint-' + row.id },
- React.createElement(BrandModelsClauseHint, { brandModels: brandModels }),
- ),
),
React.createElement('td', null,
React.createElement(LeaseOrderPlateNoField, {
@@ -1113,19 +1306,24 @@ function LeaseOrderEditor(props) {
}),
),
),
- React.createElement('td', null,
- React.createElement(DatePicker.RangePicker, {
- className: 'lc-form-datepicker vm-focus-border',
- style: { width: '100%' },
- placeholder: ['开始日期', '结束日期'],
- value: leasePeriodValue,
- onChange: function (_dates, dateStrings) {
+ React.createElement('td', { className: 'lc-lease-order-editor__period-cell' },
+ React.createElement(InputNumber, {
+ className: 'lc-form-number lc-lease-order-period-input vm-focus-border',
+ controls: false,
+ min: 1,
+ step: 1,
+ precision: 0,
+ value: row.leasePeriodMonths,
+ onChange: function (v) {
updateRow(row.id, {
- leasePeriodStart: dateStrings && dateStrings[0] ? dateStrings[0] : null,
- leasePeriodEnd: dateStrings && dateStrings[1] ? dateStrings[1] : null,
+ leasePeriodMonths: v,
+ leasePeriodStart: null,
+ leasePeriodEnd: null,
});
},
+ placeholder: '月数',
'aria-label': '车辆租赁期限',
+ 'data-annotation-id': 'lc-lease-order-lease-period',
}),
),
React.createElement('td', { className: 'lc-lease-order-editor__actions-col' },
@@ -1140,7 +1338,13 @@ function LeaseOrderEditor(props) {
),
),
React.createElement('div', { className: 'lc-lease-order-editor__delivery lc-form-grid lc-form-grid--pair' },
- React.createElement(FormField, { label: '车辆交付(交还)地点' },
+ React.createElement(FormField, {
+ label: '车辆交付(交还)地点',
+ labelHint: React.createElement(FormFieldLabelHint, {
+ annotationId: 'lc-lease-order-delivery-region-hint',
+ text: '交还地点决定由哪个区域运维负责交车与还车。',
+ }),
+ },
React.createElement(Cascader, {
className: 'lc-form-cascader vm-focus-border',
options: PROVINCE_CITY_CASCADER_OPTIONS,
@@ -1151,9 +1355,14 @@ function LeaseOrderEditor(props) {
'aria-label': '车辆交付交还地点',
'data-annotation-id': 'lc-lease-order-delivery-region',
}),
- React.createElement(FieldHint, { annotationId: 'lc-lease-order-delivery-region-hint' }, '交付、交还地点决定由哪个区域的运维负责向客户交车;若与运维用户权限省市重合,则由该地区运维负责交车。'),
),
- React.createElement(FormField, { label: '车辆交付时间' },
+ React.createElement(FormField, {
+ label: '车辆交付时间',
+ labelHint: React.createElement(FormFieldLabelHint, {
+ annotationId: 'lc-lease-order-delivery-date-hint',
+ text: '用于生成交车任务时,提示运维何时交车。',
+ }),
+ },
React.createElement(DatePicker, {
className: 'lc-form-datepicker vm-focus-border',
style: { width: '100%' },
@@ -1165,7 +1374,6 @@ function LeaseOrderEditor(props) {
'aria-label': '车辆交付时间',
'data-annotation-id': 'lc-lease-order-delivery-date',
}),
- React.createElement(FieldHint, { annotationId: 'lc-lease-order-delivery-date-hint' }, '用于生成交车任务时,提示运维何时交车。'),
),
),
);
@@ -1210,9 +1418,6 @@ function PowerOfAttorneyEditor(props) {
var hasStartedDelegate = delegates.some(isDelegateRowStarted);
return React.createElement('div', { className: 'lc-poa-editor' },
- React.createElement(FieldHint, { annotationId: 'lc-create-poa-hint' },
- '授权委托书为选填;如未填写,提交审核后须在交车前完成上传。可从合同列表「添加授权委托书」补充委托人信息并自动衔接入合同。填写姓名、联系方式、证件号码任一字段后,其余两项为必填;最多 ' + POA_MAX_DELEGATES + ' 个委托人,信息将同步至右侧授权委托书。',
- ),
delegates.map(function (row, index) {
var started = isDelegateRowStarted(row);
return React.createElement('div', {
@@ -1297,12 +1502,38 @@ function CustomerCredentialThumb(props) {
);
}
+function FormFieldLabelHint(props) {
+ if (!props.text) return null;
+
+ return React.createElement('span', {
+ className: 'lc-form-card__hint lc-form-field__inline-hint',
+ role: 'note',
+ id: props.id,
+ 'data-annotation-id': props.annotationId || undefined,
+ },
+ React.createElement(Info, {
+ size: 13,
+ className: 'lc-form-card__hint-icon',
+ 'aria-hidden': true,
+ }),
+ React.createElement('span', { className: 'lc-form-card__hint-text' }, props.text),
+ );
+}
+
function FormField(props) {
+ var labelContent = [
+ props.required ? React.createElement('span', { className: 'lc-form-field__required', 'aria-hidden': 'true' }, '*') : null,
+ props.label,
+ ];
+ var labelEl = props.labelHint
+ ? React.createElement('span', { className: 'lc-form-field__label-row' },
+ React.createElement('span', { className: 'lc-form-field__label' }, labelContent),
+ props.labelHint,
+ )
+ : React.createElement('span', { className: 'lc-form-field__label' }, labelContent);
+
return React.createElement('label', { className: 'lc-form-field' },
- React.createElement('span', { className: 'lc-form-field__label' },
- props.required ? React.createElement('span', { className: 'lc-form-field__required', 'aria-hidden': 'true' }, '*') : null,
- props.label,
- ),
+ labelEl,
props.children,
);
}
@@ -1397,11 +1628,14 @@ export default function LeaseContractEditorForm(props) {
var lessorId = props.lessorId;
var customerId = props.customerId;
var contractCode = props.contractCode || '';
+ var businessDept = props.businessDept || '';
+ var businessOwner = props.businessOwner || '';
var mileage = props.mileage || DEFAULT_MILEAGE_STANDARD;
var feeInfo = props.feeInfo || DEFAULT_FEE_INFO;
var onLessorChange = props.onLessorChange;
var onCustomerChange = props.onCustomerChange;
var onContractCodeChange = props.onContractCodeChange;
+ var onBusinessAssignmentChange = props.onBusinessAssignmentChange;
var onMileageChange = props.onMileageChange;
var onFeeInfoChange = props.onFeeInfoChange;
@@ -1457,10 +1691,28 @@ export default function LeaseContractEditorForm(props) {
'aria-label': '合同编号',
}),
),
+ React.createElement(FormField, {
+ label: '业务部门 / 业务人员',
+ required: true,
+ },
+ React.createElement(BusinessDeptOwnerPickerField, {
+ businessDept: businessDept,
+ businessOwner: businessOwner,
+ onChange: function (dept, owner) {
+ if (onBusinessAssignmentChange) onBusinessAssignmentChange(dept, owner);
+ },
+ }),
+ ),
React.createElement('div', { className: 'lc-form-grid lc-form-grid--pair' },
React.createElement(FormField, {
label: '甲方',
required: true,
+ labelHint: lessorId
+ ? React.createElement(FormFieldLabelHint, {
+ annotationId: 'lc-lessor-select-hint',
+ text: '将使用该公司用章进行电子签章',
+ })
+ : null,
},
React.createElement(Select, {
className: 'lc-form-select vm-focus-border',
@@ -1733,11 +1985,32 @@ export function LeaseContractLeaseOrderSection(props) {
export function LeaseContractPowerOfAttorneySection(props) {
var powerOfAttorney = props.powerOfAttorney;
var onPowerOfAttorneyChange = props.onPowerOfAttorneyChange;
+ var contractRemark = props.contractRemark;
+ var onContractRemarkChange = props.onContractRemarkChange;
return React.createElement('div', { className: 'lc-create-editor-scroll lc-create-editor-scroll--section' },
React.createElement(PowerOfAttorneyEditor, {
poa: powerOfAttorney || createDefaultPowerOfAttorneyState(),
onChange: onPowerOfAttorneyChange,
}),
+ React.createElement('div', {
+ className: 'lc-contract-remark',
+ 'data-annotation-id': 'lc-create-contract-remark',
+ },
+ React.createElement(FormField, { label: '合同备注' },
+ React.createElement(Input.TextArea, {
+ className: 'lc-contract-remark__textarea vm-focus-border',
+ rows: 4,
+ maxLength: 500,
+ showCount: true,
+ value: contractRemark || '',
+ onChange: function (e) {
+ if (onContractRemarkChange) onContractRemarkChange(e.target.value);
+ },
+ placeholder: '请输入合同备注(选填)',
+ 'aria-label': '合同备注',
+ }),
+ ),
+ ),
);
}
diff --git a/src/prototypes/lease-contract-management/LeaseContractManagement.jsx b/src/prototypes/lease-contract-management/LeaseContractManagement.jsx
index 38be833..f41cf69 100644
--- a/src/prototypes/lease-contract-management/LeaseContractManagement.jsx
+++ b/src/prototypes/lease-contract-management/LeaseContractManagement.jsx
@@ -2,11 +2,40 @@
// 设计规范:oneos-app.css · 样式前缀 lc-*
import LeaseContractCreate from './LeaseContractCreate.jsx';
+import { StatusTag } from '../vehicle-management/components/StatusTag';
import {
LEASE_CONTRACT_LIST_RECORDS,
- formatMileageSummary,
+ enrichLeaseContractListRecords,
formatDeliveryRegion,
+ formatPaymentPeriodLabel,
+ formatHydrogenPaymentLabel,
+ formatContractApprovalTypeLabel,
+ getAuthorizedDelegates,
+ formatContractHandoverMileage,
+ formatContractHandoverDateTimeMinute,
+ isContractVehicleDelivered,
+ isContractVehicleReturned,
+ canContractVehicleReturn,
+ formatContractMileageTargetKm,
+ getContractMileagePeriodTag,
+ formatContractVehicleCurrentMileage,
+ resolveContractVehicleMileageSource,
+ getContractVehicleMileageSourceTagClass,
+ formatContractVehicleRemainingMileage,
+ formatContractPeriodStartDate,
+ computeContractOverallMileageProgress,
+ computeContractMileageForecastStatus,
+ getContractMileageForecastStatusTone,
+ resolveContractTemplateTypeLabel,
+ getReturnSettlementStatusTone,
+ getCurrentApproverLabel,
+ shouldHideCurrentApprover,
+ DELIVERY_DATE_TBD_LABEL,
+ isDeliveryDateTbd,
+ formatContractDeliveryDateLabel,
+ resolveContractStatusByVehicleLease,
} from './lease-contract-list-data.js';
+import { getLessorCompanies } from '../contract-template-management/contract-template-vars.js';
const Component = function(props) {
var useEffect = React.useEffect;
@@ -36,6 +65,7 @@ const Component = function(props) {
var Upload = antd.Upload;
var message = antd.message;
var App = antd.App;
+ var Checkbox = antd.Checkbox;
var RangePicker = DatePicker.RangePicker;
@@ -75,6 +105,10 @@ const Component = function(props) {
var _vehiclePopoverRecord = useState(null);
var _deliveredPopoverRecord = useState(null);
+ var _deliveryChangePopoverRecord = useState(null);
+ var _deliveryChangeDraftDate = useState(null);
+ var _deliveryChangeDraftTbd = useState(false);
+ var _delegatePopoverRecord = useState(null);
var _authorizedModalVisible = useState(false);
var _authorizedModalRecord = useState(null);
var _authorizedList = useState([{ name: '', phone: '', idCard: '' }]);
@@ -87,12 +121,13 @@ const Component = function(props) {
var _withdrawModalRecord = useState(null);
var _terminateModalVisible = useState(false);
var _terminateModalRecord = useState(null);
- var _requirementModalVisible = useState(false);
var _stampModalVisible = useState(false);
var _stampModalRecord = useState(null);
var _stampFileList = useState([]);
// 上传盖章合同完成后,按合同 id 记录已上传(原型:与列表 legalStampedContractUploaded 合并判断)
var _stampedUploadedOverride = useState({});
+ // 撤回等列表行状态覆盖(原型)
+ var _recordOverrides = useState({});
useEffect(function() {
if (props && props.onPageViewChange) {
@@ -135,19 +170,18 @@ const Component = function(props) {
{ value: 'c2', label: '上海某某运输公司' },
{ value: 'c3', label: '杭州某某租赁有限公司' }
];
- var signingCompanyOptions = [
- { value: '嘉兴羚牛', label: '嘉兴羚牛' },
- { value: '上海羚牛', label: '上海羚牛' },
- { value: '广东羚牛', label: '广东羚牛' }
- ];
- // 审批状态:待审批、审批中、审批通过、审批驳回、未提交(无已结束,已结束归属合同状态)
+ var signingCompanyOptions = getLessorCompanies().map(function (c) {
+ return { value: c.shortName, label: c.legalName };
+ });
+ // 审批状态:未提交、待审批、审批中、审批通过、审批驳回、撤回
var approvalStatusOptions = [
{ value: '全部', label: '全部' },
+ { value: '未提交', label: '未提交' },
{ value: '待审批', label: '待审批' },
{ value: '审批中', label: '审批中' },
{ value: '审批通过', label: '审批通过' },
{ value: '审批驳回', label: '审批驳回' },
- { value: '未提交', label: '未提交' }
+ { value: '撤回', label: '撤回' },
];
// 合同状态:草稿、变更、合同进行中、到期合同、已提交审批、已结束
var contractStatusOptions = [
@@ -166,8 +200,8 @@ const Component = function(props) {
];
var contractApprovalTypeOptions = [
{ value: '全部', label: '全部' },
- { value: '标准合同审批', label: '标准合同审批' },
- { value: '非标准合同审批', label: '非标准合同审批' }
+ { value: '标准合同', label: '标准合同' },
+ { value: '非标准合同', label: '非标准合同' }
];
var deptOptions = [
{ value: '业务1部', label: '业务1部' },
@@ -183,7 +217,7 @@ const Component = function(props) {
];
// 列表样例:字段与新增页主体合同 / 租赁订单 / 授权委托书对齐
- var rawList = LEASE_CONTRACT_LIST_RECORDS;
+ var rawList = enrichLeaseContractListRecords(LEASE_CONTRACT_LIST_RECORDS);
var KPI_CARDS = [
{ key: 'all', title: '全部合同', desc: '显示全部合同' },
@@ -302,7 +336,13 @@ const Component = function(props) {
}, [rawList]);
var filteredList = useMemo(function() {
- var list = rawList.slice();
+ var overrides = _recordOverrides[0] || {};
+ var list = rawList.map(function (r) {
+ var merged = overrides[r.id] ? Object.assign({}, r, overrides[r.id]) : r;
+ return Object.assign({}, merged, {
+ contractStatus: resolveContractStatusByVehicleLease(merged),
+ });
+ });
var f = appliedFilter;
if (f.contractCode) {
list = list.filter(function(r) {
@@ -338,7 +378,9 @@ const Component = function(props) {
}
var capType = f.contractApprovalType;
if (capType && capType.length > 0 && capType.indexOf('全部') === -1) {
- list = list.filter(function(r) { return capType.indexOf(r.contractApprovalType) !== -1; });
+ list = list.filter(function(r) {
+ return capType.indexOf(formatContractApprovalTypeLabel(r.contractApprovalType)) !== -1;
+ });
}
if (f.creator && f.creator.length > 0) {
list = list.filter(function(r) { return f.creator.indexOf(r.creator) !== -1; });
@@ -365,7 +407,7 @@ const Component = function(props) {
list = list.filter(function(r) { return matchContractKpi(r, _kpiTab[0]); });
}
return list;
- }, [rawList, appliedFilter, _kpiTab[0]]);
+ }, [rawList, appliedFilter, _kpiTab[0], _recordOverrides[0]]);
var totalCount = filteredList.length;
var totalPages = Math.max(1, Math.ceil(totalCount / _pageSize[0]));
@@ -620,22 +662,112 @@ const Component = function(props) {
}, []);
- // 租赁车辆数气泡:不含实际交车日期
- var vehicleColumnsRental = [
- { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 180 },
- { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 },
- { title: '型号', dataIndex: 'model', key: 'model', width: 100 },
- { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }
- ];
- // 已交车辆数气泡:含实际交车日期,后方增加交车人
- var vehicleColumnsDelivered = [
- { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 180 },
- { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 },
- { title: '型号', dataIndex: 'model', key: 'model', width: 100 },
- { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 },
- { title: '实际交车日期', dataIndex: 'actualDelivery', key: 'actualDelivery', width: 140 },
- { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 100 }
- ];
+ // 租赁/已交车 Popover:卡片式车辆明细
+ function renderVehiclePopoverIcon(type) {
+ if (type === 'delivered') {
+ return React.createElement('svg', {
+ xmlns: 'http://www.w3.org/2000/svg', width: 18, height: 18, viewBox: '0 0 24 24',
+ fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
+ },
+ React.createElement('path', { d: 'M22 11.08V12a10 10 0 1 1-5.93-9.14' }),
+ React.createElement('polyline', { points: '22 4 12 14.01 9 11.01' }),
+ );
+ }
+ return React.createElement('svg', {
+ xmlns: 'http://www.w3.org/2000/svg', width: 18, height: 18, viewBox: '0 0 24 24',
+ fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
+ },
+ React.createElement('path', { d: 'M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2' }),
+ React.createElement('path', { d: 'M15 18H9' }),
+ React.createElement('path', { d: 'M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14' }),
+ React.createElement('circle', { cx: '17', cy: '18', r: '2' }),
+ React.createElement('circle', { cx: '7', cy: '18', r: '2' }),
+ );
+ }
+
+ function formatVehiclePlateLabel(plateNo) {
+ var plate = plateNo && String(plateNo).trim();
+ if (!plate || plate === '-') return null;
+ return plate;
+ }
+
+ function renderVehiclePopoverCard(vehicle, index, variant) {
+ var plate = formatVehiclePlateLabel(vehicle.plateNo);
+ var brandModel = [vehicle.brand, vehicle.model].filter(function (part) {
+ return part && String(part).trim() && part !== '-';
+ }).join(' · ') || '-';
+
+ return React.createElement('article', {
+ key: 'vehicle-card-' + index,
+ className: 'lc-vehicle-popover-card' + (variant === 'delivered' ? ' lc-vehicle-popover-card--delivered' : ''),
+ },
+ React.createElement('div', { className: 'lc-vehicle-popover-card__head' },
+ React.createElement('span', { className: 'lc-vehicle-popover-card__type', title: vehicle.vehicleType || '' }, vehicle.vehicleType || '-'),
+ plate
+ ? React.createElement('span', { className: 'lc-vehicle-popover-card__plate tabular-nums' }, plate)
+ : React.createElement('span', { className: 'lc-vehicle-popover-card__plate lc-vehicle-popover-card__plate--pending' }, '待上牌'),
+ ),
+ React.createElement('div', { className: 'lc-vehicle-popover-card__body' },
+ React.createElement('div', { className: 'lc-vehicle-popover-card__field' },
+ React.createElement('span', { className: 'lc-vehicle-popover-card__label' }, '品牌型号'),
+ React.createElement('span', { className: 'lc-vehicle-popover-card__value', title: brandModel }, brandModel),
+ ),
+ variant === 'delivered'
+ ? React.createElement('div', { className: 'lc-vehicle-popover-card__delivery' },
+ React.createElement('span', { className: 'lc-vehicle-popover-card__delivery-item tabular-nums' },
+ 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('rect', { x: '3', y: '4', width: '18', height: '18', rx: '2' }),
+ React.createElement('line', { x1: '16', y1: '2', x2: '16', y2: '6' }),
+ React.createElement('line', { x1: '8', y1: '2', x2: '8', y2: '6' }),
+ React.createElement('line', { x1: '3', y1: '10', x2: '21', y2: '10' }),
+ ),
+ vehicle.actualDelivery || '-',
+ ),
+ React.createElement('span', { className: 'lc-vehicle-popover-card__delivery-item' },
+ 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('path', { d: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' }),
+ React.createElement('circle', { cx: '12', cy: '7', r: '4' }),
+ ),
+ vehicle.deliveryPerson && vehicle.deliveryPerson !== '-' ? vehicle.deliveryPerson : '-',
+ ),
+ )
+ : null,
+ ),
+ );
+ }
+
+ function renderVehiclePopoverPanel(config) {
+ var vehicles = config.vehicles || [];
+ var count = vehicles.length;
+ return React.createElement('div', { className: 'lc-vehicle-popover__panel', role: 'region', 'aria-label': config.title },
+ React.createElement('div', { className: 'lc-vehicle-popover__header' },
+ React.createElement('div', {
+ className: 'lc-vehicle-popover__header-icon' + (config.variant === 'delivered' ? ' is-delivered' : ''),
+ 'aria-hidden': true,
+ }, renderVehiclePopoverIcon(config.variant)),
+ React.createElement('div', { className: 'lc-vehicle-popover__header-text' },
+ React.createElement('div', { className: 'lc-vehicle-popover__header-title' }, config.title),
+ React.createElement('div', { className: 'lc-vehicle-popover__header-sub' },
+ config.subtitle,
+ React.createElement('span', { className: 'lc-vehicle-popover__count tabular-nums' }, count + ' 辆'),
+ ),
+ ),
+ ),
+ count === 0
+ ? React.createElement('div', { className: 'lc-vehicle-popover__empty' }, config.emptyText || '暂无车辆')
+ : React.createElement('div', { className: 'lc-vehicle-popover__list' },
+ vehicles.map(function (vehicle, index) {
+ return renderVehiclePopoverCard(vehicle, index, config.variant);
+ }),
+ ),
+ );
+ }
function approvalAvatarText(name) {
if (!name) return '?';
@@ -643,17 +775,27 @@ const Component = function(props) {
return s.length <= 2 ? s : s.slice(-2);
}
+ function canShowApprovalFlowPopover(status, nodes) {
+ if (!nodes || !nodes.length) return false;
+ return status === '待审批'
+ || status === '审批中'
+ || status === '审批通过'
+ || status === '审批驳回';
+ }
+
function renderApprovalFlowContent(nodes) {
var list = nodes && nodes.length ? nodes : [];
if (!list.length) {
- return React.createElement('div', { style: { color: 'rgba(0,0,0,0.45)', fontSize: 13, padding: '4px 0' } }, '暂无审批节点明细');
+ return React.createElement('div', { className: 'lc-approval-flow__empty' }, '暂无审批节点明细');
}
return React.createElement(
'div',
- { style: { maxWidth: 360, paddingTop: 4 } },
+ { className: 'lc-approval-flow' },
list.map(function(node, index) {
var isLast = index === list.length - 1;
var isPending = node.result === 'pending';
+ var isRejected = node.result === 'rejected';
+ var comment = node.comment && String(node.comment).trim();
var dot = React.createElement(
Avatar,
{
@@ -683,7 +825,10 @@ const Component = function(props) {
'div',
{ style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8, marginBottom: 6 } },
React.createElement('span', { style: { fontSize: 14, color: 'rgba(0,0,0,0.88)', fontWeight: 500 } }, node.nodeTitle || '-'),
- React.createElement(Tag, { color: isPending ? 'processing' : 'success', style: { margin: 0 } }, isPending ? '待审核' : '通过')
+ React.createElement(Tag, {
+ color: isPending ? 'processing' : (isRejected ? 'error' : 'success'),
+ style: { margin: 0 },
+ }, isPending ? '待审核' : (isRejected ? '驳回' : '通过'))
),
isPending && node.pendingApprovers && node.pendingApprovers.length
? React.createElement(
@@ -719,13 +864,23 @@ const Component = function(props) {
)
: React.createElement(
'div',
- { style: { fontSize: 13, color: 'rgba(0,0,0,0.45)' } },
- (node.operatorName || '') + (node.operatorTime ? ' ' + node.operatorTime : '')
+ { className: 'lc-approval-flow__meta' },
+ React.createElement('span', { className: 'lc-approval-flow__operator' },
+ (node.operatorName || '') + (node.operatorTime ? ' ' + node.operatorTime : ''),
+ ),
+ comment
+ ? React.createElement('div', {
+ className: 'lc-approval-flow__comment' + (isRejected ? ' is-rejected' : ''),
+ },
+ React.createElement('span', { className: 'lc-approval-flow__comment-label' }, '审批意见'),
+ React.createElement('p', { className: 'lc-approval-flow__comment-text' }, comment),
+ )
+ : null,
)
);
return React.createElement(
'div',
- { key: index, style: { display: 'flex', alignItems: 'stretch' } },
+ { key: index, className: 'lc-approval-flow__item' },
React.createElement(
'div',
{
@@ -792,7 +947,7 @@ const Component = function(props) {
if (status === '到期合同') {
items.push({ key: 'renew2', label: '续签合同', onClick: function() { message.info('续签合同(原型)'); } });
}
- if (status === '已提交审批' && approval !== '审批驳回') {
+ if (approval === '待审批' || approval === '审批中') {
items.push({ key: 'withdraw', label: '撤回合同', onClick: function() { _withdrawModalRecord[1](record); _withdrawModalVisible[1](true); } });
}
if (approval === '审批驳回') {
@@ -824,22 +979,419 @@ const Component = function(props) {
message.info('查看(跳转车辆租赁合同-查看)' + (code ? ':' + code : ''));
}
- function renderProjectInfoCell(_, record) {
- return React.createElement(
- 'button',
- {
- type: 'button',
- className: 'lc-project-info-btn',
- onClick: function() { openContractView(record); },
- title: '查看合同详情'
+ function renderMileageProgressCell(vehicle) {
+ var percent = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0));
+ return React.createElement('div', { className: 'lc-vehicle-mileage-progress' },
+ React.createElement('div', {
+ className: 'lc-vehicle-mileage-progress__track',
+ role: 'progressbar',
+ 'aria-valuenow': percent,
+ 'aria-valuemin': 0,
+ 'aria-valuemax': 100,
+ 'aria-label': '里程完成 ' + percent + '%',
},
- 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-vehicle-mileage-progress__bar',
+ style: { width: percent + '%' },
+ }),
+ ),
+ React.createElement('span', { className: 'lc-vehicle-mileage-progress__label tabular-nums' }, percent + '%'),
+ );
+ }
+
+ function renderOverallMileageProgressCell(_, record) {
+ if (!record || !record.hasMinimumMileage) {
+ return React.createElement('div', { className: 'lc-overall-mileage-progress lc-overall-mileage-progress--empty' }, '无里程要求');
+ }
+ var percent = record.overallMileageProgress;
+ if (percent == null) percent = computeContractOverallMileageProgress(record);
+ percent = Math.min(100, Math.max(0, Number(percent) || 0));
+ var forecastStatus = record.overallMileageForecastStatus || computeContractMileageForecastStatus(record);
+ var forecastTone = getContractMileageForecastStatusTone(forecastStatus);
+ return React.createElement('div', { className: 'lc-overall-mileage-progress' },
+ React.createElement('div', { className: 'lc-overall-mileage-progress__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 + '%',
+ },
+ React.createElement('div', {
+ className: 'lc-vehicle-mileage-progress__bar',
+ style: { width: percent + '%' },
+ }),
+ ),
+ React.createElement('span', { className: 'lc-vehicle-mileage-progress__label tabular-nums' }, percent + '%'),
+ ),
+ forecastStatus
+ ? React.createElement('div', { className: 'lc-overall-mileage-progress__forecast' },
+ React.createElement(StatusTag, {
+ label: forecastStatus,
+ tone: forecastTone,
+ title: '合同里程任务完成预判,供运维人员评估是否需换车干预',
+ }),
+ )
+ : null,
+ );
+ }
+
+ function renderContractTypeCell(_, record) {
+ var label = resolveContractTemplateTypeLabel(record);
+ return React.createElement('span', {
+ className: 'lc-contract-type-label',
+ title: label,
+ }, label);
+ }
+
+ function renderPickupReceivableCell(vehicle) {
+ if (!isContractVehicleDelivered(vehicle)) {
+ return React.createElement('div', { className: 'lc-contract-pickup-payment lc-contract-pickup-payment--empty' }, '—');
+ }
+ var paid = Boolean(vehicle.pickupPaymentPaid);
+ var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
+ var tag = React.createElement(StatusTag, {
+ label: paid ? '已支付' : '未支付',
+ tone: paid ? 'green' : 'red',
+ });
+ if (!paid) {
+ return React.createElement('div', { className: 'lc-contract-pickup-payment' }, tag);
+ }
+ return React.createElement('div', { className: 'lc-contract-pickup-payment' },
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-contract-status-tag-btn',
+ title: '点击查看提车应收款详情:' + plate,
+ onClick: function () {
+ message.info('即将跳转至提车应收款详情(原型演示):' + plate);
+ },
+ }, tag),
+ );
+ }
+
+ function renderReturnSettlementCell(vehicle) {
+ if (!isContractVehicleReturned(vehicle)) {
+ return React.createElement('div', { className: 'lc-contract-return-settlement lc-contract-return-settlement--empty' }, '—');
+ }
+ var status = vehicle.returnSettlementStatus || '待提交';
+ var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
+ var tone = getReturnSettlementStatusTone(status);
+ return React.createElement('div', { className: 'lc-contract-return-settlement' },
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-contract-status-tag-btn',
+ title: '点击查看还车应结款详情:' + plate,
+ onClick: function () {
+ message.info('即将跳转至还车应结款详情(原型演示):' + plate);
+ },
+ },
+ React.createElement(StatusTag, { label: status, tone: tone }),
+ ),
+ );
+ }
+
+ function renderMileageRequirementCell(contractRecord, vehicle) {
+ var hasRequirement = contractRecord && contractRecord.hasMinimumMileage;
+ var requirementNode;
+ if (!hasRequirement) {
+ requirementNode = React.createElement('div', {
+ className: 'lc-contract-mileage-req-row__requirement lc-contract-mileage-req-row__requirement--empty',
+ }, '无里程要求');
+ } else {
+ var kmText = formatContractMileageTargetKm(contractRecord.mileageTargetKm);
+ var periodTag = getContractMileagePeriodTag(contractRecord.mileagePeriod);
+ requirementNode = React.createElement('div', { className: 'lc-contract-mileage-req-row__requirement' },
+ React.createElement('span', { className: 'lc-contract-mileage-req-row__section-label' }, '里程要求'),
+ React.createElement('div', { className: 'vm-cell-tags lc-contract-mileage-req-row__km-line' },
+ React.createElement('span', { className: 'vm-mileage tabular-nums', title: kmText }, kmText),
+ periodTag
+ ? React.createElement('span', { className: 'vm-tag vm-tag-manual lc-contract-mileage-period-tag' }, periodTag)
+ : null,
+ ),
+ );
+ }
+ var periodNode;
+ if (!hasRequirement || !isContractVehicleDelivered(vehicle)) {
+ periodNode = React.createElement('div', {
+ className: 'lc-contract-mileage-req-row__period lc-contract-mileage-req-row__period--empty',
+ }, '—');
+ } else {
+ var periodMileText = formatContractHandoverMileage(vehicle.periodStartMileage);
+ var periodDateText = formatContractPeriodStartDate(vehicle.periodStartDate);
+ periodNode = React.createElement('div', { className: 'lc-contract-mileage-req-row__period' },
+ React.createElement('span', { className: 'lc-contract-mileage-req-row__section-label' }, '当期里程'),
+ React.createElement('div', {
+ className: 'lc-contract-mileage-req-row__period-mile tabular-nums',
+ title: periodMileText,
+ }, periodMileText || '—'),
+ React.createElement('div', {
+ className: 'lc-contract-mileage-req-row__period-date tabular-nums',
+ title: periodDateText,
+ }, periodDateText),
+ );
+ }
+ var mileText = formatContractVehicleCurrentMileage(vehicle.currentMileage, vehicle, '—');
+ var hasMileageReading = mileText !== '—';
+ var mileageSource = resolveContractVehicleMileageSource(vehicle);
+ var sourceTagClass = getContractVehicleMileageSourceTagClass(mileageSource);
+ return React.createElement('div', { className: 'lc-contract-mileage-req-row lc-contract-mileage-req-row--triple' },
+ requirementNode,
+ periodNode,
+ React.createElement('div', { className: 'lc-contract-mileage-req-row__current' },
+ React.createElement('span', { className: 'lc-contract-mileage-req-row__section-label' }, '当前里程'),
+ React.createElement('div', { className: 'vm-cell-tags lc-contract-mileage-req-row__current-line' },
+ React.createElement('span', { className: 'vm-mileage tabular-nums', title: mileText }, mileText),
+ hasMileageReading
+ ? React.createElement('span', { className: sourceTagClass }, mileageSource)
+ : null,
+ ),
+ ),
+ );
+ }
+
+ function renderMileageCompletionCell(vehicle, contractRecord) {
+ if (!contractRecord || !contractRecord.hasMinimumMileage) {
+ return React.createElement('div', { className: 'lc-contract-mileage-completion lc-contract-mileage-completion--empty' }, '—');
+ }
+ var delivered = isContractVehicleDelivered(vehicle);
+ var remainingNode = null;
+ if (delivered) {
+ var remainingText = formatContractVehicleRemainingMileage(vehicle.remainingMileage);
+ remainingNode = React.createElement('div', {
+ className: 'lc-contract-mileage-completion__remaining tabular-nums',
+ title: remainingText,
+ }, remainingText);
+ }
+ return React.createElement('div', { className: 'lc-contract-mileage-completion' },
+ remainingNode,
+ renderMileageProgressCell(vehicle),
+ );
+ }
+
+ function renderContractVehicleActionCell(vehicle) {
+ if (!canContractVehicleReturn(vehicle)) {
+ return React.createElement('span', { className: 'lc-contract-vehicle-action-empty' }, '—');
+ }
+ var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
+ return React.createElement(Button, {
+ type: 'link',
+ size: 'small',
+ className: 'lc-contract-vehicle-action-btn',
+ onClick: function () {
+ message.info('即将跳转至还车管理(原型演示):' + plate);
+ },
+ }, '还车');
+ }
+
+ function formatVehicleBrandModelLabel(vehicle) {
+ var brand = vehicle.brand || '';
+ var model = vehicle.model || '';
+ if (brand && model) return brand + ' · ' + model;
+ return brand || model || '-';
+ }
+
+ function renderVehicleInfoCell(vehicle) {
+ var plateLabel = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
+ var vinLabel = vehicle.vin && String(vehicle.vin).trim() ? vehicle.vin : '-';
+ var brandModelLabel = formatVehicleBrandModelLabel(vehicle);
+ return React.createElement('div', { className: 'lc-contract-vehicle-info' },
+ React.createElement('div', {
+ className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary tabular-nums',
+ }, plateLabel),
+ React.createElement('div', {
+ className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary tabular-nums',
+ title: vinLabel,
+ }, vinLabel),
+ React.createElement('div', {
+ className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--secondary',
+ title: brandModelLabel,
+ }, brandModelLabel),
+ );
+ }
+
+ function renderHandoverSituationCell(vehicle, kind) {
+ var emptyLabel = kind === 'delivery' ? '未交车' : '未还车';
+ var hasData = kind === 'delivery'
+ ? isContractVehicleDelivered(vehicle)
+ : isContractVehicleReturned(vehicle);
+ if (!hasData) {
+ return React.createElement('div', {
+ className: 'lc-contract-handover-situation lc-contract-handover-situation--empty',
+ }, emptyLabel);
+ }
+ var mile = kind === 'delivery' ? vehicle.deliveryMileage : vehicle.returnMileage;
+ var person = kind === 'delivery' ? vehicle.deliveryPerson : vehicle.returnPerson;
+ var time = kind === 'delivery' ? vehicle.deliveryTime : vehicle.returnTime;
+ var mileText = formatContractHandoverMileage(mile);
+ var personText = person && String(person).trim() && person !== '-' ? String(person).trim() : '-';
+ var timeText = formatContractHandoverDateTimeMinute(time);
+ var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
+ var jumpTitle = kind === 'delivery'
+ ? '点击查看交车管理:' + plate
+ : '点击查看还车管理:' + plate;
+ return React.createElement('button', {
+ type: 'button',
+ className: 'lc-contract-handover-situation lc-contract-handover-situation--clickable',
+ title: jumpTitle,
+ onClick: function () {
+ if (kind === 'delivery') {
+ message.info('即将跳转至交车管理(原型演示):' + plate);
+ } else {
+ message.info('即将跳转至还车管理(原型演示):' + plate);
+ }
+ },
+ },
+ React.createElement('div', {
+ className: 'lc-contract-handover-situation__line lc-contract-handover-situation__line--mile tabular-nums',
+ title: mileText,
+ }, mileText || '-'),
+ React.createElement('div', {
+ className: 'lc-contract-handover-situation__line lc-contract-handover-situation__line--person',
+ title: personText,
+ }, personText),
+ React.createElement('div', {
+ className: 'lc-contract-handover-situation__line lc-contract-handover-situation__line--time tabular-nums',
+ title: timeText,
+ }, timeText || '-'),
+ );
+ }
+
+ function renderExpandedVehicleTable(record) {
+ var vehicles = record.vehicles || [];
+ if (!vehicles.length) {
+ return React.createElement('div', { className: 'lc-contract-vehicle-expand__empty' }, '暂无车辆明细');
+ }
+ 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' }, '交车情况'),
+ React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover' }, '还车情况'),
+ 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' }, '里程要求'),
+ React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion' }, '里程完成情况'),
+ React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__action' }, '操作'),
+ ),
+ ),
+ 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')),
+ React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return')),
+ 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)),
+ );
+ }),
+ ),
+ ),
+ );
+ }
+
+ function approvalStatusTone(status) {
+ switch (status) {
+ case '审批通过': return 'green';
+ case '审批中': return 'amber';
+ case '待审批': return 'blue';
+ case '审批驳回': return 'red';
+ case '撤回': return 'gray';
+ case '未提交': return 'gray';
+ default: return 'gray';
+ }
+ }
+
+ function contractStatusTone(status) {
+ switch (status) {
+ case '合同进行中': return 'green';
+ case '已提交审批': return 'blue';
+ case '变更': return 'amber';
+ case '到期合同': return 'amber';
+ case '草稿': return 'gray';
+ case '已结束': return 'gray';
+ case '已终止': return 'red';
+ default: return 'gray';
+ }
+ }
+
+ function renderApprovalStatusCell(_, record) {
+ var status = record.approvalStatus || '-';
+ var approver = getCurrentApproverLabel(record);
+ var showApprover = !shouldHideCurrentApprover(status) && approver;
+ var nodes = record.approvalFlowNodes;
+ var hasFlowPopover = canShowApprovalFlowPopover(status, nodes);
+ var tone = approvalStatusTone(status);
+ var statusTag = React.createElement(StatusTag, {
+ label: status,
+ tone: tone,
+ title: hasFlowPopover ? '悬停查看审批流' : status,
+ });
+
+ var statusContent = hasFlowPopover
+ ? React.createElement(Popover, {
+ content: renderApprovalFlowContent(nodes),
+ trigger: 'hover',
+ placement: 'rightTop',
+ overlayClassName: 'lc-approval-flow-popover',
+ overlayInnerStyle: { maxWidth: 400 },
+ mouseEnterDelay: 0.15,
+ },
+ React.createElement('span', {
+ className: 'lc-approval-status-cell__tag-wrap lc-approval-status-cell__tag-wrap--interactive',
+ }, statusTag),
)
+ : statusTag;
+
+ return React.createElement('div', { className: 'vm-stack lc-approval-status-cell' },
+ statusContent,
+ showApprover
+ ? React.createElement('span', {
+ className: 'sub lc-approval-status-cell__approver',
+ title: approver,
+ }, approver)
+ : null,
+ );
+ }
+
+ function renderSigningCompanyCell(_, record) {
+ var fullName = record.signingCompanyFullName || record.signingCompany || '-';
+ return React.createElement('span', {
+ className: 'lc-signing-company-cell',
+ title: fullName,
+ }, fullName);
+ }
+
+ 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: '查看合同详情'
+ },
+ 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,
+ }),
+ ),
);
}
@@ -861,20 +1413,422 @@ const Component = function(props) {
);
}
- function renderFeeTermsCell(_, record) {
- return renderStackCell(record.paymentPeriod, record.hydrogenPaymentMethod);
+ function renderFeeInfoCell(_, record) {
+ var periodLabel = formatPaymentPeriodLabel(record.paymentPeriod);
+ var hydrogenLabel = formatHydrogenPaymentLabel(record.hydrogenPaymentMethod);
+ var isPrepay = record.hydrogenPaymentMethod === 'prepay';
+ return React.createElement('div', { className: 'lc-fee-info-cell' },
+ React.createElement('div', { className: 'lc-fee-info-cell__side lc-fee-info-cell__side--period' },
+ React.createElement('span', { className: 'lc-fee-info-cell__caption' }, '付款周期'),
+ React.createElement('span', {
+ className: 'lc-fee-info-cell__value',
+ title: periodLabel,
+ }, periodLabel),
+ ),
+ React.createElement('span', { className: 'lc-fee-info-cell__sep', 'aria-hidden': true }),
+ React.createElement('div', {
+ className: 'lc-fee-info-cell__side lc-fee-info-cell__side--hydrogen' + (isPrepay ? ' is-prepay' : ''),
+ },
+ React.createElement('span', { className: 'lc-fee-info-cell__caption' }, '氢费支付'),
+ React.createElement('span', {
+ className: 'lc-fee-info-cell__value',
+ title: hydrogenLabel,
+ }, hydrogenLabel),
+ ),
+ );
+ }
+
+ function formatPrototypeOperateTime() {
+ var dayjs = window.dayjs;
+ if (dayjs) return dayjs().format('YYYY-MM-DD HH:mm:ss');
+ var now = new Date();
+ var pad = function (n) { return n < 10 ? '0' + n : String(n); };
+ return now.getFullYear() + '-'
+ + pad(now.getMonth() + 1) + '-'
+ + pad(now.getDate()) + ' '
+ + pad(now.getHours()) + ':'
+ + pad(now.getMinutes()) + ':'
+ + pad(now.getSeconds());
+ }
+
+ function closeDeliveryChangePopover() {
+ _deliveryChangePopoverRecord[1](null);
+ _deliveryChangeDraftDate[1](null);
+ _deliveryChangeDraftTbd[1](false);
+ }
+
+ function confirmDeliveryDateChange(record) {
+ var draftTbd = _deliveryChangeDraftTbd[0];
+ var newDate = draftTbd ? DELIVERY_DATE_TBD_LABEL : _deliveryChangeDraftDate[0];
+ var oldTbd = record.deliveryDateTbd || isDeliveryDateTbd(record.deliveryDate);
+ var oldDate = oldTbd ? DELIVERY_DATE_TBD_LABEL : (record.deliveryDate || null);
+ if (!draftTbd && !newDate) {
+ message.warning('请选择交车日期,或标记为暂未确定');
+ return;
+ }
+ if (draftTbd === oldTbd && (draftTbd || newDate === oldDate)) {
+ closeDeliveryChangePopover();
+ return;
+ }
+ var logEntry = {
+ beforeDate: oldDate || '-',
+ afterDate: newDate,
+ operatorName: record.businessOwner || record.creator || '当前用户',
+ operateTime: formatPrototypeOperateTime(),
+ };
+ _recordOverrides[1](function (prev) {
+ var prevRec = prev[record.id] || {};
+ var baseVehicles = prevRec.vehicles || record.vehicles || [];
+ var updatedVehicles = baseVehicles.map(function (vehicle) {
+ if (isContractVehicleDelivered(vehicle)) return vehicle;
+ return Object.assign({}, vehicle, {
+ plannedDeliveryDate: draftTbd ? null : newDate,
+ deliveryDateTbd: draftTbd,
+ deliveryTaskSuspended: draftTbd,
+ });
+ });
+ var baseLogs = prevRec.deliveryDateChangeLogs != null
+ ? prevRec.deliveryDateChangeLogs
+ : (record.deliveryDateChangeLogs || []);
+ return Object.assign({}, prev, {
+ [record.id]: Object.assign({}, prevRec, {
+ deliveryDate: draftTbd ? DELIVERY_DATE_TBD_LABEL : newDate,
+ deliveryDateTbd: draftTbd,
+ deliveryTaskSuspended: draftTbd,
+ vehicles: updatedVehicles,
+ deliveryDateChangeLogs: [logEntry].concat(baseLogs),
+ }),
+ });
+ });
+ message.success(draftTbd ? '交车时间已标记为暂未确定(原型)' : '交车时间已更新,已同步至未交车车辆(原型)');
+ closeDeliveryChangePopover();
+ }
+
+ function renderDeliveryChangePanel(record) {
+ var dayjs = window.dayjs;
+ var logs = record.deliveryDateChangeLogs || [];
+ var draftDate = _deliveryChangeDraftDate[0];
+ var draftTbd = _deliveryChangeDraftTbd[0];
+ var draftValue = draftDate && dayjs && !draftTbd ? dayjs(draftDate) : null;
+ 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__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) {
+ _deliveryChangeDraftDate[1](dateString || null);
+ },
+ }),
+ ),
+ React.createElement('label', { className: 'lc-delivery-change__tbd' },
+ React.createElement(Checkbox, {
+ checked: draftTbd,
+ onChange: function (event) {
+ var checked = event.target.checked;
+ _deliveryChangeDraftTbd[1](checked);
+ if (checked) {
+ _deliveryChangeDraftDate[1](null);
+ } else {
+ _deliveryChangeDraftDate[1](record.deliveryDate && !isDeliveryDateTbd(record.deliveryDate)
+ ? record.deliveryDate
+ : null);
+ }
+ },
+ }, '暂未确定'),
+ ),
+ React.createElement('p', { className: 'lc-delivery-change__hint' },
+ '修改后将同步至本合同所有未交车车辆;标记暂未确定后,交车任务暂时挂起。',
+ ),
+ React.createElement('div', { className: 'lc-delivery-change__actions' },
+ React.createElement(Button, {
+ size: 'small',
+ onClick: closeDeliveryChangePopover,
+ }, '取消'),
+ React.createElement(Button, {
+ size: 'small',
+ type: 'primary',
+ onClick: function () { confirmDeliveryDateChange(record); },
+ }, '确定'),
+ ),
+ ),
+ React.createElement('div', { className: 'lc-delivery-change__history' },
+ React.createElement('div', { className: 'lc-delivery-change__history-title' }, '修改记录'),
+ logs.length === 0
+ ? React.createElement('p', { className: 'lc-delivery-change__empty' }, '暂无修改记录')
+ : React.createElement('div', { className: 'lc-delivery-change__history-list' },
+ logs.map(function (log, index) {
+ return React.createElement('div', {
+ key: (log.operateTime || '') + '-' + index,
+ className: 'lc-delivery-change__history-item',
+ },
+ React.createElement('div', { className: 'lc-delivery-change__history-row' },
+ React.createElement('span', { className: 'lc-delivery-change__history-field-label' }, '交车时间'),
+ React.createElement('div', { className: 'lc-delivery-change__date-compare' },
+ React.createElement('span', {
+ className: 'lc-delivery-change__date-before tabular-nums',
+ title: '修改前',
+ }, log.beforeDate || '-'),
+ React.createElement('span', {
+ className: 'lc-delivery-change__date-arrow',
+ 'aria-hidden': true,
+ }, '→'),
+ React.createElement('span', {
+ className: 'lc-delivery-change__date-after tabular-nums',
+ title: '修改后',
+ }, log.afterDate || '-'),
+ ),
+ ),
+ React.createElement('div', { className: 'lc-delivery-change__history-row' },
+ React.createElement('span', { className: 'lc-delivery-change__history-field-label' }, '操作人'),
+ React.createElement('span', { className: 'lc-delivery-change__history-field-value' }, log.operatorName || '-'),
+ ),
+ React.createElement('div', { className: 'lc-delivery-change__history-row' },
+ React.createElement('span', { className: 'lc-delivery-change__history-field-label' }, '操作时间'),
+ React.createElement('span', {
+ className: 'lc-delivery-change__history-field-value tabular-nums',
+ }, log.operateTime || '-'),
+ ),
+ );
+ }),
+ ),
+ ),
+ );
+ }
+
+ function renderDeliveryEditIcon() {
+ 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('path', { d: 'M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z' }),
+ React.createElement('path', { d: 'm15 5 4 4' }),
+ );
+ }
+
+ function closeDelegatePopover() {
+ _delegatePopoverRecord[1](null);
+ }
+
+ function renderDelegatePopoverPanel(record) {
+ var delegates = getAuthorizedDelegates(record);
+ return React.createElement('div', {
+ className: 'lc-delegate-popover__panel',
+ role: 'region',
+ 'aria-label': '被授权人明细',
+ },
+ React.createElement('div', { className: 'lc-delegate-popover__header' },
+ React.createElement('div', { className: 'lc-delegate-popover__title' }, '被授权人'),
+ React.createElement('div', { className: 'lc-delegate-popover__sub' }, record.contractCode || ''),
+ ),
+ delegates.length === 0
+ ? React.createElement('p', { className: 'lc-delegate-popover__empty' }, '暂无被授权人')
+ : React.createElement('div', { className: 'lc-delegate-popover__list' },
+ delegates.map(function (person, index) {
+ return React.createElement('div', {
+ key: (person.idNumber || person.contact || person.name || '') + '-' + index,
+ className: 'lc-delegate-popover__card',
+ },
+ React.createElement('div', { className: 'lc-delegate-popover__card-head' },
+ React.createElement('span', { className: 'lc-delegate-popover__name' }, person.name || '-'),
+ React.createElement('span', { className: 'lc-delegate-popover__index' }, '第 ' + (index + 1) + ' 人'),
+ ),
+ React.createElement('div', { className: 'lc-delegate-popover__field' },
+ React.createElement('span', { className: 'lc-delegate-popover__label' }, '联系方式'),
+ React.createElement('span', { className: 'lc-delegate-popover__value tabular-nums' }, person.contact || person.phone || '-'),
+ ),
+ React.createElement('div', { className: 'lc-delegate-popover__field' },
+ React.createElement('span', { className: 'lc-delegate-popover__label' }, '身份证号'),
+ React.createElement('span', { className: 'lc-delegate-popover__value tabular-nums' }, person.idNumber || person.idCard || '-'),
+ ),
+ );
+ }),
+ ),
+ );
+ }
+
+ function renderDelegateCountCell(_, record) {
+ var count = record.delegateCount != null ? record.delegateCount : 0;
+ if (count <= 0) return '-';
+ var popoverOpen = _delegatePopoverRecord[0] && _delegatePopoverRecord[0].id === record.id;
+ return React.createElement(Popover, {
+ overlayClassName: 'lc-delegate-popover',
+ placement: 'bottom',
+ trigger: 'click',
+ arrow: { pointAtCenter: true },
+ destroyTooltipOnHide: true,
+ open: popoverOpen,
+ title: null,
+ content: renderDelegatePopoverPanel(record),
+ onOpenChange: function (visible) {
+ if (!visible) {
+ closeDelegatePopover();
+ return;
+ }
+ _vehiclePopoverRecord[1](null);
+ _deliveredPopoverRecord[1](null);
+ closeDeliveryChangePopover();
+ _delegatePopoverRecord[1](record);
+ },
+ },
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-delegate-count-btn tabular-nums',
+ 'aria-label': '查看被授权人 ' + count + ' 人',
+ }, count + ' 人'),
+ );
+ }
+
+ function renderApprovalTypeCell(_, record) {
+ var label = formatContractApprovalTypeLabel(record.contractApprovalType);
+ var isNonStandard = label === '非标准合同';
+ return React.createElement('span', {
+ className: 'lc-approval-type-label' + (isNonStandard ? ' lc-approval-type-label--nonstandard' : ''),
+ }, label);
}
function renderDeliveryCell(_, record) {
- return renderStackCell(formatDeliveryRegion(record.deliveryRegion), record.deliveryDate || '-');
+ var regionLabel = formatDeliveryRegion(record.deliveryRegion);
+ var dateLabel = formatContractDeliveryDateLabel(record);
+ var isTbd = record.deliveryDateTbd || isDeliveryDateTbd(record.deliveryDate);
+ var popoverOpen = _deliveryChangePopoverRecord[0] && _deliveryChangePopoverRecord[0].id === record.id;
+ return React.createElement('div', { className: 'lc-delivery-cell' },
+ React.createElement('span', { className: 'lc-delivery-cell__region' }, 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' : ''),
+ }, dateLabel),
+ React.createElement(Popover, {
+ overlayClassName: 'lc-delivery-change-popover',
+ placement: 'bottom',
+ trigger: 'click',
+ arrow: { pointAtCenter: true },
+ destroyTooltipOnHide: true,
+ open: popoverOpen,
+ title: null,
+ content: renderDeliveryChangePanel(record),
+ onOpenChange: function (visible) {
+ if (!visible) {
+ closeDeliveryChangePopover();
+ return;
+ }
+ _vehiclePopoverRecord[1](null);
+ _deliveredPopoverRecord[1](null);
+ closeDelegatePopover();
+ var recordTbd = record.deliveryDateTbd || isDeliveryDateTbd(record.deliveryDate);
+ _deliveryChangePopoverRecord[1](record);
+ _deliveryChangeDraftTbd[1](!!recordTbd);
+ _deliveryChangeDraftDate[1](recordTbd ? null : (record.deliveryDate || null));
+ },
+ },
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-delivery-cell__edit-btn',
+ 'aria-label': '编辑交车时间',
+ }, renderDeliveryEditIcon()),
+ ),
+ ),
+ );
}
- function renderLeaseOrderCell(_, record) {
- var insured = record.insuredVehicleCount != null ? '投保 ' + record.insuredVehicleCount + ' 辆' : '-';
- var liability = record.thirdPartyLiabilityMillion != null
- ? '三者险 ' + record.thirdPartyLiabilityMillion + ' 百万'
- : '-';
- return renderStackCell(insured, liability);
+ function renderLeaseOrderCountsCell(_, record) {
+ var deliveredVehicles = (record.vehicles || []).filter(function (v) {
+ var d = v.actualDelivery;
+ return d && String(d).trim() && d !== '-';
+ });
+ var deliveredCount = deliveredVehicles.length;
+ var rentalOpen = _vehiclePopoverRecord[0] && _vehiclePopoverRecord[0].id === record.id;
+ var deliveredOpen = _deliveredPopoverRecord[0] && _deliveredPopoverRecord[0].id === record.id;
+ var rentalContent = renderVehiclePopoverPanel({
+ variant: 'rental',
+ title: '租赁车辆明细',
+ subtitle: record.contractCode || '',
+ vehicles: record.vehicles || [],
+ emptyText: '该合同暂无租赁车辆',
+ });
+ var deliveredContent = renderVehiclePopoverPanel({
+ variant: 'delivered',
+ title: '已交车列表',
+ subtitle: record.contractCode || '',
+ vehicles: deliveredVehicles,
+ emptyText: '暂无已交车记录',
+ });
+ var popoverCommonProps = {
+ overlayClassName: 'lc-vehicle-popover',
+ placement: 'bottom',
+ trigger: 'click',
+ arrow: { pointAtCenter: true },
+ destroyTooltipOnHide: true,
+ };
+
+ return React.createElement('div', { className: 'lc-lease-order-counts' },
+ React.createElement(Popover, Object.assign({}, popoverCommonProps, {
+ content: rentalContent,
+ title: null,
+ open: rentalOpen,
+ onOpenChange: function (visible) {
+ if (!visible) _vehiclePopoverRecord[1](null);
+ else {
+ _deliveredPopoverRecord[1](null);
+ closeDeliveryChangePopover();
+ closeDelegatePopover();
+ _vehiclePopoverRecord[1](record);
+ }
+ },
+ }),
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-lease-order-counts__item',
+ 'aria-label': '租赁车辆数 ' + (record.vehicleCount != null ? record.vehicleCount : 0),
+ },
+ React.createElement('span', { className: 'lc-lease-order-counts__value tabular-nums' }, record.vehicleCount != null ? record.vehicleCount : 0),
+ React.createElement('span', { className: 'lc-lease-order-counts__label' }, '租赁'),
+ ),
+ ),
+ React.createElement('span', { className: 'lc-lease-order-counts__sep', 'aria-hidden': true }),
+ React.createElement(Popover, Object.assign({}, popoverCommonProps, {
+ content: deliveredContent,
+ title: null,
+ open: deliveredOpen,
+ onOpenChange: function (visible) {
+ if (!visible) _deliveredPopoverRecord[1](null);
+ else {
+ _vehiclePopoverRecord[1](null);
+ closeDeliveryChangePopover();
+ closeDelegatePopover();
+ _deliveredPopoverRecord[1](record);
+ }
+ },
+ }),
+ React.createElement('button', {
+ type: 'button',
+ className: 'lc-lease-order-counts__item',
+ 'aria-label': '已交车辆数 ' + deliveredCount,
+ },
+ React.createElement('span', { className: 'lc-lease-order-counts__value tabular-nums' }, deliveredCount),
+ React.createElement('span', { className: 'lc-lease-order-counts__label' }, '已交'),
+ ),
+ ),
+ );
}
function renderContactCell(_, record) {
@@ -913,114 +1867,44 @@ const Component = function(props) {
{
title: '项目信息',
key: 'projectInfo',
- width: 200,
+ width: 248,
fixed: 'left',
className: 'lc-project-info-col',
onCell: function() { return { className: 'lc-project-info-col' }; },
render: renderProjectInfoCell
},
{
- title: '租赁车辆数',
- key: 'vehicleCount',
- width: 100,
- render: function(_, record) {
- var open = _vehiclePopoverRecord[0] && _vehiclePopoverRecord[0].id === record.id;
- var content = React.createElement('div', { style: { padding: 8 } },
- React.createElement(Table, {
- size: 'small',
- rowKey: function(r, i) { return String(i); },
- columns: vehicleColumnsRental,
- dataSource: record.vehicles || [],
- pagination: false,
- scroll: { x: 460 }
- })
- );
- return React.createElement(
- Popover,
- {
- content: content,
- title: '车辆明细',
- open: open,
- onOpenChange: function(visible) {
- if (!visible) _vehiclePopoverRecord[1](null);
- else _vehiclePopoverRecord[1](record);
- },
- trigger: 'click'
- },
- React.createElement('a', { className: 'oneos-cell-link', style: { fontWeight: 500, cursor: 'pointer' } }, record.vehicleCount)
- );
- }
+ title: '租赁订单',
+ key: 'leaseOrder',
+ width: 132,
+ className: 'lc-lease-order-col',
+ onCell: function () { return { className: 'lc-lease-order-col' }; },
+ render: renderLeaseOrderCountsCell,
},
{
- title: '已交车辆数',
- key: 'deliveredCount',
- width: 100,
- render: function(_, record) {
- var deliveredVehicles = (record.vehicles || []).filter(function(v) {
- var d = v.actualDelivery;
- return d && String(d).trim() && d !== '-';
- });
- var deliveredCount = deliveredVehicles.length;
- var open = _deliveredPopoverRecord[0] && _deliveredPopoverRecord[0].id === record.id;
- var content = React.createElement('div', { style: { padding: 8 } },
- React.createElement(Table, {
- size: 'small',
- rowKey: function(r, i) { return String(i); },
- columns: vehicleColumnsDelivered,
- dataSource: deliveredVehicles,
- pagination: false,
- scroll: { x: 700 }
- })
- );
- return React.createElement(
- Popover,
- {
- content: content,
- title: '已交车列表',
- open: open,
- onOpenChange: function(visible) {
- if (!visible) _deliveredPopoverRecord[1](null);
- else _deliveredPopoverRecord[1](record);
- },
- trigger: 'click'
- },
- React.createElement('a', { className: 'oneos-cell-link', style: { fontWeight: 500, cursor: 'pointer' } }, deliveredCount)
- );
- }
+ title: '审批类型',
+ key: 'contractApprovalType',
+ width: 108,
+ className: 'lc-approval-type-col',
+ onCell: function () { return { className: 'lc-approval-type-col' }; },
+ render: renderApprovalTypeCell,
},
{
title: '审批状态',
- dataIndex: 'approvalStatus',
key: 'approvalStatus',
- width: 100,
- render: function(text, record) {
- if (text !== '审批中') return text;
- var nodes = record.approvalFlowNodes;
- return React.createElement(
- Popover,
- {
- content: renderApprovalFlowContent(nodes),
- trigger: 'hover',
- placement: 'rightTop',
- overlayInnerStyle: { maxWidth: 400 },
- mouseEnterDelay: 0.15
- },
- React.createElement(
- 'span',
- {
- style: {
- cursor: 'help',
- borderBottom: '1px dashed rgba(0,0,0,0.35)',
- color: 'rgba(0,0,0,0.88)'
- }
- },
- text
- )
- );
- }
+ width: 120,
+ className: 'lc-approval-status-col',
+ onCell: function () { return { className: 'lc-approval-status-col' }; },
+ render: renderApprovalStatusCell,
+ },
+ {
+ title: '签约公司',
+ key: 'signingCompany',
+ width: 220,
+ className: 'lc-signing-company-col',
+ onCell: function () { return { className: 'lc-signing-company-col' }; },
+ render: renderSigningCompanyCell,
},
- { title: '合同状态', dataIndex: 'contractStatus', key: 'contractStatus', width: 110 },
- { title: '签约公司', dataIndex: 'signingCompany', key: 'signingCompany', width: 100 },
{
title: '业务部门',
key: 'businessDept',
@@ -1028,34 +1912,36 @@ const Component = function(props) {
render: renderBusinessDeptCell
},
{
- title: '费用条款',
- key: 'feeTerms',
- width: 108,
- render: renderFeeTermsCell
+ title: '费用信息',
+ key: 'feeInfo',
+ width: 192,
+ className: 'lc-fee-info-col',
+ onCell: function () { return { className: 'lc-fee-info-col' }; },
+ render: renderFeeInfoCell,
},
{
title: '交车安排',
key: 'deliveryPlan',
- width: 128,
- render: renderDeliveryCell
+ width: 148,
+ className: 'lc-delivery-col',
+ onCell: function () { return { className: 'lc-delivery-col' }; },
+ render: renderDeliveryCell,
},
{
- title: '租赁订单',
- key: 'leaseOrderSummary',
- width: 120,
- render: renderLeaseOrderCell
+ title: '整体里程完成情况',
+ key: 'overallMileageProgress',
+ width: 168,
+ className: 'lc-overall-mileage-col',
+ onCell: function () { return { className: 'lc-overall-mileage-col' }; },
+ render: renderOverallMileageProgressCell,
},
{
- title: '里程要求',
- key: 'mileageRequirement',
- width: 128,
- render: function(_, record) {
- return formatMileageSummary(record);
- }
+ title: '合同类型',
+ key: 'contractType',
+ width: 148,
+ ellipsis: true,
+ render: renderContractTypeCell,
},
- { title: '合同类型', dataIndex: 'contractType', key: 'contractType', width: 96 },
- { title: '合同审批类型', dataIndex: 'contractApprovalType', key: 'contractApprovalType', width: 124 },
- { title: '合同结束日期', dataIndex: 'contractEndDate', key: 'contractEndDate', width: 118 },
{
title: '客户联系人',
key: 'contact',
@@ -1066,11 +1952,9 @@ const Component = function(props) {
title: '被授权人',
key: 'delegateCount',
width: 88,
- className: 'tabular-nums',
- render: function(_, record) {
- var count = record.delegateCount != null ? record.delegateCount : 0;
- return count > 0 ? count + ' 人' : '-';
- }
+ className: 'tabular-nums lc-delegate-col',
+ onCell: function () { return { className: 'lc-delegate-col tabular-nums' }; },
+ render: renderDelegateCountCell,
},
{
title: '创建信息',
@@ -1079,12 +1963,11 @@ const Component = function(props) {
render: renderCreateAuditCell
},
{
- title: '更新信息',
+ title: '最后更新',
key: 'updateAudit',
width: 148,
render: renderUpdateAuditCell
},
- { title: '备注', dataIndex: 'remark', key: 'remark', width: 120, ellipsis: true },
{ title: '操作', key: 'action', width: 140, fixed: 'right', render: function(_, record) { return getOperationButtons(record); } }
];
@@ -1192,7 +2075,7 @@ const Component = function(props) {
options: contractTypeOptions
})),
React.createElement('label', { key: 'contractApprovalType', className: 'vm-filter-field' },
- React.createElement('span', null, '合同审批类型'),
+ React.createElement('span', null, '审批类型'),
React.createElement(Select, {
mode: 'multiple',
placeholder: '请选择',
@@ -1229,12 +2112,6 @@ const Component = function(props) {
filterNodes.push(filterItems[i]);
}
- var reqTitleStyle = { fontSize: 18, fontWeight: 600, marginBottom: 16, color: 'rgba(0,0,0,0.85)' };
- var reqSectionStyle = { fontSize: 15, fontWeight: 600, marginTop: 16, marginBottom: 8, color: 'rgba(0,0,0,0.85)' };
- var reqSubStyle = { fontSize: 14, fontWeight: 500, marginLeft: 16, marginTop: 8, marginBottom: 4, color: 'rgba(0,0,0,0.85)' };
- var reqItemStyle = { fontSize: 13, marginLeft: 32, marginTop: 4, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.75)' };
- var reqSubItemStyle = { fontSize: 13, marginLeft: 48, marginTop: 2, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.7)' };
-
if (_view[0] === 'create') {
return React.createElement(LeaseContractCreate, {
onBack: function() { _view[1]('list'); }
@@ -1244,34 +2121,6 @@ const Component = function(props) {
return React.createElement(App, null,
React.createElement('div', { className: 'vm-page lc-page' },
React.createElement('style', null, ONEOS_ANT_TABLE_GLOBAL_FIX.join('\n')),
- React.createElement('div', { className: 'vm-doc-toolbar', role: 'toolbar', 'aria-label': '文档与测试用例' },
- React.createElement('button', {
- type: 'button',
- className: 'vm-doc-toolbar-btn',
- onClick: function() { _requirementModalVisible[1](true); },
- 'aria-label': '查看需求说明'
- },
- 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 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' }),
- React.createElement('path', { d: 'M14 2v4a2 2 0 0 0 2 2h4' }),
- React.createElement('path', { d: 'M10 9H8' }),
- React.createElement('path', { d: 'M16 13H8' }),
- React.createElement('path', { d: 'M16 17H8' })
- ),
- '查看需求说明'
- )
- ),
React.createElement('section', { className: 'vm-filter-card', 'data-annotation-id': 'lc-list-filter', 'aria-label': '筛选条件' },
React.createElement('header', { className: 'vm-filter-header' },
React.createElement('h2', { className: 'vm-filter-title' }, '筛选条件')
@@ -1378,13 +2227,20 @@ const Component = function(props) {
React.createElement('div', { className: 'vm-table-card', 'data-annotation-id': 'lc-list-table' },
React.createElement('div', { className: 'vm-table-wrap' },
React.createElement(Table, {
- className: 'vm-list-table',
+ className: 'vm-list-table lc-list-table--nested',
rowKey: 'id',
columns: columns,
dataSource: pagedList,
- scroll: { x: 2120 },
+ scroll: { x: 2072 },
size: 'small',
- pagination: false
+ pagination: false,
+ expandable: {
+ columnWidth: 48,
+ expandedRowRender: renderExpandedVehicleTable,
+ rowExpandable: function (record) {
+ return (record.vehicles || []).length > 0;
+ },
+ },
})
),
React.createElement('div', { className: 'vm-table-footer' }, renderTablePagination())
@@ -1407,6 +2263,18 @@ const Component = function(props) {
open: _withdrawModalVisible[0],
onCancel: function() { _withdrawModalVisible[1](false); _withdrawModalRecord[1](null); },
onOk: function() {
+ var rec = _withdrawModalRecord[0];
+ if (rec && rec.id) {
+ _recordOverrides[1](function (prev) {
+ var next = Object.assign({}, prev);
+ next[rec.id] = {
+ approvalStatus: '撤回',
+ contractStatus: '草稿',
+ approvalFlowNodes: [],
+ };
+ return next;
+ });
+ }
message.success('已撤回(原型)');
_withdrawModalVisible[1](false);
_withdrawModalRecord[1](null);
@@ -1531,88 +2399,6 @@ const Component = function(props) {
},
accept: '.pdf,.doc,.docx,image/*'
}, React.createElement('p', { style: { margin: 0, padding: '20px 0' } }, '点击或拖拽文件到此区域上传;支持一次选择多个附件,可分批添加'))
- )),
- React.createElement(Modal, {
- title: '需求说明',
- open: _requirementModalVisible[0],
- onCancel: function() { _requirementModalVisible[1](false); },
- width: 720,
- footer: React.createElement(Button, { onClick: function() { _requirementModalVisible[1](false); } }, '关闭'),
- bodyStyle: { maxHeight: '70vh', overflow: 'auto' }
- }, React.createElement('div', { style: { padding: '8px 0' } },
- React.createElement('div', { style: reqTitleStyle }, '车辆租赁合同(2026年3月3日版本)'),
- React.createElement('div', { style: { fontSize: 14, marginBottom: 12, color: 'rgba(0,0,0,0.85)' } }, '一个「数字化资产ONEOS运管平台」中的「车辆租赁合同」模块'),
- React.createElement('div', { style: reqSectionStyle }, '1.面包屑:'),
- React.createElement('div', { style: reqSubStyle }, '1.1.业务管理-租赁合同管理'),
- React.createElement('div', { style: reqSectionStyle }, '2.筛选:'),
- React.createElement('div', { style: reqSubStyle }, '2.1.支持通过合同编码、项目名称、客户名称、签约公司、审批状态、合同状态、业务部门、业务负责人、合同类型、合同审批类型、创建人、合同结束日期等条件进行筛选,右侧为重置、查询、展开/收起(筛选条件以3列显示,默认显示一行,点击展开/收起对筛选栏卡片进行展开/收起所有筛选条件),点击查询后,筛选条件与列表内容联动。点击重置会回到默认筛选条件并在列表展示结果:'),
- React.createElement('div', { style: reqItemStyle }, '2.1.1.合同编码:选择器,支持从输入框内输入内容进行模糊搜索,下拉显示匹配选项;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.2.项目名称:选择器,支持从输入框内输入内容进行模糊搜索,下拉显示匹配选项;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.3.客户名称:选择器,支持从输入框内输入内容进行模糊搜索,下拉显示匹配选项;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.4.签约公司:选择器,显示租赁合同所有签约公司;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.5.审批状态:选择器,支持全选或多选,选项为:全部、待审批、审批中、审批通过、审批驳回、未提交,默认显示全部;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.6.合同状态:选择器,支持全选或多选,选项为:全部、草稿、变更、合同进行中、到期合同、已提交审批、已结束;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.7.业务部门:选择器,支持全选或多选,拉取部门下所有业务相关部门;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.8.业务负责人:选择器,支持全选或多选,拉取所有业务相关部门下所有用户姓名;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.9.合同类型:选择器,支持全选或多选,选项为:全部、正式合同、试用合同;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.10.合同审批类型:选择器,支持全选或多选,选项为:全部、标准合同审批、非标准合同审批;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.11.创建人:选择器,支持全选或多选,拉取所有业务相关部门下所有用户姓名;'),
- React.createElement('div', { style: reqItemStyle }, '2.1.12.合同结束日期:日期选择器,支持单输入框内双日历选择开始-结束时间;'),
- React.createElement('div', { style: reqSectionStyle }, '3.列表:'),
- React.createElement('div', { style: reqSubStyle }, '3.1.列表展示所有租赁合同信息,字段依次为:合同编码、项目名称、租赁车辆数、已交车辆数、审批状态、合同状态、客户名称、签约公司、业务部门、业务负责人、合同类型、合同审批类型、合同结束日期、客户联系人、联系电话、创建人、创建时间、更新人、最后更新时间、备注、操作;列表右上角为新增、导出;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.1.合同编码:显示租赁合同对应合同编码;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.2.项目名称:显示租赁合同对应项目名称;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.3.租赁车辆数:显示租赁车辆数,点击显示气泡卡片,卡片中列表显示:车辆类型、品牌、型号、车牌号、实际交车日期;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.4.已交车辆数:显示已交车辆数,样式与交互同租赁车辆数,点击显示气泡卡片,卡片中显示已交车列表(同租赁车辆数列表结构);'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.3.1.车辆类型:4.5吨冷链车-轻型厢式货车、18吨双飞翼货车-重型厢式货车、49吨牵引车头-重型半挂牵引车、4.5吨货车-轻型厢式货车、18吨厢式货车-重型厢式货车、重型集装箱半挂车-重型集装箱半挂车、公务用车/小客车-小型普通客车、35吨牵引车头-重型半挂牵引车、重型平板半挂车-重型平板半挂车;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.3.2.品牌:显示租赁合同中对应车辆品牌;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.3.3.型号:显示租赁合同中对应车辆型号;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.3.4.车牌号:显示租赁合同中对应车牌号,无则显示为-,有则显示具体车牌号;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.3.5.实际交车日期:显示租赁合同中对应车辆对应实际交车日期,精确至分钟,格式为YYYY-MM-DD HH:MM;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.4.审批状态:显示租赁合同实际审批状态,状态分为:待审批、审批中、审批通过、审批驳回、未提交;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.4.1.待审批:发起人已提交,但还没有任何流程节点完成审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.4.2.审批中:发起人已提交,已有1个以上节点完成审批,但未完成最终节点审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.4.3.审批通过:发起人已提交,最终节点完成审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.4.4.审批驳回:发起人已提交,任意流程节点驳回,该状态下操作列支持编辑和重新提交;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.4.5.未提交:发起人仅保存,但未提交审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.4.6.撤回:发起人主动撤回审批流程;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.5.合同状态:显示租赁合同状态,状态分为:草稿、变更、合同进行中、到期合同、已提交审批、已结束;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.5.1.草稿:发起人仅保存,但未提交审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.5.2.变更:发起人提交后,合同已通过审批的基础上,进行了「变更内容」操作,且已提交审批,但未完成最终节点审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.5.3.合同进行中:发起人提交后,合同完成最终节点审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.5.4.到期合同:发起人已提交,最终节点完成审批,但当前日期超过合同结束日期;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.5.5.已提交审批:发起人初次提交,但未完成最终节点审批;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.5.6.已结束:当前日期超过合同结束日期或在操作列终止合同并完成审核;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.6.客户名称:显示租赁合同创建时所选客户名称,客户名称来自「客户管理」-「客户名称」;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.7.签约公司:显示租赁合同创建时所选签约公司,签约公司来自组织机构表;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.8.业务部门:显示租赁合同创建时所选业务部门,业务部门来自部门表(业务组);'),
- React.createElement('div', { style: reqItemStyle }, '3.1.9.业务负责人:显示租赁合同创建时所选业务负责人,与业务部门联动,查询该业务组下人员;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.10.合同类型:显示租赁合同类型,类型包括:正式合同、试用合同,于创建租赁合同时选取;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.11.合同审批类型:显示租赁合同创建时所选合同审批类型,包括:标准合同审批、非标准合同审批;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.12.合同结束日期:显示租赁合同结束日期,精确至日,格式为YYYY-MM-DD;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.13.客户联系人:显示租赁合同客户联系人姓名,客户联系人姓名来自「客户管理」-「联系人」;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.14.联系电话:显示租赁合同客户联系电话,客户联系电话来自「客户管理」-「联系人手机」;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.15.创建人:显示租赁合同创建人姓名,取自操作用户姓名;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.16.创建时间:显示租赁合同创建时间,精确至分钟,格式为YYYY-MM-DD HH:MM;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.17.更新人:显示租赁合同最后一次更新人姓名,取自操作用户姓名,如无则显示:-;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.18.最后更新时间:显示租赁合同最后一次更新时间,精确至分钟,格式为YYYY-MM-DD HH:MM,如无则显示:-;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.19.备注:显示租赁合同创建时输入的备注信息,如无则显示:-;'),
- React.createElement('div', { style: reqItemStyle }, '3.1.20.操作:操作分为:查看、编辑、新增车辆、续签合同、删除合同、撤回合同、添加被授权人、附加费用、变更为三方合同、转正式合同、终止合同、上传盖章合同;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.1.查看:跳转车辆租赁合同-查看;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.2.编辑:当「合同状态」为「草稿」时显示,点击跳转编辑租赁合同页面,编辑租赁合同页面可输入项参考「新增租赁合同」页面,并支持对保存时已填内容进行修改;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.3.新增车辆:当「合同状态」为「合同进行中」时显示,仅能在租赁订单信息卡片下新订单中进行车辆新增;新增车辆提交后触发重新审核流程,审核通过后生效(不影响原有合同正常业务);'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.4.续签合同:当「合同状态」为「合同进行中」、「合同到期」时显示,点击跳转车辆租赁合同-续签合同页,提交时重新触发租赁合同审核流程;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.5.删除合同:当「合同状态」为「草稿」时显示,点击删除合同时进行二次确认,提示语:是否确认删除该合同草稿;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.6.撤回合同:当「合同状态」为「已提交审核」时显示,点击撤回合同时进行二次确认,提示语:是否确认撤回该合同;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.7.添加被授权人:当「合同状态」为「合同进行中」时显示,点击弹出卡片,卡片中可编辑被授权人、被授权人联系电话、被授权人身份证,同时支持添加一行、删除已有行等操作;添加被授权人触发重新审核流程,审核通过后生效;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.附加费用:当「合同状态」为「合同进行中」时显示,点击弹出卡片,卡片中显示该租赁合同内:车辆类型、品牌、型号、车牌号,同时可对服务项目、费用、生效时间进行编辑,触发租赁合同审核流程,审核通过后生效;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.1.服务项目:代处理费用、罚款、违章处理违约金、未参加安全培训、车辆出险、年检年审违约、停车费、设备损坏金(包含易损件)、清洗费、上门收车人工费、上门收车送车行驶费、上门收车基础服务费、保险上浮、保养费用、补办驾驶证、补办牌照、补办营运证、补办加氢证、借用备用钥匙、补配钥匙、租金、氢气费-客、退还车氢量差、能源费补缴、能源费退款、送车上门人工费、送车上门送车行驶费、送车上门基础服务费、保证金、氢气预付费、维修费用、ETC-客、ETC卡缺损费、ETC设备缺损费、电费-客、未结算保养费、未结算维修费、车损费、工具损坏或丢失费、证件费、广告损坏费、送车服务费、接车服务费、补办行驶证、超赔险、轮胎磨损费、无忧包、轮胎保、养护保、尾板;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.2.费用:输入框,后缀为元,支持2位小数输入;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.8.3.生效时间:日期选择器,精确至日;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.9.变更为三方合同:当「合同状态」为「合同进行中」时显示,点击跳转车辆租赁合同-变更为三方合同页面,提交时重新触发租赁合同审核流程;;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.10.转正式合同:当「合同类型」为「试用合同」时显示,点击后跳转车辆租赁合同-转正式合同页面,提交时重新触发租赁合同审核流程;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.11.终止合同:当「合同状态」为「合同进行中」时显示,点击后进行二次确认,提示语:是否确认终止合同,确认按钮为提交审核,会重新发起合同审核流程,审核通过后,合同状态变更为:已结束;'),
- React.createElement('div', { style: reqSubItemStyle }, '3.1.20.12.上传盖章合同:当「审批状态」为「审批通过」且法务审核环节尚未上传盖章合同附件时,在「更多」中显示,仅法务部员工可见并可操作;点击后弹出上传窗口,可浏览本地文件上传,支持多文件;上传完成后关闭该入口(列表不再展示「上传盖章合同」);')
))
)
);
diff --git a/src/prototypes/lease-contract-management/LeaseContractPreviewPanel.jsx b/src/prototypes/lease-contract-management/LeaseContractPreviewPanel.jsx
index 908d3aa..65d9fef 100644
--- a/src/prototypes/lease-contract-management/LeaseContractPreviewPanel.jsx
+++ b/src/prototypes/lease-contract-management/LeaseContractPreviewPanel.jsx
@@ -33,6 +33,26 @@ function EditablePreviewPageBody(props) {
});
}
+export function PreviewEditableHint() {
+ return React.createElement('span', {
+ className: 'lc-form-card__hint lc-preview-editable-hint lc-preview-editable-hint--inline',
+ role: 'note',
+ 'data-annotation-id': 'lc-preview-editable-hint',
+ id: 'lc-preview-editable-hint',
+ },
+ React.createElement(Info, {
+ size: 13,
+ className: 'lc-form-card__hint-icon',
+ 'aria-hidden': true,
+ }),
+ React.createElement('span', { className: 'lc-form-card__hint-text lc-preview-editable-hint__text' },
+ '正文可编辑;改动',
+ React.createElement('span', { className: 'lc-preview-editable-hint__risk' }, '风控红线条款'),
+ '走非标准审批,未改则标准审批。',
+ ),
+ );
+}
+
function previewPagerIcon(name) {
var common = {
xmlns: 'http://www.w3.org/2000/svg',
@@ -143,31 +163,6 @@ export default function LeaseContractPreviewPanel({
);
}
- var editableHint = editable
- ? React.createElement('div', {
- className: 'lc-preview-editable-hint',
- role: 'note',
- 'data-annotation-id': 'lc-preview-editable-hint',
- },
- React.createElement('div', { className: 'lc-preview-editable-hint__card' },
- React.createElement(Info, {
- size: 16,
- className: 'lc-preview-editable-hint__icon',
- 'aria-hidden': true,
- }),
- React.createElement('p', { className: 'lc-preview-editable-hint__text' },
- '预览正文可直接修改。若修改了',
- React.createElement('span', { className: 'lc-preview-editable-hint__risk' }, '风控红线条款'),
- ',提交时将自动走',
- React.createElement('span', { className: 'lc-preview-editable-hint__tag lc-preview-editable-hint__tag--nonstd' }, '非标准合同审批'),
- ';未改动风控条款则走',
- React.createElement('span', { className: 'lc-preview-editable-hint__tag lc-preview-editable-hint__tag--std' }, '标准审批'),
- '。',
- ),
- ),
- )
- : null;
-
var previewToolbar = React.createElement('div', { className: 'ct-preview-toolbar' },
React.createElement('div', { className: 'ct-preview-toolbar__row ct-preview-toolbar__row--primary', role: 'toolbar', 'aria-label': '预览缩放' },
React.createElement('div', { className: 'ct-preview-pager__group', role: 'group', 'aria-label': '缩放' },
@@ -275,7 +270,6 @@ export default function LeaseContractPreviewPanel({
});
return React.createElement('div', { className: 'ct-preview-doc ct-word-doc ct-word-paginated' + (editable ? ' lc-preview-doc--editable' : '') },
- editableHint,
previewToolbar,
React.createElement('div', { className: 'ct-preview-body-shell' },
React.createElement('div', { className: 'ct-preview-pages-wrap' },
diff --git a/src/prototypes/lease-contract-management/annotation-source.json b/src/prototypes/lease-contract-management/annotation-source.json
index d82dfed..aa86d9a 100644
--- a/src/prototypes/lease-contract-management/annotation-source.json
+++ b/src/prototypes/lease-contract-management/annotation-source.json
@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "lease-contract-management",
"pageId": "list",
- "updatedAt": 1782494384325,
+ "updatedAt": 1782810809220,
"nodes": [
{
"id": "lc-list-filter",
@@ -190,10 +190,10 @@
]
},
"markdownMap": {
- "lc-list-filter": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---",
+ "lc-list-filter": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---",
"lc-list-kpi": "## 5. KPI 统计卡片\n\n共 5 张卡片,点击切换列表筛选(与筛选区叠加生效)。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部合同 | 不做 KPI 维度过滤 |\n| 进行中 | 合同状态为「合同进行中」且结束日期 ≥ 今日 |\n| 已终止 | 合同状态为「已终止」或「已结束」 |\n| 临期合同 | 距离合同结束日期 ≤ 30 天且未过期 |\n| 过期合同 | 合同状态为「已过期」或「到期合同」 |\n\n卡片右上角 `?` 悬停展示口径说明。\n\n---",
"lc-list-toolbar": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 租赁费用模板 | 跳转或打开费用模板配置(本期原型 Toast) |\n| 导出 | 按当前筛选结果导出(本期原型 Toast) |\n| 新增 | 进入「新增租赁合同」页 |\n\n---",
- "lc-list-table": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |\n| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 签约公司 | 创建时所选甲方签约主体简称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 已提交审批且非驳回 | 二次确认后撤回 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传",
+ "lc-list-table": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回;上方状态、下方当前审批人(通过/驳回/撤回不显示);待审批/审批中悬停展示审批流 |\n| 合同状态 | 仅保存未提交审批为草稿;提交审批后为已提交审批;另有变更 / 合同进行中 / 到期合同 / 已结束等 |\n| 签约公司 | 合同甲方名称全称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 待审批或审批中 | 二次确认后审批状态变更为撤回,合同状态回草稿 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传",
"lc-create-main-contract": "## 2. 主体合同信息\n\n卡片内三个可折叠区块,标题行右侧展示区块说明(展开时显示在标题后方)。\n\n### 2.1 签约信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 合同编号 | 是 | 前缀 `HT-ZL-` + 用户输入 |\n| 甲方 | 是 | 出租方签约主体,来自组织机构 |\n| 乙方 | 是 | 客户选择器;仅 A/B 级客户可选 |\n\n选择甲方/乙方后,下方只读面板自动带出档案:\n\n**甲方**:户名、开户行、账号、通讯地址、联系人、电话、邮箱。\n\n**乙方**:企业名称、开户银行、银行账号、纳税人识别号、企业地址、企业电话、通讯地址、联系人、电话、邮箱;资质证照缩略图可点击放大。\n\n未选双方时展示空状态引导。\n\n### 2.2 里程标准\n\n对应合同正文最低里程与租金减免条款。\n\n| 字段 | 说明 |\n|---|---|\n| 是否有最低里程要求 | 开关 |\n| 统计周期 | 有要求时必选:月 / 季 / 年 |\n| 目标里程 | 有要求时必填(公里) |\n| 里程类型 | 多选标签:实际里程 / 仪表里程 等 |\n\n### 2.3 费用信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 付款周期 | 是 | 如月付、季付 |\n| 氢费支付方式 | 是 | 后付 / 预付 |\n| 预付金额 | 条件 | 预付时必填 |\n| 提前付款工作日 | 条件 | 预付时必填 |\n| 还车氢量差单价 | 是 | 元 |\n\n**完成度**:上述必填项均已填则卡片显示「已完成」。\n\n---",
"lc-create-lease-order": "## 3. 附件1:租赁订单\n\n| 区域 | 说明 |\n|---|---|\n| 投保车辆数 / 三者责任险(百万) | 三者责任险必填 |\n| 交车区域 | 省市区级联 |\n| 计划交车日期 | 日期 |\n| 订单车辆表 | 品牌型号级联、车牌、租期、租金、氢费、增值服务等 |\n\n支持新增行、复制行、删除行;表头 KPI 展示合计。\n\n**完成度**:三者责任险已填即视为完成(原型简化口径)。\n\n---",
"lc-create-poa": "## 4. 授权委托书\n\n被委托人表格:姓名、联系方式、身份证号。\n\n支持新增行、删除行。\n\n**完成度**:至少一行且三项均非空。\n\n---",
@@ -203,6 +203,198 @@
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "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": "lc-doc-root",
@@ -219,7 +411,7 @@
"type": "link",
"id": "lc-link-template",
"title": "合同模板管理",
- "href": "/prototypes/contract-template-management",
+ "href": "../contract-template-management/index.html",
"target": "self"
},
{
@@ -239,7 +431,7 @@
"type": "markdown",
"id": "lc-doc-prd-list",
"title": "列表页 PRD",
- "markdown": "# 租赁合同管理 · 列表页 PRD\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 租赁合同管理 |\n| 所属系统 | ONE-OS 业务管理 |\n| 面包屑 | 业务管理 → 租赁合同管理 |\n| 目标用户 | 业务人员(主)、法务(盖章上传)、客服/运营(查看) |\n| 核心任务 | 筛选与查看租赁合同台账,发起新增/变更/续签等流程 |\n| 交互原型 | `/prototypes/lease-contract-management` |\n\n---\n\n## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---\n\n## 5. KPI 统计卡片\n\n共 5 张卡片,点击切换列表筛选(与筛选区叠加生效)。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部合同 | 不做 KPI 维度过滤 |\n| 进行中 | 合同状态为「合同进行中」且结束日期 ≥ 今日 |\n| 已终止 | 合同状态为「已终止」或「已结束」 |\n| 临期合同 | 距离合同结束日期 ≤ 30 天且未过期 |\n| 过期合同 | 合同状态为「已过期」或「到期合同」 |\n\n卡片右上角 `?` 悬停展示口径说明。\n\n---\n\n## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 租赁费用模板 | 跳转或打开费用模板配置(本期原型 Toast) |\n| 导出 | 按当前筛选结果导出(本期原型 Toast) |\n| 新增 | 进入「新增租赁合同」页 |\n\n---\n\n## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |\n| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 签约公司 | 创建时所选甲方签约主体简称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 已提交审批且非驳回 | 二次确认后撤回 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传\n",
+ "markdown": "# 租赁合同管理 · 列表页 PRD\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 租赁合同管理 |\n| 所属系统 | ONE-OS 业务管理 |\n| 面包屑 | 业务管理 → 租赁合同管理 |\n| 目标用户 | 业务人员(主)、法务(盖章上传)、客服/运营(查看) |\n| 核心任务 | 筛选与查看租赁合同台账,发起新增/变更/续签等流程 |\n| 交互原型 | `/prototypes/lease-contract-management` |\n\n---\n\n## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---\n\n## 5. KPI 统计卡片\n\n共 5 张卡片,点击切换列表筛选(与筛选区叠加生效)。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部合同 | 不做 KPI 维度过滤 |\n| 进行中 | 合同状态为「合同进行中」且结束日期 ≥ 今日 |\n| 已终止 | 合同状态为「已终止」或「已结束」 |\n| 临期合同 | 距离合同结束日期 ≤ 30 天且未过期 |\n| 过期合同 | 合同状态为「已过期」或「到期合同」 |\n\n卡片右上角 `?` 悬停展示口径说明。\n\n---\n\n## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 租赁费用模板 | 跳转或打开费用模板配置(本期原型 Toast) |\n| 导出 | 按当前筛选结果导出(本期原型 Toast) |\n| 新增 | 进入「新增租赁合同」页 |\n\n---\n\n## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回;上方状态、下方当前审批人(通过/驳回/撤回不显示);待审批/审批中悬停展示审批流 |\n| 合同状态 | 仅保存未提交审批为草稿;提交审批后为已提交审批;另有变更 / 合同进行中 / 到期合同 / 已结束等 |\n| 签约公司 | 合同甲方名称全称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 待审批或审批中 | 二次确认后审批状态变更为撤回,合同状态回草稿 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传\n",
"markdownPath": "src/prototypes/lease-contract-management/.spec/requirements-prd-list.md"
},
{
@@ -276,7 +468,7 @@
"type": "markdown",
"id": "lc-doc-node-lc-list-filter",
"title": "筛选条件",
- "markdown": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---"
+ "markdown": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---"
},
{
"type": "markdown",
@@ -294,7 +486,7 @@
"type": "markdown",
"id": "lc-doc-node-lc-list-table",
"title": "合同台账列表",
- "markdown": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |\n| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 签约公司 | 创建时所选甲方签约主体简称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 已提交审批且非驳回 | 二次确认后撤回 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传"
+ "markdown": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回;上方状态、下方当前审批人(通过/驳回/撤回不显示);待审批/审批中悬停展示审批流 |\n| 合同状态 | 仅保存未提交审批为草稿;提交审批后为已提交审批;另有变更 / 合同进行中 / 到期合同 / 已结束等 |\n| 签约公司 | 合同甲方名称全称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 待审批或审批中 | 二次确认后审批状态变更为撤回,合同状态回草稿 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传"
}
]
},
diff --git a/src/prototypes/lease-contract-management/index.tsx b/src/prototypes/lease-contract-management/index.tsx
index 01f668a..99c66d5 100644
--- a/src/prototypes/lease-contract-management/index.tsx
+++ b/src/prototypes/lease-contract-management/index.tsx
@@ -13,6 +13,7 @@ import {
import 'antd/dist/reset.css';
import '../vehicle-management/style.css';
import './styles/index.css';
+import '../contract-template-management/contract-template-catalog.js';
import LeaseContractApp from './LeaseContractManagement.jsx';
import annotationSourceDocument from './annotation-source.json';
diff --git a/src/prototypes/lease-contract-management/lease-business-org-data.js b/src/prototypes/lease-contract-management/lease-business-org-data.js
new file mode 100644
index 0000000..2d48c16
--- /dev/null
+++ b/src/prototypes/lease-contract-management/lease-business-org-data.js
@@ -0,0 +1,37 @@
+/**
+ * 业务部门 / 业务人员级联数据(与列表筛选项、台账样例对齐)
+ */
+
+export var LEASE_BUSINESS_ORG_TREE = [
+ { dept: '业务1部', owners: ['张经理', '赵经理'] },
+ { dept: '业务2部', owners: ['李专员', '钱专员'] },
+ { dept: '业务3部', owners: ['王专员'] },
+];
+
+export function getBusinessOwnersByDept(dept) {
+ var group = LEASE_BUSINESS_ORG_TREE.find(function (item) {
+ return item.dept === dept;
+ });
+ return group ? group.owners.slice() : [];
+}
+
+export function flattenBusinessOwnerOptions() {
+ return LEASE_BUSINESS_ORG_TREE.reduce(function (list, group) {
+ return list.concat(group.owners.map(function (owner) {
+ return { dept: group.dept, owner: owner };
+ }));
+ }, []);
+}
+
+export function matchBusinessOwnerOption(item, searchText) {
+ if (!searchText) return true;
+ var q = searchText.toLowerCase();
+ return item.owner.toLowerCase().indexOf(q) >= 0;
+}
+
+export function formatBusinessAssignmentLabel(dept, owner) {
+ if (dept && owner) return dept + ' · ' + owner;
+ if (dept) return dept;
+ if (owner) return owner;
+ return '';
+}
diff --git a/src/prototypes/lease-contract-management/lease-contract-form-validation.js b/src/prototypes/lease-contract-management/lease-contract-form-validation.js
index 9800828..fbd78c0 100644
--- a/src/prototypes/lease-contract-management/lease-contract-form-validation.js
+++ b/src/prototypes/lease-contract-management/lease-contract-form-validation.js
@@ -9,6 +9,8 @@ function isMainContractFieldsComplete(form) {
if (!contractCode) return false;
if (!form.lessorId) return false;
if (!form.customerId) return false;
+ if (!form.businessDept) return false;
+ if (!form.businessOwner) return false;
var mileage = form.mileage || {};
if (mileage.hasRequirement) {
@@ -47,23 +49,38 @@ export function isLeaseOrderFormComplete(form) {
});
}
-/** 授权委托书卡片完成度(选填;有填写时须完整) */
+/** 授权委托书卡片完成度(提交前须至少一名完整委托人,且已填行须完整) */
export function isPowerOfAttorneyFormComplete(form) {
var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || [];
var started = delegates.filter(isDelegateRowStarted);
- if (!started.length) return true;
+ if (!started.length) return false;
return started.every(isDelegateRowComplete);
}
+/** 授权委托书是否满足提交审核要求 */
+export function isPowerOfAttorneySubmitReady(form) {
+ var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || [];
+ return delegates.some(isDelegateRowComplete);
+}
+
/** 授权委托书是否有未完成行(用于徽章展示) */
export function isPowerOfAttorneyOptionalEmpty(form) {
var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || [];
return !delegates.some(isDelegateRowStarted);
}
-/** 判断新增租赁合同左侧表单必填项是否均已填写 */
+/** 用章类型是否已选择 */
+export function isSealTypeSelected(form) {
+ if (Array.isArray(form.sealTypes)) {
+ return form.sealTypes.length > 0;
+ }
+ return Boolean(form.sealType);
+}
+
+/** 判断新增租赁合同左侧表单必填项是否均已填写(提交审核) */
export function isLeaseContractFormComplete(form) {
return isMainContractFieldsComplete(form)
&& isLeaseOrderFormComplete(form)
- && isPowerOfAttorneyFormComplete(form);
+ && isPowerOfAttorneySubmitReady(form)
+ && isSealTypeSelected(form);
}
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 3a4a73d..ad9868f 100644
--- a/src/prototypes/lease-contract-management/lease-contract-list-data.js
+++ b/src/prototypes/lease-contract-management/lease-contract-list-data.js
@@ -2,6 +2,24 @@
* 租赁合同列表样例数据 — 字段与新增/编辑页表单结构对齐
*/
+import { getLessorCompanyById } from '../contract-template-management/contract-template-vars.js';
+import { getContractTemplateOption } from '../contract-template-management/contract-template-catalog.js';
+import { getContractTypeLabel } from '../contract-template-management/contract-template-types.js';
+
+export var SIGNING_COMPANY_LESSOR_IDS = {
+ '嘉兴羚牛': 'jx',
+ '上海羚牛': 'sh',
+ '广东羚牛': 'gd',
+};
+
+export var PAYMENT_PERIOD_CYCLE_LABELS = {
+ 1: '月付',
+ 3: '季付',
+ 6: '半年付',
+ 12: '年付',
+};
+
+/** @deprecated 列表展示请用 formatPaymentPeriodLabel */
export var PAYMENT_PERIOD_LABELS = {
1: '1个月',
3: '3个月',
@@ -14,12 +32,194 @@ export var HYDROGEN_PAYMENT_LABELS = {
prepay: '预付款',
};
+var LEGACY_PAYMENT_PERIOD_MAP = {
+ '1个月': 1,
+ '3个月': 3,
+ '6个月': 6,
+ '12个月': 12,
+};
+
+var LEGACY_HYDROGEN_PAYMENT_MAP = {
+ '自行解决': 'self',
+ '预付款': 'prepay',
+};
+
+export function formatPaymentPeriodLabel(period) {
+ if (period == null || period === '') return '-';
+ var months = typeof period === 'number' ? period : LEGACY_PAYMENT_PERIOD_MAP[period];
+ if (months != null) return PAYMENT_PERIOD_CYCLE_LABELS[months] || '-';
+ return String(period);
+}
+
+export function formatHydrogenPaymentLabel(method) {
+ if (!method) return '-';
+ if (HYDROGEN_PAYMENT_LABELS[method]) return HYDROGEN_PAYMENT_LABELS[method];
+ var code = LEGACY_HYDROGEN_PAYMENT_MAP[method];
+ if (code) return HYDROGEN_PAYMENT_LABELS[code] || '-';
+ return String(method);
+}
+
export var MILEAGE_PERIOD_LABELS = {
month: '每月',
quarter: '每季度',
year: '每年度',
};
+/** 子表里程要求周期标签:每月 / 每季度 / 每年度 */
+export var CONTRACT_MILEAGE_PERIOD_TAG_LABELS = {
+ month: '每月',
+ quarter: '每季度',
+ year: '每年度',
+};
+
+export function getContractMileagePeriodTag(period) {
+ return CONTRACT_MILEAGE_PERIOD_TAG_LABELS[period] || '';
+}
+
+export function formatContractMileageTargetKm(km) {
+ if (km == null || km === '') return '-';
+ var num = Number(km);
+ if (!Number.isFinite(num)) return String(km);
+ return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 0 }) + ' km';
+}
+
+export function resolveContractVehicleMileageSource(vehicle) {
+ if (!vehicle) return '人工';
+ if (vehicle.mileageSource) return vehicle.mileageSource;
+ if (vehicle.telematicsLinked === true || vehicle.onlineStatus === '在线') return '车机';
+ if (vehicle.telematicsLinked === false && vehicle.gpsTime) return 'GPS';
+ if (vehicle.gpsTime) return 'GPS';
+ return '人工';
+}
+
+export function getContractVehicleMileageSourceTagClass(source) {
+ if (source === '车机') return 'vm-tag vm-tag-teal';
+ if (source === 'GPS') return 'vm-tag vm-tag-gps';
+ return 'vm-tag vm-tag-manual';
+}
+
+export function computeContractVehicleRemainingMileage(record, vehicle) {
+ if (!record || !record.hasMinimumMileage) return null;
+ var target = Number(record.mileageTargetKm);
+ if (!Number.isFinite(target)) return null;
+ var progress = Number(vehicle && vehicle.mileageProgress);
+ if (!Number.isFinite(progress)) progress = 0;
+ return Math.max(0, Math.round(target * (100 - progress) / 100));
+}
+
+export function formatContractVehicleRemainingMileage(km) {
+ if (km == null) return '—';
+ return '剩余 ' + Number(km).toLocaleString('zh-CN') + ' km';
+}
+
+var CONTRACT_TYPE_FULL_NAME_FALLBACK = {
+ '正式合同': '现代18吨正式合同',
+ '试用合同': '现代18吨试用合同',
+};
+
+var RETURN_SETTLEMENT_STATUSES = ['待提交', '审批中', '已结算'];
+
+export function parseContractDateOnly(raw) {
+ if (raw == null || raw === '' || raw === '-') return null;
+ var text = String(raw).trim().slice(0, 10);
+ var parts = text.split('-');
+ if (parts.length !== 3) return null;
+ var date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
+ return Number.isNaN(date.getTime()) ? null : date;
+}
+
+export function formatContractPeriodStartDate(date) {
+ if (!date) return '—';
+ var y = date.getFullYear();
+ var m = String(date.getMonth() + 1).padStart(2, '0');
+ var d = String(date.getDate()).padStart(2, '0');
+ return y + '-' + m + '-' + d;
+}
+
+function daysBetween(startDate, endDate) {
+ var ms = endDate.getTime() - startDate.getTime();
+ return Math.max(0, Math.floor(ms / 86400000));
+}
+
+export function computeCurrentMileagePeriodStart(deliveryRaw, period, referenceDate) {
+ var delivery = parseContractDateOnly(deliveryRaw);
+ var ref = referenceDate instanceof Date ? referenceDate : (parseContractDateOnly(referenceDate) || new Date());
+ if (!delivery) return null;
+ if (period === 'month') {
+ var monthStart = new Date(ref.getFullYear(), ref.getMonth(), delivery.getDate());
+ if (monthStart > ref) monthStart.setMonth(monthStart.getMonth() - 1);
+ if (monthStart < delivery) return new Date(delivery.getTime());
+ return monthStart;
+ }
+ if (period === 'quarter') {
+ var quarterStart = new Date(delivery.getTime());
+ while (true) {
+ var nextQuarter = new Date(quarterStart.getFullYear(), quarterStart.getMonth() + 3, quarterStart.getDate());
+ if (nextQuarter > ref) return quarterStart;
+ quarterStart = nextQuarter;
+ }
+ }
+ if (period === 'year') {
+ var yearStart = new Date(ref.getFullYear(), delivery.getMonth(), delivery.getDate());
+ if (yearStart > ref) yearStart.setFullYear(yearStart.getFullYear() - 1);
+ if (yearStart < delivery) return new Date(delivery.getTime());
+ return yearStart;
+ }
+ return new Date(delivery.getTime());
+}
+
+export function estimatePeriodStartMileage(deliveryRaw, periodStart, deliveryMileage, currentMileage) {
+ var delivery = parseContractDateOnly(deliveryRaw);
+ var start = periodStart instanceof Date ? periodStart : parseContractDateOnly(periodStart);
+ var deliveryMile = Number(deliveryMileage);
+ var currentMile = Number(currentMileage);
+ if (!Number.isFinite(deliveryMile)) deliveryMile = 0;
+ if (!Number.isFinite(currentMile)) currentMile = deliveryMile;
+ if (!delivery || !start) return deliveryMile;
+ if (start.getTime() <= delivery.getTime()) return deliveryMile;
+ var ref = new Date();
+ var totalDays = Math.max(1, daysBetween(delivery, ref));
+ var periodDays = Math.max(0, daysBetween(start, ref));
+ var driven = Math.max(0, currentMile - deliveryMile);
+ var ratioBeforePeriod = Math.max(0, Math.min(1, 1 - periodDays / totalDays));
+ var startMileage = deliveryMile + Math.round(driven * ratioBeforePeriod);
+ return Math.min(currentMile, Math.max(deliveryMile, startMileage));
+}
+
+export function inferContractTemplateId(record) {
+ if (record && record.contractTemplateId) return record.contractTemplateId;
+ var contractType = record && record.contractType;
+ if (contractType === '试用合同') {
+ var vehicles = record.vehicles || [];
+ var has18t = vehicles.some(function (vehicle) {
+ var text = String(vehicle.model || vehicle.vehicleType || '');
+ return /18/.test(text);
+ });
+ return has18t ? 'doc-5' : 'doc-4';
+ }
+ if (contractType === '正式合同') return 'doc-2';
+ return '';
+}
+
+export function resolveContractTemplateTypeLabel(record) {
+ if (!record) return '—';
+ if (record.contractTemplateTypeLabel) return record.contractTemplateTypeLabel;
+ var templateId = inferContractTemplateId(record);
+ if (templateId) {
+ var option = getContractTemplateOption(templateId);
+ if (option && option.contractTypeLabel) return option.contractTypeLabel;
+ }
+ var shortType = record.contractType || '';
+ if (CONTRACT_TYPE_FULL_NAME_FALLBACK[shortType]) return CONTRACT_TYPE_FULL_NAME_FALLBACK[shortType];
+ return getContractTypeLabel(shortType);
+}
+
+export function getReturnSettlementStatusTone(status) {
+ if (status === '已结算') return 'green';
+ if (status === '审批中') return 'amber';
+ return 'gray';
+}
+
export function formatMileageSummary(record) {
if (!record.hasMinimumMileage) return '无最低里程要求';
var period = MILEAGE_PERIOD_LABELS[record.mileagePeriod] || '';
@@ -27,11 +227,67 @@ export function formatMileageSummary(record) {
return period + km + ' 公里';
}
+/** 合同子表车辆行:无里程要求时显示「无里程要求」 */
+export function formatContractMileageRequirement(record) {
+ if (!record || !record.hasMinimumMileage) return '无里程要求';
+ var period = MILEAGE_PERIOD_LABELS[record.mileagePeriod] || '';
+ var km = record.mileageTargetKm != null ? record.mileageTargetKm : '-';
+ return period + km + ' 公里';
+}
+
+/** 与车辆管理 formatMileage 一致:车机在线且空值时展示 0 km */
+export function formatContractVehicleCurrentMileage(mile, vehicle, emptyFallback) {
+ var empty = emptyFallback != null ? emptyFallback : '—';
+ var source = resolveContractVehicleMileageSource(vehicle);
+ var forceZero = source === '车机' && vehicle && vehicle.onlineStatus === '在线';
+ if (mile == null || mile === '' || mile === '-') {
+ return forceZero ? '0 km' : empty;
+ }
+ var num = Number(mile);
+ if (!Number.isFinite(num)) return String(mile);
+ return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) + ' km';
+}
+
+export function isContractVehicleMileageOnline(vehicle) {
+ return vehicle && vehicle.onlineStatus === '在线';
+}
+
export function formatDeliveryRegion(region) {
if (!region || !region.length) return '-';
return region.join(' / ');
}
+var LEGACY_CONTRACT_APPROVAL_TYPE_MAP = {
+ '标准合同审批': '标准合同',
+ '非标准合同审批': '非标准合同',
+};
+
+export function formatContractApprovalTypeLabel(value) {
+ if (!value) return '-';
+ return LEGACY_CONTRACT_APPROVAL_TYPE_MAP[value] || value;
+}
+
+var SAMPLE_AUTHORIZED_DELEGATES = [
+ { name: '王明', contact: '13900139001', idNumber: '330102199001011234' },
+ { name: '李华', contact: '13900139002', idNumber: '330102199002022345' },
+ { name: '赵强', contact: '13900139003', idNumber: '330102199003033456' },
+ { name: '陈静', contact: '13900139004', idNumber: '330102199004044567' },
+ { name: '周磊', contact: '13900139005', idNumber: '330102199005055678' },
+];
+
+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]);
+ }
+ return list;
+}
+
export var LEASE_CONTRACT_LIST_RECORDS = [
{
id: '1',
@@ -49,11 +305,19 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部',
businessOwner: '张经理',
contractType: '正式合同',
- contractApprovalType: '标准合同审批',
- paymentPeriod: '1个月',
- hydrogenPaymentMethod: '自行解决',
+ contractApprovalType: '标准合同',
+ paymentPeriod: 1,
+ hydrogenPaymentMethod: 'self',
deliveryRegion: ['浙江省', '嘉兴市'],
deliveryDate: '2025-03-01',
+ deliveryDateChangeLogs: [
+ {
+ beforeDate: '2025-02-28',
+ afterDate: '2025-03-01',
+ operatorName: '张经理',
+ operateTime: '2025-02-10 16:20:00',
+ },
+ ],
insuredVehicleCount: 2,
thirdPartyLiabilityMillion: 200,
hasMinimumMileage: true,
@@ -85,9 +349,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务2部',
businessOwner: '李专员',
contractType: '试用合同',
- contractApprovalType: '非标准合同审批',
- paymentPeriod: '3个月',
- hydrogenPaymentMethod: '预付款',
+ contractApprovalType: '非标准合同',
+ paymentPeriod: 3,
+ hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['上海市', '上海市'],
deliveryDate: '2025-02-20',
insuredVehicleCount: 1,
@@ -121,9 +385,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务3部',
businessOwner: '王专员',
contractType: '正式合同',
- contractApprovalType: '标准合同审批',
- paymentPeriod: '1个月',
- hydrogenPaymentMethod: '自行解决',
+ contractApprovalType: '标准合同',
+ paymentPeriod: 1,
+ hydrogenPaymentMethod: 'self',
deliveryRegion: ['浙江省', '杭州市'],
deliveryDate: '2025-03-10',
insuredVehicleCount: 1,
@@ -141,6 +405,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '-',
remark: '-',
legalStampedContractUploaded: undefined,
+ approvalFlowNodes: [
+ { nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['王专员'] },
+ ],
},
{
id: '4',
@@ -158,9 +425,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部',
businessOwner: '张经理',
contractType: '正式合同',
- contractApprovalType: '非标准合同审批',
- paymentPeriod: '6个月',
- hydrogenPaymentMethod: '预付款',
+ contractApprovalType: '非标准合同',
+ paymentPeriod: 6,
+ hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['浙江省', '宁波市'],
deliveryDate: '2025-04-01',
insuredVehicleCount: 2,
@@ -179,7 +446,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
remark: '-',
legalStampedContractUploaded: undefined,
approvalFlowNodes: [
- { nodeTitle: '业务服务主管审批', result: 'passed', operatorName: '姚守涛', operatorTime: '2026-04-29 17:57:15' },
+ { nodeTitle: '业务服务主管审批', result: 'passed', operatorName: '姚守涛', operatorTime: '2026-04-29 17:57:15', comment: '租赁订单车辆配置与报价一致,同意进入下一节点。' },
{ nodeTitle: '发起审批', result: 'passed', operatorName: '超级用户', operatorTime: '2026-04-28 17:44:45' },
{ nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['超级用户', '金可鹏'] },
],
@@ -199,9 +466,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务2部',
businessOwner: '李专员',
contractType: '正式合同',
- contractApprovalType: '标准合同审批',
- paymentPeriod: '3个月',
- hydrogenPaymentMethod: '自行解决',
+ contractApprovalType: '标准合同',
+ paymentPeriod: 3,
+ hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '苏州市'],
deliveryDate: '2025-03-15',
insuredVehicleCount: 1,
@@ -220,8 +487,8 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
remark: '变更车辆数量',
legalStampedContractUploaded: undefined,
approvalFlowNodes: [
- { nodeTitle: '法务审核', result: 'passed', operatorName: '李法务', operatorTime: '2025-02-21 18:00:00' },
- { nodeTitle: '发起审批', result: 'passed', operatorName: '李专员', operatorTime: '2025-02-22 10:00:00' },
+ { nodeTitle: '法务审核', result: 'passed', operatorName: '李法务', operatorTime: '2025-02-21 18:00:00', comment: '变更后条款已复核,未发现新增合规风险。' },
+ { nodeTitle: '发起审批', result: 'passed', operatorName: '李专员', operatorTime: '2025-02-22 10:00:00', comment: '因客户增购 1 台车辆发起变更审批。' },
{ nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['张经理', '赵总监'] },
],
},
@@ -231,9 +498,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
projectName: '南京氢能示范项目',
vehicleCount: 3,
vehicles: [
- { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏A60006', actualDelivery: '2025-01-20 08:00', deliveryPerson: '吴运维' },
- { vehicleType: '18吨厢式货车', brand: '现代', model: '18吨氢燃料电池车', plateNo: '苏A60007', actualDelivery: '2025-01-21 10:00', deliveryPerson: '郑运维' },
- { vehicleType: '牵引车头', brand: '飞驰', model: '集卡头', plateNo: '苏A60008', actualDelivery: '2025-01-22 14:00', deliveryPerson: '冯运维' },
+ { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏A60006', actualDelivery: '2025-01-20 08:00', deliveryPerson: '吴运维', leasePeriodMonths: 24 },
+ { vehicleType: '18吨厢式货车', brand: '现代', model: '18吨氢燃料电池车', plateNo: '苏A60007', actualDelivery: '2025-01-21 10:00', deliveryPerson: '郑运维', leasePeriodMonths: 24 },
+ { vehicleType: '牵引车头', brand: '飞驰', model: '集卡头', plateNo: '苏A60008', actualDelivery: '2025-01-22 14:00', deliveryPerson: '冯运维', leasePeriodMonths: 24 },
],
approvalStatus: '审批通过',
contractStatus: '合同进行中',
@@ -242,9 +509,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务3部',
businessOwner: '王专员',
contractType: '正式合同',
- contractApprovalType: '标准合同审批',
- paymentPeriod: '12个月',
- hydrogenPaymentMethod: '预付款',
+ contractApprovalType: '标准合同',
+ paymentPeriod: 12,
+ hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['江苏省', '南京市'],
deliveryDate: '2025-01-15',
insuredVehicleCount: 3,
@@ -262,6 +529,12 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2025-01-25 11:00',
remark: '-',
legalStampedContractUploaded: true,
+ approvalFlowNodes: [
+ { nodeTitle: '法务审核', result: 'passed', operatorName: '陈法务', operatorTime: '2025-01-22 16:40:00', comment: '标准合同审批,正文无红线条款改动。' },
+ { nodeTitle: '业务负责人审批', result: 'passed', operatorName: '赵总监', operatorTime: '2025-01-20 11:05:00', comment: '客户为 A 级客户,三车批量租赁方案可行,同意通过。' },
+ { nodeTitle: '业务服务主管审批', result: 'passed', operatorName: '刘主管', operatorTime: '2025-01-18 15:20:00', comment: '氢费预付条款已与客户确认。' },
+ { nodeTitle: '发起审批', result: 'passed', operatorName: '王专员', operatorTime: '2025-01-16 09:30:00' },
+ ],
},
{
id: '7',
@@ -269,7 +542,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
projectName: '无锡试用租赁项目',
vehicleCount: 1,
vehicles: [
- { vehicleType: '小客车', brand: '宇通', model: '氢燃料电池客车', plateNo: '苏B70007', actualDelivery: '2025-02-01 09:30', deliveryPerson: '陈运维' },
+ { vehicleType: '小客车', brand: '宇通', model: '氢燃料电池客车', plateNo: '苏B70007', actualDelivery: '2025-02-01 09:30', deliveryPerson: '陈运维', leasePeriodMonths: 3 },
],
approvalStatus: '审批通过',
contractStatus: '合同进行中',
@@ -278,9 +551,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部',
businessOwner: '张经理',
contractType: '试用合同',
- contractApprovalType: '非标准合同审批',
- paymentPeriod: '1个月',
- hydrogenPaymentMethod: '自行解决',
+ contractApprovalType: '非标准合同',
+ paymentPeriod: 1,
+ hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '无锡市'],
deliveryDate: '2025-02-05',
insuredVehicleCount: 1,
@@ -298,6 +571,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '-',
remark: '试用 3 个月',
legalStampedContractUploaded: false,
+ approvalFlowNodes: [
+ { nodeTitle: '法务审核', result: 'passed', operatorName: '周法务', operatorTime: '2025-01-30 17:10:00', comment: '试用合同条款已按非标准审批要求补充说明。' },
+ { nodeTitle: '业务负责人审批', result: 'passed', operatorName: '张经理', operatorTime: '2025-01-29 10:22:00', comment: '试用期 3 个月,到期后转正式合同。' },
+ { nodeTitle: '发起审批', result: 'passed', operatorName: '张经理', operatorTime: '2025-01-28 10:15:00' },
+ ],
},
{
id: '8',
@@ -315,9 +593,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务2部',
businessOwner: '李专员',
contractType: '正式合同',
- contractApprovalType: '标准合同审批',
- paymentPeriod: '3个月',
- hydrogenPaymentMethod: '预付款',
+ contractApprovalType: '标准合同',
+ paymentPeriod: 3,
+ hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['江苏省', '常州市'],
deliveryDate: '2025-04-20',
insuredVehicleCount: 2,
@@ -335,6 +613,10 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2025-02-23 09:00',
remark: '驳回原因:费用条款需调整',
legalStampedContractUploaded: undefined,
+ approvalFlowNodes: [
+ { nodeTitle: '发起审批', result: 'passed', operatorName: '李专员', operatorTime: '2025-02-20 14:30:00' },
+ { nodeTitle: '业务负责人审批', result: 'rejected', operatorName: '张经理', operatorTime: '2025-02-23 09:00:00', comment: '预付金额与商务报价不一致,请按最新报价单调整费用条款后重新提交。' },
+ ],
},
{
id: '9',
@@ -342,8 +624,8 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
projectName: '南通去年到期项目',
vehicleCount: 2,
vehicles: [
- { vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '9.6米氢燃料电池车', plateNo: '苏F90009', actualDelivery: '2024-03-01 09:00', deliveryPerson: '褚运维' },
- { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏F90010', actualDelivery: '2024-03-02 10:00', deliveryPerson: '卫运维' },
+ { vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '9.6米氢燃料电池车', plateNo: '苏F90009', actualDelivery: '2024-03-01 09:00', deliveryPerson: '褚运维', leasePeriodMonths: 12 },
+ { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏F90010', actualDelivery: '2024-03-02 10:00', deliveryPerson: '卫运维', leasePeriodMonths: 12 },
],
approvalStatus: '审批通过',
contractStatus: '到期合同',
@@ -352,9 +634,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务3部',
businessOwner: '王专员',
contractType: '正式合同',
- contractApprovalType: '标准合同审批',
- paymentPeriod: '6个月',
- hydrogenPaymentMethod: '自行解决',
+ contractApprovalType: '标准合同',
+ paymentPeriod: 6,
+ hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '南通市'],
deliveryDate: '2024-02-15',
insuredVehicleCount: 2,
@@ -372,6 +654,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2024-12-20 16:00',
remark: '已到期可续签',
legalStampedContractUploaded: true,
+ approvalFlowNodes: [
+ { nodeTitle: '法务审核', result: 'passed', operatorName: '陈法务', operatorTime: '2024-02-25 16:00:00', comment: '归档合同与系统版本一致。' },
+ { nodeTitle: '业务负责人审批', result: 'passed', operatorName: '王专员', operatorTime: '2024-02-22 14:30:00', comment: '南通区域试点项目,同意按标准流程审批。' },
+ { nodeTitle: '发起审批', result: 'passed', operatorName: '王专员', operatorTime: '2024-02-20 10:30:00' },
+ ],
},
{
id: '10',
@@ -388,9 +675,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部',
businessOwner: '张经理',
contractType: '正式合同',
- contractApprovalType: '非标准合同审批',
- paymentPeriod: '1个月',
- hydrogenPaymentMethod: '自行解决',
+ contractApprovalType: '非标准合同',
+ paymentPeriod: 1,
+ hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '镇江市'],
deliveryDate: '2024-05-10',
insuredVehicleCount: 1,
@@ -408,5 +695,322 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2025-01-10 14:00',
remark: '-',
legalStampedContractUploaded: true,
+ approvalFlowNodes: [
+ { nodeTitle: '业务负责人审批', result: 'passed', operatorName: '张经理', operatorTime: '2024-05-22 11:00:00', comment: '单车试用转正式合作,条款无异常。' },
+ { nodeTitle: '发起审批', result: 'passed', operatorName: '张经理', operatorTime: '2024-05-20 09:10:00' },
+ ],
+ },
+ {
+ id: '11',
+ contractCode: 'HT-ZL-2025-011',
+ projectName: '温州撤回示例项目',
+ vehicleCount: 1,
+ vehicles: [
+ { vehicleType: '4.5吨货车', brand: '跃进', model: '4.2米冷链车', plateNo: '-', actualDelivery: '-', deliveryPerson: '-' },
+ ],
+ approvalStatus: '撤回',
+ contractStatus: '草稿',
+ customerName: '杭州某某租赁有限公司',
+ signingCompany: '嘉兴羚牛',
+ businessDept: '业务3部',
+ businessOwner: '王专员',
+ contractType: '正式合同',
+ contractApprovalType: '标准合同',
+ paymentPeriod: 1,
+ hydrogenPaymentMethod: 'self',
+ deliveryRegion: ['浙江省', '温州市'],
+ deliveryDate: '2025-05-01',
+ insuredVehicleCount: 1,
+ thirdPartyLiabilityMillion: 200,
+ hasMinimumMileage: false,
+ mileagePeriod: 'month',
+ mileageTargetKm: null,
+ delegateCount: 0,
+ contractEndDate: '2026-04-30',
+ contactName: '王五',
+ contactPhone: '13800138003',
+ creator: '王专员',
+ createTime: '2025-03-01 10:00',
+ updater: '王专员',
+ updateTime: '2025-03-02 15:00',
+ remark: '提交后撤回',
+ legalStampedContractUploaded: undefined,
},
];
+
+export function resolveSigningCompanyFullName(record) {
+ var lessorId = record.lessorId || SIGNING_COMPANY_LESSOR_IDS[record.signingCompany];
+ var lessor = getLessorCompanyById(lessorId || record.signingCompany);
+ return lessor && lessor.legalName ? lessor.legalName : (record.signingCompany || '-');
+}
+
+export function getCurrentApproverLabel(record) {
+ if (record.currentApprover) return record.currentApprover;
+ var nodes = record.approvalFlowNodes || [];
+ var pendingNode = null;
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i].result === 'pending') {
+ pendingNode = nodes[i];
+ break;
+ }
+ }
+ if (!pendingNode) return '';
+ if (pendingNode.pendingApprovers && pendingNode.pendingApprovers.length) {
+ return pendingNode.pendingApprovers.join('、');
+ }
+ return pendingNode.nodeTitle || '';
+}
+
+export function shouldHideCurrentApprover(approvalStatus) {
+ return approvalStatus === '审批通过'
+ || approvalStatus === '审批驳回'
+ || approvalStatus === '撤回';
+}
+
+export function formatContractHandoverMileage(mile) {
+ if (mile == null || mile === '' || mile === '-') return '';
+ var num = Number(mile);
+ if (!Number.isFinite(num)) return String(mile);
+ return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) + ' km';
+}
+
+export function formatContractHandoverDateTimeMinute(raw) {
+ if (raw == null || raw === '' || raw === '-') return '';
+ var text = String(raw).trim().replace('T', ' ');
+ var match = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/);
+ if (match) return match[1] + ' ' + match[2];
+ if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return text;
+ return text.slice(0, 16);
+}
+
+export var DELIVERY_DATE_TBD_LABEL = '暂未确定';
+
+export function isDeliveryDateTbd(value) {
+ return value === DELIVERY_DATE_TBD_LABEL || value === 'TBD';
+}
+
+export function formatContractDeliveryDateLabel(record) {
+ if (!record) return '-';
+ if (record.deliveryDateTbd || isDeliveryDateTbd(record.deliveryDate)) return DELIVERY_DATE_TBD_LABEL;
+ return record.deliveryDate || '-';
+}
+
+function addMonthsToDate(date, months) {
+ var next = new Date(date.getTime());
+ next.setMonth(next.getMonth() + months);
+ return next;
+}
+
+export function getVehicleLeasePeriodMonths(vehicle, record) {
+ if (vehicle && vehicle.leasePeriodMonths != null && vehicle.leasePeriodMonths !== '') {
+ return Number(vehicle.leasePeriodMonths);
+ }
+ if (record && record.defaultLeasePeriodMonths != null) {
+ return Number(record.defaultLeasePeriodMonths);
+ }
+ return 12;
+}
+
+export function isVehicleLeasePeriodExpired(vehicle, record, referenceDate) {
+ if (!isContractVehicleDelivered(vehicle)) return false;
+ var delivery = parseContractDateOnly(vehicle.deliveryTime || vehicle.actualDelivery);
+ if (!delivery) return false;
+ var months = getVehicleLeasePeriodMonths(vehicle, record);
+ if (!Number.isFinite(months) || months <= 0) return false;
+ var leaseEnd = addMonthsToDate(delivery, months);
+ leaseEnd.setHours(23, 59, 59, 999);
+ var ref = referenceDate || new Date();
+ return ref.getTime() > leaseEnd.getTime();
+}
+
+var LEASE_STATUS_DYNAMIC_BASE = ['合同进行中', '到期合同'];
+
+export function resolveContractStatusByVehicleLease(record) {
+ if (!record) return '-';
+ var baseStatus = record.contractStatus || '-';
+ if (LEASE_STATUS_DYNAMIC_BASE.indexOf(baseStatus) === -1) return baseStatus;
+ var vehicles = record.vehicles || [];
+ if (!vehicles.length) return baseStatus;
+ var allDelivered = vehicles.every(isContractVehicleDelivered);
+ if (!allDelivered) return baseStatus === '到期合同' ? '合同进行中' : baseStatus;
+ var allLeaseExpired = vehicles.every(function (vehicle) {
+ return isVehicleLeasePeriodExpired(vehicle, record);
+ });
+ if (allLeaseExpired) return '到期合同';
+ return '合同进行中';
+}
+
+export function isContractVehicleDelivered(vehicle) {
+ var time = vehicle.deliveryTime || vehicle.actualDelivery;
+ return Boolean(time && String(time).trim() && time !== '-');
+}
+
+export function isContractVehicleReturned(vehicle) {
+ var time = vehicle.returnTime;
+ return Boolean(time && String(time).trim() && time !== '-');
+}
+
+export function canContractVehicleReturn(vehicle) {
+ return isContractVehicleDelivered(vehicle) && !isContractVehicleReturned(vehicle);
+}
+
+export function computeContractOverallMileageProgress(record) {
+ if (!record || !record.hasMinimumMileage) return null;
+ var vehicles = record.vehicles || [];
+ var total = 0;
+ var count = 0;
+ vehicles.forEach(function (vehicle) {
+ if (!isContractVehicleDelivered(vehicle)) return;
+ if (vehicle.mileageProgress == null) return;
+ total += Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0));
+ count += 1;
+ });
+ if (count === 0) return 0;
+ return Math.round(total / count);
+}
+
+export function computeContractMileageForecastStatus(record) {
+ if (!record || !record.hasMinimumMileage) return null;
+ var percent = record.overallMileageProgress;
+ if (percent == null) percent = computeContractOverallMileageProgress(record);
+ percent = Math.min(100, Math.max(0, Number(percent) || 0));
+ var vehicles = record.vehicles || [];
+ var hasRiskVehicle = false;
+ var hasUnableVehicle = false;
+ vehicles.forEach(function (vehicle) {
+ if (!isContractVehicleDelivered(vehicle)) return;
+ var progress = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0));
+ if (progress < 40) hasUnableVehicle = true;
+ else if (progress < 70) hasRiskVehicle = true;
+ });
+ if (percent < 45 || hasUnableVehicle) return '预计无法完成';
+ if (percent < 75 || hasRiskVehicle) return '存在完成风险';
+ return '预计可完成';
+}
+
+export function getContractMileageForecastStatusTone(status) {
+ if (status === '预计可完成') return 'green';
+ if (status === '存在完成风险') return 'amber';
+ if (status === '预计无法完成') return 'red';
+ return 'gray';
+}
+
+export function enrichLeaseContractVehicle(vehicle, index, recordIndex, record) {
+ var delivered = vehicle.actualDelivery && String(vehicle.actualDelivery).trim() && vehicle.actualDelivery !== '-';
+ var seed = (recordIndex || 0) * 10 + (index || 0);
+ var demoDeliveryMiles = [25468, 6210, 31233, 48202, 12580];
+ var demoReturnMiles = [25372, 127012, 11578];
+ var demoCurrentMiles = [25468, 6210, 31233, 45822, 127012, 11578, 45195];
+ var demoReturnPersons = ['刘洋', '李娜', '吴磊'];
+ var hasExplicitReturn = vehicle.returnTime && String(vehicle.returnTime).trim() && vehicle.returnTime !== '-';
+ var hasMinMileage = record && record.hasMinimumMileage;
+ var progress = vehicle.mileageProgress;
+ if (!hasMinMileage) {
+ progress = null;
+ } else if (progress == null) {
+ if (!delivered) progress = 0;
+ else progress = [38, 56, 72, 91][seed % 4];
+ }
+ var hasReturn = hasExplicitReturn || (progress >= 90 && delivered);
+ var deliveryTime = vehicle.deliveryTime || (delivered ? vehicle.actualDelivery : null);
+ var deliveryMileage = vehicle.deliveryMileage != null && vehicle.deliveryMileage !== ''
+ ? vehicle.deliveryMileage
+ : (delivered ? demoDeliveryMiles[seed % demoDeliveryMiles.length] : null);
+ var deliveryPerson = delivered
+ ? (vehicle.deliveryPerson && vehicle.deliveryPerson !== '-' ? vehicle.deliveryPerson : '张运维')
+ : null;
+ var returnTime = hasReturn
+ ? (hasExplicitReturn ? vehicle.returnTime : '2025-12-01 17:00')
+ : null;
+ var returnMileage = hasReturn
+ ? (vehicle.returnMileage != null && vehicle.returnMileage !== '' ? vehicle.returnMileage : demoReturnMiles[seed % demoReturnMiles.length])
+ : null;
+ var returnPerson = hasReturn
+ ? (vehicle.returnPerson && vehicle.returnPerson !== '-' ? vehicle.returnPerson : demoReturnPersons[seed % demoReturnPersons.length])
+ : null;
+ var currentMileage = vehicle.currentMileage != null && vehicle.currentMileage !== ''
+ ? vehicle.currentMileage
+ : (delivered ? demoCurrentMiles[seed % demoCurrentMiles.length] : '-');
+ var mileageSources = ['车机', 'GPS', '人工'];
+ var mileageSource = vehicle.mileageSource || mileageSources[seed % mileageSources.length];
+ var telematicsLinked = mileageSource === '车机';
+ var gpsTime = mileageSource === 'GPS'
+ ? (vehicle.gpsTime || '2026-06-28 14:32:15')
+ : (vehicle.gpsTime || null);
+ var onlineStatus = vehicle.onlineStatus
+ || (mileageSource === '车机' ? '在线' : '离线');
+ var pickupPaymentPaid = vehicle.pickupPaymentPaid;
+ if (pickupPaymentPaid == null) {
+ pickupPaymentPaid = delivered ? seed % 4 !== 1 : false;
+ }
+ var remainingMileage = computeContractVehicleRemainingMileage(record, { mileageProgress: progress });
+ var deliveryRaw = deliveryTime || vehicle.actualDelivery;
+ var periodStartDate = null;
+ var periodStartMileage = null;
+ if (delivered && hasMinMileage && record && record.mileagePeriod) {
+ periodStartDate = computeCurrentMileagePeriodStart(deliveryRaw, record.mileagePeriod);
+ periodStartMileage = estimatePeriodStartMileage(
+ deliveryRaw,
+ periodStartDate,
+ deliveryMileage,
+ currentMileage,
+ );
+ }
+ var returnSettlementStatus = vehicle.returnSettlementStatus;
+ if (hasReturn && !returnSettlementStatus) {
+ returnSettlementStatus = RETURN_SETTLEMENT_STATUSES[seed % RETURN_SETTLEMENT_STATUSES.length];
+ }
+
+ return Object.assign({}, vehicle, {
+ vin: vehicle.vin || (vehicle.plateNo && vehicle.plateNo !== '-'
+ ? 'LFWNKVPH8K1' + String(10000 + seed).slice(-5)
+ : '-'),
+ deliveryTime: deliveryTime,
+ deliveryMileage: deliveryMileage,
+ deliveryPerson: deliveryPerson,
+ returnTime: returnTime,
+ returnMileage: returnMileage,
+ returnPerson: returnPerson,
+ mileageProgress: progress,
+ currentMileage: currentMileage,
+ onlineStatus: onlineStatus,
+ mileageSource: mileageSource,
+ telematicsLinked: telematicsLinked,
+ gpsTime: gpsTime,
+ pickupPaymentPaid: pickupPaymentPaid,
+ remainingMileage: remainingMileage,
+ periodStartDate: periodStartDate,
+ periodStartMileage: periodStartMileage,
+ returnSettlementStatus: hasReturn ? returnSettlementStatus : null,
+ leasePeriodMonths: vehicle.leasePeriodMonths != null
+ ? vehicle.leasePeriodMonths
+ : getVehicleLeasePeriodMonths(vehicle, record),
+ plannedDeliveryDate: vehicle.plannedDeliveryDate != null
+ ? vehicle.plannedDeliveryDate
+ : (delivered ? null : (record && record.deliveryDate) || null),
+ });
+}
+
+export function enrichLeaseContractRecord(record, recordIndex) {
+ var lessorId = record.lessorId || SIGNING_COMPANY_LESSOR_IDS[record.signingCompany];
+ var templateId = inferContractTemplateId(record);
+ var enriched = Object.assign({}, record, {
+ lessorId: lessorId,
+ contractTemplateId: record.contractTemplateId || templateId,
+ signingCompanyFullName: resolveSigningCompanyFullName(Object.assign({}, record, { lessorId: lessorId })),
+ vehicles: (record.vehicles || []).map(function (vehicle, index) {
+ return enrichLeaseContractVehicle(vehicle, index, recordIndex, record);
+ }),
+ });
+ enriched.contractTemplateTypeLabel = resolveContractTemplateTypeLabel(enriched);
+ enriched.overallMileageProgress = computeContractOverallMileageProgress(enriched);
+ enriched.overallMileageForecastStatus = computeContractMileageForecastStatus(enriched);
+ enriched.contractStatus = resolveContractStatusByVehicleLease(enriched);
+ return enriched;
+}
+
+export function enrichLeaseContractListRecords(records) {
+ return (records || []).map(function (record, index) {
+ return enrichLeaseContractRecord(record, index);
+ });
+}
diff --git a/src/prototypes/lease-contract-management/lease-contract-preview-build.js b/src/prototypes/lease-contract-management/lease-contract-preview-build.js
index 5fd4e34..53652af 100644
--- a/src/prototypes/lease-contract-management/lease-contract-preview-build.js
+++ b/src/prototypes/lease-contract-management/lease-contract-preview-build.js
@@ -1,7 +1,9 @@
/**
* 租赁合同预览 HTML 拼装:占位符替换 + 2.6.1 开票信息 + 里程标准 + 客户资质附件
*/
-import { V266_LEASE_DOCUMENT_HTML } from '../contract-template-management/v266-lease-document.js';
+import { getContractTemplateBaseHtml } from '../contract-template-management/contract-template-catalog.js';
+import { getTemplateSectionVehicleBindings } from '../contract-template-management/contract-template-store.js';
+import { applyAttachmentSectionVehicleFilter } from '../contract-template-management/contract-template-section-filter.js';
import {
applyTemplateVars,
buildLeasePreviewVars,
@@ -405,9 +407,13 @@ function stripUnresolvedPlaceholders(html) {
}
export function buildLeaseContractPreviewHtml(form) {
- var base = V266_LEASE_DOCUMENT_HTML || '';
+ var templateId = form && form.contractTemplateId;
+ var base = getContractTemplateBaseHtml(templateId) || '';
+ var sectionBindings = getTemplateSectionVehicleBindings(templateId);
var vars = buildLeasePreviewVars(form || {});
var html = stripUnresolvedPlaceholders(applyTemplateVars(base, vars));
+ var vehicleKeys = collectVehicleKeysFromLeaseOrder(form);
+ html = applyAttachmentSectionVehicleFilter(html, sectionBindings, vehicleKeys);
html = injectPrototypeVehicleClauses(html);
var customer = getLeaseCustomerById(form && form.customerId);
var lessor = getLessorCompanyById(form && form.lessorId);
@@ -420,7 +426,6 @@ export function buildLeaseContractPreviewHtml(form) {
html = applyLeaseOrderAttachmentPatch(html, form, customer, lessor);
html = applyPowerOfAttorneyPatch(html, form);
html = appendCustomerCredentialAttachments(html, customer);
- var vehicleKeys = collectVehicleKeysFromLeaseOrder(form);
html = applyVehicleClausePreviewFilter(html, vehicleKeys);
return html;
}
diff --git a/src/prototypes/lease-contract-management/lease-contract-risk-detect.js b/src/prototypes/lease-contract-management/lease-contract-risk-detect.js
index 955dbba..9af8ca0 100644
--- a/src/prototypes/lease-contract-management/lease-contract-risk-detect.js
+++ b/src/prototypes/lease-contract-management/lease-contract-risk-detect.js
@@ -2,8 +2,8 @@
* 租赁合同预览:检测风控红线条款(模板标记)是否被修改
*/
-export var STANDARD_CONTRACT_APPROVAL = '标准合同审批';
-export var NONSTANDARD_CONTRACT_APPROVAL = '非标准合同审批';
+export var STANDARD_CONTRACT_APPROVAL = '标准合同';
+export var NONSTANDARD_CONTRACT_APPROVAL = '非标准合同';
var RISK_SELECTOR = '.ct-risk-redline[data-risk-redline="1"]';
diff --git a/src/prototypes/lease-contract-management/lease-order-vars.js b/src/prototypes/lease-contract-management/lease-order-vars.js
index eb38c90..21bf7c7 100644
--- a/src/prototypes/lease-contract-management/lease-order-vars.js
+++ b/src/prototypes/lease-contract-management/lease-order-vars.js
@@ -112,6 +112,7 @@ export function createEmptyLeaseOrderRow() {
serviceFee: null,
deposit: null,
extraServices: [],
+ leasePeriodMonths: null,
leasePeriodStart: null,
leasePeriodEnd: null,
};
@@ -214,6 +215,9 @@ export function isInsuredVehicleCountEditable(order) {
export function formatLeasePeriod(row) {
if (!row) return '';
+ if (row.leasePeriodMonths != null && row.leasePeriodMonths !== '') {
+ return String(row.leasePeriodMonths) + ' 个月';
+ }
if (row.leasePeriodStart && row.leasePeriodEnd) {
return row.leasePeriodStart + ' ~ ' + row.leasePeriodEnd;
}
diff --git a/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs b/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs
index 96780d7..b03b7fd 100644
--- a/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs
+++ b/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs
@@ -19,7 +19,7 @@ const NODE_DEFS = [
{ id: 'lc-create-main-contract', title: '主体合同信息', pageId: 'create', color: '#32a06e', aiPrompt: '签约、里程、费用三块表单。', annotationText: '完成度徽章;甲乙档案只读同步。' },
{ id: 'lc-create-lease-order', title: '附件1:租赁订单', pageId: 'create', color: '#2563eb', aiPrompt: '订单车辆与交车信息。', annotationText: '三者责任险等为完成度必填项。' },
{ id: 'lc-create-poa', title: '授权委托书', pageId: 'create', color: '#7c3aed', aiPrompt: '被委托人列表编辑。', annotationText: '至少一行且姓名/联系方式/身份证完整。' },
- { id: 'lc-create-preview', title: '实时预览', pageId: 'create', color: '#0ea5e9', aiPrompt: 'Word 版式预览与可编辑正文。', annotationText: '改动风控红线条款将触发非标准合同审批。' },
+ { id: 'lc-create-preview', title: '实时预览', pageId: 'create', color: '#0ea5e9', aiPrompt: 'Word 版式预览与可编辑正文。', annotationText: '改动风控红线条款将触发非标准合同。' },
{ id: 'lc-create-footer', title: '底栏操作', pageId: 'create', color: '#64748b', aiPrompt: '取消、保存草稿、提交审核。', annotationText: '保存/提交时展示将走的审批类型。' },
];
diff --git a/src/prototypes/lease-contract-management/styles/lease-contract-create.css b/src/prototypes/lease-contract-management/styles/lease-contract-create.css
index 897099f..c96ebed 100644
--- a/src/prototypes/lease-contract-management/styles/lease-contract-create.css
+++ b/src/prototypes/lease-contract-management/styles/lease-contract-create.css
@@ -24,6 +24,338 @@
background: var(--ln-primary-soft);
}
+.vm-page.lc-create-page .lc-create-section--disabled {
+ position: relative;
+}
+
+.vm-page.lc-create-page .lc-create-editor-column > .ct-editor-column-card.lc-create-section--disabled {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.vm-page.lc-create-page .lc-create-section--disabled .ct-editor-panel,
+.vm-page.lc-create-page .lc-create-section--disabled .lc-main-contract-panel {
+ pointer-events: none;
+ user-select: none;
+}
+
+.vm-page.lc-create-page .lc-create-section {
+ scroll-margin-top: 72px;
+}
+
+.vm-page.lc-create-page .lc-create-section__lock {
+ position: absolute;
+ inset: 48px 0 0;
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 24px 20px;
+ text-align: center;
+ background: color-mix(in srgb, var(--ln-surface-card) 82%, transparent);
+ backdrop-filter: blur(2px);
+ border-radius: 0 0 var(--ln-radius-card) var(--ln-radius-card);
+}
+
+.vm-page.lc-create-page .lc-create-section__lock-text {
+ margin: 0;
+ max-width: 18rem;
+ font-size: 0.875rem;
+ line-height: 1.55;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-create-section__lock-btn {
+ min-height: 44px;
+ cursor: pointer;
+}
+
+.vm-page.lc-create-page .lc-create-step-nav {
+ position: sticky;
+ top: 0;
+ z-index: 5;
+ flex-shrink: 0;
+ margin: 0 0 4px;
+ padding: 10px 12px;
+ border: 1px solid var(--ln-hairline);
+ border-radius: var(--ln-radius-card);
+ background: color-mix(in srgb, var(--ln-surface-card) 94%, transparent);
+ backdrop-filter: blur(8px);
+ box-shadow: var(--ln-shadow-soft);
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__list {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ overflow-x: auto;
+ scrollbar-width: thin;
+ -webkit-overflow-scrolling: touch;
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__item {
+ display: flex;
+ align-items: center;
+ flex: 1 1 0;
+ min-width: 5.5rem;
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ width: 100%;
+ min-height: 44px;
+ padding: 6px 8px;
+ border: 0;
+ border-radius: var(--ln-radius-control);
+ background: transparent;
+ color: var(--ln-muted);
+ font-size: 0.75rem;
+ line-height: 1.3;
+ cursor: pointer;
+ transition: background-color 0.15s ease, color 0.15s ease;
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__btn:hover {
+ background: var(--ln-canvas-soft);
+ color: var(--ln-ink);
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__btn:focus-visible {
+ outline: 2px solid var(--ln-primary);
+ outline-offset: 2px;
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__index {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 9999px;
+ background: var(--ln-surface-strong);
+ font-size: 0.6875rem;
+ font-weight: 700;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__item--done .lc-create-step-nav__index {
+ background: var(--ln-primary-soft);
+ color: var(--ln-primary);
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__item--done .lc-create-step-nav__btn {
+ color: var(--ln-ink);
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__item--pending .lc-create-step-nav__index {
+ background: var(--ln-canvas-soft);
+ color: var(--ln-ink);
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__item--locked .lc-create-step-nav__btn {
+ opacity: 0.72;
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__label {
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__tag {
+ font-size: 0.625rem;
+ font-weight: 500;
+ padding: 1px 6px;
+ border-radius: var(--ln-radius-pill);
+ background: var(--ln-canvas-soft);
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-create-step-nav__sep {
+ flex: 0 0 12px;
+ width: 12px;
+ height: 1px;
+ margin-top: 18px;
+ background: var(--ln-hairline-strong);
+}
+
+.vm-page.lc-create-page .lc-create-topbar__intro {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-create-topbar__title {
+ margin: 0;
+ font-size: 1.125rem;
+ font-weight: 700;
+ line-height: 1.35;
+ color: var(--ln-ink);
+}
+
+.vm-page.lc-create-page .lc-create-topbar__meta {
+ margin: 0;
+ font-size: 0.8125rem;
+ line-height: 1.45;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-create-topbar__meta strong {
+ color: var(--ln-primary);
+ font-weight: 700;
+}
+
+.vm-page.lc-create-page .lc-create-topbar__meta-divider {
+ margin: 0 6px;
+}
+
+.vm-page.lc-create-page .lc-create-topbar__meta-template {
+ color: var(--ln-ink);
+}
+
+.vm-page.lc-create-page .lc-create-topbar__meta-hint {
+ color: #c2410c;
+}
+
+.vm-page.lc-create-page .lc-template-step-panel {
+ padding: 12px 16px 16px;
+}
+
+.vm-page.lc-create-page .lc-template-step-card__head {
+ align-items: center;
+}
+
+.vm-page.lc-create-page .lc-template-step-card__title-row {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px 10px;
+ flex: 1;
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-template-step-card__title-row .ct-panel-head__title {
+ flex-shrink: 0;
+}
+
+.vm-page.lc-create-page .lc-template-step-card .lc-form-card__hint {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ min-width: 0;
+ max-width: 100%;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1.45;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-template-step-card .lc-form-card__hint-icon {
+ flex-shrink: 0;
+ color: color-mix(in srgb, var(--ln-primary) 72%, var(--ln-muted));
+}
+
+.vm-page.lc-create-page .lc-template-step-card .lc-form-card__hint-text {
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-template-select {
+ width: 100%;
+}
+
+.vm-page.lc-create-page .lc-template-select .ant-select-selector {
+ min-height: 44px;
+ align-items: center;
+ border-radius: var(--ln-radius-control);
+ cursor: pointer;
+}
+
+.vm-page.lc-create-page .lc-template-step-subtitle {
+ margin: 16px 0 8px;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: var(--ln-ink);
+}
+
+.vm-page.lc-create-page .lc-template-step-empty {
+ margin: 12px 0 0;
+ padding: 10px 12px;
+ border-radius: var(--ln-radius-control);
+ background: rgba(234, 88, 12, 0.08);
+ color: #c2410c;
+ font-size: 0.875rem;
+ line-height: 1.5;
+}
+
+.vm-page.lc-create-page .lc-template-option-list--nested {
+ margin-top: 0;
+}
+
+.vm-page.lc-create-page .lc-template-option-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.vm-page.lc-create-page .lc-template-option {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ width: 100%;
+ padding: 12px 14px;
+ border: 1px solid var(--ln-line);
+ border-radius: 10px;
+ background: #fff;
+ text-align: left;
+ cursor: pointer;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.vm-page.lc-create-page .lc-template-option:hover {
+ border-color: #94b8ff;
+}
+
+.vm-page.lc-create-page .lc-template-option--active {
+ border-color: var(--ln-primary);
+ box-shadow: 0 0 0 1px var(--ln-primary);
+ background: var(--ln-primary-soft);
+}
+
+.vm-page.lc-create-page .lc-template-option__title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ln-text);
+}
+
+.vm-page.lc-create-page .lc-template-option__meta {
+ font-size: 12px;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-create-editor-column > .ct-editor-column-card .lc-create-editor-panel {
+ padding: 12px 16px 16px;
+}
+
+.vm-page.lc-create-page .lc-template-preview-empty {
+ display: grid;
+ place-items: center;
+ min-height: 320px;
+ padding: 24px;
+ color: var(--ln-muted);
+ font-size: 14px;
+ text-align: center;
+}
+
/* —— 主体合同信息编辑区 —— */
.vm-page.lc-create-page .lc-main-contract-card__head {
align-items: center;
@@ -107,6 +439,21 @@
border: 1px solid var(--ln-hairline);
}
+.vm-page.lc-create-page .lc-signing-core .lc-form-field__label-row {
+ flex-wrap: nowrap;
+ align-items: center;
+}
+
+.vm-page.lc-create-page .lc-signing-core .lc-form-field__label {
+ flex-shrink: 0;
+}
+
+.vm-page.lc-create-page .lc-signing-core .lc-form-field__inline-hint {
+ flex: 0 1 auto;
+ min-width: 0;
+ white-space: nowrap;
+}
+
.vm-page.lc-create-page .lc-party-profiles {
display: flex;
flex-direction: column;
@@ -316,7 +663,8 @@
}
.vm-page.lc-create-page .lc-lease-order-brand-model-picker__trigger,
-.vm-page.lc-create-page .lc-lease-order-plate-picker__trigger {
+.vm-page.lc-create-page .lc-lease-order-plate-picker__trigger,
+.vm-page.lc-create-page .lc-business-dept-picker__trigger {
min-height: 36px;
height: auto;
align-items: flex-start;
@@ -324,6 +672,18 @@
padding-bottom: 6px;
}
+.vm-page.lc-create-page .lc-business-dept-picker {
+ width: 100%;
+}
+
+.vm-page.lc-create-page .lc-business-dept-picker__trigger {
+ width: 100%;
+}
+
+.vm-page.lc-create-page .lc-business-dept-picker__summary {
+ text-align: left;
+}
+
.vm-page.lc-create-page .lc-lease-order-brand-model-picker__summary-list,
.vm-page.lc-create-page .lc-lease-order-plate-picker__summary-list {
display: flex;
@@ -342,6 +702,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ max-width: 220px;
}
.lc-lease-order-brand-model-picker__selected-list {
@@ -450,6 +811,67 @@
min-width: 160px;
}
+.vm-page.lc-create-page .lc-lease-order-brand-model-picker {
+ width: 100%;
+}
+
+.vm-page.lc-create-page .lc-lease-order-brand-model-picker__trigger {
+ width: 100%;
+}
+
+.vm-page.lc-create-page .lc-lease-order-service-field {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ flex-wrap: nowrap;
+}
+
+.vm-page.lc-create-page .lc-lease-order-service-field__summary {
+ margin: 0;
+ font-size: 0.8125rem;
+ line-height: 1.45;
+ color: var(--ln-ink);
+ white-space: nowrap;
+ max-width: 180px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.vm-page.lc-create-page .lc-lease-order-service-picker__btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ min-height: 32px;
+ padding: 0 12px;
+ border: 1px solid color-mix(in srgb, var(--ln-primary) 32%, var(--ln-hairline));
+ border-radius: var(--ln-radius-control);
+ background: var(--ln-surface-card, #fff);
+ color: var(--ln-primary-focus, #15803d);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ line-height: 1.2;
+ white-space: nowrap;
+ cursor: pointer;
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
+}
+
+.vm-page.lc-create-page .lc-lease-order-service-picker__btn:hover {
+ background: var(--ln-primary-soft, #ecfdf3);
+ border-color: color-mix(in srgb, var(--ln-primary) 45%, var(--ln-hairline));
+}
+
+.vm-page.lc-create-page .lc-lease-order-service-picker__btn:focus-visible {
+ outline: 2px solid color-mix(in srgb, var(--ln-primary) 45%, transparent);
+ outline-offset: 2px;
+}
+
+.vm-page.lc-create-page .lc-lease-order-period-field {
+ width: 100%;
+}
+
.vm-page.lc-create-page .lc-lease-order-service-picker {
width: 100%;
min-width: 148px;
@@ -877,14 +1299,16 @@
.vm-page.lc-create-page .lc-lease-order-editor__table-wrap {
overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
border: 1px solid var(--ln-hairline);
border-radius: var(--ln-radius-control);
background: var(--ln-surface-card);
}
.vm-page.lc-create-page .lc-lease-order-editor__table {
- width: 100%;
- min-width: 1120px;
+ width: max-content;
+ min-width: 100%;
+ table-layout: auto;
border-collapse: collapse;
}
@@ -894,6 +1318,7 @@
border: 1px solid var(--ln-hairline);
vertical-align: middle;
font-size: 0.8125rem;
+ white-space: nowrap;
}
.vm-page.lc-create-page .lc-lease-order-editor__table th {
@@ -910,12 +1335,12 @@
}
.vm-page.lc-create-page .lc-lease-order-editor__th-brand {
- min-width: 148px;
+ min-width: 0;
}
.vm-page.lc-create-page .lc-lease-order-editor__brand-cell {
- min-width: 148px;
- vertical-align: top;
+ min-width: 0;
+ vertical-align: middle;
}
.vm-page.lc-create-page .lc-lease-order-editor__brand-cell .lc-form-cascader {
@@ -928,6 +1353,14 @@
line-height: 1.45;
}
+.vm-page.lc-create-page .lc-lease-order-editor__clause-hint {
+ margin: 0 0 12px;
+}
+
+.vm-page.lc-create-page .lc-lease-order-editor__clause-hint .lc-brand-model-clause-hint {
+ margin: 0;
+}
+
.vm-page.lc-create-page .lc-brand-model-clause-hint--empty {
display: flex;
align-items: flex-start;
@@ -977,7 +1410,20 @@
}
.vm-page.lc-create-page .lc-lease-order-editor__th-period {
- min-width: 200px;
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-lease-order-editor__period-cell {
+ width: 1%;
+}
+
+.vm-page.lc-create-page .lc-lease-order-period-input {
+ width: 72px;
+ min-width: 72px;
+}
+
+.vm-page.lc-create-page .lc-lease-order-period-input.ant-input-number {
+ width: 72px;
}
.vm-page.lc-create-page .lc-lease-order-editor__head-row--sub th {
@@ -1192,6 +1638,17 @@
}
}
+.vm-page.lc-create-page .lc-contract-remark {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid var(--ln-hairline, #e2e8f0);
+}
+
+.vm-page.lc-create-page .lc-contract-remark__textarea.ant-input {
+ min-height: 96px;
+ resize: vertical;
+}
+
.vm-page.lc-create-page .lc-form-cascader.ant-cascader {
width: 100%;
}
@@ -1212,7 +1669,7 @@
.vm-page.lc-create-page .lc-create-topbar {
display: flex;
- align-items: center;
+ align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
@@ -1288,7 +1745,66 @@
height: 100%;
}
-.lc-preview-editable-hint {
+.lc-preview-editable-hint--inline {
+ display: inline-flex;
+ align-items: center;
+ flex: 0 1 auto;
+ min-width: 0;
+ margin: 0;
+ padding: 0;
+ justify-content: flex-start;
+}
+
+.vm-page.lc-create-page .lc-create-preview-head {
+ align-items: center;
+}
+
+.vm-page.lc-create-page .lc-create-preview-head .lc-template-step-card__title-row {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ gap: 6px 10px;
+ flex: 0 1 auto;
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-create-preview-head .lc-template-step-card__title-row .ct-panel-head__title {
+ flex-shrink: 0;
+}
+
+.vm-page.lc-create-page .lc-create-preview-head .lc-form-card__hint {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ flex: 0 1 auto;
+ min-width: 0;
+ max-width: min(100%, 520px);
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1.45;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-create-preview-head .lc-form-card__hint-icon {
+ flex-shrink: 0;
+ color: color-mix(in srgb, var(--ln-primary) 72%, var(--ln-muted));
+}
+
+.vm-page.lc-create-page .lc-create-preview-head .lc-preview-editable-hint__text {
+ min-width: 0;
+ margin: 0;
+ font-size: inherit;
+ line-height: inherit;
+ color: inherit;
+}
+
+.lc-preview-editable-hint__risk {
+ color: #b91c1c;
+ font-weight: 600;
+}
+
+.lc-preview-editable-hint:not(.lc-preview-editable-hint--inline) {
display: flex;
justify-content: center;
flex-shrink: 0;
@@ -1314,7 +1830,7 @@
color: var(--ln-primary-focus, #15803d);
}
-.lc-preview-editable-hint__text {
+.lc-preview-editable-hint__text:not(.lc-form-card__hint-text) {
margin: 0;
font-size: 12px;
line-height: 1.55;
@@ -1519,6 +2035,135 @@
line-height: 1.4;
}
+.vm-page.lc-create-page .lc-form-field__label-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px 10px;
+}
+
+.vm-page.lc-create-page .lc-form-field__inline-hint {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ min-width: 0;
+ max-width: 100%;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1.45;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-form-field__inline-hint .lc-form-card__hint-icon {
+ flex-shrink: 0;
+ color: color-mix(in srgb, var(--ln-primary) 72%, var(--ln-muted));
+}
+
+.vm-page.lc-create-page .lc-create-section__head .lc-template-step-card__title-row {
+ flex: 1;
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-create-section-poa-head .lc-template-step-card__title-row,
+.vm-page.lc-create-page .lc-create-section-seal-head .lc-template-step-card__title-row {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ gap: 6px 10px;
+ flex: 0 1 auto;
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-create-section-poa-head .lc-create-poa-hint {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ flex: 0 1 auto;
+ min-width: 0;
+ font-size: 0.8125rem;
+ font-weight: 400;
+ line-height: 1.45;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-create-page .lc-create-section-poa-head .lc-form-card__hint-icon {
+ flex-shrink: 0;
+ margin-top: 0;
+ color: color-mix(in srgb, var(--ln-primary) 72%, var(--ln-muted));
+}
+
+.vm-page.lc-create-page .lc-seal-type-head-tags {
+ display: inline-flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
+ flex: 0 1 auto;
+ min-width: 0;
+}
+
+.vm-page.lc-create-page .lc-seal-type-head-tag {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ line-height: 1.4;
+ color: var(--ln-primary-focus, #15803d);
+ background: var(--ln-primary-soft, #ecfdf3);
+ border: 1px solid color-mix(in srgb, var(--ln-primary, #32a06e) 24%, transparent);
+}
+
+.vm-page.lc-create-page .lc-seal-type-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.vm-page.lc-create-page .lc-seal-type-chip {
+ display: inline-flex;
+ align-items: center;
+ min-height: 34px;
+ padding: 0 14px;
+ border-radius: 999px;
+ border: 1px solid var(--ln-hairline);
+ background: var(--ln-surface-card, #fff);
+ color: var(--ln-ink);
+ font-size: 0.875rem;
+ line-height: 1.2;
+ cursor: pointer;
+ transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
+}
+
+.vm-page.lc-create-page .lc-seal-type-chip:hover {
+ border-color: color-mix(in srgb, var(--ln-primary) 35%, var(--ln-hairline));
+}
+
+.vm-page.lc-create-page .lc-seal-type-chip.is-active {
+ color: var(--ln-primary-focus, #15803d);
+ background: var(--ln-primary-soft, #ecfdf3);
+ border-color: color-mix(in srgb, var(--ln-primary, #32a06e) 35%, transparent);
+ font-weight: 600;
+}
+
+.vm-page.lc-create-page .lc-seal-type-chip:focus-visible {
+ outline: 2px solid color-mix(in srgb, var(--ln-primary) 45%, transparent);
+ outline-offset: 2px;
+}
+
+.vm-page.lc-create-page .lc-create-section__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.vm-page.lc-create-page .lc-create-section-poa-head,
+.vm-page.lc-create-page .lc-create-section-seal-head {
+ align-items: center;
+}
+
.vm-page.lc-create-page .lc-form-field__required {
color: #dc2626;
margin-right: 4px;
diff --git a/src/prototypes/lease-contract-management/styles/lease-contract.css b/src/prototypes/lease-contract-management/styles/lease-contract.css
index 75c5b95..f7d0192 100644
--- a/src/prototypes/lease-contract-management/styles/lease-contract.css
+++ b/src/prototypes/lease-contract-management/styles/lease-contract.css
@@ -143,6 +143,73 @@
border-color: color-mix(in srgb, var(--ln-primary) 35%, transparent);
}
+.lc-approval-flow-popover .ant-popover-inner {
+ padding: 12px 14px;
+}
+
+.lc-approval-flow {
+ width: min(380px, calc(100vw - 48px));
+ max-height: min(420px, calc(100vh - 120px));
+ overflow-y: auto;
+ padding-top: 2px;
+}
+
+.lc-approval-flow__empty {
+ color: var(--ln-muted);
+ font-size: 13px;
+ padding: 4px 0;
+}
+
+.lc-approval-flow__item {
+ display: flex;
+ align-items: stretch;
+}
+
+.lc-approval-flow__meta {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+}
+
+.lc-approval-flow__operator {
+ font-size: 13px;
+ color: var(--ln-muted);
+ line-height: 1.4;
+}
+
+.lc-approval-flow__comment {
+ padding: 8px 10px;
+ border-radius: 8px;
+ background: var(--ln-surface-strong, #f8fafc);
+ border: 1px solid var(--ln-hairline, #e2e8f0);
+}
+
+.lc-approval-flow__comment.is-rejected {
+ background: color-mix(in srgb, var(--ln-error, #e5484d) 6%, #fff);
+ border-color: color-mix(in srgb, var(--ln-error, #e5484d) 18%, var(--ln-hairline, #e2e8f0));
+}
+
+.lc-approval-flow__comment-label {
+ display: block;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--ln-muted);
+ margin-bottom: 4px;
+}
+
+.lc-approval-flow__comment-text {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--ln-body, #334155);
+ word-break: break-word;
+}
+
+.lc-approval-flow__comment.is-rejected .lc-approval-flow__comment-text {
+ color: var(--ln-error, #e5484d);
+}
+
/* 面包屑与 vm 页头 */
.vm-page.lc-page .vm-page-header .ant-breadcrumb {
font-size: 0.875rem;
@@ -159,6 +226,26 @@
vertical-align: middle;
}
+.vm-page.lc-page .lc-project-info-cell {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-project-info-cell .lc-project-info-btn {
+ flex: 1;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-project-info-cell__status {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
+}
+
.vm-page.lc-page .lc-project-info-btn {
display: block;
width: 100%;
@@ -208,3 +295,1145 @@
font-size: 0.8125rem;
color: var(--ln-body);
}
+
+/* 列表「租赁订单」列:租赁 / 已交 双计数 */
+.vm-page.lc-page .lc-lease-order-counts {
+ display: flex;
+ align-items: stretch;
+ justify-content: center;
+ gap: 0;
+ min-width: 108px;
+}
+
+.vm-page.lc-page .lc-lease-order-counts__item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ min-width: 44px;
+ min-height: 44px;
+ padding: 4px 6px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: var(--ln-radius-control, 8px);
+ color: inherit;
+ font: inherit;
+ transition: background-color 0.15s ease;
+}
+
+.vm-page.lc-page .lc-lease-order-counts__item:hover {
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 8%, transparent);
+}
+
+.vm-page.lc-page .lc-lease-order-counts__item:focus-visible {
+ outline: 2px solid var(--ln-primary, #32a06e);
+ outline-offset: 1px;
+}
+
+.vm-page.lc-page .lc-lease-order-counts__value {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ln-primary, #32a06e);
+ line-height: 1.2;
+}
+
+.vm-page.lc-page .lc-lease-order-counts__label {
+ font-size: 11px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.2;
+}
+
+.vm-page.lc-page .lc-lease-order-counts__sep {
+ width: 1px;
+ align-self: stretch;
+ margin: 6px 0;
+ background: var(--ln-hairline, #e2e8f0);
+ flex-shrink: 0;
+}
+
+/* 列表「费用信息」列:付款周期 / 氢费支付 左右均分展示 */
+.vm-page.lc-page .vm-list-table .lc-fee-info-col {
+ white-space: normal !important;
+ min-width: 192px !important;
+ padding-left: 6px !important;
+ padding-right: 6px !important;
+}
+
+.vm-page.lc-page .lc-fee-info-cell {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
+ align-items: stretch;
+ width: 100%;
+ min-width: 0;
+ box-sizing: border-box;
+}
+
+.vm-page.lc-page .lc-fee-info-cell__side {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ min-width: 0;
+ padding: 6px 8px;
+ box-sizing: border-box;
+}
+
+.vm-page.lc-page .lc-fee-info-cell__caption {
+ font-size: 11px;
+ font-weight: 400;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.2;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.vm-page.lc-page .lc-fee-info-cell__value {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: fit-content;
+ max-width: 100%;
+ min-height: 22px;
+ padding: 2px 7px;
+ border-radius: var(--ln-radius-pill, 999px);
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 1.3;
+ text-align: center;
+ white-space: nowrap;
+ box-sizing: border-box;
+}
+
+.vm-page.lc-page .lc-fee-info-cell__side--period .lc-fee-info-cell__value {
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 12%, transparent);
+ color: var(--ln-primary, #32a06e);
+}
+
+.vm-page.lc-page .lc-fee-info-cell__side--hydrogen .lc-fee-info-cell__value {
+ background: var(--ln-surface-strong, #f1f5f9);
+ color: var(--ln-body, #334155);
+}
+
+.vm-page.lc-page .lc-fee-info-cell__side--hydrogen.is-prepay .lc-fee-info-cell__value {
+ background: color-mix(in srgb, var(--ln-warning, #d97706) 12%, transparent);
+ color: var(--ln-warning, #d97706);
+}
+
+.vm-page.lc-page .lc-fee-info-cell__sep {
+ width: 1px;
+ align-self: stretch;
+ margin: 8px 0;
+ background: var(--ln-hairline, #e2e8f0);
+}
+
+/* 列表「交车安排」列 */
+.vm-page.lc-page .vm-list-table .lc-delivery-col {
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-delivery-cell {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ min-width: 120px;
+}
+
+.vm-page.lc-page .lc-delivery-cell__region {
+ font-size: 13px;
+ color: var(--ln-body, #1e293b);
+ line-height: 1.35;
+}
+
+.vm-page.lc-page .lc-delivery-cell__date-row {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: nowrap;
+ white-space: nowrap;
+ max-width: 100%;
+}
+
+.vm-page.lc-page .lc-delivery-cell__date {
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.3;
+ flex-shrink: 0;
+}
+
+.vm-page.lc-page .lc-delivery-cell__date--tbd {
+ color: var(--ln-muted, #64748b);
+}
+
+.vm-page.lc-page .lc-delivery-cell__edit-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ border-radius: var(--ln-radius-xs, 4px);
+ background: transparent;
+ color: var(--ln-primary, #32a06e);
+ cursor: pointer;
+ transition: background-color 0.15s ease, color 0.15s ease;
+}
+
+.vm-page.lc-page .lc-delivery-cell__edit-btn:hover {
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 10%, transparent);
+ color: var(--ln-primary-hover, #278a5c);
+}
+
+.vm-page.lc-page .lc-delivery-cell__edit-btn:focus-visible {
+ outline: 2px solid var(--ln-primary, #32a06e);
+ outline-offset: 1px;
+}
+
+/* 交车时间变更 Popover */
+.lc-delivery-change-popover .ant-popover-inner {
+ padding: 0;
+ border-radius: 12px;
+ box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14);
+}
+
+.lc-delivery-change-popover .ant-popover-title {
+ display: none;
+}
+
+.lc-delivery-change__panel {
+ width: min(360px, calc(100vw - 32px));
+ max-height: min(480px, calc(100vh - 120px));
+ display: flex;
+ flex-direction: column;
+}
+
+.lc-delivery-change__header {
+ padding: 14px 16px 10px;
+ border-bottom: 1px solid var(--ln-hairline, #e2e8f0);
+}
+
+.lc-delivery-change__title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--ln-body, #1e293b);
+ line-height: 1.3;
+}
+
+.lc-delivery-change__sub {
+ margin-top: 2px;
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.3;
+}
+
+.lc-delivery-change__form {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--ln-hairline, #e2e8f0);
+}
+
+.lc-delivery-change__field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.lc-delivery-change__label {
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delivery-change__tbd {
+ display: flex;
+ align-items: center;
+ margin: 8px 0 0;
+ cursor: pointer;
+}
+
+.lc-delivery-change__hint {
+ margin: 8px 0 0;
+ font-size: 12px;
+ line-height: 1.45;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delivery-change__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.lc-delivery-change__history {
+ padding: 12px 16px 14px;
+ overflow: auto;
+}
+
+.lc-delivery-change__history-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--ln-body, #1e293b);
+ margin-bottom: 8px;
+}
+
+.lc-delivery-change__empty {
+ margin: 0;
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delivery-change__history-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.lc-delivery-change__history-item {
+ padding: 10px 12px;
+ border: 1px solid var(--ln-hairline, #e2e8f0);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--ln-surface, #f8fafc) 80%, #fff);
+}
+
+.lc-delivery-change__history-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+.lc-delivery-change__history-row + .lc-delivery-change__history-row {
+ margin-top: 6px;
+}
+
+.lc-delivery-change__history-field-label {
+ flex-shrink: 0;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delivery-change__history-field-value {
+ color: var(--ln-body, #1e293b);
+ text-align: right;
+}
+
+.lc-delivery-change__date-compare {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.lc-delivery-change__date-before {
+ color: var(--ln-muted, #64748b);
+ text-decoration: line-through;
+}
+
+.lc-delivery-change__date-arrow {
+ color: var(--ln-muted, #94a3b8);
+ font-size: 11px;
+}
+
+.lc-delivery-change__date-after {
+ color: var(--ln-primary, #32a06e);
+ font-weight: 600;
+}
+
+.vm-page.lc-page .lc-approval-type-label {
+ display: inline-block;
+ font-size: 13px;
+ color: var(--ln-body, #1e293b);
+ line-height: 1.35;
+}
+
+.vm-page.lc-page .lc-approval-type-label--nonstandard {
+ color: #b45309;
+ font-weight: 500;
+}
+
+.vm-page.lc-page .lc-delegate-count-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 28px;
+ padding: 0 4px;
+ border: none;
+ background: transparent;
+ color: var(--ln-link, var(--ln-primary, #32a06e));
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ border-radius: var(--ln-radius-xs, 4px);
+ transition: color 0.15s ease, background-color 0.15s ease;
+}
+
+.vm-page.lc-page .lc-delegate-count-btn:hover {
+ color: var(--ln-primary-hover, #278a5c);
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 8%, transparent);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.vm-page.lc-page .lc-delegate-count-btn:focus-visible {
+ outline: 2px solid var(--ln-primary, #32a06e);
+ outline-offset: 1px;
+}
+
+.lc-delegate-popover .ant-popover-inner {
+ padding: 0;
+ border-radius: 12px;
+ box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14);
+}
+
+.lc-delegate-popover .ant-popover-title {
+ display: none;
+}
+
+.lc-delegate-popover__panel {
+ width: min(340px, calc(100vw - 32px));
+ max-height: min(420px, calc(100vh - 120px));
+ overflow: auto;
+}
+
+.lc-delegate-popover__header {
+ padding: 14px 16px 10px;
+ border-bottom: 1px solid var(--ln-hairline, #e2e8f0);
+}
+
+.lc-delegate-popover__title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--ln-body, #1e293b);
+}
+
+.lc-delegate-popover__sub {
+ margin-top: 2px;
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delegate-popover__empty {
+ margin: 0;
+ padding: 16px;
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delegate-popover__list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px 16px 14px;
+}
+
+.lc-delegate-popover__card {
+ padding: 10px 12px;
+ border: 1px solid var(--ln-hairline, #e2e8f0);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--ln-surface, #f8fafc) 80%, #fff);
+}
+
+.lc-delegate-popover__card-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.lc-delegate-popover__name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ln-body, #1e293b);
+}
+
+.lc-delegate-popover__index {
+ font-size: 11px;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delegate-popover__field {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+.lc-delegate-popover__field + .lc-delegate-popover__field {
+ margin-top: 6px;
+}
+
+.lc-delegate-popover__label {
+ flex-shrink: 0;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-delegate-popover__value {
+ color: var(--ln-body, #1e293b);
+ text-align: right;
+ word-break: break-all;
+}
+
+.vm-page.lc-page .vm-list-table .lc-approval-status-col,
+.vm-page.lc-page .vm-list-table .lc-contract-status-col {
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-approval-status-cell {
+ align-items: flex-start;
+}
+
+.vm-page.lc-page .lc-approval-status-cell__tag-wrap {
+ display: inline-flex;
+}
+
+.vm-page.lc-page .lc-approval-status-cell__tag-wrap--interactive {
+ cursor: help;
+}
+
+.vm-page.lc-page .lc-approval-status-cell__approver {
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.vm-page.lc-page .lc-signing-company-col {
+ max-width: 220px;
+}
+
+.vm-page.lc-page .lc-signing-company-cell {
+ display: inline-block;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: middle;
+}
+
+/* 租赁/已交车 Popover 卡片 */
+.lc-vehicle-popover .ant-popover-inner {
+ padding: 0;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
+}
+
+.lc-vehicle-popover .ant-popover-title {
+ display: none;
+}
+
+.lc-vehicle-popover__panel {
+ width: min(380px, calc(100vw - 32px));
+ max-height: min(420px, calc(100vh - 120px));
+ display: flex;
+ flex-direction: column;
+ background: var(--ln-surface-card, #fff);
+}
+
+.lc-vehicle-popover__header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--ln-hairline, #e2e8f0);
+ background: linear-gradient(180deg, color-mix(in srgb, var(--ln-primary, #32a06e) 6%, #fff), #fff);
+}
+
+.lc-vehicle-popover__header-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ flex-shrink: 0;
+ color: var(--ln-primary, #32a06e);
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 12%, #fff);
+ border: 1px solid color-mix(in srgb, var(--ln-primary, #32a06e) 18%, transparent);
+}
+
+.lc-vehicle-popover__header-icon.is-delivered {
+ color: #0f766e;
+ background: color-mix(in srgb, #0f766e 10%, #fff);
+ border-color: color-mix(in srgb, #0f766e 18%, transparent);
+}
+
+.lc-vehicle-popover__header-text {
+ min-width: 0;
+ flex: 1;
+}
+
+.lc-vehicle-popover__header-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--ln-ink, #0f172a);
+ line-height: 1.3;
+}
+
+.lc-vehicle-popover__header-sub {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 4px;
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-vehicle-popover__count {
+ display: inline-flex;
+ align-items: center;
+ padding: 1px 8px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--ln-primary, #32a06e);
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 10%, #fff);
+ border: 1px solid color-mix(in srgb, var(--ln-primary, #32a06e) 16%, transparent);
+}
+
+.lc-vehicle-popover__list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px;
+ overflow-y: auto;
+}
+
+.lc-vehicle-popover__empty {
+ padding: 28px 16px;
+ text-align: center;
+ font-size: 13px;
+ color: var(--ln-muted, #64748b);
+}
+
+.lc-vehicle-popover-card {
+ border: 1px solid var(--ln-hairline, #e2e8f0);
+ border-radius: 10px;
+ background: var(--ln-surface-card, #fff);
+ overflow: hidden;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.lc-vehicle-popover-card:hover {
+ border-color: color-mix(in srgb, var(--ln-primary, #32a06e) 28%, var(--ln-hairline, #e2e8f0));
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
+}
+
+.lc-vehicle-popover-card--delivered {
+ border-left: 3px solid #0f766e;
+}
+
+.lc-vehicle-popover-card__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 10px 12px;
+ background: var(--ln-surface-strong, #f8fafc);
+ border-bottom: 1px solid var(--ln-hairline, #e2e8f0);
+}
+
+.lc-vehicle-popover-card__type {
+ flex: 1;
+ min-width: 0;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--ln-body, #334155);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.lc-vehicle-popover-card__plate {
+ flex-shrink: 0;
+ padding: 2px 8px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ color: var(--ln-primary, #32a06e);
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 8%, #fff);
+ border: 1px solid color-mix(in srgb, var(--ln-primary, #32a06e) 20%, transparent);
+}
+
+.lc-vehicle-popover-card__plate--pending {
+ color: var(--ln-muted, #64748b);
+ background: var(--ln-surface-strong, #f8fafc);
+ border-color: var(--ln-hairline, #e2e8f0);
+ font-weight: 500;
+}
+
+.lc-vehicle-popover-card__body {
+ padding: 10px 12px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.lc-vehicle-popover-card__field {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.lc-vehicle-popover-card__label {
+ font-size: 11px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.3;
+}
+
+.lc-vehicle-popover-card__value {
+ font-size: 13px;
+ color: var(--ln-body, #334155);
+ line-height: 1.4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.lc-vehicle-popover-card__delivery {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 12px;
+ padding-top: 4px;
+ margin-top: 2px;
+ border-top: 1px dashed var(--ln-hairline, #e2e8f0);
+}
+
+.lc-vehicle-popover-card__delivery-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--ln-body, #334155);
+}
+
+.lc-vehicle-popover-card__delivery-item svg {
+ flex-shrink: 0;
+ color: #0f766e;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .lc-vehicle-popover-card {
+ transition: none;
+ }
+}
+
+/* 列表嵌套子表:参照合同模板 expandable 样式 */
+.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 .ant-table-expanded-row > .ant-table-cell {
+ padding: 10px 12px 14px 60px;
+ background: var(--ln-canvas-soft, #f8fafc);
+}
+
+.vm-page.lc-page .lc-contract-vehicle-table-wrap {
+ width: 100%;
+ max-width: 100%;
+ overflow-x: auto;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table {
+ width: 100%;
+ min-width: 1120px;
+ border-collapse: collapse;
+ table-layout: fixed;
+ background: var(--ln-surface-card, #fff);
+ border: 1px solid var(--ln-hairline, #e2e8f0);
+ border-radius: var(--ln-radius-control, 8px);
+ overflow: hidden;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table th,
+.vm-page.lc-page .lc-contract-vehicle-native-table td {
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--ln-hairline, #e2e8f0);
+ font-size: 13px;
+ color: var(--ln-body, #334155);
+ vertical-align: middle;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table thead th {
+ background: var(--ln-surface-strong, #f1f5f9);
+ font-weight: 600;
+ color: var(--ln-muted, #64748b);
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__info {
+ width: 14%;
+ min-width: 156px;
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__pickup {
+ width: 7%;
+ min-width: 84px;
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__handover {
+ width: 10%;
+ min-width: 100px;
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__return-settlement {
+ width: 8%;
+ min-width: 88px;
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__mileage-req {
+ width: 32%;
+ min-width: 300px;
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__mileage-completion {
+ width: 14%;
+ min-width: 148px;
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__action {
+ width: 7%;
+ min-width: 64px;
+ white-space: normal !important;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row--triple {
+ gap: 0;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row__requirement,
+.vm-page.lc-page .lc-contract-mileage-req-row__period,
+.vm-page.lc-page .lc-contract-mileage-req-row__current {
+ flex: 1 1 0;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 0 8px;
+ box-sizing: border-box;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row--triple .lc-contract-mileage-req-row__period {
+ border-left: 1px solid var(--ln-hairline, #e2e8f0);
+ border-right: 1px solid var(--ln-hairline, #e2e8f0);
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row__section-label {
+ font-size: 11px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.2;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row__requirement--empty,
+.vm-page.lc-page .lc-contract-mileage-req-row__period--empty {
+ font-size: 13px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.35;
+ padding-top: 2px;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row__period-mile {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--ln-body, #334155);
+ line-height: 1.3;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row__period-date {
+ font-size: 12px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.3;
+}
+
+.vm-page.lc-page .lc-contract-mileage-req-row__km-line,
+.vm-page.lc-page .lc-contract-mileage-req-row__current-line {
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-contract-mileage-period-tag {
+ flex-shrink: 0;
+}
+
+.vm-page.lc-page .lc-contract-return-settlement,
+.vm-page.lc-page .lc-contract-pickup-payment {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 32px;
+}
+
+.vm-page.lc-page .lc-contract-return-settlement--empty,
+.vm-page.lc-page .lc-contract-pickup-payment--empty {
+ font-size: 13px;
+ color: var(--ln-muted, #64748b);
+}
+
+.vm-page.lc-page .lc-contract-status-tag-btn {
+ display: inline-flex;
+ padding: 0;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: var(--ln-radius-pill, 999px);
+ font: inherit;
+ line-height: 0;
+}
+
+.vm-page.lc-page .lc-contract-status-tag-btn:hover {
+ filter: brightness(0.96);
+}
+
+.vm-page.lc-page .lc-contract-status-tag-btn:focus-visible {
+ outline: 2px solid var(--ln-primary, #32a06e);
+ outline-offset: 2px;
+}
+
+.vm-page.lc-page .lc-overall-mileage-progress {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 6px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-overall-mileage-progress__bar-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-overall-mileage-progress__bar-row .lc-vehicle-mileage-progress__track {
+ flex: 1;
+ min-width: 72px;
+}
+
+.vm-page.lc-page .lc-overall-mileage-progress__forecast {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.vm-page.lc-page .lc-overall-mileage-progress__forecast .vm-status {
+ max-width: 100%;
+ white-space: normal;
+ text-align: left;
+ line-height: 1.25;
+}
+
+.vm-page.lc-page .lc-overall-mileage-progress--empty {
+ font-size: 13px;
+ color: var(--ln-muted, #64748b);
+}
+
+.vm-page.lc-page .lc-overall-mileage-progress .lc-vehicle-mileage-progress__track {
+ flex: 1;
+ min-width: 72px;
+}
+
+.vm-page.lc-page .lc-contract-type-label {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 13px;
+ color: var(--ln-body, #334155);
+ vertical-align: middle;
+}
+
+.vm-page.lc-page .lc-contract-mileage-completion {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-contract-mileage-completion--empty {
+ font-size: 13px;
+ color: var(--ln-muted, #64748b);
+}
+
+.vm-page.lc-page .lc-contract-mileage-completion__remaining {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--ln-body, #334155);
+ line-height: 1.3;
+}
+
+.vm-page.lc-page .lc-contract-mileage-completion .lc-vehicle-mileage-progress {
+ width: 100%;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-action-btn {
+ padding: 0;
+ height: auto;
+ font-size: 13px;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-action-empty {
+ color: var(--ln-muted, #64748b);
+ font-size: 13px;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation--empty {
+ font-size: 13px;
+ color: var(--ln-muted, #64748b);
+ line-height: 1.35;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation--clickable {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+ text-align: left;
+ font: inherit;
+ cursor: pointer;
+ border-radius: var(--ln-radius-xs, 4px);
+ transition: background-color 0.15s ease;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation--clickable:hover {
+ background: color-mix(in srgb, var(--ln-primary, #32a06e) 8%, transparent);
+}
+
+.vm-page.lc-page .lc-contract-handover-situation--clickable:focus-visible {
+ outline: 2px solid var(--ln-primary, #32a06e);
+ outline-offset: 1px;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation__line {
+ font-size: 13px;
+ line-height: 1.35;
+ color: var(--ln-primary, #32a06e);
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation__line--mile {
+ font-weight: 500;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation__line--person {
+ font-weight: 500;
+}
+
+.vm-page.lc-page .lc-contract-handover-situation__line--time {
+ font-weight: 400;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-info__line {
+ line-height: 1.35;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-info__line--primary {
+ color: var(--ln-primary, #32a06e);
+ font-size: 13px;
+ font-weight: 500;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-info__line--secondary {
+ color: var(--ln-body, #334155);
+ font-size: 12px;
+ font-weight: 400;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-native-table__progress {
+ width: 168px;
+}
+
+.vm-page.lc-page .lc-contract-vehicle-expand__empty {
+ padding: 8px 0;
+ font-size: 13px;
+ color: var(--ln-muted);
+}
+
+.vm-page.lc-page .lc-vehicle-mileage-progress {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.vm-page.lc-page .lc-vehicle-mileage-progress__track {
+ flex: 1;
+ min-width: 72px;
+ height: 8px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--ln-hairline) 65%, #fff);
+ overflow: hidden;
+}
+
+.vm-page.lc-page .lc-vehicle-mileage-progress__bar {
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--ln-primary, #32a06e), color-mix(in srgb, var(--ln-primary) 72%, #22c55e));
+ transition: width 0.2s ease;
+}
+
+.vm-page.lc-page .lc-vehicle-mileage-progress__label {
+ flex-shrink: 0;
+ font-size: 12px;
+ color: var(--ln-muted);
+ min-width: 36px;
+ text-align: right;
+}
diff --git a/src/prototypes/self-operated-business-ledger/annotation-source.json b/src/prototypes/self-operated-business-ledger/annotation-source.json
index 5a8309f..768729d 100644
--- a/src/prototypes/self-operated-business-ledger/annotation-source.json
+++ b/src/prototypes/self-operated-business-ledger/annotation-source.json
@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "self-operated-business-ledger",
"pageId": "list",
- "updatedAt": 1782697685043,
+ "updatedAt": 1782810809221,
"nodes": [
{
"id": "sob-filter",
@@ -98,6 +98,198 @@
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "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": "sob-doc-root",
@@ -114,7 +306,7 @@
"type": "link",
"id": "sob-link-self",
"title": "自营业务台账原型",
- "href": "/prototypes/self-operated-business-ledger",
+ "href": "./index.html",
"target": "self"
},
{
diff --git a/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json b/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json
index 8029b71..d7eb375 100644
--- a/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json
+++ b/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json
@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "vehicle-h2-fee-ledger",
"pageId": "list",
- "updatedAt": 1782796599832,
+ "updatedAt": 1782810809221,
"nodes": [
{
"id": "vh2-header",
@@ -252,6 +252,198 @@
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "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": "vh2-doc-root",
diff --git a/src/prototypes/vehicle-management/annotation-source.json b/src/prototypes/vehicle-management/annotation-source.json
index 834a4f7..7192daa 100644
--- a/src/prototypes/vehicle-management/annotation-source.json
+++ b/src/prototypes/vehicle-management/annotation-source.json
@@ -5,7 +5,7 @@
"version": 2,
"prototypeName": "vehicle-management",
"pageId": "list",
- "updatedAt": 1782486426114,
+ "updatedAt": 1782810809222,
"nodes": [
{
"id": "vm-doc-toolbar",
@@ -668,6 +668,198 @@
"assetMap": {},
"directory": {
"nodes": [
+ {
+ "type": "folder",
+ "id": "oneos-project-nav",
+ "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",
+ "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-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",
+ "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",
@@ -684,7 +876,7 @@
"type": "link",
"id": "vm-link-self",
"title": "车辆管理原型",
- "href": "/prototypes/vehicle-management",
+ "href": "./index.html",
"target": "self"
},
{
diff --git a/src/resources/contract-template-management/PRD.md b/src/resources/contract-template-management/PRD.md
index ef903bd..3a2ca72 100644
--- a/src/resources/contract-template-management/PRD.md
+++ b/src/resources/contract-template-management/PRD.md
@@ -2,13 +2,13 @@
| 项 | 内容 |
|---|---|
-| 文档版本 | v1.0 |
+| 文档版本 | v1.2 |
| 模块名称 | 合同模板管理 |
| 所属系统 | ONE-OS 业务管理 |
| 目标读者 | 前端 / 后端 / 测试 / 法务产品对接人 |
| 交互原型 | `/prototypes/contract-template-management` |
| 关联原型 | [车辆管理](/prototypes/vehicle-management)(车型主数据口径参考) |
-| 文档状态 | 已对齐原型,待研发排期 |
+| 文档状态 | 已对齐原型(v1.2:租赁预览贯通 / 冷启动 / 合同类型筛选) |
---
@@ -20,8 +20,8 @@
- 以 **Word 版式保真** 的方式维护整份合同;
- 按 **章节** 管理内容,并配置 **适用车型**;
-- 发布 **版本**,并指定 **标准模板** 供客服草拟新合同时默认使用;
-- 在合同主体中配置 **条件条款**(按订单车辆命中显隐)。
+- 发布 **版本**,通过 **启用/停用** 控制模板是否可供租赁合同选用;
+- 在合同主体及租赁订单~安全责任协议各章节中配置 **条件条款**(按订单车辆命中显隐)。
### 1.2 产品目标
@@ -29,9 +29,9 @@
|---|---|
| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |
| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |
-| 支撑业务差异化 | 附件章节按车型绑定;合同主体支持条件条款 |
-| 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本 |
-| 标准模板唯一 | 同一合同分类同时仅一套标准模板生效 |
+| 支撑业务差异化 | 附件章节按车型绑定;各章节均支持条件条款 |
+| 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本并写入版本日志 |
+| 多套模板并存 | 正式/试用、不同车型等多套已发布模板可同时存在;租赁创建时由用户选择 |
### 1.3 非目标(本期不做)
@@ -48,18 +48,19 @@
| 角色 | 诉求 |
|---|---|
-| 法务人员(主用户) | 导入/编辑 Word 模板、发布版本、设标准模板 |
-| 客服人员(间接用户) | 草拟新合同时默认引用标准模板(下游模块消费) |
+| 法务人员(主用户) | 导入/编辑 Word 模板、发布版本、启用/停用模板 |
+| 客服人员(间接用户) | 草拟新合同时从已启用模板中选择(下游「租赁合同管理」消费) |
| 运营 / 风控(间接用户) | 查看风控红线条款配置情况 |
### 2.2 核心用户故事
1. **作为法务**,我希望导入一份完整 Word 合同,系统自动拆成 9 个章节,以便分章节维护。
2. **作为法务**,我希望为「租赁订单」等附件章节绑定适用车型,以便创建合同时只纳入匹配车辆的条款。
-3. **作为法务**,我希望在合同主体中为某段落设置「条件条款」并绑定车型,以便不同车型看到不同正文。
+3. **作为法务**,我希望在合同主体及租赁订单~安全责任协议各章节为某段落设置「条件条款」并绑定车型,以便不同车型看到不同正文。
4. **作为法务**,我希望保存草稿不产生版本号,发布后才生成版本号,以便区分未上线内容。
-5. **作为法务**,我希望将某已发布版本设为「标准合同」,且系统自动禁用旧标准,以便客服始终使用唯一标准。
-6. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。
+5. **作为法务**,我希望对已发布模板执行启用/停用,停用后方可编辑,并在发布变更时自动归档旧版快照到版本日志。
+6. **作为客服**,我希望在创建租赁合同时第一步选择要使用的已启用合同模板。
+7. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。
---
@@ -71,10 +72,12 @@
| 章节 | 合同内的逻辑切片,共 9 个(1 个主体 + 8 个附件) |
| 草稿 | `status = draft`,无版本号 |
| 已发布 | `status = published`,有版本号 `vX.XX` |
-| 标准模板 | `isDefault = true` 且已发布的模板;同分类唯一 |
+| 启用 | `enabled = true` 的已发布模板,可供租赁合同创建选用 |
+| 停用 | `enabled = false` 的已发布模板,不可编辑/删除,不可被租赁创建选用 |
+| 版本日志 | `versionLogs[]`,记录发布变更前自动归档的快照 |
| 锁定区 | 编辑区内不可随意修改的段落,HTML 标记 `data-inline-lock="1"` |
| 风控红线条款 | 需重点合规关注的条款,HTML 标记 `data-risk-redline="1"` |
-| 条件条款 | 仅合同主体可用,按订单车辆命中显隐,HTML 标记 `data-vehicle-clause="1"` |
+| 条件条款 | 各章节可用,按订单车辆命中显隐,HTML 标记 `data-vehicle-clause="1"` |
| 适用车型绑定 | 附件章节级别配置,决定该章节是否纳入某份合同 |
| 模板占位符 | 形如 `{{contractCode}}` 的变量,创建合同时由系统替换 |
@@ -93,7 +96,7 @@
└── 编辑 / 新增页
├── 顶栏(返回、保存、保存并发布)
├── 章节 Tab(仅本页展示)
- ├── 条件条款说明(合同主体 Tab 时显示)
+ ├── 条件条款说明(各章节均可使用工具栏「条件」)
├── 适用车型绑定(附件章节 Tab 时显示)
└── 工作区
├── 左侧:Word 版式编辑区
@@ -112,11 +115,13 @@
interface ContractTemplateDocument {
id: string; // 唯一 ID
category: 'lease'; // 合同分类,当前固定商用车租赁
+ contractType: string; // 用户自定义合同类型,如「正式合同」「试用合同」
fileName: string; // Word 文件名,如「现代18吨车型-数智中心版.docx」
versionName: string; // 展示用版本名称
versionNo: string | null; // 已发布:v1.00;草稿:null
status: 'draft' | 'published';
- isDefault: boolean; // 是否标准模板
+ enabled: boolean; // 已发布模板是否启用(草稿恒为 false)
+ versionLogs: VersionLogEntry[]; // 版本变更日志与快照
sectionContents: Record; // 各章节 HTML
sectionVehicleBindings: Record; // 各章节车型绑定
creator: string;
@@ -126,6 +131,24 @@ interface ContractTemplateDocument {
remark?: string; // 内部备注(列表不展示)
}
+interface VersionLogEntry {
+ id: string;
+ versionNo: string; // 归档时的版本号
+ action: string; // 如「发布变更」
+ operator: string;
+ time: string;
+ remark?: string;
+ snapshot: {
+ fileName: string;
+ versionName: string;
+ versionNo: string;
+ sectionContents: Record;
+ sectionVehicleBindings: Record;
+ remark?: string;
+ archivedAt: string;
+ };
+}
+
interface VehicleBinding {
brand: string; // 品牌,如「现代」
model: string; // 型号,如「18吨氢燃料电池车」
@@ -137,14 +160,14 @@ interface VehicleBinding {
| key | 章节名称 | 车型绑定 | 条件条款 |
|---|---|:---:|:---:|
| `main` | 合同主体 | 否 | 是 |
-| `lease_order` | 租赁订单 | 是 | 否 |
-| `vehicle_cost` | 车辆产生的费用 | 是 | 否 |
-| `safety_notice` | 安全责任告知书 | 是 | 否 |
-| `dynamic_supervision` | 重型普通货运车辆租赁动态监管责任告知与确认书 | 是 | 否 |
-| `safety_commitment` | 安全责任承诺书 | 是 | 否 |
-| `vehicle_spec` | 车辆使用规范和驾驶员要求 | 是 | 否 |
-| `authorization` | 授权委托书 | 是 | 否 |
-| `safety_agreement` | 安全责任协议 | 是 | 否 |
+| `lease_order` | 租赁订单 | 是 | 是 |
+| `vehicle_cost` | 车辆产生的费用 | 是 | 是 |
+| `safety_notice` | 安全责任告知书 | 是 | 是 |
+| `dynamic_supervision` | 重型普通货运车辆租赁动态监管责任告知与确认书 | 是 | 是 |
+| `safety_commitment` | 安全责任承诺书 | 是 | 是 |
+| `vehicle_spec` | 车辆使用规范和驾驶员要求 | 是 | 是 |
+| `authorization` | 授权委托书 | 是 | 是 |
+| `safety_agreement` | 安全责任协议 | 是 | 是 |
### 5.3 章节内容 HTML 标记规范
@@ -170,41 +193,48 @@ interface VehicleBinding {
### 6.2 保存与发布
-| 操作 | 新增模式 | 编辑草稿 | 编辑已发布 |
+| 操作 | 新增模式 | 编辑草稿 | 编辑已发布(须先停用) |
|---|---|---|---|
-| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**降为草稿**(清空版本号) |
-| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 |
+| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**保持已发布与版本号**,仍为停用态 |
+| **保存并发布** | 新建已发布记录 + 版本号,默认启用 | 原记录变已发布 + 版本号,默认启用 | **同记录升级版本号**;旧内容写入 `versionLogs` 快照;发布后默认停用,需手动启用 |
**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。
-### 6.3 标准模板规则
+**离开编辑**:有未保存修改时返回列表需二次确认。
-1. 同一 `category` **同时仅允许一条** `isDefault = true` 且 `status = published` 的记录。
-2. 仅 **已发布且非当前标准** 的记录可执行「设为标准模板」。
-3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。
-4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。
-5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。
+### 6.3 启用 / 停用规则
-### 6.4 删除规则
+1. 仅 **已发布** 模板具有启用态;草稿显示「—」。
+2. **启用中**:不可编辑、不可删除;可供租赁合同创建选用。
+3. **停用**:需二次确认(提示可能导致标准合同无法正常使用);停用后可编辑、可删除;租赁创建不可选用。
+4. **启用**:直接生效,无需二次确认。
+5. 编辑已发布模板并发布后,模板自动变为 **停用**,需重新启用后下游才可选。
-- 仅 **非标准模板** 可删除。
-- 删除前二次确认;删除后不可恢复。
+### 6.4 版本日志规则
-### 6.5 列表排序
+1. 对已发布模板执行「保存并发布」前,系统将 **当前内容自动归档** 为快照写入 `versionLogs`。
+2. 列表操作列提供「版本日志」,可查看历史版本号、操作人、时间与快照正文(只读)。
+3. 列表主表 **一行对应一份 Word 文件**,历史版本通过日志追溯,不另增列表行。
-1. 标准模板置顶;
-2. 其余按 `updateTime` 降序。
+### 6.5 删除规则
-### 6.6 筛选匹配规则
+- **启用中** 的已发布模板不可删除。
+- 其余记录可删除;删除前二次确认;删除后不可恢复。
+
+### 6.6 列表排序
+
+按 `updateTime` 降序。
+
+### 6.7 筛选匹配规则
| 字段 | 匹配逻辑 |
|---|---|
-| Word 文件名称 | 模糊匹配 `fileName`、`versionName`、`versionNo`、`creator`、`updater` |
+| 合同类型 | 下拉选择(支持输入模糊搜索候选项);精确匹配 `contractType` |
| 状态 | 精确匹配 `draft` / `published` |
| 创建人 | 精确匹配 `creator` |
-| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 |
+| 启用状态 | `enabled`:仅已发布且启用;`disabled`:仅已发布且停用 |
-> **已移除**:筛选项「适用型号」、主表列「备注」(产品确认不再展示)。
+> **已移除**:筛选项「适用品牌/适用车型」、主表列「备注」、「设为标准模板」能力。
---
@@ -218,10 +248,10 @@ interface VehicleBinding {
| 字段 | 控件 | 说明 |
|---|---|---|
-| Word 文件名称 | 文本输入 | 支持回车触发查询 |
+| 合同类型 | 可搜索下拉 | 选项来自现有模板 `contractType` 去重;支持输入模糊搜索 |
| 状态 | 下拉 | 全部 / 已发布 / 草稿 |
| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |
-| 适用品牌 | 下拉 | 选项来自车型主数据品牌列表 |
+| 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |
**按钮**:
@@ -239,9 +269,10 @@ interface VehicleBinding {
| 列名 | 字段 | 展示规则 |
|---|---|---|
| (展开列) | — | 宽 48px |
-| Word 文件名称 | `fileName` | 超长省略;标准模板显示金色标签「标准合同」 |
+| 合同类型 | `contractType` | 展示用户自定义的合同类型文案 |
| 版本号 | `versionNo` | 草稿显示「—」 |
| 状态 | `status` | 已发布 / 草稿 Tag |
+| 启用状态 | `enabled` | 已发布:启用/停用;草稿:「—」 |
| 创建时间 | `createTime` | — |
| 创建人 | `creator` | 展示短名(如 `法务-王静` → `王静`) |
| 最后更新时间 | `updateTime` | — |
@@ -254,14 +285,10 @@ interface VehicleBinding {
| 操作 | 显示条件 | 行为 |
|---|---|---|
-| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` |
-| 设为标准模板 | 已发布 && 非标准 | 弹窗确认后切换标准 |
-| 删除 | 非标准 | 弹窗确认后删除 |
-
-**设为标准模板弹窗文案要点**:
-
-- 说明:同一分类同时只能有一套标准模板;客服草拟新合同时将默认使用新标准。
-- 若存在旧标准:明示将先禁用「{旧标准文件名}」,再设置「{新标准文件名}」。
+| 启用 / 停用 | 已发布 | 停用需确认;启用直接生效 |
+| 版本日志 | 已发布 | 查看历史快照 |
+| 编辑 | 草稿,或已发布且停用 | 启用中按钮禁用 |
+| 删除 | 草稿,或已发布且停用 | 启用中按钮禁用;删除前确认 |
### 7.6 子表(展开行)
@@ -272,8 +299,7 @@ interface VehicleBinding {
| 章节 | 章节名称 |
| 适用车型 | 附件章节展示绑定列表,格式「品牌 · 型号」;多个用「;」连接;未绑定显示「全部车型」;合同主体显示「—」 |
| 风控条款数 | 该章节 HTML 内风控红线标记数量 |
-| 条件条款数 | 仅合同主体统计条件条款数;其他章节显示 `0` |
-| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |
+| 条件条款数 | 按各章节 HTML 统计条件条款标记数量 |
---
@@ -293,7 +319,7 @@ interface VehicleBinding {
### 8.3 章节 Tab
-- 共 9 个章节;**前 4 个直显**,其余收入「更多」下拉。
+- 共 9 个章节;**横向滚动 Tab 条**展示全部章节,右侧提供「章节列表」按钮打开可搜索的章节面板(便于长标题章节快速定位)。
- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。
- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**。
- **其他 Tab**:仅展示该章节切片内容。
@@ -310,11 +336,11 @@ interface VehicleBinding {
8. 授权委托书
9. 安全责任协议
-### 8.4 条件条款说明(仅合同主体 Tab)
+### 8.4 条件条款说明(合同主体及租赁订单~安全责任协议)
展示说明卡片,文案要点:
-> 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。
+> 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。适用于合同主体及全部附件章节。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。
### 8.5 适用车型绑定(仅附件章节 Tab)
@@ -349,9 +375,11 @@ interface VehicleBinding {
- **锁定**:选中内容设为锁定区
- **解锁**:解除锁定
- **风控**:标记风控红线条款
- - **条件**(仅合同主体):选中段落绑定车型条件
+ - **条件**:在各章节选中段落绑定车型条件
-#### 8.6.3 条件条款交互(合同主体)
+#### 8.6.3 条件条款交互
+
+**适用范围**:合同主体、租赁订单、车辆产生的费用、安全责任告知书、重型普通货运车辆租赁动态监管责任告知与确认书、安全责任承诺书、车辆使用规范和驾驶员要求、授权委托书、安全责任协议。
1. 用户选中段落或文字;
2. 点击工具栏「条件」;
@@ -374,11 +402,11 @@ interface VehicleBinding {
`锁定 N 处 · 风控红线 M 处 · 条件条款 K 处`
-合同主体且已选模拟车型时追加:`(预览命中 X)`
+当前章节支持条件且已选模拟车型时追加:`(预览命中 X)`
-#### 8.7.3 模拟车型(仅合同主体)
+#### 8.7.3 模拟车型
-- 多选下拉,选项来自车型主数据(品牌 · 型号)。
+- 在合同主体及全部附件章节可用;多选下拉,选项来自车型主数据(品牌 · 型号)。
- 未选车型时:**隐藏**所有条件条款段落(与创建合同时无车辆命中行为一致)。
- 选中后按命中规则过滤并展开条件条款正文。
@@ -493,7 +521,7 @@ interface VehicleBinding {
```mermaid
flowchart LR
- A[合同模板管理] -->|标准模板 + 章节内容| B[租赁合同创建]
+ A[合同模板管理] -->|已启用模板 + 章节内容| B[租赁合同创建]
A -->|车型绑定口径| C[车辆管理]
A -->|占位符变量定义| B
B -->|订单车辆列表| A
@@ -501,7 +529,7 @@ flowchart LR
| 模块 | 关系 |
|---|---|
-| 租赁合同创建 | 读取标准模板或指定版本;按订单车辆过滤条件条款与附件章节 |
+| 租赁合同创建 | 创建流程第一步选择 **已启用** 的已发布模板;按订单车辆过滤条件条款与附件章节 |
| 车辆管理 | 车型品牌/型号枚举口径一致 |
| 非标准合同审批 | 合同正文出现 `data-nonstd-trigger` 时触发(标记规范已预留) |
@@ -513,10 +541,10 @@ flowchart LR
|---|---|
| 查看列表 | 法务、客服(只读) |
| 新增 / 编辑 / 发布 | 法务 |
-| 设为标准模板 | 法务负责人 |
+| 启用 / 停用 | 法务 |
| 删除 | 法务负责人 |
-**审计字段**:`creator`、`createTime`、`updater`、`updateTime` 必须在每次保存时更新。
+**审计字段**:`creator`、`createTime`、`updater`、`updateTime` 必须在每次保存时更新;发布变更写入 `versionLogs`。
---
@@ -525,7 +553,7 @@ flowchart LR
### 14.1 列表查询
```
-GET /api/contract-templates?category=lease&keyword=&status=&creator=&vehicleBrand=&page=1&pageSize=10
+GET /api/contract-templates?category=lease&fileName=&status=&creator=&enabled=&page=1&pageSize=10
```
响应需包含主表字段;章节摘要可列表内嵌或展开时按需加载。
@@ -536,7 +564,7 @@ GET /api/contract-templates?category=lease&keyword=&status=&creator=&vehicleBran
GET /api/contract-templates/{id}
```
-返回完整 `sectionContents`、`sectionVehicleBindings`。
+返回完整 `sectionContents`、`sectionVehicleBindings`、`versionLogs`。
### 14.3 创建 / 更新
@@ -547,13 +575,14 @@ PUT /api/contract-templates/{id}
请求体含章节 HTML 与绑定;`publish: boolean` 控制是否发布。
-### 14.4 设为标准
+### 14.4 启用 / 停用
```
-POST /api/contract-templates/{id}/set-default
+POST /api/contract-templates/{id}/enable
+POST /api/contract-templates/{id}/disable
```
-服务端事务:同 category 旧标准 `isDefault=false`,新标准 `isDefault=true`。
+停用前需二次确认;启用中记录拒绝编辑/删除类写操作(由业务层校验)。
### 14.5 删除
@@ -561,7 +590,7 @@ POST /api/contract-templates/{id}/set-default
DELETE /api/contract-templates/{id}
```
-拒绝删除 `isDefault=true` 的记录。
+拒绝删除 `enabled=true` 的记录。
### 14.6 Word 导入
@@ -590,31 +619,35 @@ Content-Type: multipart/form-data
### 16.1 列表页
-- [ ] 筛选项包含:Word 文件名称、状态、创建人、适用品牌;**不含**适用型号、备注列
+- [ ] 筛选项包含:合同类型(可搜索下拉)、状态、创建人、启用状态
+- [ ] 主表含合同类型、启用状态列;操作含启用/停用、版本日志、编辑、删除
+- [ ] 启用中模板编辑/删除按钮禁用;停用需二次确认
- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表
-- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态
-- [ ] 标准模板金色标签置顶,且不可删除
-- [ ] 切换标准模板时旧标准自动禁用
+- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数
### 16.2 编辑页
-- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多
+- [ ] 章节 Tab 仅在编辑页展示;横向滚动 + 章节列表面板
- [ ] 导入 Word 后 9 章节正确拆分
-- [ ] 附件章节可绑定车型;合同主体可设条件条款
-- [ ] 预览支持连续/单页切换;合同主体可模拟车型验证条件条款
+- [ ] 附件章节可绑定车型;各章节均可设条件条款
+- [ ] 预览支持连续/单页切换;各章节可模拟车型验证条件条款
- [ ] 保存草稿无版本号;发布生成版本号
-- [ ] 编辑已发布并发布时产生新版本,旧版本保留
+- [ ] 编辑已发布(停用后)并发布时版本号递增,旧版写入版本日志
+- [ ] 编辑已发布(停用后)点「保存」不改变发布态与版本号
+- [ ] 有未保存修改时返回列表需确认
-### 16.3 数据一致性
+### 16.3 跨模块与数据一致性
+- [ ] 模板管理停用后,租赁合同创建页不可再选用该模板
+- [ ] 租赁创建预览读取 store 中模板 `sectionContents`(与模板管理编辑一致)
- [ ] 列表子表「风控条款数」与编辑区统计一致
-- [ ] 列表子表「条件条款数」仅合同主体可大于 0
-- [ ] 筛选「适用品牌」与子表车型绑定逻辑一致
+- [ ] 列表子表「条件条款数」与各章节实际标记数一致
### 16.4 标注与文档(原型)
- [ ] 原型已接入 `@axhub/annotation`,关键区域有 `data-annotation-id`
- [ ] 目录路由可切换列表页 / 新增页
+- [ ] `PRD.md` 与 `annotation-source.json` 与当前实现一致
---
@@ -623,24 +656,29 @@ Content-Type: multipart/form-data
| 能力 | 原型文件 / 符号 |
|---|---|
| 主逻辑 | `src/prototypes/contract-template-management/ContractTemplate.jsx` |
+| 跨模块模板状态 | `src/prototypes/contract-template-management/contract-template-store.js` |
+| 章节 HTML 合并 | `src/prototypes/contract-template-management/contract-template-section-html.js` |
+| 冷启动种子 | `src/prototypes/contract-template-management/contract-template-seed.js` |
+| 租赁侧消费目录 | `src/prototypes/contract-template-management/contract-template-catalog.js` |
| 入口与标注 | `src/prototypes/contract-template-management/index.tsx` |
| 标注数据 | `src/prototypes/contract-template-management/annotation-source.json` |
+| 下游选用 | `src/prototypes/lease-contract-management/LeaseContractCreate.jsx` |
| 占位符与出租方 | `src/prototypes/contract-template-management/contract-template-vars.js` |
| 样式 | `src/prototypes/contract-template-management/styles/` |
+> **原型限制**:模板数据经 `localStorage`(兼写 `sessionStorage`)持久化;冷启动由 `contract-template-seed.js` 注入种子,无需先打开模板管理页即可在租赁创建选用已启用模板。
+
---
-## 18. 附录:标准模板确认弹窗(原文案)
+## 18. 附录:停用确认弹窗(原文案)
-**标题**:确认设为标准模板
+**标题**:确认停用
**正文**:
-> 同一分类同时只能有一套标准模板生效。确认后,客服草拟新合同时将默认使用新标准模板。
->
-> (若存在旧标准)将先禁用当前标准模板「{旧标准名称}」,再设置「{新标准名称}」为新的标准模板。
+> 停用后,该模板将不可在租赁合同创建时选用,是否确定?
-**按钮**:取消 / 确认设置
+**按钮**:取消 / 确定停用
---
@@ -648,4 +686,6 @@ Content-Type: multipart/form-data
| 版本 | 日期 | 作者 | 说明 |
|---|---|---|---|
-| v1.0 | 2026-06-25 | 产品 | 基于原型与需求讨论首版;明确 Word 文档级模型、嵌套列表、标准模板唯一、移除适用型号筛选与备注列 |
+| v1.2 | 2026-06-29 | 产品 | 租赁预览贯通 store;冷启动种子;列表按合同类型筛选;移除 isDefault;localStorage 持久化;PRD 与实现对齐 |
+| v1.1 | 2026-06-29 | 产品 | 移除标准模板模型,改为启用/停用 + 版本日志;列表筛选与操作同步;各章节支持条件条款 |
+| v1.0 | 2026-06-25 | 产品 | 首版:Word 文档级模型、嵌套列表、章节拆分与条件条款 |