迭代 ONE-OS 多原型:租赁合同增强、Clay 车辆备份、侧边栏分组,移除周报与安全培训原型。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-01 16:28:46 +08:00
parent 2e0a3d7da4
commit b9dee3a78d
32 changed files with 4456 additions and 4340 deletions

View File

@@ -1,43 +1,71 @@
{
"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": "folder-1782874576229-r8atbu",
"kind": "folder",
"title": "OneOS",
"children": [
{
"id": "item:prototypes:oneos-weekly-report",
"kind": "item",
"title": "数智部周报",
"itemKey": "prototypes/oneos-weekly-report"
},
"id": "folder-1782874599304-roj8fd",
"kind": "folder",
"title": "运维管理",
"children": [
{
"id": "item:prototypes:vehicle-management",
"kind": "item",
"title": "车辆管理",
"title": "车辆资产",
"itemKey": "prototypes/vehicle-management"
},
{
"id": "item:prototypes:contract-template-management",
"id": "item:prototypes:vehicle-management-clay",
"kind": "item",
"title": "合同模板管理",
"itemKey": "prototypes/contract-template-management"
"title": "车辆资产Clay",
"itemKey": "prototypes/vehicle-management-clay"
}
]
},
{
"id": "item:prototypes:lease-contract-management",
"kind": "item",
"title": "租赁合同管理",
"itemKey": "prototypes/lease-contract-management"
},
"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",
"kind": "item",
@@ -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",

View File

@@ -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} (需求说明文档浏览)`);

View File

@@ -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",

View File

@@ -69,13 +69,15 @@ const vmTheme = {
},
};
export default function ContractTemplateManagementPage() {
export function ContractTemplateManagementContent({ embedded = false }: { embedded?: boolean }) {
const [annotationPageId, setAnnotationPageId] = useState<ContractPageId>('list');
const navigationRef = useRef<ContractNavigationRef>({});
useEffect(() => {
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() {
</ConfigProvider>
);
}
export default function ContractTemplateManagementPage() {
return <ContractTemplateManagementContent />;
}

View File

@@ -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",

View File

@@ -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) => (
<div className="lc-filter-field">
<label className="lc-filter-field">
<span className="lc-filter-field-label">{label}</span>
<div className="lc-filter-field-control">{control}</div>
</div>
</label>
);
const isRetiredVehicle = (record) => record?.status === '退出运营';
@@ -6429,8 +6360,7 @@ const Component = function () {
return (
<div className="lc-list-expire-cell">
<div
className="lc-list-expire-date"
style={{ color: muted ? '#94a3b8' : (dateVal ? '#334155' : '#94a3b8') }}
className={`lc-list-expire-date${muted ? ' is-muted' : (dateVal ? ' has-value' : ' is-empty')}`}
>
{dateVal || '—'}
</div>
@@ -6472,14 +6402,17 @@ const Component = function () {
return (
<div className="lc-list-plate-cell">
<span
className={noPlate && !muted ? 'lc-list-plate-empty' : ''}
style={{ fontWeight: 600, fontSize: 13, color: muted ? '#94a3b8' : (noPlate ? '#94a3b8' : '#0f172a') }}
className={[
'lc-list-plate-primary',
muted ? 'is-muted' : (noPlate ? 'is-empty' : 'is-active'),
noPlate && !muted ? 'lc-list-plate-empty' : '',
].filter(Boolean).join(' ')}
>
{displayPlate}
</span>
{sub ? (
<Tooltip title={sub}>
<span className="lc-list-plate-sub lc-cell-ellipsis" style={{ color: muted ? '#94a3b8' : '#64748b' }}>
<span className={`lc-list-plate-sub lc-cell-ellipsis${muted ? ' is-muted' : ' is-active'}`}>
{sub}
</span>
</Tooltip>
@@ -6496,7 +6429,7 @@ const Component = function () {
onHeaderCell: listColumnHeaderCell,
render: (vin, record) => (
<Tooltip title={vin}>
<span className="lc-cell-ellipsis" style={{ fontFamily: 'monospace', fontSize: 11, color: isRetiredVehicle(record) ? '#94a3b8' : '#475569' }}>
<span className={`lc-list-vin lc-cell-ellipsis${isRetiredVehicle(record) ? ' is-muted' : ''}`}>
{vin}
</span>
</Tooltip>
@@ -6509,7 +6442,7 @@ const Component = function () {
width: 80,
onHeaderCell: listColumnHeaderCell,
render: (status) => (
<span style={{ fontSize: 13, fontWeight: 600, color: status === '退出运营' ? '#94a3b8' : '#334155' }}>
<span className={`lc-list-status-text${status === '退出运营' ? ' is-muted' : ''}`}>
{status}
</span>
),
@@ -6574,7 +6507,7 @@ const Component = function () {
<Button
type="link"
size="small"
style={{ fontWeight: 600, color: '#10b981', padding: 0 }}
className="lc-action-btn"
onClick={() => openVehicleInsuranceMgmt(record)}
>
管理
@@ -6592,7 +6525,6 @@ const Component = function () {
value={listFilters.plateNo}
onChange={(e) => setListFilters((prev) => ({ ...prev, plateNo: e.target.value }))}
onPressEnter={handleListFilterQuery}
style={{ borderRadius: 8 }}
/>
)),
renderFilterField('多车牌', (
@@ -6609,7 +6541,6 @@ const Component = function () {
value={multiPlateDraft}
onChange={(e) => setMultiPlateDraft(e.target.value)}
placeholder={'沪A03561F\n粤B58888F'}
style={{ borderRadius: 8 }}
/>
<div className="lc-multi-plate-pop-actions">
<Button size="small" onClick={() => setMultiPlateOpen(false)}>取消</Button>
@@ -6623,7 +6554,6 @@ const Component = function () {
readOnly
placeholder="点击输入多个车牌"
value={appliedMultiPlates.length ? `已选 ${appliedMultiPlates.length} 个车牌` : ''}
style={{ borderRadius: 8 }}
/>
</Popover>
)),
@@ -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 (
<div
className="lc-edit-page"
style={{
padding: '24px 24px 80px',
height: '100vh',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%)',
overflow: 'hidden',
boxSizing: 'border-box',
}}
>
<div className="vm-page ipc-page">
<style>{PAGE_STYLE}</style>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<div className="lc-page-header" data-annotation-id="ipc-header" style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
<div>
<div style={{ fontSize: 18, fontWeight: 800, color: '#0f172a', lineHeight: 1.3 }}>保险采购</div>
<div style={{ fontSize: 12, color: '#64748b', marginTop: 4 }}>保单管理一车一档· 比价单独立管理互不关联</div>
</div>
<Button
type="default"
icon={ICONS.policy}
style={{ borderRadius: 8, border: '1px solid #cbd5e1', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 6, color: '#475569' }}
onClick={() => setPrdOpen(true)}
>
查看需求说明
</Button>
</div>
<Card className="lc-filter-card" data-annotation-id="ipc-filter" title="保单管理 · 筛选条件" bordered={false}>
<div className="ipc-list-shell">
<Card className="lc-filter-card" data-annotation-id="ipc-filter" title="筛选条件" bordered={false}>
<div className="lc-filter-grid">
{(listFilterExpanded ? listFilterFieldNodes : listFilterFieldNodes.slice(0, 4))}
</div>
@@ -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'
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 () {
))}
</div>
<div className="lc-table-section" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div className="lc-table-section">
<div className="lc-table-toolbar">
<span style={{ fontSize: 13, fontWeight: 600, color: '#475569' }}>保单录入</span>
<span className="lc-table-toolbar-meta">保单录入</span>
<div className="lc-table-toolbar-actions lc-policy-toolbar">
<Button
data-annotation-id="ipc-btn-compare-mgmt"
type="primary"
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', border: 'none' }}
onClick={openCompareMgmtModal}
>
比价单管理
</Button>
<Button
data-annotation-id="ipc-btn-policy-ocr"
type="default"
style={{ borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }}
onClick={() => openPolicyRecogn('ocr', 'policy')}
>
保单批量识别
</Button>
<Button
data-annotation-id="ipc-btn-recogn-tasks"
style={{ borderRadius: 8, fontWeight: 600 }}
onClick={openPolicyRecognTasksModal}
>
识别任务记录
</Button>
<Button
data-annotation-id="ipc-btn-batch-import"
style={{ borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }}
onClick={() => openPolicyRecogn('import', 'policy')}
>
批量导入
</Button>
<Button
data-annotation-id="ipc-btn-policy-add"
style={{ borderRadius: 8, fontWeight: 600 }}
onClick={() => {
setPolicyAddDraft({ ...EMPTY_POLICY_DETAIL, bizType: 'policy' });
setPolicyAddAttachmentFileList([]);
@@ -6875,7 +6767,7 @@ const Component = function () {
</Button>
</div>
</div>
<div className="lc-table-card" data-annotation-id="ipc-vehicle-table" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
<div className="lc-table-card" data-annotation-id="ipc-vehicle-table">
<Table
className="lc-list-table"
columns={listColumns}
@@ -6886,9 +6778,9 @@ const Component = function () {
scroll={{ x: 1040 }}
locale={{
emptyText: (
<div style={{ padding: '40px 0' }}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" strokeWidth="1.5" style={{ marginBottom: 12 }}><circle cx="12" cy="12" r="10" /><line x1="8" y1="12" x2="16" y2="12" /></svg>
<div style={{ color: '#94a3b8' }}>暂无符合检索条件的保险台账车辆</div>
<div className="lc-list-empty">
<svg className="lc-list-empty-icon" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden><circle cx="12" cy="12" r="10" /><line x1="8" y1="12" x2="16" y2="12" /></svg>
<div>暂无符合检索条件的保险台账车辆</div>
</div>
),
}}

View File

@@ -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",

View File

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

View File

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

View File

@@ -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",

View File

@@ -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' },
'添加授权人须同时上传授权委托书',
'添加受托人须同时上传授权委托书',
),
),
),

View File

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

View File

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

View File

@@ -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', {
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);
}, 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,
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],

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -338,7 +338,7 @@ function buildLeaseOrderPreviewRow(row, brandModelPair, plate, leaseOrder) {
+ '</tr>';
}
/** 授权委托书:托人信息反写 */
/** 授权委托书:托人信息反写 */
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) {
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 + ' ');
}

View File

@@ -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: '保存/提交时展示将走的审批类型。' },
];

View File

@@ -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 (
<OneosWebLegacyShell
moduleTitle="安全培训扫码"
pages={pages}
defaultPageId="u5b89u5168u57f9u8badu626bu7801"
/>
);
}

View File

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

View File

@@ -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<IssueStatus, string> = {
open: '待处理',
in_progress: '处理中',
resolved: '已闭环',
pending_feedback: '待反馈',
};
const STATUS_TONE: Record<IssueStatus, string> = {
open: 'wr-badge--gray',
in_progress: 'wr-badge--blue',
resolved: 'wr-badge--green',
pending_feedback: 'wr-badge--amber',
};
const TRACK_ICON: Record<TrackKey, React.ReactNode> = {
product: <Layers size={20} strokeWidth={2.25} aria-hidden />,
dev: <Code2 size={20} strokeWidth={2.25} aria-hidden />,
qa: <FlaskConical size={20} strokeWidth={2.25} aria-hidden />,
};
const MILESTONE_STATUS: Record<string, { label: string; tone: string }> = {
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 (
<div className="wr-work-report">
<h5 className="wr-work-report__title">{props.title || '工作汇报'}</h5>
{props.items.length ? (
<ul className="wr-work-report__list">
{props.items.map(function (item) {
return (
<li key={item.id} className={'wr-work-report__item' + (item.done ? ' wr-work-report__item--done' : '')}>
<span className="wr-work-report__icon" aria-hidden>
{item.done ? <CheckCircle2 size={16} strokeWidth={2.25} /> : <Circle size={16} strokeWidth={2.25} />}
</span>
<div className="wr-work-report__body">
<span className="wr-work-report__text">{item.title}</span>
{item.note ? <span className="wr-work-report__note">{item.note}</span> : null}
</div>
</li>
);
})}
</ul>
) : (
<p className="wr-work-report__empty">{props.emptyText}</p>
)}
</div>
);
}
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 (
<svg
className="wr-req-progress"
width={size}
height={size}
viewBox={'0 0 ' + size + ' ' + size}
role="img"
aria-label={props.label + ',完成 ' + clamped + '%'}
>
<circle
className="wr-req-progress__track"
cx={center}
cy={center}
r={radius}
fill="none"
strokeWidth={stroke}
/>
<circle
className={'wr-req-progress__fill' + (clamped >= 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 + ')'}
/>
<text
className="wr-req-progress__text tabular-nums"
x={center}
y={center}
dominantBaseline="middle"
textAnchor="middle"
>
{clamped}%
</text>
</svg>
);
}
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 (
<div className="wr-work-report">
<h5 className="wr-work-report__title">{props.title}</h5>
<ul className="wr-req-list">
{sortedItems.map(function (item) {
return (
<li key={item.id} className="wr-req-item">
<CircularProgressRing value={item.progress} label={item.title} />
<div className="wr-req-item__body">
<div className="wr-req-item__tags">
<span className="wr-req-tag wr-req-tag--stage">{item.stage}</span>
<span className={'wr-req-tag wr-req-tag--priority ' + requirementPriorityTone(item.priority)}>
{item.priority}
</span>
</div>
<p className="wr-req-item__text">{item.title}</p>
</div>
</li>
);
})}
</ul>
</div>
);
}
const EXEC_HEALTH_TONE: Record<ExecutiveHealth, string> = {
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 (
<article className={'wr-kpi-card wr-kpi-card--' + (kpi.tone || 'default')}>
<div className="wr-kpi-card__body">
<div className="wr-kpi-card__value tabular-nums">{kpi.value}</div>
<div className="wr-kpi-card__title">{kpi.label}</div>
{kpi.hint ? <div className="wr-kpi-card__desc">{kpi.hint}</div> : null}
</div>
</article>
);
}
function ExecutiveSummarySection() {
var summary = EXECUTIVE_SUMMARY;
return (
<section className="wr-exec-summary" aria-labelledby="wr-exec-summary-title">
<div className="wr-exec-summary__head">
<div>
<h2 id="wr-exec-summary-title" className="wr-section__title"></h2>
<p className="wr-section__desc"></p>
</div>
<span className={'wr-exec-health ' + EXEC_HEALTH_TONE[summary.health]}>{summary.healthLabel}</span>
</div>
<p className="wr-exec-summary__headline">{summary.headline}</p>
<div className="wr-exec-summary__grid">
<div className="wr-exec-summary__block">
<h3 className="wr-exec-summary__block-title">
<CheckCircle2 size={16} strokeWidth={2.25} aria-hidden />
</h3>
<ul className="wr-exec-summary__list">
{summary.achievements.map(function (item, index) {
return <li key={index}>{item}</li>;
})}
</ul>
</div>
<div className="wr-exec-summary__block wr-exec-summary__block--risk">
<h3 className="wr-exec-summary__block-title">
<AlertTriangle size={16} strokeWidth={2.25} aria-hidden />
</h3>
<ul className="wr-exec-summary__list">
{summary.risks.map(function (item, index) {
return <li key={index}>{item}</li>;
})}
</ul>
</div>
</div>
</section>
);
}
function ExecutiveDecisionCard(props: { decision: ExecutiveDecision }) {
var decision = props.decision;
return (
<article className={'wr-exec-decision' + (decision.urgency === 'high' ? ' wr-exec-decision--urgent' : '')}>
<header className="wr-exec-decision__head">
<h3 className="wr-exec-decision__title">{decision.title}</h3>
<span className={'wr-badge ' + (decision.urgency === 'high' ? 'wr-badge--red' : 'wr-badge--amber')}>
{decision.urgency === 'high' ? '紧急' : '一般'}
</span>
</header>
<dl className="wr-exec-decision__meta">
<div>
<dt></dt>
<dd>{decision.impact}</dd>
</div>
<div>
<dt></dt>
<dd>{decision.recommendation}</dd>
</div>
<div>
<dt></dt>
<dd className="tabular-nums">{decision.deadline}</dd>
</div>
</dl>
</article>
);
}
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 (
<div className="wr-exec-next">
<div className="wr-exec-next__col">
<h3 className="wr-exec-next__title">
<Flag size={16} strokeWidth={2.25} aria-hidden />
</h3>
<ul className="wr-exec-next__list">
{goals.map(function (item) {
return (
<li key={item.id} className="wr-exec-next__item">
<strong>{item.title}</strong>
<span>{item.detail}</span>
</li>
);
})}
</ul>
</div>
<div className="wr-exec-next__col wr-exec-next__col--risk">
<h3 className="wr-exec-next__title">
<AlertTriangle size={16} strokeWidth={2.25} aria-hidden />
</h3>
<ul className="wr-exec-next__list">
{risks.map(function (item) {
return (
<li key={item.id} className="wr-exec-next__item">
<strong>{item.title}</strong>
<span>{item.detail}</span>
</li>
);
})}
</ul>
</div>
</div>
);
}
function ProgressBar(props: { value: number; label: string }) {
return (
<div className="wr-progress" role="group" aria-label={props.label}>
<div className="wr-progress__head">
<span className="wr-progress__label">{props.label}</span>
<span className="wr-progress__pct tabular-nums">{props.value}%</span>
</div>
<div className="wr-progress__track" aria-hidden>
<div className="wr-progress__fill" style={{ width: props.value + '%' }} />
</div>
</div>
);
}
function FourRGrid(props: { fourR: WeeklyIssue['fourR'] }) {
return (
<dl className="wr-four-r-grid">
<div className="wr-four-r-item">
<dt><Target size={15} strokeWidth={2.25} aria-hidden /> </dt>
<dd>{props.fourR.reason}</dd>
</div>
<div className="wr-four-r-item">
<dt><ClipboardList size={15} strokeWidth={2.25} aria-hidden /> </dt>
<dd>{props.fourR.measure}</dd>
</div>
<div className="wr-four-r-item">
<dt><CheckCircle2 size={15} strokeWidth={2.25} aria-hidden /> </dt>
<dd>{props.fourR.result}</dd>
</div>
<div className="wr-four-r-item wr-four-r-item--feedback">
<dt><MessageSquareQuote size={15} strokeWidth={2.25} aria-hidden /> </dt>
<dd>{props.fourR.feedback}</dd>
</div>
</dl>
);
}
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 (
<article className="wr-phase-card" role="listitem">
<h4 className="wr-phase-card__title">{phase.label}</h4>
<div className="wr-phase-card__stats tabular-nums">
<span><strong>{phase.done}</strong> / {phase.count}</span>
</div>
{phase.note ? <p className="wr-phase-card__note">{phase.note}</p> : null}
</article>
);
}
return (
<article className="wr-phase-card" role="listitem">
<h4 className="wr-phase-card__title">{phase.label}</h4>
<div className="wr-phase-tabs" role="tablist" aria-label={phase.label + '分类'}>
{tabs.map(function (tab) {
return (
<button
key={tab.key}
type="button"
role="tab"
aria-selected={activeTabKey === tab.key}
className={'wr-phase-tab' + (activeTabKey === tab.key ? ' wr-phase-tab--active' : '')}
onClick={function () { setActiveTabKey(tab.key); }}
>
{tab.label}
</button>
);
})}
</div>
{activeTab ? (
<div className="wr-phase-panel" role="tabpanel">
{phase.key === 'prototype' && activeTab.requirementSections ? (
activeTab.requirementSections.map(function (section) {
return (
<RequirementWorkReportCard
key={section.title}
title={section.title}
items={section.items}
/>
);
})
) : phase.key === 'prototype' ? (
<>
<WorkReportCard
items={activeTab.workReport || []}
title="PC"
emptyText={activeTab.emptyText ?? (!(activeTab.workReport && activeTab.workReport.length) ? '无' : undefined)}
/>
<WorkReportCard
items={activeTab.miniProgramWorkReport || []}
title="小程序"
emptyText={activeTab.miniProgramEmptyText ?? '无'}
/>
</>
) : (activeTab.workReport?.length || activeTab.emptyText) ? (
<WorkReportCard
items={activeTab.workReport || []}
title={phase.workReportTitle}
emptyText={activeTab.emptyText}
/>
) : null}
</div>
) : null}
</article>
);
}
function PhaseModuleCards(props: { phases: TrackPhase[] }) {
return (
<div className="wr-phase-grid" role="list" aria-label="产品阶段模块">
{props.phases.map(function (phase) {
return <PhaseCard key={phase.key} phase={phase} />;
})}
</div>
);
}
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 (
<button
type="button"
role="tab"
aria-selected={props.active}
aria-controls={'wr-track-panel-' + track.key}
id={'wr-track-tab-' + track.key}
className={'wr-track-tab' + (props.active ? ' wr-track-tab--active' : '')}
onClick={props.onSelect}
>
<span className="wr-track-tab__icon" aria-hidden>{TRACK_ICON[track.key]}</span>
<span className="wr-track-tab__body">
<span className="wr-track-tab__title">{track.label}</span>
<span className="wr-track-tab__stats tabular-nums">
{track.planned} · {track.done} · {track.blocked}
</span>
{subLabel ? <span className="wr-track-tab__hint">{subLabel}</span> : null}
</span>
</button>
);
}
function TrackDetailPanel(props: { track: (typeof TRACK_PROGRESS)[number] }) {
var track = props.track;
if (track.phases) {
return <PhaseModuleCards phases={track.phases} />;
}
return (
<div className="wr-track-detail">
<div className="wr-track-detail__head">
<h3 className="wr-track-detail__title">{track.label} · </h3>
<ProgressBar value={track.progress} label={track.label + '完成度'} />
</div>
{track.lines ? (
<div className="wr-track-lines">
{track.lines.map(function (line) {
return (
<div key={line.key} className="wr-track-line">
<h4 className="wr-track-line__title">{line.label}</h4>
<ProgressBar value={line.progress} label={line.label + '完成度'} />
<ul className="wr-track-stats">
<li><span></span><strong className="tabular-nums">{line.planned}</strong></li>
<li><span></span><strong className="tabular-nums">{line.done}</strong></li>
<li><span></span><strong className={'tabular-nums' + (line.blocked > 0 ? ' wr-text-warning' : '')}>{line.blocked}</strong></li>
</ul>
<p className="wr-track-card__highlight">{line.highlight}</p>
</div>
);
})}
</div>
) : (
<>
<ul className="wr-track-stats">
<li><span></span><strong className="tabular-nums">{track.planned}</strong></li>
<li><span></span><strong className="tabular-nums">{track.done}</strong></li>
<li><span></span><strong className={'tabular-nums' + (track.blocked > 0 ? ' wr-text-warning' : '')}>{track.blocked}</strong></li>
</ul>
{track.highlight ? <p className="wr-track-card__highlight">{track.highlight}</p> : null}
</>
)}
</div>
);
}
function KeyIssueCard(props: { issue: WeeklyIssue }) {
var issue = props.issue;
return (
<article className={'wr-issue-card wr-issue-card--' + issue.priority.toLowerCase()}>
<header className="wr-issue-card__head">
<div className="wr-issue-card__tags">
<span className={'wr-badge wr-badge--priority-' + issue.priority.toLowerCase()}>{issue.priority}</span>
<span className={'wr-badge ' + STATUS_TONE[issue.status]}>{STATUS_LABEL[issue.status]}</span>
<span className="wr-badge wr-badge--track">{issue.track === 'product' ? '产品' : issue.track === 'dev' ? '开发' : '测试'}</span>
</div>
<h3 className="wr-issue-card__title">{issue.title}</h3>
<p className="wr-issue-card__meta">
<span>{issue.module}</span>
<span aria-hidden>·</span>
<span>{issue.owner}</span>
<span aria-hidden>·</span>
<span> {issue.updatedAt}</span>
</p>
</header>
<FourRGrid fourR={issue.fourR} />
</article>
);
}
export default function WeeklyReportPage() {
var minorOpenState = useState(false);
var minorOpen = minorOpenState[0];
var setMinorOpen = minorOpenState[1];
var activeTrackState = useState<TrackKey>('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 (
<div className="wr-page">
<header className="wr-hero">
<div className="wr-hero__main">
<p className="wr-hero__eyebrow">{REPORT_META.department} · OneOS</p>
<h1 className="wr-hero__title"></h1>
<p className="wr-hero__sub">
{REPORT_META.weekLabel}
<span aria-hidden> · </span>
{REPORT_META.weekRange}
</p>
</div>
<div className="wr-hero__aside">
<div className="wr-hero__meta">
<span>{REPORT_META.reporter}</span>
<span>{REPORT_META.reportDate}</span>
</div>
<button type="button" className="wr-btn wr-btn--primary" onClick={function () { window.print(); }}>
<Download size={16} strokeWidth={2.25} aria-hidden />
PDF
</button>
</div>
</header>
<section className="wr-kpi-row wr-kpi-row--exec" aria-label="关键指标">
{EXECUTIVE_KPIS.map(function (item) {
return <ExecutiveKpiCard key={item.key} kpi={item} />;
})}
</section>
<ExecutiveSummarySection />
<section className="wr-section" aria-labelledby="wr-track-title">
<div className="wr-section__head">
<h2 id="wr-track-title" className="wr-section__title">线</h2>
</div>
<div className="wr-track-switcher" role="tablist" aria-label="产品、开发、测试进度切换">
{TRACK_PROGRESS.map(function (track) {
return (
<TrackSummaryCard
key={track.key}
track={track}
active={activeTrack === track.key}
onSelect={function () { setActiveTrack(track.key); }}
/>
);
})}
</div>
<div
role="tabpanel"
id={'wr-track-panel-' + activeTrack}
aria-labelledby={'wr-track-tab-' + activeTrack}
className={'wr-track-detail-wrap' + (activeTrackData.phases ? ' wr-track-detail-wrap--flat' : '')}
>
<TrackDetailPanel track={activeTrackData} />
</div>
</section>
<section className="wr-section" aria-labelledby="wr-exec-decision-title">
<div className="wr-section__head">
<h2 id="wr-exec-decision-title" className="wr-section__title">
<Scale size={20} strokeWidth={2.25} aria-hidden className="wr-section__title-icon" />
/
</h2>
<p className="wr-section__desc"></p>
</div>
<div className="wr-exec-decision-list">
{EXECUTIVE_DECISIONS.map(function (decision) {
return <ExecutiveDecisionCard key={decision.id} decision={decision} />;
})}
</div>
</section>
<section className="wr-section" aria-labelledby="wr-exec-next-title">
<div className="wr-section__head">
<h2 id="wr-exec-next-title" className="wr-section__title">
<CalendarClock size={20} strokeWidth={2.25} aria-hidden className="wr-section__title-icon" />
</h2>
<p className="wr-section__desc"></p>
</div>
<NextWeekPanel />
</section>
<section className="wr-section" aria-labelledby="wr-key-issues-title">
<div className="wr-section__head">
<h2 id="wr-key-issues-title" className="wr-section__title">P0 / P1</h2>
</div>
<div className="wr-issue-list">
{KEY_ISSUES.map(function (issue) {
return <KeyIssueCard key={issue.id} issue={issue} />;
})}
</div>
</section>
<section className="wr-section" aria-labelledby="wr-milestone-title">
<div className="wr-section__head">
<h2 id="wr-milestone-title" className="wr-section__title"></h2>
<p className="wr-section__desc">OneOS </p>
</div>
<div className="wr-table-wrap">
<table className="wr-table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{MILESTONES.map(function (row) {
var st = MILESTONE_STATUS[row.status];
return (
<tr key={row.id}>
<td>{row.module}</td>
<td>{row.milestone}</td>
<td>{row.owner}</td>
<td className="tabular-nums">{row.planDate}</td>
<td className="tabular-nums">{row.actualDate || '—'}</td>
<td>
<div className="wr-table-progress">
<div className="wr-progress__track wr-progress__track--sm" aria-hidden>
<div className="wr-progress__fill" style={{ width: row.progress + '%' }} />
</div>
<span className="tabular-nums">{row.progress}%</span>
</div>
</td>
<td><span className={'wr-badge ' + st.tone}>{st.label}</span></td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
<section className="wr-section wr-section--minor" aria-labelledby="wr-minor-title">
<button
type="button"
className="wr-minor-toggle"
aria-expanded={minorOpen}
aria-controls="wr-minor-panel"
onClick={function () { setMinorOpen(!minorOpen); }}
>
<div className="wr-section__head wr-section__head--inline">
<h2 id="wr-minor-title" className="wr-section__title">P2</h2>
<p className="wr-section__desc"> {MINOR_ISSUES.length} · </p>
</div>
{minorOpen ? <ChevronUp size={22} strokeWidth={2.25} aria-hidden /> : <ChevronDown size={22} strokeWidth={2.25} aria-hidden />}
</button>
{minorOpen ? (
<div id="wr-minor-panel" className="wr-table-wrap">
<table className="wr-table wr-table--compact">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col">线</th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{MINOR_ISSUES.map(function (issue) {
return (
<tr key={issue.id}>
<td className="tabular-nums">{issue.id}</td>
<td>{issue.module}</td>
<td>{issue.title}</td>
<td>{issue.track === 'product' ? '产品' : issue.track === 'dev' ? '开发' : '测试'}</td>
<td>{issue.owner}</td>
<td><span className={'wr-badge ' + STATUS_TONE[issue.status]}>{STATUS_LABEL[issue.status]}</span></td>
<td className="tabular-nums">{issue.updatedAt}</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<div id="wr-minor-panel" className="wr-minor-summary" aria-live="polite">
{MINOR_ISSUES.map(function (issue) {
return (
<span key={issue.id} className="wr-minor-chip">
{issue.module}{issue.title.slice(0, 12)}{issue.title.length > 12 ? '…' : ''}
</span>
);
})}
</div>
)}
</section>
<footer className="wr-footer">
<p>OneOS · · </p>
</footer>
</div>
);
}

View File

@@ -1,7 +0,0 @@
/**
* @name 数智部周报
*/
import './styles/weekly-report.css';
import WeeklyReportPage from './WeeklyReportPage';
export default WeeklyReportPage;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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: '分页一致性专项与租赁合同新增页问题并行,需评估是否临时增援',
},
];

View File

@@ -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",

View File

@@ -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",

File diff suppressed because one or more lines are too long

View File

@@ -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 (
<div className="clay-backup-banner" role="note" aria-label="Clay 主题备份说明">
<div className="clay-backup-banner-main">
<span className="clay-backup-eyebrow">Clay Design System</span>
<h1 className="clay-backup-title"> · Clay </h1>
<p className="clay-backup-desc">
12px 24px KPI
</p>
</div>
<a className="clay-backup-link" href="/prototypes/vehicle-management">
</a>
</div>
);
}
export default function VehicleManagementClayApp() {
const [records, setRecords] = useState<VehicleRecord[]>(seedVehicles as VehicleRecord[]);
const [pendingFilters, setPendingFilters] = useState<VehicleFilters>(EMPTY_FILTERS);
const [appliedFilters, setAppliedFilters] = useState<VehicleFilters>(EMPTY_FILTERS);
const [kpiTab, setKpiTab] = useState<KpiKey>('all');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [detailRecord, setDetailRecord] = useState<VehicleRecord | null>(null);
const [importOpen, setImportOpen] = useState(false);
const [opsRecord, setOpsRecord] = useState<VehicleRecord | null>(null);
const [operateCityRecord, setOperateCityRecord] = useState<VehicleRecord | null>(null);
const [locationMapRecord, setLocationMapRecord] = useState<VehicleRecord | null>(null);
const [editRecord, setEditRecord] = useState<VehicleRecord | null>(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<VehicleRecord>) => {
setRecords((prev) => prev.map((r) => (r.id === recordId ? { ...r, ...patch } : r)));
showToast('车辆信息已更新');
};
const handleDownloadTestCases = () => {
downloadVehicleTestCasesExcel(showToast);
};
const docToolbar = (
<PageDocToolbar onDownloadTestCases={handleDownloadTestCases} />
);
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<AnnotationViewerOptions>(
() => ({
showToolbar: true,
showThemeToggle: true,
showColorFilter: true,
emptyWhenNoData: false,
toolbarEdge: 'right',
currentPageId: activeDetail ? 'detail' : 'list',
onDirectoryRoute: handleDirectoryRoute,
}),
[activeDetail, handleDirectoryRoute],
);
return (
<>
<div className="vm-page clay-vm-page">
{docToolbar}
{!activeDetail ? <ClayBackupBanner /> : null}
{activeDetail ? (
<DetailPage
record={activeDetail}
onBack={() => 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}
/>
) : (
<>
<FilterPanel
records={records}
filters={pendingFilters}
onChange={(patch) => setPendingFilters((prev) => ({ ...prev, ...patch }))}
onReset={handleReset}
onSearch={handleSearch}
/>
<KpiCards counts={kpiCounts} active={kpiTab} onChange={(key) => { setKpiTab(key); setPage(1); }} />
<section className="vm-table-section">
<div className="vm-table-toolbar" data-annotation-id="vm-table-toolbar">
<div className="vm-table-actions">
<button type="button" className="vm-btn vm-btn-ghost" onClick={handleExport}>
<Download size={16} aria-hidden />
</button>
<button type="button" className="vm-btn vm-btn-secondary" onClick={() => setImportOpen(true)}>
<FileUp size={16} aria-hidden />
</button>
</div>
</div>
<div className="vm-table-card">
<VehicleTable
records={paged}
insuranceExpireMap={insuranceExpireMap}
canEditOpsManager={CAN_EDIT_OPS_MANAGER}
isAdmin={IS_ADMIN}
onView={setDetailRecord}
onEdit={setEditRecord}
onEditOpsManager={setOpsRecord}
onEditOperateCity={setOperateCityRecord}
onOpenContract={handleOpenContract}
onOpenLicenseEdit={handleOpenLicenseEdit}
onOpenInsurancePurchase={handleOpenInsurancePurchase}
onOpenLocationMap={setLocationMapRecord}
onOpenDeliveryManagement={handleOpenDeliveryManagement}
onOpenReturnManagement={handleOpenReturnManagement}
/>
<div className="vm-table-footer">
<TablePagination
page={Math.min(page, totalPages)}
pageSize={pageSize}
total={filtered.length}
onPageChange={setPage}
onPageSizeChange={handlePageSizeChange}
/>
</div>
</div>
</section>
</>
)}
<ImportModal open={importOpen} onClose={() => setImportOpen(false)} onImport={handleImport} />
<OpsManagerModal
open={!!opsRecord}
record={opsRecord}
onClose={() => setOpsRecord(null)}
onSave={handleSaveOps}
/>
<OperateCityEditModal
open={!!operateCityRecord}
record={operateCityRecord}
records={records}
onClose={() => setOperateCityRecord(null)}
onSave={handleSaveOperateCity}
/>
<LocationMapModal
open={!!locationMapRecord}
record={locationMapRecord}
onClose={() => setLocationMapRecord(null)}
/>
<VehicleEditModal
open={!!editRecord}
record={editRecord}
records={records}
onClose={() => setEditRecord(null)}
onSave={handleSaveEdit}
/>
{toast && <div className="vm-toast" role="status" aria-live="polite">{toast}</div>}
</div>
<AnnotationViewer
source={annotationSourceDocument as AnnotationSourceDocument}
options={annotationOptions}
/>
</>
);
}

View File

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

View File

@@ -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",