From 2e0a3d7da4cd46dd086417fc26f34dd484b5576e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=86=95?= Date: Wed, 1 Jul 2026 09:11:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=AD=E4=BB=A3=20ONE-OS=20=E5=A4=9A?= =?UTF-8?q?=E5=8E=9F=E5=9E=8B=EF=BC=9A=E5=90=88=E5=90=8C=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=8E=E7=A7=9F=E8=B5=81=E5=90=88=E5=90=8C?= =?UTF-8?q?=E5=88=97=E8=A1=A8/=E8=A1=A8=E5=8D=95=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合同模板管理:模板数据层、章节编辑与品牌车型选择器 - 租赁合同管理:列表子表、交车安排、里程汇总及新建/编辑表单优化 - 同步各原型 annotation-source 与 PRD;新增项目导航同步脚本 Co-authored-by: Cursor --- .axhub/make/axhub.config.json | 34 + .axhub/make/sidebar-tree.json | 4 +- .../annotation-nodes.json | 287 ++++ .../markdown-nodes.json | 57 + .local/extract-annotation-source/output.json | 711 ++++++--- package.json | 1 + scripts/sync-project-prototype-directory.mjs | 188 +++ src/common/useHashPage.ts | 13 + .../.spec/prototype-review.md | 54 + .../.spec/ui-review.md | 89 ++ .../ContractTemplate.jsx | 1184 ++++++++------ .../VehicleBrandModelPicker.jsx | 352 +++++ .../annotation-source.json | 398 ++++- .../contract-template-catalog.js | 75 + .../contract-template-section-filter.js | 95 ++ .../contract-template-section-html.js | 102 ++ .../contract-template-seed.js | 164 ++ .../contract-template-store.js | 205 +++ .../contract-template-types.js | 23 + .../contract-template-management/index.tsx | 7 +- .../scripts/build-annotation-source.mjs | 104 ++ .../styles/contract-template.css | 628 +++++++- .../vehicle-binding-utils.js | 58 + .../annotation-source.json | 196 ++- .../annotation-source.json | 388 ++++- .../annotation-source.json | 196 ++- .../.spec/requirements-prd-create.md | 6 +- .../.spec/requirements-prd-list.md | 9 +- .../LeaseContractCreate.jsx | 504 +++++- .../LeaseContractEditorForm.jsx | 391 ++++- .../LeaseContractManagement.jsx | 1390 +++++++++++++---- .../LeaseContractPreviewPanel.jsx | 46 +- .../annotation-source.json | 206 ++- .../lease-contract-management/index.tsx | 1 + .../lease-business-org-data.js | 37 + .../lease-contract-form-validation.js | 25 +- .../lease-contract-list-data.js | 682 +++++++- .../lease-contract-preview-build.js | 11 +- .../lease-contract-risk-detect.js | 4 +- .../lease-order-vars.js | 4 + .../scripts/build-annotation-source.mjs | 2 +- .../styles/lease-contract-create.css | 665 +++++++- .../styles/lease-contract.css | 1229 +++++++++++++++ .../annotation-source.json | 196 ++- .../annotation-source.json | 194 ++- .../vehicle-management/annotation-source.json | 196 ++- .../contract-template-management/PRD.md | 224 +-- 47 files changed, 10200 insertions(+), 1435 deletions(-) create mode 100644 .local/extract-annotation-source/annotation-nodes.json create mode 100644 .local/extract-annotation-source/markdown-nodes.json create mode 100644 scripts/sync-project-prototype-directory.mjs create mode 100644 src/prototypes/contract-template-management/.spec/prototype-review.md create mode 100644 src/prototypes/contract-template-management/.spec/ui-review.md create mode 100644 src/prototypes/contract-template-management/VehicleBrandModelPicker.jsx create mode 100644 src/prototypes/contract-template-management/contract-template-catalog.js create mode 100644 src/prototypes/contract-template-management/contract-template-section-filter.js create mode 100644 src/prototypes/contract-template-management/contract-template-section-html.js create mode 100644 src/prototypes/contract-template-management/contract-template-seed.js create mode 100644 src/prototypes/contract-template-management/contract-template-store.js create mode 100644 src/prototypes/contract-template-management/contract-template-types.js create mode 100644 src/prototypes/contract-template-management/scripts/build-annotation-source.mjs create mode 100644 src/prototypes/contract-template-management/vehicle-binding-utils.js create mode 100644 src/prototypes/lease-contract-management/lease-business-org-data.js 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 文档级模型、嵌套列表、章节拆分与条件条款 |