// 【重要】必须使用 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
| 卡片 | 统计口径 | 点击行为 |
|---|---|---|
| 待办任务 | tab=待办 的任务总数 | 列表切待办,展示全部待办 |
| 已逾期 | 待办中检验到期日 < 今日 | 列表切待办,仅展示逾期车 |
| 已完成 | tab=历史 的任务总数 | 列表切历史 |