// 【重要】必须使用 const Component 作为组件变量名
// ONE-OS 小程序 - 年审管理（待处理 / 历史记录 + 筛选抽屉）

const { useState, useMemo, useEffect, useRef, useCallback } = React;
const moment = window.moment || window.dayjs;

const COLOR_PRIMARY = '#16D1A1';
const COLOR_PRIMARY_DEEP = '#00BFA5';
const COLOR_TEXT = '#1D2129';
const COLOR_MUTED = '#86909C';
const COLOR_LINE = '#E5E6EB';
const COLOR_BG = '#FFFFFF';
const COLOR_PAGE = '#F2F3F5';
const COLOR_WARN = '#FF7D00';
const COLOR_DANGER = '#F53F3F';
const FONT_FAMILY =
  '-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", STHeiti, sans-serif';

/** 兼容 Axhub：antd 可能是 default 导出或子集包，避免解构出 undefined 导致 React #130 */
const resolveAntdBundle = () => {
  const raw = window.antd;
  if (!raw) return {};
  if (raw.default && typeof raw.default === 'object') {
    return { ...raw, ...raw.default };
  }
  return raw;
};

const antd = resolveAntdBundle();

const FallbackTag = ({ children, className, style, color }) => (
  <span
    className={className}
    style={{
      display: 'inline-block',
      fontSize: 11,
      lineHeight: '18px',
      padding: '0 6px',
      borderRadius: 4,
      fontWeight: 600,
      color:
        color === 'error' ? COLOR_DANGER : color === 'warning' ? COLOR_WARN : COLOR_PRIMARY_DEEP,
      background:
        color === 'error'
          ? 'rgba(245, 63, 63, 0.1)'
          : color === 'warning'
            ? 'rgba(255, 125, 0, 0.1)'
            : 'rgba(22, 209, 161, 0.1)',
      ...style
    }}
  >
    {children}
  </span>
);

const FallbackInputNumber = ({ value, onChange, placeholder, min, precision, bordered, controls }) => (
  <input
    type="number"
    className="ant-input"
    style={{ width: '100%', textAlign: 'right', border: bordered === false ? 'none' : undefined }}
    placeholder={placeholder}
    min={min}
    step={precision === 2 ? 0.01 : 1}
    value={value == null ? '' : value}
    onChange={(e) => {
      const raw = e.target.value;
      if (onChange) onChange(raw === '' ? null : Number(raw));
    }}
  />
);

const FallbackUpload = ({ children, className, onChange, fileList, multiple, accept, beforeUpload, showUploadList }) => (
  <label className={className} style={{ display: 'block', cursor: 'pointer' }}>
    <input
      type="file"
      accept={accept || 'image/*'}
      multiple={multiple}
      style={{ display: 'none' }}
      onChange={(e) => {
        const files = Array.from(e.target.files || []);
        if (beforeUpload && files.some((f) => beforeUpload(f) === false)) return;
        const next = files.map((f, i) => ({
          uid: `local-${Date.now()}-${i}`,
          name: f.name,
          status: 'done',
          originFileObj: f
        }));
        if (onChange) onChange({ fileList: [...(fileList || []), ...next] });
        e.target.value = '';
      }}
    />
    {children}
  </label>
);

const FallbackDrawer = ({ open, title, placement, width, height, onClose, children, footer, styles }) =>
  open ? (
    <div
      className="ar-fallback-drawer-mask"
      style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,.45)' }}
      onClick={onClose}
      role="presentation"
    >
      <div
        className="ar-fallback-drawer"
        style={{
          position: 'fixed',
          background: '#fff',
          zIndex: 1001,
          ...(placement === 'bottom'
            ? { left: 0, right: 0, bottom: 0, height: height || 360, borderRadius: '14px 14px 0 0' }
            : { top: 0, right: 0, bottom: 0, width: width || 320 }),
          display: 'flex',
          flexDirection: 'column'
        }}
        onClick={(e) => e.stopPropagation()}
        role="dialog"
      >
        <div style={{ padding: '14px 16px', fontWeight: 700, borderBottom: '1px solid #e5e6eb' }}>{title}</div>
        <div style={{ flex: 1, overflow: 'auto', ...(styles?.body || {}) }}>{children}</div>
        {footer ? <div>{footer}</div> : null}
      </div>
    </div>
  ) : null;

const FallbackModal = ({ open, title, children, footer, onCancel, centered, width, bodyStyle }) =>
  open ? (
    <div
      style={{
        position: 'fixed',
        inset: 0,
        zIndex: 1100,
        background: 'rgba(0,0,0,.45)',
        display: 'flex',
        alignItems: centered ? 'center' : 'flex-start',
        justifyContent: 'center',
        padding: 24
      }}
      onClick={onCancel}
      role="presentation"
    >
      <div
        style={{
          background: '#fff',
          borderRadius: 12,
          width: width || 400,
          maxWidth: '100%',
          maxHeight: '85vh',
          overflow: 'hidden',
          display: 'flex',
          flexDirection: 'column'
        }}
        onClick={(e) => e.stopPropagation()}
        role="dialog"
      >
        <div style={{ padding: '14px 16px', fontWeight: 700, borderBottom: '1px solid #e5e6eb' }}>{title}</div>
        <div style={{ overflow: 'auto', padding: 16, ...(bodyStyle || {}) }}>{children}</div>
        {footer ? (
          <div style={{ padding: 12, borderTop: '1px solid #e5e6eb', textAlign: 'right' }}>{footer}</div>
        ) : null}
      </div>
    </div>
  ) : null;

const FallbackInput = ({
  value,
  onChange,
  readOnly,
  placeholder,
  bordered,
  disabled,
  className,
  onFocus,
  style
}) => (
  <input
    className={`ant-input${className ? ` ${className}` : ''}`}
    readOnly={readOnly}
    disabled={disabled}
    placeholder={placeholder}
    value={value ?? ''}
    onChange={(e) => onChange && onChange(e)}
    onFocus={onFocus}
    style={{ width: '100%', border: bordered === false ? 'none' : undefined, ...style }}
  />
);

const FallbackSelect = ({ value, onChange, options, placeholder, bordered, allowClear }) => (
  <select
    className="ant-select"
    value={value ?? ''}
    onChange={(e) => {
      const v = e.target.value;
      if (!onChange) return;
      onChange(allowClear && !v ? undefined : v);
    }}
    style={{ width: '100%', border: bordered === false ? 'none' : undefined, textAlign: 'right' }}
  >
    <option value="">{placeholder || '请选择'}</option>
    {(options || []).map((o) => (
      <option key={String(o.value)} value={o.value}>
        {o.label}
      </option>
    ))}
  </select>
);

const FallbackButton = ({ children, onClick, type, block, size, disabled, style }) => (
  <button
    type="button"
    disabled={disabled}
    onClick={onClick}
    style={{
      display: block ? 'block' : 'inline-block',
      width: block ? '100%' : undefined,
      padding: size === 'large' ? '10px 16px' : '6px 12px',
      borderRadius: 8,
      border: type === 'primary' ? 'none' : '1px solid #e5e6eb',
      background: type === 'primary' ? COLOR_PRIMARY : '#fff',
      color: type === 'primary' ? '#fff' : COLOR_TEXT,
      fontWeight: 600,
      cursor: disabled ? 'not-allowed' : 'pointer',
      opacity: disabled ? 0.5 : 1,
      ...style
    }}
  >
    {children}
  </button>
);

const Drawer = antd.Drawer || FallbackDrawer;
const Input = antd.Input || FallbackInput;
const Select = antd.Select || FallbackSelect;
const Button = antd.Button || FallbackButton;
const Tag = antd.Tag || FallbackTag;
const Modal = antd.Modal || FallbackModal;
const message = antd.message || {
  success: (t) => window.alert(t),
  error: (t) => window.alert(t),
  warning: (t) => window.alert(t),
  info: (t) => window.alert(t)
};
const InputNumber = antd.InputNumber || (Input && Input.Number) || FallbackInputNumber;
const Upload = antd.Upload || FallbackUpload;
const Image = antd.Image;

const FallbackTextArea = ({ value, onChange, placeholder, bordered, rows }) => (
  <textarea
    className="ant-input"
    rows={rows || 2}
    placeholder={placeholder}
    value={value ?? ''}
    onChange={(e) => onChange && onChange(e)}
    style={{ width: '100%', border: bordered === false ? 'none' : undefined, resize: 'none' }}
  />
);

const TextArea = (antd.Input && antd.Input.TextArea) || FallbackTextArea;

/** 省市数据（小程序级联选择） */
const PROVINCE_CITY_MAP = {
  广东省: ['广州市', '深圳市', '东莞市'],
  江苏省: ['南京市', '苏州市', '无锡市'],
  浙江省: ['杭州市', '宁波市', '温州市'],
  上海市: ['上海市'],
  安徽省: ['合肥市', '芜湖市']
};
const PROVINCE_LIST = Object.keys(PROVINCE_CITY_MAP);
const MAX_PLATE_INPUT_LEN = 8;
const PLATE_PROVINCES = [
  '京', '津', '沪', '渝', '冀', '豫', '云', '辽', '黑', '湘', '皖', '鲁', '新', '苏', '浙', '赣',
  '鄂', '桂', '甘', '晋', '蒙', '陕', '吉', '闽', '贵', '粤', '青', '藏', '川', '宁', '琼', '使', '领'
];
const PLATE_LETTERS = 'ABCDEFGHJKLMNPQRSTUVWXYZ'.split('');
const PLATE_ALNUM = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ'.split('');
const PLATE_LAST_CHARS = ['港', '澳', '学', '警', '挂', '使', '领'];

const getPlateKeysForIndex = (index) => {
  if (index === 0) return PLATE_PROVINCES;
  if (index === 1) return PLATE_LETTERS;
  if (index >= 6) return [...PLATE_ALNUM, ...PLATE_LAST_CHARS];
  return PLATE_ALNUM;
};
/** 检测服务站列表（检测站、二保共用） */
const INSPECTION_STATION_LIST = ['汇通检测站', '平湖检测站', '嘉兴检测站', '松江检测站'];
/** 维修站列表（整备服务站） */
const REPAIR_STATION_LIST = [
  '杭州拱墅维修站',
  '广州天河维修站',
  '上海浦东维修站',
  '深圳南山维修站',
  '天津港保税区维修站'
];
const INSPECTION_STATION_STORAGE_KEY = 'annual-review.station.inspection';

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 toStationSelectOptions = (list) => (list || []).map((s) => ({ label: s, value: s }));

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 EMPTY_SERVICE_FORM = {
  station: '',
  cost: '',
  costBearer: undefined,
  remark: '',
  photos: []
};
const EMPTY_INSPECTION_FORM = {
  station: '',
  cost: '',
  remark: ''
};
const MAX_SERVICE_PHOTOS = 10;
const MAX_LICENSE_PHOTOS = 4;
const LICENSE_OCR_MOCK_MS = 1500;
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: ''
  },
  {
    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) => ({
  ...t,
  operateStatus: mapOperateStatus(t.operateStatusRaw)
}));

/** 退出运营车辆无需年审，不纳入任务列表 */
const isAnnualReviewExcluded = (task) =>
  task.operateStatusRaw === '退出运营' || task.operateStatus === '退出运营';

/** 待办：已过期天数（未过期为 0，越大表示过期越久） */
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 AR_DRAFT_STORAGE_KEY = 'oneos_ar_operate_drafts_v1';
/** 原型：年审提交后写入，供 Web 端证照管理读取演示 */
const CERTIFICATE_LICENSE_SYNC_KEY = 'oneos_certificate_license_sync';

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 {
    /* 原型环境可能无 localStorage */
  }
};

/**
 * 年审提交后，将最新行驶证照片 + 检验有效期同步至证照管理（正式环境走服务端接口）
 * @returns {{ plateNo: string, inspectionValidUntil: string, photoCount: number } | null}
 */
const syncLicenseToCertificateManagement = (task, licenseForm, operator) => {
  if (!task?.plateNo) return null;
  const photos = serializeUploadFileList(licenseForm?.photos || []);
  const inspectionValidUntil = licenseForm?.inspectionValidUntil || '';
  const payload = {
    plateNo: task.plateNo,
    vin: task.vin || '',
    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 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 formatOperateStatusDisplay = (task) => {
  const base = task.operateStatus || '—';
  if (base === '库存' && (task.operateStatusRaw === '可运营' || task.operateStatusRaw === '待运营')) {
    return `库存（${task.operateStatusRaw}）`;
  }
  return base;
};

/** 完成时间展示至分钟 YYYY-MM-DD HH:mm */
const formatCompleteTime = (value) => {
  if (!value) return '—';
  if (moment) {
    const m = moment(value);
    if (m.isValid && m.isValid()) return m.format('YYYY-MM-DD HH:mm');
  }
  const normalized = String(value).trim().replace('T', ' ').slice(0, 16);
  return normalized.length >= 16 ? normalized : value;
};

const DEFAULT_FILTER = {
  provinceCity: null,
  expireRange: null,
  handler: '',
  executeTimeRange: null
};

/** 历史记录 · 办理人选项 */
const HANDLER_OPTIONS = [
  { label: '全部', value: '' },
  { label: '张明辉', value: '张明辉' },
  { label: '李晓彤', value: '李晓彤' },
  { label: '王建国', value: '王建国' }
];

/** 原型：当前登录办理人 */
const MOCK_CURRENT_HANDLER = '张明辉';

const formatDisplayMoney = (cost) => {
  if (cost === '' || cost == null) return '—';
  const n = Number(cost);
  return Number.isFinite(n) ? `${n.toFixed(2)} 元` : '—';
};

const hasServiceContent = (form) =>
  !!(form?.station || form?.cost || form?.remark || (form?.photos || []).length);

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: [] }
      : { ...EMPTY_SERVICE_FORM },
  zbExpanded: false,
  zbForm: { ...EMPTY_SERVICE_FORM }
});

const getHistorySnapshot = (task) => {
  const snap = task?.operateSnapshot || buildSampleHistorySnapshot(task);
  const licensePhotos = snap.licenseForm?.photos;
  if (task?.tab === 'history' && (!licensePhotos || !licensePhotos.length)) {
    return {
      ...snap,
      licenseForm: {
        ...snap.licenseForm,
        photos: buildMockLicensePhotos(task.id)
      }
    };
  }
  return snap;
};

const formatProvinceCity = (provinceCity) => {
  if (!provinceCity || !provinceCity[0]) return '';
  if (provinceCity[1]) return `${provinceCity[0]} / ${provinceCity[1]}`;
  return provinceCity[0];
};

const formatTaskRegion = (task) => {
  if (!task?.province) return '—';
  if (task.city) return `${task.province}-${task.city}`;
  return task.province;
};

const formatDateRangeDisplay = (range) => {
  if (!range || !range[0] || !range[1]) return '';
  if (moment) {
    const start = moment(range[0]);
    const end = moment(range[1]);
    if (start.isValid() && end.isValid()) {
      return `${start.format('YYYY-MM-DD')} ~ ${end.format('YYYY-MM-DD')}`;
    }
  }
  return '已选择时间范围';
};

