diff --git a/web端/运维管理/车辆业务/证照管理-编辑.jsx b/web端/运维管理/车辆业务/证照管理-编辑.jsx
new file mode 100644
index 0000000..676e0d8
--- /dev/null
+++ b/web端/运维管理/车辆业务/证照管理-编辑.jsx
@@ -0,0 +1,2830 @@
+// 【重要】必须使用 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 (
+
+
+
+
+
+ {/* 顶栏 */}
+
+
+
+
+
+ {/* 双栏布局 */}
+
+ {/* 左侧侧边栏 */}
+
+ }
+ >
+
+
+
+
+
+ {/* 右侧明细编辑区 */}
+
+
+
+ {/* 底部浮动提交栏 */}
+
+
+
+ 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 + 车牌校验防串证;超前感知提示驱动检验续办。
+
+
+
端到端主流程
+
+ - 列表点「管理」→ 带入车牌进入本页
+ - 左侧看索引状态点 → 点击定位右侧证照卡片
+ - 上传/改字段 → 触发「未保存」;行驶证/运输证另触发 OCR
+ - 单卡「保存该项」→ 写回证照库 → 列表 KPI/行状态更新
+ - 「返回台账列表」→ 回到证照管理列表(已保存数据保留)
+
+
+
【重点】五条必读规则
+
+
+ ① 分卡片保存(非整页提交)
+ — 每张证照独立保存;未保存切换车辆须二次确认;返回列表不丢已保存数据。
+
+
+ ② 行驶证/运输证 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;
diff --git a/web端/运维管理/车辆业务/证照管理.jsx b/web端/运维管理/车辆业务/证照管理.jsx
index d101fb3..f9572fa 100644
--- a/web端/运维管理/车辆业务/证照管理.jsx
+++ b/web端/运维管理/车辆业务/证照管理.jsx
@@ -1,303 +1,3093 @@
// 【重要】必须使用 const Component 作为组件变量名
-// 运维管理 - 车辆管理 - 证照管理
+// 运维管理 - 车辆业务 - 证照管理(台账列表页)
-const Component = function () {
- var useState = React.useState;
- var useMemo = React.useMemo;
- var useCallback = React.useCallback;
+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,
+ Table,
+ Tag,
+ message,
+ Checkbox,
+ Upload
+} = antd;
- var antd = window.antd;
- var Breadcrumb = antd.Breadcrumb;
- var Card = antd.Card;
- var Table = antd.Table;
- var Button = antd.Button;
- var Select = antd.Select;
- var Input = antd.Input;
- var DatePicker = antd.DatePicker;
- var Modal = antd.Modal;
- var message = antd.message;
+const CERT_EXPORT_OPTIONS = [
+ { key: 'driverLicense', label: '行驶证', folder: '行驶证' },
+ { key: 'transportLicense', label: '道路运输证', folder: '道路运输证' },
+ { key: 'registrationCert', label: '登记证', folder: '登记证' },
+ { key: 'specialEquipCert', label: '特种设备使用登记证', folder: '特种设备使用登记证' },
+ { key: 'specialEquipDecal', label: '特种设备使用标识', folder: '特种设备使用标识' },
+ { key: 'safetyValve', label: '安全阀', folder: '安全阀' },
+ { key: 'pressureGauge', label: '压力表', folder: '压力表' }
+];
- function pad2(n) { return n < 10 ? '0' + n : '' + n; }
- function fmtDate(d) { return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); }
- function todayPlus(days) { var d = new Date(); d.setDate(d.getDate() + days); return fmtDate(d); }
+const BATCH_UPLOAD_CERT_OPTIONS = [
+ { key: 'driverLicense', label: '行驶证' },
+ { key: 'transportLicense', label: '道路运输证' },
+ { key: 'registrationCert', label: '登记证', photoOnly: true },
+ { key: 'specialEquipCert', label: '特种设备使用登记证', photoOnly: true },
+ { key: 'specialEquipDecal', label: '特种设备使用标识' }
+];
- var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' };
- var cardStyle = { marginBottom: 16 };
+const isBatchUploadPhotoOnlyType = (certType) => (
+ BATCH_UPLOAD_CERT_OPTIONS.find((o) => o.key === certType)?.photoOnly === true
+);
- // 筛选
- var filtersState = useState({
- customerName: undefined,
- contractCode: undefined,
- plateNo: undefined,
- vin: undefined,
- licenseInspectValid: null,
- operationPermitValid: null,
- passPermitValid: null
- });
- var filters = filtersState[0];
- var setFilters = filtersState[1];
- var moreOpenState = useState(false);
+const BATCH_UPLOAD_OPERATOR = '张明辉';
- // mock 列表数据
- var tableDataState = useState(function () {
- var rows = [];
- for (var i = 1; i <= 20; i++) {
- rows.push({
- key: 'row-' + i,
- plateNo: i < 10 ? ('粤A1234' + i) : ('京A5432' + i),
- vin: 'LJ8ABC' + (100000 + i),
- licenseRegDate: todayPlus(-500 - i),
- licenseScrapDate: todayPlus(2000 + i),
- licenseValidDate: todayPlus(60 + i),
- operationCertNo: 'YYZ-' + (10000 + i),
- operationRegDate: todayPlus(-480 - i),
- operationInspectValid: todayPlus(90 + i),
- operationValid: todayPlus(365 + i),
- passPermitNo: 'TXZ-' + (20000 + i),
- passArea: i % 3 === 0 ? '上海市-浦东新区' : i % 3 === 1 ? '广东省-广州市' : '北京市-朝阳区',
- passValid: todayPlus(120 + i),
- h2CertCode: 'JQZ-' + (30000 + i),
- h2CertInspectDate: todayPlus(-30 - i),
- h2CardCode: 'JQK-' + (40000 + i),
- safetyValveInspectDate: todayPlus(-20 - i),
- safetyValveCycleMonth: 12,
- pressureGaugeInspectDate: todayPlus(-10 - i),
- pressureGaugeCycleMonth: 12,
- h2BottleVendor: i % 2 === 0 ? '亿华' : '中集',
- h2BottleInspectDate: todayPlus(-5 - i),
- h2BottleCycleMonth: 24
- });
- }
- return rows;
- });
- var tableData = tableDataState[0];
- var setTableData = tableDataState[1];
+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';
- // 下拉选项(来自列表 mock)
- var options = useMemo(function () {
- var customers = ['嘉兴某某物流有限公司', '上海某某运输公司', '北京某某租赁有限公司'];
- var contracts = ['HI-2024-001', 'HI-2024-002', 'HI-2024-003'];
- return {
- customerName: customers.map(function (v) { return { value: v, label: v }; }),
- contractCode: contracts.map(function (v) { return { value: v, label: v }; }),
- plateNo: tableData.map(function (r) { return r.plateNo; }).slice(0, 20).map(function (v) { return { value: v, label: v }; }),
- vin: tableData.map(function (r) { return r.vin; }).slice(0, 20).map(function (v) { return { value: v, label: v }; })
- };
- }, [tableData]);
-
- var filteredData = useMemo(function () {
- var list = tableData;
- if (filters.customerName) {
- // mock:按 index 分组匹配
- list = list.filter(function (_, idx) { return (idx % 3) === 0; });
- }
- if (filters.contractCode) {
- list = list.filter(function (_, idx) { return (idx % 3) === 1; });
- }
- if (filters.plateNo) list = list.filter(function (r) { return r.plateNo === filters.plateNo; });
- if (filters.vin) list = list.filter(function (r) { return r.vin === filters.vin; });
- return list;
- }, [tableData, filters.customerName, filters.contractCode, filters.plateNo, filters.vin]);
-
- // 选中
- var selectedRowKeysState = useState([]);
-
- // 导入弹窗
- var importOpenState = useState(false);
- var importFileState = useState(null);
-
- function resetFilters() {
- setFilters({
- customerName: undefined,
- contractCode: undefined,
- plateNo: undefined,
- vin: undefined,
- licenseInspectValid: null,
- operationPermitValid: null,
- passPermitValid: null
- });
- message.success('已重置');
- }
-
- function handleQuery() {
- message.success('已查询(原型)');
- }
-
- function openAdd() {
- message.info('进入证照录入页(原型)');
- }
-
- function confirmDeleteSelected() {
- var keys = selectedRowKeysState[0] || [];
- if (!keys.length) return;
- Modal.confirm({
- title: '确认删除',
- content: '确定要删除选中的证照记录吗?删除后将无法恢复。',
- okText: '确认删除',
- cancelText: '取消',
- onOk: function () {
- setTableData(function (p) { return p.filter(function (r) { return keys.indexOf(r.key) === -1; }); });
- selectedRowKeysState[1]([]);
- message.success('已删除');
- }
- });
- }
-
- function openExport() {
- message.info('导出当前筛选结果(原型)');
- }
-
- function openImport() {
- importFileState[1](null);
- importOpenState[1](true);
- }
-
- function onPickImportFile(e) {
- var f = e && e.target && e.target.files && e.target.files[0];
- if (!f) return;
- var name = String(f.name || '').toLowerCase();
- if (!(name.endsWith('.xls') || name.endsWith('.xlsx'))) {
- message.error('仅支持 .xls、.xlsx 格式');
- return;
- }
- importFileState[1](f);
- }
-
- function doImportUpload() {
- if (!importFileState[0]) {
- message.error('请先选取要上传的文件');
- return;
- }
- message.success('上传成功(原型)');
- importOpenState[1](false);
- }
-
- // 表格列
- var columns = useMemo(function () {
- return [
- { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110, fixed: 'left' },
- {
- title: 'vin码',
- dataIndex: 'vin',
- key: 'vin',
- width: 150,
- render: function (v) {
- return React.createElement('span', { style: { color: '#f5222d', fontWeight: 600 } }, v || '-');
- }
- },
- { title: '行驶证 注册日期', dataIndex: 'licenseRegDate', key: 'licenseRegDate', width: 140 },
- { title: '行驶证 强制报废日期', dataIndex: 'licenseScrapDate', key: 'licenseScrapDate', width: 160 },
- { title: '行驶证 有效期', dataIndex: 'licenseValidDate', key: 'licenseValidDate', width: 130 },
- { title: '营运证 编号', dataIndex: 'operationCertNo', key: 'operationCertNo', width: 140 },
- { title: '营运证 注册日期', dataIndex: 'operationRegDate', key: 'operationRegDate', width: 140 },
- { title: '营运证 审验有效期', dataIndex: 'operationInspectValid', key: 'operationInspectValid', width: 160 },
- { title: '营运证 有效期', dataIndex: 'operationValid', key: 'operationValid', width: 140 },
- { title: '通行证 编号', dataIndex: 'passPermitNo', key: 'passPermitNo', width: 140 },
- { title: '通行区域', dataIndex: 'passArea', key: 'passArea', width: 160, ellipsis: true },
- { title: '通行证 有效期', dataIndex: 'passValid', key: 'passValid', width: 140 },
- { title: '加氢证 编码', dataIndex: 'h2CertCode', key: 'h2CertCode', width: 140 },
- { title: '加氢证 检验日期', dataIndex: 'h2CertInspectDate', key: 'h2CertInspectDate', width: 150 },
- { title: '加氢卡 编码', dataIndex: 'h2CardCode', key: 'h2CardCode', width: 140 },
- { title: '安全阀 检验日期', dataIndex: 'safetyValveInspectDate', key: 'safetyValveInspectDate', width: 150 },
- { title: '安全阀 检验周期:单位 (月)', dataIndex: 'safetyValveCycleMonth', key: 'safetyValveCycleMonth', width: 200 },
- { title: '压力表 检验日期', dataIndex: 'pressureGaugeInspectDate', key: 'pressureGaugeInspectDate', width: 150 },
- { title: '压力表 检验周期:单位 (月)', dataIndex: 'pressureGaugeCycleMonth', key: 'pressureGaugeCycleMonth', width: 200 },
- { title: '氢气瓶 厂家', dataIndex: 'h2BottleVendor', key: 'h2BottleVendor', width: 120 },
- { title: '氢气瓶 检验日期', dataIndex: 'h2BottleInspectDate', key: 'h2BottleInspectDate', width: 150 },
- { title: '氢气瓶 检验周期:单位 (月)', dataIndex: 'h2BottleCycleMonth', key: 'h2BottleCycleMonth', width: 200 },
- {
- title: '操作',
- key: 'action',
- width: 120,
- fixed: 'right',
- render: function (_, r) {
- return React.createElement('div', { style: { display: 'flex', gap: 8 } },
- React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('编辑:' + (r.plateNo || '')); } }, '编辑'),
- React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('查看:' + (r.plateNo || '')); } }, '查看')
- );
- }
- }
- ];
- }, []);
-
- function filterOption(input, opt) {
- return String((opt && opt.label) || '').toLowerCase().indexOf(String(input || '').toLowerCase()) !== -1;
- }
-
- return React.createElement('div', { style: layoutStyle },
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } },
- React.createElement(Breadcrumb, { items: [{ title: '运维管理' }, { title: '车辆管理' }, { title: '证照管理' }] })
- ),
-
- React.createElement(Card, { title: '筛选与操作区', style: cardStyle },
- React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 } },
- React.createElement('div', { style: { flex: 1 } },
- React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 } },
- React.createElement(Select, { placeholder: '请选择客户名称', value: filters.customerName, onChange: function (v) { setFilters(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.customerName = v; return n; }); }, allowClear: true, showSearch: true, filterOption: filterOption, options: options.customerName }),
- React.createElement(Select, { placeholder: '请选择合同编码', value: filters.contractCode, onChange: function (v) { setFilters(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.contractCode = v; return n; }); }, allowClear: true, showSearch: true, filterOption: filterOption, options: options.contractCode }),
- React.createElement(Select, { placeholder: '请选择单车车牌号', value: filters.plateNo, onChange: function (v) { setFilters(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.plateNo = v; return n; }); }, allowClear: true, showSearch: true, filterOption: filterOption, options: options.plateNo }),
- React.createElement(Select, { placeholder: '请选择车辆VIN', value: filters.vin, onChange: function (v) { setFilters(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.vin = v; return n; }); }, allowClear: true, showSearch: true, filterOption: filterOption, options: options.vin })
- ),
- moreOpenState[0]
- ? React.createElement('div', { style: { marginTop: 12, display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 } },
- React.createElement(DatePicker, { placeholder: '请选择审验有效期', style: { width: '100%' }, value: filters.licenseInspectValid, onChange: function (v) { setFilters(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.licenseInspectValid = v; return n; }); } }),
- React.createElement(DatePicker, { placeholder: '请选择证件有效期', style: { width: '100%' }, value: filters.operationPermitValid, onChange: function (v) { setFilters(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.operationPermitValid = v; return n; }); } }),
- React.createElement(DatePicker, { placeholder: '请选择有效期', style: { width: '100%' }, value: filters.passPermitValid, onChange: function (v) { setFilters(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.passPermitValid = v; return n; }); } }),
- React.createElement('div', null)
- )
- : null,
- React.createElement('div', { style: { marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 } },
- React.createElement(Button, { type: 'link', onClick: function () { moreOpenState[1](!moreOpenState[0]); } }, moreOpenState[0] ? '收起条件 ▲' : '更多条件 ▼'),
- React.createElement(Button, { onClick: resetFilters }, '重置'),
- React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询')
- )
- ),
-
- React.createElement('div', { style: { width: 360, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 10 } },
- React.createElement('div', { style: { color: '#666', fontSize: 13 } },
- '选中 ', (selectedRowKeysState[0] || []).length, '/', (filteredData || []).length, ' 条'
- ),
- React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' } },
- React.createElement(Button, { type: 'primary', onClick: openAdd }, '+ 新增'),
- React.createElement(Button, { danger: true, disabled: !(selectedRowKeysState[0] || []).length, onClick: confirmDeleteSelected }, '删除'),
- React.createElement(Button, { onClick: openExport }, '导出'),
- React.createElement(Button, { onClick: openImport }, '导入')
- )
- )
- )
- ),
-
- React.createElement(Card, { title: '证照列表', style: cardStyle },
- React.createElement(Table, {
- rowKey: 'key',
- size: 'middle',
- bordered: true,
- columns: columns,
- dataSource: filteredData,
- rowSelection: {
- selectedRowKeys: selectedRowKeysState[0],
- onChange: function (keys) { selectedRowKeysState[1](keys || []); }
- },
- scroll: { x: 2600 },
- pagination: { pageSize: 20, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'], showTotal: function (t) { return '共 ' + t + ' 条'; }, showQuickJumper: true }
- })
- ),
-
- React.createElement(Modal, {
- title: '证件导入',
- open: importOpenState[0],
- onCancel: function () { importOpenState[1](false); },
- okText: '上传',
- cancelText: '取消',
- onOk: doImportUpload
- },
- React.createElement('div', { style: { marginBottom: 12, color: '#666', fontSize: 13 } }, '*支持文件类型 .xls .xlsx'),
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 } },
- React.createElement(Button, { onClick: function () { message.info('下载模板:证件信息模板.xlsx(原型)'); } }, '模板下载'),
- React.createElement('input', { type: 'file', accept: '.xls,.xlsx', onChange: onPickImportFile })
- ),
- React.createElement('div', { style: { fontSize: 13, color: '#333' } }, '已选文件:', (importFileState[0] && importFileState[0].name) ? importFileState[0].name : '—')
- )
- );
+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 goLicenseEditPage = (plateNo, master) => {
+ try {
+ sessionStorage.setItem(LC_EDIT_PLATE_KEY, plateNo);
+ persistLicensesToStorage(master);
+ } catch {
+ /* ignore */
+ }
+ if (typeof window.__axhubNavigate === 'function') {
+ window.__axhubNavigate('证照管理-编辑');
+ message.success(`已进入 [${plateNo}] 资质维护`);
+ return;
+ }
+ message.info(`已带入 [${plateNo}] 车辆信息,请打开「证照管理-编辑」页面继续维护`);
+};
+
+
+/** 列表「证件状态」列:八类证照,双行四列展示 */
+const LIST_CERT_STATUS_ITEMS = [
+ { key: 'driverLicense', label: '行驶证', fullLabel: '行驶证' },
+ { key: 'transportLicense', label: '运输证', fullLabel: '道路运输证', mergedTransport: true },
+ { key: 'registrationCert', label: '登记', fullLabel: '机动车登记证书' },
+ { key: 'specialEquipCert', label: '特种', fullLabel: '特种设备使用登记证' },
+ { key: 'specialEquipDecal', label: '特设标', fullLabel: '特种设备使用标识' },
+ { key: 'hydrogenCard', label: '加氢卡', fullLabel: '加氢卡' },
+ { key: 'safetyValve', label: '安全阀', fullLabel: '安全阀检验' },
+ { key: 'pressureGauge', label: '压力表', fullLabel: '压力表检验' }
+];
+
+/** 列表表头:过长标题拆为多行,收窄列宽便于一屏展示 */
+const tableTitleMultiline = (...lines) => (
+
+ {lines.map((line, idx) => (
+ {line}
+ ))}
+
+);
+
+const listColumnHeaderCell = () => ({ className: 'lc-th-wrap' });
+
+const BATCH_EXPORT_RULE_LINES = [
+ '按当前筛选条件(含 KPI 看板筛选)导出,勾选证照类型各生成一个文件夹,最终打包为 ZIP。',
+ '文件夹内文件以车牌号命名;仅 1 张影像时为「车牌号.jpg」,多张依次为「车牌号-1」「车牌号-2」……'
+];
+
+/** 批量导出影像命名:单张=车牌号,多张=车牌号-序号 */
+const buildExportPhotoBaseName = (plateNo, index, total) => {
+ if (total <= 1) return plateNo;
+ return `${plateNo}-${index + 1}`;
+};
+
+/** 列表排序:退出运营车辆置底,组内保持原台账顺序 */
+const sortVehiclesRetiredLast = (vehicles) => {
+ const active = [];
+ const retired = [];
+ vehicles.forEach((v) => {
+ if (v.status === '退出运营') retired.push(v);
+ else active.push(v);
+ });
+ return [...active, ...retired];
+};
+
+/** 多车牌:优先按行解析,单行内仍支持逗号分隔 */
+const parseMultiPlates = (text) => {
+ const raw = (text || '').trim();
+ if (!raw) return [];
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
+ const expanded = lines.flatMap((line) => {
+ if (/[,,、;;]/.test(line)) {
+ return line.split(/[,,、;;]+/).map((s) => s.trim()).filter(Boolean);
+ }
+ return [line];
+ });
+ return [...new Set(expanded.map((s) => s.toUpperCase()))];
+};
+
+const loadJsZip = () => new Promise((resolve, reject) => {
+ if (typeof window !== 'undefined' && window.JSZip) {
+ resolve(window.JSZip);
+ return;
+ }
+ const script = document.createElement('script');
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
+ script.async = true;
+ script.onload = () => (window.JSZip ? resolve(window.JSZip) : reject(new Error('JSZip load failed')));
+ script.onerror = () => reject(new Error('JSZip script error'));
+ document.head.appendChild(script);
+});
+
+const fetchImageBlob = async (url) => {
+ try {
+ const res = await fetch(url, { mode: 'cors' });
+ if (!res.ok) return null;
+ return await res.blob();
+ } catch {
+ return null;
+ }
+};
+
+const downloadBlobFile = (blob, filename) => {
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(a.href);
+};
+
+const formatExportFilename = () => {
+ const d = new Date();
+ const pad = (n) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}`;
+};
+
+const normalizePlateNo = (plate) => (plate || '').trim().toUpperCase();
+
+const findVehicleByPlate = (plate) => {
+ const key = normalizePlateNo(plate);
+ return MOCK_VEHICLES.find((v) => normalizePlateNo(v.plateNo) === key) || null;
+};
+
+/** 批量 OCR:识别车牌须在证照台账中存在,否则视为校验失败 */
+const validateBatchOcrPlate = (ocrPlateNo) => {
+ const normalized = normalizePlateNo(ocrPlateNo);
+ if (!normalized) {
+ return { plateValid: false, ocrPlateNo: '', matchedPlateNo: '', plateError: '未识别到车牌号,请更换清晰照片后重试' };
+ }
+ const vehicle = findVehicleByPlate(normalized);
+ if (!vehicle) {
+ return {
+ plateValid: false,
+ ocrPlateNo: normalized,
+ matchedPlateNo: '',
+ plateError: `识别车牌「${normalized}」与证照台账不一致,请核对照片是否为台账车辆证照`
+ };
+ }
+ return { plateValid: true, ocrPlateNo: vehicle.plateNo, matchedPlateNo: vehicle.plateNo, plateError: '' };
+};
+
+const buildMockBatchOcrItem = (certType, photoIndex) => {
+ const samplePlate = MOCK_VEHICLES[photoIndex % MOCK_VEHICLES.length]?.plateNo || '沪A00000';
+ const isMismatch = Math.random() < 0.2;
+ const rawOcrPlate = isMismatch ? '京A88888' : samplePlate;
+ const plateCheck = validateBatchOcrPlate(rawOcrPlate);
+
+ if (certType === 'driverLicense') {
+ const fields = {
+ regDate: '2024-06-05',
+ issueDate: '2024-06-05',
+ scrapDate: '2039-06-04',
+ expireDate: '2026-06-30',
+ updateType: '批量上传'
+ };
+ return { ...plateCheck, fields };
+ }
+ if (certType === 'transportLicense') {
+ return {
+ ...plateCheck,
+ fields: {
+ licenseNo: '交字310115582910号',
+ issueDate: '2024-08-15',
+ expireDate: '2026-08-31',
+ inspectValidUntil: '2026-07-31'
+ }
+ };
+ }
+ if (certType === 'registrationCert' || certType === 'specialEquipCert') {
+ return { ...plateCheck, fields: {} };
+ }
+ if (certType === 'specialEquipDecal') {
+ return {
+ ...plateCheck,
+ fields: { nextInspectDate: '2027-05-20' }
+ };
+ }
+ return { ...plateCheck, fields: {} };
+};
+
+const countBatchOcrResults = (results) => {
+ if (!results || !results.length) return { ocrSuccessCount: 0, ocrFailCount: 0 };
+ const ocrSuccessCount = results.filter((r) => r.plateValid).length;
+ return { ocrSuccessCount, ocrFailCount: results.length - ocrSuccessCount };
+};
+
+/** 按车牌聚合识别结果,逐张确认时每组对应一个车牌及其全部照片 */
+const buildOcrConfirmGroups = (results) => {
+ const order = [];
+ const map = new Map();
+ (results || []).forEach((item, sourceIndex) => {
+ const plateKey = item.plateValid
+ ? normalizePlateNo(item.matchedPlateNo || item.ocrPlateNo)
+ : `__invalid__${sourceIndex}`;
+ if (!map.has(plateKey)) {
+ map.set(plateKey, {
+ plateNo: item.matchedPlateNo || item.ocrPlateNo || '',
+ plateValid: !!item.plateValid,
+ items: [],
+ fields: { ...(item.fields || {}) }
+ });
+ order.push(plateKey);
+ }
+ const group = map.get(plateKey);
+ group.items.push({ ...item, sourceIndex });
+ if (item.fields) Object.assign(group.fields, item.fields);
+ });
+ return order.map((k) => map.get(k));
+};
+
+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 applyBatchOcrGroupToCert = (existingCert, certType, group, operator) => {
+ const photos = group.items.map((it) => it.photoUrl).filter(Boolean);
+ const f = group.fields || {};
+ const now = new Date().toLocaleString('zh-CN', { hour12: false });
+ const base = existingCert ? JSON.parse(JSON.stringify(existingCert)) : {};
+
+ if (certType === 'driverLicense') {
+ const maxPhotos = photos.slice(0, 4);
+ return {
+ ...base,
+ photos: maxPhotos,
+ regDate: f.regDate || base.regDate || '',
+ issueDate: f.issueDate || base.issueDate || '',
+ scrapDate: f.scrapDate || base.scrapDate || '',
+ expireDate: f.expireDate || base.expireDate || '',
+ updateType: f.updateType || '批量上传',
+ updateTime: now,
+ updateUser: operator || base.updateUser || ''
+ };
+ }
+ if (certType === 'transportLicense') {
+ return {
+ ...base,
+ photos: photos.length ? photos.slice(0, 1) : base.photos || [],
+ licenseNo: f.licenseNo || base.licenseNo || '',
+ issueDate: f.issueDate || base.issueDate || '',
+ expireDate: f.expireDate || base.expireDate || '',
+ inspectValidUntil: f.inspectValidUntil || base.inspectValidUntil || '',
+ updateTime: now,
+ updateUser: operator || base.updateUser || ''
+ };
+ }
+ if (certType === 'specialEquipDecal') {
+ return {
+ ...base,
+ photos: photos.length ? photos : base.photos || [],
+ nextInspectDate: f.nextInspectDate || base.nextInspectDate || '',
+ updateTime: now,
+ updateUser: operator || base.updateUser || ''
+ };
+ }
+ if (certType === 'registrationCert') {
+ return {
+ ...base,
+ photos: photos.length ? photos : base.photos || []
+ };
+ }
+ if (certType === 'specialEquipCert') {
+ return {
+ ...base,
+ photos: photos.length ? photos.slice(0, 1) : base.photos || []
+ };
+ }
+ return base;
+};
+
+// 常用矢量图标,保证 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: '2027-05-20' },
+ 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 [allLicenses, setAllLicenses] = useState(() => (
+ loadLicensesFromStorage() || JSON.parse(JSON.stringify(INITIAL_LICENSE_DATA))
+ ));
+
+ const patchAllLicenses = (updater) => {
+ setAllLicenses((prev) => {
+ const next = typeof updater === 'function' ? updater(prev) : updater;
+ persistLicensesToStorage(next);
+ return next;
+ });
+ };
+
+ const DEFAULT_LIST_FILTERS = {
+ plateNo: '',
+ plateNos: '',
+ vin: '',
+ brand: '',
+ model: '',
+ operateStatus: '全部'
+ };
+ const [listFilters, setListFilters] = useState(() => ({ ...DEFAULT_LIST_FILTERS }));
+ const [appliedFilters, setAppliedFilters] = useState(() => ({ ...DEFAULT_LIST_FILTERS }));
+ const [multiPlateOpen, setMultiPlateOpen] = useState(false);
+ const [multiPlateDraft, setMultiPlateDraft] = useState('');
+
+
+ const [previewOpen, setPreviewOpen] = useState(false);
+ const [previewUrl, setPreviewUrl] = useState('');
+ const [prdOpen, setPrdOpen] = useState(false);
+ /** total | normal | warning | expired | unuploaded */
+ const [kpiFilter, setKpiFilter] = useState('total');
+
+ useEffect(() => {
+ const refreshFromStorage = () => {
+ const stored = loadLicensesFromStorage();
+ if (stored) setAllLicenses(stored);
+ };
+ const onReturn = () => {
+ try {
+ if (sessionStorage.getItem(LC_NAV_TARGET_KEY) === 'list') {
+ sessionStorage.removeItem(LC_NAV_TARGET_KEY);
+ refreshFromStorage();
+ }
+ } catch {
+ /* ignore */
+ }
+ };
+ window.addEventListener(LC_NAV_EVENT, onReturn);
+ onReturn();
+ return () => window.removeEventListener(LC_NAV_EVENT, onReturn);
+ }, []);
+
+ const [batchExportOpen, setBatchExportOpen] = useState(false);
+ const [exportCertTypes, setExportCertTypes] = useState([]);
+ const [batchOcrOpen, setBatchOcrOpen] = useState(false);
+ const [batchOcrCertType, setBatchOcrCertType] = useState('driverLicense');
+ const [batchOcrFileList, setBatchOcrFileList] = useState([]);
+ const [ocrTasks, setOcrTasks] = useState([]);
+ const [ocrConfirmOpen, setOcrConfirmOpen] = useState(false);
+ const [ocrConfirmTask, setOcrConfirmTask] = useState(null);
+ const [ocrConfirmGroups, setOcrConfirmGroups] = useState([]);
+ const [ocrConfirmGroupIdx, setOcrConfirmGroupIdx] = useState(0);
+ const [ocrConfirmPhotoIdx, setOcrConfirmPhotoIdx] = useState(0);
+ const ocrTaskTimersRef = useRef({});
+
+ // 辅助函数:根据车牌前缀获取车牌颜色类别
+ const getPlateClass = (plate) => {
+ if (plate.endsWith('F') || plate.endsWith('D') || plate.length === 8) {
+ return 'lc-plate-green'; // 绿牌(新能源车)
+ }
+ if (plate.startsWith('粤') || plate.startsWith('京')) {
+ return 'lc-plate-blue'; // 蓝牌(普通货车)
+ }
+ return 'lc-plate-yellow'; // 黄牌(中重卡)
+ };
+
+ // 辅助函数:获取到期天数
+ const getDiffDays = (dateStr) => {
+ if (!dateStr) return null;
+ const expDate = new Date(dateStr);
+ const today = new Date('2026-06-01'); // 锚定今天日期 2026-06-01
+ expDate.setHours(0, 0, 0, 0);
+ today.setHours(0, 0, 0, 0);
+ const diffTime = expDate.getTime() - today.getTime();
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ };
+
+ const mergeLicenseStatusTypes = (a, b) => {
+ const rank = { expired: 0, warning: 1, unuploaded: 2, success: 3 };
+ return (rank[a] ?? 9) <= (rank[b] ?? 9) ? a : b;
+ };
+
+ const getListCertStatus = (plateNo, item) => {
+ if (item.mergedTransport) {
+ const cert = getLicenseStatus(plateNo, 'transportLicense', { dateField: 'expireDate' });
+ const inspect = getLicenseStatus(plateNo, 'transportLicense', { dateField: 'inspectValidUntil' });
+ const type = mergeLicenseStatusTypes(cert.type, inspect.type);
+ return {
+ type,
+ text: `证件有效期:${cert.text};审验有效期:${inspect.text}`
+ };
+ }
+ const st = getLicenseStatus(plateNo, item.key);
+ return { type: st.type, text: st.text };
+ };
+
+ // 获取单个车辆各证件的到期状态(道路运输证可通过 dateField 区分证件有效期 / 审验有效期)
+ const getLicenseStatus = (plate, key, options = {}) => {
+ const item = allLicenses[plate]?.[key];
+ if (!item) return { type: 'unuploaded', text: '未上传', diffDays: null };
+
+ if (key === 'hydrogenCard') {
+ return item.cardNo ? { type: 'success', text: '已绑定', diffDays: null } : { type: 'unuploaded', text: '未绑定', diffDays: null };
+ }
+
+ if (!item.photos || item.photos.length === 0) {
+ return { type: 'unuploaded', text: '未上传', diffDays: null };
+ }
+
+ // 针对有日期的
+ const today = new Date('2026-06-01');
+ today.setHours(0,0,0,0);
+
+ let dateValue = '';
+ let warnThreshold = 30; // 默认30天警告
+
+ if (key === 'driverLicense') {
+ dateValue = item.expireDate;
+ warnThreshold = 90; // 行驶证 90 天
+ } else if (key === 'transportLicense') {
+ dateValue = options.dateField === 'inspectValidUntil' ? item.inspectValidUntil : item.expireDate;
+ warnThreshold = 60; // 运输证 60 天
+ } else if (key === 'specialEquipDecal') {
+ dateValue = item.nextInspectDate;
+ warnThreshold = 60; // 特种设备使用标识 60 天
+ } else if (key === 'safetyValve' || key === 'pressureGauge') {
+ dateValue = item.nextInspectDate;
+ warnThreshold = 60; // 安全阀 / 压力表 60 天
+ }
+
+ if (!dateValue) {
+ return { type: 'success', text: '正常', diffDays: null }; // 已上传照片无日期的默认为正常
+ }
+
+ const expDate = new Date(dateValue);
+ expDate.setHours(0, 0, 0, 0);
+ const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
+
+ if (diffDays <= 0) {
+ return { type: 'expired', text: `已到期 (逾期 ${Math.abs(diffDays)} 天)`, diffDays };
+ }
+ if (diffDays <= warnThreshold) {
+ return { type: 'warning', text: `临期 (${diffDays} 天后)`, diffDays };
+ }
+ return { type: 'success', text: '正常', diffDays };
+ };
+
+ const mapLicenseTypeToBadge = (type) => {
+ if (type === 'success') return 'success';
+ if (type === 'warning') return 'warning';
+ if (type === 'expired') return 'error';
+ return 'default';
+ };
+
+ /** 列表日期列:状态文案单行展示,完整说明放 Tooltip */
+ const getListStatusShortText = (status) => {
+ const { type, text, diffDays } = status || {};
+ if (type === 'warning') {
+ return diffDays != null ? `临期${diffDays}天` : '临期';
+ }
+ if (type === 'expired') {
+ return diffDays != null ? `逾期${Math.abs(diffDays)}天` : '已到期';
+ }
+ if (type === 'unuploaded') return text === '未绑定' ? '未绑定' : '未上传';
+ return text || '正常';
+ };
+
+ const renderListLicenseStatusBadge = (status) => (
+
+
+ {getListStatusShortText(status)}}
+ />
+
+
+ );
+
+ const isRetiredVehicle = (record) => record?.status === '退出运营';
+
+ const CERT_STATUS_LEGEND = (
+
+ 正常
+
+ 临期
+
+
+ 已到期
+
+ 未上传
+
+ );
+
+ const isTransportLicenseWarning = (plateNo) => (
+ getLicenseStatus(plateNo, 'transportLicense', { dateField: 'expireDate' }).type === 'warning'
+ || getLicenseStatus(plateNo, 'transportLicense', { dateField: 'inspectValidUntil' }).type === 'warning'
+ );
+
+ const isTransportLicenseExpired = (plateNo) => (
+ getLicenseStatus(plateNo, 'transportLicense', { dateField: 'expireDate' }).type === 'expired'
+ || getLicenseStatus(plateNo, 'transportLicense', { dateField: 'inspectValidUntil' }).type === 'expired'
+ );
+
+ const isQualificationNormal = (plateNo) => {
+ const d = getLicenseStatus(plateNo, 'driverLicense');
+ const tCert = getLicenseStatus(plateNo, 'transportLicense', { dateField: 'expireDate' });
+ const tInspect = getLicenseStatus(plateNo, 'transportLicense', { dateField: 'inspectValidUntil' });
+ return d.type === 'success' && tCert.type === 'success' && tInspect.type === 'success';
+ };
+
+ const isCertNearExpiry = (plateNo) => {
+ if (isTransportLicenseWarning(plateNo)) return true;
+ const keys = ['driverLicense', 'specialEquipDecal', 'safetyValve', 'pressureGauge'];
+ return keys.some((key) => getLicenseStatus(plateNo, key).type === 'warning');
+ };
+
+ const isCoreCertExpired = (plateNo) => {
+ if (isTransportLicenseExpired(plateNo)) return true;
+ const keys = ['driverLicense', 'safetyValve', 'pressureGauge'];
+ return keys.some((key) => getLicenseStatus(plateNo, key).type === 'expired');
+ };
+
+ const isCertPendingUpload = (plateNo) => {
+ const keys = ['driverLicense', 'transportLicense', 'specialEquipCert', 'specialEquipDecal'];
+ return keys.some((key) => getLicenseStatus(plateNo, key).type === 'unuploaded');
+ };
+
+ const matchKpiFilter = (plateNo, filterKey) => {
+ if (filterKey === 'total') return true;
+ if (filterKey === 'normal') return isQualificationNormal(plateNo);
+ if (filterKey === 'warning') return isCertNearExpiry(plateNo);
+ if (filterKey === 'expired') return isCoreCertExpired(plateNo);
+ if (filterKey === 'unuploaded') return isCertPendingUpload(plateNo);
+ return true;
+ };
+
+ const handleKpiCardClick = (key) => {
+ setKpiFilter(key);
+ };
+
+ // ==================== 统计面板数据动态推算 ====================
+ const stats = useMemo(() => {
+ let normal = 0;
+ let warning = 0;
+ let expired = 0;
+ let unuploaded = 0;
+
+ MOCK_VEHICLES.forEach((v) => {
+ if (isQualificationNormal(v.plateNo)) normal++;
+ if (isCertNearExpiry(v.plateNo)) warning++;
+ if (isCoreCertExpired(v.plateNo)) expired++;
+ if (isCertPendingUpload(v.plateNo)) unuploaded++;
+ });
+
+ return {
+ total: MOCK_VEHICLES.length,
+ normal,
+ warning,
+ expired,
+ unuploaded,
+ };
+ }, [allLicenses]);
+
+ const brandOptions = useMemo(() => {
+ return [...new Set(MOCK_VEHICLES.map(v => v.brand))].map(b => ({ label: b, value: b }));
+ }, []);
+
+ const modelOptions = useMemo(() => {
+ return [...new Set(MOCK_VEHICLES.map(v => v.model))].map(m => ({ label: m, value: m }));
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ Object.values(ocrTaskTimersRef.current).forEach((id) => clearInterval(id));
+ };
+ }, []);
+
+ const runBatchOcrTask = (taskId) => {
+ if (ocrTaskTimersRef.current[taskId]) clearInterval(ocrTaskTimersRef.current[taskId]);
+ ocrTaskTimersRef.current[taskId] = setInterval(() => {
+ setOcrTasks((prev) => {
+ const task = prev.find((t) => t.id === taskId);
+ if (!task || task.status === 'done') return prev;
+ const nextProgress = Math.min(100, task.progress + 12 + Math.floor(Math.random() * 18));
+ if (nextProgress < 100) {
+ return prev.map((t) => (t.id === taskId ? { ...t, progress: nextProgress } : t));
+ }
+ clearInterval(ocrTaskTimersRef.current[taskId]);
+ delete ocrTaskTimersRef.current[taskId];
+ const results = task.photos.map((ph, idx) => ({
+ ...buildMockBatchOcrItem(task.certType, idx),
+ photoUrl: ph.url,
+ photoName: ph.name
+ }));
+ const { ocrSuccessCount, ocrFailCount } = countBatchOcrResults(results);
+ return prev.map((t) => (t.id === taskId ? {
+ ...t,
+ progress: 100,
+ status: 'done',
+ results,
+ ocrSuccessCount,
+ ocrFailCount
+ } : t));
+ });
+ }, 450);
+ };
+
+ const handleStartBatchOcr = () => {
+ if (!batchOcrFileList.length) {
+ message.warning('请先上传至少一张证照照片');
+ return;
+ }
+ const photos = batchOcrFileList.map((f) => ({
+ url: f.url || (f.originFileObj ? URL.createObjectURL(f.originFileObj) : ''),
+ name: f.name
+ }));
+ const task = {
+ id: `ocr-${Date.now()}`,
+ certType: batchOcrCertType,
+ certLabel: BATCH_UPLOAD_CERT_OPTIONS.find((o) => o.key === batchOcrCertType)?.label || '',
+ operator: BATCH_UPLOAD_OPERATOR,
+ operateTime: new Date().toLocaleString('zh-CN', { hour12: false }),
+ photoCount: photos.length,
+ progress: 0,
+ status: 'running',
+ photos,
+ results: null
+ };
+ setOcrTasks((prev) => [task, ...prev]);
+ setBatchOcrFileList([]);
+ runBatchOcrTask(task.id);
+ message.success('已创建批量上传任务');
+ };
+
+ const currentOcrConfirmGroup = ocrConfirmGroups[ocrConfirmGroupIdx] || null;
+ const ocrConfirmTotalSheets = ocrConfirmGroups.length;
+
+ const closeOcrConfirm = () => {
+ setOcrConfirmOpen(false);
+ setOcrConfirmTask(null);
+ setOcrConfirmGroups([]);
+ setOcrConfirmGroupIdx(0);
+ setOcrConfirmPhotoIdx(0);
+ };
+
+ const finishOcrConfirmFlow = (lastPlate) => {
+ if (ocrConfirmTask?.id) {
+ setOcrTasks((prev) => prev.map((t) => (
+ t.id === ocrConfirmTask.id ? { ...t, confirmDone: true } : t
+ )));
+ }
+ const photoOnly = isBatchUploadPhotoOnlyType(ocrConfirmTask?.certType);
+ message.success(
+ lastPlate
+ ? (photoOnly ? `全部确认完成,末次已同步 ${lastPlate} 证照照片` : `全部确认完成,末次已更新 ${lastPlate} 证照`)
+ : '全部逐张确认完成'
+ );
+ closeOcrConfirm();
+ };
+
+ const goToNextOcrConfirmGroup = (lastUpdatedPlate) => {
+ if (ocrConfirmGroupIdx >= ocrConfirmGroups.length - 1) {
+ finishOcrConfirmFlow(lastUpdatedPlate);
+ return;
+ }
+ setOcrConfirmGroupIdx((i) => i + 1);
+ setOcrConfirmPhotoIdx(0);
+ };
+
+ const openOcrConfirm = (task) => {
+ if (task.progress < 100 || !task.results) return;
+ const groups = buildOcrConfirmGroups(task.results);
+ if (!groups.length) {
+ message.warning('暂无上传结果可确认');
+ return;
+ }
+ setOcrConfirmTask(task);
+ setOcrConfirmGroups(JSON.parse(JSON.stringify(groups)));
+ setOcrConfirmGroupIdx(0);
+ setOcrConfirmPhotoIdx(0);
+ setOcrConfirmOpen(true);
+ };
+
+ const updateOcrConfirmField = (fieldKey, value) => {
+ setOcrConfirmGroups((prev) => prev.map((g, i) => (
+ i === ocrConfirmGroupIdx ? { ...g, fields: { ...g.fields, [fieldKey]: value } } : g
+ )));
+ };
+
+ const handleOcrConfirmSkip = () => {
+ message.info('已跳过本张,进入下一张确认');
+ goToNextOcrConfirmGroup();
+ };
+
+ const handleOcrConfirmSubmit = () => {
+ if (!ocrConfirmTask || !currentOcrConfirmGroup) return;
+ if (!currentOcrConfirmGroup.plateValid) {
+ message.error('当前识别车牌未通过校验,请跳过或重新识别');
+ return;
+ }
+ const plateNo = currentOcrConfirmGroup.plateNo;
+ const certType = ocrConfirmTask.certType;
+ const certLabel = ocrConfirmTask.certLabel;
+ const photoOnly = isBatchUploadPhotoOnlyType(certType);
+
+ patchAllLicenses((prev) => {
+ const copy = JSON.parse(JSON.stringify(prev));
+ if (!copy[plateNo]) copy[plateNo] = createEmptyLicenseRecord();
+ const existing = copy[plateNo][certType] || {};
+ copy[plateNo][certType] = applyBatchOcrGroupToCert(
+ existing,
+ certType,
+ currentOcrConfirmGroup,
+ BATCH_UPLOAD_OPERATOR
+ );
+ return copy;
+ });
+
+ message.success(
+ photoOnly
+ ? `已同步 ${plateNo} 的${certLabel}照片至台账`
+ : `已更新 ${plateNo} 的${certLabel}`
+ );
+ goToNextOcrConfirmGroup(plateNo);
+ };
+
+ const renderOcrPlateValidation = (group) => {
+ if (!group) return null;
+ if (group.plateValid) {
+ return (
+
+ OCR 识别车牌:{group.plateNo}
+ (已匹配台账车辆,不可修改)
+
+ }
+ style={{ marginBottom: 14, borderRadius: 10 }}
+ />
+ );
+ }
+ return (
+
+ {group.plateNo ? (
+
+ OCR 识别车牌:{group.plateNo}
+
+ ) : null}
+
+ {group.items[0]?.plateError || '识别车牌与证照台账不一致,请跳过本张后继续'}
+
+
+ }
+ style={{ marginBottom: 14, borderRadius: 10 }}
+ />
+ );
+ };
+
+ const handleBatchExport = async () => {
+ if (!exportCertTypes.length) {
+ message.warning('请至少勾选一种证照类型');
+ return;
+ }
+ const hide = message.loading({ content: '正在按筛选结果打包导出...', key: 'batchExport', duration: 0 });
+ try {
+ const JSZipLib = await loadJsZip();
+ const zip = new JSZipLib();
+ let fileCount = 0;
+ const selectedOpts = CERT_EXPORT_OPTIONS.filter((o) => exportCertTypes.includes(o.key));
+
+ for (const opt of selectedOpts) {
+ const folder = zip.folder(opt.folder);
+ for (const v of filteredVehicles) {
+ const lic = allLicenses[v.plateNo]?.[opt.key];
+ if (!lic) continue;
+
+ const photos = lic.photos || [];
+ if (!photos.length) continue;
+ for (let i = 0; i < photos.length; i += 1) {
+ const blob = await fetchImageBlob(photos[i]);
+ const baseName = buildExportPhotoBaseName(v.plateNo, i, photos.length);
+ if (blob) folder.file(`${baseName}.jpg`, blob);
+ else folder.file(`${baseName}_链接.txt`, photos[i]);
+ fileCount += 1;
+ }
+ }
+ }
+
+ if (fileCount === 0) {
+ hide();
+ message.warning('当前筛选条件下,所选证照类型均无可用文件可导出');
+ return;
+ }
+
+ const blob = await zip.generateAsync({ type: 'blob' });
+ downloadBlobFile(blob, `证照批量导出_${formatExportFilename()}.zip`);
+ hide();
+ message.success({ content: `导出完成,共 ${fileCount} 个文件,已按证照类型分文件夹打包`, key: 'batchExport' });
+ setBatchExportOpen(false);
+ } catch {
+ hide();
+ message.error({ content: '打包导出失败,请检查网络后重试', key: 'batchExport' });
+ }
+ };
+
+ const renderOcrConfirmFields = (certType, group) => {
+ if (isBatchUploadPhotoOnlyType(certType)) return null;
+ const f = group?.fields || {};
+ const fieldDisabled = !group?.plateValid;
+ if (certType === 'driverLicense') {
+ return (
+
+
注册日期} required>
+ updateOcrConfirmField('regDate', ds)}
+ style={{ width: '100%' }}
+ />
+
+ 发证日期} required>
+ updateOcrConfirmField('issueDate', ds)}
+ style={{ width: '100%' }}
+ />
+
+ 强制报废日期}>
+ updateOcrConfirmField('scrapDate', ds)}
+ style={{ width: '100%' }}
+ />
+
+ 检验有效期至} required>
+ updateOcrConfirmField('expireDate', ds)}
+ style={{ width: '100%' }}
+ />
+
+
+ );
+ }
+ if (certType === 'transportLicense') {
+ return (
+
+
经营许可证号} required>
+ updateOcrConfirmField('licenseNo', e.target.value)}
+ placeholder="例如:交字31011..."
+ />
+
+ 核发时间} required>
+ updateOcrConfirmField('issueDate', ds)}
+ style={{ width: '100%' }}
+ />
+
+ 证件有效期} required>
+ updateOcrConfirmField('expireDate', ds)}
+ style={{ width: '100%' }}
+ />
+
+ 审验有效期} required>
+ updateOcrConfirmField('inspectValidUntil', ds)}
+ style={{ width: '100%' }}
+ />
+
+
+ );
+ }
+ return (
+
+
下次检验日期} required>
+ updateOcrConfirmField('nextInspectDate', ds)}
+ style={{ width: '100%' }}
+ placeholder="选择检验截止日期"
+ />
+
+
+ );
+ };
+
+ const batchOcrTaskColumns = [
+ { title: '操作人', dataIndex: 'operator', width: 88 },
+ { title: '操作时间', dataIndex: 'operateTime', width: 168 },
+ { title: '证照类型', dataIndex: 'certLabel', width: 140 },
+ { title: '照片数量', dataIndex: 'photoCount', width: 88, align: 'center' },
+ {
+ title: 'OCR结果',
+ key: 'ocrStats',
+ width: 128,
+ render: (_, record) => {
+ if (record.status !== 'done') return —;
+ return (
+
+ 成功 {record.ocrSuccessCount ?? 0}
+ 失败 {record.ocrFailCount ?? 0}
+
+ );
+ }
+ },
+ {
+ title: '处理进度',
+ dataIndex: 'progress',
+ width: 160,
+ render: (val, record) => (
+
+ )
+ },
+ {
+ title: '操作',
+ key: 'action',
+ width: 88,
+ render: (_, record) => (
+
+ )
+ }
+ ];
+
+ const filterVehiclesByFilters = (vehicles, f, kpi) => {
+ const plateKey = (f.plateNo || '').trim().toLowerCase();
+ const multiPlates = parseMultiPlates(f.plateNos);
+ const vinKey = (f.vin || '').trim().toLowerCase();
+ const brandKey = (f.brand || '').trim().toLowerCase();
+ const modelKey = (f.model || '').trim().toLowerCase();
+
+ return vehicles.filter((v) => {
+ const plateUpper = v.plateNo.toUpperCase();
+ if (multiPlates.length) {
+ if (!multiPlates.includes(plateUpper)) return false;
+ } else if (plateKey && !v.plateNo.toLowerCase().includes(plateKey)) return false;
+ if (vinKey && !v.vin.toLowerCase().includes(vinKey)) return false;
+ if (brandKey && !v.brand.toLowerCase().includes(brandKey)) return false;
+ if (modelKey && !v.model.toLowerCase().includes(modelKey)) return false;
+ if (f.operateStatus !== '全部' && v.status !== f.operateStatus) return false;
+ if (!matchKpiFilter(v.plateNo, kpi)) return false;
+ return true;
+ });
+ };
+
+ const handleListFilterQuery = () => {
+ const plates = parseMultiPlates(multiPlateDraft);
+ const next = {
+ ...listFilters,
+ plateNos: multiPlateDraft.trim(),
+ plateNo: plates.length ? '' : listFilters.plateNo,
+ };
+ setListFilters(next);
+ setAppliedFilters(next);
+ setMultiPlateOpen(false);
+ const hitCount = filterVehiclesByFilters(MOCK_VEHICLES, next, kpiFilter).length;
+ if (plates.length) {
+ message.success(`已按 ${plates.length} 个车牌筛选,命中 ${hitCount} 条记录`);
+ } else {
+ message.success(`已按筛选条件更新列表,共 ${hitCount} 条记录`);
+ }
+ };
+
+ const handleListFilterReset = () => {
+ const next = { ...DEFAULT_LIST_FILTERS };
+ setListFilters(next);
+ setAppliedFilters(next);
+ setMultiPlateDraft('');
+ setMultiPlateOpen(false);
+ message.info('已重置筛选条件');
+ };
+
+ const appliedMultiPlates = useMemo(
+ () => parseMultiPlates(appliedFilters.plateNos),
+ [appliedFilters.plateNos]
+ );
+
+ const multiPlateTriggerText = useMemo(() => {
+ if (!appliedMultiPlates.length) return '';
+ if (appliedMultiPlates.length <= 2) return appliedMultiPlates.join('、');
+ return `已选 ${appliedMultiPlates.length} 个车牌`;
+ }, [appliedMultiPlates]);
+
+ const handleMultiPlateOpenChange = (open) => {
+ setMultiPlateOpen(open);
+ if (open) setMultiPlateDraft(listFilters.plateNos || '');
+ };
+
+ const handleMultiPlateDraftClear = () => {
+ setMultiPlateDraft('');
+ setListFilters((prev) => ({ ...prev, plateNos: '' }));
+ };
+
+ const renderFilterField = (label, control) => (
+
+ );
+
+ // ==================== 车辆列表筛选逻辑 ====================
+ const filteredVehicles = useMemo(
+ () => sortVehiclesRetiredLast(filterVehiclesByFilters(MOCK_VEHICLES, appliedFilters, kpiFilter)),
+ [appliedFilters, allLicenses, kpiFilter]
+ );
+
+ // ==================== 列表台账表格列配置 ====================
+ const listColumns = [
+ {
+ title: '车牌号',
+ dataIndex: 'plateNo',
+ key: 'plateNo',
+ width: 96,
+ onHeaderCell: listColumnHeaderCell,
+ render: (plate, record) => (
+
+ {plate}
+
+ )
+ },
+ {
+ title: tableTitleMultiline('车辆识别代码', '(VIN码)'),
+ dataIndex: 'vin',
+ key: 'vin',
+ width: 112,
+ onHeaderCell: listColumnHeaderCell,
+ render: (vin, record) => (
+
+
+ {vin}
+
+
+ )
+ },
+ {
+ title: tableTitleMultiline('运营', '状态'),
+ dataIndex: 'status',
+ key: 'status',
+ width: 72,
+ onHeaderCell: listColumnHeaderCell,
+ render: (status) => (
+
+ {status}
+
+ )
+ },
+ {
+ title: '品牌',
+ dataIndex: 'brand',
+ key: 'brand',
+ width: 72,
+ onHeaderCell: listColumnHeaderCell,
+ render: (brand, record) => (
+
+ {brand}
+
+ )
+ },
+ {
+ title: '型号',
+ dataIndex: 'model',
+ key: 'model',
+ width: 108,
+ onHeaderCell: listColumnHeaderCell,
+ render: (model, record) => (
+
+
+ {model}
+
+
+ )
+ },
+ {
+ title: tableTitleMultiline('行驶证', '到期时间'),
+ key: 'driverLicense',
+ width: 92,
+ onHeaderCell: listColumnHeaderCell,
+ render: (record) => {
+ const status = getLicenseStatus(record.plateNo, 'driverLicense');
+ const dateVal = allLicenses[record.plateNo]?.driverLicense?.expireDate;
+ const muted = isRetiredVehicle(record);
+
+ return (
+
+
+ {dateVal || '—'}
+
+
+ {renderListLicenseStatusBadge(status)}
+
+
+ );
+ }
+ },
+ {
+ title: tableTitleMultiline('道路运输证', '证件有效期'),
+ key: 'transportLicenseExpire',
+ width: 92,
+ onHeaderCell: listColumnHeaderCell,
+ render: (record) => {
+ const status = getLicenseStatus(record.plateNo, 'transportLicense', { dateField: 'expireDate' });
+ const dateVal = allLicenses[record.plateNo]?.transportLicense?.expireDate;
+ const muted = isRetiredVehicle(record);
+
+ return (
+
+
+ {dateVal || '—'}
+
+
+ {renderListLicenseStatusBadge(status)}
+
+
+ );
+ }
+ },
+ {
+ title: tableTitleMultiline('道路运输证', '审验有效期'),
+ key: 'transportLicenseInspect',
+ width: 92,
+ onHeaderCell: listColumnHeaderCell,
+ render: (record) => {
+ const status = getLicenseStatus(record.plateNo, 'transportLicense', { dateField: 'inspectValidUntil' });
+ const dateVal = allLicenses[record.plateNo]?.transportLicense?.inspectValidUntil;
+ const muted = isRetiredVehicle(record);
+
+ return (
+
+
+ {dateVal || '—'}
+
+
+ {renderListLicenseStatusBadge(status)}
+
+
+ );
+ }
+ },
+ {
+ title: tableTitleMultiline('特种设备标识', '到期时间'),
+ key: 'specialEquipDecal',
+ width: 92,
+ onHeaderCell: listColumnHeaderCell,
+ render: (record) => {
+ const status = getLicenseStatus(record.plateNo, 'specialEquipDecal');
+ const dateVal = allLicenses[record.plateNo]?.specialEquipDecal?.nextInspectDate;
+ const muted = isRetiredVehicle(record);
+
+ return (
+
+
+ {dateVal || '—'}
+
+
+ {renderListLicenseStatusBadge(status)}
+
+
+ );
+ }
+ },
+ {
+ title: tableTitleMultiline('证件', '状态'),
+ key: 'allCertStatus',
+ width: 272,
+ onHeaderCell: listColumnHeaderCell,
+ render: (record) => {
+ const muted = isRetiredVehicle(record);
+ const labelColor = muted ? '#94a3b8' : '#64748b';
+
+ return (
+
+ {LIST_CERT_STATUS_ITEMS.map((item) => {
+ const st = getListCertStatus(record.plateNo, item);
+ return (
+
+
+ {item.label}}
+ />
+
+
+ );
+ })}
+
+ );
+ }
+ },
+ {
+ title: '操作',
+ key: 'action',
+ width: 64,
+ onHeaderCell: listColumnHeaderCell,
+ render: (record) => (
+
+ )
+ }
+ ];
+
+ return (
+
+
+
+ {/* ======================================================== */}
+ {/* ======================= 1. 列表台账视图 =================== */}
+ {/* ======================================================== */}
+
+
+ {/* 顶栏 */}
+
+
+
+
+ {/* 筛选条件 */}
+
+
+ {renderFilterField('车牌号', (
+
0}
+ value={listFilters.plateNo}
+ onChange={e => setListFilters(prev => ({ ...prev, plateNo: e.target.value }))}
+ onPressEnter={handleListFilterQuery}
+ style={{ borderRadius: 8 }}
+ />
+ ))}
+ {renderFilterField('多车牌', (
+
+
+ 每行一个车牌号,可从 Excel 等批量复制粘贴;点击「查询」后列表展示全部命中车辆。
+
+ setMultiPlateDraft(e.target.value)}
+ placeholder={'沪A03561F\n粤B58888F\n苏E33333'}
+ autoSize={{ minRows: 5, maxRows: 10 }}
+ style={{ borderRadius: 8, fontFamily: 'monospace', fontSize: 13 }}
+ />
+
+
+
+
+
+ }
+ >
+ setMultiPlateOpen(true)}
+ onClear={(e) => {
+ e.stopPropagation();
+ handleMultiPlateDraftClear();
+ setAppliedFilters((prev) => ({ ...prev, plateNos: '' }));
+ }}
+ style={{ borderRadius: 8 }}
+ suffix={
+
+ }
+ />
+
+ ))}
+ {renderFilterField('VIN码', (
+ setListFilters(prev => ({ ...prev, vin: e.target.value }))}
+ onPressEnter={handleListFilterQuery}
+ style={{ borderRadius: 8 }}
+ />
+ ))}
+ {renderFilterField('品牌', (
+
+
+
+
+
+
+
+ {/* 资质预警看板(筛选与列表之间) */}
+
+ {[
+ {
+ 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: '行驶证≤90天 / 道路运输证≤60天 / 特种设备标识≤60天 / 安全阀≤60天 / 压力表≤60天',
+ 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 => (
+
handleKpiCardClick(card.key)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleKpiCardClick(card.key);
+ }
+ }}
+ >
+
+
+ e.stopPropagation()}
+ onMouseDown={(e) => e.stopPropagation()}
+ >
+
+
+
+
+
{card.icon}
+
+
{card.val}
+
{card.title}
+
+
+ ))}
+
+
+ {/* 表格台账区域:图示左上,批量操作右上 */}
+
+
+
+ 证件状态图示
+ {CERT_STATUS_LEGEND}
+
+
+
+
+
+
+
+
(record.status === '退出运营' ? 'lc-row-retired' : '')}
+ pagination={false}
+ locale={{ emptyText: }}
+ />
+
+
+
+
+ {/* ======================================================== */}
+ {/* ======================= 2. 批量导出 / 批量上传 ============== */}
+ {/* ======================================================== */}
+
+ setBatchExportOpen(false)}
+ onOk={handleBatchExport}
+ >
+
+ {BATCH_EXPORT_RULE_LINES.map((line) => (
+ {line}
+ ))}
+
+ }
+ />
+
+ {CERT_EXPORT_OPTIONS.map((opt) => (
+ {opt.label}
+ ))}
+
+
+ 当前筛选命中 {filteredVehicles.length} 辆车
+
+
+
+ setBatchOcrOpen(false)}
+ destroyOnClose={false}
+ >
+
+ 新建上传任务
+
+ 证照类型
+ ({ label: o.label, value: o.key }))}
+ />
+
+
+ 登记证、特种设备使用登记证:识别车牌通过后仅需确认同步照片;行驶证、道路运输证、特种设备使用标识需核对识别字段。
+
+ false}
+ onChange={({ fileList }) => setBatchOcrFileList(fileList)}
+ >
+ {batchOcrFileList.length < 20 && (
+
+ )}
+
+
+
+
+
+ 上传任务列表
+
+
+
+
+
+ {currentOcrConfirmGroup && !currentOcrConfirmGroup.plateValid ? (
+
+ ) : null}
+
+
+ }
+ onCancel={closeOcrConfirm}
+ >
+ {currentOcrConfirmGroup && (
+
+
+
+
逐张确认进度
+
+ 当前 {ocrConfirmGroupIdx + 1} / {ocrConfirmTotalSheets} 张
+
+
+
+ {currentOcrConfirmGroup.plateValid
+ ? (isBatchUploadPhotoOnlyType(ocrConfirmTask?.certType) ? '可同步照片' : '可提交更新')
+ : '车牌校验失败'}
+
+
+
+
+
+ 本车牌上传照片(共 {currentOcrConfirmGroup.items.length} 张)
+
+
+ {currentOcrConfirmGroup.items.map((it, idx) => (
+
setOcrConfirmPhotoIdx(idx)}
+ title={it.photoName || `照片${idx + 1}`}
+ >
+

+
+ ))}
+
+
{
+ const url = currentOcrConfirmGroup.items[ocrConfirmPhotoIdx]?.photoUrl;
+ if (url) { setPreviewUrl(url); setPreviewOpen(true); }
+ }}
+ >
+

+
+
+ 预览第 {ocrConfirmPhotoIdx + 1} 张 · 点击大图可放大
+
+
+
+ {isBatchUploadPhotoOnlyType(ocrConfirmTask?.certType) ? (
+ <>
+ {renderOcrPlateValidation(currentOcrConfirmGroup)}
+
+ >
+ ) : (
+ <>
+
+ 识别字段(可编辑,提交后写入该车辆证照)
+
+ {renderOcrPlateValidation(currentOcrConfirmGroup)}
+
+ >
+ )}
+
+
+
+ )}
+
+
+ {/* ======================================================== */}
+ {/* ======================= 4. 公共大图/PRD 弹窗 ============== */}
+ {/* ======================================================== */}
+
+ {/* 照片预览大图弹窗 */}
+ setPreviewOpen(false)}
+ width={720}
+ centered
+ >
+
+
+
+ {/* 需求说明弹窗(产品经理视角 PRD) */}
+
+ 📋
+ 车辆证照管理 · 产品需求说明(PRD)
+
+ }
+ footer={[
+
+ ]}
+ onCancel={() => setPrdOpen(false)}
+ width={980}
+ centered
+ style={{ top: 20 }}
+ bodyStyle={{ maxHeight: '72vh', overflowY: 'auto', padding: '12px 24px 24px' }}
+ >
+
+
+ 本页:证照台账列表(监管台)
+ 模块路径:运维管理 > 车辆业务 > 证照管理
+ 文档版本:V1.1
+ 读者:产品 / 运营 / 合规 / 研发测试
+
+
+
本文档说明「证照管理」列表页能力}
+ description="单车证照录入、OCR、分卡保存等在独立页面「证照管理-编辑」;两页通过 session 车牌 + 本地证照库同步。维护页需求见其页面内「查看需求说明」。"
+ />
+
+
+
+
+
+
产品定位(本页)
+
+ 监管台:对全量车辆证照进行检索、预警、批量导出/上传、进入单车维护,不承担逐车表单编辑。
+
+
+ 价值:让合规专员「先看见风险车辆 → 再处理单车」,批量作业与台账维护分工清晰。
+
+
+
+
端到端主流程
+
+ - 进入列表 → 默认展示全量监管车辆(退出运营置底、行置灰)
+ - 筛选 / 点击看板 → 缩小待处理范围(看板计数不随筛选变,见规则②)
+ - 行内读数 → 到期列 +「证件状态」八类状态点(双行四列)
+ - 单车维护 →「管理」进入「证照管理-编辑」,分卡保存后回写列表与看板
+ - 批量作业 → 按当前筛选结果导出 ZIP,或批量上传 OCR 任务确认入库
+
+
+
【重点】五条必读业务规则
+
+
+ ① 运营状态归并
+ — 主数据「可运营」「待运营」在本模块统一为「库存」;筛选「库存」须命中上述车辆;列表与维护页一致。
+
+
+ ② 看板 vs 筛选
+ — 看板按全量台账统计,不随筛选变化;点击看板仅过滤列表,可与筛选叠加。
+
+
+ ③ 临期 / 逾期口径
+ — 行驶证 ≤90 天临期;运输证证件有效期、审验有效期分列展示、分别算状态;特设标 / 安全阀 / 压力表 ≤60 天临期;≤0 天为已到期。
+
+
+ ④ 批量上传
+ — OCR 车牌须在台账;按车牌聚合确认;登记证 / 特种设备使用登记证仅同步照片。
+
+
+ ⑤ 数据同步
+ — 「管理」写 session 车牌;维护页保存、批量确认写回证照库,列表 KPI 与行状态联动更新。
+
+
+
+
看板五项指标(点击可筛列表,口径以 ⓘ 为准)
+
+
+
+ | 指标 |
+ 统计逻辑 |
+
+
+
+ | 监管车辆总数 | 全部台账车辆;点击恢复全量 |
+ | 资质全部正常 | 行驶证 + 运输证(证件有效期、审验有效期)均在有效期内 |
+ | 证件临期预警 | 行驶证 90 天内;运输证/特设标/安全阀/压力表 60 天内(运输证两日期任一即计入) |
+ | 已逾期 | 行驶证到期;运输证证件或审验到期;安全阀/压力表下次检验到期 |
+ | 证照待补录 | 四类核心证照任一未上传:行驶证、道路运输证、特种设备使用登记证、特种设备使用标识 |
+
+
+
+
+
+
+
+
1. 页面结构(自上而下)
+
+ - 右上角「查看需求说明」(本文档)
+ - 筛选条件:三列网格;「查询」生效、「重置」清空
+ - 资质预警看板:五项 KPI,ⓘ 看口径,点击筛列表
+ - 证照台账表格:左上图例、右上批量导出/上传
+
+
+
2. 筛选逻辑
+
+
+
+ | 筛选项 |
+ 规则 |
+
+
+
+ | 车牌号 | 模糊匹配;启用多车牌时禁用 |
+ | 多车牌 | 点击输入框展开文本域,每行一个车牌(支持 Excel 粘贴);点「查询」后精确匹配并展示全部命中,提示命中条数 |
+ | VIN码 | 模糊匹配 |
+ | 品牌 / 车型 | 可搜索下拉,可清空 |
+ | 运营状态 | 全部 / 租赁 / 自营 / 库存 / 退出运营;选库存含主数据可运营、待运营 |
+
+
+
筛选与看板点击可叠加;修改筛选项后须点「查询」才生效。
+
+
3. 列表字段与交互
+
+
+
+ | 列 / 能力 |
+ 说明 |
+
+
+
+ | 基础信息 | 车牌、VIN、运营状态、品牌、型号;表头过长字段支持两行展示 |
+ | 三类到期列 | 行驶证到期;运输证证件有效期与审验有效期分列;特种设备标识到期;日期下 Badge 单行展示(临期N天/逾期N天),悬停看全文 |
+ | 证件状态 | 八类证照双行四列:行驶/运输/登记/特种 · 特设标/加氢/安全阀/压力表;绿正常、橙临期、红到期、灰未上传(加氢:已绑定/未绑定);运输证状态取两日期最严重 |
+ | 管理 | 跳转「证照管理-编辑」;无删除 |
+ | 退出运营 | 行置灰,同筛选结果内固定排底部,仍可点管理 |
+
+
+
+
4. 批量导出证照
+
入口:列表右上方。范围:当前筛选命中车辆(含看板筛选)。类型:7 类影像(不含加氢卡)。
+
+ {BATCH_EXPORT_RULE_LINES.map((line) => (
+ - {line}
+ ))}
+
+
+ 证照批量导出_时间戳.zip → 按类型分文件夹 → 沪A03561F.jpg / 粤B58888F-1.jpg …
+
+
+
5. 批量上传证照
+
入口:列表右上方。一次任务仅选一种证照类型。
+
+
+
+ | 阶段 |
+ 逻辑要点 |
+
+
+
+ | 建任务 | 多图上传 → 生成任务 → 列表展示 OCR 成功/失败条数、处理进度 |
+ | 逐张确认 | 按车牌聚合;页头「当前 M/总 N 张」为车牌数;同页展示该车牌全部照片 |
+ | 仅照片类 | 登记证、特种设备使用登记证:车牌通过 →「提交并同步照片」 |
+ | 字段类 | 行驶证、道路运输证、特种设备使用标识:可编辑识别字段 →「提交并更新」 |
+ | 异常 | 车牌不在台账 → 失败,可「跳过本张」;全部确认后任务显示「已确认」 |
+
+
+
+
+
+
+
+
+
+ - 进入:列表「管理」→ session 写入车牌 → 打开编辑页(支持 Axhub 导航)
+ - 布局:左车辆信息 + 八类证照索引(状态点);右八类卡片锚点滚动
+ - 保存:分卡片「保存该项」,非整页提交;未保存切换车辆二次确认
+ - 安全:行驶证/运输证 OCR 车牌须与当前车一致,否则阻断并清空无效图
+ - 回写:保存后更新证照库,返回列表后看板与行状态刷新
+
+
八类证照字段、照片生命周期、沪牌等级评定等详见「证照管理-编辑」页内 PRD。
+
+
+
+
+
+
+
运营状态归并(接口必读)
+
+
+
+ | 主数据 |
+ 本模块展示 |
+
+
+
+ | 租赁 / 自营 / 退出运营 | 同左 |
+ | 可运营、待运营 | 库存 |
+
+
+
+
+
到期感知阈值
+
+ - 行驶证:≤90 天临期,≤0 天到期
+ - 道路运输证:证件有效期、审验有效期各算,≤60 天临期
+ - 特种设备标识 / 安全阀 / 压力表:下次检验 ≤60 天临期
+ - 登记类、特种设备使用登记证:有图无日期视为正常(列表状态点)
+
+
+
+
批量与筛选
+
批量导出/上传均基于当前列表筛选结果(含 KPI 点击后的列表范围),与看板全量统计独立。
+
+
+
+
+
+
+
列表页验收清单
+
+ - 可运营/待运营展示为库存;筛库存可命中
+ - 多车牌:每行一车、查询后精确匹配并提示条数
+ - 看板全量统计;点击看板仅筛列表;ⓘ 展示口径
+ - 八类证件状态双行展示;到期 Badge 不换行
+ - 批量导出 7 类 ZIP 命名规则;批量上传 5 类及确认流
+ - 管理跳转编辑页;保存后列表与 KPI 一致
+ - 退出运营置底置灰;无删除
+
+
本期不做
+
+ - 列表页内嵌单车维护(已拆至编辑页)
+ - 台账车辆删除、批量导入台账
+ - 年审状态作列表筛选项
+ - 加氢卡页面内编辑(能源模块回写)
+
+
+
+
+
+
+
+ );
+};
+
+export default Component;