// 【重要】必须使用 const Component 作为组件变量名 // 运维管理 - 车辆业务 - 证照管理(资质维护页,由台账列表「管理」进入) const { useState, useEffect, useMemo, useRef } = React; const moment = window.moment || window.dayjs; const antd = window.antd; const { Form, Input, Select, Button, DatePicker, Card, Row, Col, Space, Badge, Alert, InputNumber, Divider, Switch, Spin, Tooltip, Modal, Progress, Popover, Tabs, Tag, message } = antd; const LC_LICENSES_STORAGE_KEY = 'oneos_lc_licenses_v1'; const LC_EDIT_PLATE_KEY = 'oneos_lc_edit_plate'; const LC_NAV_TARGET_KEY = 'oneos_lc_navigate_target'; const LC_NAV_EVENT = 'oneos-lc-return-list'; const loadLicensesFromStorage = () => { try { const raw = localStorage.getItem(LC_LICENSES_STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); return parsed && typeof parsed === 'object' ? parsed : null; } catch { return null; } }; const persistLicensesToStorage = (data) => { try { localStorage.setItem(LC_LICENSES_STORAGE_KEY, JSON.stringify(data)); } catch { /* ignore */ } }; const navigateToLicenseList = (toastMsg) => { try { sessionStorage.setItem(LC_NAV_TARGET_KEY, 'list'); sessionStorage.removeItem(LC_EDIT_PLATE_KEY); } catch { /* ignore */ } try { window.dispatchEvent(new CustomEvent(LC_NAV_EVENT)); } catch { /* ignore */ } if (typeof window.__axhubNavigate === 'function') { window.__axhubNavigate('证照管理'); if (toastMsg) message.success(toastMsg); return; } if (toastMsg) message.success(toastMsg); else message.info('请切换至「证照管理」台账列表页查看'); }; const createEmptyLicenseRecord = () => ({ driverLicense: { photos: [], regDate: '', issueDate: '', scrapDate: '', expireDate: '', updateType: '直接上传', updateTime: '', updateUser: '' }, transportLicense: { photos: [], licenseNo: '', issueDate: '', expireDate: '', inspectValidUntil: '', updateTime: '', updateUser: '' }, registrationCert: { photos: [] }, specialEquipCert: { photos: [] }, specialEquipDecal: { photos: [], nextInspectDate: '', updateTime: '', updateUser: '' }, hydrogenCard: { cardNo: '', cardType: '中石化加氢卡', balance: 0, issueDate: '', issueUser: '' }, safetyValve: { photos: [], inspectDate: '', nextInspectDate: '' }, pressureGauge: { photos: [], inspectDate: '', nextInspectDate: '' } }); const mergeLicenseStatusTypes = (a, b) => { const rank = { error: 0, warning: 1, default: 2, success: 3, processing: 2 }; const ra = rank[a] ?? 9; const rb = rank[b] ?? 9; return ra <= rb ? a : b; }; const loadPlateLicenseBundle = (master, plate) => { if (master[plate]) return JSON.parse(JSON.stringify(master[plate])); return createEmptyLicenseRecord(); }; /** 特种设备使用标识:与台账列表一致,提前 60 天临期感知,≤15 天高危提示 */ const SPECIAL_EQUIP_DECAL_WARN_DAYS = 60; const SPECIAL_EQUIP_DECAL_CRITICAL_DAYS = 15; // 常用矢量图标,保证 100% 渲染且支持高保真样式 const ICONS = { ocr: , upload: , camera: , warning: , card: , success: , delete: , shield: , vehicle: , edit: , filter: }; // 预设的车辆列表(扩充至 5 辆以完美支持台账数据测试) const MOCK_VEHICLES = [ { plateNo: '沪A03561F', brand: '宇通', model: '49吨牵引车头', vin: 'LMRKH9AC0R1004086', status: '自营' }, { plateNo: '粤B58888F', brand: '福田', model: '奥铃4.5吨冷藏车', vin: 'LGHXCAE28M6789012', status: '租赁' }, { plateNo: '苏E33333', brand: '陕汽', model: '德龙X3000混动牵引车', vin: 'LSXCH9AE8M1094857', status: '自营' }, { plateNo: '京A12345', brand: '东风', model: 'DFH1180厢式货车', vin: 'LKLG7C4E4NA774759', status: '退出运营' }, { plateNo: '浙A88888', brand: '宇通', model: '氢燃料电池大巴', vin: 'LMRKH9AE2P9876543', status: '库存' } ]; // 模拟预设的所有车辆证照档案数据库 const INITIAL_LICENSE_DATA = { '沪A03561F': { driverLicense: { photos: ['https://picsum.photos/seed/license1/600/400', 'https://picsum.photos/seed/license2/600/400'], regDate: '2024-06-05', issueDate: '2024-06-05', scrapDate: '2039-06-04', expireDate: '2026-06-30', // 行驶证 29 天后到期(临期警告) updateType: '直接上传', updateTime: '2026-05-28 14:32:00', updateUser: '李明辉', shNextEvaluation: '2026-12-05' }, transportLicense: { photos: ['https://picsum.photos/seed/transport/600/400'], licenseNo: '交字310115102345号', issueDate: '2024-07-12', expireDate: '2026-07-31', // 证件有效期 60 天后到期(临期临界) inspectValidUntil: '2026-07-20', // 审验有效期 49 天后(临期) updateTime: '2026-05-10 11:20:00', updateUser: '陈高伟' }, registrationCert: { photos: ['https://picsum.photos/seed/regcert1/600/400'] }, specialEquipCert: { photos: ['https://picsum.photos/seed/spec1/600/400'] }, specialEquipDecal: { photos: ['https://picsum.photos/seed/spec2/600/400'], nextInspectDate: '2026-07-20', updateTime: '2026-05-20 16:45:00', updateUser: '系统管理员' }, hydrogenCard: { cardNo: 'H2-9988-7766-5544', cardType: '中石化加氢卡', balance: 12850.50, issueDate: '2025-01-15 14:30', issueUser: '能源管理部-张晓' }, safetyValve: { photos: ['https://picsum.photos/seed/valve/600/400'], inspectDate: '2025-10-10', nextInspectDate: '2026-10-09' }, pressureGauge: { photos: ['https://picsum.photos/seed/gauge/600/400'], inspectDate: '2025-12-15', nextInspectDate: '2026-06-14' } }, '粤B58888F': { driverLicense: { photos: [], regDate: '', issueDate: '', scrapDate: '', expireDate: '', updateType: '直接上传', updateTime: '', updateUser: '' }, transportLicense: { photos: ['https://picsum.photos/seed/trans_ocr/600/400'], licenseNo: '粤字440301102947号', issueDate: '2024-07-20', expireDate: '2026-07-20', inspectValidUntil: '2026-08-15', updateTime: '2026-05-15 09:30:00', updateUser: '黄志杰' }, registrationCert: { photos: [] }, specialEquipCert: { photos: [] }, specialEquipDecal: { photos: [], nextInspectDate: '' }, hydrogenCard: { cardNo: 'H2-5566-4433-2211', cardType: '中石化加氢卡', balance: 5200.00, issueDate: '2025-03-10 10:15', issueUser: '能源管理部-张晓' }, safetyValve: { photos: [], inspectDate: '', nextInspectDate: '' }, pressureGauge: { photos: [], inspectDate: '', nextInspectDate: '' } }, '京A12345': { driverLicense: { photos: [], regDate: '', issueDate: '', scrapDate: '', expireDate: '', updateType: '直接上传', updateTime: '', updateUser: '' }, transportLicense: { photos: [], licenseNo: '', issueDate: '', expireDate: '', inspectValidUntil: '', updateTime: '', updateUser: '' }, registrationCert: { photos: [] }, specialEquipCert: { photos: [] }, specialEquipDecal: { photos: [], nextInspectDate: '' }, hydrogenCard: { cardNo: '', cardType: '中石化加氢卡', balance: 0, issueDate: '', issueUser: '' }, safetyValve: { photos: [], inspectDate: '', nextInspectDate: '' }, pressureGauge: { photos: [], inspectDate: '', nextInspectDate: '' } }, '苏E33333': { driverLicense: { photos: ['https://picsum.photos/seed/su_lic/600/400'], regDate: '2024-05-16', issueDate: '2024-05-16', scrapDate: '2039-05-15', expireDate: '2026-05-15', // 已过期(逾期 17 天) updateType: '直接上传', updateTime: '2026-05-01 10:00:00', updateUser: '王东东' }, transportLicense: { photos: ['https://picsum.photos/seed/su_trans/600/400'], licenseNo: '苏字320501104829号', issueDate: '2024-08-10', expireDate: '2026-08-10', inspectValidUntil: '2026-08-10', updateTime: '2026-05-12 15:40:00', updateUser: '王东东' }, registrationCert: { photos: [] }, specialEquipCert: { photos: [] }, specialEquipDecal: { photos: [], nextInspectDate: '' }, hydrogenCard: { cardNo: '', cardType: '中石化加氢卡', balance: 0, issueDate: '', issueUser: '' }, safetyValve: { photos: [], inspectDate: '', nextInspectDate: '' }, pressureGauge: { photos: [], inspectDate: '', nextInspectDate: '' } }, '浙A88888': { driverLicense: { photos: ['https://picsum.photos/seed/zhe1/600/400'], regDate: '2025-01-01', issueDate: '2025-01-01', scrapDate: '2040-01-01', expireDate: '2027-12-31', // 正常 updateType: '直接上传', updateTime: '2026-01-10 11:00:00', updateUser: '张小凡' }, transportLicense: { photos: ['https://picsum.photos/seed/zhe2/600/400'], licenseNo: '浙字330101582910号', issueDate: '2025-01-05', expireDate: '2027-12-31', inspectValidUntil: '2027-12-31', updateTime: '2026-01-10 11:00:00', updateUser: '张小凡' }, registrationCert: { photos: ['https://picsum.photos/seed/zhe3/600/400'] }, specialEquipCert: { photos: [] }, specialEquipDecal: { photos: [], nextInspectDate: '' }, hydrogenCard: { cardNo: 'H2-8888-6666-5555', cardType: '中石化加氢卡', balance: 8800.00, issueDate: '2025-05-20 16:30', issueUser: '能源管理部-张晓' }, safetyValve: { photos: ['https://picsum.photos/seed/zhe_v/600/400'], inspectDate: '2025-12-20', nextInspectDate: '2026-12-19' }, pressureGauge: { photos: ['https://picsum.photos/seed/zhe_g/600/400'], inspectDate: '2025-12-20', nextInspectDate: '2026-12-19' } } }; 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-page-title { font-size: 22px; font-weight: 700; color: #0f172a; margin: 0; } .lc-layout-row { display: flex; gap: 24px; flex: 1; min-height: 0; } .lc-sidebar { width: 320px; flex-shrink: 0; height: 100%; display: flex; flex-direction: column; } .lc-content-area { flex: 1; min-width: 0; height: 100%; overflow-y: auto; padding-right: 12px; padding-bottom: 80px; } .lc-content-area::-webkit-scrollbar { width: 6px; } .lc-content-area::-webkit-scrollbar-track { background: transparent; } .lc-content-area::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } .lc-content-area::-webkit-scrollbar-thumb:hover { background: #94a3b8; } .lc-sticky-card { height: 100%; display: flex; flex-direction: column; gap: 16px; min-height: 0; } .lc-sidebar-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.05) !important; overflow: hidden !important; } .lc-sidebar-card-index { flex: 1; min-height: 0; display: flex; flex-direction: column; } .lc-sidebar-card-index .ant-card-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; padding: 12px 16px !important; } .lc-sidebar-info-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px dashed #f1f5f9; } .lc-sidebar-info-row:last-child { border-bottom: none; } .lc-sidebar-label { font-size: 13px; color: #64748b; } .lc-sidebar-val { font-size: 13px; font-weight: 600; color: #0f172a; } .lc-nav-list { display: flex; flex-direction: column; gap: 5px; flex: 1; overflow-y: auto; padding-right: 4px; } .lc-nav-list::-webkit-scrollbar { width: 4px; } .lc-nav-list::-webkit-scrollbar-track { background: transparent; } .lc-nav-list::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; } .lc-nav-list::-webkit-scrollbar-thumb:hover { background: #94a3b8; } .lc-nav-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-radius: 8px; font-size: 13px; font-weight: 500; color: #475569; background: #f8fafc; border: 1px solid #f1f5f9; cursor: pointer; transition: all .2s ease; } .lc-nav-item:hover { background: #f1f5f9; border-color: #cbd5e1; color: #0f172a; } .lc-nav-item.active { background: #ecfdf5; border-color: #a7f3d0; color: #065f46; font-weight: 600; box-shadow: 0 2px 8px -2px rgba(16, 185, 129, 0.1); } .lc-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.05) !important; transition: border-color .3s, box-shadow .3s; overflow: hidden !important; } .lc-card:hover { border-color: #cbd5e1 !important; box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.08) !important; } .lc-card .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; background: #fafbfc; padding: 14px 24px !important; } .lc-card-title { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; color: #0f172a; } .lc-card-subtitle { font-size: 11px; font-weight: 400; color: #94a3b8; margin-top: 2px; } .lc-upload-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } .lc-upload-box { aspect-ratio: 4/3; border: 1.5px dashed #cbd5e1; border-radius: 12px; background: #f8fafc; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; color: #64748b; transition: all .2s ease; position: relative; overflow: hidden; } .lc-upload-box:hover { border-color: #10b981; background: #ecfdf5; color: #10b981; } .lc-image-thumb { width: 100%; height: 100%; object-fit: cover; } .lc-image-mask { position: absolute; inset: 0; background: rgba(15, 23, 42, 0.6); opacity: 0; display: flex; align-items: center; justify-content: center; gap: 12px; transition: opacity .2s ease; color: #fff; } .lc-upload-box:hover .lc-image-mask { opacity: 1; } .lc-image-action-btn { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.15); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background .2s; } .lc-image-action-btn:hover { background: rgba(255,255,255,0.3); } .lc-image-action-btn.delete:hover { background: #ef4444; } .lc-ocr-tag { background: #fef3c7; color: #d97706; border: 1px solid #fde68a; font-size: 11px; padding: 1px 6px; border-radius: 4px; font-weight: 600; display: inline-flex; align-items: center; gap: 4px; } .lc-ocr-scanline { position: absolute; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, transparent, #10b981, transparent); animation: lc-scan 1.5s infinite linear; z-index: 10; box-shadow: 0 0 8px #10b981; } @keyframes lc-scan { 0% { top: 0%; } 50% { top: 100%; } 100% { top: 0%; } } .lc-sh-badge { background: #f5f3ff; color: #7c3aed; border: 1px solid #ddd6fe; font-size: 11px; padding: 1px 6px; border-radius: 4px; font-weight: 600; } .lc-form-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px 20px; } .lc-form-grid .ant-form-item { margin-bottom: 0 !important; display: flex !important; flex-direction: column !important; } .lc-form-grid .ant-form-item-label { padding-bottom: 6px !important; text-align: left !important; } .lc-form-grid .ant-form-item-control { width: 100% !important; } .lc-form-grid .ant-form-item-control-input-content { display: flex !important; width: 100% !important; } .lc-form-grid .ant-form-item-control-input-content > * { width: 100% !important; } .lc-form-group-title { font-size: 13px; font-weight: 600; color: #475569; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; } .lc-h2-refuel-card { width: 100%; height: 160px; border-radius: 16px; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); position: relative; overflow: hidden; padding: 20px; color: #fff; display: flex; flex-direction: column; justify-content: space-between; box-shadow: 0 8px 30px rgba(15, 23, 42, 0.15); } .lc-h2-refuel-card::before { content: ""; position: absolute; top: -20%; right: -20%; width: 180px; height: 180px; border-radius: 50%; background: radial-gradient(circle, rgba(16, 185, 129, 0.2) 0%, transparent 70%); filter: blur(20px); } .lc-h2-card-logo { font-size: 16px; font-weight: 800; letter-spacing: 0.05em; background: linear-gradient(90deg, #10b981, #34d399); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: flex; align-items: center; gap: 6px; } .lc-h2-card-number { font-size: 18px; font-weight: 700; font-family: monospace; letter-spacing: 0.1em; color: #f1f5f9; } .lc-h2-card-balance { font-size: 24px; font-weight: 800; font-family: monospace; color: #34d399; } .lc-h2-card-meta { display: flex; justify-content: space-between; font-size: 11px; color: #94a3b8; } .lc-page-footer { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); border-top: 1px solid #e2e8f0; padding: 12px 24px; display: flex; justify-content: flex-end; gap: 12px; z-index: 100; box-shadow: 0 -4px 20px rgba(15, 23, 42, 0.03); } .lc-ocr-flash { animation: flash-green .4s ease-out 2; } @keyframes flash-green { 0% { background-color: transparent; } 50% { background-color: rgba(16, 185, 129, 0.15); } 100% { background-color: transparent; } } /* ==================== 列表台账页面专属样式 ==================== */ .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(3, 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, 0.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 0.2s ease, border-color 0.2s ease, transform 0.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-cert-legend-outer { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-start; gap: 10px; padding: 6px 4px; } .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; } .lc-cert-legend-items { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; font-size: 12px; font-weight: 400; color: #64748b; } .lc-cert-legend-item { display: inline-flex; align-items: center; gap: 4px; } .lc-cert-legend-item--help { cursor: help; } .lc-plate-badge { display: inline-flex; align-items: center; justify-content: center; font-weight: 800; font-size: 13px; height: 25px; padding: 0 10px; border-radius: 5px; letter-spacing: 0.05em; box-shadow: 0 2px 4px rgba(0,0,0,0.06); border: 1.5px solid #000000; } .lc-plate-blue { background: linear-gradient(135deg, #1e40af 0%, #1d4ed8 100%); color: #ffffff; border-color: #3b82f6; } .lc-plate-yellow { background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%); color: #0f172a; border-color: #facc15; } .lc-plate-green { background: linear-gradient(90deg, #34d399 0%, #a7f3d0 50%, #34d399 100%); color: #0f172a; border-color: #10b981; } .lc-table-card { background: #ffffff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; } .lc-table-cert-legend-label { font-size: 12px; font-weight: 600; color: #64748b; } .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: 8px 8px !important; text-align: center; vertical-align: middle; } .lc-table-th-multiline { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; line-height: 1.3; white-space: normal; word-break: keep-all; } .lc-table-th-line { display: block; font-size: 12px; font-weight: 700; color: #475569; } .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-list-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 10px 8px !important; } .lc-table-card .ant-table-tbody > tr.ant-table-measure-row, .lc-table-card .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; } .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 14px 16px !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-table-card .ant-table-tbody > tr.lc-row-retired:hover > td { background: #f1f5f9 !important; } .lc-table-card .ant-table-tbody > tr.lc-row-retired .lc-muted-text { color: #94a3b8 !important; } .lc-table-card .ant-table-tbody > tr.lc-row-retired .ant-badge-status-text { color: #94a3b8 !important; } .lc-mini-badge { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; padding: 2px 6px; border-radius: 4px; } .lc-mini-badge-success { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; } .lc-mini-badge-warning { background: #fff7ed; color: #ea580c; border: 1px solid #ffedd5; } .lc-mini-badge-error { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; } .lc-mini-badge-default { background: #f1f5f9; color: #64748b; border: 1px solid #e2e8f0; } .lc-list-cert-status-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: auto auto; gap: 3px 2px; width: 100%; max-width: 100%; } .lc-list-cert-status-item { display: inline-flex; align-items: center; min-width: 0; line-height: 1.2; } .lc-list-cert-status-item .ant-badge { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; font-size: 10px; } .lc-list-cert-status-item .ant-badge-status-dot { width: 5px !important; height: 5px !important; top: 0 !important; } .lc-list-cert-status-item .ant-badge-status-text { font-size: 10px !important; white-space: nowrap; margin-left: 2px !important; } .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: 10px; white-space: nowrap; line-height: 1.2; } .lc-list-date-cell-status { margin-top: 3px; } .lc-dot-indicator { width: 7px; height: 7px; border-radius: 50%; display: inline-block; } .lc-batch-export-types { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 16px; margin-top: 8px; } @media (max-width: 640px) { .lc-batch-export-types { grid-template-columns: 1fr; } } .lc-batch-ocr-confirm { display: grid; grid-template-columns: minmax(200px, 340px) 1fr; gap: 20px; min-height: 420px; } @media (max-width: 900px) { .lc-batch-ocr-confirm { grid-template-columns: 1fr; } } .lc-batch-ocr-photos { display: flex; flex-direction: column; gap: 10px; max-height: 480px; overflow-y: auto; } .lc-batch-ocr-photo-main { border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; cursor: zoom-in; background: #f8fafc; } .lc-batch-ocr-photo-main img { width: 100%; display: block; max-height: 280px; object-fit: contain; } .lc-batch-ocr-thumb-row { display: flex; gap: 8px; flex-wrap: wrap; } .lc-batch-ocr-thumb { width: 56px; height: 56px; border-radius: 8px; border: 2px solid transparent; overflow: hidden; cursor: pointer; opacity: 0.75; } .lc-batch-ocr-thumb.active { border-color: #10b981; opacity: 1; } .lc-batch-ocr-thumb--fail { border-color: #ef4444 !important; opacity: 1; } .lc-batch-ocr-thumb img { width: 100%; height: 100%; object-fit: cover; } .lc-batch-ocr-plate-readonly { font-size: 15px; font-weight: 700; color: #0f172a; letter-spacing: 0.04em; } .lc-batch-ocr-step-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; padding: 10px 14px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; } .lc-batch-ocr-step-val { font-size: 18px; font-weight: 800; color: #0f172a; font-variant-numeric: tabular-nums; } .lc-batch-ocr-photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 8px; margin-bottom: 10px; } `; const Component = function () { const [selectedPlate, setSelectedPlate] = useState('沪A03561F'); const [activeNav, setActiveTab] = useState('driverLicense'); const [allLicenses, setAllLicenses] = useState(() => ( loadLicensesFromStorage() || JSON.parse(JSON.stringify(INITIAL_LICENSE_DATA)) )); const [licenses, setLicenses] = useState(() => createEmptyLicenseRecord()); const [savedLicenses, setSavedLicenses] = useState(() => createEmptyLicenseRecord()); const [ocrActiveCard, setOcrActiveCard] = useState(null); const [ocrLoading, setOcrLoading] = useState(false); const [ocrHighlight, setOcrHighlight] = useState(false); const [previewOpen, setPreviewOpen] = useState(false); const [previewUrl, setPreviewUrl] = useState(''); const [prdOpen, setPrdOpen] = useState(false); const syncLicensesForPlate = (plate, master) => { const bundle = loadPlateLicenseBundle(master, plate); setLicenses(JSON.parse(JSON.stringify(bundle))); setSavedLicenses(JSON.parse(JSON.stringify(bundle))); }; useEffect(() => { const master = loadLicensesFromStorage() || JSON.parse(JSON.stringify(INITIAL_LICENSE_DATA)); setAllLicenses(master); let plate = MOCK_VEHICLES[0]?.plateNo || '沪A03561F'; try { const fromSession = sessionStorage.getItem(LC_EDIT_PLATE_KEY); if (fromSession) plate = fromSession; } catch { /* ignore */ } setSelectedPlate(plate); syncLicensesForPlate(plate, master); message.loading({ content: `正在调取 [${plate}] 资质档案明细...`, key: 'loading_edit', duration: 0.5 }); }, []); // 切换车辆时,自动填充该车辆的已有证照数据 const handlePlateChange = (plate) => { setSelectedPlate(plate); try { sessionStorage.setItem(LC_EDIT_PLATE_KEY, plate); } catch { /* ignore */ } syncLicensesForPlate(plate, allLicenses); message.success(`已切换车辆:${plate}`); }; // 车辆基本信息 const currentVehicleInfo = useMemo(() => ( MOCK_VEHICLES.find((v) => v.plateNo === selectedPlate) || MOCK_VEHICLES[0] ), [selectedPlate]); // 计算上海牌照 const isShanghaiPlate = useMemo(() => { return selectedPlate.startsWith('沪'); }, [selectedPlate]); // 行驶证有效期月份自动算最后一天 const handleDriverLicenseExpireChange = (dateString) => { if (!dateString) { setLicenses(prev => { const copy = { ...prev }; copy.driverLicense.expireDate = ''; return copy; }); return; } const match = dateString.match(/^(\d{4})-(\d{2})/); if (match) { const year = parseInt(match[1]); const month = parseInt(match[2]); const lastDay = new Date(year, month, 0).getDate(); const adjustedDate = `${year}-${String(month).padStart(2, '0')}-${lastDay}`; setLicenses(prev => { const copy = { ...prev }; copy.driverLicense.expireDate = adjustedDate; return copy; }); } }; // 模拟 OCR 识别全过程(编辑页联动) const simulateOcr = (cardType) => { setOcrActiveCard(cardType); setOcrLoading(true); setOcrHighlight(false); setTimeout(() => { const isMismatch = Math.random() < 0.25; const ocrPlateNo = isMismatch ? '京A88888' : selectedPlate; if (ocrPlateNo !== selectedPlate) { setOcrLoading(false); setOcrActiveCard(null); Modal.error({ title: 'OCR 校验失败', content: (