const formatSingleDateDisplay = (val) => {
  if (!val) return '';
  if (moment) {
    const m = moment(val);
    if (m.isValid()) return m.format('YYYY-MM-DD');
  }
  if (typeof val === 'string') return val;
  return '';
};

const normalizePlateNo = (plate) => (plate || '').replace(/\s/g, '').toUpperCase();

const platesMatch = (a, b) => {
  const na = normalizePlateNo(a);
  const nb = normalizePlateNo(b);
  return na.length > 0 && na === nb;
};

/** 模拟 OCR 识别行驶证车牌号（原型：默认与任务一致；文件名含 mismatch 可模拟不一致） */
const mockOcrLicensePlate = (task, photos) => {
  const list = photos || [];
  const last = list[list.length - 1];
  const name = String(last?.name || last?.originFileObj?.name || '').toLowerCase();
  if (name.includes('mismatch') || name.includes('不一致')) {
    return '粤B00000D';
  }
  return task?.plateNo || '';
};

/** 模拟 OCR 仅识别到年月，返回该月最后一天 YYYY-MM-DD */
const mockOcrLicenseValidUntil = (task) => {
  const base = task?.expireDate || '2026-07-20';
  if (moment) {
    return moment(base).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;
  const day = getDaysInMonth(year, month);
  return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
};

const PAGE_STYLE = `
.ar-mini-root {
  min-height: 100vh;
  background: linear-gradient(180deg, #e8ebeF 0%, ${COLOR_PAGE} 32%);
  display: flex;
  justify-content: center;
  padding: 16px 12px 32px;
  box-sizing: border-box;
  font-family: ${FONT_FAMILY};
}
.ar-phone {
  width: 100%;
  max-width: 390px;
  min-height: 844px;
  background: ${COLOR_PAGE};
  border-radius: 28px;
  overflow: hidden;
  box-shadow: 0 20px 50px rgba(15, 23, 42, 0.12), 0 0 0 1px rgba(15, 23, 42, 0.06);
  display: flex;
  flex-direction: column;
  position: relative;
}
.ar-chrome {
  flex-shrink: 0;
  background: ${COLOR_BG};
}
.ar-status-bar {
  height: 44px;
  padding: 14px 24px 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
}
.ar-status-time {
  font-size: 15px;
  font-weight: 600;
  color: ${COLOR_TEXT};
  letter-spacing: -0.02em;
}
.ar-status-icons {
  display: flex;
  align-items: center;
  gap: 6px;
  color: ${COLOR_TEXT};
}
.ar-mp-navbar {
  height: 44px;
  display: flex;
  align-items: center;
  padding: 0 12px 0 8px;
  box-sizing: border-box;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  position: relative;
}
.ar-mp-navbar-left {
  display: flex;
  align-items: center;
  min-width: 72px;
  z-index: 2;
}
.ar-mp-navbar-title {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  max-width: 46%;
  font-size: 17px;
  font-weight: 600;
  color: ${COLOR_TEXT};
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}
.ar-mp-navbar-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: 2;
}
.ar-mp-back {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  color: ${COLOR_TEXT};
  cursor: pointer;
  border-radius: 8px;
  padding: 0;
}
.ar-mp-back:active {
  background: rgba(0, 0, 0, 0.05);
}
.ar-mp-link {
  border: none;
  background: transparent;
  color: ${COLOR_PRIMARY_DEEP};
  font-size: 13px;
  font-weight: 600;
  padding: 6px 4px;
  cursor: pointer;
  white-space: nowrap;
}
.ar-mp-capsule {
  display: flex;
  align-items: center;
  height: 32px;
  border-radius: 16px;
  border: 0.5px solid rgba(0, 0, 0, 0.12);
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
  overflow: hidden;
}
.ar-mp-capsule-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 32px;
  border: none;
  background: transparent;
  color: ${COLOR_TEXT};
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.ar-mp-capsule-btn:active {
  background: rgba(0, 0, 0, 0.06);
}
.ar-mp-capsule-btn--more {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 1px;
}
.ar-mp-capsule-close-icon {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1.5px solid ${COLOR_TEXT};
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  line-height: 1;
  font-weight: 300;
}
.ar-mp-capsule-divider {
  width: 0.5px;
  height: 18px;
  background: rgba(0, 0, 0, 0.12);
  flex-shrink: 0;
}
.ar-tabs {
  display: flex;
  border-bottom: 1px solid ${COLOR_LINE};
  background: ${COLOR_BG};
  flex-shrink: 0;
}
.ar-tab {
  flex: 1;
  text-align: center;
  padding: 12px 0;
  font-size: 15px;
  font-weight: 500;
  color: ${COLOR_MUTED};
  cursor: pointer;
  position: relative;
  border: none;
  background: transparent;
  min-height: 44px;
  transition: color 0.2s ease;
}
.ar-tab.active {
  color: ${COLOR_PRIMARY_DEEP};
  font-weight: 700;
}
.ar-tab.active::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: 28px;
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(90deg, ${COLOR_PRIMARY}, ${COLOR_PRIMARY_DEEP});
}
.ar-search-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: ${COLOR_PAGE};
  flex-shrink: 0;
}
.ar-search-box {
  flex: 1;
  position: relative;
  display: flex;
  align-items: center;
  background: ${COLOR_BG};
  border-radius: 22px;
  padding: 0 14px;
  min-height: 42px;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.05);
  cursor: pointer;
}
.ar-search-placeholder {
  position: absolute;
  left: 14px;
  right: 14px;
  font-size: 15px;
  color: ${COLOR_MUTED};
  pointer-events: none;
  line-height: 42px;
  z-index: 0;
}
.ar-search-box input {
  flex: 1;
  position: relative;
  z-index: 1;
  border: none;
  outline: none;
  font-size: 15px;
  background: transparent;
  color: ${COLOR_TEXT};
  min-width: 0;
  width: 100%;
  cursor: pointer;
  caret-color: transparent;
}
.ar-region-trigger {
  width: 100%;
  cursor: pointer;
  caret-color: transparent;
}
.ar-region-picker {
  display: flex;
  height: 220px;
  border: 1px solid ${COLOR_LINE};
  border-radius: 10px;
  overflow: hidden;
  background: ${COLOR_BG};
}
.ar-region-col {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.ar-region-col + .ar-region-col {
  border-left: 1px solid ${COLOR_LINE};
}
.ar-region-item {
  display: block;
  width: 100%;
  border: none;
  background: transparent;
  text-align: left;
  padding: 12px 14px;
  font-size: 15px;
  color: ${COLOR_TEXT};
  cursor: pointer;
  font-family: inherit;
}
.ar-region-item.active {
  background: rgba(22, 209, 161, 0.12);
  color: ${COLOR_PRIMARY_DEEP};
  font-weight: 600;
}
.ar-region-item:active {
  background: #f2f3f5;
}
.ar-region-drawer-hint {
  font-size: 12px;
  color: ${COLOR_MUTED};
  margin-bottom: 10px;
}
.ar-filter-section-label {
  font-size: 13px;
  font-weight: 600;
  color: ${COLOR_MUTED};
  margin: 8px 0 12px;
  padding-top: 4px;
  border-top: 1px solid ${COLOR_LINE};
}
.ar-mini-trigger {
  width: 100%;
  cursor: pointer;
  caret-color: transparent;
}
.ar-picker-single {
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid ${COLOR_LINE};
  border-radius: 10px;
  background: ${COLOR_BG};
  -webkit-overflow-scrolling: touch;
}
.ar-date-range-tabs {
  display: flex;
  gap: 6px;
  background: #f2f3f5;
  border-radius: 10px;
  padding: 4px;
  margin-bottom: 4px;
}
.ar-date-range-tab {
  flex: 1;
  padding: 8px 6px;
  border: none;
  border-radius: 8px;
  background: transparent;
  font-size: 13px;
  color: ${COLOR_MUTED};
  cursor: pointer;
  line-height: 1.35;
  transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.ar-date-range-tab.active {
  background: ${COLOR_BG};
  color: ${COLOR_TEXT};
  font-weight: 600;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
}
.ar-date-range-tab em {
  display: block;
  font-style: normal;
  font-size: 11px;
  font-weight: 400;
  color: ${COLOR_MUTED};
  margin-top: 3px;
}
.ar-date-range-tab.active em {
  color: ${COLOR_PRIMARY_DEEP};
  font-weight: 500;
}
.ar-date-wheel-wrap {
  position: relative;
  height: 220px;
  display: flex;
  border-radius: 10px;
  overflow: hidden;
  background: ${COLOR_BG};
  border: 1px solid ${COLOR_LINE};
}
.ar-date-wheel-highlight {
  position: absolute;
  left: 12px;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  height: 40px;
  border-top: 1px solid ${COLOR_LINE};
  border-bottom: 1px solid ${COLOR_LINE};
  pointer-events: none;
  z-index: 2;
  border-radius: 4px;
}
.ar-date-wheel-fade {
  position: absolute;
  left: 0;
  right: 0;
  height: 72px;
  pointer-events: none;
  z-index: 1;
}
.ar-date-wheel-fade.top {
  top: 0;
  background: linear-gradient(180deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 100%);
}
.ar-date-wheel-fade.bottom {
  bottom: 0;
  background: linear-gradient(0deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 100%);
}
.ar-date-wheel-col {
  flex: 1;
  height: 220px;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.ar-date-wheel-col::-webkit-scrollbar {
  display: none;
}
.ar-date-wheel-col + .ar-date-wheel-col {
  border-left: 1px solid ${COLOR_LINE};
}
.ar-date-wheel-pad {
  height: 90px;
  flex-shrink: 0;
}
.ar-date-wheel-item {
  height: 40px;
  line-height: 40px;
  text-align: center;
  scroll-snap-align: center;
  font-size: 15px;
  color: ${COLOR_MUTED};
  user-select: none;
}
.ar-date-wheel-item.active {
  color: ${COLOR_TEXT};
  font-weight: 600;
  font-size: 17px;
}
.ar-date-wheel-unit {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: 13px;
  color: ${COLOR_MUTED};
  pointer-events: none;
  z-index: 3;
  font-weight: 500;
}
.ar-date-wheel-unit.y { left: 33.33%; margin-left: 28px; }
.ar-date-wheel-unit.m { left: 66.66%; margin-left: 18px; }
.ar-date-wheel-unit.d { right: 12px; }
.ar-plate-kb-mask {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 30;
}
.ar-plate-kb {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 31;
  background: #ebedf0;
  border-radius: 14px 14px 0 0;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.12);
}
.ar-plate-kb-head {
  padding: 12px 16px 10px;
  background: ${COLOR_BG};
  border-radius: 14px 14px 0 0;
  border-bottom: 1px solid ${COLOR_LINE};
}
.ar-plate-kb-title {
  font-size: 13px;
  color: ${COLOR_MUTED};
  margin-bottom: 10px;
  text-align: center;
}
.ar-plate-cells {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex-wrap: wrap;
}
.ar-plate-cell {
  width: 34px;
  height: 44px;
  border: 1px solid ${COLOR_LINE};
  border-radius: 6px;
  background: ${COLOR_PAGE};
  font-size: 18px;
  font-weight: 700;
  color: ${COLOR_TEXT};
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
}
.ar-plate-cell.active {
  border-color: ${COLOR_PRIMARY_DEEP};
  box-shadow: 0 0 0 2px rgba(0, 191, 165, 0.2);
  background: ${COLOR_BG};
}
.ar-plate-cell.filled {
  color: ${COLOR_PRIMARY_DEEP};
}
.ar-plate-cell-sep {
  width: 6px;
  height: 4px;
  border-radius: 50%;
  background: ${COLOR_MUTED};
  opacity: 0.5;
  flex-shrink: 0;
}
.ar-plate-kb-toolbar {
  display: flex;
  gap: 10px;
  padding: 8px 12px;
  background: #ebedf0;
}
.ar-plate-kb-tool {
  flex: 1;
  height: 40px;
  border: none;
  border-radius: 8px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
}
.ar-plate-kb-tool--delete {
  background: #d8dce3;
  color: ${COLOR_TEXT};
}
.ar-plate-kb-tool--ok {
  background: linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%);
  color: #fff;
}
.ar-plate-keys {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 6px;
  padding: 0 10px 12px;
  max-height: 220px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.ar-plate-key {
  height: 42px;
  border: none;
  border-radius: 6px;
  background: ${COLOR_BG};
  font-size: 17px;
  font-weight: 600;
  color: ${COLOR_TEXT};
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
  font-family: inherit;
  padding: 0;
}
.ar-plate-key:active {
  background: #dfe3e8;
}
.ar-filter-btn {
  width: 42px;
  height: 42px;
  border-radius: 21px;
  border: none;
  background: ${COLOR_BG};
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
  flex-shrink: 0;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.ar-filter-btn:active {
  transform: scale(0.96);
}
.ar-list {
  flex: 1;
  overflow-y: auto;
  padding: 0 14px 24px;
  -webkit-overflow-scrolling: touch;
}
.ar-card {
  background: ${COLOR_BG};
  border-radius: 14px;
  padding: 14px 16px;
  margin-bottom: 12px;
  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
  border: 1px solid rgba(0,0,0,0.03);
  cursor: pointer;
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.ar-card:active {
  transform: scale(0.985);
  box-shadow: 0 1px 6px rgba(15, 23, 42, 0.06);
}
.ar-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.ar-plate-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
  flex: 1;
}
.ar-plate {
  font-size: 17px;
  font-weight: 800;
  color: ${COLOR_PRIMARY_DEEP};
  letter-spacing: 0.04em;
}
.ar-plate-tag {
  margin: 0 !important;
  font-size: 11px !important;
  line-height: 18px !important;
  border-radius: 4px !important;
  font-weight: 600 !important;
}
.ar-plate-tag--saved {
  color: ${COLOR_PRIMARY_DEEP} !important;
  border-color: rgba(22, 209, 161, 0.35) !important;
  background: rgba(22, 209, 161, 0.1) !important;
}
.ar-operate-foot-btns {
  display: flex;
  gap: 10px;
}
.ar-operate-foot-btns .ant-btn {
  flex: 1;
  height: 48px !important;
  border-radius: 10px !important;
  font-weight: 600;
}
.ar-card-head-right {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  margin-left: 8px;
}
.ar-card-status {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 14px;
  color: ${COLOR_MUTED};
  font-weight: 500;
  flex-shrink: 0;
}
.ar-kv {
  display: flex;
  font-size: 14px;
  line-height: 1.5;
  margin-top: 6px;
}
.ar-kv-label {
  color: ${COLOR_MUTED};
  flex: 0 0 72px;
}
.ar-kv-value {
  color: ${COLOR_TEXT};
  flex: 1;
  font-weight: 500;
}
.ar-empty {
  text-align: center;
  padding: 48px 16px;
  color: ${COLOR_MUTED};
  font-size: 14px;
}
.ar-filter-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  min-height: 40px;
}
.ar-filter-label {
  flex: 0 0 72px;
  text-align: right;
  font-size: 14px;
  font-weight: 500;
  color: ${COLOR_MUTED};
  line-height: 1.35;
}
.ar-filter-control {
  flex: 1;
  min-width: 0;
}
.ar-filter-control .ant-input,
.ar-filter-control .ant-select,
.ar-filter-control .ant-picker {
  width: 100% !important;
}
.ar-drawer-foot {
  display: flex;
  gap: 10px;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid ${COLOR_LINE};
}
.ar-operate-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.ar-operate-scroll {
  flex: 1;
  overflow-y: auto;
  padding: 12px 14px 100px;
  -webkit-overflow-scrolling: touch;
}
.ar-ocr-mask {
  position: absolute;
  inset: 0;
  z-index: 25;
  background: rgba(255, 255, 255, 0.78);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  touch-action: none;
}
.ar-ocr-mask-panel {
  text-align: center;
  padding: 28px 32px;
  background: ${COLOR_BG};
  border-radius: 14px;
  box-shadow: 0 10px 40px rgba(15, 23, 42, 0.14);
  border: 1px solid ${COLOR_LINE};
  min-width: 220px;
  max-width: 280px;
}
.ar-ocr-mask-title {
  font-size: 16px;
  font-weight: 700;
  color: ${COLOR_TEXT};
  margin-top: 14px;
}
.ar-ocr-mask-desc {
  font-size: 13px;
  color: ${COLOR_MUTED};
  margin-top: 8px;
  line-height: 1.45;
}
.ar-operate-foot .ant-btn-primary[disabled] {
  background: #c9cdd4 !important;
  border-color: #c9cdd4 !important;
  color: #fff !important;
  opacity: 1;
}
.ar-section {
  background: ${COLOR_BG};
  border-radius: 14px;
  padding: 14px 16px;
  margin-bottom: 12px;
  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
  border: 1px solid rgba(0,0,0,0.03);
}
.ar-section--form {
  padding: 14px 0 8px;
}
.ar-section--form .ar-section-title {
  padding: 0 20px 10px;
  margin-bottom: 0;
}
.ar-section-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px 10px;
  margin-bottom: 0;
}
.ar-section-title-row .ar-section-title-text {
  font-size: 15px;
  font-weight: 700;
  color: ${COLOR_TEXT};
}
.ar-section-fold {
  border: none;
  background: transparent;
  color: ${COLOR_PRIMARY_DEEP};
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  padding: 4px 0;
}
.ar-section-title {
  font-size: 15px;
  font-weight: 700;
  color: ${COLOR_TEXT};
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.ar-section-title .required {
  color: ${COLOR_DANGER};
  font-size: 14px;
}
.ar-form-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 52px;
  box-sizing: border-box;
  position: relative;
  border-bottom: none;
}
.ar-form-row::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: ${COLOR_LINE};
  pointer-events: none;
}
.ar-form-row:last-child::after {
  display: none;
}
.ar-form-label {
  flex: 0 0 92px;
  text-align: left;
  font-size: 14px;
  color: ${COLOR_MUTED};
  font-weight: 500;
  line-height: 1.35;
  flex-shrink: 0;
}
.ar-form-label.required::before {
  content: '*';
  color: ${COLOR_DANGER};
  margin-right: 2px;
}
.ar-form-control {
  flex: 1;
  min-width: 0;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
.ar-form-control .ant-input,
.ar-form-control .ant-select,
.ar-form-control .ant-input-number,
.ar-form-control .ant-picker {
  width: 100% !important;
  max-width: 100%;
}
.ar-form-control .ant-select-selector,
.ar-form-control .ant-input,
.ar-form-control .ant-input-number-input-wrap,
.ar-form-control .ant-input-number {
  min-height: 32px !important;
}
/* 隐藏式输入：无边框，点击即录入，内容右对齐 */
.ar-form-control .ant-input,
.ar-form-control .ant-input-number,
.ar-form-control .ant-input-number-input-wrap,
.ar-form-control textarea.ant-input {
  border: none !important;
  box-shadow: none !important;
  background: transparent !important;
  text-align: right !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
}
.ar-form-control .ant-input-number-input {
  text-align: right !important;
}
.ar-form-control .ant-input::placeholder,
.ar-form-control .ant-input-number-input::placeholder,
.ar-form-control textarea.ant-input::placeholder {
  color: ${COLOR_MUTED} !important;
  opacity: 1;
}
.ar-form-control .ant-input::placeholder,
.ar-form-control .ant-input-number-input::placeholder {
  text-align: right;
}
.ar-form-control .ant-input:focus,
.ar-form-control .ant-input-number-focused,
.ar-form-control .ant-input-number:focus-within,
.ar-form-control textarea.ant-input:focus {
  box-shadow: none !important;
  background: rgba(22, 209, 161, 0.06) !important;
  border-radius: 6px !important;
}
.ar-form-control .ant-select {
  text-align: right;
}
.ar-form-control .ant-select .ant-select-selector {
  border: none !important;
  box-shadow: none !important;
  background: transparent !important;
  padding-left: 4px !important;
  padding-right: 28px !important;
  display: flex !important;
  align-items: center !important;
  justify-content: flex-end !important;
}
.ar-form-control .ant-select .ant-select-arrow {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 24px;
  margin: 0;
  pointer-events: none;
  flex-shrink: 0;
}
.ar-form-control .ant-select .ant-select-selection-wrap,
.ar-form-control .ant-select .ant-select-selection-overflow {
  flex: 1;
  min-width: 0;
  justify-content: flex-end;
}
.ar-form-control .ant-select .ant-select-selection-search {
  inset-inline-start: auto !important;
  inset-inline-end: 28px !important;
  left: auto !important;
  right: 28px !important;
  width: auto !important;
  max-width: calc(100% - 28px);
}
.ar-form-control .ant-select .ant-select-selection-search-input {
  text-align: right !important;
}
.ar-form-control .ant-select-selection-item,
.ar-form-control .ant-select-selection-placeholder,
.ar-form-control .ant-select .ant-select-selection-selected-value,
.ar-form-control .ant-select .ant-select-selection__placeholder {
  flex: 1;
  min-width: 0;
  text-align: right !important;
  padding-inline-end: 4px !important;
  padding-right: 4px !important;
  margin-inline-end: 0 !important;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  justify-content: flex-end;
}
.ar-form-control .ant-select-selection-placeholder,
.ar-form-control .ant-select .ant-select-selection__placeholder {
  color: ${COLOR_MUTED} !important;
}
.ar-form-control .ant-select-focused .ant-select-selector {
  background: rgba(22, 209, 161, 0.06) !important;
  border-radius: 6px !important;
}
/* Ant Design 4 下拉兼容 */
.ar-form-control .ant-select-selection {
  border: none !important;
  box-shadow: none !important;
  background: transparent !important;
  padding-right: 28px !important;
}
.ar-form-control .ant-select-selection__rendered {
  margin-left: 0 !important;
  margin-right: 0 !important;
  text-align: right;
}
.ar-form-control .ant-select-selection-selected-value,
.ar-form-control .ant-select-selection__placeholder {
  float: right !important;
  max-width: calc(100% - 4px);
  padding-right: 4px;
  text-align: right !important;
}
.ar-form-control .ant-select-arrow {
  right: 0 !important;
}
.ar-form-control .ant-input-number-handler-wrap {
  display: none;
}
.ar-form-readonly {
  box-sizing: border-box;
  width: 100%;
  min-height: 32px;
  padding: 4px 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  font-size: 14px;
  line-height: 1.5715;
  color: ${COLOR_TEXT};
  font-weight: 500;
  word-break: break-all;
  text-align: right;
  background: transparent;
}
.ar-form-readonly.plate {
  font-size: 17px;
  font-weight: 800;
  color: ${COLOR_PRIMARY_DEEP};
  letter-spacing: 0.04em;
}
.ar-form-readonly--multiline {
  white-space: pre-wrap;
  word-break: break-word;
  line-height: 1.45;
}
.ar-operate-scroll .ar-section {
  text-align: left;
}
.ar-form-grid {
  display: flex;
  flex-direction: column;
}
.ar-form-grid .ar-form-row {
  padding-left: 20px;
  padding-right: 20px;
}
.ar-form-grid .ar-form-row::after {
  left: 20px;
  right: 20px;
}
.ar-form-grid .ar-form-control .ant-input-affix-wrapper,
.ar-form-grid .ar-form-control textarea.ant-input {
  resize: none;
}
.ar-form-grid .ar-form-row--textarea textarea.ant-input {
  height: 32px !important;
  min-height: 32px !important;
  line-height: 22px !important;
  padding-top: 4px !important;
  padding-bottom: 4px !important;
}
.ar-form-row--remark-block {
  min-height: 72px;
  align-items: flex-start;
  padding-top: 8px;
  padding-bottom: 10px;
}
.ar-form-row--remark-block .ar-form-control {
  width: 100%;
  justify-content: stretch;
}
.ar-form-row--remark-block textarea.ant-input {
  width: 100% !important;
  min-height: 52px !important;
  height: 52px !important;
  line-height: 22px !important;
  padding: 4px 0 !important;
  text-align: left !important;
  resize: none !important;
}
.ar-form-row--remark-block textarea.ant-input::placeholder {
  color: ${COLOR_MUTED} !important;
  text-align: left;
}
.ar-photo-block {
  padding: 4px 20px 16px;
}
.ar-photo-block-title {
  font-size: 14px;
  font-weight: 500;
  color: ${COLOR_MUTED};
  margin-bottom: 10px;
  text-align: left;
}
.ar-photo-block-title.required::before {
  content: '*';
  color: ${COLOR_DANGER};
  margin-right: 2px;
}
.ar-photo-hint {
  margin-top: 8px;
  font-size: 12px;
  color: ${COLOR_MUTED};
  text-align: left;
  line-height: 1.4;
}
.ar-photo-empty {
  margin: 0 20px 8px;
  padding: 28px 12px;
  text-align: center;
  font-size: 13px;
  color: ${COLOR_MUTED};
  background: #f7f8fa;
  border-radius: 10px;
  border: 1px dashed ${COLOR_LINE};
}
.ar-photo-gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
.ar-photo-upload-cell {
  display: block;
  width: 100%;
  min-width: 0;
}
.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-slot {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  border-radius: 8px;
  box-sizing: border-box;
}
.ar-photo-slot--add {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: 1px dashed ${COLOR_LINE};
  background: #fafafa;
  color: ${COLOR_MUTED};
  cursor: pointer;
  gap: 2px;
}
.ar-photo-slot--add:active {
  border-color: ${COLOR_PRIMARY};
  background: #f0fdf9;
}
.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: ${COLOR_PAGE};
  cursor: pointer;
}
.ar-photo-item-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ar-photo-del {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid #fff;
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  font-size: 14px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  z-index: 2;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.ar-photo-del:active {
  background: ${COLOR_DANGER};
}
.ar-photo-preview-img {
  width: 100%;
  max-height: 70vh;
  object-fit: contain;
}
.ar-ocr-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  margin-bottom: 12px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.4;
}
.ar-ocr-banner--done {
  background: #f2f3f5;
  color: ${COLOR_MUTED};
}
.ar-ocr-banner--error {
  background: rgba(245, 63, 63, 0.08);
  color: ${COLOR_DANGER};
}
.ar-ocr-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: ${COLOR_PRIMARY};
  flex-shrink: 0;
  animation: ar-ocr-pulse 1s ease-in-out infinite;
}
@keyframes ar-ocr-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1); }
}
.ar-form-control .ar-form-date-trigger {
  width: 100% !important;
  max-width: 100%;
  min-height: 32px !important;
  border: none !important;
  box-shadow: none !important;
  background: transparent !important;
  text-align: right !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
  cursor: pointer;
  caret-color: transparent;
}
.ar-form-control .ar-form-date-trigger::placeholder {
  color: ${COLOR_MUTED} !important;
  opacity: 1;
  text-align: right;
}
.ar-form-control .ar-form-date-trigger:focus {
  box-shadow: none !important;
  background: rgba(22, 209, 161, 0.06) !important;
  border-radius: 6px !important;
}
.ar-form-control .ar-form-date-trigger[disabled] {
  color: ${COLOR_MUTED};
  cursor: not-allowed;
  opacity: 0.65;
}
.ar-add-btn {
  width: 100%;
  box-sizing: border-box;
  min-height: 48px;
  padding: 12px 16px;
  margin-bottom: 12px;
  background: ${COLOR_BG};
  border-radius: 10px;
  border: 1px dashed ${COLOR_LINE};
  font-family: inherit;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: 15px;
  font-weight: 600;
  color: ${COLOR_TEXT};
  line-height: 1.4;
  transition: border-color 0.2s ease, background 0.2s ease;
}
.ar-add-btn:active {
  background: #f7f8fa;
}
.ar-add-btn.filled {
  border-style: solid;
  border-color: rgba(22, 209, 161, 0.4);
  color: ${COLOR_PRIMARY_DEEP};
}
.ar-add-btn-sub {
  margin-top: 4px;
  font-size: 12px;
  font-weight: 400;
  color: ${COLOR_MUTED};
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ar-operate-foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
  background: ${COLOR_BG};
  border-top: 1px solid ${COLOR_LINE};
  box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
}
.ar-sheet-body {
  padding: 4px 0 8px;
  max-height: 70vh;
  overflow-y: auto;
}
.ar-prd-doc {
  font-size: 13px;
  line-height: 1.7;
  color: ${COLOR_TEXT};
}
.ar-prd-meta {
  font-size: 12px;
  color: ${COLOR_MUTED};
  margin-bottom: 12px;
  padding-bottom: 10px;
  border-bottom: 1px solid ${COLOR_LINE};
}
.ar-prd-h2 {
  font-size: 15px;
  font-weight: 700;
  color: ${COLOR_TEXT};
  margin: 18px 0 8px;
}
.ar-prd-h2:first-of-type {
  margin-top: 4px;
}
.ar-prd-h3 {
  font-size: 14px;
  font-weight: 600;
  color: ${COLOR_TEXT};
  margin: 12px 0 6px;
}
.ar-prd-p {
  margin: 0 0 8px;
  color: ${COLOR_TEXT};
}
.ar-prd-ul, .ar-prd-ol {
  margin: 0 0 10px;
  padding-left: 20px;
}
.ar-prd-li {
  margin-bottom: 5px;
}
.ar-prd-table-wrap {
  overflow-x: auto;
  margin: 8px 0 12px;
}
.ar-prd-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}
.ar-prd-table th,
.ar-prd-table td {
  border: 1px solid ${COLOR_LINE};
  padding: 6px 8px;
  text-align: left;
  vertical-align: top;
}
.ar-prd-table th {
  background: #f7f8fa;
  font-weight: 600;
  white-space: nowrap;
}
.ar-prd-footnote {
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid ${COLOR_LINE};
  font-size: 12px;
  color: ${COLOR_MUTED};
  line-height: 1.55;
}
.ar-prd-highlight {
  margin: 14px 0;
  padding: 12px 14px;
  background: rgba(22, 209, 161, 0.08);
  border: 1px solid rgba(22, 209, 161, 0.35);
  border-radius: 10px;
}
.ar-prd-highlight-title {
  font-size: 14px;
  font-weight: 700;
  color: ${COLOR_PRIMARY_DEEP};
  margin-bottom: 8px;
}
@media (prefers-reduced-motion: reduce) {
  .ar-card, .ar-filter-btn, .ar-tab { transition: none; }
}
`;

const XLL_GREEN = '#7AB929';
const XLL_GREEN_DEEP = '#6AA322';
const XLL_GREEN_SOFT = 'rgba(122, 185, 41, 0.14)';

const EMBED_STYLE = `
.ar-embed-root {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: ${COLOR_PAGE};
  position: relative;
}
.ar-embed-root .ar-tabs,
.ar-embed-root .ar-search-row { flex-shrink: 0; }
.ar-embed-root .ar-list,
.ar-embed-root .ar-operate-scroll,
.ar-embed-root .ar-history-scroll { flex: 1; min-height: 0; }
`;

const XLL_THEME_PATCH = `
.xll-module-theme .ar-tab.active { color: ${XLL_GREEN}; }
.xll-module-theme .ar-tab.active::after { background: ${XLL_GREEN}; }
.xll-module-theme .ar-filter-btn:active { border-color: ${XLL_GREEN}; color: ${XLL_GREEN}; }
.xll-module-theme .ar-card-action { color: ${XLL_GREEN}; }
.xll-module-theme .ar-add-btn { color: ${XLL_GREEN_DEEP}; }
.xll-module-theme .ar-mp-link { color: ${XLL_GREEN_DEEP}; }
.xll-module-theme .ar-form-control .ant-select-focused .ant-select-selector {
  background: ${XLL_GREEN_SOFT} !important;
}
.xll-module-theme .ar-operate-foot .ant-btn-primary {
  background: linear-gradient(135deg, ${XLL_GREEN} 0%, ${XLL_GREEN_DEEP} 100%) !important;
  border: none !important;
}
`;

const IconFilter = () => (
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={COLOR_PRIMARY_DEEP} strokeWidth="2" strokeLinecap="round">
    <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
  </svg>
);

const IconChevron = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
    <polyline points="9 18 15 12 9 6" />
  </svg>
);

