// 【重要】必须使用 const Component 作为组件变量名 // 运维管理 - 车辆业务 - 年审管理 · 办理(分组表单页,逻辑对齐 ONE-OS 小程序) const { useState, useMemo, useEffect, useRef } = React; const moment = window.moment || window.dayjs; const antd = window.antd; const { App, Alert, Button, Col, DatePicker, Form, Input, InputNumber, Modal, Row, Select, Spin, Tag, Typography, Upload, message, } = antd; const Image = antd.Image; const { Text, Paragraph } = Typography; const TextArea = Input.TextArea; const AR_DRAFT_STORAGE_KEY = 'oneos_ar_operate_drafts_v1'; const AR_TASK_ID_STORAGE_KEY = 'oneos_ar_operate_task_id'; const AR_TASKS_STORAGE_KEY = 'oneos_ar_web_tasks_v1'; const AR_NAV_TARGET_KEY = 'oneos_ar_navigate_target'; const AR_NAV_EVENT = 'oneos-ar-return-list'; const CERTIFICATE_LICENSE_SYNC_KEY = 'oneos_certificate_license_sync'; /** 办理页保存/提交后返回列表(Axhub 多文件原型:事件 + session 标记) */ const navigateToAnnualReviewList = (toastMsg) => { try { sessionStorage.setItem(AR_NAV_TARGET_KEY, 'list'); sessionStorage.removeItem(AR_TASK_ID_STORAGE_KEY); } catch { /* ignore */ } try { window.dispatchEvent(new CustomEvent(AR_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 INSPECTION_STATION_LIST = ['汇通检测站', '平湖检测站', '嘉兴检测站', '松江检测站']; const REPAIR_STATION_LIST = [ '杭州拱墅维修站', '广州天河维修站', '上海浦东维修站', '深圳南山维修站', '天津港保税区维修站', ]; const INSPECTION_STATION_STORAGE_KEY = 'annual-review.station.inspection'; const MAX_SERVICE_PHOTOS = 10; const MAX_LICENSE_PHOTOS = 4; const LICENSE_OCR_MOCK_MS = 1500; const MOCK_CURRENT_HANDLER = '张明辉'; const EMPTY_SERVICE_FORM = { station: '', cost: '', remark: '', photos: [] }; const EMPTY_INSPECTION_FORM = { station: '', cost: '', remark: '' }; const EMPTY_LICENSE_FORM = { photos: [], inspectionValidUntil: null, ocrStatus: 'idle' }; const mapOperateStatus = (raw) => { if (raw === '可运营' || raw === '待运营') return '库存'; return raw || '—'; }; const MOCK_TASKS = [ { id: 'ar-1', plateNo: '粤B58888F', vin: 'LGHXCAE28M6789012', brand: '福田', model: '奥铃4.5吨冷藏车', operateStatusRaw: '租赁', expireDate: '2026-07-20', daysLeft: 49, tab: 'pending', province: '广东省', city: '深圳市', executor: '', executeTime: '', }, { id: 'ar-2', plateNo: '沪A03561F', vin: 'LMRKH9AC0R1004086', brand: '宇通', model: '49吨牵引车头', operateStatusRaw: '自营', expireDate: '2026-07-31', daysLeft: 60, tab: 'pending', province: '上海市', city: '上海市', executor: '', executeTime: '', }, { id: 'ar-3', plateNo: '苏E33333', vin: 'LSXCH9AE8M1094857', brand: '陕汽', model: '德龙X3000混动牵引车', operateStatusRaw: '可运营', expireDate: '2026-05-15', daysLeft: -17, tab: 'pending', province: '江苏省', city: '苏州市', executor: '', executeTime: '', }, { id: 'ar-7', plateNo: '鲁Q88901', vin: 'LZZ5CLSB8NC778899', brand: '重汽', model: '豪沃T7H牵引车', operateStatusRaw: '租赁', expireDate: '2026-04-10', daysLeft: -52, tab: 'pending', province: '山东省', city: '临沂市', executor: '', executeTime: '', }, { id: 'ar-8', plateNo: '闽D55662', vin: 'LFWNHXSD8P1122334', brand: '金龙', model: '凯歌纯电动厢货', operateStatusRaw: '自营', expireDate: '2026-04-27', daysLeft: -35, tab: 'pending', province: '福建省', city: '厦门市', executor: '', executeTime: '', }, { id: 'ar-4', plateNo: '浙A88888', vin: 'LMRKH9AE2P9876543', brand: '宇通', model: '氢燃料电池大巴', operateStatusRaw: '待运营', expireDate: '2026-08-10', daysLeft: 70, tab: 'pending', province: '浙江省', city: '杭州市', executor: '', executeTime: '', }, { id: 'ar-6', plateNo: '皖B66221', vin: 'LZZ5CLSB8NA123456', brand: '江淮', model: '格尔发A5', operateStatusRaw: '库存', expireDate: '2026-06-28', daysLeft: 27, tab: 'pending', province: '安徽省', city: '合肥市', executor: '', executeTime: '', }, ].map((t) => ({ ...t, operateStatus: mapOperateStatus(t.operateStatusRaw), })); const isFormCostEmpty = (cost) => cost === '' || cost == null; const validateStationCost = (form, stationLabel) => { if (form?.station && isFormCostEmpty(form.cost)) { message.error(`请填写${stationLabel}费用`); return false; } return true; }; const validateLicenseForm = (licenseForm) => { if (!(licenseForm?.photos || []).length) { message.error('请上传行驶证照片'); return false; } if (!licenseForm?.inspectionValidUntil) { message.error('请填写检验有效期'); return false; } return true; }; const formatTaskRegion = (task) => { if (!task?.province) return '—'; if (task.city) return `${task.province}-${task.city}`; return task.province; }; const formatOperateStatusDisplay = (task) => { const base = task.operateStatus || '—'; if (base === '库存' && (task.operateStatusRaw === '可运营' || task.operateStatusRaw === '待运营')) { return `库存(${task.operateStatusRaw})`; } return base; }; const readInspectionStationList = () => { try { const raw = localStorage.getItem(INSPECTION_STATION_STORAGE_KEY); const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.length) return parsed; } catch { /* ignore */ } return [...INSPECTION_STATION_LIST]; }; const loadTasksFromStorage = () => { try { const raw = localStorage.getItem(AR_TASKS_STORAGE_KEY); if (!raw) return MOCK_TASKS; const parsed = JSON.parse(raw); return Array.isArray(parsed) && parsed.length ? parsed : MOCK_TASKS; } catch { return MOCK_TASKS; } }; const persistTasksToStorage = (tasks) => { try { localStorage.setItem(AR_TASKS_STORAGE_KEY, JSON.stringify(tasks)); } catch { /* ignore */ } }; const loadOperateDrafts = () => { try { const raw = localStorage.getItem(AR_DRAFT_STORAGE_KEY); if (!raw) return {}; const parsed = JSON.parse(raw); return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } }; const persistOperateDrafts = (drafts) => { try { localStorage.setItem(AR_DRAFT_STORAGE_KEY, JSON.stringify(drafts)); } catch { /* ignore */ } }; const getUploadPreviewUrl = (file) => { if (!file) return ''; if (file.url) return file.url; if (file.thumbUrl) return file.thumbUrl; if (file.originFileObj && typeof URL !== 'undefined' && URL.createObjectURL) { return URL.createObjectURL(file.originFileObj); } return ''; }; const serializeUploadFileList = (list) => (list || []).map((f) => ({ uid: f.uid, name: f.name, url: getUploadPreviewUrl(f) || '', status: f.status || 'done', })); const deserializeUploadFileList = (list) => (list || []).map((f) => ({ uid: f.uid || `file-${f.name}-${Math.random().toString(36).slice(2, 8)}`, name: f.name, url: f.url, thumbUrl: f.url, status: f.status || 'done', })); const syncLicenseToCertificateManagement = (task, licenseForm, operator) => { if (!task?.plateNo) return null; const photos = serializeUploadFileList(licenseForm?.photos || []); const payload = { plateNo: task.plateNo, vin: task.vin || '', inspectionValidUntil: licenseForm?.inspectionValidUntil || '', photos, photoCount: photos.length, operator: operator || MOCK_CURRENT_HANDLER, source: 'annual_review', syncedAt: new Date().toISOString(), }; try { const store = JSON.parse(localStorage.getItem(CERTIFICATE_LICENSE_SYNC_KEY) || '{}'); store[task.plateNo] = payload; localStorage.setItem(CERTIFICATE_LICENSE_SYNC_KEY, JSON.stringify(store)); } catch { /* ignore */ } return payload; }; const platesMatch = (a, b) => { const na = (a || '').replace(/\s/g, '').toUpperCase(); const nb = (b || '').replace(/\s/g, '').toUpperCase(); return na.length > 0 && na === nb; }; const getUploadFileName = (file) => String(file?.name || file?.originFileObj?.name || '').toLowerCase(); /** 模拟单张行驶证 OCR 车牌(文件名含 mismatch / 不一致 可测失败) */ const mockOcrLicensePlateForFile = (task, file) => { const name = getUploadFileName(file); if (name.includes('mismatch') || name.includes('不一致')) return '粤B00000D'; return task?.plateNo || ''; }; /** 模拟单张是否识别到检验有效期至(无有效期:文件名含 novalid / 无有效期) */ const mockOcrLicenseValidUntilForFile = (task, file, index) => { const name = getUploadFileName(file); if (name.includes('novalid') || name.includes('无有效期')) return null; const base = task?.expireDate || '2026-07-20'; if (moment) { return moment(base).add(index, 'month').endOf('month').format('YYYY-MM-DD'); } const parts = String(base).split('-'); const year = Number(parts[0]) || new Date().getFullYear(); const month = (Number(parts[1]) || 1) + index; const day = new Date(year, month, 0).getDate(); return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; }; /** 批量 OCR:逐张校验车牌;多张均有有效期时取最后识别的一条 */ const runLicenseOcrResult = (task, photos) => { const list = photos || []; const expectedPlate = task?.plateNo || ''; for (let i = 0; i < list.length; i++) { const recognizedPlate = mockOcrLicensePlateForFile(task, list[i]); if (!platesMatch(recognizedPlate, expectedPlate)) { return { ok: false }; } } let lastValidUntil = null; list.forEach((file, index) => { const validUntil = mockOcrLicenseValidUntilForFile(task, file, index); if (validUntil) lastValidUntil = validUntil; }); return { ok: true, validUntil: lastValidUntil }; }; const readInitialTaskId = () => { try { return sessionStorage.getItem(AR_TASK_ID_STORAGE_KEY) || ''; } catch { return ''; } }; const PAGE_STYLES = ` .ar-handle{min-height:100vh;background:#f2f3f5;font-family:Inter,Helvetica,PingFang SC,Microsoft YaHei,Arial,sans-serif} .ar-handle-inner{max-width:1200px;margin:0 auto;padding:16px 24px 96px} .ar-handle-top{display:flex;justify-content:flex-end;align-items:center;gap:12px;margin-bottom:12px;min-height:32px} .ar-handle-top-extra{margin-right:auto} .ar-form-group{background:#fff;border:1px solid #e5e6eb;border-radius:4px;margin-bottom:16px} .ar-form-group-head{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #f2f3f5} .ar-form-group-title{font-size:14px;font-weight:600;color:#1d2129} .ar-form-group-body{padding:20px 20px 8px} .ar-form-group-body .ant-form-item{margin-bottom:20px} .ar-form-group-body .ant-form-item-label>label{color:#4e5969;font-size:13px} .ar-form-label-hint{font-size:12px;font-weight:400;color:#86909c;margin-left:8px} .ar-form-footer{position:fixed;left:0;right:0;bottom:0;z-index:100;background:#fff;border-top:1px solid #e5e6eb;box-shadow:0 -2px 8px rgba(0,0,0,.06)} .ar-form-footer-inner{max-width:1200px;margin:0 auto;padding:12px 24px;display:flex;justify-content:flex-end;gap:12px} .ar-ocr-banner{padding:10px 12px;border-radius:4px;font-size:13px;margin-bottom:16px} .ar-ocr-banner--error{background:#ffece8;border:1px solid #ffccc7;color:#cb2634} .ar-ocr-banner--done{background:#e8ffea;border:1px solid #aff0b5;color:#009a29} .ar-ocr-mask{position:fixed;inset:0;z-index:200;background:rgba(29,33,41,.45);display:flex;align-items:center;justify-content:center} .ar-ocr-mask-panel{background:#fff;border-radius:8px;padding:32px 48px;text-align:center} .ar-prd-doc{max-height:65vh;overflow-y:auto;font-size:13px;line-height:1.65;color:#4e5969} .ar-prd-highlight{background:#f0f9ff;border:1px solid #bedaff;border-radius:4px;padding:12px;margin:12px 0} .ar-prd-h2{font-size:15px;font-weight:600;color:#1d2129;margin:16px 0 8px} .ar-prd-h3{font-size:14px;font-weight:600;color:#1d2129;margin:12px 0 6px} .ar-prd-ul{margin:0;padding-left:20px} .ar-prd-ul li{margin-bottom:6px} .ar-photo-block{width:100%} .ar-photo-row{display:flex;align-items:flex-start;gap:12px} .ar-photo-upload-fixed{flex-shrink:0;width:104px} .ar-photo-upload-cell{display:block;width:100%} .ar-photo-upload-cell .ant-upload{display:block;width:100%} .ar-photo-upload-cell .ant-upload-select{display:block!important;width:100%!important;height:auto!important;margin:0!important;border:none!important;background:transparent!important} .ar-photo-list-scroll{flex:1;min-width:0;display:flex;flex-wrap:wrap;gap:10px;align-content:flex-start} .ar-photo-slot{position:relative;width:104px;height:104px;border-radius:8px;box-sizing:border-box;flex-shrink:0} .ar-photo-slot--add{display:flex;flex-direction:column;align-items:center;justify-content:center;border:1px dashed #c9cdd4;background:#fafafa;color:#86909c;cursor:pointer;gap:2px;transition:border-color .2s,background .2s} .ar-photo-slot--add:hover{border-color:#165dff;background:#f0f5ff;color:#165dff} .ar-photo-add-icon{font-size:22px;line-height:1;font-weight:300} .ar-photo-add-text{font-size:12px;line-height:1.2} .ar-photo-item{overflow:visible} .ar-photo-item-thumb{width:100%;height:100%;border-radius:8px;overflow:hidden;border:none;padding:0;background:#f2f3f5;cursor:pointer;display:block} .ar-photo-item-thumb img{width:100%;height:100%;object-fit:cover;display:block} .ar-photo-del{position:absolute;top:-6px;right:-6px;z-index:2;width:20px;height:20px;border-radius:50%;border:none;background:rgba(29,33,41,.72);color:#fff;font-size:14px;line-height:1;cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center} .ar-photo-del:hover{background:#cb2634} .ar-photo-hint{margin-top:8px;font-size:12px;color:#86909c;line-height:1.4} .ar-photo-preview-img{max-width:100%;max-height:70vh;object-fit:contain} `; /** 照片上传:左固定上传、右照片列表,支持批量(对齐小程序 PhotoUploadBlock) */ const PhotoUploadBlock = ({ fileList, onChange, maxPhotos = MAX_SERVICE_PHOTOS }) => { const list = fileList || []; const [previewOpen, setPreviewOpen] = useState(false); const [previewUrl, setPreviewUrl] = useState(''); const canUpload = list.length < maxPhotos; const handleChange = ({ fileList: nextList }) => { onChange((nextList || []).slice(0, maxPhotos)); }; const handleRemove = (uid, e) => { e.stopPropagation(); e.preventDefault(); onChange(list.filter((f) => f.uid !== uid)); }; const handlePreview = (file) => { const url = getUploadPreviewUrl(file); if (!url) return; if (Image && typeof Image.preview === 'function') { Image.preview({ src: url }); return; } setPreviewUrl(url); setPreviewOpen(true); }; return (