迭代 ONE-OS 多原型:合同模板重构与租赁合同列表/表单增强

- 合同模板管理:模板数据层、章节编辑与品牌车型选择器
- 租赁合同管理:列表子表、交车安排、里程汇总及新建/编辑表单优化
- 同步各原型 annotation-source 与 PRD;新增项目导航同步脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-01 09:11:54 +08:00
parent e278332e3d
commit 2e0a3d7da4
47 changed files with 10200 additions and 1435 deletions

View File

@@ -14,5 +14,39 @@
"remote": { "remote": {
"url": "https://gitea.lnh2e.com/wangmian/OneOS1.2.git" "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"
]
}
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-06-30T08:53:12.196Z", "updatedAt": "2026-06-30T10:18:56.724Z",
"prototypes": [ "prototypes": [
{ {
"id": "item:prototypes:insurance-procurement", "id": "item:prototypes:insurance-procurement",
@@ -945,4 +945,4 @@
"templates": [], "templates": [],
"components": [], "components": [],
"canvas": [] "canvas": []
} }

View File

@@ -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 个章节,**全部在横向滚动条中展示**(带序号 19当前章节自动滚入视野。\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": []
}
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@
"dev": "node scripts/sync-vendor-if-present.mjs && vite", "dev": "node scripts/sync-vendor-if-present.mjs && vite",
"start": "npm run dev", "start": "npm run dev",
"metadata:sync": "node scripts/sync-project-metadata.mjs", "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", "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", "build": "node scripts/sync-vendor-if-present.mjs && node scripts/scan-entries.js && node scripts/build-all.js",
"preview": "vite preview", "preview": "vite preview",

View File

@@ -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();

View File

@@ -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') { export function useHashPage(routeOrDefault: HashPageRoute | string = 'home') {
const route = normalizeRouteInput(routeOrDefault); const route = normalizeRouteInput(routeOrDefault);
const { pages, defaultPageId } = route; const { pages, defaultPageId } = route;

View File

@@ -0,0 +1,54 @@
# Prototype Review
- 审查目标:`src/prototypes/contract-template-management`
- 用户资料/参考资料:
- 本轮用户任务:处理评审结论中 P1P3 共 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 条评审问题全部处理完毕 |

View File

@@ -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` 7277 行);`LIST_TABLE_SCROLL_X` 合计约 1332px含操作列 300px`ContractTemplate.jsx`)。右侧还有 `@axhub/annotation` 工具栏占用宽度。
- 影响13 寸笔记本或分屏预览时,用户需频繁横向拖动才能看到「操作」列;筛选结果核对与批量处理效率下降。
- 修复方向≥1280px 保持全列;`<1200px` 将「启用状态」与次要时间列折叠为 expandable 摘要,或操作列收进「更多」菜单;与 Linear inverse 表格「密度可伸缩」原则对齐。
### P2 - 操作列 45 个并列链接,视觉密度与扫读成本高
- 证据:已发布行同时展示「停用/启用」「版本日志」「编辑」「删除」(`getListColumns` 操作列宽 300px多行换行时`Space wrap`)行高不齐。
- 影响:法务需逐行扫描多个同色链接,难以快速区分主操作与危险操作;与 DESIGN 中 button-primary / button-secondary 层级未充分映射到表格场景。
- 修复方向:主操作保留 12 个(如「编辑」);其余收入「更多」`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 报告 1120px 多档字号、比例约 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未拆分子代理未做浏览器实机检查

View File

@@ -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)
)
)
)
);
}

File diff suppressed because one or more lines are too long

View File

@@ -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(' · ');
}

View File

@@ -0,0 +1,95 @@
/**
* 按章节适用车型绑定,从整份合同 HTML 中移除未命中的附件章节。
*/
import { sectionBindingsMatchOrder } from './vehicle-binding-utils.js';
var ATTACHMENT_SECTIONS = [
{ key: 'lease_order', markers: ['<b>1租赁订单 </b>', '附件 1租赁订单', '附件1租赁订单', '1租赁订单'] },
{ key: 'vehicle_cost', markers: ['附件2:车辆产生的费用', '附件2车辆产生的费用', '<b>附件2车辆产生的费用</b>'] },
{ key: 'safety_notice', markers: ['附件3:安全责任告知书', '附件3安全责任告知书', '<b>附件3安全责任告知书</b>'] },
{ key: 'dynamic_supervision', markers: ['附件4重型普通货运车辆租赁动态监管责任告知与确认书', '附件4:'] },
{ key: 'safety_commitment', markers: ['<b>附件5</b>', '<b>安全责任承诺书</b></p><p class="ct-word-body">为严格落实', '附件5', '附件5:'] },
{ key: 'vehicle_spec', markers: ['<b>附件6车辆使用规范和驾驶员要求</b>', '附件6车辆使用规范和驾驶员要求', '附件6:'] },
{ key: 'authorization', markers: ['font: 28.0px Times; color: #000000"><b>授权委托书</b>', '<b>授权委托书</b></p><p class="ct-word-body">致:', '<b>授权委托书</b><b></b></p><p class="ct-word-body">致:'] },
{ key: 'safety_agreement', markers: ['data-risk-redline="1"><b>安全责任协议</b></span></p>', '<b>安全责任协议</b></p><p class="ct-word-body"><br></p><p class="ct-word-body"><b>甲方(出租方):</b>', '<b>安全责任协议</b></p><p class="ct-word-body"><br></p>'] },
];
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 && !/<b>/.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;
}

View File

@@ -0,0 +1,102 @@
/**
* 章节 HTML 合并 / 拆分(合同模板管理 ↔ 租赁合同预览共用)
*/
export var DOCUMENT_SECTION_TABS = [
{ key: 'main', label: '合同主体' },
{ key: 'lease_order', label: '租赁订单', markers: ['<b>1租赁订单 </b>', '附件 1租赁订单', '附件1租赁订单'] },
{ key: 'vehicle_cost', label: '车辆产生的费用', markers: ['附件2:车辆产生的费用', '附件2车辆产生的费用', '<b>附件2车辆产生的费用</b>'] },
{ key: 'safety_notice', label: '安全责任告知书', markers: ['附件3:安全责任告知书', '附件3安全责任告知书', '<b>附件3安全责任告知书</b>'] },
{ key: 'dynamic_supervision', label: '重型普通货运车辆租赁动态监管责任告知与确认书', markers: ['附件4重型普通货运车辆租赁动态监管责任告知与确认书', '附件4:'] },
{ key: 'safety_commitment', label: '安全责任承诺书', markers: ['<b>附件5</b>', '<b>安全责任承诺书</b></p><p class="ct-word-body">为严格落实', '附件5', '附件5:'] },
{ key: 'vehicle_spec', label: '车辆使用规范和驾驶员要求', markers: ['<b>附件6车辆使用规范和驾驶员要求</b>', '附件6车辆使用规范和驾驶员要求', '附件6:'] },
{ key: 'authorization', label: '授权委托书', markers: ['font: 28.0px Times; color: #000000"><b>授权委托书</b>', '<b>授权委托书</b></p><p class="ct-word-body">致:', '<b>授权委托书</b><b></b></p><p class="ct-word-body">致:'] },
{ key: 'safety_agreement', label: '安全责任协议', markers: ['data-risk-redline="1"><b>安全责任协议</b></span></p>', '<b>安全责任协议</b></p><p class="ct-word-body"><br></p><p class="ct-word-body"><b>甲方(出租方):</b>', '<b>安全责任协议</b></p><p class="ct-word-body"><br></p>'] },
];
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 && !/<b>/.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 = ['</p>', '</table>', '</div>'];
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;
}

View File

@@ -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吨车型试用协议含完整安全附件',
}),
];
}

View File