const IconBack = () => (
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <polyline points="15 18 9 12 15 6" />
  </svg>
);

const IconStatusSignal = () => (
  <svg width="18" height="12" viewBox="0 0 18 12" fill="currentColor" aria-hidden="true">
    <rect x="0" y="7" width="3" height="5" rx="0.5" opacity="0.35" />
    <rect x="5" y="5" width="3" height="7" rx="0.5" opacity="0.55" />
    <rect x="10" y="2" width="3" height="10" rx="0.5" opacity="0.75" />
    <rect x="15" y="0" width="3" height="12" rx="0.5" />
  </svg>
);

const IconStatusWifi = () => (
  <svg width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
    <path d="M8 10.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" fill="currentColor" stroke="none" />
    <path d="M4.5 7.2a4.8 4.8 0 0 1 7 0" strokeLinecap="round" />
    <path d="M1.5 4.2a8.5 8.5 0 0 1 13 0" strokeLinecap="round" />
  </svg>
);

const IconStatusBattery = () => (
  <svg width="26" height="12" viewBox="0 0 26 12" fill="none" aria-hidden="true">
    <rect x="0.5" y="0.5" width="21" height="11" rx="2.5" stroke="currentColor" strokeOpacity="0.45" />
    <rect x="2.5" y="2.5" width="16" height="7" rx="1.5" fill="currentColor" />
    <path d="M23 4.5v3c.8-.45 1.3-1.15 1.3-1.5S23.8 4.95 23 4.5Z" fill="currentColor" fillOpacity="0.45" />
  </svg>
);

