diff --git a/.axhub/make/sidebar-tree.json b/.axhub/make/sidebar-tree.json index 9094c09..22aca21 100644 --- a/.axhub/make/sidebar-tree.json +++ b/.axhub/make/sidebar-tree.json @@ -1,42 +1,70 @@ { "version": 1, - "updatedAt": "2026-06-30T10:18:56.724Z", + "updatedAt": "2026-07-01T02:59:09.593Z", "prototypes": [ { - "id": "item:prototypes:insurance-procurement", - "kind": "item", - "title": "保险采购", - "itemKey": "prototypes/insurance-procurement" - }, - { - "id": "item:prototypes:oneos-weekly-report", - "kind": "item", - "title": "数智部周报", - "itemKey": "prototypes/oneos-weekly-report" - }, - { - "id": "item:prototypes:vehicle-management", - "kind": "item", - "title": "车辆管理", - "itemKey": "prototypes/vehicle-management" - }, - { - "id": "item:prototypes:contract-template-management", - "kind": "item", - "title": "合同模板管理", - "itemKey": "prototypes/contract-template-management" - }, - { - "id": "item:prototypes:lease-contract-management", - "kind": "item", - "title": "租赁合同管理", - "itemKey": "prototypes/lease-contract-management" - }, - { - "id": "item:prototypes:customer-management", - "kind": "item", - "title": "客户管理", - "itemKey": "prototypes/customer-management" + "id": "folder-1782874576229-r8atbu", + "kind": "folder", + "title": "OneOS", + "children": [ + { + "id": "folder-1782874599304-roj8fd", + "kind": "folder", + "title": "运维管理", + "children": [ + { + "id": "item:prototypes:vehicle-management", + "kind": "item", + "title": "车辆资产", + "itemKey": "prototypes/vehicle-management" + }, + { + "id": "item:prototypes:vehicle-management-clay", + "kind": "item", + "title": "车辆资产(Clay)", + "itemKey": "prototypes/vehicle-management-clay" + } + ] + }, + { + "id": "folder-1782874624657-4c8hzv", + "kind": "folder", + "title": "业务管理", + "children": [ + { + "id": "item:prototypes:customer-management", + "kind": "item", + "title": "客户管理", + "itemKey": "prototypes/customer-management" + }, + { + "id": "item:prototypes:lease-contract-management", + "kind": "item", + "title": "租赁合同", + "itemKey": "prototypes/lease-contract-management" + }, + { + "id": "item:prototypes:insurance-procurement", + "kind": "item", + "title": "保险采购", + "itemKey": "prototypes/insurance-procurement" + } + ] + }, + { + "id": "folder-1782874683252-db5b3y", + "kind": "folder", + "title": "合同配置", + "children": [ + { + "id": "item:prototypes:contract-template-management", + "kind": "item", + "title": "合同模板管理", + "itemKey": "prototypes/contract-template-management" + } + ] + } + ] }, { "id": "item:prototypes:lease-business-ledger", @@ -56,19 +84,6 @@ "title": "自营业务台账", "itemKey": "prototypes/self-operated-business-ledger" }, - { - "id": "folder-prototypes-xll-miniapp", - "kind": "folder", - "title": "小羚羚小程序", - "children": [ - { - "id": "item:prototypes:oneos-web-safety-training", - "kind": "item", - "title": "安全培训扫码", - "itemKey": "prototypes/oneos-web-safety-training" - } - ] - }, { "id": "folder-prototypes-oneos-web", "kind": "folder", @@ -945,4 +960,4 @@ "templates": [], "components": [], "canvas": [] -} +} \ No newline at end of file diff --git a/scripts/sync-oneos-web-prototypes.mjs b/scripts/sync-oneos-web-prototypes.mjs index 7594c08..175c03f 100644 --- a/scripts/sync-oneos-web-prototypes.mjs +++ b/scripts/sync-oneos-web-prototypes.mjs @@ -17,7 +17,7 @@ const sidebarPath = path.join(projectRoot, '.axhub/make/sidebar-tree.json'); const PAGE_ID_RE = /^[a-z0-9-]+$/u; const XLL_MODULES = [ - { slug: 'oneos-web-safety-training', title: '安全培训扫码', files: ['安全培训扫码.jsx'] }, + { slug: 'oneos-web-safety-training', title: '业务-司机安全培训', skipCodegen: true }, ]; const MODULES = [ @@ -239,6 +239,7 @@ function updateSidebar(webModules, xllModules) { (item) => item.id !== 'folder-prototypes-oneos-web' && item.id !== 'folder-prototypes-xll-miniapp' + && item.itemKey !== 'prototypes/contract-template-management' && !String(item.id).startsWith(prefix) && !String(item.itemKey).startsWith('prototypes/oneos-web-'), ); @@ -272,6 +273,10 @@ function main() { const allModules = [...XLL_MODULES, ...MODULES]; for (const module of allModules) { + if (module.skipCodegen) { + console.log(`[skip codegen] ${module.slug}(入口由项目内手写维护)`); + continue; + } if (module.docsOnly) { writeRequirementsPrototype(module); console.log(`[ok] ${module.slug} (需求说明文档浏览)`); diff --git a/src/prototypes/contract-template-management/annotation-source.json b/src/prototypes/contract-template-management/annotation-source.json index 3be4d06..1acbeca 100644 --- a/src/prototypes/contract-template-management/annotation-source.json +++ b/src/prototypes/contract-template-management/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "contract-template-management", "pageId": "list", - "updatedAt": 1782832088411, + "updatedAt": 1782871095265, "nodes": [ { "id": "ct-list-filter", @@ -332,6 +332,13 @@ "title": "ONE-OS 原型导航", "defaultExpanded": true, "children": [ + { + "type": "link", + "id": "nav-link-contract-template-management", + "title": "合同模板管理(当前)", + "href": "/prototypes/contract-template-management", + "target": "self" + }, { "type": "link", "id": "nav-link-insurance-procurement", @@ -339,13 +346,6 @@ "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", @@ -353,13 +353,6 @@ "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", @@ -395,21 +388,6 @@ "href": "/prototypes/self-operated-business-ledger", "target": "self" }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, { "type": "folder", "id": "nav-folder-prototypes-oneos-web", diff --git a/src/prototypes/contract-template-management/index.tsx b/src/prototypes/contract-template-management/index.tsx index 4afcc30..6d765d5 100644 --- a/src/prototypes/contract-template-management/index.tsx +++ b/src/prototypes/contract-template-management/index.tsx @@ -69,13 +69,15 @@ const vmTheme = { }, }; -export default function ContractTemplateManagementPage() { +export function ContractTemplateManagementContent({ embedded = false }: { embedded?: boolean }) { const [annotationPageId, setAnnotationPageId] = useState('list'); const navigationRef = useRef({}); useEffect(() => { - clearHostPrototypeRouteInfo(); - }, []); + if (!embedded) { + clearHostPrototypeRouteInfo(); + } + }, [embedded]); const handleDirectoryRoute = useCallback((node: AnnotationDirectoryRouteNode) => { const payload = node.payload as { pageId?: string } | undefined; @@ -114,3 +116,7 @@ export default function ContractTemplateManagementPage() { ); } + +export default function ContractTemplateManagementPage() { + return ; +} diff --git a/src/prototypes/customer-management/annotation-source.json b/src/prototypes/customer-management/annotation-source.json index 4a36dfa..cfc8ea8 100644 --- a/src/prototypes/customer-management/annotation-source.json +++ b/src/prototypes/customer-management/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "customer-management", "pageId": "list", - "updatedAt": 1782810809219, + "updatedAt": 1782871095268, "nodes": [ { "id": "cm-list-filter", @@ -167,6 +167,13 @@ "title": "ONE-OS 原型导航", "defaultExpanded": true, "children": [ + { + "type": "link", + "id": "nav-link-contract-template-management", + "title": "合同模板管理", + "href": "/prototypes/contract-template-management", + "target": "self" + }, { "type": "link", "id": "nav-link-insurance-procurement", @@ -174,13 +181,6 @@ "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", @@ -188,13 +188,6 @@ "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", @@ -230,21 +223,6 @@ "href": "/prototypes/self-operated-business-ledger", "target": "self" }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, { "type": "folder", "id": "nav-folder-prototypes-oneos-web", diff --git a/src/prototypes/insurance-procurement/InsuranceProcurement.jsx b/src/prototypes/insurance-procurement/InsuranceProcurement.jsx index 2fe757e..dcf4ec0 100644 --- a/src/prototypes/insurance-procurement/InsuranceProcurement.jsx +++ b/src/prototypes/insurance-procurement/InsuranceProcurement.jsx @@ -2903,76 +2903,7 @@ const ICONS = { }; const PAGE_STYLE = ` -.lc-edit-page { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; } .lc-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } -.lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; } -.lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; } -.lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; } -.lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; } -.lc-filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 24px; } -@media (max-width: 1100px) { .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } -@media (max-width: 720px) { .lc-filter-grid { grid-template-columns: 1fr; } } -.lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; } -.lc-filter-field-label { flex: 0 0 72px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 1.4; white-space: nowrap; } -.lc-filter-field-control { flex: 1; min-width: 0; } -.lc-filter-field-control .ant-input, .lc-filter-field-control .ant-select { width: 100%; } -.lc-multi-plate-pop { width: 320px; padding: 4px 2px; } -.lc-multi-plate-pop-hint { font-size: 12px; color: #64748b; margin-bottom: 8px; line-height: 1.5; } -.lc-multi-plate-pop-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; } -.lc-multi-plate-trigger { cursor: pointer; } -.lc-multi-plate-trigger .ant-input { cursor: pointer; } -.lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; } -.lc-alert-stats-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } -@media (max-width: 1200px) { .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } } -@media (max-width: 768px) { .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } } -.lc-alert-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 30px 14px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; } -.lc-alert-card-main { flex: 1; min-width: 0; } -.lc-alert-card-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; } -.lc-alert-card-val { font-size: 26px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; } -.lc-alert-card-title { font-size: 13px; font-weight: 600; color: #334155; margin-top: 2px; } -.lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; } -.lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; } -.lc-alert-card-tip:hover { color: #64748b; border-color: #cbd5e1; background: #fff; } -.lc-alert-card--total { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); } -.lc-alert-card--total .lc-alert-card-icon { background: #e2e8f0; color: #475569; } -.lc-alert-card--normal { background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%); border-color: #bbf7d0; } -.lc-alert-card--normal .lc-alert-card-icon { background: #d1fae5; color: #059669; } -.lc-alert-card--normal .lc-alert-card-val { color: #047857; } -.lc-alert-card--warning { background: linear-gradient(135deg, #fff7ed 0%, #fff 55%); border-color: #fed7aa; } -.lc-alert-card--warning .lc-alert-card-icon { background: #ffedd5; color: #ea580c; } -.lc-alert-card--warning .lc-alert-card-val { color: #c2410c; } -.lc-alert-card--expired { background: linear-gradient(135deg, #fef2f2 0%, #fff 55%); border-color: #fecaca; } -.lc-alert-card--expired .lc-alert-card-icon { background: #fee2e2; color: #dc2626; } -.lc-alert-card--expired .lc-alert-card-val { color: #b91c1c; } -.lc-alert-card--unuploaded { background: linear-gradient(135deg, #f8fafc 0%, #fff 55%); } -.lc-alert-card--unuploaded .lc-alert-card-icon { background: #f1f5f9; color: #64748b; } -.lc-alert-card--unuploaded .lc-alert-card-val { color: #64748b; } -.lc-alert-card-clickable { cursor: pointer; transition: box-shadow .2s ease, border-color .2s ease; } -.lc-alert-card-clickable:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); } -.lc-alert-card-active { box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; border-color: #165dff !important; } -.lc-table-section { margin-bottom: 0; } -.lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; } -.lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; } -.lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; vertical-align: middle; } -.lc-table-card .ant-table-thead > tr > th.lc-th-wrap { padding: 10px 8px !important; text-align: center; vertical-align: middle; white-space: nowrap; } -.lc-table-th-multiline { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; line-height: 1.3; white-space: normal; } -.lc-table-th-line { display: block; font-size: 12px; font-weight: 700; color: #475569; } -.lc-list-expire-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; } -.lc-list-expire-date { font-size: 12px; font-weight: 600; line-height: 1.35; } -.lc-list-expire-meta { line-height: 1.2; } -.lc-list-status-badge-wrap { display: inline-flex; max-width: 100%; } -.lc-list-status-badge-wrap .ant-badge { display: inline-flex; align-items: center; max-width: 100%; } -.lc-list-status-badge-text { font-size: 11px; font-weight: 600; white-space: nowrap; } -.lc-list-table .ant-table-wrapper, .lc-list-table .ant-table { width: 100% !important; } -.lc-list-table .ant-table-content table { table-layout: fixed; width: 100% !important; } -.lc-cell-ellipsis { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 10px 8px !important; border-bottom: 1px solid #f1f5f9 !important; } -.lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; } -.lc-table-card .ant-table-tbody > tr.lc-row-retired:not(.ant-table-measure-row) > td { background: #f8fafc !important; color: #94a3b8; } -.lc-list-plate-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; } -.lc-list-plate-sub { display: block; font-size: 11px; font-weight: 500; line-height: 1.35; } -.lc-list-plate-empty { color: #94a3b8 !important; font-style: italic; font-weight: 500 !important; } -.lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; } .lc-compare-modal .ant-modal-content { border-radius: 16px; overflow: hidden; } .lc-compare-modal .ant-modal-header { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); border-bottom: 1px solid #e2e8f0; padding: 16px 20px; } .lc-compare-modal .ant-modal-title { font-size: 16px; font-weight: 700; color: #0f172a; } @@ -6411,10 +6342,10 @@ const Component = function () { }; const renderFilterField = (label, control) => ( -
+
+ ); const isRetiredVehicle = (record) => record?.status === '退出运营'; @@ -6429,8 +6360,7 @@ const Component = function () { return (
{dateVal || '—'}
@@ -6472,14 +6402,17 @@ const Component = function () { return (
{displayPlate} {sub ? ( - + {sub} @@ -6496,7 +6429,7 @@ const Component = function () { onHeaderCell: listColumnHeaderCell, render: (vin, record) => ( - + {vin} @@ -6509,7 +6442,7 @@ const Component = function () { width: 80, onHeaderCell: listColumnHeaderCell, render: (status) => ( - + {status} ), @@ -6574,7 +6507,7 @@ const Component = function () { @@ -6623,7 +6554,6 @@ const Component = function () { readOnly placeholder="点击输入多个车牌" value={appliedMultiPlates.length ? `已选 ${appliedMultiPlates.length} 个车牌` : ''} - style={{ borderRadius: 8 }} /> )), @@ -6634,7 +6564,6 @@ const Component = function () { value={listFilters.vin} onChange={(e) => setListFilters((prev) => ({ ...prev, vin: e.target.value }))} onPressEnter={handleListFilterQuery} - style={{ borderRadius: 8 }} /> )), renderFilterField('品牌', ( @@ -6723,37 +6652,11 @@ const Component = function () { ]; return ( -
+
-
-
-
-
保险采购
-
保单管理(一车一档)· 比价单独立管理,互不关联
-
- -
- - +
+
{(listFilterExpanded ? listFilterFieldNodes : listFilterFieldNodes.slice(0, 4))}
@@ -6772,7 +6675,6 @@ const Component = function () { {[ { key: 'total', type: 'total', title: '台账车辆总数', desc: '纳入保险采购台账管理的车辆(一车一档)', val: stats.total, icon: ICONS.vehicle }, { key: 'normal', type: 'normal', title: '保险状态正常', desc: '交强险、商业险均已购买且在有效期内,与车辆管理「保险状态=正常」一致', val: stats.normal, icon: ICONS.success }, - { key: 'warning', type: 'warning', title: '险种临期预警', desc: `任一类险种止期 ≤ ${INSURANCE_WARN_DAYS} 天(含交强险、商业险、超赔险、货物险、驾意险)`, val: stats.warning, icon: ICONS.warning }, { key: 'expired', type: 'expired', title: '核心险种逾期', desc: '交强险或商业险已到期,车辆管理保险状态为异常,禁止交车', val: stats.expired, icon: ICONS.warning }, { key: 'unuploaded', type: 'unuploaded', title: '核心险种待购', desc: '交强险或商业险未录入/未购买,车辆管理保险状态为异常,禁止交车', val: stats.unuploaded, icon: ICONS.shield }, ].map((card) => ( @@ -6782,22 +6684,18 @@ const Component = function () { role="button" tabIndex={0} className={`lc-alert-card lc-alert-card--${card.type} lc-alert-card-clickable${ - card.key === 'warning' - ? (insuranceAlertOpen && insuranceAlertMode === 'expiring' ? ' lc-alert-card-active' : '') - : card.key === 'expired' - ? (insuranceAlertOpen && insuranceAlertMode === 'coreExpired' ? ' lc-alert-card-active' : '') - : (kpiFilter === card.key ? ' lc-alert-card-active' : '') + card.key === 'expired' + ? (insuranceAlertOpen && insuranceAlertMode === 'coreExpired' ? ' lc-alert-card-active' : '') + : (kpiFilter === card.key ? ' lc-alert-card-active' : '') }`} onClick={() => { - if (card.key === 'warning') openInsuranceAlertModal('expiring'); - else if (card.key === 'expired') openInsuranceAlertModal('coreExpired'); + if (card.key === 'expired') openInsuranceAlertModal('coreExpired'); else setKpiFilter(card.key); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - if (card.key === 'warning') openInsuranceAlertModal('expiring'); - else if (card.key === 'expired') openInsuranceAlertModal('coreExpired'); + if (card.key === 'expired') openInsuranceAlertModal('coreExpired'); else setKpiFilter(card.key); } }} @@ -6828,43 +6726,37 @@ const Component = function () { ))}
-
+
- 保单录入 + 保单录入
-
+
- -
暂无符合检索条件的保险台账车辆
+
+ +
暂无符合检索条件的保险台账车辆
), }} diff --git a/src/prototypes/insurance-procurement/annotation-source.json b/src/prototypes/insurance-procurement/annotation-source.json index d8c753e..c36fdb5 100644 --- a/src/prototypes/insurance-procurement/annotation-source.json +++ b/src/prototypes/insurance-procurement/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "insurance-procurement", "pageId": "list", - "updatedAt": 1782810809220, + "updatedAt": 1782871095269, "nodes": [ { "id": "ipc-header", @@ -30,7 +30,7 @@ { "id": "ipc-filter", "index": 2, - "title": "保单管理 · 筛选条件", + "title": "筛选条件", "pageId": "list", "locator": { "selectors": [ @@ -502,6 +502,13 @@ "title": "ONE-OS 原型导航", "defaultExpanded": true, "children": [ + { + "type": "link", + "id": "nav-link-contract-template-management", + "title": "合同模板管理", + "href": "/prototypes/contract-template-management", + "target": "self" + }, { "type": "link", "id": "nav-link-insurance-procurement", @@ -509,13 +516,6 @@ "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", @@ -523,13 +523,6 @@ "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", @@ -565,21 +558,6 @@ "href": "/prototypes/self-operated-business-ledger", "target": "self" }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, { "type": "folder", "id": "nav-folder-prototypes-oneos-web", diff --git a/src/prototypes/insurance-procurement/index.tsx b/src/prototypes/insurance-procurement/index.tsx index 4c4d99e..0937438 100644 --- a/src/prototypes/insurance-procurement/index.tsx +++ b/src/prototypes/insurance-procurement/index.tsx @@ -10,35 +10,54 @@ import { type AnnotationViewerOptions, } from '@axhub/annotation'; import 'antd/dist/reset.css'; +import './styles/index.css'; import InsuranceProcurement from './InsuranceProcurement.jsx'; import annotationSourceDocument from './annotation-source.json'; +/** 与 oneos-tokens.css / Antd设计规范(v5)一致 */ const ipcTheme = { token: { colorPrimary: '#10b981', colorLink: '#10b981', colorLinkHover: '#059669', + colorSuccess: '#10b981', + colorWarning: '#f59e0b', + colorError: '#ef4444', borderRadius: 8, fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif', fontSize: 14, colorText: '#0f172a', - colorTextSecondary: '#64748b', + colorTextSecondary: '#475569', + colorTextTertiary: '#64748b', + colorTextQuaternary: '#94a3b8', colorBorder: '#e2e8f0', colorBgContainer: '#ffffff', + colorBgLayout: '#f8fafc', }, components: { Table: { headerBg: '#f8fafc', - headerColor: '#64748b', + headerColor: '#475569', rowHoverBg: '#f8fafc', borderColor: '#e2e8f0', - cellPaddingBlock: 8, - cellPaddingInline: 12, + cellPaddingBlock: 12, + cellPaddingInline: 16, + fontSize: 13, }, Card: { borderRadiusLG: 16, }, + Button: { + controlHeight: 32, + fontWeight: 500, + }, + Input: { + controlHeight: 32, + }, + Select: { + controlHeight: 32, + }, }, }; diff --git a/src/prototypes/insurance-procurement/styles/index.css b/src/prototypes/insurance-procurement/styles/index.css new file mode 100644 index 0000000..2b9ea9c --- /dev/null +++ b/src/prototypes/insurance-procurement/styles/index.css @@ -0,0 +1,408 @@ +/** + * 保险采购 — 列表筛选区 / 表格区 + * 设计规范:Antd设计规范(v5).md · oneos-tokens.css · oneos-app.css + */ +@import url('../../../resources/oneos-web-legacy/styles/oneos-app.css'); +@import url('../../../resources/oneos-web-legacy/styles/ant-table-global-fix.css'); + +/* 列表页全屏滚动布局 */ +.vm-page.ipc-page { + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: column; + padding-bottom: 80px; +} + +.vm-page.ipc-page .ipc-list-shell { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.vm-page.ipc-page .lc-table-section { + flex: 1; + min-height: 0; +} + +.vm-page.ipc-page .lc-table-card { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +/* KPI:本页 4 张卡片 */ +.vm-page.ipc-page .lc-alert-stats-row { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.vm-page.ipc-page .lc-alert-card { + align-items: flex-start; + padding: 14px 30px 14px var(--oneos-space-4); +} + +.vm-page.ipc-page .lc-alert-card-icon { + width: 40px; + height: 40px; + border-radius: var(--oneos-radius-lg); + box-shadow: none; + background: var(--oneos-color-bg-hover); + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-alert-card-main { + flex: 1; + min-width: 0; +} + +.vm-page.ipc-page .lc-alert-card-val { + font-size: 26px; + font-weight: var(--oneos-font-weight-extrabold); + line-height: 1.1; + color: var(--oneos-color-text); + font-variant-numeric: tabular-nums; +} + +.vm-page.ipc-page .lc-alert-card-title { + margin-top: 2px; + font-size: var(--oneos-font-size-sm); + font-weight: var(--oneos-font-weight-semibold); + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-alert-card-tip-anchor { + position: absolute; + top: var(--oneos-space-2); + right: var(--oneos-space-2); + z-index: 2; + line-height: 0; +} + +.vm-page.ipc-page .lc-alert-card-tip { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border: 1px solid var(--oneos-color-border); + border-radius: 50%; + color: var(--oneos-color-text-placeholder); + background: rgba(255, 255, 255, 0.92); + cursor: help; + line-height: 0; +} + +.vm-page.ipc-page .lc-alert-card-tip:hover { + color: var(--oneos-color-text-muted); + border-color: var(--oneos-color-border-strong); + background: var(--oneos-color-bg-card); +} + +.vm-page.ipc-page .lc-alert-card--total { + background: linear-gradient(135deg, var(--oneos-color-bg-muted) 0%, var(--oneos-color-bg-card) 100%); +} + +.vm-page.ipc-page .lc-alert-card--total .lc-alert-card-icon { + background: var(--oneos-color-border); + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-alert-card--normal { + background: linear-gradient(135deg, var(--oneos-color-success-bg) 0%, var(--oneos-color-bg-card) 55%); + border-color: var(--oneos-color-primary-border); +} + +.vm-page.ipc-page .lc-alert-card--normal .lc-alert-card-icon { + background: #d1fae5; + color: var(--oneos-color-primary-hover); +} + +.vm-page.ipc-page .lc-alert-card--normal .lc-alert-card-val { + color: var(--oneos-color-success-text); +} + +.vm-page.ipc-page .lc-alert-card--warning .lc-alert-card-icon { + background: #ffedd5; + color: #ea580c; +} + +.vm-page.ipc-page .lc-alert-card--warning .lc-alert-card-val { + color: #c2410c; +} + +.vm-page.ipc-page .lc-alert-card--warning { + background: linear-gradient(135deg, #fff7ed 0%, var(--oneos-color-bg-card) 55%); + border-color: #fed7aa; +} + +.vm-page.ipc-page .lc-alert-card--expired { + background: linear-gradient(135deg, var(--oneos-color-error-bg) 0%, var(--oneos-color-bg-card) 55%); + border-color: var(--oneos-color-error-border); +} + +.vm-page.ipc-page .lc-alert-card--expired .lc-alert-card-icon { + background: #fee2e2; + color: var(--oneos-color-error); +} + +.vm-page.ipc-page .lc-alert-card--expired .lc-alert-card-val { + color: #b91c1c; +} + +.vm-page.ipc-page .lc-alert-card--unuploaded .lc-alert-card-icon { + background: var(--oneos-color-bg-hover); + color: var(--oneos-color-text-muted); +} + +.vm-page.ipc-page .lc-alert-card--unuploaded .lc-alert-card-val { + color: var(--oneos-color-text-muted); +} + +.vm-page.ipc-page .lc-alert-card--unuploaded { + background: linear-gradient(135deg, var(--oneos-color-bg-muted) 0%, var(--oneos-color-bg-card) 55%); +} + +/* 筛选:多车牌弹层 */ +.vm-page.ipc-page .lc-multi-plate-pop { + width: 320px; + padding: 4px 2px; +} + +.vm-page.ipc-page .lc-multi-plate-pop-hint { + margin-bottom: var(--oneos-space-2); + font-size: var(--oneos-font-size-xs); + line-height: var(--oneos-line-height-normal); + color: var(--oneos-color-text-muted); +} + +.vm-page.ipc-page .lc-multi-plate-pop-actions { + display: flex; + justify-content: flex-end; + gap: var(--oneos-space-2); + margin-top: 10px; +} + +.vm-page.ipc-page .lc-multi-plate-trigger { + cursor: pointer; +} + +.vm-page.ipc-page .lc-multi-plate-trigger .ant-input { + cursor: pointer; +} + +.vm-page.ipc-page .lc-filter-expand-btn { + margin-right: auto; + padding: 0 var(--oneos-space-2); + font-weight: var(--oneos-font-weight-medium); +} + +/* 列表工具栏 */ +.vm-page.ipc-page .lc-table-toolbar-meta { + font-size: var(--oneos-font-size-sm); + font-weight: var(--oneos-font-weight-semibold); + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-policy-toolbar { + margin-left: auto; +} + +/* 表格 */ +.vm-page.ipc-page .lc-list-table .ant-table-wrapper, +.vm-page.ipc-page .lc-list-table .ant-table { + width: 100% !important; +} + +.vm-page.ipc-page .lc-list-table .ant-table-content table { + table-layout: fixed; + width: 100% !important; +} + +.vm-page.ipc-page .lc-table-card .ant-table-thead > tr > th.lc-th-wrap { + padding: 10px var(--oneos-space-2) !important; + text-align: center; + vertical-align: middle; + white-space: nowrap; +} + +.vm-page.ipc-page .lc-table-th-multiline { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + line-height: 1.3; + white-space: normal; +} + +.vm-page.ipc-page .lc-table-th-line { + display: block; + font-size: var(--oneos-font-size-xs); + font-weight: var(--oneos-font-weight-bold); + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { + border-bottom: 1px solid var(--oneos-color-border-light) !important; +} + +.vm-page.ipc-page .lc-table-card .ant-table-tbody > tr.lc-row-retired:not(.ant-table-measure-row) > td { + background: var(--oneos-color-bg-muted) !important; + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-cell-ellipsis { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 表格单元格 */ +.vm-page.ipc-page .lc-list-expire-cell { + display: flex; + flex-direction: column; + gap: var(--oneos-space-1); + min-width: 0; +} + +.vm-page.ipc-page .lc-list-expire-date { + font-size: var(--oneos-font-size-xs); + font-weight: var(--oneos-font-weight-semibold); + line-height: var(--oneos-line-height-tight); +} + +.vm-page.ipc-page .lc-list-expire-meta { + line-height: 1.2; +} + +.vm-page.ipc-page .lc-list-status-badge-wrap { + display: inline-flex; + max-width: 100%; +} + +.vm-page.ipc-page .lc-list-status-badge-wrap .ant-badge { + display: inline-flex; + align-items: center; + max-width: 100%; +} + +.vm-page.ipc-page .lc-list-status-badge-text { + font-size: 11px; + font-weight: var(--oneos-font-weight-semibold); + white-space: nowrap; +} + +.vm-page.ipc-page .lc-list-plate-cell { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.vm-page.ipc-page .lc-list-plate-sub { + display: block; + font-size: 11px; + font-weight: var(--oneos-font-weight-medium); + line-height: var(--oneos-line-height-tight); +} + +.vm-page.ipc-page .lc-list-plate-empty { + color: var(--oneos-color-text-placeholder) !important; + font-style: italic; + font-weight: var(--oneos-font-weight-medium) !important; +} + +.vm-page.ipc-page .lc-list-plate-primary { + font-size: var(--oneos-font-size-sm); + font-weight: var(--oneos-font-weight-semibold); +} + +.vm-page.ipc-page .lc-list-plate-primary.is-muted { + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-list-plate-primary.is-empty { + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-list-plate-primary.is-active { + color: var(--oneos-color-text); +} + +.vm-page.ipc-page .lc-list-plate-sub.is-muted { + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-list-plate-sub.is-active { + color: var(--oneos-color-text-muted); +} + +.vm-page.ipc-page .lc-list-vin { + font-family: var(--oneos-font-mono); + font-size: 11px; + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-list-vin.is-muted { + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-list-status-text { + font-size: var(--oneos-font-size-sm); + font-weight: var(--oneos-font-weight-semibold); + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-list-status-text.is-muted { + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-list-expire-date.is-muted { + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-list-expire-date.has-value { + color: var(--oneos-color-text-secondary); +} + +.vm-page.ipc-page .lc-list-expire-date.is-empty { + color: var(--oneos-color-text-placeholder); +} + +.vm-page.ipc-page .lc-list-table .lc-action-btn { + min-height: auto; + height: auto; + padding: 0; + font-size: var(--oneos-font-size-sm); +} + +.vm-page.ipc-page .lc-list-policy-tag { + margin-top: var(--oneos-space-1); +} + +.vm-page.ipc-page .lc-list-empty { + padding: 40px 0; + color: var(--oneos-color-text-placeholder); + text-align: center; +} + +.vm-page.ipc-page .lc-list-empty-icon { + margin-bottom: var(--oneos-space-3); +} + +@media (max-width: 1280px) { + .vm-page.ipc-page .lc-alert-stats-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .vm-page.ipc-page .lc-alert-stats-row { + grid-template-columns: 1fr; + } +} diff --git a/src/prototypes/lease-business-ledger/annotation-source.json b/src/prototypes/lease-business-ledger/annotation-source.json index 1a6af83..c4ec07d 100644 --- a/src/prototypes/lease-business-ledger/annotation-source.json +++ b/src/prototypes/lease-business-ledger/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "lease-business-ledger", "pageId": "list", - "updatedAt": 1782810809220, + "updatedAt": 1782871095270, "nodes": [ { "id": "lbl-filter", @@ -878,6 +878,13 @@ "title": "ONE-OS 原型导航", "defaultExpanded": true, "children": [ + { + "type": "link", + "id": "nav-link-contract-template-management", + "title": "合同模板管理", + "href": "/prototypes/contract-template-management", + "target": "self" + }, { "type": "link", "id": "nav-link-insurance-procurement", @@ -885,13 +892,6 @@ "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", @@ -899,13 +899,6 @@ "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", @@ -941,21 +934,6 @@ "href": "/prototypes/self-operated-business-ledger", "target": "self" }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, { "type": "folder", "id": "nav-folder-prototypes-oneos-web", diff --git a/src/prototypes/lease-contract-management/LeaseContractCreate.jsx b/src/prototypes/lease-contract-management/LeaseContractCreate.jsx index aacc1e1..0784d26 100644 --- a/src/prototypes/lease-contract-management/LeaseContractCreate.jsx +++ b/src/prototypes/lease-contract-management/LeaseContractCreate.jsx @@ -38,7 +38,7 @@ var SEAL_TYPE_OPTIONS = [ { value: 'invoice', label: '发票章' }, ]; -export default function LeaseContractCreate({ onBack }) { +export default function LeaseContractCreate({ onBack, initialScrollSection, editRecord }) { var contractTypeState = useState(''); var contractType = contractTypeState[0]; var setContractType = contractTypeState[1]; @@ -242,6 +242,14 @@ export default function LeaseContractCreate({ onBack }) { seal: useRef(null), }; + useEffect(function () { + if (!initialScrollSection) return undefined; + var timer = window.setTimeout(function () { + scrollToCreateSection(initialScrollSection); + }, 160); + return function () { window.clearTimeout(timer); }; + }, [initialScrollSection, editRecord && editRecord.id]); + var createSteps = useMemo(function () { return [ { @@ -536,7 +544,7 @@ export default function LeaseContractCreate({ onBack }) { 'aria-hidden': true, }), React.createElement('span', { className: 'lc-form-card__hint-text' }, - '添加授权人须同时上传授权委托书', + '添加受托人须同时上传授权委托书', ), ), ), diff --git a/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx b/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx index 3b372a9..1630bc8 100644 --- a/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx +++ b/src/prototypes/lease-contract-management/LeaseContractEditorForm.jsx @@ -1401,7 +1401,7 @@ function PowerOfAttorneyEditor(props) { function addDelegate() { if (delegates.length >= POA_MAX_DELEGATES) { - message.warning('一份授权委托书最多添加 ' + POA_MAX_DELEGATES + ' 个委托人'); + message.warning('一份授权委托书最多添加 ' + POA_MAX_DELEGATES + ' 个受托人'); return; } updateDelegates(delegates.concat([createEmptyDelegateRow()])); @@ -1424,10 +1424,10 @@ function PowerOfAttorneyEditor(props) { key: row.id, className: 'lc-poa-editor__row', role: 'group', - 'aria-label': '委托人 ' + (index + 1), + 'aria-label': '受托人 ' + (index + 1), }, delegates.length > 1 ? React.createElement('div', { className: 'lc-poa-editor__row-head' }, - React.createElement('span', { className: 'lc-poa-editor__row-index' }, '委托人 ' + (index + 1)), + React.createElement('span', { className: 'lc-poa-editor__row-index' }, '受托人 ' + (index + 1)), ) : null, React.createElement('div', { className: 'lc-poa-editor__fields lc-poa-editor__fields--inline' }, React.createElement(FormField, { label: '姓名', required: started }, @@ -1437,7 +1437,7 @@ function PowerOfAttorneyEditor(props) { onChange: function (e) { updateDelegate(row.id, { name: e.target.value }); }, placeholder: '请输入姓名', allowClear: true, - 'aria-label': '委托人姓名', + 'aria-label': '受托人姓名', }), ), React.createElement(FormField, { label: '联系方式', required: started }, @@ -1447,7 +1447,7 @@ function PowerOfAttorneyEditor(props) { onChange: function (e) { updateDelegate(row.id, { contact: e.target.value }); }, placeholder: '请输入联系方式', allowClear: true, - 'aria-label': '委托人联系方式', + 'aria-label': '受托人联系方式', }), ), React.createElement(FormField, { label: '证件号码', required: started }, @@ -1457,7 +1457,7 @@ function PowerOfAttorneyEditor(props) { onChange: function (e) { updateDelegate(row.id, { idNumber: e.target.value }); }, placeholder: '请输入证件号码', allowClear: true, - 'aria-label': '委托人证件号码', + 'aria-label': '受托人证件号码', }), ), React.createElement('div', { className: 'lc-poa-editor__row-action lc-poa-editor__row-action--ops' }, @@ -1466,7 +1466,7 @@ function PowerOfAttorneyEditor(props) { type: 'button', className: 'lc-poa-editor__remove-btn', onClick: function () { removeDelegate(row.id); }, - 'aria-label': '删除委托人 ' + (index + 1), + 'aria-label': '删除受托人 ' + (index + 1), title: '删除', }, React.createElement(Trash2, { size: 14, 'aria-hidden': true })), ), @@ -1480,7 +1480,7 @@ function PowerOfAttorneyEditor(props) { onClick: addDelegate, disabled: delegates.length >= POA_MAX_DELEGATES, icon: React.createElement(Plus, { size: 14, 'aria-hidden': true }), - }, hasStartedDelegate && delegates.length < POA_MAX_DELEGATES ? '新增委托人' : '新增委托人(' + delegates.length + '/' + POA_MAX_DELEGATES + ')'), + }, hasStartedDelegate && delegates.length < POA_MAX_DELEGATES ? '新增受托人' : '新增受托人(' + delegates.length + '/' + POA_MAX_DELEGATES + ')'), ), ); } diff --git a/src/prototypes/lease-contract-management/LeaseContractFleetSummary.jsx b/src/prototypes/lease-contract-management/LeaseContractFleetSummary.jsx new file mode 100644 index 0000000..3b1d442 --- /dev/null +++ b/src/prototypes/lease-contract-management/LeaseContractFleetSummary.jsx @@ -0,0 +1,305 @@ +// 在租车辆概览 — 按品牌型号统计 +import { buildOnLeaseFleetByBrandModel, formatDeliveryRegion } from './lease-contract-list-data.js'; + +function formatPlateLabel(plateNo) { + var plate = plateNo && String(plateNo).trim(); + if (!plate || plate === '-') return '待上牌'; + return plate; +} + +function formatMileageLabel(vehicle) { + var raw = vehicle.currentMileage; + if (raw == null || raw === '' || raw === '-') return '-'; + var num = Number(String(raw).replace(/,/g, '')); + if (!Number.isFinite(num)) return String(raw); + return num.toLocaleString('zh-CN') + '\u00a0km'; +} + +function FleetDetailModal(props) { + var Modal = props.Modal; + var item = props.item; + var open = props.open; + var onClose = props.onClose; + var onViewContract = props.onViewContract; + if (!Modal) return null; + + var titleNode = item + ? React.createElement('div', { className: 'lc-fleet-detail-modal__title' }, + React.createElement('span', { className: 'lc-fleet-detail-modal__title-main' }, item.brand + ' · ' + item.model), + React.createElement('span', { className: 'lc-fleet-detail-modal__title-badge tabular-nums' }, item.count + ' 辆在租'), + React.createElement('span', { className: 'lc-fleet-detail-modal__title-type' }, item.vehicleType || '-'), + ) + : null; + + return React.createElement(Modal, { + className: 'lc-fleet-detail-modal', + rootClassName: 'lc-fleet-detail-modal-root', + title: titleNode, + open: open, + onCancel: onClose, + footer: React.createElement('button', { + type: 'button', + className: 'vm-btn vm-btn-ghost lc-fleet-detail-modal__close', + onClick: onClose, + }, '关闭'), + width: 960, + centered: true, + destroyOnClose: true, + }, + item && item.entries && item.entries.length + ? React.createElement('div', { className: 'lc-fleet-detail-modal__body' }, + React.createElement('div', { className: 'lc-fleet-detail-table-wrap' }, + React.createElement('table', { className: 'lc-fleet-detail-table' }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-plate' }, '车牌号'), + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-vin' }, '识别代码'), + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-type' }, '车辆类型'), + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-announce' }, '公告型号'), + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-city' }, '运营城市'), + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-mileage' }, '里程数'), + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-project' }, '所属项目'), + React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-action' }, '操作'), + ), + ), + React.createElement('tbody', null, + item.entries.map(function (entry, index) { + var vehicle = entry.vehicle; + var record = entry.record; + var plate = formatPlateLabel(vehicle.plateNo); + var vin = vehicle.vin && String(vehicle.vin).trim() ? vehicle.vin : '-'; + var regionLabel = formatDeliveryRegion(record.deliveryRegion) || '-'; + var mileageLabel = formatMileageLabel(vehicle); + return React.createElement('tr', { key: record.id + '-' + index }, + React.createElement('td', { className: 'lc-fleet-detail-table__col-plate' }, + React.createElement('span', { + className: 'lc-fleet-detail-table__plate tabular-nums', + title: plate, + }, plate), + ), + React.createElement('td', { className: 'lc-fleet-detail-table__col-vin' }, + React.createElement('span', { + className: 'lc-fleet-detail-table__vin tabular-nums', + title: vin, + }, vin), + ), + React.createElement('td', { className: 'lc-fleet-detail-table__col-type' }, + React.createElement('span', { className: 'lc-fleet-detail-table__type-pill' }, vehicle.vehicleType || '-'), + ), + React.createElement('td', { className: 'lc-fleet-detail-table__col-announce' }, + React.createElement('span', { + className: 'lc-fleet-detail-table__announce', + title: vehicle.officialModel || '-', + }, vehicle.officialModel || '-'), + ), + React.createElement('td', { className: 'lc-fleet-detail-table__col-city' }, + React.createElement('span', { className: 'lc-fleet-detail-table__city' }, regionLabel), + ), + React.createElement('td', { className: 'lc-fleet-detail-table__col-mileage' }, + React.createElement('span', { className: 'lc-fleet-detail-table__mileage tabular-nums' }, mileageLabel), + ), + React.createElement('td', { className: 'lc-fleet-detail-table__col-project' }, + React.createElement('div', { className: 'lc-fleet-detail-table__project' }, + React.createElement('span', { + className: 'lc-fleet-detail-table__project-name', + title: record.projectName || '-', + }, record.projectName || '-'), + React.createElement('button', { + type: 'button', + className: 'lc-fleet-detail-table__contract-link tabular-nums', + onClick: function () { onViewContract(record); }, + title: '查看合同 ' + (record.contractCode || ''), + }, record.contractCode || '-'), + React.createElement('span', { + className: 'lc-fleet-detail-table__customer', + title: record.customerName || '-', + }, record.customerName || '-'), + ), + ), + React.createElement('td', { className: 'lc-fleet-detail-table__col-action' }, + React.createElement('button', { + type: 'button', + className: 'lc-fleet-detail-table__action', + onClick: function () { onViewContract(record); }, + }, '查看合同'), + ), + ); + }), + ), + ), + ), + ) + : React.createElement('div', { className: 'lc-fleet-detail-modal__empty' }, '暂无车辆明细'), + ); +} + +export default function LeaseContractFleetSummary(props) { + var records = props.records || []; + var onViewContract = props.onViewContract || function () {}; + var Modal = props.Modal; + + var useState = React.useState; + var useMemo = React.useMemo; + + var _expanded = useState(false); + var expanded = _expanded[0]; + var setExpanded = _expanded[1]; + var _brand = useState('全部'); + var brandFilter = _brand[0]; + var setBrandFilter = _brand[1]; + var _detailItem = useState(null); + var detailItem = _detailItem[0]; + var setDetailItem = _detailItem[1]; + var _detailOpen = useState(false); + var detailOpen = _detailOpen[0]; + var setDetailOpen = _detailOpen[1]; + + function openDetail(item) { + setDetailItem(item); + setDetailOpen(true); + } + + function closeDetail() { + setDetailOpen(false); + setDetailItem(null); + } + + var summary = useMemo(function () { + return buildOnLeaseFleetByBrandModel(records); + }, [records]); + + var brandOptions = useMemo(function () { + var set = {}; + summary.forEach(function (item) { set[item.brand] = true; }); + return Object.keys(set).sort(function (a, b) { return a.localeCompare(b, 'zh-CN'); }); + }, [summary]); + + var filtered = useMemo(function () { + return summary.filter(function (item) { + return brandFilter === '全部' || item.brand === brandFilter; + }); + }, [summary, brandFilter]); + + var totalOnLease = useMemo(function () { + return summary.reduce(function (sum, item) { return sum + item.count; }, 0); + }, [summary]); + + var maxCount = filtered.length ? Math.max.apply(null, filtered.map(function (item) { return item.count; })) : 1; + + if (!totalOnLease) return null; + + return React.createElement('section', { + className: 'lc-fleet-summary' + (expanded ? ' is-expanded' : ' is-collapsed'), + 'data-annotation-id': 'lc-fleet-summary', + 'aria-label': '在租车辆概览', + }, + React.createElement('header', { className: 'lc-fleet-summary__header' }, + React.createElement('div', { className: 'lc-fleet-summary__title-block' }, + React.createElement('div', { className: 'lc-fleet-summary__title-row' }, + React.createElement('span', { className: 'lc-fleet-summary__icon', 'aria-hidden': true }, + React.createElement('svg', { + xmlns: 'http://www.w3.org/2000/svg', width: 20, height: 20, viewBox: '0 0 24 24', + fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', + }, + React.createElement('path', { d: 'M14 16H9m10 0h3v-3.5a1.5 1.5 0 0 0-1.5-1.5h-15A1.5 1.5 0 0 0 3 16.5V20h3' }), + React.createElement('circle', { cx: 7.5, cy: 17.5, r: 2.5 }), + React.createElement('circle', { cx: 17.5, cy: 17.5, r: 2.5 }), + ), + ), + React.createElement('h2', { className: 'lc-fleet-summary__title' }, '在租车辆'), + React.createElement('span', { className: 'lc-fleet-summary__badge tabular-nums' }, totalOnLease + ' 辆'), + ), + ), + React.createElement('div', { className: 'lc-fleet-summary__toolbar' }, + React.createElement('button', { + type: 'button', + className: 'lc-fleet-summary__toggle', + onClick: function () { setExpanded(!expanded); }, + 'aria-expanded': expanded, + 'aria-label': expanded ? '收起在租车辆概览' : '查看在租车辆详情', + }, + React.createElement('svg', { + xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24', + fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', + 'aria-hidden': true, + className: 'lc-fleet-summary__toggle-icon', + }, + React.createElement('path', { d: expanded ? 'm18 15-6-6-6 6' : 'm6 9 6 6 6-6' }), + ), + expanded ? '收起' : '查看在租车辆详情', + ), + ), + ), + expanded ? React.createElement('div', { className: 'lc-fleet-summary__body' }, + React.createElement('div', { className: 'lc-fleet-summary__brands', role: 'group', 'aria-label': '按品牌筛选' }, + React.createElement('button', { + type: 'button', + className: 'lc-fleet-summary__brand-chip' + (brandFilter === '全部' ? ' is-active' : ''), + onClick: function () { setBrandFilter('全部'); }, + 'aria-pressed': brandFilter === '全部', + }, '全部'), + brandOptions.map(function (brand) { + return React.createElement('button', { + key: brand, + type: 'button', + className: 'lc-fleet-summary__brand-chip' + (brandFilter === brand ? ' is-active' : ''), + onClick: function () { setBrandFilter(brandFilter === brand ? '全部' : brand); }, + 'aria-pressed': brandFilter === brand, + }, brand); + }), + ), + filtered.length ? React.createElement('div', { className: 'lc-fleet-summary__grid', role: 'list' }, + filtered.map(function (item) { + var share = Math.round((item.count / totalOnLease) * 100); + var barWidth = Math.max(8, Math.round((item.count / maxCount) * 100)); + return React.createElement('article', { + key: item.key, + className: 'lc-fleet-summary__card', + role: 'listitem', + tabIndex: 0, + onClick: function () { openDetail(item); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + openDetail(item); + } + }, + 'aria-label': item.brand + ' ' + item.model + ',' + item.count + ' 辆在租,点击查看明细', + }, + React.createElement('div', { className: 'lc-fleet-summary__card-top' }, + React.createElement('span', { className: 'lc-fleet-summary__card-brand' }, item.brand), + React.createElement('span', { className: 'lc-fleet-summary__card-type' }, item.vehicleType), + ), + React.createElement('h3', { className: 'lc-fleet-summary__card-model', title: item.model }, item.model), + React.createElement('div', { className: 'lc-fleet-summary__card-metrics' }, + React.createElement('span', { className: 'lc-fleet-summary__card-count tabular-nums' }, item.count), + React.createElement('span', { className: 'lc-fleet-summary__card-unit' }, '辆'), + React.createElement('span', { className: 'lc-fleet-summary__card-share tabular-nums' }, '占 ' + share + '%'), + ), + React.createElement('div', { className: 'lc-fleet-summary__card-bar', 'aria-hidden': true }, + React.createElement('span', { + className: 'lc-fleet-summary__card-bar-fill', + style: { width: barWidth + '%' }, + }), + ), + React.createElement('span', { className: 'lc-fleet-summary__card-hint' }, '点击查看车辆明细'), + ); + }), + ) : React.createElement('div', { className: 'lc-fleet-summary__empty' }, + React.createElement('p', null, '该品牌暂无在租车型'), + React.createElement('button', { + type: 'button', + className: 'lc-fleet-summary__empty-reset', + onClick: function () { setBrandFilter('全部'); }, + }, '查看全部'), + ), + ) : null, + React.createElement(FleetDetailModal, { + Modal: Modal, + item: detailItem, + open: detailOpen, + onClose: closeDetail, + onViewContract: onViewContract, + }), + ); +} diff --git a/src/prototypes/lease-contract-management/LeaseContractManagement.jsx b/src/prototypes/lease-contract-management/LeaseContractManagement.jsx index f41cf69..0b33535 100644 --- a/src/prototypes/lease-contract-management/LeaseContractManagement.jsx +++ b/src/prototypes/lease-contract-management/LeaseContractManagement.jsx @@ -2,6 +2,7 @@ // 设计规范:oneos-app.css · 样式前缀 lc-* import LeaseContractCreate from './LeaseContractCreate.jsx'; +import LeaseContractFleetSummary from './LeaseContractFleetSummary.jsx'; import { StatusTag } from '../vehicle-management/components/StatusTag'; import { LEASE_CONTRACT_LIST_RECORDS, @@ -11,6 +12,7 @@ import { formatHydrogenPaymentLabel, formatContractApprovalTypeLabel, getAuthorizedDelegates, + needsContractDelegateSupplement, formatContractHandoverMileage, formatContractHandoverDateTimeMinute, isContractVehicleDelivered, @@ -23,11 +25,16 @@ import { getContractVehicleMileageSourceTagClass, formatContractVehicleRemainingMileage, formatContractPeriodStartDate, + computeCurrentMileagePeriodEnd, + computeVehiclePeriodTargetMileage, + computeVehicleMileageForecastStatus, computeContractOverallMileageProgress, computeContractMileageForecastStatus, getContractMileageForecastStatusTone, resolveContractTemplateTypeLabel, - getReturnSettlementStatusTone, + resolveReturnSettlementDisplayLabel, + resolveReturnSettlementCellMode, + getReturnSettlementDisplayTone, getCurrentApproverLabel, shouldHideCurrentApprover, DELIVERY_DATE_TBD_LABEL, @@ -37,6 +44,8 @@ import { } from './lease-contract-list-data.js'; import { getLessorCompanies } from '../contract-template-management/contract-template-vars.js'; +var LC_LIST_TABLE_EXPAND_WIDTH = 48; + const Component = function(props) { var useEffect = React.useEffect; var ONEOS_ANT_TABLE_GLOBAL_FIX = [ @@ -75,6 +84,8 @@ const Component = function(props) { var _page = useState(1); var _pageSize = useState(10); var _view = useState('list'); + var _createInitialSection = useState(null); + var _editContractRecord = useState(null); var _contractCode = useState(undefined); var _projectName = useState(undefined); var _customerName = useState(undefined); @@ -335,6 +346,16 @@ const Component = function(props) { return counts; }, [rawList]); + var fleetSourceList = useMemo(function () { + var overrides = _recordOverrides[0] || {}; + return rawList.map(function (r) { + var merged = overrides[r.id] ? Object.assign({}, r, overrides[r.id]) : r; + return Object.assign({}, merged, { + contractStatus: resolveContractStatusByVehicleLease(merged), + }); + }); + }, [rawList, _recordOverrides[0]]); + var filteredList = useMemo(function() { var overrides = _recordOverrides[0] || {}; var list = rawList.map(function (r) { @@ -705,7 +726,7 @@ const Component = function(props) { React.createElement('span', { className: 'lc-vehicle-popover-card__type', title: vehicle.vehicleType || '' }, vehicle.vehicleType || '-'), plate ? React.createElement('span', { className: 'lc-vehicle-popover-card__plate tabular-nums' }, plate) - : React.createElement('span', { className: 'lc-vehicle-popover-card__plate lc-vehicle-popover-card__plate--pending' }, '待上牌'), + : React.createElement('span', { className: 'lc-vehicle-popover-card__plate lc-vehicle-popover-card__plate--pending' }, '车辆待选'), ), React.createElement('div', { className: 'lc-vehicle-popover-card__body' }, React.createElement('div', { className: 'lc-vehicle-popover-card__field' }, @@ -1030,7 +1051,7 @@ const Component = function(props) { React.createElement(StatusTag, { label: forecastStatus, tone: forecastTone, - title: '合同里程任务完成预判,供运维人员评估是否需换车干预', + title: '基于近7天日均里程与本期剩余天数推算的合同整体完成预判', }), ) : null, @@ -1072,22 +1093,43 @@ const Component = function(props) { function renderReturnSettlementCell(vehicle) { if (!isContractVehicleReturned(vehicle)) { - return React.createElement('div', { className: 'lc-contract-return-settlement lc-contract-return-settlement--empty' }, '—'); + return React.createElement('div', { className: 'lc-contract-return-settlement lc-contract-return-settlement--empty' }); } var status = vehicle.returnSettlementStatus || '待提交'; + var displayLabel = resolveReturnSettlementDisplayLabel(status); + var tone = getReturnSettlementDisplayTone(displayLabel); + var mode = resolveReturnSettlementCellMode(status); var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌'; - var tone = getReturnSettlementStatusTone(status); - return React.createElement('div', { className: 'lc-contract-return-settlement' }, - React.createElement('button', { + var tag = React.createElement(StatusTag, { + label: displayLabel, + tone: tone, + title: displayLabel, + }); + var tagNode = mode + ? React.createElement('button', { type: 'button', className: 'lc-contract-status-tag-btn', - title: '点击查看还车应结款详情:' + plate, + title: mode === 'edit' + ? ('点击处理还车应结款:' + plate) + : ('点击查看还车应结款:' + plate), onClick: function () { - message.info('即将跳转至还车应结款详情(原型演示):' + plate); + if (mode === 'edit') { + message.info('即将跳转至还车应结款编辑页(原型演示):' + plate); + } else { + message.info('即将跳转至还车应结款查看页(原型演示):' + plate); + } }, - }, - React.createElement(StatusTag, { label: status, tone: tone }), - ), + }, tag) + : tag; + var approverNode = displayLabel === '审批中' + ? React.createElement('div', { + className: 'lc-contract-return-settlement__approver', + title: '当前审批节点操作人', + }, vehicle.returnSettlementApprover || '—') + : null; + return React.createElement('div', { className: 'lc-contract-return-settlement lc-contract-return-settlement--stacked' }, + tagNode, + approverNode, ); } @@ -1111,34 +1153,17 @@ const Component = function(props) { ), ); } - var periodNode; - if (!hasRequirement || !isContractVehicleDelivered(vehicle)) { - periodNode = React.createElement('div', { - className: 'lc-contract-mileage-req-row__period lc-contract-mileage-req-row__period--empty', - }, '—'); - } else { - var periodMileText = formatContractHandoverMileage(vehicle.periodStartMileage); - var periodDateText = formatContractPeriodStartDate(vehicle.periodStartDate); - periodNode = React.createElement('div', { className: 'lc-contract-mileage-req-row__period' }, - React.createElement('span', { className: 'lc-contract-mileage-req-row__section-label' }, '当期里程'), - React.createElement('div', { - className: 'lc-contract-mileage-req-row__period-mile tabular-nums', - title: periodMileText, - }, periodMileText || '—'), - React.createElement('div', { - className: 'lc-contract-mileage-req-row__period-date tabular-nums', - title: periodDateText, - }, periodDateText), - ); - } var mileText = formatContractVehicleCurrentMileage(vehicle.currentMileage, vehicle, '—'); var hasMileageReading = mileText !== '—'; var mileageSource = resolveContractVehicleMileageSource(vehicle); var sourceTagClass = getContractVehicleMileageSourceTagClass(mileageSource); - return React.createElement('div', { className: 'lc-contract-mileage-req-row lc-contract-mileage-req-row--triple' }, - requirementNode, - periodNode, - React.createElement('div', { className: 'lc-contract-mileage-req-row__current' }, + var currentNode; + if (!hasRequirement || !isContractVehicleDelivered(vehicle)) { + currentNode = React.createElement('div', { + className: 'lc-contract-mileage-req-row__current lc-contract-mileage-req-row__current--empty', + }, '—'); + } else { + currentNode = React.createElement('div', { className: 'lc-contract-mileage-req-row__current' }, React.createElement('span', { className: 'lc-contract-mileage-req-row__section-label' }, '当前里程'), React.createElement('div', { className: 'vm-cell-tags lc-contract-mileage-req-row__current-line' }, React.createElement('span', { className: 'vm-mileage tabular-nums', title: mileText }, mileText), @@ -1146,7 +1171,68 @@ const Component = function(props) { ? React.createElement('span', { className: sourceTagClass }, mileageSource) : null, ), - ), + ); + } + return React.createElement('div', { className: 'lc-contract-mileage-req-row lc-contract-mileage-req-row--dual' }, + requirementNode, + currentNode, + ); + } + + function getMileageCompletionRangeData(vehicle, contractRecord) { + if (!contractRecord || !contractRecord.hasMinimumMileage || !isContractVehicleDelivered(vehicle)) { + return null; + } + var startDate = vehicle.periodStartDate; + var periodEndDate = startDate && contractRecord.mileagePeriod + ? computeCurrentMileagePeriodEnd(startDate, contractRecord.mileagePeriod) + : null; + var startDateText = formatContractPeriodStartDate(startDate); + var endDateText = formatContractPeriodStartDate(periodEndDate); + var startMileText = formatContractHandoverMileage(vehicle.periodStartMileage); + var targetMileage = computeVehiclePeriodTargetMileage(contractRecord, vehicle); + var endMileText = targetMileage != null ? formatContractHandoverMileage(targetMileage) : '—'; + if (startDateText === '—' && startMileText === '—' && endDateText === '—' && endMileText === '—') { + return null; + } + return { + startDateText: startDateText, + startMileText: startMileText, + endDateText: endDateText, + endMileText: endMileText, + }; + } + + function renderMileageCompletionRangeEnd(rangeData) { + if (!rangeData) return null; + return React.createElement('div', { + className: 'lc-contract-mileage-completion__range-side lc-contract-mileage-completion__range-side--end', + }, + React.createElement('span', { + className: 'lc-contract-mileage-completion__range-date tabular-nums', + title: '到达时间 ' + rangeData.endDateText, + }, rangeData.endDateText), + React.createElement('span', { + className: 'lc-contract-mileage-completion__range-mile tabular-nums', + title: '到达里程 ' + rangeData.endMileText, + }, rangeData.endMileText), + ); + } + + function renderMileageCompletionRangeStart(rangeData) { + if (!rangeData) return null; + return React.createElement('div', { + className: 'lc-contract-mileage-completion__range-side lc-contract-mileage-completion__range-side--start', + 'aria-label': '行程起点', + }, + React.createElement('span', { + className: 'lc-contract-mileage-completion__range-date tabular-nums', + title: '开始时间 ' + rangeData.startDateText, + }, rangeData.startDateText), + React.createElement('span', { + className: 'lc-contract-mileage-completion__range-mile tabular-nums', + title: '开始里程 ' + rangeData.startMileText, + }, rangeData.startMileText || '—'), ); } @@ -1158,14 +1244,32 @@ const Component = function(props) { var remainingNode = null; if (delivered) { var remainingText = formatContractVehicleRemainingMileage(vehicle.remainingMileage); - remainingNode = React.createElement('div', { - className: 'lc-contract-mileage-completion__remaining tabular-nums', - title: remainingText, - }, remainingText); + var forecastStatus = vehicle.mileageForecastStatus || computeVehicleMileageForecastStatus(contractRecord, vehicle); + var forecastTone = getContractMileageForecastStatusTone(forecastStatus); + remainingNode = React.createElement('div', { className: 'lc-contract-mileage-completion__remaining-row' }, + React.createElement('div', { + className: 'lc-contract-mileage-completion__remaining tabular-nums', + title: remainingText, + }, remainingText), + forecastStatus + ? React.createElement(StatusTag, { + label: forecastStatus, + tone: forecastTone, + title: '基于近7天日均里程与本期剩余天数推算的单车完成预判', + }) + : null, + ); } + var rangeData = getMileageCompletionRangeData(vehicle, contractRecord); return React.createElement('div', { className: 'lc-contract-mileage-completion' }, remainingNode, - renderMileageProgressCell(vehicle), + React.createElement('div', { className: 'lc-contract-mileage-completion__progress-row' }, + React.createElement('div', { className: 'lc-contract-mileage-completion__progress-main' }, + renderMileageProgressCell(vehicle), + ), + renderMileageCompletionRangeEnd(rangeData), + ), + renderMileageCompletionRangeStart(rangeData), ); } @@ -1198,9 +1302,10 @@ const Component = function(props) { return React.createElement('div', { className: 'lc-contract-vehicle-info' }, React.createElement('div', { className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary tabular-nums', + title: plateLabel, }, plateLabel), React.createElement('div', { - className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary tabular-nums', + className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary lc-contract-vehicle-info__line--vin tabular-nums', title: vinLabel, }, vinLabel), React.createElement('div', { @@ -1210,7 +1315,18 @@ const Component = function(props) { ); } - function renderHandoverSituationCell(vehicle, kind) { + function formatContractHandoverDateTimeCompact(raw) { + var full = formatContractHandoverDateTimeMinute(raw); + if (!full) return ''; + var match = full.match(/^(\d{4})-(\d{2}-\d{2})[ T](\d{2}:\d{2})$/); + if (match) return match[2] + ' ' + match[3]; + if (/^\d{4}-(\d{2}-\d{2})$/.test(full)) return full.replace(/^\d{4}-/, ''); + return full; + } + + function renderHandoverSituationCell(vehicle, kind, options) { + options = options || {}; + var compact = Boolean(options.compact); var emptyLabel = kind === 'delivery' ? '未交车' : '未还车'; var hasData = kind === 'delivery' ? isContractVehicleDelivered(vehicle) @@ -1225,7 +1341,10 @@ const Component = function(props) { var time = kind === 'delivery' ? vehicle.deliveryTime : vehicle.returnTime; var mileText = formatContractHandoverMileage(mile); var personText = person && String(person).trim() && person !== '-' ? String(person).trim() : '-'; - var timeText = formatContractHandoverDateTimeMinute(time); + var timeTextFull = formatContractHandoverDateTimeMinute(time); + var timeText = compact + ? formatContractHandoverDateTimeCompact(time) + : timeTextFull; var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌'; var jumpTitle = kind === 'delivery' ? '点击查看交车管理:' + plate @@ -1252,7 +1371,7 @@ const Component = function(props) { }, personText), React.createElement('div', { className: 'lc-contract-handover-situation__line lc-contract-handover-situation__line--time tabular-nums', - title: timeText, + title: timeTextFull || timeText, }, timeText || '-'), ); } @@ -1268,11 +1387,11 @@ const Component = function(props) { React.createElement('tr', null, React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__info' }, '车辆信息'), React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__pickup' }, '提车应收款'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover' }, '交车情况'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover' }, '还车情况'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '交车情况' }, '交车'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '还车情况' }, '还车'), React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__return-settlement' }, '还车应结款'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req' }, '里程要求'), - React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion' }, '里程完成情况'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req', title: '里程要求' }, '里程要求'), + React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion', title: '里程完成情况' }, '里程完成情况'), React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__action' }, '操作'), ), ), @@ -1281,8 +1400,8 @@ const Component = function(props) { return React.createElement('tr', { key: record.id + '-vehicle-' + index }, React.createElement('td', { className: 'lc-contract-vehicle-native-table__info' }, renderVehicleInfoCell(vehicle)), React.createElement('td', { className: 'lc-contract-vehicle-native-table__pickup' }, renderPickupReceivableCell(vehicle)), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery')), - React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return')), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery', { compact: true })), + React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return', { compact: true })), React.createElement('td', { className: 'lc-contract-vehicle-native-table__return-settlement' }, renderReturnSettlementCell(vehicle)), React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-req' }, renderMileageRequirementCell(record, vehicle)), React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-completion' }, renderMileageCompletionCell(vehicle, record)), @@ -1629,19 +1748,31 @@ const Component = function(props) { _delegatePopoverRecord[1](null); } + function openContractEditForPoa(record) { + _createInitialSection[1]('poa'); + _editContractRecord[1](record || null); + _view[1]('create'); + } + + function backToContractList() { + _createInitialSection[1](null); + _editContractRecord[1](null); + _view[1]('list'); + } + function renderDelegatePopoverPanel(record) { var delegates = getAuthorizedDelegates(record); return React.createElement('div', { className: 'lc-delegate-popover__panel', role: 'region', - 'aria-label': '被授权人明细', + 'aria-label': '受托人明细', }, React.createElement('div', { className: 'lc-delegate-popover__header' }, - React.createElement('div', { className: 'lc-delegate-popover__title' }, '被授权人'), + React.createElement('div', { className: 'lc-delegate-popover__title' }, '受托人'), React.createElement('div', { className: 'lc-delegate-popover__sub' }, record.contractCode || ''), ), delegates.length === 0 - ? React.createElement('p', { className: 'lc-delegate-popover__empty' }, '暂无被授权人') + ? React.createElement('p', { className: 'lc-delegate-popover__empty' }, '暂无受托人') : React.createElement('div', { className: 'lc-delegate-popover__list' }, delegates.map(function (person, index) { return React.createElement('div', { @@ -1667,6 +1798,15 @@ const Component = function(props) { } function renderDelegateCountCell(_, record) { + if (needsContractDelegateSupplement(record)) { + return React.createElement('button', { + type: 'button', + className: 'lc-delegate-supplement-btn', + title: '补充授权委托书与受托人信息', + 'aria-label': '请补充授权委托书与受托人信息', + onClick: function () { openContractEditForPoa(record); }, + }, '请补充'); + } var count = record.delegateCount != null ? record.delegateCount : 0; if (count <= 0) return '-'; var popoverOpen = _delegatePopoverRecord[0] && _delegatePopoverRecord[0].id === record.id; @@ -1693,7 +1833,7 @@ const Component = function(props) { React.createElement('button', { type: 'button', className: 'lc-delegate-count-btn tabular-nums', - 'aria-label': '查看被授权人 ' + count + ' 人', + 'aria-label': '查看受托人 ' + count + ' 人', }, count + ' 人'), ); } @@ -1864,13 +2004,21 @@ const Component = function(props) { } var columns = [ + Table.EXPAND_COLUMN, { title: '项目信息', key: 'projectInfo', width: 248, fixed: 'left', + ellipsis: true, className: 'lc-project-info-col', - onCell: function() { return { className: 'lc-project-info-col' }; }, + onHeaderCell: function () { + return { + className: 'lc-project-info-col', + 'data-annotation-id': 'lc-project-info-col', + }; + }, + onCell: function () { return { className: 'lc-project-info-col' }; }, render: renderProjectInfoCell }, { @@ -1932,6 +2080,12 @@ const Component = function(props) { key: 'overallMileageProgress', width: 168, className: 'lc-overall-mileage-col', + onHeaderCell: function () { + return { + className: 'lc-overall-mileage-col', + 'data-annotation-id': 'lc-overall-mileage-col', + }; + }, onCell: function () { return { className: 'lc-overall-mileage-col' }; }, render: renderOverallMileageProgressCell, }, @@ -1949,10 +2103,16 @@ const Component = function(props) { render: renderContactCell }, { - title: '被授权人', + title: '受托人', key: 'delegateCount', width: 88, className: 'tabular-nums lc-delegate-col', + onHeaderCell: function () { + return { + className: 'lc-delegate-col tabular-nums', + 'data-annotation-id': 'lc-delegate-col', + }; + }, onCell: function () { return { className: 'lc-delegate-col tabular-nums' }; }, render: renderDelegateCountCell, }, @@ -1971,6 +2131,13 @@ const Component = function(props) { { title: '操作', key: 'action', width: 140, fixed: 'right', render: function(_, record) { return getOperationButtons(record); } } ]; + var listTableScrollX = columns.reduce(function (sum, col) { + if (col === Table.EXPAND_COLUMN) { + return sum + LC_LIST_TABLE_EXPAND_WIDTH; + } + return sum + (Number(col.width) || 0); + }, 0); + var filterItems = [ React.createElement('label', { key: 'contractCode', className: 'vm-filter-field' }, React.createElement('span', null, '合同编码'), @@ -2114,7 +2281,9 @@ const Component = function(props) { if (_view[0] === 'create') { return React.createElement(LeaseContractCreate, { - onBack: function() { _view[1]('list'); } + onBack: backToContractList, + initialScrollSection: _createInitialSection[0], + editRecord: _editContractRecord[0], }); } @@ -2186,6 +2355,13 @@ const Component = function(props) { ); }) ), + React.createElement(LeaseContractFleetSummary, { + records: fleetSourceList, + Modal: Modal, + onViewContract: function (record) { + message.info('查看合同(原型):' + (record.contractCode || record.projectName || '')); + }, + }), React.createElement('section', { className: 'vm-table-section' }, React.createElement('div', { className: 'vm-table-toolbar', 'data-annotation-id': 'lc-list-toolbar' }, React.createElement('div', { className: 'vm-table-actions' }, @@ -2231,11 +2407,13 @@ const Component = function(props) { rowKey: 'id', columns: columns, dataSource: pagedList, - scroll: { x: 2072 }, + tableLayout: 'fixed', + scroll: { x: listTableScrollX, y: 'calc(100vh - 640px)' }, size: 'small', pagination: false, expandable: { - columnWidth: 48, + fixed: 'left', + columnWidth: LC_LIST_TABLE_EXPAND_WIDTH, expandedRowRender: renderExpandedVehicleTable, rowExpandable: function (record) { return (record.vehicles || []).length > 0; @@ -2308,9 +2486,9 @@ const Component = function(props) { rowKey: function(_, i) { return String(i); }, size: 'small', columns: [ - { title: '被授权人', key: 'name', width: 140, render: function(_, row, index) { return React.createElement(Input, { value: row.name, onChange: function(e) { updateAuthorizedRow(index, 'name', e.target.value); }, placeholder: '请输入' }); } }, - { title: '被授权人联系电话', key: 'phone', width: 160, render: function(_, row, index) { return React.createElement(Input, { value: row.phone, onChange: function(e) { updateAuthorizedRow(index, 'phone', e.target.value); }, placeholder: '请输入' }); } }, - { title: '被授权人身份证', key: 'idCard', width: 200, render: function(_, row, index) { return React.createElement(Input, { value: row.idCard, onChange: function(e) { updateAuthorizedRow(index, 'idCard', e.target.value); }, placeholder: '请输入' }); } }, + { title: '受托人', key: 'name', width: 140, render: function(_, row, index) { return React.createElement(Input, { value: row.name, onChange: function(e) { updateAuthorizedRow(index, 'name', e.target.value); }, placeholder: '请输入' }); } }, + { title: '受托人联系电话', key: 'phone', width: 160, render: function(_, row, index) { return React.createElement(Input, { value: row.phone, onChange: function(e) { updateAuthorizedRow(index, 'phone', e.target.value); }, placeholder: '请输入' }); } }, + { title: '受托人身份证', key: 'idCard', width: 200, render: function(_, row, index) { return React.createElement(Input, { value: row.idCard, onChange: function(e) { updateAuthorizedRow(index, 'idCard', e.target.value); }, placeholder: '请输入' }); } }, { title: '操作', key: 'action', width: 80, render: function(_, row, index) { return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeAuthorizedRow(index); } }, '删除'); } } ], dataSource: _authorizedList[0], diff --git a/src/prototypes/lease-contract-management/annotation-source.json b/src/prototypes/lease-contract-management/annotation-source.json index aa86d9a..53e4b45 100644 --- a/src/prototypes/lease-contract-management/annotation-source.json +++ b/src/prototypes/lease-contract-management/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "lease-contract-management", "pageId": "list", - "updatedAt": 1782810809220, + "updatedAt": 1782884134073, "nodes": [ { "id": "lc-list-filter", @@ -48,8 +48,28 @@ "updatedAt": 1740787200000 }, { - "id": "lc-list-toolbar", + "id": "lc-fleet-summary", "index": 3, + "title": "在租车辆概览", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"lc-fleet-summary\"]" + ], + "fingerprint": "lc-fleet-summary", + "path": [] + }, + "aiPrompt": "按品牌型号统计在租车辆,支持品牌标签筛选。", + "annotationText": "顶部展示在租总数;品牌标签筛选车型卡片;点击卡片查看车辆明细。", + "hasMarkdown": true, + "color": "#32a06e", + "images": [], + "createdAt": 1740787200000, + "updatedAt": 1740787200000 + }, + { + "id": "lc-list-toolbar", + "index": 4, "title": "列表工具栏", "pageId": "list", "locator": { @@ -69,7 +89,7 @@ }, { "id": "lc-list-table", - "index": 4, + "index": 5, "title": "合同台账列表", "pageId": "list", "locator": { @@ -89,7 +109,7 @@ }, { "id": "lc-create-main-contract", - "index": 5, + "index": 6, "title": "主体合同信息", "pageId": "create", "locator": { @@ -109,7 +129,7 @@ }, { "id": "lc-create-lease-order", - "index": 6, + "index": 7, "title": "附件1:租赁订单", "pageId": "create", "locator": { @@ -129,7 +149,7 @@ }, { "id": "lc-create-poa", - "index": 7, + "index": 8, "title": "授权委托书", "pageId": "create", "locator": { @@ -139,7 +159,7 @@ "fingerprint": "lc-create-poa", "path": [] }, - "aiPrompt": "被委托人列表编辑。", + "aiPrompt": "受托人列表编辑。", "annotationText": "至少一行且姓名/联系方式/身份证完整。", "hasMarkdown": true, "color": "#7c3aed", @@ -149,7 +169,7 @@ }, { "id": "lc-create-preview", - "index": 8, + "index": 9, "title": "实时预览", "pageId": "create", "locator": { @@ -160,7 +180,7 @@ "path": [] }, "aiPrompt": "Word 版式预览与可编辑正文。", - "annotationText": "改动风控红线条款将触发非标准合同审批。", + "annotationText": "改动风控红线条款将触发非标准合同。", "hasMarkdown": true, "color": "#0ea5e9", "images": [], @@ -169,7 +189,7 @@ }, { "id": "lc-create-footer", - "index": 9, + "index": 10, "title": "底栏操作", "pageId": "create", "locator": { @@ -190,211 +210,20 @@ ] }, "markdownMap": { - "lc-list-filter": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---", + "lc-list-filter": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 审批类型 | 多选下拉 | 全部 / 标准合同 / 非标准合同 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---", "lc-list-kpi": "## 5. KPI 统计卡片\n\n共 5 张卡片,点击切换列表筛选(与筛选区叠加生效)。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部合同 | 不做 KPI 维度过滤 |\n| 进行中 | 合同状态为「合同进行中」且结束日期 ≥ 今日 |\n| 已终止 | 合同状态为「已终止」或「已结束」 |\n| 临期合同 | 距离合同结束日期 ≤ 30 天且未过期 |\n| 过期合同 | 合同状态为「已过期」或「到期合同」 |\n\n卡片右上角 `?` 悬停展示口径说明。\n\n---", + "lc-fleet-summary": "# 在租车辆概览\n\n按品牌型号统计在租车辆,支持品牌标签筛选。\n\n## 说明\n\n顶部展示在租总数;品牌标签筛选车型卡片;点击卡片查看车辆明细。", "lc-list-toolbar": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 租赁费用模板 | 跳转或打开费用模板配置(本期原型 Toast) |\n| 导出 | 按当前筛选结果导出(本期原型 Toast) |\n| 新增 | 进入「新增租赁合同」页 |\n\n---", - "lc-list-table": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回;上方状态、下方当前审批人(通过/驳回/撤回不显示);待审批/审批中悬停展示审批流 |\n| 合同状态 | 仅保存未提交审批为草稿;提交审批后为已提交审批;另有变更 / 合同进行中 / 到期合同 / 已结束等 |\n| 签约公司 | 合同甲方名称全称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 待审批或审批中 | 二次确认后审批状态变更为撤回,合同状态回草稿 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传", + "lc-list-table": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批类型 | 标准合同 / 非标准合同;位于审批状态左侧 |\n| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |\n| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 签约公司 | 创建时所选甲方签约主体简称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 被授权人 | 显示人数;点击弹出卡片查看姓名、联系方式、身份证号 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 最后更新 | 更新人 / 最后更新时间,无则显示 `-` |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 已提交审批且非驳回 | 二次确认后撤回 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传", "lc-create-main-contract": "## 2. 主体合同信息\n\n卡片内三个可折叠区块,标题行右侧展示区块说明(展开时显示在标题后方)。\n\n### 2.1 签约信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 合同编号 | 是 | 前缀 `HT-ZL-` + 用户输入 |\n| 甲方 | 是 | 出租方签约主体,来自组织机构 |\n| 乙方 | 是 | 客户选择器;仅 A/B 级客户可选 |\n\n选择甲方/乙方后,下方只读面板自动带出档案:\n\n**甲方**:户名、开户行、账号、通讯地址、联系人、电话、邮箱。\n\n**乙方**:企业名称、开户银行、银行账号、纳税人识别号、企业地址、企业电话、通讯地址、联系人、电话、邮箱;资质证照缩略图可点击放大。\n\n未选双方时展示空状态引导。\n\n### 2.2 里程标准\n\n对应合同正文最低里程与租金减免条款。\n\n| 字段 | 说明 |\n|---|---|\n| 是否有最低里程要求 | 开关 |\n| 统计周期 | 有要求时必选:月 / 季 / 年 |\n| 目标里程 | 有要求时必填(公里) |\n| 里程类型 | 多选标签:实际里程 / 仪表里程 等 |\n\n### 2.3 费用信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 付款周期 | 是 | 如月付、季付 |\n| 氢费支付方式 | 是 | 后付 / 预付 |\n| 预付金额 | 条件 | 预付时必填 |\n| 提前付款工作日 | 条件 | 预付时必填 |\n| 还车氢量差单价 | 是 | 元 |\n\n**完成度**:上述必填项均已填则卡片显示「已完成」。\n\n---", "lc-create-lease-order": "## 3. 附件1:租赁订单\n\n| 区域 | 说明 |\n|---|---|\n| 投保车辆数 / 三者责任险(百万) | 三者责任险必填 |\n| 交车区域 | 省市区级联 |\n| 计划交车日期 | 日期 |\n| 订单车辆表 | 品牌型号级联、车牌、租期、租金、氢费、增值服务等 |\n\n支持新增行、复制行、删除行;表头 KPI 展示合计。\n\n**完成度**:三者责任险已填即视为完成(原型简化口径)。\n\n---", - "lc-create-poa": "## 4. 授权委托书\n\n被委托人表格:姓名、联系方式、身份证号。\n\n支持新增行、删除行。\n\n**完成度**:至少一行且三项均非空。\n\n---", - "lc-create-preview": "## 5. 实时预览\n\n- 基于标准合同模板 V26.6 拼装 HTML,A4 分页展示。\n- 支持缩放(50%—150%)、连续预览 / 单页预览切换。\n- **可编辑预览**:合同正文 `contentEditable`;用户可直接改字。\n- 顶栏展示当前 **审批类型** 徽章。\n\n### 5.1 占位符替换\n\n左侧表单变更时,预览内 `{{contractCode}}`、`{{customerName}}` 等变量实时替换。\n\n### 5.2 条件条款与附件章节\n\n按租赁订单车辆品牌型号,过滤模板中条件条款与附件章节绑定(与合同模板管理规则一致)。\n\n---\n\n---\n\n## 6. 审批类型判定\n\n| 类型 | 触发条件 |\n|---|---|\n| 标准合同审批 | 默认可编辑预览相对基线未改动风控红线条款 |\n| 非标准合同审批 | 预览中 `.ct-risk-redline[data-risk-redline=\"1\"]` 正文相对基线有增删改 |\n\n检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。\n\n保存草稿 / 提交审核时 Toast 展示将走的审批类型(原型)。\n\n---", + "lc-create-poa": "## 4. 授权委托书\n\n被委托人表格:姓名、联系方式、身份证号。\n\n支持新增行、删除行。\n\n**合同备注**:授权委托书区块最下方提供文本域,支持自定义输入,非必填;列表页不再展示备注列。\n\n**完成度**:至少一行且三项均非空。\n\n---", + "lc-create-preview": "## 5. 实时预览\n\n- 基于标准合同模板 V26.6 拼装 HTML,A4 分页展示。\n- 支持缩放(50%—150%)、连续预览 / 单页预览切换。\n- **可编辑预览**:合同正文 `contentEditable`;用户可直接改字。\n- 顶栏展示当前 **审批类型** 徽章。\n\n### 5.1 占位符替换\n\n左侧表单变更时,预览内 `{{contractCode}}`、`{{customerName}}` 等变量实时替换。\n\n### 5.2 条件条款与附件章节\n\n按租赁订单车辆品牌型号,过滤模板中条件条款与附件章节绑定(与合同模板管理规则一致)。\n\n---\n\n---\n\n## 6. 审批类型判定\n\n| 类型 | 触发条件 |\n|---|---|\n| 标准合同 | 默认可编辑预览相对基线未改动风控红线条款 |\n| 非标准合同 | 预览中 `.ct-risk-redline[data-risk-redline=\"1\"]` 正文相对基线有增删改 |\n\n检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。\n\n保存草稿 / 提交审核时 Toast 展示将走的审批类型(原型)。\n\n---", "lc-create-footer": "## 7. 底栏操作\n\n| 按钮 | 行为 |\n|---|---|\n| 取消 / 返回列表 | 不保存,回列表 |\n| 保存草稿 | 保存为草稿,审批类型随检测结果记录 |\n| 提交审核 | 校验必填后发起审批流程 |\n\n---" }, "assetMap": {}, "directory": { "nodes": [ - { - "type": "folder", - "id": "oneos-project-nav", - "title": "ONE-OS 原型导航", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-insurance-procurement", - "title": "保险采购", - "href": "/prototypes/insurance-procurement", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-weekly-report", - "title": "数智部周报", - "href": "/prototypes/oneos-weekly-report", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-vehicle-management", - "title": "车辆管理", - "href": "/prototypes/vehicle-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-contract-template-management", - "title": "合同模板管理", - "href": "/prototypes/contract-template-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-lease-contract-management", - "title": "租赁合同管理(当前)", - "href": "/prototypes/lease-contract-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-customer-management", - "title": "客户管理", - "href": "/prototypes/customer-management", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-lease-business-ledger", - "title": "租赁业务台账", - "href": "/prototypes/lease-business-ledger", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-vehicle-h2-fee-ledger", - "title": "车辆氢费明细", - "href": "/prototypes/vehicle-h2-fee-ledger", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-self-operated-business-ledger", - "title": "自营业务台账", - "href": "/prototypes/self-operated-business-ledger", - "target": "self" - }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, - { - "type": "folder", - "id": "nav-folder-prototypes-oneos-web", - "title": "ONE-OS Web 端", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-workbench", - "title": "工作台", - "href": "/prototypes/oneos-web-workbench", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-login", - "title": "登录", - "href": "/prototypes/oneos-web-login", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-vehicle-asset", - "title": "车辆管理", - "href": "/prototypes/oneos-web-vehicle-asset", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-contract-template", - "title": "合同模板管理", - "href": "/prototypes/oneos-web-contract-template", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-help-center", - "title": "帮助中心", - "href": "/prototypes/oneos-web-help-center", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-finance", - "title": "财务管理", - "href": "/prototypes/oneos-web-finance", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-procurement", - "title": "采购管理", - "href": "/prototypes/oneos-web-procurement", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-lease-contract", - "title": "车辆租赁合同", - "href": "/prototypes/oneos-web-lease-contract", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-h2-station", - "title": "加氢站管理", - "href": "/prototypes/oneos-web-h2-station", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-data-analysis", - "title": "数据分析", - "href": "/prototypes/oneos-web-data-analysis", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-ledger-data", - "title": "台账数据", - "href": "/prototypes/oneos-web-ledger-data", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-business", - "title": "业务管理", - "href": "/prototypes/oneos-web-business", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-ops", - "title": "运维管理", - "href": "/prototypes/oneos-web-ops", - "target": "self" - }, - { - "type": "link", - "id": "nav-link-oneos-web-requirements", - "title": "需求说明", - "href": "/prototypes/oneos-web-requirements", - "target": "self" - } - ] - } - ] - }, { "type": "folder", "id": "lc-doc-root", @@ -411,7 +240,7 @@ "type": "link", "id": "lc-link-template", "title": "合同模板管理", - "href": "../contract-template-management/index.html", + "href": "/prototypes/contract-template-management", "target": "self" }, { @@ -431,14 +260,14 @@ "type": "markdown", "id": "lc-doc-prd-list", "title": "列表页 PRD", - "markdown": "# 租赁合同管理 · 列表页 PRD\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 租赁合同管理 |\n| 所属系统 | ONE-OS 业务管理 |\n| 面包屑 | 业务管理 → 租赁合同管理 |\n| 目标用户 | 业务人员(主)、法务(盖章上传)、客服/运营(查看) |\n| 核心任务 | 筛选与查看租赁合同台账,发起新增/变更/续签等流程 |\n| 交互原型 | `/prototypes/lease-contract-management` |\n\n---\n\n## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---\n\n## 5. KPI 统计卡片\n\n共 5 张卡片,点击切换列表筛选(与筛选区叠加生效)。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部合同 | 不做 KPI 维度过滤 |\n| 进行中 | 合同状态为「合同进行中」且结束日期 ≥ 今日 |\n| 已终止 | 合同状态为「已终止」或「已结束」 |\n| 临期合同 | 距离合同结束日期 ≤ 30 天且未过期 |\n| 过期合同 | 合同状态为「已过期」或「到期合同」 |\n\n卡片右上角 `?` 悬停展示口径说明。\n\n---\n\n## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 租赁费用模板 | 跳转或打开费用模板配置(本期原型 Toast) |\n| 导出 | 按当前筛选结果导出(本期原型 Toast) |\n| 新增 | 进入「新增租赁合同」页 |\n\n---\n\n## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回;上方状态、下方当前审批人(通过/驳回/撤回不显示);待审批/审批中悬停展示审批流 |\n| 合同状态 | 仅保存未提交审批为草稿;提交审批后为已提交审批;另有变更 / 合同进行中 / 到期合同 / 已结束等 |\n| 签约公司 | 合同甲方名称全称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 待审批或审批中 | 二次确认后审批状态变更为撤回,合同状态回草稿 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传\n", + "markdown": "# 租赁合同管理 · 列表页 PRD\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 租赁合同管理 |\n| 所属系统 | ONE-OS 业务管理 |\n| 面包屑 | 业务管理 → 租赁合同管理 |\n| 目标用户 | 业务人员(主)、法务(盖章上传)、客服/运营(查看) |\n| 核心任务 | 筛选与查看租赁合同台账,发起新增/变更/续签等流程 |\n| 交互原型 | `/prototypes/lease-contract-management` |\n\n---\n\n## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 审批类型 | 多选下拉 | 全部 / 标准合同 / 非标准合同 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---\n\n## 5. KPI 统计卡片\n\n共 5 张卡片,点击切换列表筛选(与筛选区叠加生效)。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部合同 | 不做 KPI 维度过滤 |\n| 进行中 | 合同状态为「合同进行中」且结束日期 ≥ 今日 |\n| 已终止 | 合同状态为「已终止」或「已结束」 |\n| 临期合同 | 距离合同结束日期 ≤ 30 天且未过期 |\n| 过期合同 | 合同状态为「已过期」或「到期合同」 |\n\n卡片右上角 `?` 悬停展示口径说明。\n\n---\n\n## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 租赁费用模板 | 跳转或打开费用模板配置(本期原型 Toast) |\n| 导出 | 按当前筛选结果导出(本期原型 Toast) |\n| 新增 | 进入「新增租赁合同」页 |\n\n---\n\n## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批类型 | 标准合同 / 非标准合同;位于审批状态左侧 |\n| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |\n| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 签约公司 | 创建时所选甲方签约主体简称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 被授权人 | 显示人数;点击弹出卡片查看姓名、联系方式、身份证号 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 最后更新 | 更新人 / 最后更新时间,无则显示 `-` |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 已提交审批且非驳回 | 二次确认后撤回 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传\n", "markdownPath": "src/prototypes/lease-contract-management/.spec/requirements-prd-list.md" }, { "type": "markdown", "id": "lc-doc-prd-create", "title": "新增页 PRD", - "markdown": "# 租赁合同管理 · 新增/编辑页 PRD\n\n## 1. 页面结构\n\n左右分栏工作区:\n\n```text\n新增租赁合同\n├── 顶栏:返回列表\n├── 左侧编辑列(可滚动)\n│ ├── 主体合同信息(完成度徽章)\n│ ├── 附件1:租赁订单\n│ └── 授权委托书\n├── 右侧:实时预览(V26.6 完整合同 + 审批类型徽章)\n└── 底栏:取消 / 保存草稿 / 提交审核\n```\n\n编辑页与新增页表单结构一致;编辑时回填已保存数据。\n\n---\n\n## 2. 主体合同信息\n\n卡片内三个可折叠区块,标题行右侧展示区块说明(展开时显示在标题后方)。\n\n### 2.1 签约信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 合同编号 | 是 | 前缀 `HT-ZL-` + 用户输入 |\n| 甲方 | 是 | 出租方签约主体,来自组织机构 |\n| 乙方 | 是 | 客户选择器;仅 A/B 级客户可选 |\n\n选择甲方/乙方后,下方只读面板自动带出档案:\n\n**甲方**:户名、开户行、账号、通讯地址、联系人、电话、邮箱。\n\n**乙方**:企业名称、开户银行、银行账号、纳税人识别号、企业地址、企业电话、通讯地址、联系人、电话、邮箱;资质证照缩略图可点击放大。\n\n未选双方时展示空状态引导。\n\n### 2.2 里程标准\n\n对应合同正文最低里程与租金减免条款。\n\n| 字段 | 说明 |\n|---|---|\n| 是否有最低里程要求 | 开关 |\n| 统计周期 | 有要求时必选:月 / 季 / 年 |\n| 目标里程 | 有要求时必填(公里) |\n| 里程类型 | 多选标签:实际里程 / 仪表里程 等 |\n\n### 2.3 费用信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 付款周期 | 是 | 如月付、季付 |\n| 氢费支付方式 | 是 | 后付 / 预付 |\n| 预付金额 | 条件 | 预付时必填 |\n| 提前付款工作日 | 条件 | 预付时必填 |\n| 还车氢量差单价 | 是 | 元 |\n\n**完成度**:上述必填项均已填则卡片显示「已完成」。\n\n---\n\n## 3. 附件1:租赁订单\n\n| 区域 | 说明 |\n|---|---|\n| 投保车辆数 / 三者责任险(百万) | 三者责任险必填 |\n| 交车区域 | 省市区级联 |\n| 计划交车日期 | 日期 |\n| 订单车辆表 | 品牌型号级联、车牌、租期、租金、氢费、增值服务等 |\n\n支持新增行、复制行、删除行;表头 KPI 展示合计。\n\n**完成度**:三者责任险已填即视为完成(原型简化口径)。\n\n---\n\n## 4. 授权委托书\n\n被委托人表格:姓名、联系方式、身份证号。\n\n支持新增行、删除行。\n\n**完成度**:至少一行且三项均非空。\n\n---\n\n## 5. 实时预览\n\n- 基于标准合同模板 V26.6 拼装 HTML,A4 分页展示。\n- 支持缩放(50%—150%)、连续预览 / 单页预览切换。\n- **可编辑预览**:合同正文 `contentEditable`;用户可直接改字。\n- 顶栏展示当前 **审批类型** 徽章。\n\n### 5.1 占位符替换\n\n左侧表单变更时,预览内 `{{contractCode}}`、`{{customerName}}` 等变量实时替换。\n\n### 5.2 条件条款与附件章节\n\n按租赁订单车辆品牌型号,过滤模板中条件条款与附件章节绑定(与合同模板管理规则一致)。\n\n---\n\n## 6. 审批类型判定\n\n| 类型 | 触发条件 |\n|---|---|\n| 标准合同审批 | 默认可编辑预览相对基线未改动风控红线条款 |\n| 非标准合同审批 | 预览中 `.ct-risk-redline[data-risk-redline=\"1\"]` 正文相对基线有增删改 |\n\n检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。\n\n保存草稿 / 提交审核时 Toast 展示将走的审批类型(原型)。\n\n---\n\n## 7. 底栏操作\n\n| 按钮 | 行为 |\n|---|---|\n| 取消 / 返回列表 | 不保存,回列表 |\n| 保存草稿 | 保存为草稿,审批类型随检测结果记录 |\n| 提交审核 | 校验必填后发起审批流程 |\n\n---\n\n## 8. 与合同模板的关系\n\n- 创建时默认引用 **标准合同模板**(合同模板管理 `isDefault=true` 的已发布版本)。\n- 模板内风控红线条款、条件条款、锁定区标记在预览中保留。\n- 业务侧仅编辑表单字段与允许编辑的预览正文;锁定区不可改(正式环境由编辑器约束)。\n", + "markdown": "# 租赁合同管理 · 新增/编辑页 PRD\n\n## 1. 页面结构\n\n左右分栏工作区:\n\n```text\n新增租赁合同\n├── 顶栏:返回列表\n├── 左侧编辑列(可滚动)\n│ ├── 主体合同信息(完成度徽章)\n│ ├── 附件1:租赁订单\n│ └── 授权委托书\n├── 右侧:实时预览(V26.6 完整合同 + 审批类型徽章)\n└── 底栏:取消 / 保存草稿 / 提交审核\n```\n\n编辑页与新增页表单结构一致;编辑时回填已保存数据。\n\n---\n\n## 2. 主体合同信息\n\n卡片内三个可折叠区块,标题行右侧展示区块说明(展开时显示在标题后方)。\n\n### 2.1 签约信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 合同编号 | 是 | 前缀 `HT-ZL-` + 用户输入 |\n| 甲方 | 是 | 出租方签约主体,来自组织机构 |\n| 乙方 | 是 | 客户选择器;仅 A/B 级客户可选 |\n\n选择甲方/乙方后,下方只读面板自动带出档案:\n\n**甲方**:户名、开户行、账号、通讯地址、联系人、电话、邮箱。\n\n**乙方**:企业名称、开户银行、银行账号、纳税人识别号、企业地址、企业电话、通讯地址、联系人、电话、邮箱;资质证照缩略图可点击放大。\n\n未选双方时展示空状态引导。\n\n### 2.2 里程标准\n\n对应合同正文最低里程与租金减免条款。\n\n| 字段 | 说明 |\n|---|---|\n| 是否有最低里程要求 | 开关 |\n| 统计周期 | 有要求时必选:月 / 季 / 年 |\n| 目标里程 | 有要求时必填(公里) |\n| 里程类型 | 多选标签:实际里程 / 仪表里程 等 |\n\n### 2.3 费用信息\n\n| 字段 | 必填 | 说明 |\n|---|---|:---:|\n| 付款周期 | 是 | 如月付、季付 |\n| 氢费支付方式 | 是 | 后付 / 预付 |\n| 预付金额 | 条件 | 预付时必填 |\n| 提前付款工作日 | 条件 | 预付时必填 |\n| 还车氢量差单价 | 是 | 元 |\n\n**完成度**:上述必填项均已填则卡片显示「已完成」。\n\n---\n\n## 3. 附件1:租赁订单\n\n| 区域 | 说明 |\n|---|---|\n| 投保车辆数 / 三者责任险(百万) | 三者责任险必填 |\n| 交车区域 | 省市区级联 |\n| 计划交车日期 | 日期 |\n| 订单车辆表 | 品牌型号级联、车牌、租期、租金、氢费、增值服务等 |\n\n支持新增行、复制行、删除行;表头 KPI 展示合计。\n\n**完成度**:三者责任险已填即视为完成(原型简化口径)。\n\n---\n\n## 4. 授权委托书\n\n被委托人表格:姓名、联系方式、身份证号。\n\n支持新增行、删除行。\n\n**合同备注**:授权委托书区块最下方提供文本域,支持自定义输入,非必填;列表页不再展示备注列。\n\n**完成度**:至少一行且三项均非空。\n\n---\n\n## 5. 实时预览\n\n- 基于标准合同模板 V26.6 拼装 HTML,A4 分页展示。\n- 支持缩放(50%—150%)、连续预览 / 单页预览切换。\n- **可编辑预览**:合同正文 `contentEditable`;用户可直接改字。\n- 顶栏展示当前 **审批类型** 徽章。\n\n### 5.1 占位符替换\n\n左侧表单变更时,预览内 `{{contractCode}}`、`{{customerName}}` 等变量实时替换。\n\n### 5.2 条件条款与附件章节\n\n按租赁订单车辆品牌型号,过滤模板中条件条款与附件章节绑定(与合同模板管理规则一致)。\n\n---\n\n## 6. 审批类型判定\n\n| 类型 | 触发条件 |\n|---|---|\n| 标准合同 | 默认可编辑预览相对基线未改动风控红线条款 |\n| 非标准合同 | 预览中 `.ct-risk-redline[data-risk-redline=\"1\"]` 正文相对基线有增删改 |\n\n检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。\n\n保存草稿 / 提交审核时 Toast 展示将走的审批类型(原型)。\n\n---\n\n## 7. 底栏操作\n\n| 按钮 | 行为 |\n|---|---|\n| 取消 / 返回列表 | 不保存,回列表 |\n| 保存草稿 | 保存为草稿,审批类型随检测结果记录 |\n| 提交审核 | 校验必填后发起审批流程 |\n\n---\n\n## 8. 与合同模板的关系\n\n- 创建时默认引用 **标准合同模板**(合同模板管理 `isDefault=true` 的已发布版本)。\n- 模板内风控红线条款、条件条款、锁定区标记在预览中保留。\n- 业务侧仅编辑表单字段与允许编辑的预览正文;锁定区不可改(正式环境由编辑器约束)。\n", "markdownPath": "src/prototypes/lease-contract-management/.spec/requirements-prd-create.md" }, { @@ -468,7 +297,7 @@ "type": "markdown", "id": "lc-doc-node-lc-list-filter", "title": "筛选条件", - "markdown": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 合同审批类型 | 多选下拉 | 全部 / 标准合同审批 / 非标准合同审批 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---" + "markdown": "## 4. 筛选区\n\n默认展示 **第一行 4 个筛选项**(3 列栅格),点击「展开更多筛选项」显示全部 12 项;支持「重置」「查询」。\n\n| 字段 | 控件 | 说明 |\n|---|---|---|\n| 合同编码 | 可搜索下拉 | 模糊匹配合同编码 |\n| 项目名称 | 可搜索下拉 | 模糊匹配项目名称 |\n| 客户名称 | 可搜索下拉 | 模糊匹配客户名称 |\n| 签约公司 | 下拉 | 租赁合同签约公司枚举 |\n| 审批状态 | 多选下拉 | 全部 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交 |\n| 合同状态 | 多选下拉 | 全部 / 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 业务部门 | 多选下拉 | 业务相关部门 |\n| 业务负责人 | 多选下拉 | 业务组人员 |\n| 合同类型 | 多选下拉 | 全部 / 正式合同 / 试用合同 |\n| 审批类型 | 多选下拉 | 全部 / 标准合同 / 非标准合同 |\n| 创建人 | 多选下拉 | 业务相关人员 |\n| 合同结束日期 | 日期范围 | 开始—结束,精确至日 |\n\n**交互**:\n\n- **查询**:将当前筛选条件写入「已应用筛选」,列表与 KPI 统计联动。\n- **重置**:清空为默认(多选类默认「全部」),并刷新列表。\n\n---" }, { "type": "markdown", @@ -476,6 +305,12 @@ "title": "KPI 统计卡片", "markdown": "## 5. KPI 统计卡片\n\n共 5 张卡片,点击切换列表筛选(与筛选区叠加生效)。\n\n| 卡片 | 口径 |\n|---|---|\n| 全部合同 | 不做 KPI 维度过滤 |\n| 进行中 | 合同状态为「合同进行中」且结束日期 ≥ 今日 |\n| 已终止 | 合同状态为「已终止」或「已结束」 |\n| 临期合同 | 距离合同结束日期 ≤ 30 天且未过期 |\n| 过期合同 | 合同状态为「已过期」或「到期合同」 |\n\n卡片右上角 `?` 悬停展示口径说明。\n\n---" }, + { + "type": "markdown", + "id": "lc-doc-node-lc-fleet-summary", + "title": "在租车辆概览", + "markdown": "# 在租车辆概览\n\n按品牌型号统计在租车辆,支持品牌标签筛选。\n\n## 说明\n\n顶部展示在租总数;品牌标签筛选车型卡片;点击卡片查看车辆明细。" + }, { "type": "markdown", "id": "lc-doc-node-lc-list-toolbar", @@ -486,7 +321,7 @@ "type": "markdown", "id": "lc-doc-node-lc-list-table", "title": "合同台账列表", - "markdown": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批状态 | 未提交 / 待审批 / 审批中 / 审批通过 / 审批驳回 / 撤回;上方状态、下方当前审批人(通过/驳回/撤回不显示);待审批/审批中悬停展示审批流 |\n| 合同状态 | 仅保存未提交审批为草稿;提交审批后为已提交审批;另有变更 / 合同进行中 / 到期合同 / 已结束等 |\n| 签约公司 | 合同甲方名称全称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 合同审批类型 | 标准合同审批 / 非标准合同审批 |\n| 合同结束日期 | YYYY-MM-DD |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 更新人 / 最后更新时间 | 无则显示 `-` |\n| 备注 | 超长省略 |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 待审批或审批中 | 二次确认后审批状态变更为撤回,合同状态回草稿 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传" + "markdown": "## 7. 列表字段说明\n\n### 7.1 项目信息(合并列)\n\n一列展示三行:\n\n1. 项目名称(主文案)\n2. 合同编码(次级)\n3. 客户名称(次级)\n\n点击整格跳转「查看」页(原型 Toast)。\n\n### 7.2 车辆与交车\n\n| 列 | 说明 |\n|---|---|\n| 车辆数 | 租赁订单车辆总数;点击弹出车辆明细表 |\n| 已交车 | 已有实际交车日期的车辆数;点击弹出已交车列表 |\n\n车辆明细字段:车辆类型、品牌、型号、车牌号、实际交车日期、交车人。\n\n### 7.3 状态与业务字段\n\n| 列 | 说明 |\n|---|---|\n| 审批类型 | 标准合同 / 非标准合同;位于审批状态左侧 |\n| 审批状态 | 待审批 / 审批中 / 审批通过 / 审批驳回 / 未提交;「审批中」悬停展示审批流节点 |\n| 合同状态 | 草稿 / 变更 / 合同进行中 / 到期合同 / 已提交审批 / 已结束 |\n| 签约公司 | 创建时所选甲方签约主体简称 |\n| 业务部门 / 业务负责人 | 创建时选取 |\n| 合同类型 | 正式合同 / 试用合同 |\n| 客户联系人 / 联系电话 | 来自客户档案 |\n| 被授权人 | 显示人数;点击弹出卡片查看姓名、联系方式、身份证号 |\n| 创建人 / 创建时间 | 精确至分钟 |\n| 最后更新 | 更新人 / 最后更新时间,无则显示 `-` |\n\n**分页**:每页 10 / 20 / 50 条可选。\n\n---\n\n---\n\n## 8. 操作与弹窗\n\n### 8.1 操作列(主按钮 + 更多)\n\n| 操作 | 显示条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 跳转合同查看页 |\n| 编辑 | 草稿;或审批驳回 | 进入编辑页 |\n| 删除合同 | 草稿 | 二次确认后删除 |\n| 撤回合同 | 已提交审批且非驳回 | 二次确认后撤回 |\n| 新增车辆 | 合同进行中 | 租赁订单内增车并重新审批 |\n| 续签合同 | 进行中 / 到期 | 跳转续签页 |\n| 添加被授权人 | 合同进行中 | 弹窗编辑被授权人列表 |\n| 附加费用 | 合同进行中 | 弹窗按车辆录入附加费用 |\n| 变更为三方合同 | 合同进行中 | 跳转变更页 |\n| 转正式合同 | 试用合同 | 跳转转正式页 |\n| 终止合同 | 合同进行中 | 二次确认,提交终止审批 |\n| 上传盖章合同 | 审批通过且法务未上传 | 仅法务部可见;多文件上传后关闭入口 |\n\n### 8.2 审批状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 未提交 | 仅保存草稿 |\n| 待审批 | 已提交,尚无节点通过 |\n| 审批中 | 已有节点通过,未完成终审 |\n| 审批通过 | 终审通过 |\n| 审批驳回 | 任一节点的驳回;可编辑重提 |\n\n### 8.3 合同状态口径\n\n| 状态 | 定义 |\n|---|---|\n| 草稿 | 未提交审批 |\n| 已提交审批 | 初次提交,终审未完成 |\n| 变更 | 已通过基础上变更并重新审批中 |\n| 合同进行中 | 终审通过且在有效期内 |\n| 到期合同 | 已过结束日期 |\n| 已结束 | 终止或自然结束 |\n\n### 8.4 弹窗摘要\n\n- **删除草稿**:「是否确认删除该合同草稿」\n- **撤回**:「是否确认撤回该合同」\n- **终止**:「是否确认终止合同」→ 提交审核\n- **添加被授权人**:姓名、电话、身份证;支持增删行\n- **附加费用**:按车辆展示类型/品牌/型号/车牌,可填服务项目、费用(元,2 位小数)、生效日期\n- **上传盖章合同**:拖拽多文件;确认后标记已上传" } ] }, @@ -521,13 +356,13 @@ "type": "markdown", "id": "lc-doc-node-lc-create-poa", "title": "授权委托书", - "markdown": "## 4. 授权委托书\n\n被委托人表格:姓名、联系方式、身份证号。\n\n支持新增行、删除行。\n\n**完成度**:至少一行且三项均非空。\n\n---" + "markdown": "## 4. 授权委托书\n\n被委托人表格:姓名、联系方式、身份证号。\n\n支持新增行、删除行。\n\n**合同备注**:授权委托书区块最下方提供文本域,支持自定义输入,非必填;列表页不再展示备注列。\n\n**完成度**:至少一行且三项均非空。\n\n---" }, { "type": "markdown", "id": "lc-doc-node-lc-create-preview", "title": "实时预览", - "markdown": "## 5. 实时预览\n\n- 基于标准合同模板 V26.6 拼装 HTML,A4 分页展示。\n- 支持缩放(50%—150%)、连续预览 / 单页预览切换。\n- **可编辑预览**:合同正文 `contentEditable`;用户可直接改字。\n- 顶栏展示当前 **审批类型** 徽章。\n\n### 5.1 占位符替换\n\n左侧表单变更时,预览内 `{{contractCode}}`、`{{customerName}}` 等变量实时替换。\n\n### 5.2 条件条款与附件章节\n\n按租赁订单车辆品牌型号,过滤模板中条件条款与附件章节绑定(与合同模板管理规则一致)。\n\n---\n\n---\n\n## 6. 审批类型判定\n\n| 类型 | 触发条件 |\n|---|---|\n| 标准合同审批 | 默认可编辑预览相对基线未改动风控红线条款 |\n| 非标准合同审批 | 预览中 `.ct-risk-redline[data-risk-redline=\"1\"]` 正文相对基线有增删改 |\n\n检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。\n\n保存草稿 / 提交审核时 Toast 展示将走的审批类型(原型)。\n\n---" + "markdown": "## 5. 实时预览\n\n- 基于标准合同模板 V26.6 拼装 HTML,A4 分页展示。\n- 支持缩放(50%—150%)、连续预览 / 单页预览切换。\n- **可编辑预览**:合同正文 `contentEditable`;用户可直接改字。\n- 顶栏展示当前 **审批类型** 徽章。\n\n### 5.1 占位符替换\n\n左侧表单变更时,预览内 `{{contractCode}}`、`{{customerName}}` 等变量实时替换。\n\n### 5.2 条件条款与附件章节\n\n按租赁订单车辆品牌型号,过滤模板中条件条款与附件章节绑定(与合同模板管理规则一致)。\n\n---\n\n---\n\n## 6. 审批类型判定\n\n| 类型 | 触发条件 |\n|---|---|\n| 标准合同 | 默认可编辑预览相对基线未改动风控红线条款 |\n| 非标准合同 | 预览中 `.ct-risk-redline[data-risk-redline=\"1\"]` 正文相对基线有增删改 |\n\n检测逻辑:`lease-contract-risk-detect.js` 对比基线 HTML 与当前合并页 HTML。\n\n保存草稿 / 提交审核时 Toast 展示将走的审批类型(原型)。\n\n---" }, { "type": "markdown", diff --git a/src/prototypes/lease-contract-management/lease-contract-list-data.js b/src/prototypes/lease-contract-management/lease-contract-list-data.js index ad9868f..b04605c 100644 --- a/src/prototypes/lease-contract-management/lease-contract-list-data.js +++ b/src/prototypes/lease-contract-management/lease-contract-list-data.js @@ -117,7 +117,7 @@ var CONTRACT_TYPE_FULL_NAME_FALLBACK = { '试用合同': '现代18吨试用合同', }; -var RETURN_SETTLEMENT_STATUSES = ['待提交', '审批中', '已结算']; +var RETURN_SETTLEMENT_STATUSES = ['待提交', '审批中', '已完成']; export function parseContractDateOnly(raw) { if (raw == null || raw === '' || raw === '-') return null; @@ -136,6 +136,26 @@ export function formatContractPeriodStartDate(date) { return y + '-' + m + '-' + d; } +/** 当期里程周期剩余天数(至周期结束日) */ +export function computeMileagePeriodDaysRemaining(periodStartDate, mileagePeriod, referenceDate) { + var end = computeCurrentMileagePeriodEnd(periodStartDate, mileagePeriod); + if (!end) return null; + var ref = referenceDate instanceof Date ? referenceDate : new Date(); + ref.setHours(0, 0, 0, 0); + var endDay = new Date(end.getTime()); + endDay.setHours(0, 0, 0, 0); + return Math.max(0, daysBetween(ref, endDay)); +} + +/** 当前统计周期内已完成里程(当前里程 − 周期起始里程) */ +export function computeVehiclePeriodCompletedMileage(vehicle) { + if (!vehicle) return null; + var current = Number(vehicle.currentMileage); + var start = Number(vehicle.periodStartMileage); + if (!Number.isFinite(current) || !Number.isFinite(start)) return null; + return Math.max(0, Math.round(current - start)); +} + function daysBetween(startDate, endDate) { var ms = endDate.getTime() - startDate.getTime(); return Math.max(0, Math.floor(ms / 86400000)); @@ -186,6 +206,62 @@ export function estimatePeriodStartMileage(deliveryRaw, periodStart, deliveryMil return Math.min(currentMile, Math.max(deliveryMile, startMileage)); } +/** 当前统计周期目标里程(周期起始里程 + 当期里程要求) */ +export function computeVehiclePeriodTargetMileage(record, vehicle) { + if (!record || !vehicle) return null; + var startMileage = Number(vehicle.periodStartMileage); + var targetKm = Number(record.mileageTargetKm); + if (!Number.isFinite(startMileage) || !Number.isFinite(targetKm)) return null; + return Math.round(startMileage + targetKm); +} + +/** 当前里程统计周期的结束日(即下一周期起始日) */ +export function computeCurrentMileagePeriodEnd(periodStart, period) { + var start = periodStart instanceof Date ? periodStart : parseContractDateOnly(periodStart); + if (!start) return null; + if (period === 'month') { + return new Date(start.getFullYear(), start.getMonth() + 1, start.getDate()); + } + if (period === 'quarter') { + return new Date(start.getFullYear(), start.getMonth() + 3, start.getDate()); + } + if (period === 'year') { + return new Date(start.getFullYear() + 1, start.getMonth(), start.getDate()); + } + return null; +} + +export var MILEAGE_FORECAST_GAP_KM = 200; + +export function estimateVehicleDailyAvgMileage7d(vehicle, record) { + if (!vehicle) return 0; + if (vehicle.dailyAvgMileage7d != null && Number.isFinite(Number(vehicle.dailyAvgMileage7d))) { + return Math.max(0, Number(vehicle.dailyAvgMileage7d)); + } + var target = Number(record && record.mileageTargetKm); + var progress = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0)); + if (!Number.isFinite(target) || target <= 0) return 0; + var drivenKm = target * progress / 100; + return Math.max(0, Math.round(drivenKm / 7)); +} + +export function computeVehicleMileageForecastStatus(record, vehicle) { + if (!record || !record.hasMinimumMileage || !vehicle) return null; + if (!isContractVehicleDelivered(vehicle)) return null; + var remainingKm = computeContractVehicleRemainingMileage(record, vehicle); + if (remainingKm == null) return null; + var deliveryRaw = vehicle.deliveryTime || vehicle.actualDelivery; + var periodStart = computeCurrentMileagePeriodStart(deliveryRaw, record.mileagePeriod); + var periodEnd = computeCurrentMileagePeriodEnd(periodStart, record.mileagePeriod); + var daysLeft = periodEnd ? Math.max(0, daysBetween(new Date(), periodEnd)) : 0; + var dailyAvg = estimateVehicleDailyAvgMileage7d(vehicle, record); + var achievableKm = dailyAvg * daysLeft; + if (achievableKm < remainingKm) return '预计无法完成'; + var gapKm = Math.abs(remainingKm - achievableKm); + if (gapKm <= MILEAGE_FORECAST_GAP_KM) return '存在完成风险'; + return '预计可完成'; +} + export function inferContractTemplateId(record) { if (record && record.contractTemplateId) return record.contractTemplateId; var contractType = record && record.contractType; @@ -215,8 +291,32 @@ export function resolveContractTemplateTypeLabel(record) { } export function getReturnSettlementStatusTone(status) { - if (status === '已结算') return 'green'; + if (status === '已完成' || status === '已结算') return 'green'; if (status === '审批中') return 'amber'; + if (status === '去处理' || status === '待提交') return 'blue'; + return 'gray'; +} + +/** 还车应结款内部状态 → 子表展示文案 */ +export function resolveReturnSettlementDisplayLabel(status) { + if (!status || status === '待提交' || status === '待处理') return '去处理'; + if (status === '审批中') return '审批中'; + if (status === '已完成' || status === '已结算') return '已完成'; + return '去处理'; +} + +/** 还车应结款子表点击行为:edit 编辑页 / view 查看页 / null 不可点 */ +export function resolveReturnSettlementCellMode(status) { + var label = resolveReturnSettlementDisplayLabel(status); + if (label === '去处理') return 'edit'; + if (label === '已完成') return 'view'; + return null; +} + +export function getReturnSettlementDisplayTone(displayLabel) { + if (displayLabel === '已完成') return 'green'; + if (displayLabel === '审批中') return 'amber'; + if (displayLabel === '去处理') return 'blue'; return 'gray'; } @@ -288,6 +388,89 @@ export function getAuthorizedDelegates(record) { return list; } +/** 列表「受托人」列是否需提示补充授权委托书与受托人信息 */ +export function needsContractDelegateSupplement(record) { + if (!record) return false; + if (record.delegateSupplementRequired === false) return false; + if (record.poaUploaded === true && (record.delegateCount || 0) > 0) return false; + if ((record.delegateCount || 0) > 0 && getAuthorizedDelegates(record).length > 0) return false; + return true; +} + +/** + * 在租车辆概览 — 品牌 / 型号 / 车型用例枚举(与主数据公告型号对齐) + * 卡片按「品牌 + 型号」聚合;飞驰 49 吨牵引车头含 3 个公告型号子项。 + */ +export var ON_LEASE_FLEET_VEHICLE_CASES = [ + { + brand: '现代', + model: '帕力安牌18吨双飞翼货车', + officialModel: '帕力安牌XDQ5180XYKFCEV06', + vehicleType: '重型厢式货车', + }, + { + brand: '楚风', + model: '18吨厢式货车', + officialModel: '楚风牌HQG5180XXYFCEV', + vehicleType: '重型厢式货车', + }, + { + brand: '苏龙', + model: '海格牌18吨双飞翼货车', + officialModel: '海格牌KLQ5180XYKFCEV', + vehicleType: '重型厢式货车', + }, + { + brand: '跃进', + model: '4.5吨冷链车', + officialModel: '跃进牌SH5047XLCZFFCEVMZ1', + vehicleType: '轻型厢式货车', + }, + { + brand: '飞驰', + model: '49吨牵引车头', + officialModel: '飞驰牌FSQ4250SFFCEV', + vehicleType: '重型半挂牵引车', + }, + { + brand: '飞驰', + model: '49吨牵引车头', + officialModel: '飞驰牌FSQ4250SFFCEV6', + vehicleType: '重型半挂牵引车', + }, + { + brand: '飞驰', + model: '49吨牵引车头', + officialModel: '飞驰牌FSQ4250SFFCEV3', + vehicleType: '重型半挂牵引车', + }, +]; + +function buildOnLeaseFleetDemoVehicles(cases) { + var deliveryTimes = [ + '2025-01-08 09:00', + '2025-01-12 10:30', + '2025-01-15 14:00', + '2025-02-01 08:00', + '2025-02-05 11:00', + '2025-02-06 13:20', + '2025-02-08 16:45', + ]; + var plates = ['浙A88001', '浙A88002', '浙A88003', '浙A88004', '浙A88005', '浙A88006', '浙A88007']; + return (cases || []).map(function (item, index) { + return { + brand: item.brand, + model: item.model, + officialModel: item.officialModel, + vehicleType: item.vehicleType, + plateNo: plates[index] || '-', + actualDelivery: deliveryTimes[index] || '2025-02-01 09:00', + deliveryPerson: '车队运维', + leasePeriodMonths: 24, + }; + }); +} + export var LEASE_CONTRACT_LIST_RECORDS = [ { id: '1', @@ -416,7 +599,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ vehicleCount: 2, vehicles: [ { vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '9.6米氢燃料电池车', plateNo: '-', actualDelivery: '-', deliveryPerson: '-' }, - { vehicleType: '49吨牵引车头', brand: '飞驰', model: '集卡头', plateNo: '浙C30003', actualDelivery: '2025-02-18 14:00', deliveryPerson: '孙运维' }, + { vehicleType: '49吨牵引车头', brand: '飞驰', model: '集卡头', plateNo: '浙C30003', actualDelivery: '2025-02-18 14:00', returnTime: '2025-04-01 09:00', deliveryPerson: '孙运维' }, ], approvalStatus: '审批中', contractStatus: '已提交审批', @@ -457,7 +640,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ projectName: '苏州城配试点项目', vehicleCount: 1, vehicles: [ - { vehicleType: '重型平板半挂车', brand: '飞驰', model: '半挂车', plateNo: '苏E50005', actualDelivery: '2025-02-20 09:00', deliveryPerson: '周运维' }, + { vehicleType: '重型平板半挂车', brand: '飞驰', model: '半挂车', plateNo: '苏E50005', actualDelivery: '2025-02-20 09:00', returnTime: '2025-04-10 11:00', deliveryPerson: '周运维' }, ], approvalStatus: '审批中', contractStatus: '变更', @@ -498,9 +681,9 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ projectName: '南京氢能示范项目', vehicleCount: 3, vehicles: [ - { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏A60006', actualDelivery: '2025-01-20 08:00', deliveryPerson: '吴运维', leasePeriodMonths: 24 }, - { vehicleType: '18吨厢式货车', brand: '现代', model: '18吨氢燃料电池车', plateNo: '苏A60007', actualDelivery: '2025-01-21 10:00', deliveryPerson: '郑运维', leasePeriodMonths: 24 }, - { vehicleType: '牵引车头', brand: '飞驰', model: '集卡头', plateNo: '苏A60008', actualDelivery: '2025-01-22 14:00', deliveryPerson: '冯运维', leasePeriodMonths: 24 }, + { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏A60006', actualDelivery: '2025-01-20 08:00', returnTime: '2025-06-01 10:00', deliveryPerson: '吴运维', leasePeriodMonths: 24 }, + { vehicleType: '18吨厢式货车', brand: '现代', model: '18吨氢燃料电池车', plateNo: '苏A60007', actualDelivery: '2025-01-21 10:00', returnTime: '2025-06-01 10:00', deliveryPerson: '郑运维', leasePeriodMonths: 24 }, + { vehicleType: '牵引车头', brand: '飞驰', model: '集卡头', plateNo: '苏A60008', actualDelivery: '2025-01-22 14:00', returnTime: '2025-06-01 10:00', deliveryPerson: '冯运维', leasePeriodMonths: 24 }, ], approvalStatus: '审批通过', contractStatus: '合同进行中', @@ -542,7 +725,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ projectName: '无锡试用租赁项目', vehicleCount: 1, vehicles: [ - { vehicleType: '小客车', brand: '宇通', model: '氢燃料电池客车', plateNo: '苏B70007', actualDelivery: '2025-02-01 09:30', deliveryPerson: '陈运维', leasePeriodMonths: 3 }, + { vehicleType: '小客车', brand: '宇通', model: '氢燃料电池客车', plateNo: '苏B70007', actualDelivery: '2025-02-01 09:30', returnTime: '2025-05-15 16:00', deliveryPerson: '陈运维', leasePeriodMonths: 3 }, ], approvalStatus: '审批通过', contractStatus: '合同进行中', @@ -624,8 +807,8 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ projectName: '南通去年到期项目', vehicleCount: 2, vehicles: [ - { vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '9.6米氢燃料电池车', plateNo: '苏F90009', actualDelivery: '2024-03-01 09:00', deliveryPerson: '褚运维', leasePeriodMonths: 12 }, - { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏F90010', actualDelivery: '2024-03-02 10:00', deliveryPerson: '卫运维', leasePeriodMonths: 12 }, + { vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '9.6米氢燃料电池车', plateNo: '苏F90009', actualDelivery: '2024-03-01 09:00', returnTime: '2024-12-20 15:00', deliveryPerson: '褚运维', leasePeriodMonths: 12 }, + { vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', plateNo: '苏F90010', actualDelivery: '2024-03-02 10:00', returnTime: '2024-12-20 15:00', deliveryPerson: '卫运维', leasePeriodMonths: 12 }, ], approvalStatus: '审批通过', contractStatus: '到期合同', @@ -736,6 +919,46 @@ export var LEASE_CONTRACT_LIST_RECORDS = [ remark: '提交后撤回', legalStampedContractUploaded: undefined, }, + { + id: '12', + contractCode: 'HT-ZL-2025-012', + projectName: '长三角在租车队统计项目', + vehicleCount: ON_LEASE_FLEET_VEHICLE_CASES.length, + vehicles: buildOnLeaseFleetDemoVehicles(ON_LEASE_FLEET_VEHICLE_CASES), + approvalStatus: '审批通过', + contractStatus: '合同进行中', + customerName: '嘉兴某某物流有限公司', + signingCompany: '嘉兴羚牛', + businessDept: '业务1部', + businessOwner: '张经理', + contractType: '正式合同', + contractApprovalType: '标准合同', + paymentPeriod: 3, + hydrogenPaymentMethod: 'self', + deliveryRegion: ['浙江省', '嘉兴市'], + deliveryDate: '2025-01-05', + insuredVehicleCount: ON_LEASE_FLEET_VEHICLE_CASES.length, + thirdPartyLiabilityMillion: 300, + hasMinimumMileage: false, + mileagePeriod: 'month', + mileageTargetKm: 8000, + delegateCount: 2, + poaUploaded: true, + contractEndDate: '2027-01-31', + contactName: '张三', + contactPhone: '13800138001', + creator: '张经理', + createTime: '2025-01-05 10:00', + updater: '张经理', + updateTime: '2025-02-10 09:00', + remark: '在租车辆概览用例合同(品牌/型号/车型与主数据枚举对齐)', + legalStampedContractUploaded: true, + approvalFlowNodes: [ + { nodeTitle: '法务审核', result: 'passed', operatorName: '陈法务', operatorTime: '2025-01-08 16:00:00', comment: '七车批量租赁,车型与公告型号已核对。' }, + { nodeTitle: '业务负责人审批', result: 'passed', operatorName: '张经理', operatorTime: '2025-01-07 11:00:00', comment: '同意按标准合同审批。' }, + { nodeTitle: '发起审批', result: 'passed', operatorName: '张经理', operatorTime: '2025-01-06 09:30:00' }, + ], + }, ]; export function resolveSigningCompanyFullName(record) { @@ -854,6 +1077,50 @@ export function canContractVehicleReturn(vehicle) { return isContractVehicleDelivered(vehicle) && !isContractVehicleReturned(vehicle); } +/** 不计入在租统计的合同状态 */ +var ON_LEASE_EXCLUDED_CONTRACT_STATUSES = ['草稿', '已结束', '已终止', '未提交', '已提交审批']; + +/** 当前在租:已交车、未还车,且合同处于履约期 */ +export function isVehicleCurrentlyOnLease(vehicle, record) { + if (!canContractVehicleReturn(vehicle)) return false; + if (!record) return false; + var status = String(record.contractStatus || '').trim(); + if (!status || ON_LEASE_EXCLUDED_CONTRACT_STATUSES.indexOf(status) >= 0) return false; + return true; +} + +/** + * 按品牌 + 型号聚合在租车辆 + * @returns {Array<{ key: string, brand: string, model: string, vehicleType: string, count: number, entries: Array<{ vehicle: object, record: object }> }>} + */ +export function buildOnLeaseFleetByBrandModel(records) { + var bucket = {}; + (records || []).forEach(function (record) { + (record.vehicles || []).forEach(function (vehicle) { + if (!isVehicleCurrentlyOnLease(vehicle, record)) return; + var brand = String(vehicle.brand || '未知品牌').trim() || '未知品牌'; + var model = String(vehicle.model || '未知型号').trim() || '未知型号'; + var key = brand + '::' + model; + if (!bucket[key]) { + bucket[key] = { + key: key, + brand: brand, + model: model, + vehicleType: vehicle.vehicleType || '-', + count: 0, + entries: [], + }; + } + bucket[key].count += 1; + bucket[key].entries.push({ vehicle: vehicle, record: record }); + }); + }); + return Object.keys(bucket).map(function (key) { return bucket[key]; }).sort(function (a, b) { + if (b.count !== a.count) return b.count - a.count; + return a.brand.localeCompare(b.brand, 'zh-CN') || a.model.localeCompare(b.model, 'zh-CN'); + }); +} + export function computeContractOverallMileageProgress(record) { if (!record || !record.hasMinimumMileage) return null; var vehicles = record.vehicles || []; @@ -871,20 +1138,16 @@ export function computeContractOverallMileageProgress(record) { 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; + var statuses = []; 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; + var status = vehicle.mileageForecastStatus || computeVehicleMileageForecastStatus(record, vehicle); + if (status) statuses.push(status); }); - if (percent < 45 || hasUnableVehicle) return '预计无法完成'; - if (percent < 75 || hasRiskVehicle) return '存在完成风险'; + if (statuses.length === 0) return null; + if (statuses.some(function (status) { return status === '预计无法完成'; })) return '预计无法完成'; + if (statuses.some(function (status) { return status === '存在完成风险'; })) return '存在完成风险'; return '预计可完成'; } @@ -957,11 +1220,30 @@ export function enrichLeaseContractVehicle(vehicle, index, recordIndex, record) ); } var returnSettlementStatus = vehicle.returnSettlementStatus; + if (returnSettlementStatus === '已结算') returnSettlementStatus = '已完成'; if (hasReturn && !returnSettlementStatus) { returnSettlementStatus = RETURN_SETTLEMENT_STATUSES[seed % RETURN_SETTLEMENT_STATUSES.length]; } + var returnSettlementApprover = vehicle.returnSettlementApprover; + if (hasReturn && returnSettlementStatus === '审批中' && !returnSettlementApprover) { + returnSettlementApprover = ['王财务', '赵主管', '刘经理'][seed % 3]; + } + var dailyAvgMileage7d = vehicle.dailyAvgMileage7d; + if (dailyAvgMileage7d == null && delivered && hasMinMileage && record) { + var periodEndDate = computeCurrentMileagePeriodEnd(periodStartDate, record.mileagePeriod); + var daysLeftDemo = periodEndDate ? Math.max(1, daysBetween(new Date(), periodEndDate)) : 30; + var remainingDemo = remainingMileage != null ? remainingMileage : 0; + var profile = seed % 3; + if (profile === 0) { + dailyAvgMileage7d = Math.max(1, Math.round((remainingDemo / daysLeftDemo) * 0.82)); + } else if (profile === 1) { + dailyAvgMileage7d = Math.max(1, Math.round(remainingDemo / daysLeftDemo + 80)); + } else { + dailyAvgMileage7d = Math.max(1, Math.round(remainingDemo / daysLeftDemo + 280)); + } + } - return Object.assign({}, vehicle, { + var enrichedVehicle = Object.assign({}, vehicle, { vin: vehicle.vin || (vehicle.plateNo && vehicle.plateNo !== '-' ? 'LFWNKVPH8K1' + String(10000 + seed).slice(-5) : '-'), @@ -982,22 +1264,48 @@ export function enrichLeaseContractVehicle(vehicle, index, recordIndex, record) periodStartDate: periodStartDate, periodStartMileage: periodStartMileage, returnSettlementStatus: hasReturn ? returnSettlementStatus : null, + returnSettlementApprover: hasReturn && returnSettlementStatus === '审批中' ? returnSettlementApprover : null, leasePeriodMonths: vehicle.leasePeriodMonths != null ? vehicle.leasePeriodMonths : getVehicleLeasePeriodMonths(vehicle, record), plannedDeliveryDate: vehicle.plannedDeliveryDate != null ? vehicle.plannedDeliveryDate : (delivered ? null : (record && record.deliveryDate) || null), + dailyAvgMileage7d: dailyAvgMileage7d, }); + if (delivered && hasMinMileage && record) { + enrichedVehicle.mileageForecastStatus = computeVehicleMileageForecastStatus(record, enrichedVehicle); + } + return enrichedVehicle; +} + +export function resolveContractLastUpdateAudit(record) { + if (!record) return { updater: '-', updateTime: '-' }; + var creator = record.creator && String(record.creator).trim() && record.creator !== '-' + ? String(record.creator).trim() + : '-'; + var createTime = record.createTime && String(record.createTime).trim() && record.createTime !== '-' + ? String(record.createTime).trim() + : '-'; + var updater = record.updater && String(record.updater).trim() && record.updater !== '-' + ? String(record.updater).trim() + : creator; + var updateTime = record.updateTime && String(record.updateTime).trim() && record.updateTime !== '-' + ? String(record.updateTime).trim() + : createTime; + return { updater: updater, updateTime: updateTime }; } export function enrichLeaseContractRecord(record, recordIndex) { var lessorId = record.lessorId || SIGNING_COMPANY_LESSOR_IDS[record.signingCompany]; var templateId = inferContractTemplateId(record); + var lastUpdateAudit = resolveContractLastUpdateAudit(record); var enriched = Object.assign({}, record, { lessorId: lessorId, contractTemplateId: record.contractTemplateId || templateId, signingCompanyFullName: resolveSigningCompanyFullName(Object.assign({}, record, { lessorId: lessorId })), + updater: lastUpdateAudit.updater, + updateTime: lastUpdateAudit.updateTime, vehicles: (record.vehicles || []).map(function (vehicle, index) { return enrichLeaseContractVehicle(vehicle, index, recordIndex, record); }), diff --git a/src/prototypes/lease-contract-management/lease-contract-preview-build.js b/src/prototypes/lease-contract-management/lease-contract-preview-build.js index 53652af..093e233 100644 --- a/src/prototypes/lease-contract-management/lease-contract-preview-build.js +++ b/src/prototypes/lease-contract-management/lease-contract-preview-build.js @@ -338,7 +338,7 @@ function buildLeaseOrderPreviewRow(row, brandModelPair, plate, leaseOrder) { + ''; } -/** 授权委托书:委托人信息反写 */ +/** 授权委托书:受托人信息反写 */ export function applyPowerOfAttorneyPatch(html, form) { if (!html || !form || !form.powerOfAttorney) return html; var delegates = normalizeDelegateRows(form.powerOfAttorney.delegates || []); @@ -357,7 +357,11 @@ export function applyPowerOfAttorneyPatch(html, form) { var contact = escapeHtml((delegate.contact || '').trim()); var idNumber = escapeHtml((delegate.idNumber || '').trim()); if (index === 0) { - slice = slice.replace(/(委托人[::]\s*)([\s\u00a0]*)/, '$1' + name + ' '); + if (/受托人[::]/u.test(slice)) { + slice = slice.replace(/(受托人[::]\s*)([\s\u00a0]*)/, '$1' + name + ' '); + } else { + slice = slice.replace(/(委托人[::]\s*)([\s\u00a0]*)/, '$1' + name + ' '); + } slice = slice.replace(/(联系电话[::]\s*)([\s\u00a0]*)/, '$1' + contact + ' '); slice = slice.replace(/(身份证号码[::]\s*)([\s\u00a0]*)/, '$1' + idNumber + ' '); } diff --git a/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs b/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs index b03b7fd..87e89a5 100644 --- a/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs +++ b/src/prototypes/lease-contract-management/scripts/build-annotation-source.mjs @@ -14,11 +14,12 @@ const fullPrd = fs.readFileSync(resourcesPrd, 'utf8'); const NODE_DEFS = [ { id: 'lc-list-filter', title: '筛选条件', pageId: 'list', color: '#2563eb', aiPrompt: '列表筛选区字段与查询逻辑。', annotationText: '默认展示 4 项,可展开至 12 项;查询与 KPI 联动。' }, { id: 'lc-list-kpi', title: 'KPI 统计卡片', pageId: 'list', color: '#64748b', aiPrompt: '五张 KPI 卡片口径与点击筛选。', annotationText: '全部 / 进行中 / 已终止 / 临期 / 过期。' }, + { id: 'lc-fleet-summary', title: '在租车辆概览', pageId: 'list', color: '#32a06e', aiPrompt: '按品牌型号统计在租车辆,支持品牌标签筛选。', annotationText: '顶部展示在租总数;品牌标签筛选车型卡片;点击卡片查看车辆明细。' }, { id: 'lc-list-toolbar', title: '列表工具栏', pageId: 'list', color: '#0f766e', aiPrompt: '费用模板、导出、新增入口。', annotationText: '「新增」进入租赁合同创建页。' }, { id: 'lc-list-table', title: '合同台账列表', pageId: 'list', color: '#2563eb', 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-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-footer', title: '底栏操作', pageId: 'create', color: '#64748b', aiPrompt: '取消、保存草稿、提交审核。', annotationText: '保存/提交时展示将走的审批类型。' }, ]; diff --git a/src/prototypes/lease-contract-management/styles/lease-contract.css b/src/prototypes/lease-contract-management/styles/lease-contract.css index f7d0192..b340e43 100644 --- a/src/prototypes/lease-contract-management/styles/lease-contract.css +++ b/src/prototypes/lease-contract-management/styles/lease-contract.css @@ -56,6 +56,71 @@ max-height: calc(100vh - 640px); } +/* 列表宽表:横向滚动交给 Ant Table 内部,避免外层滚动抢滚动上下文 */ +.vm-page.lc-page .vm-table-section { + min-width: 0; + max-width: 100%; +} + +.vm-page.lc-page .vm-table-card > .vm-table-wrap { + overflow: hidden; + max-height: none; + min-width: 0; +} + +.vm-page.lc-page .lc-list-table--nested.ant-table-wrapper, +.vm-page.lc-page .lc-list-table--nested .ant-table-wrapper { + min-width: 0; + max-width: 100%; +} + +/* 左侧冻结:展开列 + 项目信息(背景与层级,不覆盖 antd 的 sticky 偏移) */ +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-start, +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-left { + background: var(--ln-surface-card); +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-start, +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-left { + z-index: 3; + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td.ant-table-cell-fix-start, +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td.ant-table-cell-fix-left { + z-index: 2; + background: var(--ln-surface-card) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td.ant-table-cell-fix-start, +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td.ant-table-cell-fix-left { + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-row-expand-icon-cell.ant-table-cell-fix-start { + z-index: 4; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-row-expand-icon-cell { + z-index: 4; + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-start, +.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-left { + z-index: 3; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.lc-project-info-col { + z-index: 3; +} + +.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-start-shadow-show::after, +.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-left-last::after, +.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-left-last::after { + box-shadow: inset 10px 0 8px -8px rgba(15, 23, 42, 0.12); +} + @media (max-width: 1280px) { .vm-page.lc-page .vm-kpi-row { grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -95,12 +160,16 @@ } .vm-page.lc-page .vm-list-table .ant-table-cell-fix-left, -.vm-page.lc-page .vm-list-table .ant-table-cell-fix-right { +.vm-page.lc-page .vm-list-table .ant-table-cell-fix-start, +.vm-page.lc-page .vm-list-table .ant-table-cell-fix-right, +.vm-page.lc-page .vm-list-table .ant-table-cell-fix-end { background: var(--ln-surface-card); } .vm-page.lc-page .vm-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, -.vm-page.lc-page .vm-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { +.vm-page.lc-page .vm-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-start, +.vm-page.lc-page .vm-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right, +.vm-page.lc-page .vm-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-end { background: var(--ln-canvas-soft) !important; } @@ -685,6 +754,34 @@ outline-offset: 1px; } +.vm-page.lc-page .lc-delegate-supplement-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 28px; + padding: 0 4px; + border: none; + background: transparent; + color: #b45309; + font-size: 13px; + font-weight: 600; + cursor: pointer; + border-radius: var(--ln-radius-xs, 4px); + transition: color 0.15s ease, background-color 0.15s ease; +} + +.vm-page.lc-page .lc-delegate-supplement-btn:hover { + color: #92400e; + background: color-mix(in srgb, #f59e0b 12%, transparent); + text-decoration: underline; + text-underline-offset: 2px; +} + +.vm-page.lc-page .lc-delegate-supplement-btn:focus-visible { + outline: 2px solid #f59e0b; + outline-offset: 2px; +} + .lc-delegate-popover .ant-popover-inner { padding: 0; border-radius: 12px; @@ -1032,7 +1129,7 @@ } } -/* 列表嵌套子表:参照合同模板 expandable 样式 */ +/* 列表嵌套:展开列宽度与冻结态 */ .vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-row-expand-icon-cell, .vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell, .vm-page.lc-page .lc-list-table--nested col.ant-table-expand-icon-col { @@ -1044,8 +1141,17 @@ box-sizing: border-box; } -.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > .ant-table-cell { - padding: 10px 12px 14px 60px; +.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover > td.ant-table-row-expand-icon-cell.ant-table-cell-fix-start { + background: var(--ln-canvas-soft) !important; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td.ant-table-row-expand-icon-cell.ant-table-cell-fix-start { + background: var(--ln-canvas-soft, #f8fafc); + z-index: 4; +} + +.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td:not(.ant-table-cell-fix-start) { + padding: 8px 8px 12px; background: var(--ln-canvas-soft, #f8fafc); } @@ -1053,11 +1159,12 @@ width: 100%; max-width: 100%; overflow-x: auto; + -webkit-overflow-scrolling: touch; } .vm-page.lc-page .lc-contract-vehicle-native-table { width: 100%; - min-width: 1120px; + min-width: 0; border-collapse: collapse; table-layout: fixed; background: var(--ln-surface-card, #fff); @@ -1068,19 +1175,26 @@ .vm-page.lc-page .lc-contract-vehicle-native-table th, .vm-page.lc-page .lc-contract-vehicle-native-table td { - padding: 10px 14px; + padding: 7px 8px; border-bottom: 1px solid var(--ln-hairline, #e2e8f0); - font-size: 13px; + font-size: 12px; color: var(--ln-body, #334155); vertical-align: middle; text-align: left; - white-space: nowrap; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; } .vm-page.lc-page .lc-contract-vehicle-native-table thead th { background: var(--ln-surface-strong, #f1f5f9); font-weight: 600; + font-size: 11px; color: var(--ln-muted, #64748b); + vertical-align: middle; + white-space: nowrap; + padding-top: 8px; + padding-bottom: 8px; } .vm-page.lc-page .lc-contract-vehicle-native-table tbody tr:last-child td { @@ -1088,45 +1202,171 @@ } .vm-page.lc-page .lc-contract-vehicle-native-table__info { - width: 14%; - min-width: 156px; - white-space: normal !important; + width: 13%; } .vm-page.lc-page .lc-contract-vehicle-native-table__pickup { - width: 7%; - min-width: 84px; - white-space: normal !important; + width: 8%; } .vm-page.lc-page .lc-contract-vehicle-native-table__handover { - width: 10%; - min-width: 100px; - white-space: normal !important; + width: 9%; } .vm-page.lc-page .lc-contract-vehicle-native-table__return-settlement { - width: 8%; - min-width: 88px; - white-space: normal !important; + width: 9%; } .vm-page.lc-page .lc-contract-vehicle-native-table__mileage-req { - width: 32%; - min-width: 300px; - white-space: normal !important; + width: 18%; } .vm-page.lc-page .lc-contract-vehicle-native-table__mileage-completion { - width: 14%; - min-width: 148px; - white-space: normal !important; + width: 20%; } .vm-page.lc-page .lc-contract-vehicle-native-table__action { - width: 7%; - min-width: 64px; - white-space: normal !important; + width: 6%; + text-align: left; +} + +/* 子表单元格:垂直居中 + 主体左对齐 */ +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-vehicle-info, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-pickup-payment, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-return-settlement, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-handover-situation, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-mileage-req-row, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-mileage-completion, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-vehicle-action-empty, +.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-vehicle-action-btn { + text-align: left; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-vehicle-info-cell { + align-items: center; + justify-content: flex-start; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-vehicle-info-cell__status { + align-self: center; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-pickup-payment, +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-return-settlement { + min-height: 0; + justify-content: flex-start; + align-items: flex-start; + text-align: left; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-handover-situation--clickable, +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-handover-situation--empty { + text-align: left; + align-items: flex-start; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion { + align-items: flex-start; + text-align: left; +} + +/* 子表里程要求列:左右两栏(里程要求 | 当前里程) */ +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual { + flex-direction: row; + align-items: center; + gap: 0; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual .lc-contract-mileage-req-row__requirement, +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual .lc-contract-mileage-req-row__current { + flex: 1 1 0; + min-width: 0; + padding: 0 6px; + box-sizing: border-box; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual .lc-contract-mileage-req-row__current { + border-left: 1px solid var(--ln-hairline, #e2e8f0); +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual .lc-contract-mileage-req-row__current--empty { + font-size: 12px; + color: var(--ln-muted, #64748b); + line-height: 1.35; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row__section-label { + font-size: 10px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row__requirement--empty { + font-size: 12px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .vm-tag { + font-size: 10px; + line-height: 1.25; + padding: 1px 5px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__remaining { + font-size: 11px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__remaining-row .vm-status { + font-size: 10px; + line-height: 1.2; + padding: 1px 6px; + max-width: 100%; + white-space: normal; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress { + gap: 6px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress__track { + min-width: 40px; + height: 6px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress__label { + font-size: 11px; + min-width: 30px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-handover-situation__line { + font-size: 12px; + line-height: 1.3; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-handover-situation__line--time { + font-size: 11px; + color: var(--ln-muted, #64748b); +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-vehicle-info__line--primary { + font-size: 12px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-vehicle-info__line--secondary { + font-size: 11px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-vehicle-info__line--vin { + font-size: 10px; + color: var(--ln-muted, #64748b); + word-break: break-all; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-vehicle-action-btn { + font-size: 12px; + padding: 0 2px; + text-align: left; } .vm-page.lc-page .lc-contract-mileage-req-row { @@ -1140,6 +1380,14 @@ gap: 0; } +.vm-page.lc-page .lc-contract-mileage-req-row--dual { + gap: 0; +} + +.vm-page.lc-page .lc-contract-mileage-req-row--dual .lc-contract-mileage-req-row__current { + border-left: 1px solid var(--ln-hairline, #e2e8f0); +} + .vm-page.lc-page .lc-contract-mileage-req-row__requirement, .vm-page.lc-page .lc-contract-mileage-req-row__period, .vm-page.lc-page .lc-contract-mileage-req-row__current { @@ -1184,6 +1432,40 @@ line-height: 1.3; } +.vm-page.lc-page .lc-contract-mileage-req-row__period-date-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; + min-width: 0; +} + +.vm-page.lc-page .lc-contract-mileage-period-days-tag { + flex-shrink: 0; + font-size: 11px; + line-height: 1.2; + padding: 1px 6px; +} + +.vm-page.lc-page .lc-contract-mileage-req-row__completed { + display: flex; + align-items: baseline; + gap: 4px; + font-size: 12px; + line-height: 1.3; + min-width: 0; +} + +.vm-page.lc-page .lc-contract-mileage-req-row__completed-label { + color: var(--ln-muted, #64748b); + flex-shrink: 0; +} + +.vm-page.lc-page .lc-contract-mileage-req-row__completed-value { + font-weight: 500; + color: var(--ln-body, #334155); +} + .vm-page.lc-page .lc-contract-mileage-req-row__km-line, .vm-page.lc-page .lc-contract-mileage-req-row__current-line { min-width: 0; @@ -1201,7 +1483,36 @@ min-height: 32px; } -.vm-page.lc-page .lc-contract-return-settlement--empty, +.vm-page.lc-page .lc-contract-return-settlement--empty { + font-size: 13px; + color: var(--ln-muted, #64748b); + min-height: 0; +} + +.vm-page.lc-page .lc-contract-return-settlement--stacked { + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 4px; + min-height: 0; +} + +.vm-page.lc-page .lc-contract-return-settlement__approver { + font-size: 11px; + line-height: 1.3; + color: var(--ln-muted, #64748b); + max-width: 100%; + word-break: break-word; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-return-settlement--stacked { + align-items: flex-start; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-return-settlement__approver { + font-size: 10px; +} + .vm-page.lc-page .lc-contract-pickup-payment--empty { font-size: 13px; color: var(--ln-muted, #64748b); @@ -1300,8 +1611,90 @@ line-height: 1.3; } +.vm-page.lc-page .lc-contract-mileage-completion__remaining-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + min-width: 0; +} + +.vm-page.lc-page .lc-contract-mileage-completion__remaining-row .vm-status { + flex-shrink: 0; +} + .vm-page.lc-page .lc-contract-mileage-completion .lc-vehicle-mileage-progress { width: 100%; + min-width: 0; +} + +.vm-page.lc-page .lc-contract-mileage-completion__progress-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.vm-page.lc-page .lc-contract-mileage-completion__progress-main { + flex: 1 1 auto; + min-width: 0; + max-width: 68%; +} + +.vm-page.lc-page .lc-contract-mileage-completion__progress-main .lc-vehicle-mileage-progress__track { + min-width: 32px; +} + +.vm-page.lc-page .lc-contract-mileage-completion__range-side { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.vm-page.lc-page .lc-contract-mileage-completion__range-side--start { + align-items: flex-start; + text-align: left; + margin-top: 2px; +} + +.vm-page.lc-page .lc-contract-mileage-completion__range-side--end { + flex: 0 0 auto; + align-items: flex-end; + text-align: right; +} + +.vm-page.lc-page .lc-contract-mileage-completion__range-date { + font-size: 10px; + color: var(--ln-muted, #64748b); + line-height: 1.25; +} + +.vm-page.lc-page .lc-contract-mileage-completion__range-mile { + font-size: 11px; + font-weight: 500; + color: var(--ln-body, #334155); + line-height: 1.25; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__progress-main { + max-width: 72%; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__progress-main .lc-vehicle-mileage-progress__track { + min-width: 36px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-side--end { + max-width: 28%; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-date { + font-size: 10px; +} + +.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-mile { + font-size: 10px; } .vm-page.lc-page .lc-contract-vehicle-action-btn { @@ -1437,3 +1830,589 @@ min-width: 36px; text-align: right; } + +/* —— 在租车辆概览 —— */ +.vm-page.lc-page .lc-fleet-summary { + margin-bottom: 16px; + border: 1px solid var(--ln-hairline); + border-radius: var(--ln-radius-card, 12px); + background: var(--ln-surface-card); + box-shadow: var(--ln-shadow-soft); + overflow: hidden; +} + +.vm-page.lc-page .lc-fleet-summary__header { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 12px 16px; + padding: 16px 16px 12px; + border-bottom: 1px solid var(--ln-divider-soft, #f4f4f5); +} + +.vm-page.lc-page .lc-fleet-summary.is-collapsed .lc-fleet-summary__header { + border-bottom: none; + padding-bottom: 16px; +} + +.vm-page.lc-page .lc-fleet-summary__title-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.vm-page.lc-page .lc-fleet-summary__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 10px; + background: var(--ln-primary-soft); + color: var(--ln-primary); + flex-shrink: 0; +} + +.vm-page.lc-page .lc-fleet-summary__title { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--ln-ink); + letter-spacing: -0.02em; +} + +.vm-page.lc-page .lc-fleet-summary__badge { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + background: var(--ln-primary-soft); + color: var(--ln-primary-focus); + font-size: 0.8125rem; + font-weight: 600; +} + +.vm-page.lc-page .lc-fleet-summary__subtitle { + margin: 6px 0 0 44px; + font-size: 0.8125rem; + color: var(--ln-muted); +} + +.vm-page.lc-page .lc-fleet-summary.is-collapsed .lc-fleet-summary__subtitle { + margin-left: 0; +} + +.vm-page.lc-page .lc-fleet-summary__toolbar { + display: flex; + align-items: center; + gap: 10px; + flex: 1 1 280px; + justify-content: flex-end; + min-width: 0; +} + +.vm-page.lc-page .lc-fleet-summary__search { + flex: 1 1 220px; + max-width: 320px; + min-width: 160px; +} + +.vm-page.lc-page .lc-fleet-summary__search .ant-input { + min-height: 36px; + border-radius: var(--ln-radius-control, 8px); +} + +.vm-page.lc-page .lc-fleet-summary__toggle { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 36px; + padding: 0 12px; + border: 1px solid var(--ln-hairline); + border-radius: var(--ln-radius-control, 8px); + background: var(--ln-canvas); + color: var(--ln-body); + font-size: 0.8125rem; + cursor: pointer; + transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease; +} + +.vm-page.lc-page .lc-fleet-summary__toggle:hover { + border-color: var(--ln-hairline-strong); + color: var(--ln-ink); + background: var(--ln-surface-pearl); +} + +.vm-page.lc-page .lc-fleet-summary__toggle:focus-visible { + outline: 2px solid var(--ln-primary); + outline-offset: 2px; +} + +.vm-page.lc-page .lc-fleet-summary__body { + padding: 12px 16px 16px; +} + +.vm-page.lc-page .lc-fleet-summary__brands { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 14px; +} + +.vm-page.lc-page .lc-fleet-summary__brand-chip { + min-height: 32px; + padding: 0 12px; + border: 1px solid var(--ln-hairline); + border-radius: 999px; + background: var(--ln-canvas); + color: var(--ln-body); + font-size: 0.8125rem; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease; +} + +.vm-page.lc-page .lc-fleet-summary__brand-chip:hover { + border-color: var(--ln-primary); + color: var(--ln-primary-focus); +} + +.vm-page.lc-page .lc-fleet-summary__brand-chip.is-active { + border-color: var(--ln-primary); + background: var(--ln-primary-soft); + color: var(--ln-primary-focus); + font-weight: 600; +} + +.vm-page.lc-page .lc-fleet-summary__brand-chip:focus-visible { + outline: 2px solid var(--ln-primary); + outline-offset: 2px; +} + +.vm-page.lc-page .lc-fleet-summary__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; +} + +.vm-page.lc-page .lc-fleet-summary__card { + display: flex; + flex-direction: column; + gap: 8px; + min-height: 132px; + padding: 14px; + border: 1px solid var(--ln-hairline); + border-radius: 10px; + background: linear-gradient(180deg, var(--ln-canvas) 0%, var(--ln-surface-pearl) 100%); + cursor: pointer; + transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; +} + +.vm-page.lc-page .lc-fleet-summary__card:hover { + border-color: color-mix(in srgb, var(--ln-primary) 35%, var(--ln-hairline)); + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06); + transform: translateY(-1px); +} + +.vm-page.lc-page .lc-fleet-summary__card:focus-visible { + outline: 2px solid var(--ln-primary); + outline-offset: 2px; +} + +.vm-page.lc-page .lc-fleet-summary__card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-width: 0; +} + +.vm-page.lc-page .lc-fleet-summary__card-brand { + font-size: 0.75rem; + font-weight: 600; + color: var(--ln-primary-focus); + letter-spacing: 0.02em; +} + +.vm-page.lc-page .lc-fleet-summary__card-type { + flex-shrink: 0; + max-width: 52%; + padding: 2px 8px; + border-radius: 999px; + background: var(--ln-surface-strong); + color: var(--ln-muted); + font-size: 0.6875rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.vm-page.lc-page .lc-fleet-summary__card-model { + margin: 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--ln-ink); + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + min-height: 2.7em; +} + +.vm-page.lc-page .lc-fleet-summary__card-metrics { + display: flex; + align-items: baseline; + gap: 4px; + flex-wrap: wrap; +} + +.vm-page.lc-page .lc-fleet-summary__card-count { + font-size: 1.5rem; + font-weight: 700; + color: var(--ln-ink); + line-height: 1; + letter-spacing: -0.03em; +} + +.vm-page.lc-page .lc-fleet-summary__card-unit { + font-size: 0.8125rem; + color: var(--ln-muted); +} + +.vm-page.lc-page .lc-fleet-summary__card-share { + margin-left: auto; + font-size: 0.75rem; + color: var(--ln-muted); +} + +.vm-page.lc-page .lc-fleet-summary__card-bar { + height: 6px; + border-radius: 999px; + background: color-mix(in srgb, var(--ln-hairline) 70%, #fff); + overflow: hidden; +} + +.vm-page.lc-page .lc-fleet-summary__card-bar-fill { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--ln-primary), color-mix(in srgb, var(--ln-primary) 75%, #22c55e)); + transition: width 0.25s ease; +} + +.vm-page.lc-page .lc-fleet-summary__card-hint { + font-size: 0.6875rem; + color: var(--ln-muted-soft); +} + +.vm-page.lc-page .lc-fleet-summary__empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 28px 16px; + text-align: center; + color: var(--ln-muted); + font-size: 0.875rem; +} + +.vm-page.lc-page .lc-fleet-summary__empty-reset { + min-height: 36px; + padding: 0 14px; + border: 1px solid var(--ln-hairline); + border-radius: var(--ln-radius-control, 8px); + background: var(--ln-canvas); + color: var(--ln-primary); + font-size: 0.8125rem; + cursor: pointer; +} + +.vm-page.lc-page .lc-fleet-summary__empty-reset:hover { + border-color: var(--ln-primary); + background: var(--ln-primary-soft); +} + +/* 在租车辆明细弹窗 — Modal 挂载在 body,样式不可依赖 .vm-page 祖先 */ +.lc-fleet-detail-modal .ant-modal-content { + padding: 0; + border-radius: 14px; + overflow: hidden; + box-shadow: 0 20px 48px rgba(15, 23, 42, 0.14); +} + +.lc-fleet-detail-modal .ant-modal-header { + margin: 0; + padding: 16px 20px; + border-bottom: 1px solid #e5e7eb; + background: linear-gradient(180deg, rgba(50, 160, 110, 0.06), #fff); +} + +.lc-fleet-detail-modal .ant-modal-body { + padding: 0; +} + +.lc-fleet-detail-modal .ant-modal-footer { + margin: 0; + padding: 12px 20px 16px; + border-top: 1px solid #f4f4f5; + background: #f6f7f7; +} + +.lc-fleet-detail-modal__title { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 10px; + padding-right: 28px; +} + +.lc-fleet-detail-modal__title-main { + font-size: 1rem; + font-weight: 600; + color: #18181b; + letter-spacing: -0.02em; +} + +.lc-fleet-detail-modal__title-badge { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + background: rgba(50, 160, 110, 0.1); + color: #2b9260; + font-size: 0.75rem; + font-weight: 600; +} + +.lc-fleet-detail-modal__title-type { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + background: #f4f4f5; + color: #71717a; + font-size: 0.75rem; +} + +.lc-fleet-detail-modal__body { + max-height: min(62vh, 540px); + overflow: auto; +} + +.lc-fleet-detail-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.lc-fleet-detail-table { + width: 100%; + min-width: 880px; + border-collapse: collapse; + table-layout: fixed; + font-size: 0.8125rem; +} + +.lc-fleet-detail-table thead th { + position: sticky; + top: 0; + z-index: 1; + padding: 10px 12px; + background: #f6f7f7; + color: #71717a; + font-size: 0.75rem; + font-weight: 600; + text-align: left; + white-space: nowrap; + border-bottom: 1px solid #e5e7eb; +} + +.lc-fleet-detail-table tbody td { + padding: 11px 12px; + vertical-align: middle; + border-bottom: 1px solid #f4f4f5; + color: #52525b; +} + +.lc-fleet-detail-table tbody tr:hover td { + background: rgba(50, 160, 110, 0.04); +} + +.lc-fleet-detail-table tbody tr:last-child td { + border-bottom: none; +} + +.lc-fleet-detail-table__col-plate { width: 88px; } +.lc-fleet-detail-table__col-vin { width: 148px; } +.lc-fleet-detail-table__col-type { width: 120px; } +.lc-fleet-detail-table__col-announce { width: 168px; } +.lc-fleet-detail-table__col-city { width: 96px; } +.lc-fleet-detail-table__col-mileage { width: 96px; } +.lc-fleet-detail-table__col-project { width: 180px; } +.lc-fleet-detail-table__col-action { width: 88px; } + +.lc-fleet-detail-table__plate { + display: block; + font-size: 0.875rem; + font-weight: 700; + color: #32a06e; + line-height: 1.3; + white-space: nowrap; +} + +.lc-fleet-detail-table__vin { + display: block; + font-size: 0.75rem; + line-height: 1.35; + color: #52525b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.lc-fleet-detail-table__city { + display: block; + white-space: nowrap; +} + +.lc-fleet-detail-table__type-pill { + display: inline-flex; + align-items: center; + max-width: 100%; + padding: 2px 8px; + border-radius: 999px; + background: #f4f4f5; + color: #71717a; + font-size: 0.6875rem; + line-height: 1.4; + white-space: normal; + word-break: break-word; +} + +.lc-fleet-detail-table__announce { + display: block; + color: #52525b; + line-height: 1.4; + font-size: 0.75rem; + word-break: break-all; +} + +.lc-fleet-detail-table__mileage { + display: inline-block; + font-weight: 600; + color: #18181b; + white-space: nowrap; +} + +.lc-fleet-detail-table__project { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} + +.lc-fleet-detail-table__project-name { + font-weight: 600; + color: #18181b; + font-size: 0.8125rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.lc-fleet-detail-table__contract-link { + padding: 0; + border: none; + background: none; + color: #32a06e; + font-size: 0.75rem; + font-weight: 600; + text-align: left; + cursor: pointer; + white-space: nowrap; +} + +.lc-fleet-detail-table__contract-link:hover { + color: #3fb87c; + text-decoration: underline; + text-underline-offset: 2px; +} + +.lc-fleet-detail-table__customer { + font-size: 0.75rem; + color: #71717a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.lc-fleet-detail-table__action { + min-height: 32px; + padding: 0 10px; + border: 1px solid rgba(50, 160, 110, 0.24); + border-radius: 8px; + background: rgba(50, 160, 110, 0.1); + color: #2b9260; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.lc-fleet-detail-table__action:hover { + background: rgba(50, 160, 110, 0.16); + border-color: #32a06e; +} + +.lc-fleet-detail-modal__empty { + padding: 40px 20px; + text-align: center; + color: #71717a; + font-size: 0.875rem; +} + +.lc-fleet-detail-modal__close { + min-width: 88px; +} + +@media (max-width: 768px) { + .vm-page.lc-page .lc-fleet-summary__subtitle { + margin-left: 0; + } + + .vm-page.lc-page .lc-fleet-summary__toolbar { + width: 100%; + justify-content: stretch; + } + + .vm-page.lc-page .lc-fleet-summary__search { + max-width: none; + flex: 1 1 auto; + } + + .vm-page.lc-page .lc-fleet-summary__grid { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + .lc-fleet-detail-table__action { + transition: none; + } + + .vm-page.lc-page .lc-fleet-summary__card, + .vm-page.lc-page .lc-fleet-summary__card-bar-fill, + .vm-page.lc-page .lc-fleet-summary__toggle, + .vm-page.lc-page .lc-fleet-summary__brand-chip { + transition: none; + } + + .vm-page.lc-page .lc-fleet-summary__card:hover { + transform: none; + } +} diff --git a/src/prototypes/oneos-web-safety-training/index.tsx b/src/prototypes/oneos-web-safety-training/index.tsx deleted file mode 100644 index e063bc0..0000000 --- a/src/prototypes/oneos-web-safety-training/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @name 安全培训扫码 - * 自 ONE-OS web端 原稿复刻(1 个页面) - */ -import '../../common/oneosWebLegacy/legacyGlobals'; -import React from 'react'; -import Page1 from './pages/01-安全培训扫码.jsx'; -import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; - -const pages = [ - { id: 'u5b89u5168u57f9u8badu626bu7801', title: '安全培训扫码', component: Page1 }, -]; - -export default function OneosWebSafetyTraining() { - return ( - - ); -} diff --git a/src/prototypes/oneos-web-safety-training/pages/01-安全培训扫码.jsx b/src/prototypes/oneos-web-safety-training/pages/01-安全培训扫码.jsx deleted file mode 100644 index 0949976..0000000 --- a/src/prototypes/oneos-web-safety-training/pages/01-安全培训扫码.jsx +++ /dev/null @@ -1,613 +0,0 @@ -// 【重要】必须使用 const Component 作为组件变量名 -// 安全培训扫码 - H5 分步表单(扫码链接进入,模拟小程序手机宽度) - -const Component = function () { - var useState = React.useState; - var useCallback = React.useCallback; - var useRef = React.useRef; - var useEffect = React.useEffect; - - var antd = window.antd; - var Steps = antd.Steps; - var Button = antd.Button; - var Input = antd.Input; - var Spin = antd.Spin; - var message = antd.message; - var Progress = antd.Progress; - - var STS_GREEN = '#7AB929'; - var STS_GREEN_DEEP = '#6AA322'; - var STS_GREEN_SOFT = 'rgba(122, 185, 41, 0.14)'; - var STS_TEXT = '#1D2129'; - var STS_TEXT_SEC = '#4E5969'; - var STS_MUTED = '#86909C'; - var STS_LINE = '#E5E6EB'; - var STS_BG = '#FFFFFF'; - var STS_PAGE = '#F2F3F5'; - var STS_FONT = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", STHeiti, sans-serif'; - - var STS_PAGE_STYLE = '' - + '.sts-root { height:100dvh; max-height:100dvh; overflow:hidden; background:linear-gradient(165deg,#e8ebef 0%,' + STS_PAGE + ' 40%); display:flex; justify-content:center; align-items:center; padding:16px 12px; box-sizing:border-box; font-family:' + STS_FONT + '; -webkit-font-smoothing:antialiased; }' - + '.sts-phone { width:100%; max-width:390px; height:min(844px, calc(100dvh - 32px)); max-height:calc(100dvh - 32px); background:' + STS_PAGE + '; border-radius:28px; overflow:hidden; box-shadow:0 24px 48px rgba(15,23,42,.14), 0 0 0 1px rgba(15,23,42,.05); display:flex; flex-direction:column; position:relative; }' - + '.sts-navbar { flex-shrink:0; height:48px; display:flex; align-items:center; justify-content:center; padding:0 16px; background:' + STS_BG + '; border-bottom:1px solid rgba(0,0,0,.05); position:relative; }' - + '.sts-nav-title { font-size:17px; font-weight:700; color:' + STS_TEXT + '; }' - + '.sts-body { flex:1; min-height:0; overflow-y:auto; overflow-x:hidden; -webkit-overflow-scrolling:touch; overscroll-behavior:contain; padding:12px 14px 20px; }' - + '.sts-steps { margin-bottom:16px; background:' + STS_BG + '; border-radius:12px; padding:12px 10px; }' - + '.sts-card { background:' + STS_BG + '; border-radius:14px; padding:16px; box-shadow:0 2px 8px rgba(15,23,42,.04); border:1px solid rgba(0,0,0,.03); }' - + '.sts-step-title { font-size:17px; font-weight:700; color:' + STS_TEXT + '; margin-bottom:12px; }' - + '.sts-hint { font-size:13px; color:' + STS_MUTED + '; line-height:1.55; margin-bottom:12px; }' - + '.sts-label { display:block; margin-bottom:8px; font-size:14px; font-weight:600; color:' + STS_TEXT + '; }' - + '.sts-driver-info { margin-bottom:12px; padding:10px 12px; background:' + STS_GREEN_SOFT + '; border:1px solid rgba(122,185,41,.28); border-radius:10px; font-size:13px; color:' + STS_TEXT_SEC + '; line-height:1.6; }' - + '.sts-upload { border:1px dashed ' + STS_LINE + '; border-radius:10px; padding:20px 14px; text-align:center; color:' + STS_MUTED + '; font-size:13px; cursor:pointer; background:' + STS_PAGE + '; touch-action:manipulation; }' - + '.sts-upload:active { background:' + STS_GREEN_SOFT + '; border-color:' + STS_GREEN + '; }' - + '.sts-upload.done { border-color:' + STS_GREEN + '; background:' + STS_GREEN_SOFT + '; color:' + STS_GREEN_DEEP + '; font-weight:600; }' - + '.sts-upload-list { display:flex; flex-direction:column; gap:10px; }' - + '.sts-btn-block { width:100%; min-height:48px; border-radius:12px; font-size:15px; font-weight:600; margin-top:16px; touch-action:manipulation; }' - + '.sts-mini-sheet { position:absolute; inset:0; z-index:40; display:flex; flex-direction:column; justify-content:flex-end; }' - + '.sts-mini-sheet-mask { position:absolute; inset:0; background:rgba(0,0,0,.45); border:none; padding:0; cursor:pointer; }' - + '.sts-mini-sheet-panel { position:relative; z-index:1; background:' + STS_BG + '; border-radius:16px 16px 0 0; max-height:min(78vh,560px); display:flex; flex-direction:column; box-shadow:0 -8px 28px rgba(15,23,42,.14); animation:sts-sheet-up .28s ease; }' - + '.sts-mini-sheet-handle { width:36px; height:4px; background:rgba(0,0,0,.12); border-radius:999px; margin:10px auto 0; flex-shrink:0; }' - + '.sts-mini-sheet-head { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:8px 16px 12px; border-bottom:1px solid ' + STS_LINE + '; flex-shrink:0; }' - + '.sts-mini-sheet-title { font-size:16px; font-weight:700; color:' + STS_TEXT + '; }' - + '.sts-mini-sheet-close { width:32px; height:32px; border:none; background:' + STS_PAGE + '; border-radius:999px; font-size:20px; line-height:1; color:' + STS_MUTED + '; cursor:pointer; flex-shrink:0; touch-action:manipulation; }' - + '.sts-mini-sheet-body { flex:1; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:12px 16px 8px; }' - + '.sts-mini-sheet-foot { flex-shrink:0; padding:10px 16px calc(10px + env(safe-area-inset-bottom,0px)); border-top:1px solid ' + STS_LINE + '; display:flex; gap:10px; }' - + '.sts-sheet-hint { font-size:13px; color:' + STS_MUTED + '; line-height:1.55; margin-bottom:12px; padding:8px 10px; background:' + STS_PAGE + '; border-radius:8px; }' - + '.sts-action-group { border-radius:12px; overflow:hidden; border:1px solid ' + STS_LINE + '; background:' + STS_BG + '; }' - + '.sts-action-item { display:block; width:100%; min-height:52px; padding:0 16px; border:none; border-bottom:1px solid ' + STS_LINE + '; background:' + STS_BG + '; font-size:16px; font-weight:600; color:' + STS_TEXT + '; cursor:pointer; touch-action:manipulation; }' - + '.sts-action-item:last-child { border-bottom:none; }' - + '.sts-action-item:active { background:' + STS_PAGE + '; }' - + '.sts-action-item--primary { color:' + STS_GREEN_DEEP + '; }' - + '.sts-sheet-cancel-wrap { padding:8px 16px calc(12px + env(safe-area-inset-bottom,0px)); background:' + STS_PAGE + '; }' - + '.sts-action-cancel { display:block; width:100%; min-height:48px; border:none; border-radius:12px; background:' + STS_BG + '; font-size:16px; font-weight:600; color:' + STS_TEXT_SEC + '; cursor:pointer; touch-action:manipulation; box-shadow:0 1px 4px rgba(15,23,42,.06); }' - + '.sts-action-cancel:active { background:' + STS_PAGE + '; }' - + '.sts-form-field { margin-bottom:14px; }' - + '.sts-form-input { width:100%; min-height:44px; border:1px solid ' + STS_LINE + '; border-radius:10px; padding:0 12px; font-size:15px; box-sizing:border-box; outline:none; background:' + STS_BG + '; color:' + STS_TEXT + '; }' - + '.sts-form-input:focus { border-color:' + STS_GREEN + '; box-shadow:0 0 0 2px ' + STS_GREEN_SOFT + '; }' - + '.sts-drawer-foot-btn { flex:1; min-height:44px; border-radius:10px; font-size:15px; font-weight:600; cursor:pointer; border:none; touch-action:manipulation; }' - + '.sts-drawer-foot-cancel { background:' + STS_PAGE + '; color:' + STS_TEXT_SEC + '; border:1px solid ' + STS_LINE + '; }' - + '.sts-drawer-foot-confirm { background:' + STS_GREEN_DEEP + '; color:#fff; }' - + '.sts-camera { position:absolute; inset:0; z-index:50; background:#000; display:flex; flex-direction:column; }' - + '.sts-camera-view { flex:1; display:flex; align-items:center; justify-content:center; color:#fff; font-size:14px; padding:24px; text-align:center; white-space:pre-line; line-height:1.6; }' - + '.sts-camera-bar { padding:16px 20px calc(16px + env(safe-area-inset-bottom,0px)); display:flex; gap:12px; justify-content:center; background:rgba(0,0,0,.55); }' - + '.sts-camera-btn { min-width:96px; min-height:44px; border-radius:22px; font-size:15px; font-weight:600; border:none; cursor:pointer; touch-action:manipulation; }' - + '.sts-camera-btn--ghost { background:rgba(255,255,255,.16); color:#fff; }' - + '.sts-camera-btn--primary { background:' + STS_GREEN + '; color:#fff; min-width:120px; }' - + '@keyframes sts-sheet-up { from { transform:translateY(100%); } to { transform:translateY(0); } }' - + '@media (prefers-reduced-motion: reduce) { .sts-mini-sheet-panel { animation:none; } }'; - - var MOCK_ID_OCR = { name: '张伟', idNo: '330102199001011234' }; - - var stepState = useState(1); - var step = stepState[0]; - var setStep = stepState[1]; - - var phoneState = useState(''); - var verifyCodeState = useState(''); - var codeCountdownState = useState(0); - var phone = phoneState[0]; - var setPhone = phoneState[1]; - var verifyCode = verifyCodeState[0]; - var setVerifyCode = verifyCodeState[1]; - var codeCountdown = codeCountdownState[0]; - var setCodeCountdown = codeCountdownState[1]; - - var completedPhonesState = useState({ '13800138000': 'TC-2026-8888', '13900139000': 'TC-2026-9999' }); - var completedPhones = completedPhonesState[0]; - var setCompletedPhones = completedPhonesState[1]; - - var driverNameState = useState(''); - var driverIdNoState = useState(''); - var idFrontState = useState(null); - var idBackState = useState(null); - var licenseFrontState = useState(null); - var licenseBackState = useState(null); - var qualificationState = useState(null); - var needQualification = false; - - var driverName = driverNameState[0]; - var setDriverName = driverNameState[1]; - var driverIdNo = driverIdNoState[0]; - var setDriverIdNo = driverIdNoState[1]; - var idFront = idFrontState[0]; - var setIdFront = idFrontState[1]; - var idBack = idBackState[0]; - var setIdBack = idBackState[1]; - var licenseFront = licenseFrontState[0]; - var setLicenseFront = licenseFrontState[1]; - var licenseBack = licenseBackState[0]; - var setLicenseBack = licenseBackState[1]; - var qualification = qualificationState[0]; - var setQualification = qualificationState[1]; - - var idDrawerOpenState = useState(false); - var idDrawerSideState = useState(null); - var idDrawerModeState = useState(null); - var idDrawerOpen = idDrawerOpenState[0]; - var setIdDrawerOpen = idDrawerOpenState[1]; - var idDrawerSide = idDrawerSideState[0]; - var setIdDrawerSide = idDrawerSideState[1]; - var idDrawerMode = idDrawerModeState[0]; - var setIdDrawerMode = idDrawerModeState[1]; - - var manualFrontPhotoState = useState(null); - var manualBackPhotoState = useState(null); - var manualFrontPhoto = manualFrontPhotoState[0]; - var setManualFrontPhoto = manualFrontPhotoState[1]; - var manualBackPhoto = manualBackPhotoState[0]; - var setManualBackPhoto = manualBackPhotoState[1]; - - var cameraOpenState = useState(false); - var cameraTargetState = useState(null); - var ocrLoadingState = useState(false); - var cameraOpen = cameraOpenState[0]; - var setCameraOpen = cameraOpenState[1]; - var cameraTarget = cameraTargetState[0]; - var setCameraTarget = cameraTargetState[1]; - var ocrLoading = ocrLoadingState[0]; - var setOcrLoading = ocrLoadingState[1]; - - var cameraFileRef = useRef(null); - - var videoProgressState = useState(0); - var videoPlayingState = useState(false); - var videoProgress = videoProgressState[0]; - var setVideoProgress = videoProgressState[1]; - var videoPlaying = videoPlayingState[0]; - var setVideoPlaying = videoPlayingState[1]; - - var pickupCodeState = useState(''); - var pickupCode = pickupCodeState[0]; - var setPickupCode = pickupCodeState[1]; - - useEffect(function () { - var styleId = 'sts-page-style'; - if (!document.getElementById(styleId)) { - var styleEl = document.createElement('style'); - styleEl.id = styleId; - styleEl.textContent = STS_PAGE_STYLE; - document.head.appendChild(styleEl); - } - }, []); - - useEffect(function () { - if (codeCountdown <= 0) return; - var t = setTimeout(function () { setCodeCountdown(function (c) { return c - 1; }); }, 1000); - return function () { clearTimeout(t); }; - }, [codeCountdown]); - - useEffect(function () { - if (!videoPlaying || videoProgress >= 100) { - if (videoProgress >= 100) setVideoPlaying(false); - return; - } - var t = setInterval(function () { - setVideoProgress(function (p) { return Math.min(100, p + 2); }); - }, 500); - return function () { clearInterval(t); }; - }, [videoPlaying, videoProgress]); - - var handleStep1Next = useCallback(function () { - var p = (phone || '').trim(); - var c = (verifyCode || '').trim(); - if (!p) { message.warning('请输入手机号'); return; } - if (!c) { message.warning('请输入验证码'); return; } - if (completedPhones[p]) { - setPickupCode(completedPhones[p]); - setStep(4); - return; - } - if (c.length < 4) { message.warning('请输入正确的验证码'); return; } - setStep(2); - }, [phone, verifyCode, completedPhones]); - - var handleSendCode = useCallback(function () { - var p = (phone || '').trim(); - if (!p || p.length < 11) { message.warning('请输入正确手机号'); return; } - if (codeCountdown > 0) return; - setCodeCountdown(60); - message.success('验证码已发送'); - }, [phone, codeCountdown]); - - var openIdDrawer = useCallback(function (side) { - setIdDrawerSide(side); - setIdDrawerMode(null); - setManualFrontPhoto(null); - setManualBackPhoto(null); - setIdDrawerOpen(true); - }, []); - - var closeIdDrawer = useCallback(function () { - setIdDrawerOpen(false); - setIdDrawerSide(null); - setIdDrawerMode(null); - setManualFrontPhoto(null); - setManualBackPhoto(null); - }, []); - - var openCamera = useCallback(function (target) { - setCameraTarget(target); - setCameraOpen(true); - }, []); - - var closeCamera = useCallback(function () { - setCameraOpen(false); - setCameraTarget(null); - setOcrLoading(false); - }, []); - - var handleIdRecognize = useCallback(function () { - if (!idDrawerSide) return; - setIdDrawerOpen(false); - openCamera(idDrawerSide === 'front' ? 'idFrontOcr' : 'idBackPhoto'); - }, [idDrawerSide, openCamera]); - - var handleIdManual = useCallback(function () { - setIdDrawerMode('manual'); - }, []); - - var simulateOcrAndUpload = useCallback(function () { - setOcrLoading(true); - setTimeout(function () { - setDriverName(MOCK_ID_OCR.name); - setDriverIdNo(MOCK_ID_OCR.idNo); - setIdFront('身份证正面-已拍摄.jpg'); - setOcrLoading(false); - closeCamera(); - message.success('身份证正面识别成功,已自动填写司机信息'); - }, 1200); - }, [closeCamera]); - - var handleCameraCapture = useCallback(function () { - if (!cameraTarget) return; - if (cameraTarget === 'idFrontOcr') { - simulateOcrAndUpload(); - return; - } - var labelMap = { - idBackPhoto: ['身份证反面', setIdBack], - licenseFront: ['驾驶证正面', setLicenseFront], - licenseBack: ['驾驶证反面', setLicenseBack], - qualification: ['从业资格证', setQualification], - manualIdFront: ['身份证正面', setManualFrontPhoto], - manualIdBack: ['身份证反面', setManualBackPhoto] - }; - var item = labelMap[cameraTarget]; - if (item) { - item[1](item[0] + '-已拍摄.jpg'); - closeCamera(); - message.success(item[0] + '拍摄完成'); - } - }, [cameraTarget, simulateOcrAndUpload, closeCamera]); - - var handleManualSubmit = useCallback(function () { - var name = (driverName || '').trim(); - var idNo = (driverIdNo || '').trim(); - if (!name) { message.warning('请输入司机姓名'); return; } - if (!idNo || idNo.length < 15) { message.warning('请输入正确的身份证号'); return; } - if (!manualFrontPhoto) { message.warning('请拍摄身份证正面照片'); return; } - setIdFront(manualFrontPhoto); - if (idDrawerSide === 'back' || manualBackPhoto) { - setIdBack(manualBackPhoto || idBack); - } - closeIdDrawer(); - message.success('人工登记信息已保存'); - }, [driverName, driverIdNo, manualFrontPhoto, manualBackPhoto, idDrawerSide, idBack, closeIdDrawer]); - - var handleManualBackOnly = useCallback(function () { - if (!manualBackPhoto) { message.warning('请拍摄身份证反面照片'); return; } - setIdBack(manualBackPhoto); - closeIdDrawer(); - message.success('身份证反面上传完成'); - }, [manualBackPhoto, closeIdDrawer]); - - function renderUpload(label, value, onClick, hint) { - var done = value != null && value !== ''; - return React.createElement('div', { - key: label, - className: 'sts-upload' + (done ? ' done' : ''), - onClick: onClick, - role: 'button', - tabIndex: 0, - onKeyDown: function (e) { if (e.key === 'Enter') onClick(); } - }, done ? (value + ' ✓') : (hint || ('点击上传 ' + label))); - } - - function renderCameraOnlyUpload(label, value, setValue, targetKey) { - var done = value != null && value !== ''; - return renderUpload(label, value, function () { - if (done) { setValue(null); return; } - openCamera(targetKey); - }, done ? null : ('点击拍摄 ' + label)); - } - - var allRequiredUploaded = (idFront != null && idFront !== '') && - (idBack != null && idBack !== '') && - (licenseFront != null && licenseFront !== '') && - (licenseBack != null && licenseBack !== '') && - (driverName != null && driverName.trim() !== '') && - (driverIdNo != null && driverIdNo.trim() !== '') && - (!needQualification || (qualification != null && qualification !== '')); - - var handleStep2WatchVideo = useCallback(function () { - if (!allRequiredUploaded) { message.warning('请完成全部必填证照上传'); return; } - setStep(3); - setVideoProgress(0); - setVideoPlaying(false); - }, [allRequiredUploaded]); - - var handleVideoPlayPause = useCallback(function () { - if (videoProgress >= 100) return; - setVideoPlaying(function (v) { return !v; }); - }, [videoProgress]); - - var handleStep3Generate = useCallback(function () { - if (videoProgress < 100) { message.warning('请完整观看安全培训视频'); return; } - var code = 'TC-' + new Date().getFullYear() + '-' + Math.floor(1000 + Math.random() * 9000); - setPickupCode(code); - setCompletedPhones(function (prev) { - var next = {}; for (var k in prev) next[k] = prev[k]; next[phone.trim()] = code; return next; - }); - setStep(4); - message.success('提车码已生成'); - }, [videoProgress, phone]); - - var inputStyle = { width: '100%', minHeight: 44, fontSize: 15, borderRadius: 10 }; - - var stepsItems = [ - { title: '验证' }, - { title: '证照' }, - { title: '视频' }, - { title: '提车码' } - ]; - - var step1Content = React.createElement('div', { className: 'sts-card' }, - React.createElement('div', { className: 'sts-step-title' }, '手机号验证'), - React.createElement('div', { style: { marginBottom: 14 } }, - React.createElement('label', { className: 'sts-label' }, '手机号'), - React.createElement(Input, { - placeholder: '请输入手机号', - value: phone, - onChange: function (e) { setPhone(e.target.value); }, - style: inputStyle, - maxLength: 11, - type: 'tel' - }) - ), - React.createElement('div', { style: { marginBottom: 14 } }, - React.createElement('label', { className: 'sts-label' }, '验证码'), - React.createElement('div', { style: { display: 'flex', gap: 8 } }, - React.createElement(Input, { - placeholder: '请输入验证码', - value: verifyCode, - onChange: function (e) { setVerifyCode(e.target.value); }, - style: Object.assign({}, inputStyle, { flex: 1 }), - maxLength: 6 - }), - React.createElement(Button, { - disabled: codeCountdown > 0, - onClick: handleSendCode, - style: { minHeight: 44, minWidth: 100, borderRadius: 10 } - }, codeCountdown > 0 ? codeCountdown + 's' : '获取验证码') - ) - ), - React.createElement(Button, { type: 'primary', size: 'large', className: 'sts-btn-block', onClick: handleStep1Next }, '下一步') - ); - - var driverInfoBlock = (driverName || driverIdNo) ? React.createElement('div', { className: 'sts-driver-info' }, - React.createElement('div', null, '司机姓名:' + (driverName || '—')), - React.createElement('div', null, '身份证号:' + (driverIdNo || '—')) - ) : null; - - var step2Content = React.createElement('div', { className: 'sts-card' }, - React.createElement('div', { className: 'sts-step-title' }, '证照上传'), - React.createElement('div', { className: 'sts-hint' }, '身份证支持图片识别或人工登记;驾驶证仅拍照上传,不做识别。'), - driverInfoBlock, - React.createElement('div', { className: 'sts-upload-list' }, - renderUpload('身份证正面', idFront, function () { - if (idFront) { setIdFront(null); return; } - openIdDrawer('front'); - }, idFront ? null : '点击上传身份证正面'), - renderUpload('身份证反面', idBack, function () { - if (idBack) { setIdBack(null); return; } - openIdDrawer('back'); - }, idBack ? null : '点击上传身份证反面'), - renderCameraOnlyUpload('驾驶证正面', licenseFront, setLicenseFront, 'licenseFront'), - renderCameraOnlyUpload('驾驶证反面', licenseBack, setLicenseBack, 'licenseBack'), - renderCameraOnlyUpload('从业资格证(18吨以上,可选)', qualification, setQualification, 'qualification') - ), - React.createElement(Button, { - type: 'primary', - size: 'large', - className: 'sts-btn-block', - onClick: handleStep2WatchVideo, - disabled: !allRequiredUploaded - }, '观看安全培训视频') - ); - - var idDrawerTitle = idDrawerMode === 'manual' - ? (idDrawerSide === 'back' ? '人工登记 · 身份证反面' : '人工登记 · 身份证信息') - : ('上传' + (idDrawerSide === 'back' ? '身份证反面' : '身份证正面')); - - var idDrawerBody = idDrawerMode === 'manual' - ? (idDrawerSide === 'back' - ? React.createElement('div', null, - React.createElement('div', { className: 'sts-sheet-hint' }, '请拍摄身份证反面照片,不做 OCR 识别。'), - renderUpload('身份证反面照片', manualBackPhoto, function () { - openCamera('manualIdBack'); - }, manualBackPhoto ? null : '点击拍摄身份证反面照片') - ) - : React.createElement('div', null, - React.createElement('div', { className: 'sts-form-field' }, - React.createElement('label', { className: 'sts-label' }, '司机姓名'), - React.createElement('input', { - className: 'sts-form-input', - placeholder: '请输入司机姓名', - value: driverName, - onChange: function (e) { setDriverName(e.target.value); } - }) - ), - React.createElement('div', { className: 'sts-form-field' }, - React.createElement('label', { className: 'sts-label' }, '身份证号'), - React.createElement('input', { - className: 'sts-form-input', - placeholder: '请输入身份证号', - value: driverIdNo, - onChange: function (e) { setDriverIdNo(e.target.value); }, - maxLength: 18 - }) - ), - React.createElement('div', { className: 'sts-form-field' }, - React.createElement('label', { className: 'sts-label' }, '身份证正面照片'), - renderUpload('身份证正面照片', manualFrontPhoto, function () { - openCamera('manualIdFront'); - }, manualFrontPhoto ? null : '点击拍摄身份证正面照片') - ) - )) - : React.createElement('div', null, - React.createElement('div', { className: 'sts-sheet-hint' }, - idDrawerSide === 'back' - ? '图片识别将调用相机拍摄反面,仅上传照片不做识别。' - : '图片识别将调用相机拍摄正面,自动 OCR 识别姓名与身份证号。' - ), - React.createElement('div', { className: 'sts-action-group' }, - React.createElement('button', { - type: 'button', - className: 'sts-action-item sts-action-item--primary', - onClick: handleIdRecognize - }, '图片识别'), - React.createElement('button', { - type: 'button', - className: 'sts-action-item', - onClick: handleIdManual - }, '人工登记') - ) - ); - - var idDrawerFoot = idDrawerMode === 'manual' - ? React.createElement('div', { className: 'sts-mini-sheet-foot' }, - React.createElement('button', { type: 'button', className: 'sts-drawer-foot-btn sts-drawer-foot-cancel', onClick: function () { setIdDrawerMode(null); } }, '返回'), - React.createElement('button', { - type: 'button', - className: 'sts-drawer-foot-btn sts-drawer-foot-confirm', - onClick: idDrawerSide === 'back' ? handleManualBackOnly : handleManualSubmit - }, idDrawerSide === 'back' ? '确认上传' : '确认登记') - ) - : React.createElement('div', { className: 'sts-sheet-cancel-wrap' }, - React.createElement('button', { type: 'button', className: 'sts-action-cancel', onClick: closeIdDrawer }, '取消') - ); - - var idMiniSheet = idDrawerOpen ? React.createElement('div', { - className: 'sts-mini-sheet', - role: 'dialog', - 'aria-modal': true, - 'aria-label': idDrawerTitle - }, - React.createElement('button', { type: 'button', className: 'sts-mini-sheet-mask', onClick: closeIdDrawer, 'aria-label': '关闭' }), - React.createElement('div', { className: 'sts-mini-sheet-panel' }, - React.createElement('div', { className: 'sts-mini-sheet-handle', 'aria-hidden': true }), - React.createElement('div', { className: 'sts-mini-sheet-head' }, - React.createElement('span', { className: 'sts-mini-sheet-title' }, idDrawerTitle), - React.createElement('button', { type: 'button', className: 'sts-mini-sheet-close', onClick: closeIdDrawer, 'aria-label': '关闭' }, '×') - ), - React.createElement('div', { className: 'sts-mini-sheet-body' }, idDrawerBody), - idDrawerFoot - ) - ) : null; - - var cameraHint = ocrLoading - ? null - : ('请将证件置于取景框内\n' + ( - cameraTarget === 'idFrontOcr' ? '(正面 · 将自动 OCR 识别)' : - cameraTarget === 'idBackPhoto' ? '(反面 · 仅拍照上传)' : - cameraTarget === 'licenseFront' ? '(驾驶证正面 · 仅拍照)' : - cameraTarget === 'licenseBack' ? '(驾驶证反面 · 仅拍照)' : '(仅拍照上传)' - )); - - var cameraOverlay = cameraOpen ? React.createElement('div', { className: 'sts-camera' }, - React.createElement('div', { className: 'sts-camera-view' }, - ocrLoading - ? React.createElement(Spin, { tip: '正在识别身份证信息...' }) - : cameraHint - ), - React.createElement('div', { className: 'sts-camera-bar' }, - React.createElement('button', { - type: 'button', - className: 'sts-camera-btn sts-camera-btn--ghost', - onClick: closeCamera, - disabled: ocrLoading - }, '取消'), - React.createElement('button', { - type: 'button', - className: 'sts-camera-btn sts-camera-btn--primary', - onClick: handleCameraCapture, - disabled: ocrLoading - }, ocrLoading ? '识别中...' : '拍照') - ), - React.createElement('input', { - ref: cameraFileRef, - type: 'file', - accept: 'image/*', - capture: 'environment', - style: { display: 'none' } - }) - ) : null; - - var step3Content = React.createElement('div', { className: 'sts-card' }, - React.createElement('div', { className: 'sts-step-title' }, '安全培训视频'), - React.createElement('div', { className: 'sts-hint' }, '请完整观看视频,不支持快进快退。'), - React.createElement('div', { - style: { background: '#000', borderRadius: 10, overflow: 'hidden', marginBottom: 14, position: 'relative', paddingBottom: '56.25%', height: 0 }, - onClick: handleVideoPlayPause - }, - React.createElement('div', { style: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 14 } }, - videoProgress >= 100 ? '播放完成' : (videoPlaying ? '播放中... 点击暂停' : '点击播放') - ) - ), - React.createElement('div', { style: { marginBottom: 12 } }, - React.createElement(Progress, { percent: videoProgress, showInfo: true }) - ), - React.createElement('div', { style: { display: 'flex', gap: 8 } }, - React.createElement(Button, { onClick: handleVideoPlayPause, disabled: videoProgress >= 100, style: { flex: 1, minHeight: 44, borderRadius: 10 } }, videoPlaying ? '暂停' : '播放'), - React.createElement(Button, { type: 'primary', disabled: videoProgress < 100, onClick: handleStep3Generate, style: { flex: 1, minHeight: 44, borderRadius: 10 } }, '生成提车码') - ) - ); - - var step4Content = React.createElement('div', { className: 'sts-card', style: { textAlign: 'center' } }, - React.createElement('div', { className: 'sts-step-title' }, '提车码'), - React.createElement('div', { className: 'sts-hint', style: { textAlign: 'left' } }, '小程序扫描提车码后自动拉取司机证件信息。提车码在运维完成扫提车码并交车成功后失效。'), - React.createElement('div', { - style: { fontSize: 26, fontWeight: 700, letterSpacing: 3, padding: '18px 12px', background: STS_PAGE, borderRadius: 12, marginBottom: 14, userSelect: 'all', color: STS_TEXT } - }, pickupCode || '—'), - React.createElement('div', { - style: { width: 148, height: 148, margin: '0 auto 14px', background: STS_PAGE, border: '2px dashed ' + STS_LINE, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', color: STS_MUTED, fontSize: 12 } - }, '提车码二维码'), - (driverName || driverIdNo) ? React.createElement('div', { - className: 'sts-driver-info', - style: { textAlign: 'left' } - }, - React.createElement('div', null, '司机:' + (driverName || '—')), - React.createElement('div', null, '身份证:' + (driverIdNo || '—')) - ) : null - ); - - var stepContents = [step1Content, step2Content, step3Content, step4Content]; - - return React.createElement('div', { className: 'sts-root' }, - React.createElement('div', { className: 'sts-phone' }, - React.createElement('div', { className: 'sts-navbar' }, - React.createElement('span', { className: 'sts-nav-title' }, '安全培训扫码') - ), - React.createElement('div', { className: 'sts-body' }, - React.createElement('div', { className: 'sts-steps' }, - React.createElement(Steps, { current: step - 1, size: 'small', items: stepsItems }) - ), - stepContents[step - 1] - ), - idMiniSheet, - cameraOverlay - ) - ); -}; - -export default Component; diff --git a/src/prototypes/oneos-weekly-report/WeeklyReportPage.tsx b/src/prototypes/oneos-weekly-report/WeeklyReportPage.tsx deleted file mode 100644 index aa7cd02..0000000 --- a/src/prototypes/oneos-weekly-report/WeeklyReportPage.tsx +++ /dev/null @@ -1,767 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import { - AlertTriangle, - CalendarClock, - CheckCircle2, - ChevronDown, - ChevronUp, - Circle, - ClipboardList, - Code2, - Download, - Flag, - FlaskConical, - Layers, - MessageSquareQuote, - Scale, - Target, -} from 'lucide-react'; -import { - EXECUTIVE_DECISIONS, - EXECUTIVE_KPIS, - EXECUTIVE_SUMMARY, - KEY_ISSUES, - MILESTONES, - MINOR_ISSUES, - NEXT_WEEK_PLAN, - REPORT_META, - TRACK_PROGRESS, - type ExecutiveDecision, - type ExecutiveHealth, - type ExecutiveKpi, - type IssueStatus, - type PhaseWorkItem, - type RequirementItem, - type RequirementPriority, - type TrackKey, - type TrackPhase, - type WeeklyIssue, -} from './weekly-report-data'; - -const STATUS_LABEL: Record = { - open: '待处理', - in_progress: '处理中', - resolved: '已闭环', - pending_feedback: '待反馈', -}; - -const STATUS_TONE: Record = { - open: 'wr-badge--gray', - in_progress: 'wr-badge--blue', - resolved: 'wr-badge--green', - pending_feedback: 'wr-badge--amber', -}; - -const TRACK_ICON: Record = { - product: , - dev: , - qa: , -}; - -const MILESTONE_STATUS: Record = { - on_track: { label: '正常', tone: 'wr-badge--green' }, - at_risk: { label: '有风险', tone: 'wr-badge--amber' }, - done: { label: '已完成', tone: 'wr-badge--green' }, - delayed: { label: '延期', tone: 'wr-badge--red' }, -}; - -function WorkReportCard(props: { items: PhaseWorkItem[]; title?: string; emptyText?: string }) { - if (!props.items.length && !props.emptyText) { - return null; - } - return ( -
-
{props.title || '工作汇报'}
- {props.items.length ? ( -
    - {props.items.map(function (item) { - return ( -
  • - - {item.done ? : } - -
    - {item.title} - {item.note ? {item.note} : null} -
    -
  • - ); - })} -
- ) : ( -

{props.emptyText}

- )} -
- ); -} - -function requirementPriorityRank(priority: RequirementPriority): number { - if (priority === '高优先级') { - return 0; - } - if (priority === '中优先级') { - return 1; - } - return 2; -} - -function requirementPriorityTone(priority: RequirementPriority): string { - if (priority === '高优先级') { - return 'wr-req-tag--high'; - } - if (priority === '中优先级') { - return 'wr-req-tag--mid'; - } - return 'wr-req-tag--low'; -} - -function CircularProgressRing(props: { value: number; label: string }) { - var size = 36; - var stroke = 3; - var radius = (size - stroke) / 2; - var circumference = 2 * Math.PI * radius; - var clamped = Math.max(0, Math.min(100, props.value)); - var offset = circumference - (clamped / 100) * circumference; - var center = size / 2; - - return ( - - - = 100 ? ' wr-req-progress__fill--done' : '')} - cx={center} - cy={center} - r={radius} - fill="none" - strokeWidth={stroke} - strokeLinecap="round" - strokeDasharray={circumference} - strokeDashoffset={offset} - transform={'rotate(-90 ' + center + ' ' + center + ')'} - /> - - {clamped}% - - - ); -} - -function RequirementWorkReportCard(props: { title: string; items: RequirementItem[] }) { - if (!props.items.length) { - return null; - } - var sortedItems = [...props.items].sort(function (a, b) { - return requirementPriorityRank(a.priority) - requirementPriorityRank(b.priority); - }); - return ( -
-
{props.title}
-
    - {sortedItems.map(function (item) { - return ( -
  • - -
    -
    - {item.stage} - - {item.priority} - -
    -

    {item.title}

    -
    -
  • - ); - })} -
-
- ); -} - -const EXEC_HEALTH_TONE: Record = { - green: 'wr-exec-health--green', - amber: 'wr-exec-health--amber', - red: 'wr-exec-health--red', -}; - -function ExecutiveKpiCard(props: { kpi: ExecutiveKpi }) { - var kpi = props.kpi; - return ( -
-
-
{kpi.value}
-
{kpi.label}
- {kpi.hint ?
{kpi.hint}
: null} -
-
- ); -} - -function ExecutiveSummarySection() { - var summary = EXECUTIVE_SUMMARY; - return ( -
-
-
-

本周执行结论

-

面向管理层的一页摘要,可直接用于周会汇报

-
- {summary.healthLabel} -
-

{summary.headline}

-
-
-

- - 本周成效 -

-
    - {summary.achievements.map(function (item, index) { - return
  • {item}
  • ; - })} -
-
-
-

- - 主要风险 -

-
    - {summary.risks.map(function (item, index) { - return
  • {item}
  • ; - })} -
-
-
-
- ); -} - -function ExecutiveDecisionCard(props: { decision: ExecutiveDecision }) { - var decision = props.decision; - return ( -
-
-

{decision.title}

- - {decision.urgency === 'high' ? '紧急' : '一般'} - -
-
-
-
业务影响
-
{decision.impact}
-
-
-
建议决策
-
{decision.recommendation}
-
-
-
期望时限
-
{decision.deadline}
-
-
-
- ); -} - -function NextWeekPanel() { - var goals = NEXT_WEEK_PLAN.filter(function (item) { return item.type === 'goal'; }); - var risks = NEXT_WEEK_PLAN.filter(function (item) { return item.type === 'risk'; }); - return ( -
-
-

- - 下周必达 -

-
    - {goals.map(function (item) { - return ( -
  • - {item.title} - {item.detail} -
  • - ); - })} -
-
-
-

- - 下周风险 -

-
    - {risks.map(function (item) { - return ( -
  • - {item.title} - {item.detail} -
  • - ); - })} -
-
-
- ); -} - -function ProgressBar(props: { value: number; label: string }) { - return ( -
-
- {props.label} - {props.value}% -
-
-
-
-
- ); -} - -function FourRGrid(props: { fourR: WeeklyIssue['fourR'] }) { - return ( -
-
-
原因
-
{props.fourR.reason}
-
-
-
措施
-
{props.fourR.measure}
-
-
-
结果
-
{props.fourR.result}
-
-
-
反馈
-
{props.fourR.feedback}
-
-
- ); -} - -function PhaseCard(props: { phase: TrackPhase }) { - var phase = props.phase; - var tabs = phase.tabs || []; - var tabState = useState(tabs[0] ? tabs[0].key : ''); - var activeTabKey = tabState[0]; - var setActiveTabKey = tabState[1]; - - var activeTab = useMemo(function () { - return tabs.find(function (t) { return t.key === activeTabKey; }) || tabs[0]; - }, [tabs, activeTabKey]); - - if (!tabs.length) { - return ( -
-

{phase.label}

-
- {phase.done} / {phase.count} -
- {phase.note ?

{phase.note}

: null} -
- ); - } - - return ( -
-

{phase.label}

-
- {tabs.map(function (tab) { - return ( - - ); - })} -
- {activeTab ? ( -
- {phase.key === 'prototype' && activeTab.requirementSections ? ( - activeTab.requirementSections.map(function (section) { - return ( - - ); - }) - ) : phase.key === 'prototype' ? ( - <> - - - - ) : (activeTab.workReport?.length || activeTab.emptyText) ? ( - - ) : null} -
- ) : null} -
- ); -} - -function PhaseModuleCards(props: { phases: TrackPhase[] }) { - return ( -
- {props.phases.map(function (phase) { - return ; - })} -
- ); -} - -function TrackSummaryCard(props: { - track: (typeof TRACK_PROGRESS)[number]; - active: boolean; - onSelect: () => void; -}) { - var track = props.track; - var subLabel = track.lines ? 'OneOS · BI' : track.phases ? track.planned + ' 项需求' : null; - return ( - - ); -} - -function TrackDetailPanel(props: { track: (typeof TRACK_PROGRESS)[number] }) { - var track = props.track; - if (track.phases) { - return ; - } - return ( -
-
-

{track.label} · 详细进展

- -
- {track.lines ? ( -
- {track.lines.map(function (line) { - return ( -
-

{line.label}

- -
    -
  • 计划{line.planned}
  • -
  • 完成{line.done}
  • -
  • 阻塞 0 ? ' wr-text-warning' : '')}>{line.blocked}
  • -
-

{line.highlight}

-
- ); - })} -
- ) : ( - <> -
    -
  • 计划{track.planned}
  • -
  • 完成{track.done}
  • -
  • 阻塞 0 ? ' wr-text-warning' : '')}>{track.blocked}
  • -
- {track.highlight ?

{track.highlight}

: null} - - )} -
- ); -} - -function KeyIssueCard(props: { issue: WeeklyIssue }) { - var issue = props.issue; - return ( -
-
-
- {issue.priority} - {STATUS_LABEL[issue.status]} - {issue.track === 'product' ? '产品' : issue.track === 'dev' ? '开发' : '测试'} -
-

{issue.title}

-

- {issue.module} - · - {issue.owner} - · - 更新 {issue.updatedAt} -

-
- -
- ); -} - -export default function WeeklyReportPage() { - var minorOpenState = useState(false); - var minorOpen = minorOpenState[0]; - var setMinorOpen = minorOpenState[1]; - - var activeTrackState = useState('product'); - var activeTrack = activeTrackState[0]; - var setActiveTrack = activeTrackState[1]; - - var activeTrackData = useMemo(function () { - return TRACK_PROGRESS.find(function (t) { return t.key === activeTrack; }) || TRACK_PROGRESS[0]; - }, [activeTrack]); - - return ( -
-
-
-

{REPORT_META.department} · OneOS

-

每周工作汇报

-

- {REPORT_META.weekLabel} - · - {REPORT_META.weekRange} -

-
-
-
- 汇报:{REPORT_META.reporter} - 生成:{REPORT_META.reportDate} -
- -
-
- -
- {EXECUTIVE_KPIS.map(function (item) { - return ; - })} -
- - - -
-
-

各条线进度

-
-
- {TRACK_PROGRESS.map(function (track) { - return ( - - ); - })} -
-
- -
-
- -
-
-

- - 需董事长 / 高管决策 -

-

需在本周期内明确方向或授权的事项,附建议方案与时限

-
-
- {EXECUTIVE_DECISIONS.map(function (decision) { - return ; - })} -
-
- -
-
-

- - 下周工作重点与风险 -

-

项目经理视角的下周执行焦点

-
- -
- -
-
-

重点问题(P0 / P1)

-
-
- {KEY_ISSUES.map(function (issue) { - return ; - })} -
-
- -
-
-

里程碑进度

-

OneOS 各模块关键节点与完成度

-
-
-
- - - - - - - - - - - - - {MILESTONES.map(function (row) { - var st = MILESTONE_STATUS[row.status]; - return ( - - - - - - - - - - ); - })} - -
模块里程碑负责人计划完成实际完成进度状态
{row.module}{row.milestone}{row.owner}{row.planDate}{row.actualDate || '—'} -
-
-
-
- {row.progress}% -
-
{st.label}
-
- - -
- - {minorOpen ? ( -
- - - - - - - - - - - - - - {MINOR_ISSUES.map(function (issue) { - return ( - - - - - - - - - - ); - })} - -
编号模块问题条线负责人状态更新
{issue.id}{issue.module}{issue.title}{issue.track === 'product' ? '产品' : issue.track === 'dev' ? '开发' : '测试'}{issue.owner}{STATUS_LABEL[issue.status]}{issue.updatedAt}
-
- ) : ( -
- {MINOR_ISSUES.map(function (issue) { - return ( - - {issue.module}:{issue.title.slice(0, 12)}{issue.title.length > 12 ? '…' : ''} - - ); - })} -
- )} -
- -
-

OneOS 数智部 · 每周汇报 · 数据为原型演示,正式环境对接项目管理与缺陷平台

-
-
- ); -} diff --git a/src/prototypes/oneos-weekly-report/index.tsx b/src/prototypes/oneos-weekly-report/index.tsx deleted file mode 100644 index 4ab0cc7..0000000 --- a/src/prototypes/oneos-weekly-report/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @name 数智部周报 - */ -import './styles/weekly-report.css'; -import WeeklyReportPage from './WeeklyReportPage'; - -export default WeeklyReportPage; diff --git a/src/prototypes/oneos-weekly-report/styles/linear-theme.css b/src/prototypes/oneos-weekly-report/styles/linear-theme.css deleted file mode 100644 index d935662..0000000 --- a/src/prototypes/oneos-weekly-report/styles/linear-theme.css +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Linear design tokens — scoped to .wr-page only. - * Source: src/themes/linear/DESIGN.md + assets/tokens.json - */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); - -.wr-page { - color-scheme: dark; - - /* Brand */ - --wr-ln-primary: #5e6ad2; - --wr-ln-primary-hover: #828fff; - --wr-ln-primary-focus: #5e69d1; - --wr-ln-on-primary: #ffffff; - --wr-ln-brand-secure: #7a7fad; - - /* Surfaces */ - --wr-ln-canvas: #010102; - --wr-ln-surface-1: #0f1011; - --wr-ln-surface-2: #141516; - --wr-ln-surface-3: #18191a; - --wr-ln-surface-4: #191a1b; - - /* Ink — bumped for readability on dark surfaces */ - --wr-ln-ink: #f7f8f8; - --wr-ln-ink-muted: #dce1e8; - --wr-ln-ink-subtle: #adb3bf; - --wr-ln-ink-tertiary: #7a808c; - - /* Hairlines — slightly stronger separation */ - --wr-ln-hairline: #2c2f36; - --wr-ln-hairline-strong: #3f434c; - --wr-ln-hairline-tertiary: #4a4e58; - - /* Semantic */ - --wr-ln-success: #3ecf5f; - --wr-ln-warning: #f0b45a; - --wr-ln-error: #ff7b82; - --wr-ln-icon: #9499e8; - --wr-ln-icon-active: #a8afff; - - /* Radius */ - --wr-ln-radius-xs: 4px; - --wr-ln-radius-sm: 6px; - --wr-ln-radius-md: 8px; - --wr-ln-radius-lg: 12px; - --wr-ln-radius-xl: 16px; - --wr-ln-radius-pill: 9999px; - - /* Typography */ - --wr-ln-font: Inter, -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; - --wr-ln-font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; - --wr-ln-font-size-base: 14px; - - /* Component aliases (weekly report) */ - --wr-ln-font-family: var(--wr-ln-font); - --wr-ln-color-primary: var(--wr-ln-primary); - --wr-ln-color-primary-hover: var(--wr-ln-primary-hover); - --wr-ln-color-primary-light: rgba(94, 106, 210, 0.2); - --wr-ln-color-primary-border: rgba(130, 143, 255, 0.45); - --wr-ln-color-primary-shadow: rgba(94, 106, 210, 0.45); - --wr-ln-color-text: var(--wr-ln-ink); - --wr-ln-color-text-muted: var(--wr-ln-ink-subtle); - --wr-ln-color-text-secondary: var(--wr-ln-ink-muted); - --wr-ln-color-icon: var(--wr-ln-icon); - --wr-ln-color-icon-active: var(--wr-ln-icon-active); - --wr-ln-color-bg-page: var(--wr-ln-canvas); - --wr-ln-color-bg-card: var(--wr-ln-surface-1); - --wr-ln-color-bg-muted: var(--wr-ln-surface-2); - --wr-ln-color-bg-hover: var(--wr-ln-surface-3); - --wr-ln-color-bg-page-alt: var(--wr-ln-surface-2); - --wr-ln-color-border: var(--wr-ln-hairline); - --wr-ln-color-border-light: var(--wr-ln-hairline); - --wr-ln-color-border-strong: var(--wr-ln-hairline-strong); - --wr-ln-color-success: var(--wr-ln-success); - --wr-ln-color-success-bg: rgba(62, 207, 95, 0.18); - --wr-ln-color-success-text: #84eb9a; - --wr-ln-color-warning: var(--wr-ln-warning); - --wr-ln-color-error: var(--wr-ln-error); - --wr-ln-color-error-bg: rgba(255, 99, 105, 0.14); - --wr-ln-color-error-border: rgba(255, 99, 105, 0.35); - --wr-ln-color-info: var(--wr-ln-primary); -} diff --git a/src/prototypes/oneos-weekly-report/styles/weekly-report.css b/src/prototypes/oneos-weekly-report/styles/weekly-report.css deleted file mode 100644 index 54913c5..0000000 --- a/src/prototypes/oneos-weekly-report/styles/weekly-report.css +++ /dev/null @@ -1,1397 +0,0 @@ - -@import './linear-theme.css'; - -.wr-page { - box-sizing: border-box; - width: 100%; - min-height: 100vh; - padding: clamp(16px, 2vw, 24px) clamp(16px, 3vw, 32px) clamp(32px, 4vw, 48px); - font-family: var(--wr-ln-font-family); - font-size: var(--wr-ln-font-size-base, 14px); - color: var(--wr-ln-color-text); - background: var(--wr-ln-canvas); - -webkit-font-smoothing: antialiased; -} - -/* Icons: clearer strokes on dark canvas */ -.wr-page svg { - stroke-width: 2.25; -} - -.wr-hero { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 20px; - flex-wrap: wrap; - margin-bottom: 24px; - padding-bottom: 20px; - border-bottom: 1px solid var(--wr-ln-hairline); -} - -.wr-hero__eyebrow { - margin: 0 0 4px; - font-size: 13px; - font-weight: 500; - color: var(--wr-ln-ink-muted); - letter-spacing: 0.04em; - text-transform: uppercase; -} - -.wr-hero__title { - margin: 0; - font-size: clamp(1.75rem, 3vw, 2rem); - font-weight: 600; - letter-spacing: -0.02em; - line-height: 1.2; - color: var(--wr-ln-ink); -} - -.wr-hero__sub { - margin: 8px 0 0; - font-size: 15px; - color: var(--wr-ln-ink-muted); - letter-spacing: -0.01em; -} - -.wr-hero__aside { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 12px; -} - -.wr-hero__meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 4px; - font-size: 13px; - color: var(--wr-ln-color-text-secondary); -} - -.wr-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - min-height: 44px; - padding: 8px 14px; - border-radius: var(--wr-ln-radius-md, 8px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-1); - color: var(--wr-ln-ink); - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: background 0.2s ease, border-color 0.2s ease; -} - -.wr-btn:hover { - background: var(--wr-ln-surface-2); - border-color: var(--wr-ln-hairline-strong); -} - -.wr-btn:focus-visible { - outline: 2px solid rgba(94, 105, 209, 0.5); - outline-offset: 2px; -} - -.wr-btn--primary { - background: var(--wr-ln-primary); - border-color: var(--wr-ln-primary); - color: var(--wr-ln-on-primary); -} - -.wr-btn--primary:hover { - background: var(--wr-ln-primary-hover); - border-color: var(--wr-ln-primary-hover); -} - -.wr-principle { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin-bottom: 20px; -} - -.wr-principle__item { - display: flex; - flex-direction: column; - gap: 4px; - padding: 14px 16px; - border-radius: var(--wr-ln-radius-lg, 12px); - border: 1px solid var(--wr-ln-color-primary-border); - background: var(--wr-ln-color-primary-light); -} - -.wr-principle__item strong { - font-size: 15px; - font-weight: 700; - color: var(--wr-ln-color-success-text); -} - -.wr-principle__item span { - font-size: 12px; - color: var(--wr-ln-color-text-muted); -} - -.wr-kpi-row { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin-bottom: 24px; -} - -.wr-kpi-card { - display: flex; - gap: 14px; - padding: 16px; - border-radius: var(--wr-ln-radius-lg, 12px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-1); -} - -.wr-kpi-card__icon { - flex-shrink: 0; - width: 44px; - height: 44px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--wr-ln-radius-md, 8px); - background: var(--wr-ln-surface-3); - border: 1px solid var(--wr-ln-hairline-strong); - color: var(--wr-ln-color-icon); -} - -.wr-kpi-card--primary .wr-kpi-card__icon { - background: var(--wr-ln-color-primary-light); - border-color: var(--wr-ln-color-primary-border); - color: var(--wr-ln-icon-active); -} - -.wr-kpi-card--warning .wr-kpi-card__icon { - background: rgba(240, 180, 90, 0.18); - border-color: rgba(240, 180, 90, 0.35); - color: var(--wr-ln-warning); -} - -.wr-kpi-card--success .wr-kpi-card__icon { - background: var(--wr-ln-color-success-bg); - border-color: rgba(62, 207, 95, 0.35); - color: var(--wr-ln-color-success-text); -} - -.wr-kpi-card__value { - font-size: 28px; - font-weight: 600; - line-height: 1.1; - letter-spacing: -0.02em; - color: var(--wr-ln-ink); -} - -.wr-kpi-card__title { - margin-top: 4px; - font-size: 13px; - font-weight: 500; - color: var(--wr-ln-ink-muted); -} - -.wr-kpi-card__desc { - margin-top: 2px; - font-size: 12px; - color: var(--wr-ln-color-text-muted); - line-height: 1.45; -} - -.wr-kpi-row--exec { - grid-template-columns: repeat(5, minmax(0, 1fr)); -} - -.wr-kpi-card--danger .wr-kpi-card__value { - color: #fca5a5; -} - -.wr-kpi-card--primary .wr-kpi-card__value { - color: var(--wr-ln-primary-hover); -} - -.wr-kpi-card--warning .wr-kpi-card__value { - color: var(--wr-ln-warning); -} - -.wr-kpi-card--success .wr-kpi-card__value { - color: var(--wr-ln-color-success-text); -} - -.wr-exec-summary { - margin-bottom: 28px; - padding: 18px 20px; - border-radius: var(--wr-ln-radius-lg, 12px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-1); -} - -.wr-exec-summary__head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - flex-wrap: wrap; - margin-bottom: 12px; -} - -.wr-exec-health { - display: inline-flex; - align-items: center; - min-height: 32px; - padding: 4px 12px; - border-radius: 999px; - font-size: 13px; - font-weight: 600; - border: 1px solid transparent; - white-space: nowrap; -} - -.wr-exec-health--green { - color: var(--wr-ln-color-success-text); - background: var(--wr-ln-color-success-bg); - border-color: rgba(62, 207, 95, 0.35); -} - -.wr-exec-health--amber { - color: #fcd34d; - background: rgba(245, 158, 11, 0.14); - border-color: rgba(245, 158, 11, 0.3); -} - -.wr-exec-health--red { - color: #fca5a5; - background: rgba(239, 68, 68, 0.14); - border-color: rgba(239, 68, 68, 0.3); -} - -.wr-exec-summary__headline { - margin: 0 0 16px; - font-size: 15px; - line-height: 1.65; - color: var(--wr-ln-ink); -} - -.wr-exec-summary__grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.wr-exec-summary__block { - padding: 12px 14px; - border-radius: var(--wr-ln-radius-md, 8px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-2); -} - -.wr-exec-summary__block--risk { - border-color: rgba(245, 158, 11, 0.28); - background: rgba(245, 158, 11, 0.06); -} - -.wr-exec-summary__block-title { - display: flex; - align-items: center; - gap: 8px; - margin: 0 0 8px; - font-size: 13px; - font-weight: 600; - color: var(--wr-ln-ink-muted); -} - -.wr-exec-summary__list { - margin: 0; - padding-left: 18px; - display: flex; - flex-direction: column; - gap: 6px; - font-size: 13px; - line-height: 1.55; - color: var(--wr-ln-ink); -} - -.wr-section__title-icon { - flex-shrink: 0; - color: var(--wr-ln-ink-muted); -} - -.wr-exec-decision-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.wr-exec-decision { - padding: 14px 16px; - border-radius: var(--wr-ln-radius-md, 8px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-1); -} - -.wr-exec-decision--urgent { - border-color: rgba(239, 68, 68, 0.35); - background: rgba(239, 68, 68, 0.06); -} - -.wr-exec-decision__head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 10px; -} - -.wr-exec-decision__title { - margin: 0; - font-size: 15px; - font-weight: 600; - color: var(--wr-ln-ink); - line-height: 1.4; -} - -.wr-exec-decision__meta { - margin: 0; - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.wr-exec-decision__meta dt { - margin: 0 0 4px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--wr-ln-ink-subtle); -} - -.wr-exec-decision__meta dd { - margin: 0; - font-size: 13px; - line-height: 1.5; - color: var(--wr-ln-ink-muted); -} - -.wr-exec-next { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.wr-exec-next__col { - padding: 14px 16px; - border-radius: var(--wr-ln-radius-md, 8px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-1); -} - -.wr-exec-next__col--risk { - border-color: rgba(245, 158, 11, 0.28); - background: rgba(245, 158, 11, 0.06); -} - -.wr-exec-next__title { - display: flex; - align-items: center; - gap: 8px; - margin: 0 0 10px; - font-size: 14px; - font-weight: 600; - color: var(--wr-ln-ink); -} - -.wr-exec-next__list { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - gap: 10px; -} - -.wr-exec-next__item { - display: flex; - flex-direction: column; - gap: 2px; - font-size: 13px; - line-height: 1.5; - color: var(--wr-ln-ink-muted); -} - -.wr-exec-next__item strong { - color: var(--wr-ln-ink); - font-weight: 600; -} - -.wr-section { - margin-bottom: 28px; -} - -.wr-section__head { - margin-bottom: 14px; -} - -.wr-section__head--inline { - margin-bottom: 0; - text-align: left; -} - -.wr-section__title { - display: flex; - align-items: center; - gap: 8px; - margin: 0; - font-size: 18px; - font-weight: 600; - letter-spacing: -0.01em; - color: var(--wr-ln-ink); -} - -.wr-section__desc { - margin: 4px 0 0; - font-size: 13px; - color: var(--wr-ln-ink-subtle); -} - -.wr-track-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 14px; -} - -.wr-track-switcher { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin-bottom: 14px; -} - -.wr-track-tab { - display: flex; - align-items: flex-start; - gap: 12px; - min-height: 44px; - padding: 14px 16px; - border-radius: var(--wr-ln-radius-lg, 12px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-1); - color: inherit; - text-align: left; - cursor: pointer; - transition: border-color 0.2s ease, background 0.2s ease; -} - -.wr-track-tab:hover { - background: var(--wr-ln-surface-2); - border-color: var(--wr-ln-hairline-strong); -} - -.wr-track-tab:focus-visible { - outline: 2px solid rgba(94, 105, 209, 0.5); - outline-offset: 2px; -} - -.wr-track-tab--active { - border-color: var(--wr-ln-color-primary-border); - background: var(--wr-ln-surface-2); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); -} - -.wr-track-tab__icon { - flex-shrink: 0; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--wr-ln-radius-md, 8px); - background: var(--wr-ln-surface-3); - border: 1px solid var(--wr-ln-hairline-strong); - color: var(--wr-ln-color-icon); -} - -.wr-track-tab--active .wr-track-tab__icon { - background: var(--wr-ln-color-primary-light); - border-color: var(--wr-ln-color-primary-border); - color: var(--wr-ln-icon-active); -} - -.wr-track-tab__body { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.wr-track-tab__title { - font-size: 14px; - font-weight: 600; - color: var(--wr-ln-ink); -} - -.wr-track-tab__value { - font-size: 24px; - font-weight: 800; - line-height: 1.1; - letter-spacing: -0.02em; - color: var(--wr-ln-color-text); -} - -.wr-track-tab__stats { - font-size: 13px; - color: var(--wr-ln-ink-subtle); -} - -.wr-track-tab__hint { - margin-top: 2px; - font-size: 12px; - color: var(--wr-ln-ink-muted); -} - -.wr-track-detail-wrap { - border: 1px solid var(--wr-ln-hairline); - border-radius: var(--wr-ln-radius-lg, 12px); - background: var(--wr-ln-surface-1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); -} - -.wr-track-detail-wrap--flat { - border: 0; - background: transparent; - box-shadow: none; - padding: 0; -} - -.wr-track-detail-wrap--flat .wr-phase-grid { - gap: 12px; -} - -.wr-track-detail-wrap--flat .wr-phase-card { - background: var(--wr-ln-surface-1); - border-color: var(--wr-ln-hairline); - padding: 14px 16px; -} - -.wr-track-detail { - padding: 16px 18px 18px; -} - -.wr-track-detail__head { - margin-bottom: 16px; -} - -.wr-track-detail__title { - margin: 0 0 10px; - font-size: 15px; - font-weight: 600; - color: var(--wr-ln-ink); -} - -.wr-phase-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.wr-phase-card { - padding: 12px 14px; - border-radius: var(--wr-ln-radius-lg, 12px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-2); -} - -.wr-phase-card__title { - margin: 0 0 6px; - font-size: 14px; - font-weight: 600; - color: var(--wr-ln-ink); -} - -.wr-phase-tabs { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-bottom: 8px; - padding: 3px; - border-radius: var(--wr-ln-radius-pill, 9999px); - background: var(--wr-ln-canvas); -} - -.wr-phase-tab { - min-height: 30px; - padding: 6px 14px; - border-radius: var(--wr-ln-radius-pill, 9999px); - border: 0; - background: transparent; - font-size: 12px; - font-weight: 500; - color: var(--wr-ln-ink-subtle); - cursor: pointer; - transition: background 0.2s ease, color 0.2s ease; -} - -.wr-phase-tab:hover { - color: var(--wr-ln-ink); -} - -.wr-phase-tab:focus-visible { - outline: 2px solid rgba(130, 143, 255, 0.65); - outline-offset: 1px; -} - -.wr-phase-tab--active { - background: var(--wr-ln-surface-3); - color: var(--wr-ln-ink); - box-shadow: inset 0 0 0 1px var(--wr-ln-hairline-strong); -} - -.wr-phase-panel { - min-height: 52px; -} - -.wr-phase-panel .wr-work-report + .wr-work-report { - margin-top: 8px; -} - -.wr-work-report { - margin-top: 10px; - padding: 10px 12px; - border-radius: var(--wr-ln-radius-md, 8px); - border: 1px solid var(--wr-ln-hairline); - background: var(--wr-ln-surface-1); -} - -.wr-work-report__title { - margin: 0 0 8px; - font-size: 12px; - font-weight: 600; - color: var(--wr-ln-ink-muted); -} - -.wr-work-report__list { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - gap: 6px; -} - -.wr-work-report__item { - display: flex; - gap: 10px; - align-items: flex-start; - font-size: 13px; - line-height: 1.5; -} - -.wr-work-report__icon { - flex-shrink: 0; - width: 18px; - height: 18px; - margin-top: 1px; - color: var(--wr-ln-ink-subtle); -} - -.wr-work-report__item--done .wr-work-report__icon { - color: var(--wr-ln-color-success-text); -} - -.wr-work-report__body { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.wr-work-report__text { - color: var(--wr-ln-ink); - font-weight: 500; -} - -.wr-work-report__item--done .wr-work-report__text { - color: var(--wr-ln-ink-muted); -} - -.wr-work-report__note { - font-size: 12px; - color: var(--wr-ln-ink-subtle); -} - -.wr-work-report__empty { - margin: 0; - font-size: 13px; - line-height: 1.5; - color: var(--wr-ln-ink-muted); -} - -.wr-req-list { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - gap: 10px; -} - -.wr-req-item { - display: flex; - gap: 10px; - align-items: flex-start; -} - -.wr-req-item__body { - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; - flex: 1; -} - -.wr-req-item__tags { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.wr-req-tag { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 999px; - font-size: 11px; - font-weight: 600; - line-height: 1.3; - border: 1px solid transparent; -} - -.wr-req-tag--stage { - color: var(--wr-ln-primary-hover); - background: color-mix(in srgb, var(--wr-ln-primary) 16%, transparent); - border-color: color-mix(in srgb, var(--wr-ln-primary) 28%, transparent); -} - -.wr-req-tag--priority.wr-req-tag--high { - color: #fca5a5; - background: color-mix(in srgb, #ef4444 14%, transparent); - border-color: color-mix(in srgb, #ef4444 24%, transparent); -} - -.wr-req-tag--priority.wr-req-tag--mid { - color: #fcd34d; - background: color-mix(in srgb, #f59e0b 14%, transparent); - border-color: color-mix(in srgb, #f59e0b 24%, transparent); -} - -.wr-req-tag--priority.wr-req-tag--low { - color: var(--wr-ln-ink-muted); - background: var(--wr-ln-surface-2); - border-color: var(--wr-ln-hairline); -} - -.wr-req-item__text { - margin: 0; - font-size: 13px; - line-height: 1.5; - font-weight: 500; - color: var(--wr-ln-ink); -} - -.wr-req-progress { - flex-shrink: 0; -} - -.wr-req-progress__track { - stroke: var(--wr-ln-hairline); -} - -.wr-req-progress__fill { - stroke: var(--wr-ln-primary); - transition: stroke-dashoffset 0.35s ease; -} - -.wr-req-progress__fill--done { - stroke: var(--wr-ln-color-success-text, #4ade80); -} - -.wr-req-progress__text { - fill: var(--wr-ln-ink-muted); - font-size: 9px; - font-weight: 600; -} - -@media (prefers-reduced-motion: reduce) { - .wr-req-progress__fill { - transition: none; - } -} - -.wr-phase-card__stats { - font-size: 13px; - color: var(--wr-ln-ink-muted); -} - -.wr-phase-card__stats strong { - font-size: 18px; - font-weight: 600; - color: var(--wr-ln-icon-active); -} - -.wr-phase-card__note { - margin: 6px 0 0; - font-size: 12px; - line-height: 1.5; - color: var(--wr-ln-ink-muted); -} - -.wr-track-card { - padding: 16px; - border-radius: var(--wr-ln-radius-lg, 12px); - border: 1px solid var(--wr-ln-color-border); - background: var(--wr-ln-color-bg-card); -} - -.wr-track-card--wide { - grid-column: 1 / -1; -} - -.wr-req-groups { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; - margin-top: 4px; -} - -.wr-req-group__title { - margin: 0 0 10px; - font-size: 13px; - font-weight: 700; - color: var(--wr-ln-color-text-secondary); -} - -.wr-req-list { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - gap: 10px; -} - -.wr-req-item { - display: flex; - gap: 10px; - align-items: flex-start; -} - -.wr-req-item__icon { - flex-shrink: 0; - margin-top: 2px; - color: var(--wr-ln-color-text-muted); -} - -.wr-req-item--done .wr-req-item__icon { - color: var(--wr-ln-color-primary); -} - -.wr-req-item__body { - min-width: 0; - flex: 1; -} - -.wr-req-item__head { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; -} - -.wr-req-item__title { - font-size: 13px; - font-weight: 600; - line-height: 1.45; - color: var(--wr-ln-color-text); -} - -.wr-req-item--done .wr-req-item__title { - color: var(--wr-ln-color-text-muted); -} - -.wr-req-item__note { - margin: 4px 0 0; - font-size: 12px; - line-height: 1.5; - color: var(--wr-ln-color-text-muted); -} - -.wr-track-card__head { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 12px; -} - -.wr-track-card__icon { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - background: var(--wr-ln-color-bg-muted); - color: var(--wr-ln-color-primary); -} - -.wr-track-card__title { - margin: 0; - font-size: 16px; - font-weight: 700; -} - -.wr-track-lines { - display: flex; - flex-direction: column; - gap: 14px; -} - -.wr-track-line { - padding-top: 14px; - border-top: 1px solid var(--wr-ln-color-border-light); -} - -.wr-track-line:first-child { - padding-top: 0; - border-top: 0; -} - -.wr-track-line__title { - margin: 0 0 8px; - font-size: 13px; - font-weight: 700; - color: var(--wr-ln-color-text-secondary); -} - -.wr-progress { - margin-bottom: 12px; -} - -.wr-progress__head { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 6px; - font-size: 12px; - color: var(--wr-ln-color-text-muted); -} - -.wr-progress__pct { - font-weight: 700; - color: var(--wr-ln-color-text); -} - -.wr-progress__track { - height: 8px; - border-radius: 999px; - background: var(--wr-ln-color-bg-page-alt); - overflow: hidden; -} - -.wr-progress__track--sm { - height: 6px; - flex: 1; - min-width: 60px; -} - -.wr-progress__fill { - height: 100%; - border-radius: inherit; - background: var(--wr-ln-primary); - transition: width 0.3s ease; -} - -.wr-track-stats { - display: flex; - gap: 16px; - margin: 0 0 10px; - padding: 0; - list-style: none; -} - -.wr-track-stats li { - display: flex; - flex-direction: column; - gap: 2px; - font-size: 11px; - color: var(--wr-ln-color-text-muted); -} - -.wr-track-stats strong { - font-size: 18px; - font-weight: 600; - color: var(--wr-ln-ink); -} - -.wr-text-warning { - color: var(--wr-ln-color-warning) !important; -} - -.wr-track-card__highlight { - margin: 0; - font-size: 12px; - line-height: 1.55; - color: var(--wr-ln-color-text-secondary); -} - -.wr-issue-list { - display: flex; - flex-direction: column; - gap: 16px; -} - -.wr-issue-card { - padding: 18px 20px; - border-radius: var(--wr-ln-radius-lg, 12px); - border: 1px solid var(--wr-ln-color-border); - background: var(--wr-ln-color-bg-card); - border-left-width: 4px; -} - -.wr-issue-card--p0 { - border-left-color: var(--wr-ln-color-error); -} - -.wr-issue-card--p1 { - border-left-color: var(--wr-ln-color-warning); -} - -.wr-issue-card__head { - margin-bottom: 14px; -} - -.wr-issue-card__tags { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 8px; -} - -.wr-issue-card__title { - margin: 0; - font-size: 16px; - font-weight: 600; - line-height: 1.4; - color: var(--wr-ln-ink); -} - -.wr-issue-card__meta { - margin: 6px 0 0; - font-size: 12px; - color: var(--wr-ln-color-text-muted); - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.wr-four-r-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; - margin: 0; -} - -.wr-four-r-item { - margin: 0; - padding: 12px 14px; - border-radius: 10px; - background: var(--wr-ln-color-bg-muted); - border: 1px solid var(--wr-ln-color-border-light); -} - -.wr-four-r-item dt { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 6px; - font-size: 12px; - font-weight: 600; - color: var(--wr-ln-ink-muted); -} - -.wr-four-r-item dt svg { - color: var(--wr-ln-color-icon); -} - -.wr-four-r-item dd { - margin: 0; - font-size: 14px; - line-height: 1.55; - color: var(--wr-ln-ink); -} - -.wr-four-r-item--feedback { - grid-column: 1 / -1; - background: rgba(94, 106, 210, 0.08); - border-color: rgba(94, 106, 210, 0.28); -} - -.wr-four-r-item--feedback dt { - color: var(--wr-ln-primary-hover); -} - -.wr-badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: var(--wr-ln-radius-pill, 9999px); - font-size: 11px; - font-weight: 500; - line-height: 1.5; - white-space: nowrap; -} - -.wr-badge--priority-p0 { - background: var(--wr-ln-color-error-bg); - color: #ff8a8f; -} - -.wr-badge--priority-p1 { - background: rgba(229, 160, 69, 0.14); - color: #f0b56a; -} - -.wr-badge--gray { - background: var(--wr-ln-surface-2); - color: var(--wr-ln-ink-subtle); -} - -.wr-badge--blue { - background: var(--wr-ln-color-primary-light); - color: var(--wr-ln-primary-hover); -} - -.wr-badge--green { - background: var(--wr-ln-color-success-bg); - color: var(--wr-ln-color-success-text); -} - -.wr-badge--amber { - background: rgba(229, 160, 69, 0.14); - color: #f0b56a; -} - -.wr-badge--red { - background: var(--wr-ln-color-error-bg); - color: #ff8a8f; -} - -.wr-badge--track { - background: var(--wr-ln-color-bg-page-alt); - color: var(--wr-ln-color-text-secondary); -} - -.wr-table-wrap { - overflow-x: auto; - border: 1px solid var(--wr-ln-color-border); - border-radius: var(--wr-ln-radius-lg, 12px); - background: var(--wr-ln-color-bg-card); -} - -.wr-table { - width: 100%; - min-width: 720px; - border-collapse: collapse; - font-size: 13px; -} - -.wr-table th, -.wr-table td { - padding: 10px 14px; - border-bottom: 1px solid var(--wr-ln-color-border-light); - text-align: left; - vertical-align: middle; -} - -.wr-table th { - background: var(--wr-ln-surface-2); - font-size: 12px; - font-weight: 600; - color: var(--wr-ln-ink-muted); - white-space: nowrap; -} - -.wr-table td { - color: var(--wr-ln-ink); -} - -.wr-table tbody tr:last-child td { - border-bottom: none; -} - -.wr-table tbody tr:hover td { - background: var(--wr-ln-surface-2); -} - -.wr-table-progress { - display: flex; - align-items: center; - gap: 8px; - min-width: 120px; -} - -.wr-section--minor { - border: 1px solid var(--wr-ln-hairline); - border-radius: var(--wr-ln-radius-lg, 12px); - padding: 14px 16px; - background: var(--wr-ln-surface-1); -} - -.wr-minor-toggle { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - width: 100%; - padding: 0; - border: 0; - background: transparent; - cursor: pointer; - text-align: left; - color: inherit; -} - -.wr-minor-toggle:focus-visible { - outline: 2px solid var(--wr-ln-color-primary); - outline-offset: 4px; - border-radius: 8px; -} - -.wr-minor-summary { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 12px; -} - -.wr-minor-chip { - display: inline-flex; - padding: 6px 10px; - border-radius: 999px; - font-size: 12px; - color: var(--wr-ln-color-text-secondary); - background: var(--wr-ln-color-bg-muted); - border: 1px solid var(--wr-ln-color-border-light); -} - -.wr-footer { - margin-top: 32px; - padding-top: 16px; - border-top: 1px solid var(--wr-ln-color-border); - font-size: 12px; - color: var(--wr-ln-color-text-muted); - text-align: center; -} - -.tabular-nums { - font-variant-numeric: tabular-nums; -} - -@media (max-width: 1100px) { - .wr-principle, - .wr-kpi-row, - .wr-kpi-row--exec, - .wr-track-grid, - .wr-track-switcher { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .wr-exec-summary__grid, - .wr-exec-next, - .wr-exec-decision__meta { - grid-template-columns: 1fr; - } - - .wr-req-groups { - grid-template-columns: 1fr; - } - - .wr-phase-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .wr-four-r-grid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 640px) { - .wr-page { - padding: 16px 16px 32px; - } - - .wr-principle, - .wr-kpi-row, - .wr-track-grid, - .wr-track-switcher { - grid-template-columns: 1fr; - } - - .wr-hero__aside { - align-items: flex-start; - width: 100%; - } - - .wr-hero__meta { - align-items: flex-start; - } - - .wr-phase-grid { - grid-template-columns: 1fr; - } -} - -@media (prefers-reduced-motion: reduce) { - .wr-progress__fill, - .wr-btn, - .wr-track-tab, - .wr-phase-tab { - transition: none; - } -} - -@media print { - .wr-btn { - display: none; - } - - .wr-page { - color-scheme: light; - background: #fff; - color: #18181b; - padding: 12px; - } - - .wr-kpi-card, - .wr-track-detail-wrap, - .wr-issue-card, - .wr-table-wrap, - .wr-phase-card { - background: #fff; - border-color: #e5e7eb; - } - - .wr-minor-toggle { - pointer-events: none; - } - - .wr-table-wrap { - break-inside: avoid; - } -} diff --git a/src/prototypes/oneos-weekly-report/weekly-report-data.ts b/src/prototypes/oneos-weekly-report/weekly-report-data.ts deleted file mode 100644 index c0e6951..0000000 --- a/src/prototypes/oneos-weekly-report/weekly-report-data.ts +++ /dev/null @@ -1,707 +0,0 @@ -export type IssuePriority = 'P0' | 'P1' | 'P2'; -export type IssueStatus = 'open' | 'in_progress' | 'resolved' | 'pending_feedback'; -export type TrackKey = 'product' | 'dev' | 'qa'; - -export interface FourRBlock { - reason: string; - measure: string; - result: string; - feedback: string; -} - -export interface WeeklyIssue { - id: string; - priority: IssuePriority; - status: IssueStatus; - track: TrackKey; - module: string; - title: string; - owner: string; - updatedAt: string; - fourR: FourRBlock; -} - -export interface ProductRequirementItem { - id: string; - priority: IssuePriority | 'V1.0'; - title: string; - note?: string; - done: boolean; -} - -export interface ProductRequirementGroup { - key: string; - label: string; - items: ProductRequirementItem[]; -} - -export interface TrackLine { - key: string; - label: string; - progress: number; - planned: number; - done: number; - blocked: number; - highlight: string; -} - -export interface PhaseWorkItem { - id: string; - title: string; - done: boolean; - note?: string; -} - -export type RequirementStage = '原型设计' | '需求确认' | '需求跟踪' | '需求规划' | 'BUG修复' | '无需求开发'; -export type RequirementPriority = '高优先级' | '中优先级' | '低优先级'; - -export interface RequirementItem { - id: string; - stage: RequirementStage; - priority: RequirementPriority; - title: string; - progress: number; -} - -export interface RequirementSection { - title: string; - items: RequirementItem[]; -} - -export interface PhaseTab { - key: string; - label: string; - count: number; - done: number; - summary?: string; - points?: string[]; - workReport?: PhaseWorkItem[]; - miniProgramWorkReport?: PhaseWorkItem[]; - miniProgramEmptyText?: string; - emptyText?: string; - requirementSections?: RequirementSection[]; -} - -export interface TrackPhase { - key: string; - label: string; - count: number; - done: number; - note?: string; - tabs?: PhaseTab[]; - workReportTitle?: string; -} - -export interface TrackProgress { - key: TrackKey; - label: string; - progress: number; - planned: number; - done: number; - blocked: number; - highlight?: string; - lines?: TrackLine[]; - requirementGroups?: ProductRequirementGroup[]; - phases?: TrackPhase[]; -} - -export interface MilestoneRow { - id: string; - module: string; - milestone: string; - owner: string; - planDate: string; - actualDate: string | null; - progress: number; - status: 'on_track' | 'at_risk' | 'done' | 'delayed'; -} - -export interface WeeklyReportMeta { - weekLabel: string; - weekRange: string; - reportDate: string; - reporter: string; - department: string; -} - -export const REPORT_META: WeeklyReportMeta = { - weekLabel: '2026 年第 25 周', - weekRange: '2026-06-16 ~ 2026-06-22', - reportDate: '2026-06-23', - reporter: '数智部 · 产品组', - department: '数智部', -}; - -export const TRACK_PROGRESS: TrackProgress[] = [ - { - key: 'product', - label: '产品', - progress: 39, - planned: 18, - done: 7, - blocked: 0, - phases: [ - { - key: 'plan', - label: '规划', - count: 5, - done: 1, - workReportTitle: '版本规划', - tabs: [ - { - key: 'v12', - label: 'V1.2', - count: 2, - done: 0, - workReport: [ - { id: 'WR-P-V12-1', title: 'V1.2版本需求管理矩阵', done: false }, - { id: 'WR-P-V12-2', title: 'V1.2版本WBS', done: false }, - ], - }, - { - key: 'v13', - label: 'V1.3', - count: 4, - done: 0, - workReport: [ - { id: 'WR-P-V13-P1', title: '资产链条全闭环', done: false }, - { id: 'WR-P-V13-P2', title: '合同合规性改造', done: false }, - { id: 'WR-P-V13-P3', title: '业财采运侧统计分析上线', done: false }, - { id: 'WR-P-V13-P4', title: '移动端商城发布', done: false }, - ], - }, - { - key: 'v14', - label: 'V1.4', - count: 3, - done: 0, - workReport: [ - { id: 'WR-P-V14-P1', title: '移动化', done: false }, - { id: 'WR-P-V14-P2', title: '外部用户业务引入', done: false }, - { id: 'WR-P-V14-P3', title: '业财一体化打通', done: false }, - { id: 'WR-P-V14-1', title: '在线商城需求预研', done: false }, - ], - }, - ], - }, - { - key: 'interview', - label: '访谈', - count: 4, - done: 2, - workReportTitle: '访谈结果', - tabs: [ - { - key: 'biz', - label: '业务', - count: 1, - done: 0, - workReport: [ - { id: 'WR-I-BIZ-1', title: '部分业务部合同代提审人员权限确认', done: false }, - ], - }, - { - key: 'ops-mgmt', - label: '业管', - count: 3, - done: 2, - workReport: [ - { id: 'WR-I-OM-1', title: '租赁、自营台账需求逻辑确认和演示,进度反馈', done: true }, - { id: 'WR-I-OM-3', title: '在线合同部分业务逻辑确认和优化方向讨论', done: true }, - { id: 'WR-I-OM-4', title: '提车应收款的生成逻辑讨论和确认', done: false }, - ], - }, - { - key: 'procurement', - label: '采购', - count: 1, - done: 1, - workReport: [ - { id: 'WR-I-PR-2', title: '保险采购功能使用培训,用户实操反馈确认,优化意见采集', done: true }, - ], - }, - { - key: 'maint', - label: '运维', - count: 1, - done: 0, - workReport: [ - { id: 'WR-I-MT-1', title: '年审/保养台账原型演示和确认', done: false }, - ], - }, - { - key: 'finance', - label: '财务', - count: 0, - done: 0, - workReport: [], - emptyText: '无', - }, - { - key: 'legal', - label: '法务', - count: 3, - done: 3, - workReport: [ - { id: 'WR-I-LG-1', title: '标准合同不同车型条款逻辑确认', done: true }, - { id: 'WR-I-LG-2', title: '标准合同里程约束和操作逻辑确认', done: true }, - { id: 'WR-I-LG-3', title: '标准合同多车型操作逻辑确认', done: true }, - ], - }, - { - key: 'ceo', - label: 'CEO', - count: 1, - done: 0, - summary: 'V1.0-V1.4 版本基调与 WBS 汇报', - workReport: [{ id: 'WR-I-CEO-1', title: '版本基调口头对齐', done: true, note: 'WBS 文档整理中' }], - }, - ], - }, - { - key: 'prototype', - label: '需求', - count: 20, - done: 7, - tabs: [ - { - key: 'new-feature', - label: '新增功能', - count: 16, - done: 6, - requirementSections: [ - { - title: 'PC', - items: [ - { id: 'WR-NF-W-1', stage: '原型设计', priority: '高优先级', title: '标准合同管理功能原型设计', progress: 100 }, - { id: 'WR-NF-W-2', stage: '原型设计', priority: '高优先级', title: '新合同体系功能原型设计', progress: 100 }, - { id: 'WR-NF-W-3', stage: '需求确认', priority: '高优先级', title: '客户评级体系功能及原型设计,法务确认中', progress: 30 }, - { id: 'WR-NF-W-4', stage: '原型设计', priority: '高优先级', title: '维修保养台账功能,待确认', progress: 100 }, - { id: 'WR-NF-W-5', stage: '原型设计', priority: '中优先级', title: '还车应结款运维处理人转交功能,未开始', progress: 0 }, - { id: 'WR-NF-W-7', stage: '原型设计', priority: '中优先级', title: '三方退租功能,择期评审', progress: 100 }, - { id: 'WR-NF-W-8', stage: '需求跟踪', priority: '高优先级', title: '保险采购功能优化', progress: 70 }, - { id: 'WR-NF-W-9', stage: '需求跟踪', priority: '高优先级', title: '加氢站管理功能,已评审开发中', progress: 50 }, - { id: 'WR-NF-W-10', stage: '原型设计', priority: '中优先级', title: '加氢站登录和加氢订单记录功能,等待加氢站管理上线推进', progress: 100 }, - { id: 'WR-NF-W-11', stage: '原型设计', priority: '高优先级', title: '租赁台账功能', progress: 100 }, - { id: 'WR-NF-W-12', stage: '原型设计', priority: '高优先级', title: '租赁业务盈亏表', progress: 80 }, - { id: 'WR-NF-W-13', stage: '原型设计', priority: '高优先级', title: '自营业务盈亏表', progress: 40 }, - { id: 'WR-NF-W-14', stage: '原型设计', priority: '中优先级', title: '总体业务盈亏表', progress: 40 }, - ], - }, - { - title: '小程序', - items: [ - { id: 'WR-NF-M-1', stage: '需求跟踪', priority: '中优先级', title: '审批中心「提车应收款、还车应结款」功能,测试发现部分问题,开发重新修改', progress: 65 }, - { id: 'WR-NF-M-2', stage: 'BUG修复', priority: '中优先级', title: '年审任务在待办列表重复显示', progress: 0 }, - ], - }, - { - title: '小羚羚', - items: [ - { id: 'WR-NF-X-1', stage: '需求规划', priority: '高优先级', title: '外部用户注册登录、在线商城,依赖web新合同功能,有完整规划', progress: 20 }, - ], - }, - ], - }, - { - key: 'optimize', - label: '功能优化', - count: 5, - done: 1, - requirementSections: [ - { - title: 'PC', - items: [ - { id: 'WR-OP-W-1', stage: '需求确认', priority: '中优先级', title: '提车应收款功能、UI、交互全面优化,有方案待与业管确认', progress: 30 }, - { id: 'WR-OP-W-2', stage: '原型设计', priority: '中优先级', title: '车辆氢费明细抹零、客户核对功能', progress: 0 }, - { id: 'WR-OP-W-3', stage: '需求确认', priority: '低优先级', title: '运维特殊权限体系设计,待确认', progress: 20 }, - { id: 'WR-OP-W-4', stage: '无需求开发', priority: '高优先级', title: '合同续签/转正式/转三方/新增车辆等逻辑修复规则确认,开发中', progress: 60 }, - ], - }, - { - title: '小程序', - items: [ - { id: 'WR-OP-M-1', stage: '需求跟踪', priority: '中优先级', title: '交车管理功能优化,测试中', progress: 85 }, - ], - }, - ], - }, - ], - }, - { - key: 'review', - label: '评审', - count: 4, - done: 2, - workReportTitle: '评审结果', - tabs: [ - { - key: 'reviewed', - label: '已评审', - count: 2, - done: 2, - workReport: [ - { id: 'WR-R-DONE-1', title: '「加氢站管理」站点管理/充值单/对账单/余额/营业状况/成本价维护评审完成', done: true }, - { id: 'WR-R-DONE-2', title: '「租赁合同」续签/转正式/转三方/新增车辆功能缺失逻辑评审确认', done: true }, - ], - }, - { - key: 'pending', - label: '待评审', - count: 2, - done: 0, - workReport: [ - { id: 'WR-R-PND-1', title: '「租赁台账」功能需求评审,待部分细节和实现逻辑与业管确认后评审', done: false }, - { id: 'WR-R-PND-2', title: '「自营台账」功能需求评审,部分依赖模块需求完善后评审,下周一评审', done: false }, - ], - }, - ], - }, - ], - }, - { - key: 'dev', - label: '开发', - progress: 72, - planned: 11, - done: 8, - blocked: 2, - lines: [ - { - key: 'oneos', - label: 'OneOS', - progress: 78, - planned: 7, - done: 6, - blocked: 1, - highlight: '车辆管理联调完成 85%;保险采购模块已迁入 oneos1.2 原型库', - }, - { - key: 'bi', - label: 'BI', - progress: 64, - planned: 4, - done: 2, - blocked: 1, - highlight: '经营看板 v2 接口对接中;车辆油耗报表 SQL 优化待 DBA 评审', - }, - ], - }, - { - key: 'qa', - label: '测试', - progress: 81, - planned: 8, - done: 6, - blocked: 0, - highlight: '租赁合同创建页 E2E 用例 12/15 通过;性能基线回归无新增 P0', - }, -]; - -export const KEY_ISSUES: WeeklyIssue[] = [ - { - id: 'ISS-001', - priority: 'P0', - status: 'in_progress', - track: 'dev', - module: '租赁合同管理', - title: '新增页左侧多卡片布局在窄屏下偶发错位', - owner: '前端 · 王磊', - updatedAt: '2026-06-21', - fourR: { - reason: 'Ant Design 5 Select 与 DatePicker 默认高度不一致,叠加 flex 嵌套导致子卡片被压缩为 0 高度。', - measure: '统一 36px 控件高度链;左侧列改为纵向滚动 + 各卡片 height:auto;补充交付区与表格列专项样式。', - result: '桌面端三卡片布局已对齐;表格内租金/保证金输入框与车牌号选择器高度一致。移动端仍需 992px 以下回归。', - feedback: '待总监确认:是否接受移动端整页纵向滚动方案(不再左右分栏固定高度)。', - }, - }, - { - id: 'ISS-002', - priority: 'P0', - status: 'pending_feedback', - track: 'qa', - module: '车辆管理', - title: '保险状态与保险采购台账偶发不一致', - owner: '测试 · 陈悦', - updatedAt: '2026-06-20', - fourR: { - reason: '车辆列表取 insurance-purchases.json 最新一条,详情页缓存未刷新;localStorage 脏数据未隔离。', - measure: '统一 insurance.ts 聚合逻辑;详情 Tab 进入时强制 re-fetch;测试环境增加 storage 前缀隔离。', - result: '复现路径已稳定;修复 PR #428 合并 staging,回归 48 台样例车全部一致。', - feedback: '请总监确认:正式环境是否允许用户手动清缓存,或仅后台运维可触发全量重算。', - }, - }, - { - id: 'ISS-003', - priority: 'P1', - status: 'resolved', - track: 'product', - module: '保险采购', - title: '比价单与保单台账业务边界需书面确认', - owner: '产品 · 李敏', - updatedAt: '2026-06-19', - fourR: { - reason: '业务方对「比价单审批中是否占用车辆险种」理解不一致,影响 KPI 统计口径。', - measure: '输出双业务线流程图 + 状态机说明;与法务、运营联合评审 1 次。', - result: '已发布《保险采购模块方案书》PDF v1.2;原型按「不关联」实现获业务签字。', - feedback: '总监已批示「按方案书执行」,无追加意见。', - }, - }, -]; - -export const MINOR_ISSUES: WeeklyIssue[] = [ - { - id: 'ISS-101', - priority: 'P2', - status: 'open', - track: 'dev', - module: '合同模板', - title: '预览分页与编辑区分页测量偶差 1px', - owner: '王磊', - updatedAt: '2026-06-18', - fourR: { reason: '—', measure: '—', result: '—', feedback: '—' }, - }, - { - id: 'ISS-102', - priority: 'P2', - status: 'in_progress', - track: 'qa', - module: '全局', - title: 'Safari 下 DatePicker 焦点环被裁剪', - owner: '陈悦', - updatedAt: '2026-06-17', - fourR: { reason: '—', measure: '—', result: '—', feedback: '—' }, - }, - { - id: 'ISS-103', - priority: 'P2', - status: 'resolved', - track: 'product', - module: '车辆管理', - title: 'PRD 导出文件名编码在 Windows 乱码', - owner: '李敏', - updatedAt: '2026-06-16', - fourR: { reason: '—', measure: '—', result: '—', feedback: '—' }, - }, - { - id: 'ISS-104', - priority: 'P2', - status: 'open', - track: 'dev', - module: '租赁订单', - title: '服务项 Modal 在 1280px 下横向滚动条样式不统一', - owner: '赵航', - updatedAt: '2026-06-15', - fourR: { reason: '—', measure: '—', result: '—', feedback: '—' }, - }, - { - id: 'ISS-105', - priority: 'P2', - status: 'in_progress', - track: 'qa', - module: '保险采购', - title: 'OCR 识别任务 mock 进度条动画在 CI 不稳定', - owner: '陈悦', - updatedAt: '2026-06-14', - fourR: { reason: '—', measure: '—', result: '—', feedback: '—' }, - }, -]; - -export const MILESTONES: MilestoneRow[] = [ - { - id: 'MS-1', - module: '租赁合同管理', - milestone: '新增合同 · 主体/订单/委托书分卡', - owner: '王磊', - planDate: '2026-06-20', - actualDate: '2026-06-21', - progress: 100, - status: 'done', - }, - { - id: 'MS-2', - module: '保险采购', - milestone: '原型迁入 oneos1.2 + 构建通过', - owner: '赵航', - planDate: '2026-06-22', - actualDate: '2026-06-22', - progress: 100, - status: 'done', - }, - { - id: 'MS-3', - module: '车辆管理', - milestone: '保险跳转保险采购联调', - owner: '王磊', - planDate: '2026-06-25', - actualDate: null, - progress: 65, - status: 'at_risk', - }, - { - id: 'MS-4', - module: '合同模板 V26.6', - milestone: '预览与编辑分页一致性', - owner: '王磊', - planDate: '2026-06-28', - actualDate: null, - progress: 40, - status: 'on_track', - }, -]; - -export function countIssuesByPriority(issues: WeeklyIssue[], priority: IssuePriority) { - return issues.filter((i) => i.priority === priority).length; -} - -export function countIssuesByStatus(issues: WeeklyIssue[], status: IssueStatus) { - return issues.filter((i) => i.status === status).length; -} - -export type ExecutiveHealth = 'green' | 'amber' | 'red'; - -export interface ExecutiveKpi { - key: string; - label: string; - value: string; - hint?: string; - tone?: 'default' | 'primary' | 'warning' | 'success' | 'danger'; -} - -export interface ExecutiveSummary { - health: ExecutiveHealth; - healthLabel: string; - headline: string; - achievements: string[]; - risks: string[]; -} - -export interface ExecutiveDecision { - id: string; - title: string; - impact: string; - recommendation: string; - deadline: string; - urgency: 'high' | 'medium'; -} - -export interface NextWeekItem { - id: string; - type: 'goal' | 'risk'; - title: string; - detail: string; -} - -export const EXECUTIVE_KPIS: ExecutiveKpi[] = [ - { - key: 'v12-progress', - label: 'V1.2 版本整体进度', - value: '64%', - hint: '较上周 +4%', - tone: 'primary', - }, - { - key: 'milestone', - label: '里程碑按时达成', - value: '2 / 4', - hint: '本季节点 50%', - tone: 'warning', - }, - { - key: 'delivery', - label: '本周价值交付', - value: '5 项', - hint: '上线 1 · 评审 4', - tone: 'success', - }, - { - key: 'decision', - label: '待高层决策', - value: '2 项', - hint: '需在本周内闭环', - tone: 'warning', - }, - { - key: 'blocker', - label: '阻塞/高风险', - value: '2 项', - hint: '开发阻塞 2', - tone: 'danger', - }, -]; - -export const EXECUTIVE_SUMMARY: ExecutiveSummary = { - health: 'amber', - healthLabel: '总体可控 · 局部需加速', - headline: - '本周 OneOS V1.2 建设按节奏推进:标准合同与新合同体系原型接近收尾,保险采购已上线运行,加氢站管理与租赁合同关键逻辑完成评审。租赁/自营台账进入评审前最后一轮需求确认,提车应收款口径仍待跨部门拍板。开发联调整体正常,车辆管理与合同模板各 1 项技术阻塞需资源协调。', - achievements: [ - '保险采购功能正式上线,业管完成使用培训并收集优化意见', - '加氢站管理、租赁合同续签/转正式等 4 项需求完成评审或规则确认', - '跨部门访谈覆盖业务、业管、采购、法务等 7 条战线,需求矩阵与 WBS 框架明确', - ], - risks: [ - '租赁台账、自营台账评审时点依赖业管细节确认,存在挤压 V1.2 里程碑窗口风险', - '提车应收款生成逻辑未最终确认,可能影响审批中心小程序联调与财务对账口径', - ], -}; - -export const EXECUTIVE_DECISIONS: ExecutiveDecision[] = [ - { - id: 'DEC-1', - title: '租赁/自营台账正式评审排期', - impact: '决定 V1.2 资产链条与台账能力能否按期进入开发与 UAT', - recommendation: '建议责成业管本周内完成细节确认,下周一启动自营台账评审,租赁台账紧随其后', - deadline: '2026-06-27', - urgency: 'high', - }, - { - id: 'DEC-2', - title: '提车应收款生成逻辑与财务口径', - impact: '影响审批中心小程序、财务对账及后续业财一体化数据一致性', - recommendation: '建议由董事长协调业管与财务联合拍板,明确规则后 3 日内冻结需求', - deadline: '2026-06-25', - urgency: 'high', - }, - { - id: 'DEC-3', - title: '合同里程减免 V1.2 实现策略', - impact: '决定本期是否投入研发,或维持手工过渡方案', - recommendation: '建议确认「合并至新合同体系、本期手工处理」策略,避免重复建设', - deadline: '2026-06-30', - urgency: 'medium', - }, -]; - -export const NEXT_WEEK_PLAN: NextWeekItem[] = [ - { - id: 'NW-1', - type: 'goal', - title: '自营台账需求评审', - detail: '下周一组织正式评审,会前补齐依赖模块需求说明', - }, - { - id: 'NW-2', - type: 'goal', - title: 'V1.2 版本 WBS 与需求矩阵定稿', - detail: '输出可执行 WBS,作为开发与测试排期基线', - }, - { - id: 'NW-3', - type: 'goal', - title: '车辆管理保险联调闭环', - detail: '推动保险状态一致性修复上线,消除 P0 质量风险', - }, - { - id: 'NW-4', - type: 'risk', - title: 'BI 数据源迁移外部依赖', - detail: '若数据平台排期延后,将直接影响 V1.2 统计分析上线节点', - }, - { - id: 'NW-5', - type: 'risk', - title: '开发人力被合同模板专项占用', - detail: '分页一致性专项与租赁合同新增页问题并行,需评估是否临时增援', - }, -]; diff --git a/src/prototypes/self-operated-business-ledger/annotation-source.json b/src/prototypes/self-operated-business-ledger/annotation-source.json index 768729d..1e76092 100644 --- a/src/prototypes/self-operated-business-ledger/annotation-source.json +++ b/src/prototypes/self-operated-business-ledger/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "self-operated-business-ledger", "pageId": "list", - "updatedAt": 1782810809221, + "updatedAt": 1782871095271, "nodes": [ { "id": "sob-filter", @@ -104,6 +104,13 @@ "title": "ONE-OS 原型导航", "defaultExpanded": true, "children": [ + { + "type": "link", + "id": "nav-link-contract-template-management", + "title": "合同模板管理", + "href": "/prototypes/contract-template-management", + "target": "self" + }, { "type": "link", "id": "nav-link-insurance-procurement", @@ -111,13 +118,6 @@ "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", @@ -125,13 +125,6 @@ "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", @@ -167,21 +160,6 @@ "href": "/prototypes/self-operated-business-ledger", "target": "self" }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, { "type": "folder", "id": "nav-folder-prototypes-oneos-web", diff --git a/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json b/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json index d7eb375..a1695dd 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json +++ b/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "vehicle-h2-fee-ledger", "pageId": "list", - "updatedAt": 1782810809221, + "updatedAt": 1782871095271, "nodes": [ { "id": "vh2-header", @@ -258,6 +258,13 @@ "title": "ONE-OS 原型导航", "defaultExpanded": true, "children": [ + { + "type": "link", + "id": "nav-link-contract-template-management", + "title": "合同模板管理", + "href": "/prototypes/contract-template-management", + "target": "self" + }, { "type": "link", "id": "nav-link-insurance-procurement", @@ -265,13 +272,6 @@ "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", @@ -279,13 +279,6 @@ "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", @@ -321,21 +314,6 @@ "href": "/prototypes/self-operated-business-ledger", "target": "self" }, - { - "type": "folder", - "id": "nav-folder-prototypes-xll-miniapp", - "title": "小羚羚小程序", - "defaultExpanded": true, - "children": [ - { - "type": "link", - "id": "nav-link-oneos-web-safety-training", - "title": "安全培训扫码", - "href": "/prototypes/oneos-web-safety-training", - "target": "self" - } - ] - }, { "type": "folder", "id": "nav-folder-prototypes-oneos-web", diff --git a/src/prototypes/vehicle-management-clay/annotation-source.json b/src/prototypes/vehicle-management-clay/annotation-source.json new file mode 100644 index 0000000..2cc307d --- /dev/null +++ b/src/prototypes/vehicle-management-clay/annotation-source.json @@ -0,0 +1,1117 @@ +{ + "documentVersion": 1, + "format": "axhub-annotation-source", + "data": { + "version": 2, + "prototypeName": "vehicle-management-clay", + "pageId": "list", + "updatedAt": 1782871095272, + "nodes": [ + { + "id": "vm-doc-toolbar", + "index": 1, + "title": "测试用例下载", + "pageId": [ + "list", + "detail" + ], + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-doc-toolbar\"]" + ], + "fingerprint": "vm-doc-toolbar", + "path": [] + }, + "aiPrompt": "右上角测试用例下载工具栏。", + "annotationText": "下载 Excel 测试用例。", + "hasMarkdown": true, + "color": "#64748b", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-filter", + "index": 2, + "title": "筛选条件", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-filter\"]" + ], + "fingerprint": "vm-filter", + "path": [] + }, + "aiPrompt": "列表筛选区字段与查询逻辑。", + "annotationText": "", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-all", + "index": 3, + "title": "所有营运车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-all\"]" + ], + "fingerprint": "vm-kpi-all", + "path": [] + }, + "aiPrompt": "KPI 口径与点击筛选。", + "annotationText": "未退出运营、可用于交付的车辆总数。点击切换列表筛选。", + "hasMarkdown": true, + "color": "#64748b", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-lease", + "index": 4, + "title": "租赁车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-lease\"]" + ], + "fingerprint": "vm-kpi-lease", + "path": [] + }, + "aiPrompt": "租赁运营状态 KPI。", + "annotationText": "运营状态为「租赁」且已交付租赁业务项目的车辆。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-self", + "index": 5, + "title": "自营车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-self\"]" + ], + "fingerprint": "vm-kpi-self", + "path": [] + }, + "aiPrompt": "自营运营状态 KPI。", + "annotationText": "运营状态为「自营」且已交付自营业务项目的车辆。", + "hasMarkdown": true, + "color": "#059669", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-stock", + "index": 6, + "title": "库存车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-stock\"]" + ], + "fingerprint": "vm-kpi-stock", + "path": [] + }, + "aiPrompt": "在库车辆 KPI。", + "annotationText": "运营状态为「可运营」或「待运营」的在库车辆。", + "hasMarkdown": true, + "color": "#d97706", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-nonOperating", + "index": 7, + "title": "非运营车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-nonOperating\"]" + ], + "fingerprint": "vm-kpi-nonOperating", + "path": [] + }, + "aiPrompt": "非运营车辆 KPI。", + "annotationText": "公务车、观光车等不可用于租赁或自营业务的车辆。", + "hasMarkdown": true, + "color": "#7c3aed", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-exit", + "index": 8, + "title": "退出运营车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-exit\"]" + ], + "fingerprint": "vm-kpi-exit", + "path": [] + }, + "aiPrompt": "退出运营 KPI。", + "annotationText": "已退出运营,不可再用于租赁或自营业务。", + "hasMarkdown": true, + "color": "#64748b", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-licenseAbnormal", + "index": 9, + "title": "证照异常车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-licenseAbnormal\"]" + ], + "fingerprint": "vm-kpi-licenseAbnormal", + "path": [] + }, + "aiPrompt": "证照异常与交车限制。", + "annotationText": "证照状态异常;须将行驶证有效期更新至未过期才可交车。", + "hasMarkdown": true, + "color": "#dc2626", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-kpi-insuranceAbnormal", + "index": 10, + "title": "保险异常车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-kpi-insuranceAbnormal\"]" + ], + "fingerprint": "vm-kpi-insuranceAbnormal", + "path": [] + }, + "aiPrompt": "保险异常与交车限制。", + "annotationText": "交强险或商业险已到期;完成采购前不可用于租赁/自营交车。", + "hasMarkdown": true, + "color": "#dc2626", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-table-toolbar", + "index": 11, + "title": "导出与批量导入", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-table-toolbar\"]" + ], + "fingerprint": "vm-table-toolbar", + "path": [] + }, + "aiPrompt": "列表工具栏操作。", + "annotationText": "导出当前筛选结果;批量导入按模板校验(原型为 Toast 演示)。", + "hasMarkdown": true, + "color": "#0f766e", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-vehicle-table", + "index": 12, + "title": "台账列表", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-vehicle-table\"]" + ], + "fingerprint": "vm-vehicle-table", + "path": [] + }, + "aiPrompt": "车辆台账表格列与操作。", + "annotationText": "", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-import-modal", + "index": 13, + "title": "批量导入", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-import-modal\"]" + ], + "fingerprint": "vm-import-modal", + "path": [] + }, + "aiPrompt": "CSV 模板下载与上传。", + "annotationText": "", + "hasMarkdown": true, + "color": "#0f766e", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-edit-modal", + "index": 14, + "title": "编辑车辆", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-edit-modal\"]" + ], + "fingerprint": "vm-edit-modal", + "path": [] + }, + "aiPrompt": "管理员编辑车辆档案。", + "annotationText": "管理员通过操作列「更多 → 编辑」打开。", + "hasMarkdown": true, + "color": "#7c3aed", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-ops-modal", + "index": 15, + "title": "运维负责人", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-ops-modal\"]" + ], + "fingerprint": "vm-ops-modal", + "path": [] + }, + "aiPrompt": "列表页运维负责人弹窗。", + "annotationText": "", + "hasMarkdown": true, + "color": "#d97706", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-operate-city-modal", + "index": 16, + "title": "修改运营城市", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-operate-city-modal\"]" + ], + "fingerprint": "vm-operate-city-modal", + "path": [] + }, + "aiPrompt": "列表页运营城市弹窗。", + "annotationText": "仅来源为「人工」时可修改。", + "hasMarkdown": true, + "color": "#0891b2", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-location-map-modal", + "index": 17, + "title": "车辆定位地图", + "pageId": "list", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-location-map-modal\"]" + ], + "fingerprint": "vm-location-map-modal", + "path": [] + }, + "aiPrompt": "GPS 地图弹窗。", + "annotationText": "点击「当前位置」打开。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-hero", + "index": 18, + "title": "车辆概要", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-hero\"]" + ], + "fingerprint": "vm-detail-hero", + "path": [] + }, + "aiPrompt": "详情页头部信息。", + "annotationText": "车牌、品牌型号、VIN、在线状态、运营城市与 GPS 时间。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-stats", + "index": 19, + "title": "统计条", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-stats\"]" + ], + "fingerprint": "vm-detail-stats", + "path": [] + }, + "aiPrompt": "运营城市、里程、停放区域、运维负责人。", + "annotationText": "", + "hasMarkdown": true, + "color": "#0f766e", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tabs", + "index": 20, + "title": "Tab 导航", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tabs\"]" + ], + "fingerprint": "vm-detail-tabs", + "path": [] + }, + "aiPrompt": "详情页 11 个 Tab。", + "annotationText": "", + "hasMarkdown": true, + "color": "#64748b", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-basic", + "index": 21, + "title": "基本信息 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-basic\"]" + ], + "fingerprint": "vm-detail-tab-basic", + "path": [] + }, + "aiPrompt": "档案、项目、证照摘要。", + "annotationText": "", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-model", + "index": 22, + "title": "型号参数 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-model\"]" + ], + "fingerprint": "vm-detail-tab-model", + "path": [] + }, + "aiPrompt": "车型参数与保养项目。", + "annotationText": "展示车型技术参数表与保养项目清单(只读)。", + "hasMarkdown": true, + "color": "#64748b", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-license", + "index": 23, + "title": "证照信息 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-license\"]" + ], + "fingerprint": "vm-detail-tab-license", + "path": [] + }, + "aiPrompt": "行驶证与营运证展示。", + "annotationText": "分组展示证照字段与照片预览。", + "hasMarkdown": true, + "color": "#d97706", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-insurance", + "index": 24, + "title": "保险记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-insurance\"]" + ], + "fingerprint": "vm-detail-tab-insurance", + "path": [] + }, + "aiPrompt": "险种概览与采购历史。", + "annotationText": "", + "hasMarkdown": true, + "color": "#059669", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-lease", + "index": 25, + "title": "租赁记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-lease\"]" + ], + "fingerprint": "vm-detail-tab-lease", + "path": [] + }, + "aiPrompt": "合同与交还车历史。", + "annotationText": "合同号、交车/还车日期可跳转对应管理模块(原型 Toast)。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-accident", + "index": 26, + "title": "事故记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-accident\"]" + ], + "fingerprint": "vm-detail-tab-accident", + "path": [] + }, + "aiPrompt": "事故记录筛选与列表。", + "annotationText": "按车牌过滤种子数据;可跳转事故详情(原型演示)。", + "hasMarkdown": true, + "color": "#dc2626", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-fault", + "index": 27, + "title": "故障记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-fault\"]" + ], + "fingerprint": "vm-detail-tab-fault", + "path": [] + }, + "aiPrompt": "故障记录筛选与操作。", + "annotationText": "支持查看详情与编辑故障记录(原型 Toast)。", + "hasMarkdown": true, + "color": "#d97706", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-violation", + "index": 28, + "title": "违章记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-violation\"]" + ], + "fingerprint": "vm-detail-tab-violation", + "path": [] + }, + "aiPrompt": "违章记录列表。", + "annotationText": "按当前车辆过滤;可跳转违章管理详情(原型演示)。", + "hasMarkdown": true, + "color": "#7c3aed", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-movement", + "index": 29, + "title": "异动记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-movement\"]" + ], + "fingerprint": "vm-detail-tab-movement", + "path": [] + }, + "aiPrompt": "异动记录列表。", + "annotationText": "展示车辆异动历史;可跳转异动管理详情(原型演示)。", + "hasMarkdown": true, + "color": "#0891b2", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-transfer", + "index": 30, + "title": "调拨记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-transfer\"]" + ], + "fingerprint": "vm-detail-tab-transfer", + "path": [] + }, + "aiPrompt": "调拨记录列表。", + "annotationText": "展示车辆调拨历史;可跳转调拨详情(原型演示)。", + "hasMarkdown": true, + "color": "#0f766e", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + }, + { + "id": "vm-detail-tab-annual", + "index": 31, + "title": "年审记录 Tab", + "pageId": "detail", + "locator": { + "selectors": [ + "[data-annotation-id=\"vm-detail-tab-annual\"]" + ], + "fingerprint": "vm-detail-tab-annual", + "path": [] + }, + "aiPrompt": "年审历史列表。", + "annotationText": "对齐年审管理历史列表;可跳转年审详情(原型演示)。", + "hasMarkdown": true, + "color": "#0891b2", + "images": [], + "createdAt": 1740096000000, + "updatedAt": 1740096000000 + } + ] + }, + "markdownMap": { + "vm-doc-toolbar": "## 下载测试用例\n\n右上角工具栏,列表页与详情页均可用;下载含列表/详情两个 Sheet 的 Excel。", + "vm-filter": "## 4. 筛选区\n\n### 4.1 字段清单\n\n默认展示前 4 项,点击「展开更多筛选项」显示全部。\n\n| 字段 | 控件 | 选项来源 | 匹配规则 |\n|---|---|---|---|\n| 车牌号 | 多行弹层输入 | 用户输入 | 确认后精确匹配(归一化:去空格、大写) |\n| 运营城市 | 省-市级联多选 | 种子 + 停车场映射 + 台账 location | 与车辆运营城市(省-市格式)包含关系 |\n| 品牌/型号/客户/部门/项目/合同/所有权/运营公司/租赁公司/停车场 | 可搜索单选 | 当前台账去重排序 | 精确相等 |\n| 项目类型 | 可搜索单选 | 租赁、自营 | 与创建合同时合同类型相等 |\n| 运营状态 | 可搜索单选 | 租赁、自营、可运营、待运营、退出运营 | 精确相等 |\n| 车辆来源 | 可搜索单选 | 自有、外租、挂靠 | 精确相等 |\n\n### 4.2 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入「已应用条件」,列表与 KPI 计数基于全量台账;页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n筛选与 KPI Tab **叠加**:先按 KPI 分类过滤,再按筛选条件过滤。\n\n\n---\n\n### 原型实现补充\n\n- **区域**:华东 / 华南 / 华中 / 华北 / 东北 / 西南 / 西北;按运营城市所在省份映射过滤", + "vm-kpi-all": "## 当前卡片口径\n\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-kpi-lease": "## 当前卡片口径\n\n| 租赁车辆 | `operateStatus === '租赁'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-kpi-self": "## 当前卡片口径\n\n| 自营车辆 | `operateStatus === '自营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-kpi-stock": "## 当前卡片口径\n\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-kpi-nonOperating": "## 当前卡片口径\n\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-kpi-exit": "## 当前卡片口径\n\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-kpi-licenseAbnormal": "## 当前卡片口径\n\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-kpi-insuranceAbnormal": "## 当前卡片口径\n\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---", + "vm-table-toolbar": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---", + "vm-vehicle-table": "## 7. 列表字段说明\n\n### 7.1 车辆信息(冻结列)\n\n- 车牌:链接色,点击进入**车辆详情页**\n- VIN、品牌·型号:只读展示\n\n### 7.2 运营城市\n\n- 展示:省-市(由 `location` 解析)\n- 标签:车机 / GPS / 人工(来源 `operateCitySource` 或推导)\n- **可编辑**:仅来源为「人工」时可点击修改 → 运营城市修改弹窗\n- 车机/GPS 来源:不可改,hover 提示原因\n\n### 7.3 所属项目\n\n仅**租赁/自营**车辆展示项目名称、合同编码、客户名称;否则三行均为空。\n\n- 项目名称、合同编码:有值时可点击 → Toast 跳转合同详情(原型演示)\n\n### 7.4 里程数\n\n- 在线车辆:显示里程 + 「车机」标签,里程按车机数据格式化\n- 离线:仅里程或空\n\n### 7.5 证照 / 保险\n\n| 列 | 有数据 | 无数据/异常 |\n|---|---|---|\n| 检验有效期 | 到期日 + 剩余/已过期天数标签 | 「未上传」可点击 + 「禁止交车」标签 |\n| 保险状态 | 正常/异常 Tag;异常时 hover 展示具体到期险种 | — |\n| 交强险/商业险到期 | 到期日 + 剩余天数 | 「未购买」可点击 + 「禁止交车」 |\n\n未上传/未购买点击 → Toast 跳转证照管理或保险采购(原型演示)。\n\n### 7.6 上次交车 / 还车情况\n\n三行纯数值(无行内标题):里程(km)、人员、时间(至分钟)。**有数据时**可点击。\n\n| 列 | 点击行为 |\n|---|---|\n| 上次交车情况 | Toast:`即将跳转至交车管理(原型演示):{车牌}` |\n| 上次还车情况 | Toast:`即将跳转至还车管理(原型演示):{车牌}` |\n\n### 7.7 三方租赁公司\n\n仅外租车辆显示 `leaseCompany`,否则空白。\n\n### 7.8 当前位置\n\n- 在线状态圆点 + 文案\n- 详细地址:有 GPS 坐标时可点击 → 地图弹窗(OpenStreetMap 嵌入)\n- GPS 时间:精确到秒\n\n### 7.9 运维负责人\n\n可编辑时显示为链接;无负责人显示「设置负责人」→ 运维负责人弹窗。\n\n### 7.10 操作列(冻结)\n\n| 操作 | 条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 进入车辆详情页 |\n| 更多 | 管理员或有运维权限 | 下拉:编辑(管理员)、修改运维负责人 |\n\n---", + "vm-import-modal": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---", + "vm-edit-modal": "### 8.3 编辑车辆(管理员)\n\n- 可搜索选择器字段与筛选区一致规范\n- 保存时运营状态、车辆状态仍为必填(按钮禁用),但标签无红色 *\n- Toast「车辆信息已更新」", + "vm-ops-modal": "### 8.1 设置运维负责人\n\n- 标题下胶囊:车牌 | 运维区域(省-市)\n- 多选搜索选择器,候选人按车辆区域过滤\n- **保存**:写回 `opsManagers`,Toast「运维负责人已更新」\n- **取消**:不保存\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联编辑面板(非弹窗)", + "vm-operate-city-modal": "### 8.2 修改运营城市\n\n- 省-市选择,保存后 `location` 更新且 `operateCitySource` 置为「人工」\n- Toast「运营城市已更新」\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联选择器(非弹窗)", + "vm-location-map-modal": "### 8.4 地图\n\n展示坐标点与 GPS 时间;关闭不保存。\n\n---", + "vm-detail-hero": "### 2.1 顶栏\n\n| 元素 | 行为 |\n|---|---|\n| 返回列表 | 回到列表页,保留列表筛选/KPI/分页状态 |\n| 查看需求说明 | 打开详情页 PRD(本文档) |\n| PRD文档MD文件下载 | 下载列表页、详情页两份 Markdown PRD |\n| 下载测试用例 | 下载 Excel,含列表页与详情页测试用例两个 Sheet |\n\n---\n\n### 2.2 车辆概要区\n\n- 车牌号、品牌·型号、VIN\n- 在线状态(在线/离线圆点)\n- 运营城市文案、GPS 时间\n\n---\n\n### 原型实现补充\n\n- 顶栏仅保留「返回列表」与「下载测试用例」", + "vm-detail-stats": "### 2.3 统计条(4 项)\n\n| 字段 | 可编辑 | 行为 |\n|---|---|---|\n| 运营城市 | 仅来源为「人工」时可改 | 点击 → 运营城市修改弹窗;展示车机/GPS/人工来源标签 |\n| 里程数 | 否 | 在线时展示车机标签 |\n| 停放区域 | 否 | 只读 |\n| 运维负责人 | 有运维权限时可改 | 点击铅笔图标 → 运维负责人弹窗 |\n\n\n---\n\n### 原型实现补充\n\n- **运营城市**、**运维负责人**:详情页使用内联编辑面板(非弹窗)", + "vm-detail-tabs": "## 3. Tab 总览\n\n| 序号 | Tab | 说明 |\n|---|---|---|\n| 1 | 基本信息 | 档案、项目、证照摘要等,部分字段行内可编辑 |\n| 2 | 型号参数 | 车型参数与保养项目表 |\n| 3 | 证照信息 | 行驶证、营运证等分组展示与照片预览 |\n| 4 | 保险记录 | 险种概览卡片 + 采购历史表 |\n| 5 | 租赁记录 | 合同/交还车历史 |\n| 6 | 事故记录 | 筛选 + 事故列表 |\n| 7 | 故障记录 | 筛选 + 故障列表 |\n| 8 | 违章记录 | 筛选 + 违章列表 |\n| 9 | 异动记录 | 筛选 + 异动列表 |\n| 10 | 调拨记录 | 筛选 + 调拨列表 |\n| 11 | 年审记录 | 筛选 + 已完成年审历史(对齐年审管理历史列表) |\n\n各业务记录 Tab 通用规则:\n\n- 数据按当前车辆 `plateNo` 从对应 JSON 种子文件过滤\n- 筛选区:**查询**生效、**重置**清空,修改未查询不影响列表\n- 日期范围使用双月历弹层组件\n- 表格左对齐;金额/数字列使用等宽数字\n- 底栏:共 N 条 + 紧凑分页(默认 10 条/页)\n\n---", + "vm-detail-tab-basic": "## 4. 基本信息 Tab\n\n### 4.1 档案信息\n\n| 字段 | 可编辑 |\n|---|---|\n| 出厂年份 | 是(年份选择) |\n| 采购入库日期 | 是(日期选择) |\n| 运营状态、车辆来源、停车场等 | 只读展示 |\n\n保存后 Toast「车辆信息已更新」,写回内存台账。\n\n### 4.2 项目信息\n\n租赁/自营车辆展示项目名称、合同编码、客户名称;否则显示「无」。\n\n### 4.3 证照摘要\n\n- 检验有效期:展示到期日与剩余/过期标签\n- 未上传时可点击 → Toast 跳转证照管理编辑(原型演示)\n\n---", + "vm-detail-tab-model": "## 5. 型号参数 Tab\n\n- 展示车辆型号技术参数(只读)\n- 保养项目表:养护项目、保养公里/时间周期、工时费、材料费、合计\n\n---", + "vm-detail-tab-license": "## 6. 证照信息 Tab\n\n- 分组:行驶证、道路运输证、上海牌专项证件等(按车牌规则显隐)\n- 每证展示:证件号、有效期、余额(如有)、照片网格\n- 照片可点击全屏预览;证件状态色点图例\n- 数据来自 `utils/vehicleLicenses` 与车辆台账关联\n\n---", + "vm-detail-tab-insurance": "## 7. 保险记录 Tab\n\n### 7.1 险种概览卡片\n\n交强险、商业险、承运人责任险等;点击切换下方历史表。\n\n- 未购买险种卡片可点击 → Toast 跳转保险采购(原型演示)\n- 展示状态 Tag 与到期剩余天数\n\n### 7.2 采购历史表\n\n| 列 | 说明 |\n|---|---|\n| 保险导入时间、操作人、类型、保单号 | 只读 |\n| 保险状态 | Tag |\n| 保险公司、付款时间、到期日期 | 只读 |\n| 保单文件 | 有附件时「预览」「下载」可点击 → Toast 演示 |\n\n---", + "vm-detail-tab-lease": "## 8. 租赁记录 Tab\n\n### 8.1 筛选\n\n合同编码、项目名称、客户名称、业务类型。\n\n### 8.2 表格与跳转\n\n| 列 | 可点击 | 跳转目标(原型 Toast) |\n|---|---|---|\n| 合同编码 | 是 | 租赁合同详情 |\n| 项目名称 | 有值时 | 租赁合同详情 |\n| 交车日期 | 有值时 | 交车管理详情 |\n| 还车日期 | 有值时 | 还车管理详情 |\n\n---", + "vm-detail-tab-accident": "## 9. 事故记录 Tab\n\n### 9.1 筛选\n\n事故时间(日期范围)、客户名称、事故等级、事故状态。\n\n### 9.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 事故编码 | 是 | 事故管理详情页 |\n| 其余列 | 否 | — |\n\n---", + "vm-detail-tab-fault": "## 10. 故障记录 Tab\n\n### 10.1 筛选\n\n上报时间(日期范围)、故障编号、故障等级、故障类型、解决情况、AI 匹配状态。\n\n### 10.2 表格\n\n| 列/操作 | 可点击 | 行为 |\n|---|---|---|\n| 故障编号 | 是 | Toast 跳转故障管理详情 |\n| 操作「查看」 | 是 | 同上 |\n| 操作「编辑」 | 是 | Toast 编辑故障记录演示 |\n\n---", + "vm-detail-tab-violation": "## 11. 违章记录 Tab\n\n### 11.1 筛选\n\n违法时间、客户名称、缴费状态、是否处理。\n\n### 11.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 违法行为 | 是 | 违章管理详情页(按违法时间标识) |\n| 其余列 | 否 | — |\n\n---", + "vm-detail-tab-movement": "## 12. 异动记录 Tab\n\n### 12.1 筛选\n\n异动开始日期、异动目的地、异动类型。\n\n### 12.2 表格\n\n宽表展示异动起止里程/电量/氢量等;**异动开始日期**可点击 → Toast 跳转异动管理详情。\n\n---", + "vm-detail-tab-transfer": "## 13. 调拨记录 Tab\n\n### 13.1 筛选\n\n调拨日期、调出人、接收人。\n\n### 13.2 表格\n\n**调拨日期**可点击 → Toast 跳转车辆调拨详情。\n\n---", + "vm-detail-tab-annual": "## 14. 年审记录 Tab\n\n对齐「年审管理」历史列表字段(不含车牌/品牌/型号,因已在详情页上下文)。\n\n### 14.1 筛选\n\n| 字段 | 说明 |\n|---|---|\n| 完成时间 | 按办结日期范围 |\n| 检验有效期 | 按检验有效期至范围 |\n| 办理人 | 下拉选择 |\n\n### 14.2 表格列\n\n| 列 | 可点击 | 说明 |\n|---|---|---|\n| 检验有效期至 | **是** | Toast 跳转年审管理详情(原型演示) |\n| 检测服务站名称 | 否 | 无数据显示「—」 |\n| 检测费用 | 否 | 金额两位小数,无数据显示「—」 |\n| 二保服务站名称 | 否 | 同上 |\n| 二保费用 | 否 | 同上 |\n| 整备服务站名称 | 否 | 同上 |\n| 整备费用 | 否 | 同上 |\n| 办理人 | 否 | — |\n| 完成时间 | 否 | 办结时间,默认倒序 |\n\n### 14.3 数据\n\n- 来源:`data/annual-review-records.json`,按 `plateNo` 过滤\n- 样例车 `粤AGP5156` 含 3 条已完成年审记录\n\n---" + }, + "assetMap": {}, + "directory": { + "nodes": [ + { + "type": "folder", + "id": "oneos-project-nav", + "title": "ONE-OS 原型导航", + "defaultExpanded": true, + "children": [ + { + "type": "link", + "id": "nav-link-contract-template-management", + "title": "合同模板管理", + "href": "/prototypes/contract-template-management", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-insurance-procurement", + "title": "保险采购", + "href": "/prototypes/insurance-procurement", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-vehicle-management", + "title": "车辆管理(原版)", + "href": "/prototypes/vehicle-management", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-vehicle-management-clay", + "title": "车辆管理(Clay 备份)", + "href": "/prototypes/vehicle-management-clay", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-lease-contract-management", + "title": "租赁合同管理", + "href": "/prototypes/lease-contract-management", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-customer-management", + "title": "客户管理", + "href": "/prototypes/customer-management", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-lease-business-ledger", + "title": "租赁业务台账", + "href": "/prototypes/lease-business-ledger", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-vehicle-h2-fee-ledger", + "title": "车辆氢费明细", + "href": "/prototypes/vehicle-h2-fee-ledger", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-self-operated-business-ledger", + "title": "自营业务台账", + "href": "/prototypes/self-operated-business-ledger", + "target": "self" + }, + { + "type": "folder", + "id": "nav-folder-prototypes-oneos-web", + "title": "ONE-OS Web 端", + "defaultExpanded": true, + "children": [ + { + "type": "link", + "id": "nav-link-oneos-web-workbench", + "title": "工作台", + "href": "/prototypes/oneos-web-workbench", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-login", + "title": "登录", + "href": "/prototypes/oneos-web-login", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-vehicle-asset", + "title": "车辆管理", + "href": "/prototypes/oneos-web-vehicle-asset", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-contract-template", + "title": "合同模板管理", + "href": "/prototypes/oneos-web-contract-template", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-help-center", + "title": "帮助中心", + "href": "/prototypes/oneos-web-help-center", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-finance", + "title": "财务管理", + "href": "/prototypes/oneos-web-finance", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-procurement", + "title": "采购管理", + "href": "/prototypes/oneos-web-procurement", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-lease-contract", + "title": "车辆租赁合同", + "href": "/prototypes/oneos-web-lease-contract", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-h2-station", + "title": "加氢站管理", + "href": "/prototypes/oneos-web-h2-station", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-data-analysis", + "title": "数据分析", + "href": "/prototypes/oneos-web-data-analysis", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-ledger-data", + "title": "台账数据", + "href": "/prototypes/oneos-web-ledger-data", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-business", + "title": "业务管理", + "href": "/prototypes/oneos-web-business", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-ops", + "title": "运维管理", + "href": "/prototypes/oneos-web-ops", + "target": "self" + }, + { + "type": "link", + "id": "nav-link-oneos-web-requirements", + "title": "需求说明", + "href": "/prototypes/oneos-web-requirements", + "target": "self" + } + ] + } + ] + }, + { + "type": "folder", + "id": "vm-doc-root", + "title": "车辆管理说明(Clay 备份)", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "vm-doc-overview", + "title": "模块总览", + "markdown": "## Clay 主题备份\n\n| 项 | 说明 |\n|---|---|\n| 设计基底 | `src/themes/clay/DESIGN.md` |\n| 业务逻辑 | 与 [车辆管理原版](/prototypes/vehicle-management) 共用组件与数据 |\n| 视觉差异 | 奶油画布 `#fffaf0`、12px 控件圆角、24px 卡片圆角、饱和 KPI 色卡、44px 触控高度 |\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 车辆管理 — 列表页 |\n| 目标用户 | 运营、业务、运维、管理员 |\n| 核心任务 | 按多维度筛选车辆台账,查看证照/保险/交还车状态,维护运维负责人,进入车辆详情 |\n| 页面结构 | 右上角工具栏:「查看需求说明」「PRD文档MD文件下载」「下载测试用例」;筛选区 + KPI + 表格 + 分页 |\n\n---" + }, + { + "type": "link", + "id": "vm-link-self", + "title": "车辆管理 Clay 备份", + "href": "./index.html", + "target": "self" + }, + { + "type": "folder", + "id": "vm-doc-prd", + "title": "PRD 全文", + "defaultExpanded": false, + "children": [ + { + "type": "markdown", + "id": "vm-doc-prd-list", + "title": "列表页 PRD", + "markdown": "# 车辆管理 · 列表页产品需求说明(PRD)\n\n> 本页为车辆台账列表。标注「原型演示」的跳转、导入、导出仅 Toast 提示,不做真实页面跳转。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 车辆管理 — 列表页 |\n| 目标用户 | 运营、业务、运维、管理员 |\n| 核心任务 | 按多维度筛选车辆台账,查看证照/保险/交还车状态,维护运维负责人,进入车辆详情 |\n| 页面结构 | 右上角工具栏:「查看需求说明」「PRD文档MD文件下载」「下载测试用例」;筛选区 + KPI + 表格 + 分页 |\n\n---\n\n## 2. 权限与角色(原型常量)\n\n| 常量 | 原型取值 | 影响 |\n|---|---|---|\n| ADMIN | `true` | 操作列「更多」菜单显示「编辑」 |\n| 运维主管 | `true` | 可点击运维负责人、操作列「修改运维负责人」 |\n\n生产环境应由登录用户权限接口下发,原型写死便于演示。\n\n---\n\n## 3. 数据来源总览\n\n| 数据 | 来源文件/规则 | 用途 |\n|---|---|---|\n| 车辆台账 | `data/vehicles.json`(100 条样例) | 列表、筛选选项、详情、编辑回写 |\n| 保险采购 | `data/insurance-purchases.json` | 按车辆 + 险种取**最新一条**采购记录的到期日 |\n| 停车场→区域 | `PARKING_REGION_MAP` 常量 | 运维负责人候选区域、运营城市辅助 |\n| 运维人员 | `OPS_STAFF` 常量 | 按车辆区域过滤可选运维负责人 |\n| 运营城市种子 | `OPERATE_CITY_SEED` + 台账 `location` | 运营城市筛选/修改选项 |\n| GPS 坐标 | 优先 `gpsLat/gpsLng`;否则按 `location` 城市基准点 + 车辆 id 偏移模拟 | 地图弹窗 |\n| GPS 详细地址 | `locationAddress` 优先,否则 `location` 省市区 | 当前位置列 |\n| 所属项目三元组 | `projectName` / `contractNo` / `customer` | 仅运营状态为「租赁」「自营」时展示 |\n| 三方租赁公司 | `leaseCompany` | 仅 `vehicleSource === '外租'` 时展示 |\n| 交还车记录 | `lastDelivery*` / `lastReturn*` 字段 | 上次交车/还车情况列 |\n\n**空值约定**:台账中 `-`、空字符串视为无数据;列表列默认显示空白(非「—」)。\n\n---\n\n## 4. 筛选区\n\n### 4.1 字段清单\n\n默认展示前 4 项,点击「展开更多筛选项」显示全部。\n\n| 字段 | 控件 | 选项来源 | 匹配规则 |\n|---|---|---|---|\n| 车牌号 | 多行弹层输入 | 用户输入 | 确认后精确匹配(归一化:去空格、大写) |\n| 运营城市 | 省-市级联多选 | 种子 + 停车场映射 + 台账 location | 与车辆运营城市(省-市格式)包含关系 |\n| 品牌/型号/客户/部门/项目/合同/所有权/运营公司/租赁公司/停车场 | 可搜索单选 | 当前台账去重排序 | 精确相等 |\n| 项目类型 | 可搜索单选 | 租赁、自营 | 与创建合同时合同类型相等 |\n| 运营状态 | 可搜索单选 | 租赁、自营、可运营、待运营、退出运营 | 精确相等 |\n| 车辆来源 | 可搜索单选 | 自有、外租、挂靠 | 精确相等 |\n\n### 4.2 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入「已应用条件」,列表与 KPI 计数基于全量台账;页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n筛选与 KPI Tab **叠加**:先按 KPI 分类过滤,再按筛选条件过滤。\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---\n\n## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---\n\n## 7. 列表字段说明\n\n### 7.1 车辆信息(冻结列)\n\n- 车牌:链接色,点击进入**车辆详情页**\n- VIN、品牌·型号:只读展示\n\n### 7.2 运营城市\n\n- 展示:省-市(由 `location` 解析)\n- 标签:车机 / GPS / 人工(来源 `operateCitySource` 或推导)\n- **可编辑**:仅来源为「人工」时可点击修改 → 运营城市修改弹窗\n- 车机/GPS 来源:不可改,hover 提示原因\n\n### 7.3 所属项目\n\n仅**租赁/自营**车辆展示项目名称、合同编码、客户名称;否则三行均为空。\n\n- 项目名称、合同编码:有值时可点击 → Toast 跳转合同详情(原型演示)\n\n### 7.4 里程数\n\n- 在线车辆:显示里程 + 「车机」标签,里程按车机数据格式化\n- 离线:仅里程或空\n\n### 7.5 证照 / 保险\n\n| 列 | 有数据 | 无数据/异常 |\n|---|---|---|\n| 检验有效期 | 到期日 + 剩余/已过期天数标签 | 「未上传」可点击 + 「禁止交车」标签 |\n| 保险状态 | 正常/异常 Tag;异常时 hover 展示具体到期险种 | — |\n| 交强险/商业险到期 | 到期日 + 剩余天数 | 「未购买」可点击 + 「禁止交车」 |\n\n未上传/未购买点击 → Toast 跳转证照管理或保险采购(原型演示)。\n\n### 7.6 上次交车 / 还车情况\n\n三行纯数值(无行内标题):里程(km)、人员、时间(至分钟)。**有数据时**可点击。\n\n| 列 | 点击行为 |\n|---|---|\n| 上次交车情况 | Toast:`即将跳转至交车管理(原型演示):{车牌}` |\n| 上次还车情况 | Toast:`即将跳转至还车管理(原型演示):{车牌}` |\n\n### 7.7 三方租赁公司\n\n仅外租车辆显示 `leaseCompany`,否则空白。\n\n### 7.8 当前位置\n\n- 在线状态圆点 + 文案\n- 详细地址:有 GPS 坐标时可点击 → 地图弹窗(OpenStreetMap 嵌入)\n- GPS 时间:精确到秒\n\n### 7.9 运维负责人\n\n可编辑时显示为链接;无负责人显示「设置负责人」→ 运维负责人弹窗。\n\n### 7.10 操作列(冻结)\n\n| 操作 | 条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 进入车辆详情页 |\n| 更多 | 管理员或有运维权限 | 下拉:编辑(管理员)、修改运维负责人 |\n\n---\n\n## 8. 弹窗交互\n\n### 8.1 设置运维负责人\n\n- 标题下胶囊:车牌 | 运维区域(省-市)\n- 多选搜索选择器,候选人按车辆区域过滤\n- **保存**:写回 `opsManagers`,Toast「运维负责人已更新」\n- **取消**:不保存\n\n### 8.2 修改运营城市\n\n- 省-市选择,保存后 `location` 更新且 `operateCitySource` 置为「人工」\n- Toast「运营城市已更新」\n\n### 8.3 编辑车辆(管理员)\n\n- 可搜索选择器字段与筛选区一致规范\n- 保存时运营状态、车辆状态仍为必填(按钮禁用),但标签无红色 *\n- Toast「车辆信息已更新」\n\n### 8.4 地图\n\n展示坐标点与 GPS 时间;关闭不保存。\n\n---\n\n## 9. 进入详情页\n\n- 入口:列表车牌链接或操作列「查看」\n- 详情页右上角同样有「查看需求说明」,展示**详情页 PRD**(与本文档分离)\n- 从详情返回列表时,保留筛选/KPI/分页状态(内存态)\n\n---\n\n## 10. 提示信息汇总(Toast)\n\n| 触发场景 | 文案模板 |\n|---|---|\n| 导出 | 导出功能(原型演示):当前列表共 {n} 条 |\n| 导入成功 | 已接收文件 {name},导入校验为原型演示 |\n| 运维负责人保存 | 运维负责人已更新 |\n| 运营城市保存 | 运营城市已更新 |\n| 车辆编辑保存 | 车辆信息已更新 |\n| 合同跳转 | 即将跳转至合同详情(原型演示):{合同编码} |\n| 证照跳转 | 即将跳转至证照管理编辑页(原型演示):{车牌} |\n| 保险跳转 | 即将跳转至保险采购编辑页(原型演示):{车牌} · {险种} |\n| 交车跳转 | 即将跳转至交车管理(原型演示):{车牌} |\n| 还车跳转 | 即将跳转至还车管理(原型演示):{车牌} |\n\nToast 约 3 秒后自动消失。\n\n---\n\n## 11. 分页\n\n- 默认每页 10 条,可选 10 / 20 / 50 / 100\n- 切换每页条数回到第 1 页\n- 总条数为筛选后的数量\n\n---\n\n## 12. 原型局限\n\n1. 数据为前端 JSON,刷新页面后编辑结果丢失(内存态)。\n2. 导入、导出、外链跳转均为演示提示。\n3. 地图依赖外网 OpenStreetMap。\n\n---\n\n## 13. 验收要点\n\n- [ ] 筛选查询/重置与 KPI 叠加正确\n- [ ] 空值列显示空白\n- [ ] 人工运营城市可改,车机/GPS 不可改\n- [ ] 未上传/未购买可点击并提示跳转\n- [ ] 交还车有数据可点击、无数据不可点\n- [ ] 运维负责人、编辑弹窗保存后列表即时更新\n- [ ] 管理员「更多」菜单与权限开关联动\n- [ ] 右上角「查看需求说明」展示本文档(列表页 PRD)\n- [ ] 「PRD文档MD文件下载」可下载列表页、详情页两份 .md\n- [ ] 「下载测试用例」可下载含列表页/详情页 Sheet 的 Excel\n" + }, + { + "type": "markdown", + "id": "vm-doc-prd-detail", + "title": "详情页 PRD", + "markdown": "# 车辆管理 · 详情页产品需求说明(PRD)\n\n> 本页为单车档案详情。标注「原型演示」的跨模块跳转仅 Toast 提示,不做真实路由。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 车辆管理 — 详情页(查看页) |\n| 入口 | 列表页点击车牌或「查看」 |\n| 核心任务 | 查看单车档案、证照、保险、租赁及各类业务记录,维护可编辑字段 |\n| 页面结构 | 顶栏返回 + 车辆概要 + 统计条 + Tab 内容区;右上角工具栏:「查看需求说明」「PRD文档MD文件下载」「下载测试用例」 |\n\n---\n\n## 2. 页面框架\n\n### 2.1 顶栏\n\n| 元素 | 行为 |\n|---|---|\n| 返回列表 | 回到列表页,保留列表筛选/KPI/分页状态 |\n| 查看需求说明 | 打开详情页 PRD(本文档) |\n| PRD文档MD文件下载 | 下载列表页、详情页两份 Markdown PRD |\n| 下载测试用例 | 下载 Excel,含列表页与详情页测试用例两个 Sheet |\n\n### 2.2 车辆概要区\n\n- 车牌号、品牌·型号、VIN\n- 在线状态(在线/离线圆点)\n- 运营城市文案、GPS 时间\n\n### 2.3 统计条(4 项)\n\n| 字段 | 可编辑 | 行为 |\n|---|---|---|\n| 运营城市 | 仅来源为「人工」时可改 | 点击 → 运营城市修改弹窗;展示车机/GPS/人工来源标签 |\n| 里程数 | 否 | 在线时展示车机标签 |\n| 停放区域 | 否 | 只读 |\n| 运维负责人 | 有运维权限时可改 | 点击铅笔图标 → 运维负责人弹窗 |\n\n---\n\n## 3. Tab 总览\n\n| 序号 | Tab | 说明 |\n|---|---|---|\n| 1 | 基本信息 | 档案、项目、证照摘要等,部分字段行内可编辑 |\n| 2 | 型号参数 | 车型参数与保养项目表 |\n| 3 | 证照信息 | 行驶证、营运证等分组展示与照片预览 |\n| 4 | 保险记录 | 险种概览卡片 + 采购历史表 |\n| 5 | 租赁记录 | 合同/交还车历史 |\n| 6 | 事故记录 | 筛选 + 事故列表 |\n| 7 | 故障记录 | 筛选 + 故障列表 |\n| 8 | 违章记录 | 筛选 + 违章列表 |\n| 9 | 异动记录 | 筛选 + 异动列表 |\n| 10 | 调拨记录 | 筛选 + 调拨列表 |\n| 11 | 年审记录 | 筛选 + 已完成年审历史(对齐年审管理历史列表) |\n\n各业务记录 Tab 通用规则:\n\n- 数据按当前车辆 `plateNo` 从对应 JSON 种子文件过滤\n- 筛选区:**查询**生效、**重置**清空,修改未查询不影响列表\n- 日期范围使用双月历弹层组件\n- 表格左对齐;金额/数字列使用等宽数字\n- 底栏:共 N 条 + 紧凑分页(默认 10 条/页)\n\n---\n\n## 4. 基本信息 Tab\n\n### 4.1 档案信息\n\n| 字段 | 可编辑 |\n|---|---|\n| 出厂年份 | 是(年份选择) |\n| 采购入库日期 | 是(日期选择) |\n| 运营状态、车辆来源、停车场等 | 只读展示 |\n\n保存后 Toast「车辆信息已更新」,写回内存台账。\n\n### 4.2 项目信息\n\n租赁/自营车辆展示项目名称、合同编码、客户名称;否则显示「无」。\n\n### 4.3 证照摘要\n\n- 检验有效期:展示到期日与剩余/过期标签\n- 未上传时可点击 → Toast 跳转证照管理编辑(原型演示)\n\n---\n\n## 5. 型号参数 Tab\n\n- 展示车辆型号技术参数(只读)\n- 保养项目表:养护项目、保养公里/时间周期、工时费、材料费、合计\n\n---\n\n## 6. 证照信息 Tab\n\n- 分组:行驶证、道路运输证、上海牌专项证件等(按车牌规则显隐)\n- 每证展示:证件号、有效期、余额(如有)、照片网格\n- 照片可点击全屏预览;证件状态色点图例\n- 数据来自 `utils/vehicleLicenses` 与车辆台账关联\n\n---\n\n## 7. 保险记录 Tab\n\n### 7.1 险种概览卡片\n\n交强险、商业险、承运人责任险等;点击切换下方历史表。\n\n- 未购买险种卡片可点击 → Toast 跳转保险采购(原型演示)\n- 展示状态 Tag 与到期剩余天数\n\n### 7.2 采购历史表\n\n| 列 | 说明 |\n|---|---|\n| 保险导入时间、操作人、类型、保单号 | 只读 |\n| 保险状态 | Tag |\n| 保险公司、付款时间、到期日期 | 只读 |\n| 保单文件 | 有附件时「预览」「下载」可点击 → Toast 演示 |\n\n---\n\n## 8. 租赁记录 Tab\n\n### 8.1 筛选\n\n合同编码、项目名称、客户名称、业务类型。\n\n### 8.2 表格与跳转\n\n| 列 | 可点击 | 跳转目标(原型 Toast) |\n|---|---|---|\n| 合同编码 | 是 | 租赁合同详情 |\n| 项目名称 | 有值时 | 租赁合同详情 |\n| 交车日期 | 有值时 | 交车管理详情 |\n| 还车日期 | 有值时 | 还车管理详情 |\n\n---\n\n## 9. 事故记录 Tab\n\n### 9.1 筛选\n\n事故时间(日期范围)、客户名称、事故等级、事故状态。\n\n### 9.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 事故编码 | 是 | 事故管理详情页 |\n| 其余列 | 否 | — |\n\n---\n\n## 10. 故障记录 Tab\n\n### 10.1 筛选\n\n上报时间(日期范围)、故障编号、故障等级、故障类型、解决情况、AI 匹配状态。\n\n### 10.2 表格\n\n| 列/操作 | 可点击 | 行为 |\n|---|---|---|\n| 故障编号 | 是 | Toast 跳转故障管理详情 |\n| 操作「查看」 | 是 | 同上 |\n| 操作「编辑」 | 是 | Toast 编辑故障记录演示 |\n\n---\n\n## 11. 违章记录 Tab\n\n### 11.1 筛选\n\n违法时间、客户名称、缴费状态、是否处理。\n\n### 11.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 违法行为 | 是 | 违章管理详情页(按违法时间标识) |\n| 其余列 | 否 | — |\n\n---\n\n## 12. 异动记录 Tab\n\n### 12.1 筛选\n\n异动开始日期、异动目的地、异动类型。\n\n### 12.2 表格\n\n宽表展示异动起止里程/电量/氢量等;**异动开始日期**可点击 → Toast 跳转异动管理详情。\n\n---\n\n## 13. 调拨记录 Tab\n\n### 13.1 筛选\n\n调拨日期、调出人、接收人。\n\n### 13.2 表格\n\n**调拨日期**可点击 → Toast 跳转车辆调拨详情。\n\n---\n\n## 14. 年审记录 Tab\n\n对齐「年审管理」历史列表字段(不含车牌/品牌/型号,因已在详情页上下文)。\n\n### 14.1 筛选\n\n| 字段 | 说明 |\n|---|---|\n| 完成时间 | 按办结日期范围 |\n| 检验有效期 | 按检验有效期至范围 |\n| 办理人 | 下拉选择 |\n\n### 14.2 表格列\n\n| 列 | 可点击 | 说明 |\n|---|---|---|\n| 检验有效期至 | **是** | Toast 跳转年审管理详情(原型演示) |\n| 检测服务站名称 | 否 | 无数据显示「—」 |\n| 检测费用 | 否 | 金额两位小数,无数据显示「—」 |\n| 二保服务站名称 | 否 | 同上 |\n| 二保费用 | 否 | 同上 |\n| 整备服务站名称 | 否 | 同上 |\n| 整备费用 | 否 | 同上 |\n| 办理人 | 否 | — |\n| 完成时间 | 否 | 办结时间,默认倒序 |\n\n### 14.3 数据\n\n- 来源:`data/annual-review-records.json`,按 `plateNo` 过滤\n- 样例车 `粤AGP5156` 含 3 条已完成年审记录\n\n---\n\n## 15. 跨模块跳转 Toast 汇总\n\n| 场景 | 文案模板 |\n|---|---|\n| 证照编辑 | 即将跳转至证照管理编辑页(原型演示):{车牌} |\n| 保险采购 | 即将跳转至保险采购编辑页(原型演示):{车牌} · {险种} |\n| 保单预览 | 预览保单文件(原型演示):{文件名} |\n| 保单下载 | 已开始下载:{文件名}(原型演示) |\n| 租赁合同 | 即将跳转至租赁合同详情(原型演示):{车牌} · {合同编码} |\n| 交车记录 | 即将跳转至交车管理详情(原型演示):{车牌} · {合同} · {交车日期} |\n| 还车记录 | 即将跳转至还车管理详情(原型演示):{车牌} · {合同} · {还车日期} |\n| 事故详情 | 即将跳转至事故管理详情页(原型演示):{事故编码} |\n| 故障详情 | 即将跳转至故障管理详情页(原型演示):{故障编号} |\n| 故障编辑 | 编辑故障记录(原型演示):{车牌} · {故障编号} |\n| 违章详情 | 即将跳转至违章管理详情页(原型演示):{违法时间} |\n| 异动详情 | 即将跳转至异动管理详情页(原型演示):{异动开始日期} |\n| 调拨详情 | 即将跳转至车辆调拨详情页(原型演示):{调拨日期} |\n| 年审详情 | 即将跳转至年审管理详情页(原型演示):{检验有效期至} |\n| 字段保存 | 车辆信息已更新 / 运维负责人已更新 / 运营城市已更新 |\n\n---\n\n## 16. 原型局限\n\n1. 各业务记录为前端 JSON 种子,不与外部年审/事故等模块真实联动。\n2. 详情 Tab 间切换不保留各 Tab 内筛选/分页状态(刷新组件即重置)。\n3. 行内编辑与弹窗保存仅更新内存,刷新页面后丢失。\n\n---\n\n## 17. 验收要点\n\n- [ ] 返回列表保留列表态\n- [ ] 11 个 Tab 均可切换且内容正确\n- [ ] 各记录 Tab 筛选查询/重置/分页正常\n- [ ] 所有标注可点击字段触发对应 Toast\n- [ ] 年审记录含检测服务站/检测费用列,检验有效期至可点击\n- [ ] 右上角「查看需求说明」展示本文档(详情页 PRD),非列表页 PRD\n- [ ] 「PRD文档MD文件下载」「下载测试用例」在详情页可用且行为正确\n" + } + ] + }, + { + "type": "folder", + "id": "vm-doc-list", + "title": "列表页模块", + "defaultExpanded": true, + "children": [ + { + "type": "route", + "id": "vm-route-list", + "title": "打开列表页", + "route": "vehicle-management-clay:list", + "payload": { + "pageId": "list" + } + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-doc-toolbar", + "title": "测试用例下载", + "markdown": "## 下载测试用例\n\n右上角工具栏,列表页与详情页均可用;下载含列表/详情两个 Sheet 的 Excel。" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-filter", + "title": "筛选条件", + "markdown": "## 4. 筛选区\n\n### 4.1 字段清单\n\n默认展示前 4 项,点击「展开更多筛选项」显示全部。\n\n| 字段 | 控件 | 选项来源 | 匹配规则 |\n|---|---|---|---|\n| 车牌号 | 多行弹层输入 | 用户输入 | 确认后精确匹配(归一化:去空格、大写) |\n| 运营城市 | 省-市级联多选 | 种子 + 停车场映射 + 台账 location | 与车辆运营城市(省-市格式)包含关系 |\n| 品牌/型号/客户/部门/项目/合同/所有权/运营公司/租赁公司/停车场 | 可搜索单选 | 当前台账去重排序 | 精确相等 |\n| 项目类型 | 可搜索单选 | 租赁、自营 | 与创建合同时合同类型相等 |\n| 运营状态 | 可搜索单选 | 租赁、自营、可运营、待运营、退出运营 | 精确相等 |\n| 车辆来源 | 可搜索单选 | 自有、外租、挂靠 | 精确相等 |\n\n### 4.2 操作逻辑\n\n| 操作 | 行为 |\n|---|---|\n| **查询** | 将当前筛选项写入「已应用条件」,列表与 KPI 计数基于全量台账;页码重置为 1 |\n| **重置** | 清空待应用与已应用条件,页码重置为 1 |\n| 修改筛选项未点查询 | 不影响当前列表 |\n\n筛选与 KPI Tab **叠加**:先按 KPI 分类过滤,再按筛选条件过滤。\n\n\n---\n\n### 原型实现补充\n\n- **区域**:华东 / 华南 / 华中 / 华北 / 东北 / 西南 / 西北;按运营城市所在省份映射过滤" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-all", + "title": "所有营运车辆", + "markdown": "## 当前卡片口径\n\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-lease", + "title": "租赁车辆", + "markdown": "## 当前卡片口径\n\n| 租赁车辆 | `operateStatus === '租赁'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-self", + "title": "自营车辆", + "markdown": "## 当前卡片口径\n\n| 自营车辆 | `operateStatus === '自营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-stock", + "title": "库存车辆", + "markdown": "## 当前卡片口径\n\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-nonOperating", + "title": "非运营车辆", + "markdown": "## 当前卡片口径\n\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-exit", + "title": "退出运营车辆", + "markdown": "## 当前卡片口径\n\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-licenseAbnormal", + "title": "证照异常车辆", + "markdown": "## 当前卡片口径\n\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-kpi-insuranceAbnormal", + "title": "保险异常车辆", + "markdown": "## 当前卡片口径\n\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---\n\n## 5. KPI 分类卡片\n\n点击卡片切换列表分类,并回到第 1 页。卡片右上角「?」展示口径说明。\n\n| KPI | 统计口径 |\n|---|---|\n| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 |\n| 租赁车辆 | `operateStatus === '租赁'` |\n| 自营车辆 | `operateStatus === '自营'` |\n| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 |\n| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` |\n| 退出运营车辆 | `operateStatus === '退出运营'` |\n| 证照异常车辆 | 检验有效期已过期(`inspectExpire` 早于今天) |\n| 保险异常车辆 | `insuranceStatus === '异常'` |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-table-toolbar", + "title": "导出与批量导入", + "markdown": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-vehicle-table", + "title": "台账列表", + "markdown": "## 7. 列表字段说明\n\n### 7.1 车辆信息(冻结列)\n\n- 车牌:链接色,点击进入**车辆详情页**\n- VIN、品牌·型号:只读展示\n\n### 7.2 运营城市\n\n- 展示:省-市(由 `location` 解析)\n- 标签:车机 / GPS / 人工(来源 `operateCitySource` 或推导)\n- **可编辑**:仅来源为「人工」时可点击修改 → 运营城市修改弹窗\n- 车机/GPS 来源:不可改,hover 提示原因\n\n### 7.3 所属项目\n\n仅**租赁/自营**车辆展示项目名称、合同编码、客户名称;否则三行均为空。\n\n- 项目名称、合同编码:有值时可点击 → Toast 跳转合同详情(原型演示)\n\n### 7.4 里程数\n\n- 在线车辆:显示里程 + 「车机」标签,里程按车机数据格式化\n- 离线:仅里程或空\n\n### 7.5 证照 / 保险\n\n| 列 | 有数据 | 无数据/异常 |\n|---|---|---|\n| 检验有效期 | 到期日 + 剩余/已过期天数标签 | 「未上传」可点击 + 「禁止交车」标签 |\n| 保险状态 | 正常/异常 Tag;异常时 hover 展示具体到期险种 | — |\n| 交强险/商业险到期 | 到期日 + 剩余天数 | 「未购买」可点击 + 「禁止交车」 |\n\n未上传/未购买点击 → Toast 跳转证照管理或保险采购(原型演示)。\n\n### 7.6 上次交车 / 还车情况\n\n三行纯数值(无行内标题):里程(km)、人员、时间(至分钟)。**有数据时**可点击。\n\n| 列 | 点击行为 |\n|---|---|\n| 上次交车情况 | Toast:`即将跳转至交车管理(原型演示):{车牌}` |\n| 上次还车情况 | Toast:`即将跳转至还车管理(原型演示):{车牌}` |\n\n### 7.7 三方租赁公司\n\n仅外租车辆显示 `leaseCompany`,否则空白。\n\n### 7.8 当前位置\n\n- 在线状态圆点 + 文案\n- 详细地址:有 GPS 坐标时可点击 → 地图弹窗(OpenStreetMap 嵌入)\n- GPS 时间:精确到秒\n\n### 7.9 运维负责人\n\n可编辑时显示为链接;无负责人显示「设置负责人」→ 运维负责人弹窗。\n\n### 7.10 操作列(冻结)\n\n| 操作 | 条件 | 行为 |\n|---|---|---|\n| 查看 | 始终 | 进入车辆详情页 |\n| 更多 | 管理员或有运维权限 | 下拉:编辑(管理员)、修改运维负责人 |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-import-modal", + "title": "批量导入", + "markdown": "## 6. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 导出 | Toast:`导出功能(原型演示):当前列表共 N 条` |\n| 批量导入 | 打开导入弹窗 |\n\n### 6.1 批量导入弹窗\n\n1. 下载 CSV 模板(含必填字段说明行)\n2. 上传文件 → Toast:`已接收文件 {文件名},导入校验为原型演示`\n\n原型不做真实校验与落库。\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-edit-modal", + "title": "编辑车辆", + "markdown": "### 8.3 编辑车辆(管理员)\n\n- 可搜索选择器字段与筛选区一致规范\n- 保存时运营状态、车辆状态仍为必填(按钮禁用),但标签无红色 *\n- Toast「车辆信息已更新」" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-ops-modal", + "title": "运维负责人", + "markdown": "### 8.1 设置运维负责人\n\n- 标题下胶囊:车牌 | 运维区域(省-市)\n- 多选搜索选择器,候选人按车辆区域过滤\n- **保存**:写回 `opsManagers`,Toast「运维负责人已更新」\n- **取消**:不保存\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联编辑面板(非弹窗)" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-operate-city-modal", + "title": "修改运营城市", + "markdown": "### 8.2 修改运营城市\n\n- 省-市选择,保存后 `location` 更新且 `operateCitySource` 置为「人工」\n- Toast「运营城市已更新」\n\n---\n\n### 原型实现补充\n\n- 详情页统计条改用内联选择器(非弹窗)" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-location-map-modal", + "title": "车辆定位地图", + "markdown": "### 8.4 地图\n\n展示坐标点与 GPS 时间;关闭不保存。\n\n---" + } + ] + }, + { + "type": "folder", + "id": "vm-doc-detail", + "title": "详情页模块", + "defaultExpanded": true, + "children": [ + { + "type": "route", + "id": "vm-route-detail", + "title": "打开详情页", + "route": "vehicle-management-clay:detail", + "payload": { + "pageId": "detail" + } + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-doc-toolbar", + "title": "测试用例下载", + "markdown": "## 下载测试用例\n\n右上角工具栏,列表页与详情页均可用;下载含列表/详情两个 Sheet 的 Excel。" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-hero", + "title": "车辆概要", + "markdown": "### 2.1 顶栏\n\n| 元素 | 行为 |\n|---|---|\n| 返回列表 | 回到列表页,保留列表筛选/KPI/分页状态 |\n| 查看需求说明 | 打开详情页 PRD(本文档) |\n| PRD文档MD文件下载 | 下载列表页、详情页两份 Markdown PRD |\n| 下载测试用例 | 下载 Excel,含列表页与详情页测试用例两个 Sheet |\n\n---\n\n### 2.2 车辆概要区\n\n- 车牌号、品牌·型号、VIN\n- 在线状态(在线/离线圆点)\n- 运营城市文案、GPS 时间\n\n---\n\n### 原型实现补充\n\n- 顶栏仅保留「返回列表」与「下载测试用例」" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-stats", + "title": "统计条", + "markdown": "### 2.3 统计条(4 项)\n\n| 字段 | 可编辑 | 行为 |\n|---|---|---|\n| 运营城市 | 仅来源为「人工」时可改 | 点击 → 运营城市修改弹窗;展示车机/GPS/人工来源标签 |\n| 里程数 | 否 | 在线时展示车机标签 |\n| 停放区域 | 否 | 只读 |\n| 运维负责人 | 有运维权限时可改 | 点击铅笔图标 → 运维负责人弹窗 |\n\n\n---\n\n### 原型实现补充\n\n- **运营城市**、**运维负责人**:详情页使用内联编辑面板(非弹窗)" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tabs", + "title": "Tab 导航", + "markdown": "## 3. Tab 总览\n\n| 序号 | Tab | 说明 |\n|---|---|---|\n| 1 | 基本信息 | 档案、项目、证照摘要等,部分字段行内可编辑 |\n| 2 | 型号参数 | 车型参数与保养项目表 |\n| 3 | 证照信息 | 行驶证、营运证等分组展示与照片预览 |\n| 4 | 保险记录 | 险种概览卡片 + 采购历史表 |\n| 5 | 租赁记录 | 合同/交还车历史 |\n| 6 | 事故记录 | 筛选 + 事故列表 |\n| 7 | 故障记录 | 筛选 + 故障列表 |\n| 8 | 违章记录 | 筛选 + 违章列表 |\n| 9 | 异动记录 | 筛选 + 异动列表 |\n| 10 | 调拨记录 | 筛选 + 调拨列表 |\n| 11 | 年审记录 | 筛选 + 已完成年审历史(对齐年审管理历史列表) |\n\n各业务记录 Tab 通用规则:\n\n- 数据按当前车辆 `plateNo` 从对应 JSON 种子文件过滤\n- 筛选区:**查询**生效、**重置**清空,修改未查询不影响列表\n- 日期范围使用双月历弹层组件\n- 表格左对齐;金额/数字列使用等宽数字\n- 底栏:共 N 条 + 紧凑分页(默认 10 条/页)\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-basic", + "title": "基本信息 Tab", + "markdown": "## 4. 基本信息 Tab\n\n### 4.1 档案信息\n\n| 字段 | 可编辑 |\n|---|---|\n| 出厂年份 | 是(年份选择) |\n| 采购入库日期 | 是(日期选择) |\n| 运营状态、车辆来源、停车场等 | 只读展示 |\n\n保存后 Toast「车辆信息已更新」,写回内存台账。\n\n### 4.2 项目信息\n\n租赁/自营车辆展示项目名称、合同编码、客户名称;否则显示「无」。\n\n### 4.3 证照摘要\n\n- 检验有效期:展示到期日与剩余/过期标签\n- 未上传时可点击 → Toast 跳转证照管理编辑(原型演示)\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-model", + "title": "型号参数 Tab", + "markdown": "## 5. 型号参数 Tab\n\n- 展示车辆型号技术参数(只读)\n- 保养项目表:养护项目、保养公里/时间周期、工时费、材料费、合计\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-license", + "title": "证照信息 Tab", + "markdown": "## 6. 证照信息 Tab\n\n- 分组:行驶证、道路运输证、上海牌专项证件等(按车牌规则显隐)\n- 每证展示:证件号、有效期、余额(如有)、照片网格\n- 照片可点击全屏预览;证件状态色点图例\n- 数据来自 `utils/vehicleLicenses` 与车辆台账关联\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-insurance", + "title": "保险记录 Tab", + "markdown": "## 7. 保险记录 Tab\n\n### 7.1 险种概览卡片\n\n交强险、商业险、承运人责任险等;点击切换下方历史表。\n\n- 未购买险种卡片可点击 → Toast 跳转保险采购(原型演示)\n- 展示状态 Tag 与到期剩余天数\n\n### 7.2 采购历史表\n\n| 列 | 说明 |\n|---|---|\n| 保险导入时间、操作人、类型、保单号 | 只读 |\n| 保险状态 | Tag |\n| 保险公司、付款时间、到期日期 | 只读 |\n| 保单文件 | 有附件时「预览」「下载」可点击 → Toast 演示 |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-lease", + "title": "租赁记录 Tab", + "markdown": "## 8. 租赁记录 Tab\n\n### 8.1 筛选\n\n合同编码、项目名称、客户名称、业务类型。\n\n### 8.2 表格与跳转\n\n| 列 | 可点击 | 跳转目标(原型 Toast) |\n|---|---|---|\n| 合同编码 | 是 | 租赁合同详情 |\n| 项目名称 | 有值时 | 租赁合同详情 |\n| 交车日期 | 有值时 | 交车管理详情 |\n| 还车日期 | 有值时 | 还车管理详情 |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-accident", + "title": "事故记录 Tab", + "markdown": "## 9. 事故记录 Tab\n\n### 9.1 筛选\n\n事故时间(日期范围)、客户名称、事故等级、事故状态。\n\n### 9.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 事故编码 | 是 | 事故管理详情页 |\n| 其余列 | 否 | — |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-fault", + "title": "故障记录 Tab", + "markdown": "## 10. 故障记录 Tab\n\n### 10.1 筛选\n\n上报时间(日期范围)、故障编号、故障等级、故障类型、解决情况、AI 匹配状态。\n\n### 10.2 表格\n\n| 列/操作 | 可点击 | 行为 |\n|---|---|---|\n| 故障编号 | 是 | Toast 跳转故障管理详情 |\n| 操作「查看」 | 是 | 同上 |\n| 操作「编辑」 | 是 | Toast 编辑故障记录演示 |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-violation", + "title": "违章记录 Tab", + "markdown": "## 11. 违章记录 Tab\n\n### 11.1 筛选\n\n违法时间、客户名称、缴费状态、是否处理。\n\n### 11.2 表格\n\n| 列 | 可点击 | 跳转 |\n|---|---|---|\n| 违法行为 | 是 | 违章管理详情页(按违法时间标识) |\n| 其余列 | 否 | — |\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-movement", + "title": "异动记录 Tab", + "markdown": "## 12. 异动记录 Tab\n\n### 12.1 筛选\n\n异动开始日期、异动目的地、异动类型。\n\n### 12.2 表格\n\n宽表展示异动起止里程/电量/氢量等;**异动开始日期**可点击 → Toast 跳转异动管理详情。\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-transfer", + "title": "调拨记录 Tab", + "markdown": "## 13. 调拨记录 Tab\n\n### 13.1 筛选\n\n调拨日期、调出人、接收人。\n\n### 13.2 表格\n\n**调拨日期**可点击 → Toast 跳转车辆调拨详情。\n\n---" + }, + { + "type": "markdown", + "id": "vm-doc-node-vm-detail-tab-annual", + "title": "年审记录 Tab", + "markdown": "## 14. 年审记录 Tab\n\n对齐「年审管理」历史列表字段(不含车牌/品牌/型号,因已在详情页上下文)。\n\n### 14.1 筛选\n\n| 字段 | 说明 |\n|---|---|\n| 完成时间 | 按办结日期范围 |\n| 检验有效期 | 按检验有效期至范围 |\n| 办理人 | 下拉选择 |\n\n### 14.2 表格列\n\n| 列 | 可点击 | 说明 |\n|---|---|---|\n| 检验有效期至 | **是** | Toast 跳转年审管理详情(原型演示) |\n| 检测服务站名称 | 否 | 无数据显示「—」 |\n| 检测费用 | 否 | 金额两位小数,无数据显示「—」 |\n| 二保服务站名称 | 否 | 同上 |\n| 二保费用 | 否 | 同上 |\n| 整备服务站名称 | 否 | 同上 |\n| 整备费用 | 否 | 同上 |\n| 办理人 | 否 | — |\n| 完成时间 | 否 | 办结时间,默认倒序 |\n\n### 14.3 数据\n\n- 来源:`data/annual-review-records.json`,按 `plateNo` 过滤\n- 样例车 `粤AGP5156` 含 3 条已完成年审记录\n\n---" + } + ] + } + ] + } + ] + } +} diff --git a/src/prototypes/vehicle-management-clay/index.tsx b/src/prototypes/vehicle-management-clay/index.tsx new file mode 100644 index 0000000..cc393c1 --- /dev/null +++ b/src/prototypes/vehicle-management-clay/index.tsx @@ -0,0 +1,395 @@ +/** + * @name 车辆管理(Clay 主题备份) + * 设计基底:src/themes/clay/DESIGN.md · 功能与标注同 vehicle-management 原版 + */ + +import '../vehicle-management/style.css'; +import './styles/clay-theme.css'; +import React, { useCallback, useMemo, useState } from 'react'; +import { Download, FileUp } from 'lucide-react'; +import { + AnnotationViewer, + type AnnotationDirectoryRouteNode, + type AnnotationSourceDocument, + type AnnotationViewerOptions, +} from '@axhub/annotation'; +import seedVehicles from '../vehicle-management/data/vehicles.json'; +import seedInsurancePurchases from '../vehicle-management/data/insurance-purchases.json'; +import { FilterPanel } from '../vehicle-management/components/FilterPanel'; +import { ImportModal } from '../vehicle-management/components/ImportModal'; +import { KpiCards } from '../vehicle-management/components/KpiCards'; +import { OpsManagerModal } from '../vehicle-management/components/OpsManagerModal'; +import { OperateCityEditModal } from '../vehicle-management/components/OperateCityEditModal'; +import { LocationMapModal } from '../vehicle-management/components/LocationMapModal'; +import { VehicleEditModal } from '../vehicle-management/components/VehicleEditModal'; +import { PageDocToolbar } from '../vehicle-management/components/PageDocToolbar'; +import { VehicleTable } from '../vehicle-management/components/VehicleTable'; +import { TablePagination } from '../vehicle-management/components/TablePagination'; +import { DetailPage } from '../vehicle-management/pages/DetailPage'; +import { + EMPTY_FILTERS, + type KpiKey, + type VehicleFilters, + type VehicleRecord, +} from '../vehicle-management/types'; +import type { InsurancePurchaseRecord } from '../vehicle-management/types'; +import { applyFilters } from '../vehicle-management/utils/filters'; +import { buildInsuranceExpireMap } from '../vehicle-management/utils/insurance'; +import type { LeaseRecordRow } from '../vehicle-management/utils/leaseRecords'; +import type { AccidentRecordRow } from '../vehicle-management/utils/accidentRecords'; +import type { ViolationRecordRow } from '../vehicle-management/utils/violationRecords'; +import type { MovementRecordRow } from '../vehicle-management/utils/movementRecords'; +import type { TransferRecordRow } from '../vehicle-management/utils/transferRecords'; +import type { AnnualReviewRecordRow } from '../vehicle-management/utils/annualReviewRecords'; +import { downloadVehicleTestCasesExcel } from '../vehicle-management/utils/testCaseExport'; +import { countKpi } from '../vehicle-management/utils/vehicle'; +import annotationSourceDocument from './annotation-source.json'; + +const CAN_EDIT_OPS_MANAGER = true; +const IS_ADMIN = true; +const DEFAULT_PAGE_SIZE = 10; + +function ClayBackupBanner() { + return ( +
+
+ Clay Design System +

车辆管理 · Clay 主题备份页

+

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

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