迭代 ONE-OS 多原型:租赁合同增强、Clay 车辆备份、侧边栏分组,移除周报与安全培训原型。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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(() => {
|
||||
clearHostPrototypeRouteInfo();
|
||||
}, []);
|
||||
if (!embedded) {
|
||||
clearHostPrototypeRouteInfo();
|
||||
}
|
||||
}, [embedded]);
|
||||
|
||||
const handleDirectoryRoute = useCallback((node: AnnotationDirectoryRouteNode) => {
|
||||
const payload = node.payload as { pageId?: string } | undefined;
|
||||
@@ -114,3 +116,7 @@ export default function ContractTemplateManagementPage() {
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ContractTemplateManagementPage() {
|
||||
return <ContractTemplateManagementContent />;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
? (insuranceAlertOpen && insuranceAlertMode === 'coreExpired' ? ' lc-alert-card-active' : '')
|
||||
: (kpiFilter === card.key ? ' lc-alert-card-active' : '')
|
||||
card.key === 'expired'
|
||||
? (insuranceAlertOpen && insuranceAlertMode === 'coreExpired' ? ' lc-alert-card-active' : '')
|
||||
: (kpiFilter === card.key ? ' lc-alert-card-active' : '')
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (card.key === 'warning') openInsuranceAlertModal('expiring');
|
||||
else if (card.key === 'expired') openInsuranceAlertModal('coreExpired');
|
||||
if (card.key === 'expired') openInsuranceAlertModal('coreExpired');
|
||||
else setKpiFilter(card.key);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (card.key === 'warning') openInsuranceAlertModal('expiring');
|
||||
else if (card.key === 'expired') openInsuranceAlertModal('coreExpired');
|
||||
if (card.key === 'expired') openInsuranceAlertModal('coreExpired');
|
||||
else setKpiFilter(card.key);
|
||||
}
|
||||
}}
|
||||
@@ -6828,43 +6726,37 @@ const Component = function () {
|
||||
))}
|
||||
</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>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
408
src/prototypes/insurance-procurement/styles/index.css
Normal file
408
src/prototypes/insurance-procurement/styles/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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' },
|
||||
'添加授权人须同时上传授权委托书',
|
||||
'添加受托人须同时上传授权委托书',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 + ')'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// 设计规范:oneos-app.css · 样式前缀 lc-*
|
||||
|
||||
import LeaseContractCreate from './LeaseContractCreate.jsx';
|
||||
import LeaseContractFleetSummary from './LeaseContractFleetSummary.jsx';
|
||||
import { StatusTag } from '../vehicle-management/components/StatusTag';
|
||||
import {
|
||||
LEASE_CONTRACT_LIST_RECORDS,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
formatHydrogenPaymentLabel,
|
||||
formatContractApprovalTypeLabel,
|
||||
getAuthorizedDelegates,
|
||||
needsContractDelegateSupplement,
|
||||
formatContractHandoverMileage,
|
||||
formatContractHandoverDateTimeMinute,
|
||||
isContractVehicleDelivered,
|
||||
@@ -23,11 +25,16 @@ import {
|
||||
getContractVehicleMileageSourceTagClass,
|
||||
formatContractVehicleRemainingMileage,
|
||||
formatContractPeriodStartDate,
|
||||
computeCurrentMileagePeriodEnd,
|
||||
computeVehiclePeriodTargetMileage,
|
||||
computeVehicleMileageForecastStatus,
|
||||
computeContractOverallMileageProgress,
|
||||
computeContractMileageForecastStatus,
|
||||
getContractMileageForecastStatusTone,
|
||||
resolveContractTemplateTypeLabel,
|
||||
getReturnSettlementStatusTone,
|
||||
resolveReturnSettlementDisplayLabel,
|
||||
resolveReturnSettlementCellMode,
|
||||
getReturnSettlementDisplayTone,
|
||||
getCurrentApproverLabel,
|
||||
shouldHideCurrentApprover,
|
||||
DELIVERY_DATE_TBD_LABEL,
|
||||
@@ -37,6 +44,8 @@ import {
|
||||
} from './lease-contract-list-data.js';
|
||||
import { getLessorCompanies } from '../contract-template-management/contract-template-vars.js';
|
||||
|
||||
var LC_LIST_TABLE_EXPAND_WIDTH = 48;
|
||||
|
||||
const Component = function(props) {
|
||||
var useEffect = React.useEffect;
|
||||
var ONEOS_ANT_TABLE_GLOBAL_FIX = [
|
||||
@@ -75,6 +84,8 @@ const Component = function(props) {
|
||||
var _page = useState(1);
|
||||
var _pageSize = useState(10);
|
||||
var _view = useState('list');
|
||||
var _createInitialSection = useState(null);
|
||||
var _editContractRecord = useState(null);
|
||||
var _contractCode = useState(undefined);
|
||||
var _projectName = useState(undefined);
|
||||
var _customerName = useState(undefined);
|
||||
@@ -335,6 +346,16 @@ const Component = function(props) {
|
||||
return counts;
|
||||
}, [rawList]);
|
||||
|
||||
var fleetSourceList = useMemo(function () {
|
||||
var overrides = _recordOverrides[0] || {};
|
||||
return rawList.map(function (r) {
|
||||
var merged = overrides[r.id] ? Object.assign({}, r, overrides[r.id]) : r;
|
||||
return Object.assign({}, merged, {
|
||||
contractStatus: resolveContractStatusByVehicleLease(merged),
|
||||
});
|
||||
});
|
||||
}, [rawList, _recordOverrides[0]]);
|
||||
|
||||
var filteredList = useMemo(function() {
|
||||
var overrides = _recordOverrides[0] || {};
|
||||
var list = rawList.map(function (r) {
|
||||
@@ -705,7 +726,7 @@ const Component = function(props) {
|
||||
React.createElement('span', { className: 'lc-vehicle-popover-card__type', title: vehicle.vehicleType || '' }, vehicle.vehicleType || '-'),
|
||||
plate
|
||||
? React.createElement('span', { className: 'lc-vehicle-popover-card__plate tabular-nums' }, plate)
|
||||
: React.createElement('span', { className: 'lc-vehicle-popover-card__plate lc-vehicle-popover-card__plate--pending' }, '待上牌'),
|
||||
: React.createElement('span', { className: 'lc-vehicle-popover-card__plate lc-vehicle-popover-card__plate--pending' }, '车辆待选'),
|
||||
),
|
||||
React.createElement('div', { className: 'lc-vehicle-popover-card__body' },
|
||||
React.createElement('div', { className: 'lc-vehicle-popover-card__field' },
|
||||
@@ -1030,7 +1051,7 @@ const Component = function(props) {
|
||||
React.createElement(StatusTag, {
|
||||
label: forecastStatus,
|
||||
tone: forecastTone,
|
||||
title: '合同里程任务完成预判,供运维人员评估是否需换车干预',
|
||||
title: '基于近7天日均里程与本期剩余天数推算的合同整体完成预判',
|
||||
}),
|
||||
)
|
||||
: null,
|
||||
@@ -1072,22 +1093,43 @@ const Component = function(props) {
|
||||
|
||||
function renderReturnSettlementCell(vehicle) {
|
||||
if (!isContractVehicleReturned(vehicle)) {
|
||||
return React.createElement('div', { className: 'lc-contract-return-settlement lc-contract-return-settlement--empty' }, '—');
|
||||
return React.createElement('div', { className: 'lc-contract-return-settlement lc-contract-return-settlement--empty' });
|
||||
}
|
||||
var status = vehicle.returnSettlementStatus || '待提交';
|
||||
var displayLabel = resolveReturnSettlementDisplayLabel(status);
|
||||
var tone = getReturnSettlementDisplayTone(displayLabel);
|
||||
var mode = resolveReturnSettlementCellMode(status);
|
||||
var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
|
||||
var tone = getReturnSettlementStatusTone(status);
|
||||
return React.createElement('div', { className: 'lc-contract-return-settlement' },
|
||||
React.createElement('button', {
|
||||
var tag = React.createElement(StatusTag, {
|
||||
label: displayLabel,
|
||||
tone: tone,
|
||||
title: displayLabel,
|
||||
});
|
||||
var tagNode = mode
|
||||
? React.createElement('button', {
|
||||
type: 'button',
|
||||
className: 'lc-contract-status-tag-btn',
|
||||
title: '点击查看还车应结款详情:' + plate,
|
||||
title: mode === 'edit'
|
||||
? ('点击处理还车应结款:' + plate)
|
||||
: ('点击查看还车应结款:' + plate),
|
||||
onClick: function () {
|
||||
message.info('即将跳转至还车应结款详情(原型演示):' + plate);
|
||||
if (mode === 'edit') {
|
||||
message.info('即将跳转至还车应结款编辑页(原型演示):' + plate);
|
||||
} else {
|
||||
message.info('即将跳转至还车应结款查看页(原型演示):' + plate);
|
||||
}
|
||||
},
|
||||
},
|
||||
React.createElement(StatusTag, { label: status, tone: tone }),
|
||||
),
|
||||
}, tag)
|
||||
: tag;
|
||||
var approverNode = displayLabel === '审批中'
|
||||
? React.createElement('div', {
|
||||
className: 'lc-contract-return-settlement__approver',
|
||||
title: '当前审批节点操作人',
|
||||
}, vehicle.returnSettlementApprover || '—')
|
||||
: null;
|
||||
return React.createElement('div', { className: 'lc-contract-return-settlement lc-contract-return-settlement--stacked' },
|
||||
tagNode,
|
||||
approverNode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1111,34 +1153,17 @@ const Component = function(props) {
|
||||
),
|
||||
);
|
||||
}
|
||||
var periodNode;
|
||||
if (!hasRequirement || !isContractVehicleDelivered(vehicle)) {
|
||||
periodNode = React.createElement('div', {
|
||||
className: 'lc-contract-mileage-req-row__period lc-contract-mileage-req-row__period--empty',
|
||||
}, '—');
|
||||
} else {
|
||||
var periodMileText = formatContractHandoverMileage(vehicle.periodStartMileage);
|
||||
var periodDateText = formatContractPeriodStartDate(vehicle.periodStartDate);
|
||||
periodNode = React.createElement('div', { className: 'lc-contract-mileage-req-row__period' },
|
||||
React.createElement('span', { className: 'lc-contract-mileage-req-row__section-label' }, '当期里程'),
|
||||
React.createElement('div', {
|
||||
className: 'lc-contract-mileage-req-row__period-mile tabular-nums',
|
||||
title: periodMileText,
|
||||
}, periodMileText || '—'),
|
||||
React.createElement('div', {
|
||||
className: 'lc-contract-mileage-req-row__period-date tabular-nums',
|
||||
title: periodDateText,
|
||||
}, periodDateText),
|
||||
);
|
||||
}
|
||||
var mileText = formatContractVehicleCurrentMileage(vehicle.currentMileage, vehicle, '—');
|
||||
var hasMileageReading = mileText !== '—';
|
||||
var mileageSource = resolveContractVehicleMileageSource(vehicle);
|
||||
var sourceTagClass = getContractVehicleMileageSourceTagClass(mileageSource);
|
||||
return React.createElement('div', { className: 'lc-contract-mileage-req-row lc-contract-mileage-req-row--triple' },
|
||||
requirementNode,
|
||||
periodNode,
|
||||
React.createElement('div', { className: 'lc-contract-mileage-req-row__current' },
|
||||
var currentNode;
|
||||
if (!hasRequirement || !isContractVehicleDelivered(vehicle)) {
|
||||
currentNode = React.createElement('div', {
|
||||
className: 'lc-contract-mileage-req-row__current lc-contract-mileage-req-row__current--empty',
|
||||
}, '—');
|
||||
} else {
|
||||
currentNode = React.createElement('div', { className: 'lc-contract-mileage-req-row__current' },
|
||||
React.createElement('span', { className: 'lc-contract-mileage-req-row__section-label' }, '当前里程'),
|
||||
React.createElement('div', { className: 'vm-cell-tags lc-contract-mileage-req-row__current-line' },
|
||||
React.createElement('span', { className: 'vm-mileage tabular-nums', title: mileText }, mileText),
|
||||
@@ -1146,7 +1171,68 @@ const Component = function(props) {
|
||||
? React.createElement('span', { className: sourceTagClass }, mileageSource)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return React.createElement('div', { className: 'lc-contract-mileage-req-row lc-contract-mileage-req-row--dual' },
|
||||
requirementNode,
|
||||
currentNode,
|
||||
);
|
||||
}
|
||||
|
||||
function getMileageCompletionRangeData(vehicle, contractRecord) {
|
||||
if (!contractRecord || !contractRecord.hasMinimumMileage || !isContractVehicleDelivered(vehicle)) {
|
||||
return null;
|
||||
}
|
||||
var startDate = vehicle.periodStartDate;
|
||||
var periodEndDate = startDate && contractRecord.mileagePeriod
|
||||
? computeCurrentMileagePeriodEnd(startDate, contractRecord.mileagePeriod)
|
||||
: null;
|
||||
var startDateText = formatContractPeriodStartDate(startDate);
|
||||
var endDateText = formatContractPeriodStartDate(periodEndDate);
|
||||
var startMileText = formatContractHandoverMileage(vehicle.periodStartMileage);
|
||||
var targetMileage = computeVehiclePeriodTargetMileage(contractRecord, vehicle);
|
||||
var endMileText = targetMileage != null ? formatContractHandoverMileage(targetMileage) : '—';
|
||||
if (startDateText === '—' && startMileText === '—' && endDateText === '—' && endMileText === '—') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
startDateText: startDateText,
|
||||
startMileText: startMileText,
|
||||
endDateText: endDateText,
|
||||
endMileText: endMileText,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMileageCompletionRangeEnd(rangeData) {
|
||||
if (!rangeData) return null;
|
||||
return React.createElement('div', {
|
||||
className: 'lc-contract-mileage-completion__range-side lc-contract-mileage-completion__range-side--end',
|
||||
},
|
||||
React.createElement('span', {
|
||||
className: 'lc-contract-mileage-completion__range-date tabular-nums',
|
||||
title: '到达时间 ' + rangeData.endDateText,
|
||||
}, rangeData.endDateText),
|
||||
React.createElement('span', {
|
||||
className: 'lc-contract-mileage-completion__range-mile tabular-nums',
|
||||
title: '到达里程 ' + rangeData.endMileText,
|
||||
}, rangeData.endMileText),
|
||||
);
|
||||
}
|
||||
|
||||
function renderMileageCompletionRangeStart(rangeData) {
|
||||
if (!rangeData) return null;
|
||||
return React.createElement('div', {
|
||||
className: 'lc-contract-mileage-completion__range-side lc-contract-mileage-completion__range-side--start',
|
||||
'aria-label': '行程起点',
|
||||
},
|
||||
React.createElement('span', {
|
||||
className: 'lc-contract-mileage-completion__range-date tabular-nums',
|
||||
title: '开始时间 ' + rangeData.startDateText,
|
||||
}, rangeData.startDateText),
|
||||
React.createElement('span', {
|
||||
className: 'lc-contract-mileage-completion__range-mile tabular-nums',
|
||||
title: '开始里程 ' + rangeData.startMileText,
|
||||
}, rangeData.startMileText || '—'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1158,14 +1244,32 @@ const Component = function(props) {
|
||||
var remainingNode = null;
|
||||
if (delivered) {
|
||||
var remainingText = formatContractVehicleRemainingMileage(vehicle.remainingMileage);
|
||||
remainingNode = React.createElement('div', {
|
||||
className: 'lc-contract-mileage-completion__remaining tabular-nums',
|
||||
title: remainingText,
|
||||
}, remainingText);
|
||||
var forecastStatus = vehicle.mileageForecastStatus || computeVehicleMileageForecastStatus(contractRecord, vehicle);
|
||||
var forecastTone = getContractMileageForecastStatusTone(forecastStatus);
|
||||
remainingNode = React.createElement('div', { className: 'lc-contract-mileage-completion__remaining-row' },
|
||||
React.createElement('div', {
|
||||
className: 'lc-contract-mileage-completion__remaining tabular-nums',
|
||||
title: remainingText,
|
||||
}, remainingText),
|
||||
forecastStatus
|
||||
? React.createElement(StatusTag, {
|
||||
label: forecastStatus,
|
||||
tone: forecastTone,
|
||||
title: '基于近7天日均里程与本期剩余天数推算的单车完成预判',
|
||||
})
|
||||
: null,
|
||||
);
|
||||
}
|
||||
var rangeData = getMileageCompletionRangeData(vehicle, contractRecord);
|
||||
return React.createElement('div', { className: 'lc-contract-mileage-completion' },
|
||||
remainingNode,
|
||||
renderMileageProgressCell(vehicle),
|
||||
React.createElement('div', { className: 'lc-contract-mileage-completion__progress-row' },
|
||||
React.createElement('div', { className: 'lc-contract-mileage-completion__progress-main' },
|
||||
renderMileageProgressCell(vehicle),
|
||||
),
|
||||
renderMileageCompletionRangeEnd(rangeData),
|
||||
),
|
||||
renderMileageCompletionRangeStart(rangeData),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1198,9 +1302,10 @@ const Component = function(props) {
|
||||
return React.createElement('div', { className: 'lc-contract-vehicle-info' },
|
||||
React.createElement('div', {
|
||||
className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary tabular-nums',
|
||||
title: plateLabel,
|
||||
}, plateLabel),
|
||||
React.createElement('div', {
|
||||
className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary tabular-nums',
|
||||
className: 'lc-contract-vehicle-info__line lc-contract-vehicle-info__line--primary lc-contract-vehicle-info__line--vin tabular-nums',
|
||||
title: vinLabel,
|
||||
}, vinLabel),
|
||||
React.createElement('div', {
|
||||
@@ -1210,7 +1315,18 @@ const Component = function(props) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderHandoverSituationCell(vehicle, kind) {
|
||||
function formatContractHandoverDateTimeCompact(raw) {
|
||||
var full = formatContractHandoverDateTimeMinute(raw);
|
||||
if (!full) return '';
|
||||
var match = full.match(/^(\d{4})-(\d{2}-\d{2})[ T](\d{2}:\d{2})$/);
|
||||
if (match) return match[2] + ' ' + match[3];
|
||||
if (/^\d{4}-(\d{2}-\d{2})$/.test(full)) return full.replace(/^\d{4}-/, '');
|
||||
return full;
|
||||
}
|
||||
|
||||
function renderHandoverSituationCell(vehicle, kind, options) {
|
||||
options = options || {};
|
||||
var compact = Boolean(options.compact);
|
||||
var emptyLabel = kind === 'delivery' ? '未交车' : '未还车';
|
||||
var hasData = kind === 'delivery'
|
||||
? isContractVehicleDelivered(vehicle)
|
||||
@@ -1225,7 +1341,10 @@ const Component = function(props) {
|
||||
var time = kind === 'delivery' ? vehicle.deliveryTime : vehicle.returnTime;
|
||||
var mileText = formatContractHandoverMileage(mile);
|
||||
var personText = person && String(person).trim() && person !== '-' ? String(person).trim() : '-';
|
||||
var timeText = formatContractHandoverDateTimeMinute(time);
|
||||
var timeTextFull = formatContractHandoverDateTimeMinute(time);
|
||||
var timeText = compact
|
||||
? formatContractHandoverDateTimeCompact(time)
|
||||
: timeTextFull;
|
||||
var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
|
||||
var jumpTitle = kind === 'delivery'
|
||||
? '点击查看交车管理:' + plate
|
||||
@@ -1252,7 +1371,7 @@ const Component = function(props) {
|
||||
}, personText),
|
||||
React.createElement('div', {
|
||||
className: 'lc-contract-handover-situation__line lc-contract-handover-situation__line--time tabular-nums',
|
||||
title: timeText,
|
||||
title: timeTextFull || timeText,
|
||||
}, timeText || '-'),
|
||||
);
|
||||
}
|
||||
@@ -1268,11 +1387,11 @@ const Component = function(props) {
|
||||
React.createElement('tr', null,
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__info' }, '车辆信息'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__pickup' }, '提车应收款'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover' }, '交车情况'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover' }, '还车情况'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '交车情况' }, '交车'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '还车情况' }, '还车'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__return-settlement' }, '还车应结款'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req' }, '里程要求'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion' }, '里程完成情况'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req', title: '里程要求' }, '里程要求'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion', title: '里程完成情况' }, '里程完成情况'),
|
||||
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__action' }, '操作'),
|
||||
),
|
||||
),
|
||||
@@ -1281,8 +1400,8 @@ const Component = function(props) {
|
||||
return React.createElement('tr', { key: record.id + '-vehicle-' + index },
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__info' }, renderVehicleInfoCell(vehicle)),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__pickup' }, renderPickupReceivableCell(vehicle)),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery')),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return')),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery', { compact: true })),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return', { compact: true })),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__return-settlement' }, renderReturnSettlementCell(vehicle)),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-req' }, renderMileageRequirementCell(record, vehicle)),
|
||||
React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-completion' }, renderMileageCompletionCell(vehicle, record)),
|
||||
@@ -1629,19 +1748,31 @@ const Component = function(props) {
|
||||
_delegatePopoverRecord[1](null);
|
||||
}
|
||||
|
||||
function openContractEditForPoa(record) {
|
||||
_createInitialSection[1]('poa');
|
||||
_editContractRecord[1](record || null);
|
||||
_view[1]('create');
|
||||
}
|
||||
|
||||
function backToContractList() {
|
||||
_createInitialSection[1](null);
|
||||
_editContractRecord[1](null);
|
||||
_view[1]('list');
|
||||
}
|
||||
|
||||
function renderDelegatePopoverPanel(record) {
|
||||
var delegates = getAuthorizedDelegates(record);
|
||||
return React.createElement('div', {
|
||||
className: 'lc-delegate-popover__panel',
|
||||
role: 'region',
|
||||
'aria-label': '被授权人明细',
|
||||
'aria-label': '受托人明细',
|
||||
},
|
||||
React.createElement('div', { className: 'lc-delegate-popover__header' },
|
||||
React.createElement('div', { className: 'lc-delegate-popover__title' }, '被授权人'),
|
||||
React.createElement('div', { className: 'lc-delegate-popover__title' }, '受托人'),
|
||||
React.createElement('div', { className: 'lc-delegate-popover__sub' }, record.contractCode || ''),
|
||||
),
|
||||
delegates.length === 0
|
||||
? React.createElement('p', { className: 'lc-delegate-popover__empty' }, '暂无被授权人')
|
||||
? React.createElement('p', { className: 'lc-delegate-popover__empty' }, '暂无受托人')
|
||||
: React.createElement('div', { className: 'lc-delegate-popover__list' },
|
||||
delegates.map(function (person, index) {
|
||||
return React.createElement('div', {
|
||||
@@ -1667,6 +1798,15 @@ const Component = function(props) {
|
||||
}
|
||||
|
||||
function renderDelegateCountCell(_, record) {
|
||||
if (needsContractDelegateSupplement(record)) {
|
||||
return React.createElement('button', {
|
||||
type: 'button',
|
||||
className: 'lc-delegate-supplement-btn',
|
||||
title: '补充授权委托书与受托人信息',
|
||||
'aria-label': '请补充授权委托书与受托人信息',
|
||||
onClick: function () { openContractEditForPoa(record); },
|
||||
}, '请补充');
|
||||
}
|
||||
var count = record.delegateCount != null ? record.delegateCount : 0;
|
||||
if (count <= 0) return '-';
|
||||
var popoverOpen = _delegatePopoverRecord[0] && _delegatePopoverRecord[0].id === record.id;
|
||||
@@ -1693,7 +1833,7 @@ const Component = function(props) {
|
||||
React.createElement('button', {
|
||||
type: 'button',
|
||||
className: 'lc-delegate-count-btn tabular-nums',
|
||||
'aria-label': '查看被授权人 ' + count + ' 人',
|
||||
'aria-label': '查看受托人 ' + count + ' 人',
|
||||
}, count + ' 人'),
|
||||
);
|
||||
}
|
||||
@@ -1864,13 +2004,21 @@ const Component = function(props) {
|
||||
}
|
||||
|
||||
var columns = [
|
||||
Table.EXPAND_COLUMN,
|
||||
{
|
||||
title: '项目信息',
|
||||
key: 'projectInfo',
|
||||
width: 248,
|
||||
fixed: 'left',
|
||||
ellipsis: true,
|
||||
className: 'lc-project-info-col',
|
||||
onCell: function() { return { className: 'lc-project-info-col' }; },
|
||||
onHeaderCell: function () {
|
||||
return {
|
||||
className: 'lc-project-info-col',
|
||||
'data-annotation-id': 'lc-project-info-col',
|
||||
};
|
||||
},
|
||||
onCell: function () { return { className: 'lc-project-info-col' }; },
|
||||
render: renderProjectInfoCell
|
||||
},
|
||||
{
|
||||
@@ -1932,6 +2080,12 @@ const Component = function(props) {
|
||||
key: 'overallMileageProgress',
|
||||
width: 168,
|
||||
className: 'lc-overall-mileage-col',
|
||||
onHeaderCell: function () {
|
||||
return {
|
||||
className: 'lc-overall-mileage-col',
|
||||
'data-annotation-id': 'lc-overall-mileage-col',
|
||||
};
|
||||
},
|
||||
onCell: function () { return { className: 'lc-overall-mileage-col' }; },
|
||||
render: renderOverallMileageProgressCell,
|
||||
},
|
||||
@@ -1949,10 +2103,16 @@ const Component = function(props) {
|
||||
render: renderContactCell
|
||||
},
|
||||
{
|
||||
title: '被授权人',
|
||||
title: '受托人',
|
||||
key: 'delegateCount',
|
||||
width: 88,
|
||||
className: 'tabular-nums lc-delegate-col',
|
||||
onHeaderCell: function () {
|
||||
return {
|
||||
className: 'lc-delegate-col tabular-nums',
|
||||
'data-annotation-id': 'lc-delegate-col',
|
||||
};
|
||||
},
|
||||
onCell: function () { return { className: 'lc-delegate-col tabular-nums' }; },
|
||||
render: renderDelegateCountCell,
|
||||
},
|
||||
@@ -1971,6 +2131,13 @@ const Component = function(props) {
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right', render: function(_, record) { return getOperationButtons(record); } }
|
||||
];
|
||||
|
||||
var listTableScrollX = columns.reduce(function (sum, col) {
|
||||
if (col === Table.EXPAND_COLUMN) {
|
||||
return sum + LC_LIST_TABLE_EXPAND_WIDTH;
|
||||
}
|
||||
return sum + (Number(col.width) || 0);
|
||||
}, 0);
|
||||
|
||||
var filterItems = [
|
||||
React.createElement('label', { key: 'contractCode', className: 'vm-filter-field' },
|
||||
React.createElement('span', null, '合同编码'),
|
||||
@@ -2114,7 +2281,9 @@ const Component = function(props) {
|
||||
|
||||
if (_view[0] === 'create') {
|
||||
return React.createElement(LeaseContractCreate, {
|
||||
onBack: function() { _view[1]('list'); }
|
||||
onBack: backToContractList,
|
||||
initialScrollSection: _createInitialSection[0],
|
||||
editRecord: _editContractRecord[0],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2186,6 +2355,13 @@ const Component = function(props) {
|
||||
);
|
||||
})
|
||||
),
|
||||
React.createElement(LeaseContractFleetSummary, {
|
||||
records: fleetSourceList,
|
||||
Modal: Modal,
|
||||
onViewContract: function (record) {
|
||||
message.info('查看合同(原型):' + (record.contractCode || record.projectName || ''));
|
||||
},
|
||||
}),
|
||||
React.createElement('section', { className: 'vm-table-section' },
|
||||
React.createElement('div', { className: 'vm-table-toolbar', 'data-annotation-id': 'lc-list-toolbar' },
|
||||
React.createElement('div', { className: 'vm-table-actions' },
|
||||
@@ -2231,11 +2407,13 @@ const Component = function(props) {
|
||||
rowKey: 'id',
|
||||
columns: columns,
|
||||
dataSource: pagedList,
|
||||
scroll: { x: 2072 },
|
||||
tableLayout: 'fixed',
|
||||
scroll: { x: listTableScrollX, y: 'calc(100vh - 640px)' },
|
||||
size: 'small',
|
||||
pagination: false,
|
||||
expandable: {
|
||||
columnWidth: 48,
|
||||
fixed: 'left',
|
||||
columnWidth: LC_LIST_TABLE_EXPAND_WIDTH,
|
||||
expandedRowRender: renderExpandedVehicleTable,
|
||||
rowExpandable: function (record) {
|
||||
return (record.vehicles || []).length > 0;
|
||||
@@ -2308,9 +2486,9 @@ const Component = function(props) {
|
||||
rowKey: function(_, i) { return String(i); },
|
||||
size: 'small',
|
||||
columns: [
|
||||
{ title: '被授权人', key: 'name', width: 140, render: function(_, row, index) { return React.createElement(Input, { value: row.name, onChange: function(e) { updateAuthorizedRow(index, 'name', e.target.value); }, placeholder: '请输入' }); } },
|
||||
{ title: '被授权人联系电话', key: 'phone', width: 160, render: function(_, row, index) { return React.createElement(Input, { value: row.phone, onChange: function(e) { updateAuthorizedRow(index, 'phone', e.target.value); }, placeholder: '请输入' }); } },
|
||||
{ title: '被授权人身份证', key: 'idCard', width: 200, render: function(_, row, index) { return React.createElement(Input, { value: row.idCard, onChange: function(e) { updateAuthorizedRow(index, 'idCard', e.target.value); }, placeholder: '请输入' }); } },
|
||||
{ title: '受托人', key: 'name', width: 140, render: function(_, row, index) { return React.createElement(Input, { value: row.name, onChange: function(e) { updateAuthorizedRow(index, 'name', e.target.value); }, placeholder: '请输入' }); } },
|
||||
{ title: '受托人联系电话', key: 'phone', width: 160, render: function(_, row, index) { return React.createElement(Input, { value: row.phone, onChange: function(e) { updateAuthorizedRow(index, 'phone', e.target.value); }, placeholder: '请输入' }); } },
|
||||
{ title: '受托人身份证', key: 'idCard', width: 200, render: function(_, row, index) { return React.createElement(Input, { value: row.idCard, onChange: function(e) { updateAuthorizedRow(index, 'idCard', e.target.value); }, placeholder: '请输入' }); } },
|
||||
{ title: '操作', key: 'action', width: 80, render: function(_, row, index) { return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeAuthorizedRow(index); } }, '删除'); } }
|
||||
],
|
||||
dataSource: _authorizedList[0],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
}),
|
||||
|
||||
@@ -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) {
|
||||
slice = slice.replace(/(委托人[::]\s*)([\s\u00a0]*)/, '$1' + name + ' ');
|
||||
if (/受托人[::]/u.test(slice)) {
|
||||
slice = slice.replace(/(受托人[::]\s*)([\s\u00a0]*)/, '$1' + name + ' ');
|
||||
} else {
|
||||
slice = slice.replace(/(委托人[::]\s*)([\s\u00a0]*)/, '$1' + name + ' ');
|
||||
}
|
||||
slice = slice.replace(/(联系电话[::]\s*)([\s\u00a0]*)/, '$1' + contact + ' ');
|
||||
slice = slice.replace(/(身份证号码[::]\s*)([\s\u00a0]*)/, '$1' + idNumber + ' ');
|
||||
}
|
||||
|
||||
@@ -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: '保存/提交时展示将走的审批类型。' },
|
||||
];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* @name 数智部周报
|
||||
*/
|
||||
import './styles/weekly-report.css';
|
||||
import WeeklyReportPage from './WeeklyReportPage';
|
||||
|
||||
export default WeeklyReportPage;
|
||||
@@ -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
@@ -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: '分页一致性专项与租赁合同新增页问题并行,需评估是否临时增援',
|
||||
},
|
||||
];
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
1117
src/prototypes/vehicle-management-clay/annotation-source.json
Normal file
1117
src/prototypes/vehicle-management-clay/annotation-source.json
Normal file
File diff suppressed because one or more lines are too long
395
src/prototypes/vehicle-management-clay/index.tsx
Normal file
395
src/prototypes/vehicle-management-clay/index.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
391
src/prototypes/vehicle-management-clay/styles/clay-theme.css
Normal file
391
src/prototypes/vehicle-management-clay/styles/clay-theme.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user