const IphoneStatusBar = () => (
  <div className="ar-status-bar">
    <span className="ar-status-time">9:41</span>
    <div className="ar-status-icons">
      <IconStatusSignal />
      <IconStatusWifi />
      <IconStatusBattery />
    </div>
  </div>
);

const MpCapsule = ({ onMore, onExit }) => (
  <div className="ar-mp-capsule" role="group" aria-label="小程序菜单">
    <button type="button" className="ar-mp-capsule-btn ar-mp-capsule-btn--more" onClick={onMore} aria-label="更多">
      ···
    </button>
    <span className="ar-mp-capsule-divider" aria-hidden="true" />
    <button type="button" className="ar-mp-capsule-btn" onClick={onExit} aria-label="关闭小程序">
      <span className="ar-mp-capsule-close-icon">×</span>
    </button>
  </div>
);

const MpNavBar = ({ title, showBack, onBack, showPrdLink, onPrdClick, onMore, onExit }) => (
  <div className="ar-mp-navbar">
    <div className="ar-mp-navbar-left">
      {showBack ? (
        <button type="button" className="ar-mp-back" onClick={onBack} aria-label="返回">
          <IconBack />
        </button>
      ) : null}
    </div>
    <div className="ar-mp-navbar-title">{title}</div>
    <div className="ar-mp-navbar-right">
      {showPrdLink && (
        <button type="button" className="ar-mp-link" onClick={onPrdClick}>
          需求说明
        </button>
      )}
      <MpCapsule onMore={onMore} onExit={onExit} />
    </div>
  </div>
);

const MiniProgramChrome = ({ title, showBack, onBack, showPrdLink, onPrdClick }) => {
  const handleMore = () => message.info('更多菜单（原型）');
  const handleExit = () => message.warning('退出小程序（原型）');

  return (
    <div className="ar-chrome">
      <IphoneStatusBar />
      <MpNavBar
        title={title}
        showBack={showBack}
        onBack={onBack}
        showPrdLink={showPrdLink}
        onPrdClick={onPrdClick}
        onMore={handleMore}
        onExit={handleExit}
      />
    </div>
  );
};

const FormField = ({ label, required, rowClassName, children }) => (
  <div className={`ar-form-row${rowClassName ? ` ${rowClassName}` : ''}`}>
    <span className={`ar-form-label${required ? ' required' : ''}`}>{label}</span>
    <div className="ar-form-control">{children}</div>
  </div>
);

const InfoRow = ({ label, children, valueClassName }) => (
  <div className="ar-form-row">
    <span className="ar-form-label">{label}</span>
    <div className="ar-form-control">
      <div className={`ar-form-readonly${valueClassName ? ` ${valueClassName}` : ''}`}>{children}</div>
    </div>
  </div>
);

const PlateKeyboardPanel = ({ open, value, onChange, onConfirm, onClose }) => {
  const [activeIndex, setActiveIndex] = useState(0);
  const plate = (value || '').toUpperCase();

  useEffect(() => {
    if (open) {
      setActiveIndex(Math.min(plate.length, MAX_PLATE_INPUT_LEN - 1));
    }
  }, [open, plate.length]);

  if (!open) return null;

  const keys = getPlateKeysForIndex(activeIndex);

  const handleKey = (key) => {
    const arr = plate.split('');
    arr[activeIndex] = key;
    const next = arr.join('').slice(0, MAX_PLATE_INPUT_LEN);
    onChange(next);
    if (activeIndex < MAX_PLATE_INPUT_LEN - 1) {
      setActiveIndex(activeIndex + 1);
    }
  };

  const handleDelete = () => {
    if (!plate.length) return;
    const next = plate.slice(0, -1);
    onChange(next);
    setActiveIndex(Math.max(0, next.length));
  };

  const cells = [];
  for (let i = 0; i < MAX_PLATE_INPUT_LEN; i += 1) {
    if (i === 2) {
      cells.push(<span key="sep" className="ar-plate-cell-sep" aria-hidden="true" />);
    }
    cells.push(
      <button
        key={`cell-${i}`}
        type="button"
        className={`ar-plate-cell${activeIndex === i ? ' active' : ''}${plate[i] ? ' filled' : ''}`}
        onClick={() => setActiveIndex(i)}
        aria-label={`车牌第${i + 1}位`}
      >
        {plate[i] || ''}
      </button>
    );
  }

  return (
    <>
      <div className="ar-plate-kb-mask" role="presentation" onClick={onClose} />
      <div className="ar-plate-kb" role="dialog" aria-label="车牌号输入">
        <div className="ar-plate-kb-head">
          <div className="ar-plate-kb-title">请输入车牌号</div>
          <div className="ar-plate-cells">{cells}</div>
        </div>
        <div className="ar-plate-kb-toolbar">
          <button type="button" className="ar-plate-kb-tool ar-plate-kb-tool--delete" onClick={handleDelete}>
            删除
          </button>
          <button
            type="button"
            className="ar-plate-kb-tool ar-plate-kb-tool--ok"
            onClick={() => onConfirm(plate)}
          >
            完成
          </button>
        </div>
        <div className="ar-plate-keys">
          {keys.map((key) => (
            <button key={`${activeIndex}-${key}`} type="button" className="ar-plate-key" onClick={() => handleKey(key)}>
              {key}
            </button>
          ))}
        </div>
      </div>
    </>
  );
};

const PhotoUploadBlock = ({ label, fileList, onChange, maxPhotos = MAX_SERVICE_PHOTOS, required }) => {
  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 (
    <div className="ar-photo-block">
      <div className={`ar-photo-block-title${required ? ' required' : ''}`}>{label}</div>
      <div className="ar-photo-gallery">
        {canUpload && Upload ? (
          <Upload
            className="ar-photo-upload-cell"
            showUploadList={false}
            multiple
            accept="image/*"
            fileList={list}
            beforeUpload={() => false}
            onChange={handleChange}
          >
            <div className="ar-photo-slot ar-photo-slot--add">
              <span className="ar-photo-add-icon">+</span>
              <span className="ar-photo-add-text">上传</span>
            </div>
          </Upload>
        ) : null}
        {list.map((file) => {
          const url = getUploadPreviewUrl(file);
          return (
            <div key={file.uid} className="ar-photo-slot ar-photo-item">
              <button
                type="button"
                className="ar-photo-item-thumb"
                onClick={() => handlePreview(file)}
                aria-label="预览照片"
              >
                {url ? <img src={url} alt="" /> : <span style={{ fontSize: 12, color: COLOR_MUTED }}>图片</span>}
              </button>
              <button
                type="button"
                className="ar-photo-del"
                aria-label="删除照片"
                onClick={(e) => handleRemove(file.uid, e)}
              >
                ×
              </button>
            </div>
          );
        })}
      </div>
      <div className="ar-photo-hint">最多上传 {maxPhotos} 张，支持批量选择</div>
      <Modal
        open={previewOpen}
        footer={null}
        onCancel={() => setPreviewOpen(false)}
        centered
        width="92%"
        styles={{ body: { padding: 8, textAlign: 'center' } }}
        destroyOnClose
      >
        {previewUrl ? <img className="ar-photo-preview-img" src={previewUrl} alt="预览" /> : null}
      </Modal>
    </div>
  );
};

/** 历史记录只读照片（仅预览，与办理页照片网格一致） */
const PhotoReadonlyGallery = ({ label, fileList, emptyText }) => {
  const list = fileList || [];
  const [previewOpen, setPreviewOpen] = useState(false);
  const [previewUrl, setPreviewUrl] = useState('');

  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 (
    <div className="ar-photo-block">
      <div className="ar-photo-block-title">{label}</div>
      {list.length > 0 ? (
        <div className="ar-photo-gallery">
          {list.map((file) => {
            const url = getUploadPreviewUrl(file);
            return (
              <div key={file.uid} className="ar-photo-slot ar-photo-item">
                <button
                  type="button"
                  className="ar-photo-item-thumb"
                  onClick={() => handlePreview(file)}
                  aria-label="预览照片"
                >
                  {url ? <img src={url} alt="" /> : <span style={{ fontSize: 12, color: COLOR_MUTED }}>图片</span>}
                </button>
              </div>
            );
          })}
        </div>
      ) : (
        <div className="ar-photo-empty">{emptyText || '暂无照片'}</div>
      )}
      <Modal
        open={previewOpen}
        footer={null}
        onCancel={() => setPreviewOpen(false)}
        centered
        width="92%"
        styles={{ body: { padding: 8, textAlign: 'center' } }}
        destroyOnClose
      >
        {previewUrl ? <img className="ar-photo-preview-img" src={previewUrl} alt="预览" /> : null}
      </Modal>
    </div>
  );
};

const FilterRow = ({ label, children }) => (
  <div className="ar-filter-row">
    <span className="ar-filter-label">{label}</span>
    <div className="ar-filter-control">{children}</div>
  </div>
);