@@ -0,0 +1,205 @@
/**
* 合同模板跨原型共享状态(合同模板管理 ↔ 租赁合同创建)
* 原型级 storelocalStorage 优先持久化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;
}

View File

@@ -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);
}

View File

@@ -1,7 +1,8 @@
/** /**
* @name 合同模板管理 * @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 * as antd from 'antd';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import { import {
@@ -72,6 +73,10 @@ export default function ContractTemplateManagementPage() {
const [annotationPageId, setAnnotationPageId] = useState<ContractPageId>('list'); const [annotationPageId, setAnnotationPageId] = useState<ContractPageId>('list');
const navigationRef = useRef<ContractNavigationRef>({}); const navigationRef = useRef<ContractNavigationRef>({});
useEffect(() => {
clearHostPrototypeRouteInfo();
}, []);
const handleDirectoryRoute = useCallback((node: AnnotationDirectoryRouteNode) => { const handleDirectoryRoute = useCallback((node: AnnotationDirectoryRouteNode) => {
const payload = node.payload as { pageId?: string } | undefined; const payload = node.payload as { pageId?: string } | undefined;
const pageId: ContractPageId = payload?.pageId === 'editor' ? 'editor' : 'list'; const pageId: ContractPageId = payload?.pageId === 'editor' ? 'editor' : 'list';

View File

@@ -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).`);

View File

@@ -9,16 +9,16 @@
.vm-page.ct-page .ct-data-table.ant-table-wrapper .ant-table-thead > tr > th { .vm-page.ct-page .ct-data-table.ant-table-wrapper .ant-table-thead > tr > th {
background: var(--ln-canvas-soft) !important; background: var(--ln-canvas-soft) !important;
color: var(--ln-muted) !important; color: var(--ln-muted-80) !important;
font-size: 0.75rem !important; font-size: 12px !important;
font-weight: 500 !important; font-weight: 600 !important;
letter-spacing: 0 !important; letter-spacing: 0 !important;
border-bottom: 1px solid var(--ln-hairline) !important; border-bottom: 1px solid var(--ln-hairline) !important;
padding: 8px 12px !important; padding: 8px 12px !important;
} }
.vm-page.ct-page .ct-data-table.ant-table-wrapper .ant-table-tbody > tr > td { .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; color: var(--ln-body) !important;
border-bottom: 1px solid var(--ln-hairline) !important; border-bottom: 1px solid var(--ln-hairline) !important;
padding: 8px 12px !important; padding: 8px 12px !important;
@@ -78,7 +78,50 @@
} }
.vm-page.ct-page .ct-data-table--nested { .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, .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; 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 { .vm-page.ct-page .vm-table-section .vm-table-toolbar {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -223,7 +279,7 @@
} }
.ct-section-tabs-more-popover .ct-section-tabs-more { .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; padding: 12px 0 8px;
} }
@@ -251,7 +307,8 @@
.ct-section-tabs-more-popover .ct-section-tabs-more__item { .ct-section-tabs-more-popover .ct-section-tabs-more__item {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 10px;
flex: 0 0 auto; flex: 0 0 auto;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
@@ -265,12 +322,37 @@
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.45; line-height: 1.45;
text-align: left; text-align: left;
white-space: normal;
word-break: break-word;
cursor: pointer; cursor: pointer;
transition: background 0.15s ease, color 0.15s ease; 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 { .ct-section-tabs-more-popover .ct-section-tabs-more__item:hover {
background: var(--ln-surface-strong, #f1f5f4); background: var(--ln-surface-strong, #f1f5f4);
color: var(--ln-ink, #0f172a); color: var(--ln-ink, #0f172a);
@@ -417,17 +499,42 @@
========================================================================== */ ========================================================================== */
.vm-page.ct-page .ct-action-btn.ant-btn-link { .vm-page.ct-page .ct-action-btn.ant-btn-link {
min-height: var(--vm-btn-height); min-height: 44px;
padding: 0 8px !important; padding: 0 10px !important;
font-size: 0.875rem !important; font-size: 0.875rem !important;
font-weight: 500 !important; font-weight: 500 !important;
color: var(--ln-primary) !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) { .vm-page.ct-page .ct-action-btn.ant-btn-link:hover:not(:disabled) {
color: var(--ln-primary-hover) !important; 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 { .vm-page.ct-page .ct-btn-cta.ant-btn-primary {
background: var(--ln-primary) !important; background: var(--ln-primary) !important;
border-color: var(--ln-primary) !important; border-color: var(--ln-primary) !important;
@@ -461,6 +568,7 @@
gap: 8px; gap: 8px;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
flex-wrap: wrap;
} }
.vm-page.ct-page .ct-panel-head__badge { .vm-page.ct-page .ct-panel-head__badge {
@@ -716,22 +824,101 @@
padding: 0 16px; 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 { .vm-page.ct-page .ct-section-tabs {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 8px; gap: 6px;
padding-bottom: 10px; padding: 2px 2px 4px;
border-bottom: 1px solid var(--ln-hairline);
} }
.vm-page.ct-page .ct-section-tab { .vm-page.ct-page .ct-section-tab {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
max-width: 100%; flex-shrink: 0;
max-width: 180px;
min-height: 36px; min-height: 36px;
padding: 6px 12px; padding: 6px 10px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: var(--ln-radius-control); border-radius: var(--ln-radius-control);
background: transparent; background: transparent;
@@ -743,6 +930,29 @@
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; 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 { .vm-page.ct-page .ct-section-tab:hover {
color: var(--ln-ink); color: var(--ln-ink);
background: var(--ln-surface-strong); background: var(--ln-surface-strong);
@@ -754,31 +964,52 @@
border-color: color-mix(in srgb, var(--ln-primary) 28%, var(--ln-hairline)); 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 { .vm-page.ct-page .ct-section-tab:focus-visible {
outline: none; outline: none;
box-shadow: inset 0 0 0 1px var(--vm-focus-border-color); 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) { @media (max-width: 992px) {
.vm-page.ct-page .ct-section-tab { .vm-page.ct-page .ct-section-nav {
min-height: 44px; grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
} }
.vm-page.ct-page .ct-section-tab--more .ct-section-tab__current { .vm-page.ct-page .ct-section-tabs-scroll {
max-width: 140px; 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-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; flex-shrink: 0;
} }
@@ -848,6 +1082,18 @@
flex-wrap: wrap; 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 { .vm-page.ct-page .ct-editor-page__topbar-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -965,7 +1211,7 @@
color: var(--ln-muted); color: var(--ln-muted);
} }
.vm-page.ct-page .ct-editor-category-select { .vm-page.ct-page .ct-editor-contract-type-input {
width: 220px; width: 220px;
} }
@@ -998,7 +1244,6 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
min-height: var(--vm-btn-height, 36px);
margin-bottom: 12px; margin-bottom: 12px;
padding: 0 0 10px; padding: 0 0 10px;
border-bottom: 1px solid var(--ln-hairline); border-bottom: 1px solid var(--ln-hairline);
@@ -1006,6 +1251,81 @@
box-sizing: border-box; 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 { .vm-page.ct-page .ct-editor-workspace .ct-panel-head__title {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1038,7 +1358,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; 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, .vm-page.ct-page .ct-editor-workspace .ct-editor-column-card .ct-editor-panel,
@@ -1373,11 +1693,6 @@
width: 100%; width: 100%;
} }
.ct-main-clause-tip {
margin-bottom: 12px;
border-left: 3px solid #7c3aed;
}
.ct-preview-doc .ct-vehicle-clause, .ct-preview-doc .ct-vehicle-clause,
.ct-rich-editor .ct-vehicle-clause { .ct-rich-editor .ct-vehicle-clause {
box-decoration-break: clone; box-decoration-break: clone;
@@ -1725,6 +2040,20 @@
line-height: 1.5; 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 { .vm-page.ct-page .ct-vehicle-binding-card {
margin-bottom: 16px; margin-bottom: 16px;
padding: 16px 20px; padding: 16px 20px;
@@ -1756,6 +2085,192 @@
margin-bottom: 12px; 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 { .vm-page.ct-page .ct-vehicle-binding-brand {
min-width: 120px; min-width: 120px;
} }
@@ -1897,8 +2412,8 @@
padding: 14px 16px; padding: 14px 16px;
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 10px; border-radius: 10px;
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%); background: linear-gradient(135deg, var(--ln-primary-soft) 0%, rgba(50, 160, 110, 0.06) 100%);
border: 1px solid #ddd6fe; border: 1px solid color-mix(in srgb, var(--ln-primary) 22%, var(--ln-hairline));
} }
.ct-vc-modal__hero-icon { .ct-vc-modal__hero-icon {
@@ -1909,23 +2424,23 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 10px; border-radius: 10px;
background: #7c3aed; background: var(--ln-primary);
color: #fff; color: #fff;
} }
.ct-vc-modal__hero-title { .ct-vc-modal__hero-title {
margin: 0 0 6px; margin: 0 0 6px;
font-size: 0.8125rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: #5b21b6; color: var(--ln-ink);
} }
.ct-vc-modal__hero-list { .ct-vc-modal__hero-list {
margin: 0; margin: 0;
padding-left: 1.1em; padding-left: 1.1em;
font-size: 0.8125rem; font-size: 0.875rem;
line-height: 1.55; line-height: 1.55;
color: #4c1d95; color: var(--ln-body);
} }
.ct-vc-modal__hero-list li + li { .ct-vc-modal__hero-list li + li {
@@ -2021,8 +2536,8 @@
border-radius: 999px; border-radius: 999px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
background: #ede9fe; background: var(--ln-primary-soft);
color: #6d28d9; color: var(--ln-primary-focus);
} }
.ct-vc-modal__chips { .ct-vc-modal__chips {
@@ -2032,8 +2547,8 @@
min-height: 56px; min-height: 56px;
padding: 12px; padding: 12px;
border-radius: 10px; border-radius: 10px;
border: 1px dashed #c4b5fd; border: 1px dashed color-mix(in srgb, var(--ln-primary) 30%, var(--ln-hairline));
background: #faf5ff; background: var(--ln-canvas-soft);
} }
.ct-vc-modal__chips.is-empty { .ct-vc-modal__chips.is-empty {
@@ -2043,9 +2558,8 @@
.ct-vc-modal__empty-hint { .ct-vc-modal__empty-hint {
margin: 0; margin: 0;
font-size: 0.8125rem; font-size: 0.875rem;
color: #7c3aed; color: var(--ln-muted);
opacity: 0.8;
} }
.ct-vc-modal__chip.ant-tag { .ct-vc-modal__chip.ant-tag {

View File

@@ -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('');
}

View File

@@ -5,7 +5,7 @@
"version": 2, "version": 2,
"prototypeName": "customer-management", "prototypeName": "customer-management",
"pageId": "list", "pageId": "list",
"updatedAt": 1782697342692, "updatedAt": 1782810809219,
"nodes": [ "nodes": [
{ {
"id": "cm-list-filter", "id": "cm-list-filter",
@@ -161,6 +161,198 @@
"assetMap": {}, "assetMap": {},
"directory": { "directory": {
"nodes": [ "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", "type": "folder",
"id": "cm-doc-root", "id": "cm-doc-root",
@@ -177,7 +369,7 @@
"type": "link", "type": "link",
"id": "cm-link-self", "id": "cm-link-self",
"title": "客户管理原型", "title": "客户管理原型",
"href": "/prototypes/customer-management", "href": "./index.html",
"target": "self" "target": "self"
}, },
{ {

View File

@@ -5,14 +5,20 @@
"version": 2, "version": 2,
"prototypeName": "insurance-procurement", "prototypeName": "insurance-procurement",
"pageId": "list", "pageId": "list",
"updatedAt": 1740009600000, "updatedAt": 1782810809220,
"nodes": [ "nodes": [
{ {
"id": "ipc-header", "id": "ipc-header",
"index": 1, "index": 1,
"title": "页面标题与模块边界", "title": "页面标题与模块边界",
"pageId": "list", "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": "说明保险采购模块双业务线边界。", "aiPrompt": "说明保险采购模块双业务线边界。",
"annotationText": "保单管理与比价单相互独立,不自动双向同步。", "annotationText": "保单管理与比价单相互独立,不自动双向同步。",
"hasMarkdown": true, "hasMarkdown": true,
@@ -26,7 +32,13 @@
"index": 2, "index": 2,
"title": "保单管理 · 筛选条件", "title": "保单管理 · 筛选条件",
"pageId": "list", "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": "列表筛选字段与查询逻辑。", "aiPrompt": "列表筛选字段与查询逻辑。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -40,7 +52,13 @@
"index": 3, "index": 3,
"title": "台账车辆总数", "title": "台账车辆总数",
"pageId": "list", "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 点击筛选行为。", "aiPrompt": "KPI 点击筛选行为。",
"annotationText": "纳入保险采购台账管理的车辆总数(一车一档)。点击可筛选列表。", "annotationText": "纳入保险采购台账管理的车辆总数(一车一档)。点击可筛选列表。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -54,7 +72,13 @@
"index": 4, "index": 4,
"title": "保险状态正常", "title": "保险状态正常",
"pageId": "list", "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": "与车辆管理保险状态联动口径。", "aiPrompt": "与车辆管理保险状态联动口径。",
"annotationText": "交强险、商业险均已购买且在有效期内,与车辆管理「保险状态=正常」一致。", "annotationText": "交强险、商业险均已购买且在有效期内,与车辆管理「保险状态=正常」一致。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -68,7 +92,13 @@
"index": 5, "index": 5,
"title": "险种临期预警", "title": "险种临期预警",
"pageId": "list", "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": "临期预警弹窗入口。", "aiPrompt": "临期预警弹窗入口。",
"annotationText": "任一类险种止期 ≤ 30 天。点击打开临期预警弹窗,可按险种筛选。", "annotationText": "任一类险种止期 ≤ 30 天。点击打开临期预警弹窗,可按险种筛选。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -82,7 +112,13 @@
"index": 6, "index": 6,
"title": "核心险种逾期", "title": "核心险种逾期",
"pageId": "list", "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": "交车合规禁止规则。", "aiPrompt": "交车合规禁止规则。",
"annotationText": "交强险或商业险已到期 → 车辆管理保险状态异常,禁止交车。", "annotationText": "交强险或商业险已到期 → 车辆管理保险状态异常,禁止交车。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -96,7 +132,13 @@
"index": 7, "index": 7,
"title": "核心险种待购", "title": "核心险种待购",
"pageId": "list", "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": "未购买核心险种规则。", "aiPrompt": "未购买核心险种规则。",
"annotationText": "交强险或商业险未录入/未购买 → 保险状态异常,禁止交车。", "annotationText": "交强险或商业险未录入/未购买 → 保险状态异常,禁止交车。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -110,7 +152,13 @@
"index": 8, "index": 8,
"title": "比价单管理", "title": "比价单管理",
"pageId": "list", "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": "比价单独立业务线入口。", "aiPrompt": "比价单独立业务线入口。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -124,7 +172,13 @@
"index": 9, "index": 9,
"title": "保单批量识别", "title": "保单批量识别",
"pageId": "list", "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 录入通道与确认规则。", "aiPrompt": "OCR 录入通道与确认规则。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -138,7 +192,13 @@
"index": 10, "index": 10,
"title": "识别任务记录", "title": "识别任务记录",
"pageId": "list", "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": "识别任务列表与确认进度。", "aiPrompt": "识别任务列表与确认进度。",
"annotationText": "查看 OCR/导入任务状态、识别进度与「确认识别结果」入口。", "annotationText": "查看 OCR/导入任务状态、识别进度与「确认识别结果」入口。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -152,7 +212,13 @@
"index": 11, "index": 11,
"title": "批量导入", "title": "批量导入",
"pageId": "list", "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 批量导入规则。", "aiPrompt": "Excel 批量导入规则。",
"annotationText": "仅支持新保/续保类业务行;停保/复驶/退保行须自动跳过。", "annotationText": "仅支持新保/续保类业务行;停保/复驶/退保行须自动跳过。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -166,7 +232,13 @@
"index": 12, "index": 12,
"title": "新增保单", "title": "新增保单",
"pageId": "list", "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": "手工逐条录入入口。", "aiPrompt": "手工逐条录入入口。",
"annotationText": "手工新增单条保单记录支持承保险种明细等完整字段OCR 确认页不展示部分字段)。", "annotationText": "手工新增单条保单记录支持承保险种明细等完整字段OCR 确认页不展示部分字段)。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -180,7 +252,13 @@
"index": 13, "index": 13,
"title": "保险台账列表", "title": "保险台账列表",
"pageId": "list", "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": "列表列含义与操作列。", "aiPrompt": "列表列含义与操作列。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -193,7 +271,13 @@
"id": "ipc-vehicle-mgmt", "id": "ipc-vehicle-mgmt",
"index": 14, "index": 14,
"title": "车辆保险档案", "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。", "aiPrompt": "管理弹窗结构与 Tab。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -206,7 +290,13 @@
"id": "ipc-vehicle-mgmt-tabs", "id": "ipc-vehicle-mgmt-tabs",
"index": 15, "index": 15,
"title": "险种 Tab 与全周期记录", "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": "全周期时间轴与分险种历史表。", "aiPrompt": "全周期时间轴与分险种历史表。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -219,7 +309,13 @@
"id": "ipc-compare-mgmt", "id": "ipc-compare-mgmt",
"index": 16, "index": 16,
"title": "比价单管理弹窗", "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": "比价单批次列表与付费预警。", "aiPrompt": "比价单批次列表与付费预警。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -232,7 +328,13 @@
"id": "ipc-compare-pay-alerts", "id": "ipc-compare-pay-alerts",
"index": 17, "index": 17,
"title": "最晚付费临期/超期", "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": "最晚付费日预警规则。", "aiPrompt": "最晚付费日预警规则。",
"annotationText": "最晚付费日期 ≤ 3 天视为临期;已过期为超期。用于采购前提醒,不写入保单台账。", "annotationText": "最晚付费日期 ≤ 3 天视为临期;已过期为超期。用于采购前提醒,不写入保单台账。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -245,7 +347,13 @@
"id": "ipc-compare-editor", "id": "ipc-compare-editor",
"index": 18, "index": 18,
"title": "比价单编辑", "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": "报价、最终比价、采购审批提交流程。", "aiPrompt": "报价、最终比价、采购审批提交流程。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -258,7 +366,13 @@
"id": "ipc-compare-procurement", "id": "ipc-compare-procurement",
"index": 19, "index": 19,
"title": "提交采购申请", "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": "采购状态与工作流回写。", "aiPrompt": "采购状态与工作流回写。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -271,7 +385,13 @@
"id": "ipc-policy-recogn", "id": "ipc-policy-recogn",
"index": 20, "index": 20,
"title": "保单识别/导入", "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 与导入确认页字段差异。", "aiPrompt": "OCR 与导入确认页字段差异。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -284,7 +404,13 @@
"id": "ipc-policy-add", "id": "ipc-policy-add",
"index": 21, "index": 21,
"title": "新增保单表单", "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": "手工录入字段与附件。", "aiPrompt": "手工录入字段与附件。",
"annotationText": "支持完整保单要素、承保险种明细与附件;落库后更新对应险种台账。", "annotationText": "支持完整保单要素、承保险种明细与附件;落库后更新对应险种台账。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -297,7 +423,13 @@
"id": "ipc-insurance-alert", "id": "ipc-insurance-alert",
"index": 22, "index": 22,
"title": "险种临期/逾期预警弹窗", "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": "预警列表与比价采购状态展示。", "aiPrompt": "预警列表与比价采购状态展示。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -310,7 +442,13 @@
"id": "ipc-policy-biz", "id": "ipc-policy-biz",
"index": 23, "index": 23,
"title": "停保 / 复驶 / 退保", "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": "业务变更表单与附件要求。", "aiPrompt": "业务变更表单与附件要求。",
"annotationText": "", "annotationText": "",
"hasMarkdown": true, "hasMarkdown": true,
@@ -323,7 +461,13 @@
"id": "ipc-recogn-tasks", "id": "ipc-recogn-tasks",
"index": 24, "index": 24,
"title": "识别任务列表", "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": "任务状态机与确认进度。", "aiPrompt": "任务状态机与确认进度。",
"annotationText": "任务含识别中/已完成;确认识别结果后写入台账。", "annotationText": "任务含识别中/已完成;确认识别结果后写入台账。",
"hasMarkdown": false, "hasMarkdown": false,
@@ -352,6 +496,198 @@
"assetMap": {}, "assetMap": {},
"directory": { "directory": {
"nodes": [ "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", "type": "folder",
"id": "ipc-doc-root", "id": "ipc-doc-root",
@@ -368,7 +704,7 @@
"type": "link", "type": "link",
"id": "ipc-link-vehicle", "id": "ipc-link-vehicle",
"title": "车辆管理原型", "title": "车辆管理原型",
"href": "/prototypes/vehicle-management", "href": "../vehicle-management/index.html",
"target": "self" "target": "self"
}, },
{ {

View File

@@ -5,7 +5,7 @@
"version": 2, "version": 2,
"prototypeName": "lease-business-ledger", "prototypeName": "lease-business-ledger",
"pageId": "list", "pageId": "list",
"updatedAt": 1782712021271, "updatedAt": 1782810809220,
"nodes": [ "nodes": [
{ {
"id": "lbl-filter", "id": "lbl-filter",
@@ -872,6 +872,198 @@
"assetMap": {}, "assetMap": {},
"directory": { "directory": {
"nodes": [ "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", "type": "folder",
"id": "lbl-doc-root", "id": "lbl-doc-root",
@@ -888,7 +1080,7 @@
"type": "link", "type": "link",
"id": "lbl-link-self", "id": "lbl-link-self",
"title": "租赁业务台账原型", "title": "租赁业务台账原型",
"href": "/prototypes/lease-business-ledger", "href": "./index.html",
"target": "self" "target": "self"
}, },
{ {

View File

@@ -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。 检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。

View File

@@ -28,7 +28,7 @@
| 业务部门 | 多选下拉 | 业务相关部门 | | 业务部门 | 多选下拉 | 业务相关部门 |
| 业务负责人 | 多选下拉 | 业务组人员 | | 业务负责人 | 多选下拉 | 业务组人员 |
| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 | | 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |
| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 | | 审批类型 | 多选下拉 | 全部 / 标准合同 / 非标准合同 |
| 创建人 | 多选下拉 | 业务相关人员 | | 创建人 | 多选下拉 | 业务相关人员 |
| 合同结束日期 | 日期范围 | 开始—结束,精确至日 | | 合同结束日期 | 日期范围 | 开始—结束,精确至日 |
@@ -90,17 +90,16 @@
| 列 | 说明 | | 列 | 说明 |
|---|---| |---|---|
| 审批类型 | 标准合同 / 非标准合同;位于审批状态左侧 |
| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 | | 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |
| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 | | 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |
| 签约公司 | 创建时所选甲方签约主体简称 | | 签约公司 | 创建时所选甲方签约主体简称 |
| 业务部门 / 业务负责人 | 创建时选取 | | 业务部门 / 业务负责人 | 创建时选取 |
| 合同类型 | 正式合同 / 试用合同 | | 合同类型 | 正式合同 / 试用合同 |
| 合同审批类型 | 标准合同审批 / 非标准合同审批 |
| 合同结束日期 | YYYY-MM-DD |
| 客户联系人 / 联系电话 | 来自客户档案 | | 客户联系人 / 联系电话 | 来自客户档案 |
| 被授权人 | 显示人数;点击弹出卡片查看姓名、联系方式、身份证号 |
| 创建人 / 创建时间 | 精确至分钟 | | 创建人 / 创建时间 | 精确至分钟 |
| 更新人 / 最后更新时间 | 无则显示 `-` | | 最后更新 | 更新人 / 最后更新时间无则显示 `-` |
| 备注 | 超长省略 |
**分页**:每页 10 / 20 / 50 条可选。 **分页**:每页 10 / 20 / 50 条可选。

View File

@@ -1,6 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Row, Col, message } from 'antd'; 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 { 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 { createDefaultLeaseOrderState, createDefaultPowerOfAttorneyState } from './lease-order-vars.js';
import { buildLeaseContractPreviewHtml } from './lease-contract-preview-build.js'; import { buildLeaseContractPreviewHtml } from './lease-contract-preview-build.js';
import { import {
@@ -12,15 +19,72 @@ import {
isMainContractFormComplete, isMainContractFormComplete,
isLeaseOrderFormComplete, isLeaseOrderFormComplete,
isPowerOfAttorneyFormComplete, isPowerOfAttorneyFormComplete,
isPowerOfAttorneySubmitReady,
isPowerOfAttorneyOptionalEmpty, isPowerOfAttorneyOptionalEmpty,
isSealTypeSelected,
isLeaseContractFormComplete,
} from './lease-contract-form-validation.js'; } from './lease-contract-form-validation.js';
import LeaseContractPreviewPanel from './LeaseContractPreviewPanel.jsx'; import LeaseContractPreviewPanel, { PreviewEditableHint } from './LeaseContractPreviewPanel.jsx';
import LeaseContractEditorForm, { import LeaseContractEditorForm, {
LeaseContractLeaseOrderSection, LeaseContractLeaseOrderSection,
LeaseContractPowerOfAttorneySection, LeaseContractPowerOfAttorneySection,
} from './LeaseContractEditorForm.jsx'; } 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 }) { 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 workspaceZoomState = useState(100);
var workspaceZoom = workspaceZoomState[0]; var workspaceZoom = workspaceZoomState[0];
var setWorkspaceZoom = workspaceZoomState[1]; var setWorkspaceZoom = workspaceZoomState[1];
@@ -29,6 +93,8 @@ export default function LeaseContractCreate({ onBack }) {
var mileageState = useState(Object.assign({}, DEFAULT_MILEAGE_STANDARD)); var mileageState = useState(Object.assign({}, DEFAULT_MILEAGE_STANDARD));
var feeInfoState = useState(Object.assign({}, DEFAULT_FEE_INFO)); var feeInfoState = useState(Object.assign({}, DEFAULT_FEE_INFO));
var contractCodeState = useState(''); var contractCodeState = useState('');
var businessDeptState = useState('');
var businessOwnerState = useState('');
var leaseOrderState = useState(function () { var leaseOrderState = useState(function () {
var initial = createDefaultLeaseOrderState(); var initial = createDefaultLeaseOrderState();
return { return {
@@ -54,6 +120,10 @@ export default function LeaseContractCreate({ onBack }) {
var setFeeInfo = feeInfoState[1]; var setFeeInfo = feeInfoState[1];
var contractCode = contractCodeState[0]; var contractCode = contractCodeState[0];
var setContractCode = contractCodeState[1]; 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 leaseOrder = leaseOrderState[0];
var setLeaseOrder = leaseOrderState[1]; var setLeaseOrder = leaseOrderState[1];
var powerOfAttorneyState = useState(function () { var powerOfAttorneyState = useState(function () {
@@ -66,12 +136,20 @@ export default function LeaseContractCreate({ onBack }) {
}); });
var powerOfAttorney = powerOfAttorneyState[0]; var powerOfAttorney = powerOfAttorneyState[0];
var setPowerOfAttorney = powerOfAttorneyState[1]; 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 contractApprovalTypeState = useState(STANDARD_CONTRACT_APPROVAL);
var contractApprovalType = contractApprovalTypeState[0]; var contractApprovalType = contractApprovalTypeState[0];
var setContractApprovalType = contractApprovalTypeState[1]; var setContractApprovalType = contractApprovalTypeState[1];
var previewHtml = useMemo(function () { var previewHtml = useMemo(function () {
if (!contractTemplateId) return '';
return buildLeaseContractPreviewHtml({ return buildLeaseContractPreviewHtml({
contractTemplateId: contractTemplateId,
lessorId: lessorId, lessorId: lessorId,
customerId: customerId, customerId: customerId,
mileage: mileage, mileage: mileage,
@@ -80,7 +158,7 @@ export default function LeaseContractCreate({ onBack }) {
leaseOrder: leaseOrder, leaseOrder: leaseOrder,
powerOfAttorney: powerOfAttorney, powerOfAttorney: powerOfAttorney,
}); });
}, [lessorId, customerId, mileage, feeInfo, contractCode, leaseOrder, powerOfAttorney]); }, [contractTemplateId, lessorId, customerId, mileage, feeInfo, contractCode, leaseOrder, powerOfAttorney]);
var handlePreviewContentChange = useCallback(function (mergedHtml) { var handlePreviewContentChange = useCallback(function (mergedHtml) {
setContractApprovalType(resolveContractApprovalType(previewHtml, mergedHtml)); setContractApprovalType(resolveContractApprovalType(previewHtml, mergedHtml));
@@ -91,16 +169,20 @@ export default function LeaseContractCreate({ onBack }) {
}, [previewHtml]); }, [previewHtml]);
var isNonStandardApproval = contractApprovalType === NONSTANDARD_CONTRACT_APPROVAL; var isNonStandardApproval = contractApprovalType === NONSTANDARD_CONTRACT_APPROVAL;
var isTemplateSelected = Boolean(contractTemplateId);
var previewBadge = getContractTemplatePreviewBadge(contractTemplateId);
var isMainContractComplete = useMemo(function () { var isMainContractComplete = useMemo(function () {
return isMainContractFormComplete({ return isMainContractFormComplete({
lessorId: lessorId, lessorId: lessorId,
customerId: customerId, customerId: customerId,
contractCode: contractCode, contractCode: contractCode,
businessDept: businessDept,
businessOwner: businessOwner,
mileage: mileage, mileage: mileage,
feeInfo: feeInfo, feeInfo: feeInfo,
}); });
}, [lessorId, customerId, contractCode, mileage, feeInfo]); }, [lessorId, customerId, contractCode, businessDept, businessOwner, mileage, feeInfo]);
var isLeaseOrderComplete = useMemo(function () { var isLeaseOrderComplete = useMemo(function () {
return isLeaseOrderFormComplete({ leaseOrder: leaseOrder }); return isLeaseOrderFormComplete({ leaseOrder: leaseOrder });
@@ -110,10 +192,182 @@ export default function LeaseContractCreate({ onBack }) {
return isPowerOfAttorneyFormComplete({ powerOfAttorney: powerOfAttorney }); return isPowerOfAttorneyFormComplete({ powerOfAttorney: powerOfAttorney });
}, [powerOfAttorney]); }, [powerOfAttorney]);
var isPoaSubmitReady = useMemo(function () {
return isPowerOfAttorneySubmitReady({ powerOfAttorney: powerOfAttorney });
}, [powerOfAttorney]);
var isPowerOfAttorneyEmpty = useMemo(function () { var isPowerOfAttorneyEmpty = useMemo(function () {
return isPowerOfAttorneyOptionalEmpty({ powerOfAttorney: powerOfAttorney }); return isPowerOfAttorneyOptionalEmpty({ powerOfAttorney: 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' }, return React.createElement('div', { className: 'vm-page lc-page lc-create-page ct-page' },
React.createElement('div', { className: 'lc-create-topbar' }, React.createElement('div', { className: 'lc-create-topbar' },
React.createElement('button', { React.createElement('button', {
@@ -121,15 +375,89 @@ export default function LeaseContractCreate({ onBack }) {
className: 'vm-btn vm-btn-back', className: 'vm-btn vm-btn-back',
onClick: onBack, 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-page lc-create-editor-page' },
React.createElement('div', { className: 'ct-editor-workspace' }, React.createElement('div', { className: 'ct-editor-workspace' },
React.createElement(Row, { gutter: [16, 16] }, React.createElement(Row, { gutter: [16, 16] },
React.createElement(Col, { xs: 24, lg: 12 }, React.createElement(Col, { xs: 24, lg: 12 },
React.createElement('div', { className: 'lc-create-editor-column' }, React.createElement('div', { className: 'lc-create-editor-column', ref: columnRef },
React.createElement('section', { className: 'ct-editor-column-card lc-main-contract-card', 'aria-label': '主体合同信息', 'data-annotation-id': 'lc-create-main-contract' }, 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 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', { React.createElement('span', {
className: 'lc-form-completion-badge ' + (isMainContractComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'), className: 'lc-form-completion-badge ' + (isMainContractComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'),
role: 'status', role: 'status',
@@ -141,19 +469,33 @@ export default function LeaseContractCreate({ onBack }) {
lessorId: lessorId, lessorId: lessorId,
customerId: customerId, customerId: customerId,
contractCode: contractCode, contractCode: contractCode,
businessDept: businessDept,
businessOwner: businessOwner,
mileage: mileage, mileage: mileage,
feeInfo: feeInfo, feeInfo: feeInfo,
onLessorChange: setLessorId, onLessorChange: setLessorId,
onCustomerChange: setCustomerId, onCustomerChange: setCustomerId,
onContractCodeChange: setContractCode, onContractCodeChange: setContractCode,
onBusinessAssignmentChange: function (dept, owner) {
setBusinessDept(dept);
setBusinessOwner(owner);
},
onMileageChange: setMileage, onMileageChange: setMileage,
onFeeInfoChange: setFeeInfo, 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' },
React.createElement('div', { className: 'ct-panel-head__title' }, '附件1租赁订单'), React.createElement('div', { className: 'ct-panel-head__title' }, '第 3 步附件1 · 租赁订单'),
React.createElement('span', { React.createElement('span', {
className: 'lc-form-completion-badge ' + (isLeaseOrderComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'), className: 'lc-form-completion-badge ' + (isLeaseOrderComplete ? 'lc-form-completion-badge--done' : 'lc-form-completion-badge--pending'),
role: 'status', role: 'status',
@@ -166,15 +508,45 @@ export default function LeaseContractCreate({ onBack }) {
onLeaseOrderChange: setLeaseOrder, onLeaseOrderChange: setLeaseOrder,
}), }),
), ),
renderSectionLock('leaseOrder'),
), ),
React.createElement('section', { className: 'ct-editor-column-card', 'aria-label': '授权委托书', 'data-annotation-id': 'lc-create-poa' }, React.createElement('section', {
React.createElement('div', { className: 'ct-panel-head' }, className: 'ct-editor-column-card lc-create-section'
React.createElement('div', { className: 'ct-panel-head__title' }, '授权委托书'), + (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', { React.createElement('span', {
className: 'lc-form-completion-badge ' className: 'lc-form-completion-badge '
+ (isPowerOfAttorneyEmpty + (isPowerOfAttorneyEmpty
? 'lc-form-completion-badge--optional' ? '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', role: 'status',
'aria-live': 'polite', 'aria-live': 'polite',
}, isPowerOfAttorneyEmpty ? '选填' : (isPowerOfAttorneyComplete ? '已完成' : '未完成')), }, isPowerOfAttorneyEmpty ? '选填' : (isPowerOfAttorneyComplete ? '已完成' : '未完成')),
@@ -183,8 +555,65 @@ export default function LeaseContractCreate({ onBack }) {
React.createElement(LeaseContractPowerOfAttorneySection, { React.createElement(LeaseContractPowerOfAttorneySection, {
powerOfAttorney: powerOfAttorney, powerOfAttorney: powerOfAttorney,
onPowerOfAttorneyChange: setPowerOfAttorney, 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': '实时预览', 'aria-label': '实时预览',
'data-annotation-id': 'lc-create-preview', 'data-annotation-id': 'lc-create-preview',
}, },
React.createElement('div', { className: 'ct-panel-head' }, React.createElement('div', {
React.createElement('div', { className: 'ct-panel-head__title' }, '实时预览', className: 'ct-panel-head lc-create-preview-head',
React.createElement('span', { className: 'ct-panel-head__badge' }, 'V26.6 完整合同'), '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('div', { className: 'ct-panel-head__actions' },
React.createElement('span', { 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('div', { className: 'ct-preview-panel ct-preview-panel--word' },
React.createElement(LeaseContractPreviewPanel, { isTemplateSelected
html: previewHtml, ? React.createElement(LeaseContractPreviewPanel, {
zoom: workspaceZoom, html: previewHtml,
onZoomChange: setWorkspaceZoom, zoom: workspaceZoom,
editable: true, onZoomChange: setWorkspaceZoom,
onContentChange: handlePreviewContentChange, editable: true,
}), onContentChange: handlePreviewContentChange,
})
: React.createElement('div', { className: 'lc-template-preview-empty' }, '请先在左侧选择合同模板,右侧将自动生成对应预览。'),
), ),
), ),
), ),
@@ -231,14 +666,31 @@ export default function LeaseContractCreate({ onBack }) {
type: 'button', type: 'button',
className: 'vm-btn vm-btn-secondary', className: 'vm-btn vm-btn-secondary',
onClick: function () { onClick: function () {
message.success('已保存草稿(' + contractApprovalType + ',原型)'); if (!contractTemplateId) {
message.warning('请先选择合同模板');
return;
}
message.success('已保存草稿(' + getContractTypeLabel(contractType) + ' · ' + contractApprovalType + ',原型)');
}, },
}, '保存草稿'), }, '保存草稿'),
React.createElement('button', { React.createElement('button', {
type: 'button', type: 'button',
className: 'vm-btn vm-btn-primary', className: 'vm-btn vm-btn-primary',
onClick: function () { 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 + '(原型)');
}, },
}, '提交审核'), }, '提交审核'),
), ),

View File

@@ -53,6 +53,13 @@ import {
formatKpiAmount, formatKpiAmount,
getExtraServiceNames, getExtraServiceNames,
} from './lease-order-vars.js'; } 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'; import { getMatchedPrototypeClauses } from './lease-contract-vehicle-clause.js';
function FieldHint(props) { function FieldHint(props) {
@@ -63,6 +70,20 @@ function FieldHint(props) {
}, props.children); }, 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) { function BrandModelsClauseHint(props) {
var brandModels = normalizeBrandModels({ brandModels: props.brandModels }); var brandModels = normalizeBrandModels({ brandModels: props.brandModels });
if (!brandModels.length) return null; if (!brandModels.length) return null;
@@ -286,9 +307,6 @@ function LeaseOrderServiceContentField(props) {
var query = queryState[0]; var query = queryState[0];
var setQuery = queryState[1]; var setQuery = queryState[1];
var serviceNames = getExtraServiceNames(selected); var serviceNames = getExtraServiceNames(selected);
var summary = serviceNames.length <= 2
? serviceNames.join('、')
: (serviceNames.length > 0 ? '已选 ' + serviceNames.length + ' 项' : '');
var searchText = query.trim(); var searchText = query.trim();
var isSearchMode = searchText.length > 0; var isSearchMode = searchText.length > 0;
var resolvedCategory = activeCategory || LEASE_EXTRA_SERVICE_TREE[0].category; 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, { React.createElement(Popover, {
trigger: 'click', trigger: 'click',
placement: 'bottomLeft', placement: 'bottomLeft',
@@ -469,34 +493,200 @@ function LeaseOrderServiceContentField(props) {
getPopupContainer: function () { return document.body; }, getPopupContainer: function () { return document.body; },
content: popoverContent, content: popoverContent,
}, },
React.createElement('div', { React.createElement('button', {
className: 'vm-filter-picker-control lc-lease-order-service-picker__trigger' + (open ? ' open' : ''), type: 'button',
role: 'combobox', className: 'lc-lease-order-service-picker__btn',
'aria-label': '服务内容', 'aria-label': '服务内容',
'aria-expanded': open, 'aria-expanded': open,
tabIndex: 0,
'data-annotation-id': 'lc-lease-order-service-content', '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', { React.createElement('span', { className: 'vm-operate-city-step-label' },
className: 'vm-filter-picker-input lc-lease-order-service-picker__summary' isSearchMode ? ('搜索结果(' + displayOptions.length + '') : '业务人员',
+ (summary ? '' : ' is-placeholder'), ),
}, summary || '请选择服务内容'), React.createElement('div', {
selected.length > 0 className: 'vm-operate-city-edit-chips',
? React.createElement('button', { role: 'listbox',
type: 'button', 'aria-label': '业务人员列表',
className: 'vm-filter-picker-clear', },
onClick: clearAll, displayOptions.length === 0
'aria-label': '清空服务内容', ? React.createElement('p', { className: 'vm-operate-city-empty' },
tabIndex: -1, isSearchMode ? '未找到匹配的业务人员,请换个关键词' : '该部门暂无可选业务人员',
}, React.createElement(X, { size: 14, 'aria-hidden': true })) )
: null, : displayOptions.map(function (item) {
React.createElement(ChevronDown, { size: 14, className: 'vm-filter-picker-chevron', 'aria-hidden': true }), 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' }, '辆'), 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('div', { className: 'lc-form-inline-sentence' },
React.createElement('span', { className: 'lc-form-inline-sentence__text' }, '三者责任险'), React.createElement('span', { className: 'lc-form-inline-sentence__text' }, '三者责任险'),
React.createElement(InputNumber, { React.createElement(InputNumber, {
@@ -997,9 +1190,15 @@ function LeaseOrderEditor(props) {
}), }),
React.createElement('span', { className: 'lc-form-inline-sentence__text' }, '万元、车损'), 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('div', { className: 'lc-lease-order-editor__table-wrap', role: 'region', 'aria-label': '租赁订单车辆列表' },
React.createElement('table', { className: 'lc-lease-order-editor__table' }, React.createElement('table', { className: 'lc-lease-order-editor__table' },
React.createElement('thead', null, 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', { 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', { 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' }, '保证金(元/辆)'),
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('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' }, 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 plateNos = normalizePlateNos(row);
var vehicleCount = getRowVehicleCountForPricing(row, order); var vehicleCount = getRowVehicleCountForPricing(row, order);
var subtotal = calcRentServiceSubtotal(row.rent, row.serviceFee, vehicleCount); 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 }, return React.createElement('tr', { key: row.id },
React.createElement('td', { className: 'lc-lease-order-editor__brand-cell' }, React.createElement('td', { className: 'lc-lease-order-editor__brand-cell' },
React.createElement(LeaseOrderBrandModelField, { React.createElement(LeaseOrderBrandModelField, {
@@ -1036,9 +1232,6 @@ function LeaseOrderEditor(props) {
updateRow(row.id, { brandModels: values || [] }); 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('td', null,
React.createElement(LeaseOrderPlateNoField, { React.createElement(LeaseOrderPlateNoField, {
@@ -1113,19 +1306,24 @@ function LeaseOrderEditor(props) {
}), }),
), ),
), ),
React.createElement('td', null, React.createElement('td', { className: 'lc-lease-order-editor__period-cell' },
React.createElement(DatePicker.RangePicker, { React.createElement(InputNumber, {
className: 'lc-form-datepicker vm-focus-border', className: 'lc-form-number lc-lease-order-period-input vm-focus-border',
style: { width: '100%' }, controls: false,
placeholder: ['开始日期', '结束日期'], min: 1,
value: leasePeriodValue, step: 1,
onChange: function (_dates, dateStrings) { precision: 0,
value: row.leasePeriodMonths,
onChange: function (v) {
updateRow(row.id, { updateRow(row.id, {
leasePeriodStart: dateStrings && dateStrings[0] ? dateStrings[0] : null, leasePeriodMonths: v,
leasePeriodEnd: dateStrings && dateStrings[1] ? dateStrings[1] : null, leasePeriodStart: null,
leasePeriodEnd: null,
}); });
}, },
placeholder: '月数',
'aria-label': '车辆租赁期限', 'aria-label': '车辆租赁期限',
'data-annotation-id': 'lc-lease-order-lease-period',
}), }),
), ),
React.createElement('td', { className: 'lc-lease-order-editor__actions-col' }, 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('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, { React.createElement(Cascader, {
className: 'lc-form-cascader vm-focus-border', className: 'lc-form-cascader vm-focus-border',
options: PROVINCE_CITY_CASCADER_OPTIONS, options: PROVINCE_CITY_CASCADER_OPTIONS,
@@ -1151,9 +1355,14 @@ function LeaseOrderEditor(props) {
'aria-label': '车辆交付交还地点', 'aria-label': '车辆交付交还地点',
'data-annotation-id': 'lc-lease-order-delivery-region', '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, { React.createElement(DatePicker, {
className: 'lc-form-datepicker vm-focus-border', className: 'lc-form-datepicker vm-focus-border',
style: { width: '100%' }, style: { width: '100%' },
@@ -1165,7 +1374,6 @@ function LeaseOrderEditor(props) {
'aria-label': '车辆交付时间', 'aria-label': '车辆交付时间',
'data-annotation-id': 'lc-lease-order-delivery-date', '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); var hasStartedDelegate = delegates.some(isDelegateRowStarted);
return React.createElement('div', { className: 'lc-poa-editor' }, return React.createElement('div', { className: 'lc-poa-editor' },
React.createElement(FieldHint, { annotationId: 'lc-create-poa-hint' },
'授权委托书为选填;如未填写,提交审核后须在交车前完成上传。可从合同列表「添加授权委托书」补充委托人信息并自动衔接入合同。填写姓名、联系方式、证件号码任一字段后,其余两项为必填;最多 ' + POA_MAX_DELEGATES + ' 个委托人,信息将同步至右侧授权委托书。',
),
delegates.map(function (row, index) { delegates.map(function (row, index) {
var started = isDelegateRowStarted(row); var started = isDelegateRowStarted(row);
return React.createElement('div', { 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) { 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' }, return React.createElement('label', { className: 'lc-form-field' },
React.createElement('span', { className: 'lc-form-field__label' }, labelEl,
props.required ? React.createElement('span', { className: 'lc-form-field__required', 'aria-hidden': 'true' }, '*') : null,
props.label,
),
props.children, props.children,
); );
} }
@@ -1397,11 +1628,14 @@ export default function LeaseContractEditorForm(props) {
var lessorId = props.lessorId; var lessorId = props.lessorId;
var customerId = props.customerId; var customerId = props.customerId;
var contractCode = props.contractCode || ''; var contractCode = props.contractCode || '';
var businessDept = props.businessDept || '';
var businessOwner = props.businessOwner || '';
var mileage = props.mileage || DEFAULT_MILEAGE_STANDARD; var mileage = props.mileage || DEFAULT_MILEAGE_STANDARD;
var feeInfo = props.feeInfo || DEFAULT_FEE_INFO; var feeInfo = props.feeInfo || DEFAULT_FEE_INFO;
var onLessorChange = props.onLessorChange; var onLessorChange = props.onLessorChange;
var onCustomerChange = props.onCustomerChange; var onCustomerChange = props.onCustomerChange;
var onContractCodeChange = props.onContractCodeChange; var onContractCodeChange = props.onContractCodeChange;
var onBusinessAssignmentChange = props.onBusinessAssignmentChange;
var onMileageChange = props.onMileageChange; var onMileageChange = props.onMileageChange;
var onFeeInfoChange = props.onFeeInfoChange; var onFeeInfoChange = props.onFeeInfoChange;
@@ -1457,10 +1691,28 @@ export default function LeaseContractEditorForm(props) {
'aria-label': '合同编号', '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('div', { className: 'lc-form-grid lc-form-grid--pair' },
React.createElement(FormField, { React.createElement(FormField, {
label: '甲方', label: '甲方',
required: true, required: true,
labelHint: lessorId
? React.createElement(FormFieldLabelHint, {
annotationId: 'lc-lessor-select-hint',
text: '将使用该公司用章进行电子签章',
})
: null,
}, },
React.createElement(Select, { React.createElement(Select, {
className: 'lc-form-select vm-focus-border', className: 'lc-form-select vm-focus-border',
@@ -1733,11 +1985,32 @@ export function LeaseContractLeaseOrderSection(props) {
export function LeaseContractPowerOfAttorneySection(props) { export function LeaseContractPowerOfAttorneySection(props) {
var powerOfAttorney = props.powerOfAttorney; var powerOfAttorney = props.powerOfAttorney;
var onPowerOfAttorneyChange = props.onPowerOfAttorneyChange; 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' }, return React.createElement('div', { className: 'lc-create-editor-scroll lc-create-editor-scroll--section' },
React.createElement(PowerOfAttorneyEditor, { React.createElement(PowerOfAttorneyEditor, {
poa: powerOfAttorney || createDefaultPowerOfAttorneyState(), poa: powerOfAttorney || createDefaultPowerOfAttorneyState(),
onChange: onPowerOfAttorneyChange, 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': '合同备注',
}),
),
),
); );
} }

View File

@@ -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) { function previewPagerIcon(name) {
var common = { var common = {
xmlns: 'http://www.w3.org/2000/svg', 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' }, 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-toolbar__row ct-preview-toolbar__row--primary', role: 'toolbar', 'aria-label': '预览缩放' },
React.createElement('div', { className: 'ct-preview-pager__group', role: 'group', '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' : '') }, return React.createElement('div', { className: 'ct-preview-doc ct-word-doc ct-word-paginated' + (editable ? ' lc-preview-doc--editable' : '') },
editableHint,
previewToolbar, previewToolbar,
React.createElement('div', { className: 'ct-preview-body-shell' }, React.createElement('div', { className: 'ct-preview-body-shell' },
React.createElement('div', { className: 'ct-preview-pages-wrap' }, React.createElement('div', { className: 'ct-preview-pages-wrap' },

File diff suppressed because one or more lines are too long

View File

@@ -13,6 +13,7 @@ import {
import 'antd/dist/reset.css'; import 'antd/dist/reset.css';
import '../vehicle-management/style.css'; import '../vehicle-management/style.css';
import './styles/index.css'; import './styles/index.css';
import '../contract-template-management/contract-template-catalog.js';
import LeaseContractApp from './LeaseContractManagement.jsx'; import LeaseContractApp from './LeaseContractManagement.jsx';
import annotationSourceDocument from './annotation-source.json'; import annotationSourceDocument from './annotation-source.json';

View File

@@ -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 '';
}

View File

@@ -9,6 +9,8 @@ function isMainContractFieldsComplete(form) {
if (!contractCode) return false; if (!contractCode) return false;
if (!form.lessorId) return false; if (!form.lessorId) return false;
if (!form.customerId) return false; if (!form.customerId) return false;
if (!form.businessDept) return false;
if (!form.businessOwner) return false;
var mileage = form.mileage || {}; var mileage = form.mileage || {};
if (mileage.hasRequirement) { if (mileage.hasRequirement) {
@@ -47,23 +49,38 @@ export function isLeaseOrderFormComplete(form) {
}); });
} }
/** 授权委托书卡片完成度(选填;有填写时须完整) */ /** 授权委托书卡片完成度(提交前须至少一名完整委托人,且已填行须完整) */
export function isPowerOfAttorneyFormComplete(form) { export function isPowerOfAttorneyFormComplete(form) {
var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || []; var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || [];
var started = delegates.filter(isDelegateRowStarted); var started = delegates.filter(isDelegateRowStarted);
if (!started.length) return true; if (!started.length) return false;
return started.every(isDelegateRowComplete); return started.every(isDelegateRowComplete);
} }
/** 授权委托书是否满足提交审核要求 */
export function isPowerOfAttorneySubmitReady(form) {
var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || [];
return delegates.some(isDelegateRowComplete);
}
/** 授权委托书是否有未完成行(用于徽章展示) */ /** 授权委托书是否有未完成行(用于徽章展示) */
export function isPowerOfAttorneyOptionalEmpty(form) { export function isPowerOfAttorneyOptionalEmpty(form) {
var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || []; var delegates = (form.powerOfAttorney && form.powerOfAttorney.delegates) || [];
return !delegates.some(isDelegateRowStarted); 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) { export function isLeaseContractFormComplete(form) {
return isMainContractFieldsComplete(form) return isMainContractFieldsComplete(form)
&& isLeaseOrderFormComplete(form) && isLeaseOrderFormComplete(form)
&& isPowerOfAttorneyFormComplete(form); && isPowerOfAttorneySubmitReady(form)
&& isSealTypeSelected(form);
} }

View File

@@ -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 = { export var PAYMENT_PERIOD_LABELS = {
1: '1个月', 1: '1个月',
3: '3个月', 3: '3个月',
@@ -14,12 +32,194 @@ export var HYDROGEN_PAYMENT_LABELS = {
prepay: '预付款', 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 = { export var MILEAGE_PERIOD_LABELS = {
month: '每月', month: '每月',
quarter: '每季度', quarter: '每季度',
year: '每年度', 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) { export function formatMileageSummary(record) {
if (!record.hasMinimumMileage) return '无最低里程要求'; if (!record.hasMinimumMileage) return '无最低里程要求';
var period = MILEAGE_PERIOD_LABELS[record.mileagePeriod] || ''; var period = MILEAGE_PERIOD_LABELS[record.mileagePeriod] || '';
@@ -27,11 +227,67 @@ export function formatMileageSummary(record) {
return period + km + ' 公里'; 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) { export function formatDeliveryRegion(region) {
if (!region || !region.length) return '-'; if (!region || !region.length) return '-';
return region.join(' / '); 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 = [ export var LEASE_CONTRACT_LIST_RECORDS = [
{ {
id: '1', id: '1',
@@ -49,11 +305,19 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部', businessDept: '业务1部',
businessOwner: '张经理', businessOwner: '张经理',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '标准合同审批', contractApprovalType: '标准合同',
paymentPeriod: '1个月', paymentPeriod: 1,
hydrogenPaymentMethod: '自行解决', hydrogenPaymentMethod: 'self',
deliveryRegion: ['浙江省', '嘉兴市'], deliveryRegion: ['浙江省', '嘉兴市'],
deliveryDate: '2025-03-01', deliveryDate: '2025-03-01',
deliveryDateChangeLogs: [
{
beforeDate: '2025-02-28',
afterDate: '2025-03-01',
operatorName: '张经理',
operateTime: '2025-02-10 16:20:00',
},
],
insuredVehicleCount: 2, insuredVehicleCount: 2,
thirdPartyLiabilityMillion: 200, thirdPartyLiabilityMillion: 200,
hasMinimumMileage: true, hasMinimumMileage: true,
@@ -85,9 +349,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务2部', businessDept: '业务2部',
businessOwner: '李专员', businessOwner: '李专员',
contractType: '试用合同', contractType: '试用合同',
contractApprovalType: '非标准合同审批', contractApprovalType: '非标准合同',
paymentPeriod: '3个月', paymentPeriod: 3,
hydrogenPaymentMethod: '预付款', hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['上海市', '上海市'], deliveryRegion: ['上海市', '上海市'],
deliveryDate: '2025-02-20', deliveryDate: '2025-02-20',
insuredVehicleCount: 1, insuredVehicleCount: 1,
@@ -121,9 +385,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务3部', businessDept: '业务3部',
businessOwner: '王专员', businessOwner: '王专员',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '标准合同审批', contractApprovalType: '标准合同',
paymentPeriod: '1个月', paymentPeriod: 1,
hydrogenPaymentMethod: '自行解决', hydrogenPaymentMethod: 'self',
deliveryRegion: ['浙江省', '杭州市'], deliveryRegion: ['浙江省', '杭州市'],
deliveryDate: '2025-03-10', deliveryDate: '2025-03-10',
insuredVehicleCount: 1, insuredVehicleCount: 1,
@@ -141,6 +405,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '-', updateTime: '-',
remark: '-', remark: '-',
legalStampedContractUploaded: undefined, legalStampedContractUploaded: undefined,
approvalFlowNodes: [
{ nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['王专员'] },
],
}, },
{ {
id: '4', id: '4',
@@ -158,9 +425,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部', businessDept: '业务1部',
businessOwner: '张经理', businessOwner: '张经理',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '非标准合同审批', contractApprovalType: '非标准合同',
paymentPeriod: '6个月', paymentPeriod: 6,
hydrogenPaymentMethod: '预付款', hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['浙江省', '宁波市'], deliveryRegion: ['浙江省', '宁波市'],
deliveryDate: '2025-04-01', deliveryDate: '2025-04-01',
insuredVehicleCount: 2, insuredVehicleCount: 2,
@@ -179,7 +446,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
remark: '-', remark: '-',
legalStampedContractUploaded: undefined, legalStampedContractUploaded: undefined,
approvalFlowNodes: [ 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: 'passed', operatorName: '超级用户', operatorTime: '2026-04-28 17:44:45' },
{ nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['超级用户', '金可鹏'] }, { nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['超级用户', '金可鹏'] },
], ],
@@ -199,9 +466,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务2部', businessDept: '业务2部',
businessOwner: '李专员', businessOwner: '李专员',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '标准合同审批', contractApprovalType: '标准合同',
paymentPeriod: '3个月', paymentPeriod: 3,
hydrogenPaymentMethod: '自行解决', hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '苏州市'], deliveryRegion: ['江苏省', '苏州市'],
deliveryDate: '2025-03-15', deliveryDate: '2025-03-15',
insuredVehicleCount: 1, insuredVehicleCount: 1,
@@ -220,8 +487,8 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
remark: '变更车辆数量', remark: '变更车辆数量',
legalStampedContractUploaded: undefined, legalStampedContractUploaded: undefined,
approvalFlowNodes: [ approvalFlowNodes: [
{ nodeTitle: '法务审核', result: 'passed', operatorName: '李法务', operatorTime: '2025-02-21 18: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' }, { nodeTitle: '发起审批', result: 'passed', operatorName: '李专员', operatorTime: '2025-02-22 10:00:00', comment: '因客户增购 1 台车辆发起变更审批。' },
{ nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['张经理', '赵总监'] }, { nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['张经理', '赵总监'] },
], ],
}, },
@@ -231,9 +498,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
projectName: '南京氢能示范项目', projectName: '南京氢能示范项目',
vehicleCount: 3, vehicleCount: 3,
vehicles: [ vehicles: [
{ vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏A60006', actualDelivery: '2025-01-20 08: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: '郑运维' }, { 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: '冯运维' }, { vehicleType: '牵引车头', brand: '飞驰', model: '集卡头', plateNo: '苏A60008', actualDelivery: '2025-01-22 14:00', deliveryPerson: '冯运维', leasePeriodMonths: 24 },
], ],
approvalStatus: '审批通过', approvalStatus: '审批通过',
contractStatus: '合同进行中', contractStatus: '合同进行中',
@@ -242,9 +509,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务3部', businessDept: '业务3部',
businessOwner: '王专员', businessOwner: '王专员',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '标准合同审批', contractApprovalType: '标准合同',
paymentPeriod: '12个月', paymentPeriod: 12,
hydrogenPaymentMethod: '预付款', hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['江苏省', '南京市'], deliveryRegion: ['江苏省', '南京市'],
deliveryDate: '2025-01-15', deliveryDate: '2025-01-15',
insuredVehicleCount: 3, insuredVehicleCount: 3,
@@ -262,6 +529,12 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2025-01-25 11:00', updateTime: '2025-01-25 11:00',
remark: '-', remark: '-',
legalStampedContractUploaded: true, 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', id: '7',
@@ -269,7 +542,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
projectName: '无锡试用租赁项目', projectName: '无锡试用租赁项目',
vehicleCount: 1, vehicleCount: 1,
vehicles: [ 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: '审批通过', approvalStatus: '审批通过',
contractStatus: '合同进行中', contractStatus: '合同进行中',
@@ -278,9 +551,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部', businessDept: '业务1部',
businessOwner: '张经理', businessOwner: '张经理',
contractType: '试用合同', contractType: '试用合同',
contractApprovalType: '非标准合同审批', contractApprovalType: '非标准合同',
paymentPeriod: '1个月', paymentPeriod: 1,
hydrogenPaymentMethod: '自行解决', hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '无锡市'], deliveryRegion: ['江苏省', '无锡市'],
deliveryDate: '2025-02-05', deliveryDate: '2025-02-05',
insuredVehicleCount: 1, insuredVehicleCount: 1,
@@ -298,6 +571,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '-', updateTime: '-',
remark: '试用 3 个月', remark: '试用 3 个月',
legalStampedContractUploaded: false, 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', id: '8',
@@ -315,9 +593,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务2部', businessDept: '业务2部',
businessOwner: '李专员', businessOwner: '李专员',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '标准合同审批', contractApprovalType: '标准合同',
paymentPeriod: '3个月', paymentPeriod: 3,
hydrogenPaymentMethod: '预付款', hydrogenPaymentMethod: 'prepay',
deliveryRegion: ['江苏省', '常州市'], deliveryRegion: ['江苏省', '常州市'],
deliveryDate: '2025-04-20', deliveryDate: '2025-04-20',
insuredVehicleCount: 2, insuredVehicleCount: 2,
@@ -335,6 +613,10 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2025-02-23 09:00', updateTime: '2025-02-23 09:00',
remark: '驳回原因:费用条款需调整', remark: '驳回原因:费用条款需调整',
legalStampedContractUploaded: undefined, 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', id: '9',
@@ -342,8 +624,8 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
projectName: '南通去年到期项目', projectName: '南通去年到期项目',
vehicleCount: 2, vehicleCount: 2,
vehicles: [ vehicles: [
{ vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '9.6米氢燃料电池车', plateNo: '苏F90009', actualDelivery: '2024-03-01 09: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: '卫运维' }, { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏F90010', actualDelivery: '2024-03-02 10:00', deliveryPerson: '卫运维', leasePeriodMonths: 12 },
], ],
approvalStatus: '审批通过', approvalStatus: '审批通过',
contractStatus: '到期合同', contractStatus: '到期合同',
@@ -352,9 +634,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务3部', businessDept: '业务3部',
businessOwner: '王专员', businessOwner: '王专员',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '标准合同审批', contractApprovalType: '标准合同',
paymentPeriod: '6个月', paymentPeriod: 6,
hydrogenPaymentMethod: '自行解决', hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '南通市'], deliveryRegion: ['江苏省', '南通市'],
deliveryDate: '2024-02-15', deliveryDate: '2024-02-15',
insuredVehicleCount: 2, insuredVehicleCount: 2,
@@ -372,6 +654,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2024-12-20 16:00', updateTime: '2024-12-20 16:00',
remark: '已到期可续签', remark: '已到期可续签',
legalStampedContractUploaded: true, 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', id: '10',
@@ -388,9 +675,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
businessDept: '业务1部', businessDept: '业务1部',
businessOwner: '张经理', businessOwner: '张经理',
contractType: '正式合同', contractType: '正式合同',
contractApprovalType: '非标准合同审批', contractApprovalType: '非标准合同',
paymentPeriod: '1个月', paymentPeriod: 1,
hydrogenPaymentMethod: '自行解决', hydrogenPaymentMethod: 'self',
deliveryRegion: ['江苏省', '镇江市'], deliveryRegion: ['江苏省', '镇江市'],
deliveryDate: '2024-05-10', deliveryDate: '2024-05-10',
insuredVehicleCount: 1, insuredVehicleCount: 1,
@@ -408,5 +695,322 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
updateTime: '2025-01-10 14:00', updateTime: '2025-01-10 14:00',
remark: '-', remark: '-',
legalStampedContractUploaded: true, 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);
});
}

View File

@@ -1,7 +1,9 @@
/** /**
* 租赁合同预览 HTML 拼装:占位符替换 + 2.6.1 开票信息 + 里程标准 + 客户资质附件 * 租赁合同预览 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 { import {
applyTemplateVars, applyTemplateVars,
buildLeasePreviewVars, buildLeasePreviewVars,
@@ -405,9 +407,13 @@ function stripUnresolvedPlaceholders(html) {
} }
export function buildLeaseContractPreviewHtml(form) { 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 vars = buildLeasePreviewVars(form || {});
var html = stripUnresolvedPlaceholders(applyTemplateVars(base, vars)); var html = stripUnresolvedPlaceholders(applyTemplateVars(base, vars));
var vehicleKeys = collectVehicleKeysFromLeaseOrder(form);
html = applyAttachmentSectionVehicleFilter(html, sectionBindings, vehicleKeys);
html = injectPrototypeVehicleClauses(html); html = injectPrototypeVehicleClauses(html);
var customer = getLeaseCustomerById(form && form.customerId); var customer = getLeaseCustomerById(form && form.customerId);
var lessor = getLessorCompanyById(form && form.lessorId); var lessor = getLessorCompanyById(form && form.lessorId);
@@ -420,7 +426,6 @@ export function buildLeaseContractPreviewHtml(form) {
html = applyLeaseOrderAttachmentPatch(html, form, customer, lessor); html = applyLeaseOrderAttachmentPatch(html, form, customer, lessor);
html = applyPowerOfAttorneyPatch(html, form); html = applyPowerOfAttorneyPatch(html, form);
html = appendCustomerCredentialAttachments(html, customer); html = appendCustomerCredentialAttachments(html, customer);
var vehicleKeys = collectVehicleKeysFromLeaseOrder(form);
html = applyVehicleClausePreviewFilter(html, vehicleKeys); html = applyVehicleClausePreviewFilter(html, vehicleKeys);
return html; return html;
} }

View File

@@ -2,8 +2,8 @@
* 租赁合同预览:检测风控红线条款(模板标记)是否被修改 * 租赁合同预览:检测风控红线条款(模板标记)是否被修改
*/ */
export var STANDARD_CONTRACT_APPROVAL = '标准合同审批'; export var STANDARD_CONTRACT_APPROVAL = '标准合同';
export var NONSTANDARD_CONTRACT_APPROVAL = '非标准合同审批'; export var NONSTANDARD_CONTRACT_APPROVAL = '非标准合同';
var RISK_SELECTOR = '.ct-risk-redline[data-risk-redline="1"]'; var RISK_SELECTOR = '.ct-risk-redline[data-risk-redline="1"]';

View File

@@ -112,6 +112,7 @@ export function createEmptyLeaseOrderRow() {
serviceFee: null, serviceFee: null,
deposit: null, deposit: null,
extraServices: [], extraServices: [],
leasePeriodMonths: null,
leasePeriodStart: null, leasePeriodStart: null,
leasePeriodEnd: null, leasePeriodEnd: null,
}; };
@@ -214,6 +215,9 @@ export function isInsuredVehicleCountEditable(order) {
export function formatLeasePeriod(row) { export function formatLeasePeriod(row) {
if (!row) return ''; if (!row) return '';
if (row.leasePeriodMonths != null && row.leasePeriodMonths !== '') {
return String(row.leasePeriodMonths) + ' 个月';
}
if (row.leasePeriodStart && row.leasePeriodEnd) { if (row.leasePeriodStart && row.leasePeriodEnd) {
return row.leasePeriodStart + ' ~ ' + row.leasePeriodEnd; return row.leasePeriodStart + ' ~ ' + row.leasePeriodEnd;
} }

View File

@@ -19,7 +19,7 @@ const NODE_DEFS = [
{ id: 'lc-create-main-contract', title: '主体合同信息', pageId: 'create', color: '#32a06e', aiPrompt: '签约、里程、费用三块表单。', annotationText: '完成度徽章;甲乙档案只读同步。' }, { 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-lease-order', title: '附件1租赁订单', pageId: 'create', color: '#2563eb', aiPrompt: '订单车辆与交车信息。', annotationText: '三者责任险等为完成度必填项。' },
{ id: 'lc-create-poa', title: '授权委托书', pageId: 'create', color: '#7c3aed', 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: '保存/提交时展示将走的审批类型。' }, { id: 'lc-create-footer', title: '底栏操作', pageId: 'create', color: '#64748b', aiPrompt: '取消、保存草稿、提交审核。', annotationText: '保存/提交时展示将走的审批类型。' },
]; ];

View File

@@ -24,6 +24,338 @@
background: var(--ln-primary-soft); 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 { .vm-page.lc-create-page .lc-main-contract-card__head {
align-items: center; align-items: center;
@@ -107,6 +439,21 @@
border: 1px solid var(--ln-hairline); 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 { .vm-page.lc-create-page .lc-party-profiles {
display: flex; display: flex;
flex-direction: column; 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-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; min-height: 36px;
height: auto; height: auto;
align-items: flex-start; align-items: flex-start;
@@ -324,6 +672,18 @@
padding-bottom: 6px; 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-brand-model-picker__summary-list,
.vm-page.lc-create-page .lc-lease-order-plate-picker__summary-list { .vm-page.lc-create-page .lc-lease-order-plate-picker__summary-list {
display: flex; display: flex;
@@ -342,6 +702,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 220px;
} }
.lc-lease-order-brand-model-picker__selected-list { .lc-lease-order-brand-model-picker__selected-list {
@@ -450,6 +811,67 @@
min-width: 160px; 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 { .vm-page.lc-create-page .lc-lease-order-service-picker {
width: 100%; width: 100%;
min-width: 148px; min-width: 148px;
@@ -877,14 +1299,16 @@
.vm-page.lc-create-page .lc-lease-order-editor__table-wrap { .vm-page.lc-create-page .lc-lease-order-editor__table-wrap {
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid var(--ln-hairline); border: 1px solid var(--ln-hairline);
border-radius: var(--ln-radius-control); border-radius: var(--ln-radius-control);
background: var(--ln-surface-card); background: var(--ln-surface-card);
} }
.vm-page.lc-create-page .lc-lease-order-editor__table { .vm-page.lc-create-page .lc-lease-order-editor__table {
width: 100%; width: max-content;
min-width: 1120px; min-width: 100%;
table-layout: auto;
border-collapse: collapse; border-collapse: collapse;
} }
@@ -894,6 +1318,7 @@
border: 1px solid var(--ln-hairline); border: 1px solid var(--ln-hairline);
vertical-align: middle; vertical-align: middle;
font-size: 0.8125rem; font-size: 0.8125rem;
white-space: nowrap;
} }
.vm-page.lc-create-page .lc-lease-order-editor__table th { .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 { .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 { .vm-page.lc-create-page .lc-lease-order-editor__brand-cell {
min-width: 148px; min-width: 0;
vertical-align: top; vertical-align: middle;
} }
.vm-page.lc-create-page .lc-lease-order-editor__brand-cell .lc-form-cascader { .vm-page.lc-create-page .lc-lease-order-editor__brand-cell .lc-form-cascader {
@@ -928,6 +1353,14 @@
line-height: 1.45; 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 { .vm-page.lc-create-page .lc-brand-model-clause-hint--empty {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -977,7 +1410,20 @@
} }
.vm-page.lc-create-page .lc-lease-order-editor__th-period { .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 { .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 { .vm-page.lc-create-page .lc-form-cascader.ant-cascader {
width: 100%; width: 100%;
} }
@@ -1212,7 +1669,7 @@
.vm-page.lc-create-page .lc-create-topbar { .vm-page.lc-create-page .lc-create-topbar {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 16px; gap: 16px;
margin-bottom: 16px; margin-bottom: 16px;
flex-wrap: wrap; flex-wrap: wrap;
@@ -1288,7 +1745,66 @@
height: 100%; 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; display: flex;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
@@ -1314,7 +1830,7 @@
color: var(--ln-primary-focus, #15803d); color: var(--ln-primary-focus, #15803d);
} }
.lc-preview-editable-hint__text { .lc-preview-editable-hint__text:not(.lc-form-card__hint-text) {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
line-height: 1.55; line-height: 1.55;
@@ -1519,6 +2035,135 @@
line-height: 1.4; 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 { .vm-page.lc-create-page .lc-form-field__required {
color: #dc2626; color: #dc2626;
margin-right: 4px; margin-right: 4px;

View File

@@ -5,7 +5,7 @@
"version": 2, "version": 2,
"prototypeName": "self-operated-business-ledger", "prototypeName": "self-operated-business-ledger",
"pageId": "list", "pageId": "list",
"updatedAt": 1782697685043, "updatedAt": 1782810809221,
"nodes": [ "nodes": [
{ {
"id": "sob-filter", "id": "sob-filter",
@@ -98,6 +98,198 @@
"assetMap": {}, "assetMap": {},
"directory": { "directory": {
"nodes": [ "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", "type": "folder",
"id": "sob-doc-root", "id": "sob-doc-root",
@@ -114,7 +306,7 @@
"type": "link", "type": "link",
"id": "sob-link-self", "id": "sob-link-self",
"title": "自营业务台账原型", "title": "自营业务台账原型",
"href": "/prototypes/self-operated-business-ledger", "href": "./index.html",
"target": "self" "target": "self"
}, },
{ {

View File

@@ -5,7 +5,7 @@
"version": 2, "version": 2,
"prototypeName": "vehicle-h2-fee-ledger", "prototypeName": "vehicle-h2-fee-ledger",
"pageId": "list", "pageId": "list",
"updatedAt": 1782796599832, "updatedAt": 1782810809221,
"nodes": [ "nodes": [
{ {
"id": "vh2-header", "id": "vh2-header",
@@ -252,6 +252,198 @@
"assetMap": {}, "assetMap": {},
"directory": { "directory": {
"nodes": [ "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", "type": "folder",
"id": "vh2-doc-root", "id": "vh2-doc-root",

View File

@@ -5,7 +5,7 @@
"version": 2, "version": 2,
"prototypeName": "vehicle-management", "prototypeName": "vehicle-management",
"pageId": "list", "pageId": "list",
"updatedAt": 1782486426114, "updatedAt": 1782810809222,
"nodes": [ "nodes": [
{ {
"id": "vm-doc-toolbar", "id": "vm-doc-toolbar",
@@ -668,6 +668,198 @@
"assetMap": {}, "assetMap": {},
"directory": { "directory": {
"nodes": [ "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", "type": "folder",
"id": "vm-doc-root", "id": "vm-doc-root",
@@ -684,7 +876,7 @@
"type": "link", "type": "link",
"id": "vm-link-self", "id": "vm-link-self",
"title": "车辆管理原型", "title": "车辆管理原型",
"href": "/prototypes/vehicle-management", "href": "./index.html",
"target": "self" "target": "self"
}, },
{ {

View File

@@ -2,13 +2,13 @@
| 项 | 内容 | | 项 | 内容 |
|---|---| |---|---|
| 文档版本 | v1.0 | | 文档版本 | v1.2 |
| 模块名称 | 合同模板管理 | | 模块名称 | 合同模板管理 |
| 所属系统 | ONE-OS 业务管理 | | 所属系统 | ONE-OS 业务管理 |
| 目标读者 | 前端 / 后端 / 测试 / 法务产品对接人 | | 目标读者 | 前端 / 后端 / 测试 / 法务产品对接人 |
| 交互原型 | `/prototypes/contract-template-management` | | 交互原型 | `/prototypes/contract-template-management` |
| 关联原型 | [车辆管理](/prototypes/vehicle-management)(车型主数据口径参考) | | 关联原型 | [车辆管理](/prototypes/vehicle-management)(车型主数据口径参考) |
| 文档状态 | 已对齐原型,待研发排期 | | 文档状态 | 已对齐原型v1.2:租赁预览贯通 / 冷启动 / 合同类型筛选) |
--- ---
@@ -20,8 +20,8 @@
-**Word 版式保真** 的方式维护整份合同; -**Word 版式保真** 的方式维护整份合同;
-**章节** 管理内容,并配置 **适用车型** -**章节** 管理内容,并配置 **适用车型**
- 发布 **版本**并指定 **标准模板** 供客服草拟新合同时默认使用; - 发布 **版本**通过 **启用/停用** 控制模板是否可供租赁合同选用;
- 在合同主体中配置 **条件条款**(按订单车辆命中显隐)。 - 在合同主体及租赁订单~安全责任协议各章节中配置 **条件条款**(按订单车辆命中显隐)。
### 1.2 产品目标 ### 1.2 产品目标
@@ -29,9 +29,9 @@
|---|---| |---|---|
| 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 | | 降低维护成本 | 一份 Word 文件 = 一条记录,导入后自动拆分章节 |
| 保证合规可控 | 支持锁定区、风控红线条款标记与统计 | | 保证合规可控 | 支持锁定区、风控红线条款标记与统计 |
| 支撑业务差异化 | 附件章节按车型绑定;合同主体支持条件条款 | | 支撑业务差异化 | 附件章节按车型绑定;各章节均支持条件条款 |
| 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本 | | 版本可追溯 | 草稿 / 已发布分离;已发布修改产生新版本并写入版本日志 |
| 标准模板唯一 | 同一合同分类同时仅一套标准模板生效 | | 多套模板并存 | 正式/试用、不同车型等多套已发布模板可同时存在;租赁创建时由用户选择 |
### 1.3 非目标(本期不做) ### 1.3 非目标(本期不做)
@@ -48,18 +48,19 @@
| 角色 | 诉求 | | 角色 | 诉求 |
|---|---| |---|---|
| 法务人员(主用户) | 导入/编辑 Word 模板、发布版本、设标准模板 | | 法务人员(主用户) | 导入/编辑 Word 模板、发布版本、启用/停用模板 |
| 客服人员(间接用户) | 草拟新合同时默认引用标准模板(下游模块消费) | | 客服人员(间接用户) | 草拟新合同时从已启用模板中选择(下游「租赁合同管理」消费) |
| 运营 / 风控(间接用户) | 查看风控红线条款配置情况 | | 运营 / 风控(间接用户) | 查看风控红线条款配置情况 |
### 2.2 核心用户故事 ### 2.2 核心用户故事
1. **作为法务**,我希望导入一份完整 Word 合同,系统自动拆成 9 个章节,以便分章节维护。 1. **作为法务**,我希望导入一份完整 Word 合同,系统自动拆成 9 个章节,以便分章节维护。
2. **作为法务**,我希望为「租赁订单」等附件章节绑定适用车型,以便创建合同时只纳入匹配车辆的条款。 2. **作为法务**,我希望为「租赁订单」等附件章节绑定适用车型,以便创建合同时只纳入匹配车辆的条款。
3. **作为法务**,我希望在合同主体为某段落设置「条件条款」并绑定车型,以便不同车型看到不同正文。 3. **作为法务**,我希望在合同主体及租赁订单~安全责任协议各章节为某段落设置「条件条款」并绑定车型,以便不同车型看到不同正文。
4. **作为法务**,我希望保存草稿不产生版本号,发布后才生成版本号,以便区分未上线内容。 4. **作为法务**,我希望保存草稿不产生版本号,发布后才生成版本号,以便区分未上线内容。
5. **作为法务**,我希望将某已发布版本设为「标准合同」,且系统自动禁用旧标准,以便客服始终使用唯一标准 5. **作为法务**,我希望已发布模板执行启用/停用,停用后方可编辑,并在发布变更时自动归档旧版快照到版本日志
6. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度 6. **作为客服**,我希望在创建租赁合同时第一步选择要使用的已启用合同模板
7. **作为法务**,我希望在列表展开行查看各章节的风控条款数、条件条款数,以便快速评估模板完整度。
--- ---
@@ -71,10 +72,12 @@
| 章节 | 合同内的逻辑切片,共 9 个1 个主体 + 8 个附件) | | 章节 | 合同内的逻辑切片,共 9 个1 个主体 + 8 个附件) |
| 草稿 | `status = draft`,无版本号 | | 草稿 | `status = draft`,无版本号 |
| 已发布 | `status = published`,有版本号 `vX.XX` | | 已发布 | `status = published`,有版本号 `vX.XX` |
| 标准模板 | `isDefault = true` 已发布模板;同分类唯一 | | 启用 | `enabled = true` 已发布模板,可供租赁合同创建选用 |
| 停用 | `enabled = false` 的已发布模板,不可编辑/删除,不可被租赁创建选用 |
| 版本日志 | `versionLogs[]`,记录发布变更前自动归档的快照 |
| 锁定区 | 编辑区内不可随意修改的段落HTML 标记 `data-inline-lock="1"` | | 锁定区 | 编辑区内不可随意修改的段落HTML 标记 `data-inline-lock="1"` |
| 风控红线条款 | 需重点合规关注的条款HTML 标记 `data-risk-redline="1"` | | 风控红线条款 | 需重点合规关注的条款HTML 标记 `data-risk-redline="1"` |
| 条件条款 | 仅合同主体可用按订单车辆命中显隐HTML 标记 `data-vehicle-clause="1"` | | 条件条款 | 各章节可用按订单车辆命中显隐HTML 标记 `data-vehicle-clause="1"` |
| 适用车型绑定 | 附件章节级别配置,决定该章节是否纳入某份合同 | | 适用车型绑定 | 附件章节级别配置,决定该章节是否纳入某份合同 |
| 模板占位符 | 形如 `{{contractCode}}` 的变量,创建合同时由系统替换 | | 模板占位符 | 形如 `{{contractCode}}` 的变量,创建合同时由系统替换 |
@@ -93,7 +96,7 @@
└── 编辑 / 新增页 └── 编辑 / 新增页
├── 顶栏(返回、保存、保存并发布) ├── 顶栏(返回、保存、保存并发布)
├── 章节 Tab仅本页展示 ├── 章节 Tab仅本页展示
├── 条件条款说明(合同主体 Tab 时显示 ├── 条件条款说明(各章节均可使用工具栏「条件」
├── 适用车型绑定(附件章节 Tab 时显示) ├── 适用车型绑定(附件章节 Tab 时显示)
└── 工作区 └── 工作区
├── 左侧Word 版式编辑区 ├── 左侧Word 版式编辑区
@@ -112,11 +115,13 @@
interface ContractTemplateDocument { interface ContractTemplateDocument {
id: string; // 唯一 ID id: string; // 唯一 ID
category: 'lease'; // 合同分类,当前固定商用车租赁 category: 'lease'; // 合同分类,当前固定商用车租赁
contractType: string; // 用户自定义合同类型,如「正式合同」「试用合同」
fileName: string; // Word 文件名如「现代18吨车型-数智中心版.docx」 fileName: string; // Word 文件名如「现代18吨车型-数智中心版.docx」
versionName: string; // 展示用版本名称 versionName: string; // 展示用版本名称
versionNo: string | null; // 已发布v1.00草稿null versionNo: string | null; // 已发布v1.00草稿null
status: 'draft' | 'published'; status: 'draft' | 'published';
isDefault: boolean; // 是否标准模板 enabled: boolean; // 已发布模板是否启用(草稿恒为 false
versionLogs: VersionLogEntry[]; // 版本变更日志与快照
sectionContents: Record<SectionKey, string>; // 各章节 HTML sectionContents: Record<SectionKey, string>; // 各章节 HTML
sectionVehicleBindings: Record<SectionKey, VehicleBinding[]>; // 各章节车型绑定 sectionVehicleBindings: Record<SectionKey, VehicleBinding[]>; // 各章节车型绑定
creator: string; creator: string;
@@ -126,6 +131,24 @@ interface ContractTemplateDocument {
remark?: string; // 内部备注(列表不展示) 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<SectionKey, string>;
sectionVehicleBindings: Record<SectionKey, VehicleBinding[]>;
remark?: string;
archivedAt: string;
};
}
interface VehicleBinding { interface VehicleBinding {
brand: string; // 品牌,如「现代」 brand: string; // 品牌,如「现代」
model: string; // 型号如「18吨氢燃料电池车」 model: string; // 型号如「18吨氢燃料电池车」
@@ -137,14 +160,14 @@ interface VehicleBinding {
| key | 章节名称 | 车型绑定 | 条件条款 | | key | 章节名称 | 车型绑定 | 条件条款 |
|---|---|:---:|:---:| |---|---|:---:|:---:|
| `main` | 合同主体 | 否 | 是 | | `main` | 合同主体 | 否 | 是 |
| `lease_order` | 租赁订单 | 是 | | | `lease_order` | 租赁订单 | 是 | |
| `vehicle_cost` | 车辆产生的费用 | 是 | | | `vehicle_cost` | 车辆产生的费用 | 是 | |
| `safety_notice` | 安全责任告知书 | 是 | | | `safety_notice` | 安全责任告知书 | 是 | |
| `dynamic_supervision` | 重型普通货运车辆租赁动态监管责任告知与确认书 | 是 | | | `dynamic_supervision` | 重型普通货运车辆租赁动态监管责任告知与确认书 | 是 | |
| `safety_commitment` | 安全责任承诺书 | 是 | | | `safety_commitment` | 安全责任承诺书 | 是 | |
| `vehicle_spec` | 车辆使用规范和驾驶员要求 | 是 | | | `vehicle_spec` | 车辆使用规范和驾驶员要求 | 是 | |
| `authorization` | 授权委托书 | 是 | | | `authorization` | 授权委托书 | 是 | |
| `safety_agreement` | 安全责任协议 | 是 | | | `safety_agreement` | 安全责任协议 | 是 | |
### 5.3 章节内容 HTML 标记规范 ### 5.3 章节内容 HTML 标记规范
@@ -170,41 +193,48 @@ interface VehicleBinding {
### 6.2 保存与发布 ### 6.2 保存与发布
| 操作 | 新增模式 | 编辑草稿 | 编辑已发布 | | 操作 | 新增模式 | 编辑草稿 | 编辑已发布(须先停用) |
|---|---|---|---| |---|---|---|---|
| **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**降为草稿**(清空版本号) | | **保存** | 新建草稿记录 | 更新原记录,保持草稿 | 更新原记录,**保持已发布与版本号**,仍为停用态 |
| **保存并发布** | 新建已发布记录 + 版本号 | 原记录变已发布 + 版本号 | **新建一条已发布记录**(新版本),原版本保留 | | **保存并发布** | 新建已发布记录 + 版本号,默认启用 | 原记录变已发布 + 版本号,默认启用 | **同记录升级版本号**;旧内容写入 `versionLogs` 快照;发布后默认停用,需手动启用 |
**校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。 **校验**:保存/发布前,合同主体(`sectionContents.main`)不能为空。
### 6.3 标准模板规则 **离开编辑**:有未保存修改时返回列表需二次确认。
1. 同一 `category` **同时仅允许一条** `isDefault = true``status = published` 的记录。 ### 6.3 启用 / 停用规则
2.**已发布且非当前标准** 的记录可执行「设为标准模板」。
3. 切换标准时:**先禁用**原标准(`isDefault → false`),再启用新标准(`isDefault → true`)。
4. **标准模板不可删除**;操作列不展示「设为标准模板」「删除」。
5. 下游「客服草拟新合同」默认引用当前标准模板(本期由其他模块消费)。
### 6.4 删除规则 1.**已发布** 模板具有启用态;草稿显示「—」。
2. **启用中**:不可编辑、不可删除;可供租赁合同创建选用。
3. **停用**:需二次确认(提示可能导致标准合同无法正常使用);停用后可编辑、可删除;租赁创建不可选用。
4. **启用**:直接生效,无需二次确认。
5. 编辑已发布模板并发布后,模板自动变为 **停用**,需重新启用后下游才可选。
-**非标准模板** 可删除。 ### 6.4 版本日志规则
- 删除前二次确认;删除后不可恢复。
### 6.5 列表排序 1. 对已发布模板执行「保存并发布」前,系统将 **当前内容自动归档** 为快照写入 `versionLogs`
2. 列表操作列提供「版本日志」,可查看历史版本号、操作人、时间与快照正文(只读)。
3. 列表主表 **一行对应一份 Word 文件**,历史版本通过日志追溯,不另增列表行。
1. 标准模板置顶; ### 6.5 删除规则
2. 其余按 `updateTime` 降序。
### 6.6 筛选匹配规则 - **启用中** 的已发布模板不可删除。
- 其余记录可删除;删除前二次确认;删除后不可恢复。
### 6.6 列表排序
`updateTime` 降序。
### 6.7 筛选匹配规则
| 字段 | 匹配逻辑 | | 字段 | 匹配逻辑 |
|---|---| |---|---|
| Word 文件名称 | 模糊匹配 `fileName``versionName``versionNo``creator``updater` | | 合同类型 | 下拉选择(支持输入模糊搜索候选项);精确匹配 `contractType` |
| 状态 | 精确匹配 `draft` / `published` | | 状态 | 精确匹配 `draft` / `published` |
| 创建人 | 精确匹配 `creator` | | 创建人 | 精确匹配 `creator` |
| 适用品牌 | 文档内 **任一附件章节** 的车型绑定命中该品牌;无绑定的文档在筛品牌时视为「全部车型」可通过 | | 启用状态 | `enabled`:仅已发布且启用;`disabled`:仅已发布且停用 |
> **已移除**:筛选项「适用型」、主表列「备注」(产品确认不再展示) > **已移除**:筛选项「适用品牌/适用车型」、主表列「备注」、「设为标准模板」能力
--- ---
@@ -218,10 +248,10 @@ interface VehicleBinding {
| 字段 | 控件 | 说明 | | 字段 | 控件 | 说明 |
|---|---|---| |---|---|---|
| Word 文件名称 | 文本输入 | 支持回车触发查询 | | 合同类型 | 可搜索下拉 | 选项来自现有模板 `contractType` 去重;支持输入模糊搜索 |
| 状态 | 下拉 | 全部 / 已发布 / 草稿 | | 状态 | 下拉 | 全部 / 已发布 / 草稿 |
| 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 | | 创建人 | 下拉 | 选项来自现有模板 `creator` 去重 |
| 适用品牌 | 下拉 | 选项来自车型主数据品牌列表 | | 启用状态 | 下拉 | 全部 / 启用 / 停用(仅作用于已发布记录) |
**按钮** **按钮**
@@ -239,9 +269,10 @@ interface VehicleBinding {
| 列名 | 字段 | 展示规则 | | 列名 | 字段 | 展示规则 |
|---|---|---| |---|---|---|
| (展开列) | — | 宽 48px | | (展开列) | — | 宽 48px |
| Word 文件名称 | `fileName` | 超长省略;标准模板显示金色标签「标准合同」 | | 合同类型 | `contractType` | 展示用户自定义的合同类型文案 |
| 版本号 | `versionNo` | 草稿显示「—」 | | 版本号 | `versionNo` | 草稿显示「—」 |
| 状态 | `status` | 已发布 / 草稿 Tag | | 状态 | `status` | 已发布 / 草稿 Tag |
| 启用状态 | `enabled` | 已发布:启用/停用;草稿:「—」 |
| 创建时间 | `createTime` | — | | 创建时间 | `createTime` | — |
| 创建人 | `creator` | 展示短名(如 `法务-王静``王静` | | 创建人 | `creator` | 展示短名(如 `法务-王静``王静` |
| 最后更新时间 | `updateTime` | — | | 最后更新时间 | `updateTime` | — |
@@ -254,14 +285,10 @@ interface VehicleBinding {
| 操作 | 显示条件 | 行为 | | 操作 | 显示条件 | 行为 |
|---|---|---| |---|---|---|
| 编辑 | 始终 | 进入编辑页,加载整份 `sectionContents` | | 启用 / 停用 | 已发布 | 停用需确认;启用直接生效 |
| 设为标准模板 | 已发布 && 非标准 | 弹窗确认后切换标准 | | 版本日志 | 已发布 | 查看历史快照 |
| 删除 | 非标准 | 弹窗确认后删除 | | 编辑 | 草稿,或已发布且停用 | 启用中按钮禁用 |
| 删除 | 草稿,或已发布且停用 | 启用中按钮禁用;删除前确认 |
**设为标准模板弹窗文案要点**
- 说明:同一分类同时只能有一套标准模板;客服草拟新合同时将默认使用新标准。
- 若存在旧标准:明示将先禁用「{旧标准文件名}」,再设置「{新标准文件名}」。
### 7.6 子表(展开行) ### 7.6 子表(展开行)
@@ -272,8 +299,7 @@ interface VehicleBinding {
| 章节 | 章节名称 | | 章节 | 章节名称 |
| 适用车型 | 附件章节展示绑定列表,格式「品牌 · 型号」;多个用「;」连接;未绑定显示「全部车型」;合同主体显示「—」 | | 适用车型 | 附件章节展示绑定列表,格式「品牌 · 型号」;多个用「;」连接;未绑定显示「全部车型」;合同主体显示「—」 |
| 风控条款数 | 该章节 HTML 内风控红线标记数量 | | 风控条款数 | 该章节 HTML 内风控红线标记数量 |
| 条件条款数 | 仅合同主体统计条件条款数;其他章节显示 `0` | | 条件条款数 | 按各章节 HTML 统计条件条款标记数量 |
| 内容状态 | 有内容:「已配置」;无内容:「未配置」 |
--- ---
@@ -293,7 +319,7 @@ interface VehicleBinding {
### 8.3 章节 Tab ### 8.3 章节 Tab
- 共 9 个章节;**前 4 个直显**,其余收入「更多」下拉 - 共 9 个章节;**横向滚动 Tab 条**展示全部章节,右侧提供「章节列表」按钮打开可搜索的章节面板(便于长标题章节快速定位)
- 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。 - 切换 Tab 后,编辑区与预览区 **同步切换** 至对应章节内容。
- **合同主体 Tab**:编辑/预览 **整份 Word 合并全文** - **合同主体 Tab**:编辑/预览 **整份 Word 合并全文**
- **其他 Tab**:仅展示该章节切片内容。 - **其他 Tab**:仅展示该章节切片内容。
@@ -310,11 +336,11 @@ interface VehicleBinding {
8. 授权委托书 8. 授权委托书
9. 安全责任协议 9. 安全责任协议
### 8.4 条件条款说明(合同主体 Tab ### 8.4 条件条款说明(合同主体及租赁订单~安全责任协议
展示说明卡片,文案要点: 展示说明卡片,文案要点:
> 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。 > 在编辑区选中段落后,点击工具栏「条件」并指定适用车型。适用于合同主体及全部附件章节。创建合同时,订单车辆任意命中即纳入正文;右侧预览可选择模拟车型验证显隐。
### 8.5 适用车型绑定(仅附件章节 Tab ### 8.5 适用车型绑定(仅附件章节 Tab
@@ -349,9 +375,11 @@ interface VehicleBinding {
- **锁定**:选中内容设为锁定区 - **锁定**:选中内容设为锁定区
- **解锁**:解除锁定 - **解锁**:解除锁定
- **风控**:标记风控红线条款 - **风控**:标记风控红线条款
- **条件**(仅合同主体):选中段落绑定车型条件 - **条件**在各章节选中段落绑定车型条件
#### 8.6.3 条件条款交互(合同主体) #### 8.6.3 条件条款交互
**适用范围**:合同主体、租赁订单、车辆产生的费用、安全责任告知书、重型普通货运车辆租赁动态监管责任告知与确认书、安全责任承诺书、车辆使用规范和驾驶员要求、授权委托书、安全责任协议。
1. 用户选中段落或文字; 1. 用户选中段落或文字;
2. 点击工具栏「条件」; 2. 点击工具栏「条件」;
@@ -374,11 +402,11 @@ interface VehicleBinding {
`锁定 N 处 · 风控红线 M 处 · 条件条款 K 处` `锁定 N 处 · 风控红线 M 处 · 条件条款 K 处`
合同主体且已选模拟车型时追加:`(预览命中 X` 当前章节支持条件且已选模拟车型时追加:`(预览命中 X`
#### 8.7.3 模拟车型(仅合同主体) #### 8.7.3 模拟车型
- 多选下拉,选项来自车型主数据(品牌 · 型号)。 - 在合同主体及全部附件章节可用;多选下拉,选项来自车型主数据(品牌 · 型号)。
- 未选车型时:**隐藏**所有条件条款段落(与创建合同时无车辆命中行为一致)。 - 未选车型时:**隐藏**所有条件条款段落(与创建合同时无车辆命中行为一致)。
- 选中后按命中规则过滤并展开条件条款正文。 - 选中后按命中规则过滤并展开条件条款正文。
@@ -493,7 +521,7 @@ interface VehicleBinding {
```mermaid ```mermaid
flowchart LR flowchart LR
A[合同模板管理] -->|标准模板 + 章节内容| B[租赁合同创建] A[合同模板管理] -->|已启用模板 + 章节内容| B[租赁合同创建]
A -->|车型绑定口径| C[车辆管理] A -->|车型绑定口径| C[车辆管理]
A -->|占位符变量定义| B A -->|占位符变量定义| B
B -->|订单车辆列表| A B -->|订单车辆列表| A
@@ -501,7 +529,7 @@ flowchart LR
| 模块 | 关系 | | 模块 | 关系 |
|---|---| |---|---|
| 租赁合同创建 | 读取标准模板或指定版本;按订单车辆过滤条件条款与附件章节 | | 租赁合同创建 | 创建流程第一步选择 **已启用** 的已发布模板;按订单车辆过滤条件条款与附件章节 |
| 车辆管理 | 车型品牌/型号枚举口径一致 | | 车辆管理 | 车型品牌/型号枚举口径一致 |
| 非标准合同审批 | 合同正文出现 `data-nonstd-trigger` 时触发(标记规范已预留) | | 非标准合同审批 | 合同正文出现 `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 列表查询 ### 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} GET /api/contract-templates/{id}
``` ```
返回完整 `sectionContents``sectionVehicleBindings` 返回完整 `sectionContents``sectionVehicleBindings``versionLogs`
### 14.3 创建 / 更新 ### 14.3 创建 / 更新
@@ -547,13 +575,14 @@ PUT /api/contract-templates/{id}
请求体含章节 HTML 与绑定;`publish: boolean` 控制是否发布。 请求体含章节 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 删除 ### 14.5 删除
@@ -561,7 +590,7 @@ POST /api/contract-templates/{id}/set-default
DELETE /api/contract-templates/{id} DELETE /api/contract-templates/{id}
``` ```
拒绝删除 `isDefault=true` 的记录。 拒绝删除 `enabled=true` 的记录。
### 14.6 Word 导入 ### 14.6 Word 导入
@@ -590,31 +619,35 @@ Content-Type: multipart/form-data
### 16.1 列表页 ### 16.1 列表页
- [ ] 筛选项包含:Word 文件名称、状态、创建人、适用品牌;**不含**适用型号、备注列 - [ ] 筛选项包含:合同类型(可搜索下拉)、状态、创建人、启用状态
- [ ] 主表含合同类型、启用状态列;操作含启用/停用、版本日志、编辑、删除
- [ ] 启用中模板编辑/删除按钮禁用;停用需二次确认
- [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表 - [ ] 主表每行对应一份 Word 文件,可展开 9 个章节子表
- [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数、内容状态 - [ ] 子表展示:章节名、适用车型、风控条款数、条件条款数
- [ ] 标准模板金色标签置顶,且不可删除
- [ ] 切换标准模板时旧标准自动禁用
### 16.2 编辑页 ### 16.2 编辑页
- [ ] 章节 Tab 仅在编辑页展示;前 4 个直显 + 更多 - [ ] 章节 Tab 仅在编辑页展示;横向滚动 + 章节列表面板
- [ ] 导入 Word 后 9 章节正确拆分 - [ ] 导入 Word 后 9 章节正确拆分
- [ ] 附件章节可绑定车型;合同主体可设条件条款 - [ ] 附件章节可绑定车型;各章节均可设条件条款
- [ ] 预览支持连续/单页切换;合同主体可模拟车型验证条件条款 - [ ] 预览支持连续/单页切换;各章节可模拟车型验证条件条款
- [ ] 保存草稿无版本号;发布生成版本号 - [ ] 保存草稿无版本号;发布生成版本号
- [ ] 编辑已发布并发布时产生新版本,旧版本保留 - [ ] 编辑已发布(停用后)并发布时版本号递增,旧版写入版本日志
- [ ] 编辑已发布(停用后)点「保存」不改变发布态与版本号
- [ ] 有未保存修改时返回列表需确认
### 16.3 数据一致性 ### 16.3 跨模块与数据一致性
- [ ] 模板管理停用后,租赁合同创建页不可再选用该模板
- [ ] 租赁创建预览读取 store 中模板 `sectionContents`(与模板管理编辑一致)
- [ ] 列表子表「风控条款数」与编辑区统计一致 - [ ] 列表子表「风控条款数」与编辑区统计一致
- [ ] 列表子表「条件条款数」仅合同主体可大于 0 - [ ] 列表子表「条件条款数」与各章节实际标记数一致
- [ ] 筛选「适用品牌」与子表车型绑定逻辑一致
### 16.4 标注与文档(原型) ### 16.4 标注与文档(原型)
- [ ] 原型已接入 `@axhub/annotation`,关键区域有 `data-annotation-id` - [ ] 原型已接入 `@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/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/index.tsx` |
| 标注数据 | `src/prototypes/contract-template-management/annotation-source.json` | | 标注数据 | `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/contract-template-vars.js` |
| 样式 | `src/prototypes/contract-template-management/styles/` | | 样式 | `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冷启动种子列表按合同类型筛选移除 isDefaultlocalStorage 持久化PRD 与实现对齐 |
| v1.1 | 2026-06-29 | 产品 | 移除标准模板模型,改为启用/停用 + 版本日志;列表筛选与操作同步;各章节支持条件条款 |
| v1.0 | 2026-06-25 | 产品 | 首版Word 文档级模型、嵌套列表、章节拆分与条件条款 |