系统检测到上传的证件照片对应的车牌号与当前编辑车辆不符!

当前编辑车辆:{selectedPlate}

证件识别车牌:{ocrPlateNo}

提示:请确认您上传的是否为本车的行驶证或道路运输证。为了保证台账档案的真实严谨,车牌号不一致禁止保存录入。

), okText: '重新上传', onOk() { setLicenses(prev => { const copy = { ...prev }; if (cardType === 'driverLicense') { copy.driverLicense.photos.pop(); } else if (cardType === 'transportLicense') { copy.transportLicense.photos = []; } return copy; }); } }); return; } setOcrLoading(false); setOcrHighlight(true); setLicenses(prev => { const copy = { ...prev }; if (cardType === 'driverLicense') { copy.driverLicense.photos = ['https://picsum.photos/seed/license_ocr/600/400']; copy.driverLicense.regDate = '2024-06-05'; copy.driverLicense.issueDate = '2024-06-05'; copy.driverLicense.scrapDate = '2039-06-04'; copy.driverLicense.expireDate = '2026-06-30'; copy.driverLicense.updateType = '直接上传'; copy.driverLicense.updateTime = new Date().toLocaleString(); copy.driverLicense.updateUser = '系统智能OCR'; if (isShanghaiPlate) { copy.driverLicense.shNextEvaluation = '2026-12-05'; } } else if (cardType === 'transportLicense') { copy.transportLicense.photos = ['https://picsum.photos/seed/trans_ocr/600/400']; copy.transportLicense.licenseNo = '交字310115582910号'; copy.transportLicense.issueDate = '2024-08-15'; copy.transportLicense.expireDate = '2026-08-31'; copy.transportLicense.inspectValidUntil = '2026-07-31'; copy.transportLicense.updateTime = new Date().toLocaleString(); copy.transportLicense.updateUser = '系统智能OCR'; } return copy; }); message.success('OCR识别完成,已自动提取关键信息,请进行核对'); setTimeout(() => { setOcrHighlight(false); setOcrActiveCard(null); }, 1200); }, 1800); }; // 手动增删照片(编辑页联动) const handlePhotoUpload = (cardType) => { setLicenses(prev => { const copy = { ...prev }; const randomSeed = Math.floor(Math.random() * 1000); const url = `https://picsum.photos/seed/${randomSeed}/600/400`; if (cardType === 'driverLicense') { if (copy.driverLicense.photos.length >= 4) { message.warning('行驶证最多允许上传4张照片!'); return prev; } copy.driverLicense.photos = [...copy.driverLicense.photos, url]; setTimeout(() => simulateOcr('driverLicense'), 200); } else if (cardType === 'transportLicense') { copy.transportLicense.photos = [url]; setTimeout(() => simulateOcr('transportLicense'), 200); } else if (cardType === 'registrationCert') { copy.registrationCert.photos = [...copy.registrationCert.photos, url]; } else if (cardType === 'specialEquipCert') { copy.specialEquipCert.photos = [url]; } else if (cardType === 'specialEquipDecal') { copy.specialEquipDecal.photos = [url]; } else if (cardType === 'safetyValve') { copy.safetyValve.photos = [url]; } else if (cardType === 'pressureGauge') { copy.pressureGauge.photos = [url]; } return copy; }); }; const handlePhotoDelete = (cardType, index) => { setLicenses(prev => { const copy = { ...prev }; if (cardType === 'driverLicense') { const arr = [...copy.driverLicense.photos]; arr.splice(index, 1); copy.driverLicense.photos = arr; } else if (cardType === 'transportLicense') { copy.transportLicense.photos = []; } else if (cardType === 'registrationCert') { const arr = [...copy.registrationCert.photos]; arr.splice(index, 1); copy.registrationCert.photos = arr; } else if (cardType === 'specialEquipCert') { copy.specialEquipCert.photos = []; } else if (cardType === 'specialEquipDecal') { copy.specialEquipDecal.photos = []; } else if (cardType === 'safetyValve') { copy.safetyValve.photos = []; } else if (cardType === 'pressureGauge') { copy.pressureGauge.photos = []; } return copy; }); message.info('照片已移除'); }; const handlePhotoUpdate = (cardType, index) => { setLicenses(prev => { const copy = { ...prev }; const randomSeed = Math.floor(Math.random() * 1000) + 2000; const url = `https://picsum.photos/seed/${randomSeed}/600/400`; const arr = [...copy[cardType].photos]; arr[index] = url; copy[cardType].photos = arr; if (cardType === 'driverLicense') { setTimeout(() => simulateOcr('driverLicense'), 200); } else if (cardType === 'transportLicense') { setTimeout(() => simulateOcr('transportLicense'), 200); } return copy; }); message.success('证件照片已更新!'); }; // 证照完整度(编辑页联动) const completionStats = useMemo(() => { let total = 8; let completed = 0; if (licenses.driverLicense.photos.length > 0 && licenses.driverLicense.regDate) completed++; if (licenses.transportLicense.photos.length > 0 && licenses.transportLicense.licenseNo) completed++; if (licenses.registrationCert.photos.length > 0) completed++; if (licenses.specialEquipCert.photos.length > 0) completed++; if (licenses.specialEquipDecal.photos.length > 0 && licenses.specialEquipDecal.nextInspectDate) completed++; if (licenses.hydrogenCard.cardNo) completed++; if (licenses.safetyValve.photos.length > 0 && licenses.safetyValve.inspectDate) completed++; if (licenses.pressureGauge.photos.length > 0 && licenses.pressureGauge.inspectDate) completed++; return { percent: Math.round((completed / total) * 100), count: completed, total: total }; }, [licenses]); // 提前60天警报计算(编辑页联动) const transportAlertInfo = useMemo(() => { if (!licenses.transportLicense.expireDate) return null; const expDate = new Date(licenses.transportLicense.expireDate); const today = new Date('2026-06-01'); expDate.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0); const diffTime = expDate.getTime() - today.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays > 60) return null; return { daysLeft: diffDays, shouldWarn: diffDays <= 15 }; }, [licenses.transportLicense.expireDate]); // 提前90天警报计算(编辑页联动) const driverAlertInfo = useMemo(() => { if (!licenses.driverLicense.expireDate) return null; const expDate = new Date(licenses.driverLicense.expireDate); const today = new Date('2026-06-01'); expDate.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0); const diffTime = expDate.getTime() - today.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays > 90) return null; return { daysLeft: diffDays, shouldWarn: diffDays <= 30 }; }, [licenses.driverLicense.expireDate]); // 特种设备使用标识:提前 60 天超前感知(与列表 KPI 一致) const specialEquipDecalAlertInfo = useMemo(() => { if (!licenses.specialEquipDecal.nextInspectDate) return null; const expDate = new Date(licenses.specialEquipDecal.nextInspectDate); const today = new Date('2026-06-01'); expDate.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0); const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays > SPECIAL_EQUIP_DECAL_WARN_DAYS) return null; return { daysLeft: diffDays, isExpired: diffDays <= 0, shouldWarn: diffDays <= SPECIAL_EQUIP_DECAL_CRITICAL_DAYS }; }, [licenses.specialEquipDecal.nextInspectDate]); // 沪牌评定天数(编辑页联动) const shEvaluationDaysLeft = useMemo(() => { if (!licenses.driverLicense.shNextEvaluation) return null; const targetDate = new Date(licenses.driverLicense.shNextEvaluation); const today = new Date('2026-06-01'); targetDate.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0); const diffTime = targetDate.getTime() - today.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; }, [licenses.driverLicense.shNextEvaluation]); // 获取单个徽标状态(编辑页联动) const getBadgeStatus = (key) => { const item = licenses[key]; if (!item) return 'default'; if (key === 'hydrogenCard') { if (!item.cardNo) return 'default'; } else { if (!item.photos || item.photos.length === 0) return 'default'; } const today = new Date('2026-06-01'); today.setHours(0, 0, 0, 0); if (key === 'driverLicense') { if (!item.expireDate) return 'success'; const expDate = new Date(item.expireDate); expDate.setHours(0, 0, 0, 0); const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays <= 0) return 'error'; if (diffDays <= 90) return 'warning'; return 'success'; } if (key === 'transportLicense') { const badgeFromDate = (dateStr) => { if (!dateStr) return 'success'; const expDate = new Date(dateStr); expDate.setHours(0, 0, 0, 0); const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays <= 0) return 'error'; if (diffDays <= 60) return 'warning'; return 'success'; }; return mergeLicenseStatusTypes( badgeFromDate(item.expireDate), badgeFromDate(item.inspectValidUntil) ); } if (key === 'specialEquipDecal' || key === 'safetyValve' || key === 'pressureGauge') { const dKey = key === 'specialEquipDecal' ? 'nextInspectDate' : 'nextInspectDate'; if (!item[dKey]) return 'success'; const expDate = new Date(item[dKey]); expDate.setHours(0, 0, 0, 0); const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays <= 0) return 'error'; if (diffDays <= 60) return 'warning'; return 'success'; } return 'success'; }; const isCardDirty = (cardKey) => { return JSON.stringify(licenses[cardKey]) !== JSON.stringify(savedLicenses[cardKey]); }; const hasUnsavedChanges = useMemo(() => { return Object.keys(licenses).some(key => isCardDirty(key)); }, [licenses, savedLicenses]); const handlePlateSelectChange = (plate) => { if (hasUnsavedChanges) { Modal.confirm({ title: '确认切换车辆?', content: '当前页面还有证照未保存,切换车辆将丢失未保存的修改,是否确认切换?', okText: '确认切换', cancelText: '取消', onOk() { handlePlateChange(plate); } }); } else { handlePlateChange(plate); } }; const scrollToAnchor = (id) => { setActiveTab(id); const el = document.getElementById(id); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }; const handleCardSubmit = (cardKey, cardName) => { Modal.confirm({ title: '确认保存', content: `是否确认保存「${cardName}」的所有更改至 ONE-OS?`, okText: '确认保存', cancelText: '取消', onOk() { const key = `save_${cardKey}`; message.loading({ content: `正在保存「${cardName}」数据至 ONE-OS...`, key }); setTimeout(() => { // 同步局部已保存状态 setSavedLicenses(prev => { const copy = JSON.parse(JSON.stringify(prev)); copy[cardKey] = JSON.parse(JSON.stringify(licenses[cardKey])); return copy; }); // 核心:回写至共享数据库 master state 保证列表和统计同步更新! setAllLicenses((prev) => { const copy = JSON.parse(JSON.stringify(prev)); if (!copy[selectedPlate]) copy[selectedPlate] = createEmptyLicenseRecord(); copy[selectedPlate][cardKey] = JSON.parse(JSON.stringify(licenses[cardKey])); persistLicensesToStorage(copy); return copy; }); message.success({ content: `「${cardName}」数据保存成功!已独立同步回台账。`, key, duration: 2 }); }, 600); } }); }; return (
{/* 顶栏 */}
{/* 双栏布局 */}
{/* 左侧侧边栏 */}
} >
{/* 右侧明细编辑区 */}
{/* Card 1: 行驶证 */} 行驶证 } extra={ {isCardDirty('driverLicense') && ( 未保存 )} {licenses.driverLicense.photos.length > 0 && ( {ICONS.success} 已完善 )} } > {/* 年审超前任务提醒 */} {driverAlertInfo && ( 车辆年检超前感知系统 } description={
根据法规政策,机动车行驶证需在检验有效期前完成年检。系统会提前 90 天自动生成年检任务。 本车辆距离年检最终时间还剩余 30 ? '#10b981' : '#ef4444' }}>{driverAlertInfo.daysLeft}
} type={driverAlertInfo.shouldWarn ? "error" : "warning"} showIcon icon={ICONS.warning} style={{ marginBottom: 20, borderRadius: 12 }} /> )}
证件照片上传 (支持正副本及细节图,最多4张)
{licenses.driverLicense.photos.map((url, idx) => (
{ocrLoading && ocrActiveCard === 'driverLicense' &&
} 行驶证
{ setPreviewUrl(url); setPreviewOpen(true); }}>
handlePhotoUpdate('driverLicense', idx)}> {ICONS.edit}
handlePhotoDelete('driverLicense', idx)}> {ICONS.delete}
))} {licenses.driverLicense.photos.length < 4 && (
handlePhotoUpload('driverLicense')}> {ICONS.camera} 上传照片 ({licenses.driverLicense.photos.length}/4)
)}
注册日期} required> setLicenses(prev => { const copy = { ...prev }; copy.driverLicense.regDate = dateString; return copy; })} placeholder="选择注册日期" className={ocrHighlight && ocrActiveCard === 'driverLicense' ? 'lc-ocr-flash' : ''} style={{ width: '100%' }} /> 发证日期} required> setLicenses(prev => { const copy = { ...prev }; copy.driverLicense.issueDate = dateString; return copy; })} placeholder="选择发证日期" className={ocrHighlight && ocrActiveCard === 'driverLicense' ? 'lc-ocr-flash' : ''} style={{ width: '100%' }} /> 强制报废日期} required> setLicenses(prev => { const copy = { ...prev }; copy.driverLicense.scrapDate = dateString; return copy; })} placeholder="选择强制报废日期" className={ocrHighlight && ocrActiveCard === 'driverLicense' ? 'lc-ocr-flash' : ''} style={{ width: '100%' }} /> 检验有效期至} required > handleDriverLicenseExpireChange(dateString)} placeholder="选择年月" className={ocrHighlight && ocrActiveCard === 'driverLicense' ? 'lc-ocr-flash' : ''} style={{ width: '100%' }} />
{/* 沪牌专属字段 */} {isShanghaiPlate && (
沪牌车辆等级评定 本市牌照车辆需定期进行等级评定,目前距下次等级评定还有 {shEvaluationDaysLeft !== null ? shEvaluationDaysLeft : '--'} 天
下次等级评定时间} required> setLicenses(prev => { const copy = { ...prev }; copy.driverLicense.shNextEvaluation = dateString; return copy; })} style={{ width: '100%', borderRadius: 8 }} placeholder="选择评定日期" />
)} {/* Card 2: 道路运输证 */} 道路运输证
} extra={ {isCardDirty('transportLicense') && ( 未保存 )} {licenses.transportLicense.photos.length > 0 && ( {ICONS.success} 已完善 )} } > {/* 年审超前任务提醒 */} {transportAlertInfo && ( 营运证件年审超前感知系统 } description={
根据法规政策,道路运输证需在有效期前进行续签,系统会提前 60 天列入年审任务。 本车辆距离最终检验时间还剩 15 ? '#10b981' : '#ef4444' }}>{transportAlertInfo.daysLeft}
} type={transportAlertInfo.shouldWarn ? "error" : "warning"} showIcon icon={ICONS.warning} style={{ marginBottom: 20, borderRadius: 12 }} /> )}
道路运输证照片
{licenses.transportLicense.photos.map((url, idx) => (
道路运输证
{ setPreviewUrl(url); setPreviewOpen(true); }}>
handlePhotoUpdate('transportLicense', idx)}> {ICONS.edit}
handlePhotoDelete('transportLicense', idx)}> {ICONS.delete}
))} {licenses.transportLicense.photos.length === 0 && (
handlePhotoUpload('transportLicense')}> {ICONS.camera} 上传运输证主图
)}
经营许可证号} required> setLicenses(prev => { const copy = { ...prev }; copy.transportLicense.licenseNo = e.target.value; return copy; })} placeholder="例如:交字31011..." className={ocrHighlight && ocrActiveCard === 'transportLicense' ? 'lc-ocr-flash' : ''} /> 核发时间} required> setLicenses(prev => { const copy = { ...prev }; copy.transportLicense.issueDate = dateString; return copy; })} placeholder="选择核发时间" className={ocrHighlight && ocrActiveCard === 'transportLicense' ? 'lc-ocr-flash' : ''} style={{ width: '100%' }} /> 证件有效期} required> setLicenses(prev => { const copy = { ...prev }; copy.transportLicense.expireDate = dateString; return copy; })} placeholder="道路运输证有效期至" className={ocrHighlight && ocrActiveCard === 'transportLicense' ? 'lc-ocr-flash' : ''} style={{ width: '100%' }} /> 审验有效期} required> setLicenses(prev => { const copy = { ...prev }; copy.transportLicense.inspectValidUntil = dateString; return copy; })} placeholder="选择审验有效期" className={ocrHighlight && ocrActiveCard === 'transportLicense' ? 'lc-ocr-flash' : ''} style={{ width: '100%' }} />
{/* Card 3: 登记证 */} 登记证 } extra={ {isCardDirty('registrationCert') && ( 未保存 )} {licenses.registrationCert.photos.length > 0 && ( {ICONS.success} 已完善 )} } >
登记证高清照片 (支持上传多张,包含登记栏所有变更页)
{licenses.registrationCert.photos.map((url, idx) => (
登记证
{ setPreviewUrl(url); setPreviewOpen(true); }}>
handlePhotoUpdate('registrationCert', idx)}> {ICONS.edit}
handlePhotoDelete('registrationCert', idx)}> {ICONS.delete}
))}
handlePhotoUpload('registrationCert')}> {ICONS.camera} 追加新页面
{/* Card 4: 特种设备使用登记证 */} 特种设备使用登记证 } extra={ {isCardDirty('specialEquipCert') && ( 未保存 )} {licenses.specialEquipCert.photos.length > 0 && ( {ICONS.success} 已完善 )} } >
使用登记证照片
{licenses.specialEquipCert.photos.map((url, idx) => (
特种设备使用登记证
{ setPreviewUrl(url); setPreviewOpen(true); }}>
handlePhotoUpdate('specialEquipCert', idx)}> {ICONS.edit}
handlePhotoDelete('specialEquipCert', idx)}> {ICONS.delete}
))} {licenses.specialEquipCert.photos.length === 0 && (
handlePhotoUpload('specialEquipCert')}> {ICONS.camera} 上传登记证
)}
{/* Card 5: 特种设备使用标识 */} 特种设备使用标识 } extra={ {isCardDirty('specialEquipDecal') && ( 未保存 )} {licenses.specialEquipDecal.photos.length > 0 && ( {ICONS.success} 已完善 )} } > {specialEquipDecalAlertInfo && ( 特种设备使用标识检验超前感知系统 } description={
{specialEquipDecalAlertInfo.isExpired ? ( <> 本车「下次检验日期」已到期,已逾期 {Math.abs(specialEquipDecalAlertInfo.daysLeft)} 天,请尽快安排检验并更新标识信息。 ) : ( <> 系统提前 {SPECIAL_EQUIP_DECAL_WARN_DAYS} 天感知特种设备使用标识检验临期(与证照台账列表规则一致)。 距离下次检验日期还剩 {specialEquipDecalAlertInfo.daysLeft} 天 {specialEquipDecalAlertInfo.shouldWarn ? ',已进入高危提醒区间,请优先处理。' : ',请关注检验安排。'} )}
} type={specialEquipDecalAlertInfo.isExpired || specialEquipDecalAlertInfo.shouldWarn ? 'error' : 'warning'} showIcon icon={ICONS.warning} style={{ marginBottom: 20, borderRadius: 12 }} /> )}
使用安全标识照片
{licenses.specialEquipDecal.photos.map((url, idx) => (
使用安全标识
{ setPreviewUrl(url); setPreviewOpen(true); }}>
handlePhotoUpdate('specialEquipDecal', idx)}> {ICONS.edit}
handlePhotoDelete('specialEquipDecal', idx)}> {ICONS.delete}
))} {licenses.specialEquipDecal.photos.length === 0 && (
handlePhotoUpload('specialEquipDecal')}> {ICONS.camera} 上传标识贴
)}
下次检验日期} required> setLicenses(prev => { const copy = { ...prev }; copy.specialEquipDecal.nextInspectDate = dateString; return copy; })} style={{ width: '100%' }} placeholder="选择检验截止日期" />
{/* Card 6: 加氢卡 */} 加氢卡 } extra={ {isCardDirty('hydrogenCard') && ( 未保存 )} {licenses.hydrogenCard.cardNo && ( {ICONS.success} 已完善 )} } >
加氢卡类型
{licenses.hydrogenCard.cardNo || '•••• •••• •••• ••••'}
Available Balance
¥ {licenses.hydrogenCard.balance ? licenses.hydrogenCard.balance.toLocaleString('zh-CN', {minimumFractionDigits: 2}) : '0.00'}
TYPE: {licenses.hydrogenCard.cardType || '中石化加氢卡'}
BY: {licenses.hydrogenCard.issueUser ? licenses.hydrogenCard.issueUser.split('-')[1] || licenses.hydrogenCard.issueUser : '能源管理部-张晓'}
加氢卡号} required> 实时余额} required> `¥ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')} parser={value => value.replace(/\¥\s?|(,*)/g, '')} /> 配发时间}> 配发经办人}>
{/* Card 7: 安全阀 */} 安全阀 } extra={ {isCardDirty('safetyValve') && ( 未保存 )} {licenses.safetyValve.photos.length > 0 && ( {ICONS.success} 已完善 )} } >
安全阀校验报告照片
{licenses.safetyValve.photos.map((url, idx) => (
安全阀
{ setPreviewUrl(url); setPreviewOpen(true); }}>
handlePhotoUpdate('safetyValve', idx)}> {ICONS.edit}
handlePhotoDelete('safetyValve', idx)}> {ICONS.delete}
))} {licenses.safetyValve.photos.length === 0 && (
handlePhotoUpload('safetyValve')}> {ICONS.camera} 上传阀体/报告
)}
本次检验日期} required> setLicenses(prev => { const copy = { ...prev }; copy.safetyValve.inspectDate = dateString; return copy; })} style={{ width: '100%' }} placeholder="选择检验日期" /> 下次检测日期} required> setLicenses(prev => { const copy = { ...prev }; copy.safetyValve.nextInspectDate = dateString; return copy; })} style={{ width: '100%' }} placeholder="选择下次检测日期" />
{/* Card 8: 压力表 */} 压力表 } extra={ {isCardDirty('pressureGauge') && ( 未保存 )} {licenses.pressureGauge.photos.length > 0 && ( {ICONS.success} 已完善 )} } >
压力表校验报告照片
{licenses.pressureGauge.photos.map((url, idx) => (
压力表
{ setPreviewUrl(url); setPreviewOpen(true); }}>
handlePhotoUpdate('pressureGauge', idx)}> {ICONS.edit}
handlePhotoDelete('pressureGauge', idx)}> {ICONS.delete}
))} {licenses.pressureGauge.photos.length === 0 && (
handlePhotoUpload('pressureGauge')}> {ICONS.camera} 上传表盘/报告
)}
本次检验日期} required> setLicenses(prev => { const copy = { ...prev }; copy.pressureGauge.inspectDate = dateString; return copy; })} style={{ width: '100%' }} placeholder="选择检验日期" /> 下次检验日期} required> setLicenses(prev => { const copy = { ...prev }; copy.pressureGauge.nextInspectDate = dateString; return copy; })} style={{ width: '100%' }} placeholder="选择下次检验日期" />
{/* 底部浮动提交栏 */} setPreviewOpen(false)} width={720} centered > 大图 📋 证照资质维护 · 产品需求说明 } footer={[ ]} onCancel={() => setPrdOpen(false)} width={960} centered bodyStyle={{ maxHeight: '72vh', overflowY: 'auto', padding: '12px 24px 24px' }} >
本页:证照资质维护 模块路径:运维管理 > 车辆业务 > 证照管理-编辑 文档版本:V1.1 读者:产品 / 运营 / 合规 / 研发测试
本文档说明「证照管理-编辑」维护页} description="由台账列表「管理」进入;与「证照管理」列表共用证照库(localStorage + session 车牌)。列表监管、批量作业见列表页 PRD。" />
产品定位(本页)

单车维护工作台:对一台车的八类证照分卡片录入影像与关键日期,不做列表筛选与批量任务。

价值:分卡保存降低心智负担;OCR + 车牌校验防串证;超前感知提示驱动检验续办。

端到端主流程
  1. 列表点「管理」→ 带入车牌进入本页
  2. 左侧看索引状态点 → 点击定位右侧证照卡片
  3. 上传/改字段 → 触发「未保存」;行驶证/运输证另触发 OCR
  4. 单卡「保存该项」→ 写回证照库 → 列表 KPI/行状态更新
  5. 「返回台账列表」→ 回到证照管理列表(已保存数据保留)
【重点】五条必读规则
① 分卡片保存(非整页提交) — 每张证照独立保存;未保存切换车辆须二次确认;返回列表不丢已保存数据。
② 行驶证/运输证 OCR 车牌强校验 — 上传或更换照片触发 OCR;识别车牌与当前车不一致则弹窗阻断并清空当次无效照片。
③ 三类超前感知(维护页 Alert) — 行驶证 ≤90 天;道路运输证 ≤60 天;特种设备使用标识 ≤60 天(≤15 天高危红提示,已到期单独文案)。与列表临期规则一致。
④ 运营状态展示 — 主数据「可运营」「待运营」展示为「库存」;与列表、筛选口径一致。
⑤ 数据回写 — 保存写入 oneos_lc_licenses_v1;列表通过事件刷新;支持 Axhub 导航往返。
超前感知阈值(与列表联动)
证照 感知窗口 维护页提示
行驶证≤90 天≤30 天 Alert 升为 error
道路运输证证件/审验有效期 ≤60 天≤15 天 error(按证件有效期算)
特种设备使用标识下次检验 ≤60 天≤15 天 error;≤0 天「已逾期」文案;索引橙/红与列表一致
安全阀/压力表下次检验 ≤60 天仅索引状态点,卡片内无 Alert 条(可后续迭代)
证照 维护要点
行驶证最多 4 张;OCR 回填;检验有效期至(选月取月末);沪牌「下次等级评定」;超前感知 Alert
道路运输证单图 OCR;证号、核发时间、证件有效期审验有效期;超前感知 Alert
登记证 / 特种设备使用登记证影像上传为主,无 OCR 字段表单
特种设备使用标识标识照片 + 下次检验日期60 天超前感知 Alert
加氢卡只读:卡号、类型、余额、配发时间、配发人
安全阀 / 压力表影像;本次/下次检验日期(手填,不联动)
车牌 OCR 强校验
  • 仅行驶证、道路运输证;原型含 25% 模拟失败便于测试
  • 失败:Modal 报错、阻断写入、移除当次无效图
脏数据守卫
  • 任意卡片未保存 → 切换车辆弹窗确认
  • 照片增删改、字段变更均标「未保存」
照片生命周期

上传/追加、查看大图、更换(覆盖)、删除;删空后侧边栏索引变灰「未上传」。

  • 列表「管理」带入车牌;保存后列表数据一致
  • 八类分卡保存、未保存提示、切换车辆拦截
  • 行驶证/运输证 OCR 车牌不一致阻断
  • 行驶证 90 天、运输证 60 天、特种设备标识 60 天维护页 Alert 与索引颜色正确
  • 运输证审验有效期字段可维护并回写列表
  • 沪牌等级评定区块仅沪牌展示
  • 加氢卡只读
); }; export default Component;