/** 小程序风格省市级联：底部抽屉 + 双列选择 */
const MiniRegionPicker = ({ value, onChange, placeholder }) => {
  const [open, setOpen] = useState(false);
  const [draftProvince, setDraftProvince] = useState(null);
  const [draftCity, setDraftCity] = useState(null);

  useEffect(() => {
    if (!open) return;
    const province = value?.[0] || PROVINCE_LIST[0];
    setDraftProvince(province);
    const cities = PROVINCE_CITY_MAP[province] || [];
    const city = value?.[1] && cities.includes(value[1]) ? value[1] : cities[0] || null;
    setDraftCity(city);
  }, [open, value]);

  const cityList = draftProvince ? PROVINCE_CITY_MAP[draftProvince] || [] : [];
  const displayText = formatProvinceCity(value);

  const handleProvincePick = (province) => {
    setDraftProvince(province);
    const cities = PROVINCE_CITY_MAP[province] || [];
    setDraftCity(cities[0] || null);
  };

  const handleConfirm = () => {
    if (!draftProvince) {
      setOpen(false);
      return;
    }
    const city = draftCity || (PROVINCE_CITY_MAP[draftProvince] || [])[0];
    onChange(city ? [draftProvince, city] : [draftProvince]);
    setOpen(false);
  };

  const handleClear = () => {
    onChange(null);
    setOpen(false);
  };

  return (
    <>
      <Input
        className="ar-region-trigger"
        readOnly
        value={displayText}
        placeholder={placeholder || '请选择省市'}
        onClick={() => setOpen(true)}
        onFocus={(e) => e.target.blur()}
      />
      <Drawer
        title="选择运营区域"
        placement="bottom"
        height={360}
        open={open}
        onClose={() => setOpen(false)}
        styles={{ body: { padding: '12px 16px 0' } }}
        footer={
          <div className="ar-drawer-foot">
            <Button block size="large" onClick={handleClear} style={{ height: 46, borderRadius: 8 }}>
              清空
            </Button>
            <Button
              block
              type="primary"
              size="large"
              onClick={handleConfirm}
              style={{
                height: 46,
                borderRadius: 8,
                background: `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
                border: 'none'
              }}
            >
              确定
            </Button>
          </div>
        }
      >
        <div className="ar-region-drawer-hint">先选省份，再选城市</div>
        <div className="ar-region-picker">
          <div className="ar-region-col" role="listbox" aria-label="省份">
            {PROVINCE_LIST.map((p) => (
              <button
                key={p}
                type="button"
                className={`ar-region-item${draftProvince === p ? ' active' : ''}`}
                onClick={() => handleProvincePick(p)}
              >
                {p}
              </button>
            ))}
          </div>
          <div className="ar-region-col" role="listbox" aria-label="城市">
            {cityList.map((c) => (
              <button
                key={c}
                type="button"
                className={`ar-region-item${draftCity === c ? ' active' : ''}`}
                onClick={() => setDraftCity(c)}
              >
                {c}
              </button>
            ))}
          </div>
        </div>
      </Drawer>
    </>
  );
};

/** 小程序风格单选（办理人等） */
const MiniOptionPicker = ({ value, onChange, options, placeholder, title, clearLabel }) => {
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState(value ?? '');

  useEffect(() => {
    if (open) setDraft(value ?? '');
  }, [open, value]);

  const selected = options.find((o) => o.value === (value ?? ''));
  const displayText = selected && selected.value !== '' ? selected.label : '';

  const handleConfirm = () => {
    onChange(draft);
    setOpen(false);
  };

  const handleClear = () => {
    onChange('');
    setOpen(false);
  };

  return (
    <>
      <Input
        className="ar-mini-trigger"
        readOnly
        value={displayText}
        placeholder={placeholder || '请选择'}
        onClick={() => setOpen(true)}
        onFocus={(e) => e.target.blur()}
      />
      <Drawer
        title={title || '请选择'}
        placement="bottom"
        height={320}
        open={open}
        onClose={() => setOpen(false)}
        styles={{ body: { padding: '12px 16px 0' } }}
        footer={
          <div className="ar-drawer-foot">
            <Button block size="large" onClick={handleClear} style={{ height: 46, borderRadius: 8 }}>
              {clearLabel || '全部'}
            </Button>
            <Button
              block
              type="primary"
              size="large"
              onClick={handleConfirm}
              style={{
                height: 46,
                borderRadius: 8,
                background: `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
                border: 'none'
              }}
            >
              确定
            </Button>
          </div>
        }
      >
        <div className="ar-picker-single" role="listbox">
          {options.map((opt) => (
            <button
              key={opt.value || '__all__'}
              type="button"
              className={`ar-region-item${draft === opt.value ? ' active' : ''}`}
              onClick={() => setDraft(opt.value)}
            >
              {opt.label}
            </button>
          ))}
        </div>
      </Drawer>
    </>
  );
};

const WHEEL_ITEM_H = 40;
const DATE_YEAR_MIN = 2018;
const DATE_YEAR_MAX = 2032;

const getDaysInMonth = (year, month) => new Date(year, month, 0).getDate();

const todayDateParts = () => {
  const t = new Date();
  return { year: t.getFullYear(), month: t.getMonth() + 1, day: t.getDate() };
};

const partsFromMomentValue = (val) => {
  if (val && moment) {
    const m = moment(val);
    if (m.isValid()) return { year: m.year(), month: m.month() + 1, day: m.date() };
  }
  return todayDateParts();
};

const clampDateParts = (parts) => {
  const maxDay = getDaysInMonth(parts.year, parts.month);
  return { ...parts, day: Math.min(parts.day, maxDay) };
};

const partsToMoment = (parts) => {
  if (!moment || !parts) return null;
  const p = clampDateParts(parts);
  const s = `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}`;
  const m = moment(s, 'YYYY-MM-DD');
  return m.isValid() ? m : null;
};

const formatPartsLabel = (parts) => {
  const p = clampDateParts(parts);
  return `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}`;
};

/** 微信小程序风格滚轮列（年 / 月 / 日） */
const DateWheelColumn = ({ items, value, onChange }) => {
  const colRef = useRef(null);
  const scrollTimer = useRef(null);

  const scrollToValue = useCallback(
    (v, smooth) => {
      const el = colRef.current;
      if (!el) return;
      const idx = items.indexOf(v);
      if (idx < 0) return;
      el.scrollTo({ top: idx * WHEEL_ITEM_H, behavior: smooth ? 'smooth' : 'auto' });
    },
    [items]
  );

  useEffect(() => {
    scrollToValue(value, false);
  }, [value, items, scrollToValue]);

  const handleScroll = () => {
    const el = colRef.current;
    if (!el) return;
    if (scrollTimer.current) clearTimeout(scrollTimer.current);
    scrollTimer.current = setTimeout(() => {
      const idx = Math.round(el.scrollTop / WHEEL_ITEM_H);
      const clamped = Math.max(0, Math.min(idx, items.length - 1));
      const next = items[clamped];
      if (next !== value) onChange(next);
      scrollToValue(next, true);
    }, 80);
  };

  return (
    <div className="ar-date-wheel-col" ref={colRef} onScroll={handleScroll}>
      <div className="ar-date-wheel-pad" aria-hidden="true" />
      {items.map((item) => (
        <div key={item} className={`ar-date-wheel-item${item === value ? ' active' : ''}`}>
          {item}
        </div>
      ))}
      <div className="ar-date-wheel-pad" aria-hidden="true" />
    </div>
  );
};

/** 小程序风格单日选择（滚轮，精确到日） */
const MiniSingleDatePicker = ({
  value,
  onChange,
  placeholder,
  title,
  hint,
  disabled,
  inForm
}) => {
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState(todayDateParts);

  useEffect(() => {
    if (!open) return;
    setDraft(partsFromMomentValue(value));
  }, [open, value]);

  const years = useMemo(
    () => Array.from({ length: DATE_YEAR_MAX - DATE_YEAR_MIN + 1 }, (_, i) => DATE_YEAR_MIN + i),
    []
  );
  const months = useMemo(() => Array.from({ length: 12 }, (_, i) => i + 1), []);
  const days = useMemo(() => {
    const max = getDaysInMonth(draft.year, draft.month);
    return Array.from({ length: max }, (_, i) => i + 1);
  }, [draft.year, draft.month]);

  useEffect(() => {
    const max = getDaysInMonth(draft.year, draft.month);
    if (draft.day <= max) return;
    setDraft((p) => ({ ...p, day: max }));
  }, [draft.year, draft.month, draft.day]);

  const displayText = formatSingleDateDisplay(value);

  const handleConfirm = () => {
    const m = partsToMoment(draft);
    if (!m) {
      message.warning('请选择日期');
      return;
    }
    onChange(m.format('YYYY-MM-DD'));
    setOpen(false);
  };

  const handleClear = () => {
    onChange(null);
    setOpen(false);
  };

  const inputCls = inForm ? 'ar-form-date-trigger' : 'ar-mini-trigger';

  return (
    <>
      <Input
        className={inputCls}
        bordered={false}
        readOnly
        disabled={disabled}
        value={displayText}
        placeholder={placeholder || '请选择日期'}
        onClick={() => {
          if (disabled) return;
          setOpen(true);
        }}
        onFocus={(e) => e.target.blur()}
      />
      <Drawer
        title={title || '选择日期'}
        placement="bottom"
        height={380}
        open={open}
        onClose={() => setOpen(false)}
        styles={{ body: { padding: '12px 16px 0' } }}
        footer={
          <div className="ar-drawer-foot">
            <Button block size="large" onClick={handleClear} style={{ height: 46, borderRadius: 8 }}>
              清空
            </Button>
            <Button
              block
              type="primary"
              size="large"
              onClick={handleConfirm}
              style={{
                height: 46,
                borderRadius: 8,
                background: `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
                border: 'none'
              }}
            >
              确定
            </Button>
          </div>
        }
      >
        <div className="ar-region-drawer-hint">{hint || '滑动滚轮选择年月日'}</div>
        <div className="ar-date-wheel-wrap" key={`${draft.year}-${draft.month}`}>
          <div className="ar-date-wheel-fade top" aria-hidden="true" />
          <div className="ar-date-wheel-fade bottom" aria-hidden="true" />
          <div className="ar-date-wheel-highlight" aria-hidden="true" />
          <span className="ar-date-wheel-unit y">年</span>
          <span className="ar-date-wheel-unit m">月</span>
          <span className="ar-date-wheel-unit d">日</span>
          <DateWheelColumn items={years} value={draft.year} onChange={(y) => setDraft((p) => clampDateParts({ ...p, year: y }))} />
          <DateWheelColumn
            items={months}
            value={draft.month}
            onChange={(mo) => setDraft((p) => clampDateParts({ ...p, month: mo }))}
          />
          <DateWheelColumn items={days} value={draft.day} onChange={(d) => setDraft((p) => ({ ...p, day: d }))} />
        </div>
      </Drawer>
    </>
  );
};

/** 小程序风格开始-结束日期（滚轮，精确到日） */
const MiniDateRangePicker = ({ value, onChange, placeholder, title, hint }) => {
  const [open, setOpen] = useState(false);
  const [activeEdge, setActiveEdge] = useState('start');
  const [draftStart, setDraftStart] = useState(todayDateParts);
  const [draftEnd, setDraftEnd] = useState(todayDateParts);

  useEffect(() => {
    if (!open) return;
    setActiveEdge('start');
    setDraftStart(partsFromMomentValue(value?.[0]));
    setDraftEnd(partsFromMomentValue(value?.[1]));
  }, [open, value]);

  const activeParts = activeEdge === 'start' ? draftStart : draftEnd;

  const years = useMemo(
    () => Array.from({ length: DATE_YEAR_MAX - DATE_YEAR_MIN + 1 }, (_, i) => DATE_YEAR_MIN + i),
    []
  );
  const months = useMemo(() => Array.from({ length: 12 }, (_, i) => i + 1), []);
  const days = useMemo(() => {
    const max = getDaysInMonth(activeParts.year, activeParts.month);
    return Array.from({ length: max }, (_, i) => i + 1);
  }, [activeParts.year, activeParts.month]);

  useEffect(() => {
    const max = getDaysInMonth(activeParts.year, activeParts.month);
    if (activeParts.day <= max) return;
    if (activeEdge === 'start') setDraftStart((p) => ({ ...p, day: max }));
    else setDraftEnd((p) => ({ ...p, day: max }));
  }, [activeParts.year, activeParts.month, activeParts.day, activeEdge]);

  const patchActive = (patch) => {
    const next = clampDateParts({ ...activeParts, ...patch });
    if (activeEdge === 'start') setDraftStart(next);
    else setDraftEnd(next);
  };

  const displayText = formatDateRangeDisplay(value);

  const handleConfirm = () => {
    const startM = partsToMoment(draftStart);
    const endM = partsToMoment(draftEnd);
    if (!startM || !endM) {
      message.warning('请选择开始与结束日期');
      return;
    }
    if (moment && moment(endM).isBefore(moment(startM), 'day')) {
      message.warning('结束日期不能早于开始日期');
      return;
    }
    onChange([startM, endM]);
    setOpen(false);
  };

  const handleClear = () => {
    onChange(null);
    setOpen(false);
  };

  return (
    <>
      <Input
        className="ar-mini-trigger"
        readOnly
        value={displayText}
        placeholder={placeholder || '请选择开始-结束日期'}
        onClick={() => setOpen(true)}
        onFocus={(e) => e.target.blur()}
      />
      <Drawer
        title={title || '选择日期范围'}
        placement="bottom"
        height={420}
        open={open}
        onClose={() => setOpen(false)}
        styles={{ body: { padding: '12px 16px 0' } }}
        footer={
          <div className="ar-drawer-foot">
            <Button block size="large" onClick={handleClear} style={{ height: 46, borderRadius: 8 }}>
              清空
            </Button>
            <Button
              block
              type="primary"
              size="large"
              onClick={handleConfirm}
              style={{
                height: 46,
                borderRadius: 8,
                background: `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
                border: 'none'
              }}
            >
              确定
            </Button>
          </div>
        }
      >
        <div className="ar-region-drawer-hint">
          {hint || '切换开始/结束，滑动滚轮选择日期（精确到日）'}
        </div>
        <div className="ar-date-range-tabs" role="tablist">
          <button
            type="button"
            role="tab"
            aria-selected={activeEdge === 'start'}
            className={`ar-date-range-tab${activeEdge === 'start' ? ' active' : ''}`}
            onClick={() => setActiveEdge('start')}
          >
            开始
            <em>{formatPartsLabel(draftStart)}</em>
          </button>
          <button
            type="button"
            role="tab"
            aria-selected={activeEdge === 'end'}
            className={`ar-date-range-tab${activeEdge === 'end' ? ' active' : ''}`}
            onClick={() => setActiveEdge('end')}
          >
            结束
            <em>{formatPartsLabel(draftEnd)}</em>
          </button>
        </div>
        <div className="ar-date-wheel-wrap" key={`${activeEdge}-${activeParts.year}-${activeParts.month}`}>
          <div className="ar-date-wheel-fade top" aria-hidden="true" />
          <div className="ar-date-wheel-fade bottom" aria-hidden="true" />
          <div className="ar-date-wheel-highlight" aria-hidden="true" />
          <span className="ar-date-wheel-unit y">年</span>
          <span className="ar-date-wheel-unit m">月</span>
          <span className="ar-date-wheel-unit d">日</span>
          <DateWheelColumn items={years} value={activeParts.year} onChange={(y) => patchActive({ year: y })} />
          <DateWheelColumn items={months} value={activeParts.month} onChange={(m) => patchActive({ month: m })} />
          <DateWheelColumn items={days} value={activeParts.day} onChange={(d) => patchActive({ day: d })} />
        </div>
      </Drawer>
    </>
  );
};

