// 【重要】必须使用 const Component 作为组件变量名 // 运维管理 - 车辆业务 - 年审管理(Web 列表页,逻辑对齐 ONE-OS 小程序年审管理) const { useState, useMemo, useEffect } = React; const moment = window.moment || window.dayjs; const antd = window.antd; const { Alert, App, Badge, Button, Card, Cascader, Col, DatePicker, Form, Input, Modal, Row, Select, Space, Table, Tabs, Tag, Tooltip, Typography, message, } = antd; const { Text, Paragraph } = Typography; const { RangePicker } = DatePicker; const PROVINCE_CITY_MAP = { 广东省: ['广州市', '深圳市', '东莞市'], 江苏省: ['南京市', '苏州市', '无锡市'], 浙江省: ['杭州市', '宁波市', '温州市'], 上海市: ['上海市'], 安徽省: ['合肥市', '芜湖市'], 山东省: ['临沂市'], 福建省: ['厦门市'], }; const HANDLER_OPTIONS = [ { label: '全部', value: '' }, { label: '张明辉', value: '张明辉' }, { label: '李晓彤', value: '李晓彤' }, { label: '王建国', value: '王建国' }, ]; const MOCK_CURRENT_HANDLER = '张明辉'; /** 列表 KPI、执行率统计锚定日(与原型演示一致) */ const AR_ANCHOR_DATE = '2026-06-01'; const AR_TASK_ID_STORAGE_KEY = 'oneos_ar_operate_task_id'; const AR_TASKS_STORAGE_KEY = 'oneos_ar_web_tasks_v1'; const AR_DRAFT_STORAGE_KEY = 'oneos_ar_operate_drafts_v1'; const AR_NAV_TARGET_KEY = 'oneos_ar_navigate_target'; const AR_NAV_EVENT = 'oneos-ar-return-list'; const loadTasksFromStorage = () => { try { const raw = localStorage.getItem(AR_TASKS_STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); return Array.isArray(parsed) && parsed.length ? parsed : null; } catch { return null; } }; 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 */ } }; /** 列表「已保存」标签示例:对应待办 ar-1、ar-2(办理页可续填) */ const MOCK_SAMPLE_DRAFTS = { 'ar-1': { savedAt: '2026-06-01T10:30:00.000Z', inspectionForm: { station: '汇通检测站', cost: '280', remark: '检测已完成,待提交年审结果', }, licenseForm: { photos: [ { uid: 'sample-ar-1-license-1', name: '行驶证正面.jpg', url: 'https://picsum.photos/seed/ar-license-ar1/240/180', status: 'done', }, { uid: 'sample-ar-1-license-2', name: '行驶证副页.jpg', url: 'https://picsum.photos/seed/ar-license-ar1b/240/180', status: 'done', }, ], inspectionValidUntil: '2026-07-31', ocrStatus: 'done', }, m2Expanded: false, m2Form: { station: '', cost: '', remark: '', photos: [] }, zbExpanded: false, zbForm: { station: '', cost: '', remark: '', photos: [] }, }, 'ar-2': { savedAt: '2026-06-01T14:15:00.000Z', inspectionForm: { station: '平湖检测站', cost: '320', remark: '', }, licenseForm: { photos: [], inspectionValidUntil: null, ocrStatus: 'idle', }, m2Expanded: true, m2Form: { station: '汇通检测站', cost: '180', remark: '二保已做,整备待补', photos: [], }, zbExpanded: false, zbForm: { station: '', cost: '', remark: '', photos: [] }, }, }; /** 合并示例草稿与本地已保存数据(本地同 taskId 优先);无本地数据时写入示例供办理页反写 */ const loadOperateDraftsForDisplay = () => { const stored = loadOperateDrafts(); const merged = { ...MOCK_SAMPLE_DRAFTS, ...stored }; try { if (!localStorage.getItem(AR_DRAFT_STORAGE_KEY)) { persistOperateDrafts(MOCK_SAMPLE_DRAFTS); } } catch { /* ignore */ } return merged; }; const DEFAULT_FILTER = { provinceCity: null, expireRange: null, handler: '', executeTimeRange: null, }; const mapOperateStatus = (raw) => { if (raw === '可运营' || raw === '待运营') return '库存'; return raw || '—'; }; /** 执行率 KPI 演示:6/7/8 月检验有效期任务(与 AR_ANCHOR_DATE 对齐) */ const EXECUTION_RATE_DEMO_PLANS = [ { month: '2026-06', done: 34, pending: 24 }, { month: '2026-07', done: 42, pending: 16 }, { month: '2026-08', done: 18, pending: 11 }, ]; const EXECUTION_RATE_DEMO_HANDLERS = ['张明辉', '张明辉', '李晓彤', '王建国']; const buildExecutionRateDemoTasks = () => { const rows = []; EXECUTION_RATE_DEMO_PLANS.forEach((plan, monthIdx) => { let seq = 0; const monthTag = plan.month.replace('-', ''); const baseDay = monthIdx * 3 + 1; for (let i = 0; i < plan.done; i += 1) { seq += 1; const handler = EXECUTION_RATE_DEMO_HANDLERS[i % EXECUTION_RATE_DEMO_HANDLERS.length]; const day = String(((i + baseDay) % 27) + 1).padStart(2, '0'); const expireDate = `${plan.month}-${day}`; rows.push({ id: `ar-rate-${plan.month}-done-${i}`, plateNo: `粤Y${monthTag}${String(seq).padStart(3, '0')}`, vin: `LRATED${plan.month}D${String(i).padStart(4, '0')}`, brand: '解放', model: '执行率演示车', operateStatusRaw: '自营', operateStatus: '自营', expireDate, daysLeft: 0, tab: 'history', province: '广东省', city: '深圳市', executor: handler, assignee: handler, executeTime: `${plan.month}-${day} ${String(9 + (i % 8)).padStart(2, '0')}:30`, }); } for (let i = 0; i < plan.pending; i += 1) { seq += 1; const handler = EXECUTION_RATE_DEMO_HANDLERS[(i + monthIdx) % EXECUTION_RATE_DEMO_HANDLERS.length]; const day = String(((i + baseDay + 7) % 27) + 1).padStart(2, '0'); const expireDate = `${plan.month}-${day}`; const daysLeft = monthIdx === 0 ? 12 + (i % 10) : monthIdx === 1 ? 35 + (i % 15) : 55 + (i % 12); rows.push({ id: `ar-rate-${plan.month}-pending-${i}`, plateNo: `粤Y${monthTag}${String(seq).padStart(3, '0')}`, vin: `LRATEP${plan.month}P${String(i).padStart(4, '0')}`, brand: '福田', model: '执行率演示车', operateStatusRaw: '租赁', operateStatus: '租赁', expireDate, daysLeft, tab: 'pending', province: '广东省', city: '深圳市', executor: '', assignee: handler, executeTime: '', }); } }); return rows; }; const EXECUTION_RATE_DEMO_TASKS = buildExecutionRateDemoTasks(); const mergeExecutionRateDemoTasks = (tasks) => { const list = Array.isArray(tasks) ? [...tasks] : []; const idSet = new Set(list.map((t) => t.id)); EXECUTION_RATE_DEMO_TASKS.forEach((t) => { if (!idSet.has(t.id)) { list.push(t); idSet.add(t.id); } }); return list; }; 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: '', }, { id: 'ar-h-jun', plateNo: '苏B88112', vin: 'LZZ5CLSB8NC556677', brand: '解放', model: 'J7牵引车', operateStatusRaw: '自营', expireDate: '2026-06-05', daysLeft: 0, tab: 'history', province: '江苏省', city: '无锡市', executor: '张明辉', executeTime: '2026-06-03 11:20', assignee: '张明辉', }, { id: 'ar-h1', plateNo: '苏A88991', vin: 'LSVAM4187C2123456', brand: '解放', model: 'J6P牵引车', operateStatusRaw: '自营', expireDate: '2026-03-10', daysLeft: 0, tab: 'history', province: '江苏省', city: '南京市', executor: '张明辉', executeTime: '2026-03-08 14:20', }, { id: 'ar-h2', plateNo: '粤A11223', vin: 'LFWNHXSD8P7654321', brand: '比亚迪', model: 'T5纯电轻卡', operateStatusRaw: '待运营', expireDate: '2026-02-20', daysLeft: 0, tab: 'history', province: '广东省', city: '广州市', executor: '李晓彤', executeTime: '2026-02-18 09:45', }, { id: 'ar-h3', plateNo: '京A55667', vin: 'LZZ5CLSB8NB654321', brand: '东风', model: '天龙KL', operateStatusRaw: '租赁', expireDate: '2026-01-15', daysLeft: 0, tab: 'history', province: '广东省', city: '东莞市', executor: '王建国', executeTime: '2026-01-12 16:30', }, ].map((t, idx) => ({ ...t, operateStatus: mapOperateStatus(t.operateStatusRaw), assignee: t.assignee ?? (t.tab === 'pending' ? ['张明辉', '张明辉', '李晓彤', '王建国', '张明辉', '李晓彤', '张明辉'][idx % 7] || MOCK_CURRENT_HANDLER : ''), })); const getTaskExpireMonthKey = (task) => { if (!task?.expireDate || !moment) return ''; return moment(task.expireDate).startOf('month').format('YYYY-MM'); }; const isAnnualReviewCompleted = (task) => task.tab === 'history'; const getTaskResponsibleUser = (task) => task.executor || task.assignee || ''; /** 运维主管看全量;运维专员仅统计本人负责/办理的任务 */ const taskInRoleScope = (task, isSupervisor, currentUser) => { if (isSupervisor) return true; return getTaskResponsibleUser(task) === currentUser; }; const buildThreeMonthWindows = (anchorDate = AR_ANCHOR_DATE) => { const base = moment(anchorDate).startOf('month'); return [0, 1, 2].map((offset) => { const m = base.clone().add(offset, 'month'); return { key: `m${offset}`, title: `${m.format('M')}月执行率`, monthLabel: m.format('YYYY年M月'), monthKey: m.format('YYYY-MM'), }; }); }; const computeMonthExecutionRate = (allTasks, monthKey, isSupervisor, currentUser) => { const scoped = (allTasks || []).filter((t) => { if (isAnnualReviewExcluded(t)) return false; if (getTaskExpireMonthKey(t) !== monthKey) return false; return taskInRoleScope(t, isSupervisor, currentUser); }); const total = scoped.length; const done = scoped.filter(isAnnualReviewCompleted).length; const rate = total === 0 ? null : Math.round((done / total) * 100); return { total, done, pending: total - done, rate }; }; const isAnnualReviewExcluded = (task) => task.operateStatusRaw === '退出运营' || task.operateStatus === '退出运营'; const getOverdueDays = (task) => { if (task?.daysLeft != null && task.daysLeft < 0) return -task.daysLeft; if (moment && task?.expireDate) { const overdue = moment().startOf('day').diff(moment(task.expireDate).startOf('day'), 'days'); return overdue > 0 ? overdue : 0; } return 0; }; const getDaysLeftForSort = (task) => { if (task?.daysLeft != null) return task.daysLeft; if (moment && task?.expireDate) { return moment(task.expireDate).startOf('day').diff(moment().startOf('day'), 'days'); } return Number.MAX_SAFE_INTEGER; }; const sortPendingTasks = (tasks) => [...tasks].sort((a, b) => { const overdueDiff = getOverdueDays(b) - getOverdueDays(a); if (overdueDiff !== 0) return overdueDiff; return getDaysLeftForSort(a) - getDaysLeftForSort(b); }); const buildMockLicensePhotos = (taskId) => [ { uid: `license-${taskId}-1`, name: '行驶证.jpg', url: `https://picsum.photos/seed/ar-license-${taskId}/240/180`, status: 'done', }, ]; const buildSampleHistorySnapshot = (task) => ({ inspectionForm: { station: task.id === 'ar-h2' ? '平湖检测站' : '汇通检测站', cost: task.id === 'ar-h3' ? '450' : '320', remark: task.id === 'ar-h1' ? '已通过年检' : '', }, licenseForm: { photos: buildMockLicensePhotos(task.id), inspectionValidUntil: task.expireDate, ocrStatus: 'done', }, m2Expanded: task.id === 'ar-h1', m2Form: task.id === 'ar-h1' ? { station: '汇通检测站', cost: '180', remark: '二保已完成', photos: [] } : { station: '', cost: '', remark: '', photos: [] }, zbExpanded: task.id === 'ar-h3', zbForm: task.id === 'ar-h3' ? { station: '广州天河维修站', cost: '520', remark: '整备完成', photos: [] } : { station: '', cost: '', remark: '', photos: [] }, }); const getHistoryServiceForms = (task) => { const snap = task?.operateSnapshot; if (snap) { return { m2: snap.m2Form || {}, zb: snap.zbForm || {} }; } return { m2: task?.m2Form || {}, zb: task?.zbForm || {} }; }; const formatListMoney = (cost) => { if (cost === '' || cost == null) return '—'; const n = Number(cost); return Number.isFinite(n) ? n.toFixed(2) : String(cost); }; const getM2StationName = (task) => getHistoryServiceForms(task).m2?.station || '—'; const getM2Cost = (task) => formatListMoney(getHistoryServiceForms(task).m2?.cost); const getZbStationName = (task) => getHistoryServiceForms(task).zb?.station || '—'; const getZbCost = (task) => formatListMoney(getHistoryServiceForms(task).zb?.cost); const formatTaskRegion = (task) => { if (!task?.province) return '—'; if (task.city) return `${task.province}-${task.city}`; return task.province; }; const isTaskOverdue = (task) => task?.daysLeft != null ? task.daysLeft < 0 : getOverdueDays(task) > 0; const escapeCsvCell = (val) => { const text = val == null ? '' : String(val); if (/[",\n\r]/.test(text)) return `"${text.replace(/"/g, '""')}"`; return text; }; const mapTasksForList = (rawTasks) => (rawTasks || []) .filter((t) => !isAnnualReviewExcluded(t)) .map((t) => t.tab === 'history' ? { ...t, operateSnapshot: t.operateSnapshot || buildSampleHistorySnapshot(t) } : t ); const downloadCsv = (filename, headers, rows) => { const bom = '\uFEFF'; const lines = [headers.map(escapeCsvCell).join(',')].concat( rows.map((row) => row.map(escapeCsvCell).join(',')) ); const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); }; const buildRegionCascaderOptions = () => Object.entries(PROVINCE_CITY_MAP).map(([province, cities]) => ({ label: province, value: province, children: cities.map((c) => ({ label: c, value: c })), })); const AR_ICONS = { vehicle: ( ), warning: ( ), success: ( ), rate: ( ), info: ( ), }; const PAGE_STYLES = ` .ar-web{min-height:100vh;background:#f7f8fa;font-family:Inter,Helvetica,PingFang SC,Microsoft YaHei,Arial,sans-serif} .ar-web .main-content{padding:20px 24px 32px;max-width:1440px} .ar-web .ar-page-top-bar{display:flex;justify-content:flex-end;margin-bottom:8px} .ar-web .filter-card-toolbar{display:flex;justify-content:flex-end;margin-bottom:12px} .ar-web .ar-alert-stats-row{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin-top:12px;margin-bottom:0} @media (max-width:1200px){ .ar-web .ar-alert-stats-row{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:12px;padding-bottom:4px;-webkit-overflow-scrolling:touch} .ar-web .ar-alert-stats-row .ar-alert-card{flex:0 0 168px;max-width:42vw} } .ar-web .ar-alert-card{display:flex;align-items:center;gap:12px;padding:14px 40px 14px 16px;border-radius:12px;border:1px solid #e2e8f0;background:#fff;position:relative;overflow:visible} .ar-web .ar-alert-card-clickable{cursor:pointer;transition:box-shadow .2s ease,border-color .2s ease,transform .2s ease} .ar-web .ar-alert-card-clickable:hover{box-shadow:0 4px 14px rgba(15,23,42,.08)} .ar-web .ar-alert-card-active{box-shadow:0 0 0 2px rgba(22,93,255,.18)!important;border-color:#165dff!important} .ar-web .ar-alert-card-icon{flex-shrink:0;width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center} .ar-web .ar-alert-card-val{font-size:26px;font-weight:800;line-height:1.1;color:#0f172a;font-variant-numeric:tabular-nums} .ar-web .ar-alert-card-title{font-size:13px;font-weight:600;color:#334155;margin-top:2px} .ar-web .ar-alert-card-body{flex:1;min-width:0} .ar-web .ar-alert-card-info{position:absolute;top:10px;right:10px;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#94a3b8;background:rgba(148,163,184,.12);cursor:help;z-index:1} .ar-web .ar-alert-card-info:hover{color:#475569;background:rgba(148,163,184,.22)} .ar-web .ar-rate-card{background:linear-gradient(135deg,#eff6ff 0%,#fff 55%);border-color:#bfdbfe} .ar-web .ar-rate-card .ar-alert-card-icon{background:#dbeafe;color:#2563eb} .ar-web .ar-rate-card .ar-alert-card-val{color:#1d4ed8} .ar-web .ar-rate-card--hover-tip{cursor:default} .ar-web .ar-alert-card--total{background:linear-gradient(135deg,#f8fafc 0%,#fff 100%)} .ar-web .ar-alert-card--total .ar-alert-card-icon{background:#e2e8f0;color:#475569} .ar-web .ar-alert-card--normal{background:linear-gradient(135deg,#ecfdf5 0%,#fff 55%);border-color:#bbf7d0} .ar-web .ar-alert-card--normal .ar-alert-card-icon{background:#d1fae5;color:#059669} .ar-web .ar-alert-card--normal .ar-alert-card-val{color:#047857} .ar-web .ar-alert-card--expired{background:linear-gradient(135deg,#fef2f2 0%,#fff 55%);border-color:#fecaca} .ar-web .ar-alert-card--expired .ar-alert-card-icon{background:#fee2e2;color:#dc2626} .ar-web .ar-alert-card--expired .ar-alert-card-val{color:#b91c1c} .ar-web .filter-card,.ar-web .table-card{border:1px solid #e5e6eb!important;border-radius:10px!important;box-shadow:none!important} .ar-web .filter-card .ant-card-body{padding:16px 20px 8px} .ar-web .filter-form .ant-form-item{margin-bottom:16px} .ar-web .filter-form .ant-form-item-control-input,.ar-web .filter-form .ant-select,.ar-web .filter-form .ant-picker,.ar-web .filter-form .ant-cascader{width:100%!important} .ar-web .filter-form .ant-select-selector{width:100%!important} .ar-web .filter-actions{display:flex;justify-content:flex-end;gap:8px;padding-bottom:12px;margin-top:4px} .ar-web .table-card .ant-card-body{padding:0} .ar-web .table-list-bar{display:flex;justify-content:flex-end;align-items:center;padding:12px 20px 0} .ar-web .table-inner{padding:0 20px 16px} .ar-web .plate-cell{font-weight:600;color:#1d2129;font-size:14px} .ar-web .plate-cell-with-tag{display:inline-flex;align-items:center;flex-wrap:wrap;gap:8px} .ar-web .ar-prd-ul{margin:0;padding-left:20px} .ar-web .ar-prd-ul li{margin-bottom:6px} .ar-web .action-link{color:#165dff;cursor:pointer;user-select:none} .ar-web .action-link:hover{text-decoration:underline} .ar-web .action-link+.action-link{margin-left:12px} .ar-web .filter-tags{margin-top:8px;display:flex;flex-wrap:wrap;gap:8px} .ar-web .ar-prd-doc{max-height:65vh;overflow-y:auto;font-size:13px;line-height:1.65;color:#4e5969} .ar-web .ar-prd-h2{font-size:15px;font-weight:600;color:#1d2129;margin:16px 0 8px} .ar-web .ar-prd-h3{font-size:14px;font-weight:600;color:#1d2129;margin:12px 0 6px} .ar-web .ar-prd-highlight{background:#f0f9ff;border:1px solid #bedaff;border-radius:8px;padding:12px 14px;margin:12px 0} @media (prefers-reduced-motion: reduce){ .ar-web .ar-alert-card-clickable{transition:none} .ar-web .ar-alert-card-clickable:hover{transform:none} } `; const Component = function AnnualReviewWebList() { const regionOptions = useMemo(() => buildRegionCascaderOptions(), []); const [mainTab, setMainTab] = useState('pending'); /** all=待办任务 | overdue=已逾期 | completed=已完成 */ const [kpiActive, setKpiActive] = useState('all'); const [plateInput, setPlateInput] = useState(''); const [appliedPlate, setAppliedPlate] = useState(''); const [filterDraft, setFilterDraft] = useState({ ...DEFAULT_FILTER }); const [filterApplied, setFilterApplied] = useState({ ...DEFAULT_FILTER }); const [filterExpanded, setFilterExpanded] = useState(true); const [tasks, setTasks] = useState(() => { const stored = loadTasksFromStorage(); const base = mergeExecutionRateDemoTasks(stored || MOCK_TASKS); return base.filter((t) => !isAnnualReviewExcluded(t)).map((t) => t.tab === 'history' ? { ...t, operateSnapshot: t.operateSnapshot || buildSampleHistorySnapshot(t) } : t ); }); const [prdOpen, setPrdOpen] = useState(false); const [operateDrafts, setOperateDrafts] = useState(() => loadOperateDraftsForDisplay()); const [rangePickerKey, setRangePickerKey] = useState(0); const [tablePage, setTablePage] = useState(1); const [tablePageSize, setTablePageSize] = useState(10); /** false=运维专员(仅本人任务);true=运维主管(全量) */ const [viewAsSupervisor, setViewAsSupervisor] = useState(false); useEffect(() => { setTablePage(1); }, [mainTab, kpiActive, appliedPlate, filterApplied]); const syncFromHandlePage = () => { setOperateDrafts(loadOperateDraftsForDisplay()); const stored = loadTasksFromStorage(); if (stored) setTasks(mapTasksForList(mergeExecutionRateDemoTasks(stored))); }; useEffect(() => { const handler = () => syncFromHandlePage(); window.addEventListener(AR_NAV_EVENT, handler); window.addEventListener('focus', handler); const onStorage = (e) => { if (!e.key || e.key === AR_DRAFT_STORAGE_KEY || e.key === AR_TASKS_STORAGE_KEY) { handler(); } }; window.addEventListener('storage', onStorage); try { if (sessionStorage.getItem(AR_NAV_TARGET_KEY) === 'list') { sessionStorage.removeItem(AR_NAV_TARGET_KEY); handler(); } } catch { /* ignore */ } return () => { window.removeEventListener(AR_NAV_EVENT, handler); window.removeEventListener('focus', handler); window.removeEventListener('storage', onStorage); }; }, []); const pendingSource = useMemo(() => tasks.filter((t) => t.tab === 'pending'), [tasks]); const listStats = useMemo(() => { const pending = pendingSource; const overdue = pending.filter((t) => isTaskOverdue(t)).length; const completed = tasks.filter((t) => t.tab === 'history').length; return { pendingTotal: pending.length, overdue, completed, }; }, [pendingSource, tasks]); const monthWindows = useMemo(() => buildThreeMonthWindows(AR_ANCHOR_DATE), []); const executionRateCards = useMemo(() => { const isSupervisor = viewAsSupervisor; const currentUser = MOCK_CURRENT_HANDLER; return monthWindows.map((win) => { const stat = computeMonthExecutionRate(tasks, win.monthKey, isSupervisor, currentUser); return { ...win, ...stat, tooltipText: `已处理:${stat.done}/本月任务数:${stat.total}`, }; }); }, [tasks, monthWindows, viewAsSupervisor]); const activeFilterTags = useMemo(() => { const f = filterApplied; const tags = []; if (f.provinceCity && f.provinceCity[0]) { tags.push({ key: 'region', label: `运营区域:${f.provinceCity[1] ? `${f.provinceCity[0]} / ${f.provinceCity[1]}` : f.provinceCity[0]}`, }); } if (f.expireRange && f.expireRange[0] && f.expireRange[1] && moment) { tags.push({ key: 'expire', label: `到期时间:${moment(f.expireRange[0]).format('YYYY-MM-DD')} ~ ${moment(f.expireRange[1]).format('YYYY-MM-DD')}`, }); } if (mainTab === 'history') { if (f.handler) tags.push({ key: 'handler', label: `办理人:${f.handler}` }); if (f.executeTimeRange && f.executeTimeRange[0] && f.executeTimeRange[1] && moment) { tags.push({ key: 'done', label: `完成时间:${moment(f.executeTimeRange[0]).format('YYYY-MM-DD')} ~ ${moment(f.executeTimeRange[1]).format('YYYY-MM-DD')}`, }); } } if (appliedPlate) tags.push({ key: 'plate', label: `车牌:${appliedPlate}` }); return tags; }, [filterApplied, appliedPlate, mainTab]); const filteredList = useMemo(() => { const f = filterApplied; const plateKey = (appliedPlate || '').trim().toLowerCase(); const list = tasks.filter((t) => { if (t.tab !== mainTab) return false; if (plateKey && !t.plateNo.toLowerCase().includes(plateKey)) return false; if (f.provinceCity && f.provinceCity[0]) { if (t.province !== f.provinceCity[0]) return false; if (f.provinceCity[1] && t.city !== f.provinceCity[1]) return false; } if (f.expireRange && f.expireRange[0] && f.expireRange[1] && moment) { const exp = moment(t.expireDate).startOf('day').valueOf(); const start = moment(f.expireRange[0]).startOf('day').valueOf(); const end = moment(f.expireRange[1]).endOf('day').valueOf(); if (exp < start || exp > end) return false; } if (mainTab === 'history') { if (f.handler && t.executor !== f.handler) return false; if (f.executeTimeRange && f.executeTimeRange[0] && f.executeTimeRange[1] && moment) { const done = moment(t.executeTime); if (!done.isValid()) return false; const start = moment(f.executeTimeRange[0]).startOf('day').valueOf(); const end = moment(f.executeTimeRange[1]).endOf('day').valueOf(); const doneVal = done.valueOf(); if (doneVal < start || doneVal > end) return false; } } return true; }); let scoped = list; if (kpiActive === 'overdue' && mainTab === 'pending') { scoped = scoped.filter((t) => isTaskOverdue(t)); } if (mainTab === 'pending') return sortPendingTasks(scoped); if (mainTab === 'history' && moment) { return [...scoped].sort((a, b) => { const ta = moment(a.executeTime); const tb = moment(b.executeTime); if (ta.isValid() && tb.isValid()) return tb.valueOf() - ta.valueOf(); return String(b.executeTime || '').localeCompare(String(a.executeTime || '')); }); } return scoped; }, [tasks, mainTab, kpiActive, appliedPlate, filterApplied]); const renderUrgencyTag = (task) => { if (mainTab !== 'pending') return null; const days = task.daysLeft; if (days == null) return ; if (days < 0) { return ( 已逾期 {Math.abs(days)} 天 ); } if (days <= 30) { return ( 剩余 {days} 天 ); } return 剩余 {days} 天; }; const goHandlePage = (task) => { try { sessionStorage.setItem(AR_TASK_ID_STORAGE_KEY, task.id); persistTasksToStorage(tasks); } catch { /* ignore */ } message.info('已带入车辆信息,请打开「年审管理-办理」页面继续办理'); }; const goViewPage = (task) => { try { sessionStorage.setItem(AR_TASK_ID_STORAGE_KEY, task.id); persistTasksToStorage(tasks); } catch { /* ignore */ } message.info('已带入历史记录,请打开「年审管理-查看」页面'); }; const handleKpiClick = (key) => { setKpiActive(key); if (key === 'completed') { setMainTab('history'); } else { setMainTab('pending'); } }; const formatUrgencyText = (task) => { const days = task?.daysLeft; if (days == null) return ''; if (days < 0) return `已逾期 ${Math.abs(days)} 天`; return `剩余 ${days} 天`; }; const handleExportList = () => { if (!filteredList.length) { message.warning('当前列表无可导出数据'); return; } const isPending = mainTab === 'pending'; const tabLabel = kpiActive === 'overdue' ? '已逾期' : kpiActive === 'completed' ? '已完成' : '待办任务'; const headers = isPending ? ['序号', '车牌号', '品牌', '型号', '紧急程度', '运营状态', '运营区域', '检验到期日'] : [ '序号', '车牌号', '品牌', '型号', '运营状态', '运营区域', '检验有效期至', '二保服务站名称', '二保费用', '整备服务站名称', '整备费用', '办理人', '完成时间', ]; const rows = filteredList.map((row, index) => isPending ? [ index + 1, row.plateNo, row.brand, row.model, formatUrgencyText(row), row.operateStatus, formatTaskRegion(row), row.expireDate, ] : [ index + 1, row.plateNo, row.brand, row.model, row.operateStatus, formatTaskRegion(row), row.expireDate, getM2StationName(row) === '—' ? '' : getM2StationName(row), getM2Cost(row) === '—' ? '' : getM2Cost(row), getZbStationName(row) === '—' ? '' : getZbStationName(row), getZbCost(row) === '—' ? '' : getZbCost(row), row.executor, row.executeTime, ] ); const dateStr = moment ? moment().format('YYYYMMDD_HHmm') : 'export'; downloadCsv(`年审管理_${tabLabel}_${dateStr}.csv`, headers, rows); message.success(`已导出 ${filteredList.length} 条记录`); }; const indexColumn = useMemo( () => ({ title: '序号', key: 'serial', width: 64, fixed: 'left', align: 'center', render: (_, __, index) => (tablePage - 1) * tablePageSize + index + 1, }), [tablePage, tablePageSize] ); const plateColumnPending = useMemo( () => ({ title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', fixed: 'left', width: 168, render: (_, row) => ( {row.plateNo} {operateDrafts[row.id] ? 已保存 : null} ), }), [operateDrafts] ); const plateColumnHistory = useMemo( () => ({ title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', fixed: 'left', width: 112, render: (v) => {v}, }), [] ); const brandModelColumns = useMemo( () => [ { title: '品牌', dataIndex: 'brand', key: 'brand', width: 88, ellipsis: true }, { title: '型号', dataIndex: 'model', key: 'model', width: 168, ellipsis: true }, ], [] ); const applySearch = () => { setAppliedPlate(plateInput.trim()); setFilterApplied({ ...filterDraft }); setTablePage(1); message.success('已按筛选条件更新列表'); }; const resetFilters = () => { setPlateInput(''); setAppliedPlate(''); const next = { ...DEFAULT_FILTER }; setFilterDraft(next); setFilterApplied(next); setRangePickerKey((k) => k + 1); message.info('已重置筛选条件'); }; const removeFilterTag = (key) => { if (key === 'plate') { setPlateInput(''); setAppliedPlate(''); return; } const next = { ...filterApplied }; if (key === 'region') next.provinceCity = null; if (key === 'expire') next.expireRange = null; if (key === 'handler') next.handler = ''; if (key === 'done') next.executeTimeRange = null; setFilterDraft(next); setFilterApplied(next); if (key === 'expire' || key === 'done') setRangePickerKey((k) => k + 1); }; const pendingColumns = [ indexColumn, plateColumnPending, ...brandModelColumns, { title: '紧急程度', key: 'urgency', width: 120, render: (_, row) => renderUrgencyTag(row), }, { title: '运营状态', dataIndex: 'operateStatus', key: 'operateStatus', width: 96, render: (v) => , }, { title: '运营区域', key: 'region', width: 160, render: (_, row) => formatTaskRegion(row), }, { title: '检验到期日', dataIndex: 'expireDate', key: 'expireDate', width: 120, }, { title: '操作', key: 'action', fixed: 'right', width: 88, render: (_, row) => ( goHandlePage(row)} role="button" tabIndex={0}> 办理 ), }, ]; const historyColumns = [ indexColumn, plateColumnHistory, ...brandModelColumns, { title: '运营状态', dataIndex: 'operateStatus', key: 'operateStatus', width: 96, render: (v) => , }, { title: '运营区域', key: 'region', width: 160, render: (_, row) => formatTaskRegion(row), }, { title: '检验有效期至', dataIndex: 'expireDate', key: 'expireDate', width: 120, }, { title: '二保服务站名称', key: 'm2Station', width: 128, ellipsis: true, render: (_, row) => getM2StationName(row), }, { title: '二保费用', key: 'm2Cost', width: 96, align: 'right', render: (_, row) => getM2Cost(row), }, { title: '整备服务站名称', key: 'zbStation', width: 128, ellipsis: true, render: (_, row) => getZbStationName(row), }, { title: '整备费用', key: 'zbCost', width: 96, align: 'right', render: (_, row) => getZbCost(row), }, { title: '办理人', dataIndex: 'executor', key: 'executor', width: 100, }, { title: '完成时间', dataIndex: 'executeTime', key: 'executeTime', width: 160, defaultSortOrder: 'descend', sorter: (a, b) => String(b.executeTime).localeCompare(String(a.executeTime)), }, { title: '操作', key: 'action', fixed: 'right', width: 88, render: (_, row) => ( goViewPage(row)} role="button" tabIndex={0}> 查看 ), }, ]; const kpiAlertCards = [ { key: 'all', kpi: 'all', type: 'total', title: '待办任务', desc: '当前待办理年审任务总数(不含退出运营车辆)。点击卡片筛选待办列表。', val: listStats.pendingTotal, icon: AR_ICONS.vehicle, }, { key: 'overdue', kpi: 'overdue', type: 'expired', title: '已逾期', desc: '检验到期日已过仍未办结的待办任务。点击后列表仅展示已逾期车辆,排序优先逾期最久。', val: listStats.overdue, icon: AR_ICONS.warning, }, { key: 'completed', kpi: 'completed', type: 'normal', title: '已完成', desc: '已提交办结并进入历史记录的年审任务数。点击切换至历史 Tab。', val: listStats.completed, icon: AR_ICONS.success, }, ]; const renderKpiInfoIcon = (desc) => ( e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} > {AR_ICONS.info} ); const statCards = (
{kpiAlertCards.map((card) => (
handleKpiClick(card.kpi)} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && handleKpiClick(card.kpi)} > {renderKpiInfoIcon(card.desc)}
{card.icon}
{card.val}
{card.title}
))} {executionRateCards.map((card) => (
{AR_ICONS.rate}
{card.rate == null ? '—' : `${card.rate}%`}
{card.title}
))}
); const pageContent = (
查看视角 setPlateInput(e.target.value)} onPressEnter={applySearch} /> setFilterDraft((p) => ({ ...p, provinceCity: v }))} /> setFilterDraft((p) => ({ ...p, expireRange: dates && dates[0] && dates[1] ? [dates[0], dates[1]] : null, })) } /> {mainTab === 'history' && ( <>