/** 年审管理 · 产品需求说明（嵌入「需求说明」弹窗） */
const AnnualReviewPrdDoc = () => (
  <div className="ar-prd-doc">
    <Tag color="green" style={{ marginBottom: 8 }}>ONE-OS 小程序</Tag>
    <div className="ar-prd-meta">
      文档版本：V1.0 &nbsp;|&nbsp; 端：小程序 &nbsp;|&nbsp; 模块：运维 · 车辆年审
      <br />
      适用角色：现场办理人员、车队运维、区域运营
    </div>

    <div className="ar-prd-h2">一、背景与目标</div>
    <p className="ar-prd-p">
      车辆年审需在检验到期前完成办理，并同步更新行驶证检验有效期等信息。本模块为一线人员提供<strong>待办提醒、现场办理、草稿保存、历史查询</strong>能力，降低漏办与信息回填错误率，并与 Web 端年审/证照数据口径保持一致（原型阶段为模拟数据）。
    </p>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">待办：聚焦「即将到期 / 已逾期」车辆，按紧急程度排序。</li>
      <li className="ar-prd-li">办理：单页完成行驶证更新、检测站费用、可选二保/整备信息录入。</li>
      <li className="ar-prd-li">历史：提交后归档，仅查看不可改，便于追溯与对账。</li>
      <li className="ar-prd-li">
        <strong>证照联动（重点）</strong>：年审办理中更新的行驶证照片、检验有效期，须在提交时<strong>全量同步</strong>至 Web 端「证照管理」对应车辆档案，保证台账唯一可信来源。
      </li>
    </ul>

    <div className="ar-prd-highlight">
      <div className="ar-prd-highlight-title">重点 · 与证照管理同步</div>
      <p className="ar-prd-p" style={{ marginBottom: 8 }}>
        年审小程序负责<strong>现场采集与办理</strong>；证照管理负责<strong>车辆证照台账建档与到期预警</strong>。两者通过车牌号（及 VIN 校验）关联同一车辆。
      </p>
      <ul className="ar-prd-ul" style={{ marginBottom: 0 }}>
        <li className="ar-prd-li">同步时机：<strong>点击「提交」且校验通过后</strong>（保存草稿不同步，避免半成品覆盖台账）。</li>
        <li className="ar-prd-li">同步范围：当前任务下<strong>全部最新行驶证照片</strong>（最多 4 张，以提交瞬间列表为准）+ <strong>检验有效期至</strong>（精确到日）。</li>
        <li className="ar-prd-li">覆盖策略：<strong>全量覆盖</strong>证照管理中该车辆「行驶证」卡片的影像与「检验有效期至」，不以增量追加旧图。</li>
        <li className="ar-prd-li">失败处理：同步失败则提交事务回滚，提示用户重试；禁止出现「年审已成功、证照未更新」。</li>
      </ul>
    </div>

    <div className="ar-prd-h2">二、信息架构</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">列表首页：Tab「待处理」「历史记录」+ 车牌搜索 + 筛选。</li>
      <li className="ar-prd-li">年审操作页（待办进入）：可编辑、可保存草稿、可提交。</li>
      <li className="ar-prd-li">年审记录页（历史进入）：只读详情，结构与办理页一致。</li>
    </ul>

    <div className="ar-prd-h2">三、列表页 · 待处理</div>
    <div className="ar-prd-h3">3.1 任务卡片字段</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">车牌号（主标题）、逾期/剩余天数标签（逾期为红色，剩余为橙色）。</li>
      <li className="ar-prd-li">运营状态、运营区域（省-市）、到期时间。</li>
      <li className="ar-prd-li">已保存草稿时，右侧展示「已保存」标签（位于箭头左侧）。</li>
    </ul>
    <div className="ar-prd-h3">3.2 排序规则（待办专用）</div>
    <ol className="ar-prd-ol">
      <li className="ar-prd-li">第一优先级：逾期天数越多越靠前。</li>
      <li className="ar-prd-li">第二优先级：剩余天数越少越靠前。</li>
    </ol>
    <div className="ar-prd-h3">3.3 车牌搜索</div>
    <p className="ar-prd-p">
      点击搜索框唤起<strong>小程序风格车牌键盘</strong>（省 → 字母 → 字母数字），支持删除、完成；列表按车牌模糊匹配过滤。
    </p>
    <div className="ar-prd-h3">3.4 数据范围</div>
    <p className="ar-prd-p">退出运营状态车辆不纳入待办与历史列表。</p>

    <div className="ar-prd-h2">四、列表页 · 历史记录</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">卡片在待办字段基础上增加：办理人、完成时间（精确至分钟）。</li>
      <li className="ar-prd-li">按完成时间倒序展示。</li>
      <li className="ar-prd-li">点击卡片进入年审记录详情（只读），不可编辑、不可再次提交。</li>
    </ul>

    <div className="ar-prd-h2">五、筛选</div>
    <p className="ar-prd-p">右侧抽屉筛选，确定后生效，支持重置。</p>
    <div className="ar-prd-table-wrap">
      <table className="ar-prd-table">
        <thead>
          <tr>
            <th>筛选项</th>
            <th>待处理</th>
            <th>历史记录</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>到期时间</td>
            <td>支持</td>
            <td>支持</td>
          </tr>
          <tr>
            <td>运营区域</td>
            <td>省市双列级联</td>
            <td>省市双列级联</td>
          </tr>
          <tr>
            <td>办理人</td>
            <td>—</td>
            <td>单选（含全部）</td>
          </tr>
          <tr>
            <td>完成时间</td>
            <td>—</td>
            <td>开始-结束（日）</td>
          </tr>
        </tbody>
      </table>
    </div>
    <p className="ar-prd-p">日期类筛选均采用<strong>微信小程序风格滚轮</strong>：范围选择支持开始/结束切换；精确到日。</p>

    <div className="ar-prd-h2">六、年审操作页</div>
    <p className="ar-prd-p">自待办卡片进入。顶部导航含返回；内容区与顶栏保留间距，底部固定「保存」「提交」。</p>

    <div className="ar-prd-h3">6.1 车辆信息（只读）</div>
    <p className="ar-prd-p">展示车牌号、品牌、型号、检验有效期、运营状态（库存类展示可运营/待运营细分）。</p>

    <div className="ar-prd-h3">6.2 更新行驶证</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">
        <strong>行驶证照片</strong>（必填）：最多 4 张，3 列网格，首格上传，支持批量选择；可预览、删除。
      </li>
      <li className="ar-prd-li">
        上传后触发 OCR（原型模拟）：全页遮罩「识别中」，识别期间不可保存/提交。
      </li>
      <li className="ar-prd-li">
        OCR 仅识别到<strong>年月</strong>时，检验有效期自动填入该月<strong>最后一天</strong>；识别车牌须与年审车牌一致，否则报错并禁止提交。
      </li>
      <li className="ar-prd-li">
        <strong>检验有效期</strong>（必填）：占位「上传照片自动识别」；识别完成后可点击滚轮日期组件手动调整年月日。
      </li>
      <li className="ar-prd-li">
        用户在办理过程中增删改照片或调整有效期后，<strong>以提交时最终值</strong>为准写入证照管理（非 OCR 中间态、非仅首张图）。
      </li>
    </ul>

    <div className="ar-prd-h3">6.3 同步至证照管理（字段映射）</div>
    <div className="ar-prd-table-wrap">
      <table className="ar-prd-table">
        <thead>
          <tr>
            <th>年审小程序（提交快照）</th>
            <th>证照管理 · 行驶证卡片</th>
            <th>说明</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>车牌号</td>
            <td>关联主键</td>
            <td>定位车辆；须与证照档案一致</td>
          </tr>
          <tr>
            <td>行驶证照片列表</td>
            <td>行驶证影像（最多 4 张）</td>
            <td>全量替换为最新上传列表</td>
          </tr>
          <tr>
            <td>检验有效期至</td>
            <td>检验有效期至</td>
            <td>日期 YYYY-MM-DD；与 OCR/手选结果一致</td>
          </tr>
          <tr>
            <td>办理人、完成时间</td>
            <td>操作日志 / 更新人、更新时间</td>
            <td>留痕；影像与有效期以年审提交为准</td>
          </tr>
        </tbody>
      </table>
    </div>
    <p className="ar-prd-p">
      证照管理侧规则对齐：检验有效期支持「选月取月末」展示逻辑时，年审同步须已落库为<strong>具体日期</strong>（小程序 OCR 识别到月后已补全月末，再允许手调）。
    </p>

    <div className="ar-prd-h3">6.4 检测服务站信息</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">检测服务站：从<strong>检测服务站列表</strong>选择（可搜索）。</li>
      <li className="ar-prd-li">费用（元）：<strong>必填</strong>，保留两位小数。</li>
      <li className="ar-prd-li">备注：多行文本，选填。</li>
    </ul>

    <div className="ar-prd-h3">6.5 二保信息（选填）</div>
    <p className="ar-prd-p">默认折叠，点击「添加二保信息」展开内联卡片（非弹窗）。</p>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">二保服务站：从<strong>检测服务站列表</strong>选择（与检测站同源）。</li>
      <li className="ar-prd-li">费用：仅当已选服务站时<strong>必填</strong>。</li>
      <li className="ar-prd-li">备注、二保照片（最多 10 张，可批量）选填。</li>
    </ul>

    <div className="ar-prd-h3">6.6 整备服务站信息（选填）</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">整备服务站：从<strong>维修站列表</strong>选择。</li>
      <li className="ar-prd-li">费用：仅当已选服务站时<strong>必填</strong>。</li>
      <li className="ar-prd-li">备注、整备照片（最多 10 张，可批量）选填。</li>
    </ul>

    <div className="ar-prd-h2">七、保存与提交</div>
    <div className="ar-prd-h3">7.1 保存（草稿）</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">随时保存当前表单（OCR 识别中除外），<strong>不做必填项校验</strong>，不关闭页面。</li>
      <li className="ar-prd-li">待办列表展示「已保存」；再次进入自动恢复草稿。</li>
      <li className="ar-prd-li">提交成功后清除该任务草稿。</li>
    </ul>
    <div className="ar-prd-h3">7.2 提交</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">按钮文案为「提交」，点击后校验全部必填项。</li>
      <li className="ar-prd-li">
        校验通过后服务端顺序建议：① 上传/确认影像 URL → ② <strong>同步证照管理（行驶证全量）</strong> → ③ 年审任务结案 → ④ 返回成功。
      </li>
      <li className="ar-prd-li">前端提示：「提交成功，行驶证已同步至证照管理」，约 1 秒后返回待办列表。</li>
      <li className="ar-prd-li">任务转入历史记录，记录办理人、完成时间及完整办理快照（含同步后的证照数据）。</li>
    </ul>
    <div className="ar-prd-h3">7.3 提交校验清单</div>
    <div className="ar-prd-table-wrap">
      <table className="ar-prd-table">
        <thead>
          <tr>
            <th>校验项</th>
            <th>规则</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>行驶证照片</td>
            <td>至少 1 张</td>
          </tr>
          <tr>
            <td>检验有效期</td>
            <td>已填写</td>
          </tr>
          <tr>
            <td>检测费用</td>
            <td>必填</td>
          </tr>
          <tr>
            <td>二保/整备费用</td>
            <td>已选服务站则必填</td>
          </tr>
          <tr>
            <td>OCR 状态</td>
            <td>识别中不可提交；车牌不一致不可提交</td>
          </tr>
        </tbody>
      </table>
    </div>

    <div className="ar-prd-h2">八、年审记录页（历史 · 只读）</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">模块顺序与办理页一致：车辆信息 → 更新行驶证 → 检测站 → 二保/整备（有数据才展示）。</li>
      <li className="ar-prd-li">车辆信息下增加：办理人、完成时间。</li>
      <li className="ar-prd-li">行驶证/二保/整备照片以缩略图网格展示，支持预览，无上传入口。</li>
      <li className="ar-prd-li">所有字段只读展示，不提供编辑与提交。</li>
    </ul>

    <div className="ar-prd-h2">九、交互与视觉规范</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">整体模拟 iPhone 小程序壳：状态栏、导航栏、微信胶囊。</li>
      <li className="ar-prd-li">主色：#16D1A1 / #00BFA5；表单为左标题右内容、隐藏边框右对齐输入。</li>
      <li className="ar-prd-li">选择类：底部抽屉 + 滚轮/双列（省市、日期、单选）。</li>
      <li className="ar-prd-li">车牌、日期交互对齐微信小程序常见模式。</li>
    </ul>

    <div className="ar-prd-h2">十、非功能与后续对接（建议）</div>
    <ul className="ar-prd-ul">
      <li className="ar-prd-li">正式环境需对接：任务列表 API、OCR 服务、图片上传 CDN、提交/草稿接口。</li>
      <li className="ar-prd-li">
        <strong>证照同步接口（必接）</strong>：POST 提交年审时携带 plateNo、inspectionValidUntil、photoUrls[]；或由后端在结案事务内调用证照服务「覆盖行驶证」能力。
      </li>
      <li className="ar-prd-li">办理人取当前登录用户；完成时间取服务端时间。</li>
      <li className="ar-prd-li">检测站/维修站列表与主数据或 Web 端配置同步。</li>
      <li className="ar-prd-li">图片须先落 CDN 再写证照管理，禁止仅存临时 Blob；刷新后 Web/小程序均可预览。</li>
    </ul>

    <p className="ar-prd-footnote">
      当前页面为可交互原型，业务数据、OCR 结果、站点列表均为模拟；行为与上述需求一致，供评审与开发拆票参考。
    </p>
  </div>
);

const AnnualReviewPanel = function AnnualReviewPanel({ embedded = false, theme = 'default', onBack, onOpenPrd }) {
  const [mainTab, setMainTab] = useState('pending');
  const [searchPlate, setSearchPlate] = useState('');
  const [plateKbOpen, setPlateKbOpen] = useState(false);
  const [plateKbDraft, setPlateKbDraft] = useState('');
  const [filterOpen, setFilterOpen] = useState(false);
  const [filterDraft, setFilterDraft] = useState({ ...DEFAULT_FILTER });
  const [filterApplied, setFilterApplied] = useState({ ...DEFAULT_FILTER });
  const [tasks, setTasks] = useState(() =>
    MOCK_TASKS.map((t) =>
      t.tab === 'history'
        ? { ...t, operateSnapshot: t.operateSnapshot || buildSampleHistorySnapshot(t) }
        : t
    )
  );
  const [prdOpen, setPrdOpen] = useState(false);
  const [operateTask, setOperateTask] = useState(null);
  const [historyViewTask, setHistoryViewTask] = useState(null);
  const [inspectionForm, setInspectionForm] = useState({ ...EMPTY_INSPECTION_FORM });
  const [m2Expanded, setM2Expanded] = useState(false);
  const [m2Form, setM2Form] = useState({ ...EMPTY_SERVICE_FORM });
  const [zbExpanded, setZbExpanded] = useState(false);
  const [zbForm, setZbForm] = useState({ ...EMPTY_SERVICE_FORM });
  const [licenseForm, setLicenseForm] = useState({ ...EMPTY_LICENSE_FORM });
  const [operateDrafts, setOperateDrafts] = useState(() => loadOperateDrafts());
  const licenseOcrTimerRef = useRef(null);
  const themeClass = theme === 'xll' ? ' xll-module-theme' : '';
  const embedStyles = `${PAGE_STYLE}${EMBED_STYLE}${theme === 'xll' ? XLL_THEME_PATCH : ''}`;

  const resetOperateForms = () => {
    if (licenseOcrTimerRef.current) {
      clearTimeout(licenseOcrTimerRef.current);
      licenseOcrTimerRef.current = null;
    }
    setInspectionForm({ ...EMPTY_INSPECTION_FORM });
    setM2Expanded(false);
    setM2Form({ ...EMPTY_SERVICE_FORM });
    setZbExpanded(false);
    setZbForm({ ...EMPTY_SERVICE_FORM });
    setLicenseForm({ ...EMPTY_LICENSE_FORM });
  };

  const applyOperateDraft = (draft) => {
    if (!draft) return;
    setInspectionForm({ ...EMPTY_INSPECTION_FORM, ...draft.inspectionForm });
    const lf = draft.licenseForm || {};
    setLicenseForm({
      ...EMPTY_LICENSE_FORM,
      photos: deserializeUploadFileList(lf.photos),
      inspectionValidUntil: lf.inspectionValidUntil ?? null,
      ocrStatus: lf.ocrStatus === 'recognizing' ? 'idle' : lf.ocrStatus || 'idle'
    });
    setM2Expanded(!!draft.m2Expanded);
    const m2 = draft.m2Form || {};
    setM2Form({
      ...EMPTY_SERVICE_FORM,
      ...m2,
      photos: deserializeUploadFileList(m2.photos)
    });
    setZbExpanded(!!draft.zbExpanded);
    const zb = draft.zbForm || {};
    setZbForm({
      ...EMPTY_SERVICE_FORM,
      ...zb,
      photos: deserializeUploadFileList(zb.photos)
    });
  };

  const collectOperateDraft = () => ({
    savedAt: new Date().toISOString(),
    inspectionForm: { ...inspectionForm },
    licenseForm: {
      photos: serializeUploadFileList(licenseForm.photos),
      inspectionValidUntil: licenseForm.inspectionValidUntil,
      ocrStatus: licenseForm.ocrStatus === 'recognizing' ? 'idle' : licenseForm.ocrStatus
    },
    m2Expanded,
    m2Form: { ...m2Form, photos: serializeUploadFileList(m2Form.photos) },
    zbExpanded,
    zbForm: { ...zbForm, photos: serializeUploadFileList(zbForm.photos) }
  });

  const upsertOperateDraft = (taskId, draft) => {
    const next = { ...operateDrafts, [taskId]: draft };
    setOperateDrafts(next);
    persistOperateDrafts(next);
  };

  const removeOperateDraft = (taskId) => {
    if (!operateDrafts[taskId]) return;
    const next = { ...operateDrafts };
    delete next[taskId];
    setOperateDrafts(next);
    persistOperateDrafts(next);
  };

  const openOperatePage = (task) => {
    if (licenseOcrTimerRef.current) {
      clearTimeout(licenseOcrTimerRef.current);
      licenseOcrTimerRef.current = null;
    }
    setHistoryViewTask(null);
    setOperateTask(task);
    const draft = operateDrafts[task.id];
    if (draft) applyOperateDraft(draft);
    else resetOperateForms();
  };

  const closeOperatePage = () => {
    setOperateTask(null);
    resetOperateForms();
  };

  const openHistoryViewPage = (task) => {
    setHistoryViewTask(task);
    setOperateTask(null);
    resetOperateForms();
  };

  const closeHistoryViewPage = () => {
    setHistoryViewTask(null);
  };

  useEffect(() => {
    if (!embedded || typeof window === 'undefined') return undefined;
    window.__xllInspectionBack = () => {
      if (historyViewTask) {
        closeHistoryViewPage();
        return true;
      }
      if (operateTask) {
        closeOperatePage();
        return true;
      }
      return false;
    };
    return () => { delete window.__xllInspectionBack; };
  }, [embedded, historyViewTask, operateTask]);

  const setInspection = (key, val) => setInspectionForm((p) => ({ ...p, [key]: val }));
  const setM2 = (key, val) => setM2Form((p) => ({ ...p, [key]: val }));
  const setZb = (key, val) => setZbForm((p) => ({ ...p, [key]: val }));
  const setLicense = (key, val) => setLicenseForm((p) => ({ ...p, [key]: val }));

  const runLicenseOcr = (task, photos) => {
    if (licenseOcrTimerRef.current) clearTimeout(licenseOcrTimerRef.current);
    setLicenseForm((p) => ({
      ...p,
      ocrStatus: 'recognizing',
      inspectionValidUntil: null
    }));
    licenseOcrTimerRef.current = setTimeout(() => {
      const expectedPlate = task?.plateNo || '';
      const recognizedPlate = mockOcrLicensePlate(task, photos);
      if (!platesMatch(recognizedPlate, expectedPlate)) {
        setLicenseForm((p) => ({
          ...p,
          ocrStatus: 'error',
          inspectionValidUntil: null
        }));
        message.error(
          `识别车牌号与年审车牌号不一致（识别：${recognizedPlate || '—'}，年审：${expectedPlate || '—'}）`
        );
        licenseOcrTimerRef.current = null;
        return;
      }
      const validUntil = mockOcrLicenseValidUntil(task);
      setLicenseForm((p) => ({
        ...p,
        inspectionValidUntil: validUntil,
        ocrStatus: 'done'
      }));
      message.success('检验有效期识别完成');
      licenseOcrTimerRef.current = null;
    }, LICENSE_OCR_MOCK_MS);
  };

  const handleLicensePhotosChange = (fileList) => {
    const prevLen = (licenseForm.photos || []).length;
    const nextLen = (fileList || []).length;
    if (nextLen === 0) {
      if (licenseOcrTimerRef.current) {
        clearTimeout(licenseOcrTimerRef.current);
        licenseOcrTimerRef.current = null;
      }
      setLicenseForm({ ...EMPTY_LICENSE_FORM });
      return;
    }
    setLicense('photos', fileList);
    if (nextLen > prevLen) {
      runLicenseOcr(operateTask, fileList);
    }
  };

  const inspectionStationOptions = useMemo(() => toStationSelectOptions(readInspectionStationList()), []);
  const repairStationOptions = useMemo(() => toStationSelectOptions(REPAIR_STATION_LIST), []);

  const saveOperate = () => {
    if (!operateTask) return;
    if (licenseForm.ocrStatus === 'recognizing') {
      message.warning('行驶证识别中，请稍候再保存');
      return;
    }
    upsertOperateDraft(operateTask.id, collectOperateDraft());
    message.success('已保存，可稍后继续填写');
  };

  const submitOperate = () => {
    if (licenseForm.ocrStatus === 'recognizing') {
      message.warning('行驶证识别中，请稍候');
      return;
    }
    if ((licenseForm.photos || []).length && licenseForm.ocrStatus === 'error') {
      message.error('行驶证车牌号识别不一致，请重新上传照片');
      return;
    }
    if (!validateLicenseForm(licenseForm)) return;
    if (isFormCostEmpty(inspectionForm.cost)) {
      message.error('请填写检测费用');
      return;
    }
    if (!validateStationCost(m2Form, '二保')) return;
    if (!validateStationCost(zbForm, '整备')) return;
    const snapshot = collectOperateDraft();
    const completeTime = moment ? moment().format('YYYY-MM-DD HH:mm') : new Date().toISOString().slice(0, 16).replace('T', ' ');
    const taskId = operateTask.id;

    removeOperateDraft(taskId);
    syncLicenseToCertificateManagement(operateTask, licenseForm, MOCK_CURRENT_HANDLER);
    setTasks((prev) =>
      prev.map((t) =>
        t.id === taskId
          ? {
              ...t,
              tab: 'history',
              executor: MOCK_CURRENT_HANDLER,
              executeTime: completeTime,
              expireDate: licenseForm.inspectionValidUntil || t.expireDate,
              operateSnapshot: snapshot
            }
          : t
      )
    );
    message.success('提交成功，行驶证照片及检验有效期已同步至证照管理');
    setTimeout(() => {
      closeOperatePage();
    }, 1000);
  };

  const enrichTasks = useMemo(
    () =>
      tasks.filter((t) => !isAnnualReviewExcluded(t)).map((t) => {
        const daysLeft = t.daysLeft;
        let urgency = 'normal';
        if (daysLeft <= 0) urgency = 'danger';
        else if (daysLeft <= 30) urgency = 'warn';
        return {
          ...t,
          urgency,
          hasSavedDraft: t.tab === 'pending' && !!operateDrafts[t.id]
        };
      }),
    [tasks, operateDrafts]
  );

  const filteredTasks = useMemo(() => {
    const f = filterApplied;
    const plateKey = (searchPlate || '').trim().toLowerCase();

    const list = enrichTasks.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;
    });

    if (mainTab === 'pending') return sortPendingTasks(list);
    if (mainTab === 'history' && moment) {
      return [...list].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 list;
  }, [enrichTasks, mainTab, searchPlate, filterApplied]);

  const openPlateKeyboard = () => {
    setPlateKbDraft(searchPlate);
    setPlateKbOpen(true);
  };

  const closePlateKeyboard = () => {
    setPlateKbOpen(false);
  };

  const confirmPlateKeyboard = (plate) => {
    setSearchPlate((plate || '').trim().toUpperCase());
    setPlateKbOpen(false);
  };

  const openFilter = () => {
    setFilterDraft({ ...filterApplied });
    setFilterOpen(true);
  };

  const applyFilter = () => {
    setFilterApplied({ ...filterDraft });
    setFilterOpen(false);
    message.success('筛选已应用');
  };

  const resetFilter = () => {
    setFilterDraft({ ...DEFAULT_FILTER });
  };

  const getPlateDaysTag = (task) => {
    if (task.tab === 'history') return null;
    if (task.daysLeft > 0) {
      return { text: `剩余${task.daysLeft}天`, color: 'warning' };
    }
    return { text: `逾期${Math.abs(task.daysLeft)}天`, color: 'error' };
  };

  const handleTaskCardClick = (task) => {
    if (task.tab === 'pending') {
      openOperatePage(task);
      return;
    }
    openHistoryViewPage(task);
  };

  const renderTaskCard = (task) => {
    const daysTag = getPlateDaysTag(task);

    return (
      <div
        key={task.id}
        className="ar-card"
        role="button"
        tabIndex={0}
        onClick={() => handleTaskCardClick(task)}
        onKeyDown={(e) => {
          if (e.key === 'Enter') handleTaskCardClick(task);
        }}
      >
        <div className="ar-card-head">
          <div className="ar-plate-row">
            <span className="ar-plate">{task.plateNo}</span>
            {daysTag && (
              <Tag className="ar-plate-tag" color={daysTag.color}>
                {daysTag.text}
              </Tag>
            )}
          </div>
          <div className="ar-card-head-right">
            {task.hasSavedDraft && (
              <Tag className="ar-plate-tag ar-plate-tag--saved">已保存</Tag>
            )}
            <span className="ar-card-status" aria-hidden="true">
              <IconChevron />
            </span>
          </div>
        </div>
        <div className="ar-kv">
          <span className="ar-kv-label">运营状态</span>
          <span className="ar-kv-value">{task.operateStatus}</span>
        </div>
        <div className="ar-kv">
          <span className="ar-kv-label">运营区域</span>
          <span className="ar-kv-value">{formatTaskRegion(task)}</span>
        </div>
        <div className="ar-kv">
          <span className="ar-kv-label">到期时间</span>
          <span className="ar-kv-value">{task.expireDate}</span>
        </div>
        {task.tab === 'history' && (
          <>
            <div className="ar-kv">
              <span className="ar-kv-label">办理人</span>
              <span className="ar-kv-value">{task.executor || '—'}</span>
            </div>
            <div className="ar-kv">
              <span className="ar-kv-label">完成时间</span>
              <span className="ar-kv-value">{formatCompleteTime(task.executeTime)}</span>
            </div>
          </>
        )}
      </div>
    );
  };

  const renderHistoryDetailPage = () => {
    if (!historyViewTask) return null;
    const task = historyViewTask;
    const snap = getHistorySnapshot(task);
    const licensePhotos = deserializeUploadFileList(snap.licenseForm?.photos);
    const licenseValid = snap.licenseForm?.inspectionValidUntil || '—';
    const insp = snap.inspectionForm || {};
    const m2 = snap.m2Form || {};
    const zb = snap.zbForm || {};
    const showM2 = snap.m2Expanded || hasServiceContent(m2);
    const showZb = snap.zbExpanded || hasServiceContent(zb);

    return (
      <div className="ar-operate-scroll">
        <div className="ar-section ar-section--form">
          <div className="ar-section-title">车辆信息</div>
          <div className="ar-form-grid">
            <InfoRow label="车牌号" valueClassName="plate">
              {task.plateNo}
            </InfoRow>
            <InfoRow label="品牌">{task.brand}</InfoRow>
            <InfoRow label="型号">{task.model}</InfoRow>
            <InfoRow label="检验有效期">{task.expireDate}</InfoRow>
            <InfoRow label="运营状态">{formatOperateStatusDisplay(task)}</InfoRow>
            <InfoRow label="办理人">{task.executor || '—'}</InfoRow>
            <InfoRow label="完成时间">{formatCompleteTime(task.executeTime)}</InfoRow>
          </div>
        </div>

        <div className="ar-section ar-section--form">
          <div className="ar-section-title">更新行驶证</div>
          <PhotoReadonlyGallery label="行驶证照片" fileList={licensePhotos} />
          <div className="ar-form-grid" style={{ marginTop: 4 }}>
            <InfoRow label="检验有效期">{licenseValid}</InfoRow>
          </div>
        </div>

        <div className="ar-section ar-section--form">
          <div className="ar-section-title">检测服务站信息</div>
          <div className="ar-form-grid">
            <InfoRow label="检测服务站">{insp.station || '—'}</InfoRow>
            <InfoRow label="费用（元）">{formatDisplayMoney(insp.cost)}</InfoRow>
            {insp.remark ? (
              <div className="ar-form-row">
                <span className="ar-form-label">备注</span>
                <div className="ar-form-control">
                  <div className="ar-form-readonly ar-form-readonly--multiline">{insp.remark}</div>
                </div>
              </div>
            ) : null}
          </div>
        </div>

        {showM2 ? (
          <div className="ar-section ar-section--form">
            <div className="ar-section-title">二保信息</div>
            <div className="ar-form-grid">
              <InfoRow label="二保服务站">{m2.station || '—'}</InfoRow>
              <InfoRow label="费用（元）">{formatDisplayMoney(m2.cost)}</InfoRow>
              {m2.remark ? (
                <div className="ar-form-row">
                  <span className="ar-form-label">备注</span>
                  <div className="ar-form-control">
                    <div className="ar-form-readonly ar-form-readonly--multiline">{m2.remark}</div>
                  </div>
                </div>
              ) : null}
            </div>
            <PhotoReadonlyGallery label="二保照片" fileList={deserializeUploadFileList(m2.photos)} />
          </div>
        ) : null}

        {showZb ? (
          <div className="ar-section ar-section--form">
            <div className="ar-section-title">整备服务站信息</div>
            <div className="ar-form-grid">
              <InfoRow label="整备服务站">{zb.station || '—'}</InfoRow>
              <InfoRow label="费用（元）">{formatDisplayMoney(zb.cost)}</InfoRow>
              {zb.remark ? (
                <div className="ar-form-row">
                  <span className="ar-form-label">备注</span>
                  <div className="ar-form-control">
                    <div className="ar-form-readonly ar-form-readonly--multiline">{zb.remark}</div>
                  </div>
                </div>
              ) : null}
            </div>
            <PhotoReadonlyGallery label="整备照片" fileList={deserializeUploadFileList(zb.photos)} />
          </div>
        ) : null}
      </div>
    );
  };

  const renderOperatePage = () => {
    if (!operateTask) return null;
    const task = operateTask;

    const licenseRecognizing = licenseForm.ocrStatus === 'recognizing';

    return (
      <div className="ar-operate-wrap">
        <div className="ar-operate-scroll">
          <div className="ar-section ar-section--form">
            <div className="ar-section-title">车辆信息</div>
            <div className="ar-form-grid">
              <InfoRow label="车牌号" valueClassName="plate">{task.plateNo}</InfoRow>
              <InfoRow label="品牌">{task.brand}</InfoRow>
              <InfoRow label="型号">{task.model}</InfoRow>
              <InfoRow label="检验有效期">{task.expireDate}</InfoRow>
              <InfoRow label="运营状态">{formatOperateStatusDisplay(task)}</InfoRow>
            </div>
          </div>

          <div className="ar-section ar-section--form">
            <div className="ar-section-title">更新行驶证</div>
            {licenseForm.ocrStatus === 'error' && (
              <div className="ar-ocr-banner ar-ocr-banner--error" role="alert">
                识别车牌号与年审车牌号不一致，请重新上传行驶证照片
              </div>
            )}
            {licenseForm.ocrStatus === 'done' && licenseForm.inspectionValidUntil && (
              <div className="ar-ocr-banner ar-ocr-banner--done">
                已根据行驶证照片识别检验有效期（精确到月，已自动补全至月末）
              </div>
            )}
            <PhotoUploadBlock
              label="行驶证照片"
              required
              fileList={licenseForm.photos}
              onChange={handleLicensePhotosChange}
              maxPhotos={MAX_LICENSE_PHOTOS}
            />
            <div className="ar-form-grid" style={{ marginTop: 4 }}>
              <FormField label="检验有效期" required>
                <MiniSingleDatePicker
                  inForm
                  value={licenseForm.inspectionValidUntil}
                  onChange={(v) => setLicense('inspectionValidUntil', v)}
                  placeholder="上传照片自动识别"
                  title="检验有效期"
                  hint="可调整年月日，精确到日"
                  disabled={licenseForm.ocrStatus === 'recognizing'}
                />
              </FormField>
            </div>
          </div>

          <div className="ar-section ar-section--form">
            <div className="ar-section-title">检测服务站信息</div>
            <div className="ar-form-grid">
            <FormField label="检测服务站">
              <Select
                bordered={false}
                showSearch
                allowClear
                placeholder="请选择检测服务站"
                value={inspectionForm.station || undefined}
                onChange={(v) => setInspection('station', v || '')}
                options={inspectionStationOptions}
              />
            </FormField>
            <FormField label="费用（元）" required>
              <InputNumber
                bordered={false}
                controls={false}
                min={0}
                precision={2}
                placeholder="请输入费用"
                value={inspectionForm.cost === '' ? null : Number(inspectionForm.cost)}
                onChange={(v) => setInspection('cost', v == null ? '' : String(v))}
              />
            </FormField>
            <div className="ar-form-row ar-form-row--remark-block">
              <div className="ar-form-control">
                {TextArea ? (
                  <TextArea
                    bordered={false}
                    rows={2}
                    placeholder="请输入检测备注信息"
                    value={inspectionForm.remark}
                    onChange={(e) => setInspection('remark', e.target.value)}
                  />
                ) : (
                  <Input
                    bordered={false}
                    placeholder="请输入检测备注信息"
                    value={inspectionForm.remark}
                    onChange={(e) => setInspection('remark', e.target.value)}
                  />
                )}
              </div>
            </div>
            </div>
          </div>

          {!m2Expanded ? (
            <button
              type="button"
              className={`ar-add-btn${
                m2Form.station || m2Form.cost || m2Form.remark || (m2Form.photos || []).length ? ' filled' : ''
              }`}
              onClick={() => setM2Expanded(true)}
            >
              添加二保信息
            </button>
          ) : (
            <div className="ar-section ar-section--form">
              <div className="ar-section-title-row">
                <span className="ar-section-title-text">二保信息</span>
                <button type="button" className="ar-section-fold" onClick={() => setM2Expanded(false)}>
                  收起
                </button>
              </div>
              <div className="ar-form-grid">
                <FormField label="二保服务站">
                  <Select
                    bordered={false}
                    showSearch
                    allowClear
                    placeholder="请选择二保服务站"
                    value={m2Form.station || undefined}
                    onChange={(v) => setM2('station', v || '')}
                    options={inspectionStationOptions}
                  />
                </FormField>
                <FormField label="费用（元）" required={!!m2Form.station}>
                  <InputNumber
                    bordered={false}
                    controls={false}
                    min={0}
                    precision={2}
                    placeholder={m2Form.station ? '请选择站点后必填' : '请输入费用'}
                    value={m2Form.cost === '' ? null : Number(m2Form.cost)}
                    onChange={(v) => setM2('cost', v == null ? '' : String(v))}
                  />
                </FormField>
                <div className="ar-form-row ar-form-row--remark-block">
                  <div className="ar-form-control">
                    {TextArea ? (
                      <TextArea
                        bordered={false}
                        rows={2}
                        placeholder="请输入二保备注信息"
                        value={m2Form.remark}
                        onChange={(e) => setM2('remark', e.target.value)}
                      />
                    ) : (
                      <Input
                        bordered={false}
                        placeholder="请输入二保备注信息"
                        value={m2Form.remark}
                        onChange={(e) => setM2('remark', e.target.value)}
                      />
                    )}
                  </div>
                </div>
              </div>
              <PhotoUploadBlock
                label="二保照片"
                fileList={m2Form.photos}
                onChange={(fl) => setM2('photos', fl)}
              />
            </div>
          )}

          {!zbExpanded ? (
            <button
              type="button"
              className={`ar-add-btn${
                zbForm.station || zbForm.cost || zbForm.remark || (zbForm.photos || []).length ? ' filled' : ''
              }`}
              onClick={() => setZbExpanded(true)}
            >
              添加整备服务站信息
            </button>
          ) : (
            <div className="ar-section ar-section--form">
              <div className="ar-section-title-row">
                <span className="ar-section-title-text">整备服务站信息</span>
                <button type="button" className="ar-section-fold" onClick={() => setZbExpanded(false)}>
                  收起
                </button>
              </div>
              <div className="ar-form-grid">
                <FormField label="整备服务站">
                  <Select
                    bordered={false}
                    showSearch
                    allowClear
                    placeholder="请从维修站列表选择"
                    value={zbForm.station || undefined}
                    onChange={(v) => setZb('station', v || '')}
                    options={repairStationOptions}
                  />
                </FormField>
                <FormField label="费用（元）" required={!!zbForm.station}>
                  <InputNumber
                    bordered={false}
                    controls={false}
                    min={0}
                    precision={2}
                    placeholder={zbForm.station ? '请选择站点后必填' : '请输入费用'}
                    value={zbForm.cost === '' ? null : Number(zbForm.cost)}
                    onChange={(v) => setZb('cost', v == null ? '' : String(v))}
                  />
                </FormField>
                <div className="ar-form-row ar-form-row--remark-block">
                  <div className="ar-form-control">
                    {TextArea ? (
                      <TextArea
                        bordered={false}
                        rows={2}
                        placeholder="请输入整备备注信息"
                        value={zbForm.remark}
                        onChange={(e) => setZb('remark', e.target.value)}
                      />
                    ) : (
                      <Input
                        bordered={false}
                        placeholder="请输入整备备注信息"
                        value={zbForm.remark}
                        onChange={(e) => setZb('remark', e.target.value)}
                      />
                    )}
                  </div>
                </div>
              </div>
              <PhotoUploadBlock
                label="整备照片"
                fileList={zbForm.photos}
                onChange={(fl) => setZb('photos', fl)}
              />
            </div>
          )}
        </div>

        <div className="ar-operate-foot">
          <div className="ar-operate-foot-btns">
            <Button size="large" disabled={licenseRecognizing} onClick={saveOperate}>
              保存
            </Button>
            <Button
              type="primary"
              size="large"
              disabled={licenseRecognizing}
              onClick={submitOperate}
              style={{
                fontWeight: 700,
                background: licenseRecognizing
                  ? undefined
                  : `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
                border: 'none'
              }}
            >
              提交
            </Button>
          </div>
        </div>

        {licenseRecognizing && (
          <div className="ar-ocr-mask" role="alertdialog" aria-busy="true" aria-live="polite" aria-label="行驶证识别中">
            <div className="ar-ocr-mask-panel">
              <span className="ar-ocr-dot" aria-hidden="true" />
              <div className="ar-ocr-mask-title">识别中</div>
              <div className="ar-ocr-mask-desc">正在识别行驶证信息，请稍候完成后再提交</div>
            </div>
          </div>
        )}
      </div>
    );
  };

  const phoneMain = (
    <>
      {historyViewTask ? (
        renderHistoryDetailPage()
      ) : operateTask ? (
        renderOperatePage()
      ) : (
        <>
          <div className="ar-tabs" role="tablist">
            <button
              type="button"
              role="tab"
              aria-selected={mainTab === 'pending'}
              className={`ar-tab${mainTab === 'pending' ? ' active' : ''}`}
              onClick={() => setMainTab('pending')}
            >
              待处理
            </button>
            <button
              type="button"
              role="tab"
              aria-selected={mainTab === 'history'}
              className={`ar-tab${mainTab === 'history' ? ' active' : ''}`}
              onClick={() => setMainTab('history')}
            >
              历史记录
            </button>
          </div>

          <div className="ar-search-row">
            <div
              className="ar-search-box"
              role="button"
              tabIndex={0}
              onClick={openPlateKeyboard}
              onKeyDown={(e) => e.key === 'Enter' && openPlateKeyboard()}
            >
              {!searchPlate ? (
                <span className="ar-search-placeholder" aria-hidden="true">
                  请输入车牌号
                </span>
              ) : null}
              <input
                readOnly
                value={searchPlate}
                aria-label="车牌号搜索"
                onFocus={(e) => {
                  e.target.blur();
                  openPlateKeyboard();
                }}
              />
            </div>
            <button type="button" className="ar-filter-btn" aria-label="打开筛选" onClick={openFilter}>
              <IconFilter />
            </button>
          </div>

          <div className="ar-list">
            {filteredTasks.length === 0 ? (
              <div className="ar-empty">暂无符合条件的年审任务<br />请调整搜索或筛选条件</div>
            ) : (
              filteredTasks.map(renderTaskCard)
            )}
          </div>
        </>
      )}

      <PlateKeyboardPanel
        open={plateKbOpen && !operateTask && !historyViewTask}
        value={plateKbDraft}
        onChange={setPlateKbDraft}
        onConfirm={confirmPlateKeyboard}
        onClose={closePlateKeyboard}
      />
    </>
  );

  const moduleOverlays = (
    <>
      <Drawer
        title="筛选"
        placement="right"
        width={Math.min(340, typeof window !== 'undefined' ? window.innerWidth * 0.86 : 320)}
        open={filterOpen}
        onClose={() => setFilterOpen(false)}
        styles={{ body: { padding: '12px 16px 0' } }}
        footer={
          <div className="ar-drawer-foot">
            <Button block size="large" onClick={resetFilter} style={{ height: 46, borderRadius: 8 }}>
              重置
            </Button>
            <Button
              block
              type="primary"
              size="large"
              onClick={applyFilter}
              style={{
                height: 46,
                borderRadius: 8,
                background: theme === 'xll'
                  ? `linear-gradient(135deg, ${XLL_GREEN} 0%, ${XLL_GREEN_DEEP} 100%)`
                  : `linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%)`,
                border: 'none'
              }}
            >
              确定
            </Button>
          </div>
        }
      >
        <FilterRow label="到期时间">
          <MiniDateRangePicker
            value={filterDraft.expireRange}
            onChange={(v) => setFilterDraft((p) => ({ ...p, expireRange: v }))}
            placeholder="请选择开始-结束日期"
            title="选择到期时间"
            hint="切换开始/结束，滑动选择到期日期（精确到日）"
          />
        </FilterRow>
        <FilterRow label="运营区域">
          <MiniRegionPicker
            value={filterDraft.provinceCity}
            onChange={(v) => setFilterDraft((p) => ({ ...p, provinceCity: v }))}
            placeholder="请选择省市"
          />
        </FilterRow>
        {mainTab === 'history' && (
          <>
            <div className="ar-filter-section-label">办理记录筛选</div>
            <FilterRow label="办理人">
              <MiniOptionPicker
                value={filterDraft.handler}
                onChange={(v) => setFilterDraft((p) => ({ ...p, handler: v }))}
                options={HANDLER_OPTIONS}
                placeholder="请选择办理人"
                title="选择办理人"
                clearLabel="全部"
              />
            </FilterRow>
            <FilterRow label="完成时间">
              <MiniDateRangePicker
                value={filterDraft.executeTimeRange}
                onChange={(v) => setFilterDraft((p) => ({ ...p, executeTimeRange: v }))}
                placeholder="请选择开始-结束日期"
                title="选择完成时间"
                hint="切换开始/结束，滑动选择完成日期（精确到日）"
              />
            </FilterRow>
          </>
        )}
      </Drawer>

      <Modal
        open={embedded ? false : prdOpen}
        title="年审管理 · 产品需求说明"
        onCancel={() => setPrdOpen(false)}
        footer={[
          <Button
            key="ok"
            type="primary"
            onClick={() => setPrdOpen(false)}
            style={{
              background: theme === 'xll' ? XLL_GREEN_DEEP : COLOR_PRIMARY_DEEP,
              borderColor: theme === 'xll' ? XLL_GREEN_DEEP : COLOR_PRIMARY_DEEP,
            }}
          >
            知道了
          </Button>
        ]}
        width={420}
        centered
        bodyStyle={{ maxHeight: '72vh', overflowY: 'auto', paddingTop: 8 }}
      >
        <AnnualReviewPrdDoc />
      </Modal>
    </>
  );

  if (embedded) {
    return (
      <div className={`ar-embed-root${themeClass}`}>
        <style>{embedStyles}</style>
        {phoneMain}
        {moduleOverlays}
      </div>
    );
  }

  return (
    <div className="ar-mini-root">
      <style>{PAGE_STYLE}</style>
      <div className="ar-phone">
        <MiniProgramChrome
          title={historyViewTask ? '年审记录' : operateTask ? '年审操作' : '年审'}
          showBack={!!operateTask || !!historyViewTask}
          onBack={historyViewTask ? closeHistoryViewPage : closeOperatePage}
          showPrdLink={!operateTask && !historyViewTask}
          onPrdClick={() => (onOpenPrd ? onOpenPrd() : setPrdOpen(true))}
        />
        <div className="ar-embed-root">{phoneMain}</div>
      </div>
      {moduleOverlays}
    </div>
  );
};

const Component = function AnnualReviewMiniApp() {
  return <AnnualReviewPanel embedded={false} />;
};

if (typeof window !== 'undefined') {
  window.Component = Component;
  window.ONEOS_MP_EMBED = window.ONEOS_MP_EMBED || {};
  window.ONEOS_MP_EMBED.AnnualReviewPanel = AnnualReviewPanel;
}

export default Component;
