// 【重要】必须使用 const Component 作为组件变量名 // ONE-OS 小程序 - 审批中心(我发起的 / 我的待办 / 我的已办 / 我的抄送) const { useState, useMemo, useCallback, useRef, useEffect } = React; const moment = window.moment || window.dayjs; const COLOR_PRIMARY = '#16D1A1'; const COLOR_PRIMARY_DEEP = '#00BFA5'; const COLOR_PRIMARY_SOFT = 'rgba(22, 209, 161, 0.12)'; const COLOR_TEXT = '#1D2129'; const COLOR_TEXT_SEC = '#4E5969'; 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 COLOR_SUCCESS = '#00B42A'; const FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", STHeiti, sans-serif'; 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 message = antd.message || { info: () => {}, success: () => {}, warning: () => {} }; const Drawer = antd.Drawer; const Modal = antd.Modal; const FallbackTag = ({ children, className, style, color }) => { const palette = { error: { c: COLOR_DANGER, bg: 'rgba(245, 63, 63, 0.1)' }, warning: { c: COLOR_WARN, bg: 'rgba(255, 125, 0, 0.1)' }, success: { c: COLOR_SUCCESS, bg: 'rgba(0, 180, 42, 0.1)' }, default: { c: COLOR_MUTED, bg: 'rgba(134, 144, 156, 0.12)' }, processing: { c: COLOR_PRIMARY_DEEP, bg: COLOR_PRIMARY_SOFT }, }; const p = palette[color] || palette.processing; return ( {children} ); }; const Tag = antd.Tag || FallbackTag; const MOCK_CURRENT_USER = '张明辉'; const APPROVAL_FLOW_TYPES = [ '合同审批', '提车应收款', '租赁账单', '还车应结款', '氢费对账单(对站)', '氢费对账单(对客)', '车辆调拨', '车辆异动', ]; const TAB_ITEMS = [ { key: 'initiated', label: '我发起的', short: '发起' }, { key: 'todo', label: '我的待办', short: '待办' }, { key: 'done', label: '我的已办', short: '已办' }, { key: 'cc', label: '我的抄送', short: '抄送' }, ]; /** 流程类型主题色(左侧色条 + 图标底) */ const FLOW_THEME = { 合同审批: { accent: '#2563EB', soft: 'rgba(37, 99, 235, 0.12)' }, 提车应收款: { accent: '#F97316', soft: 'rgba(249, 115, 22, 0.12)' }, 租赁账单: { accent: '#0EA5E9', soft: 'rgba(14, 165, 233, 0.12)' }, 还车应结款: { accent: '#8B5CF6', soft: 'rgba(139, 92, 246, 0.12)' }, '氢费对账单(对站)': { accent: '#10B981', soft: 'rgba(16, 185, 129, 0.12)' }, '氢费对账单(对客)': { accent: '#059669', soft: 'rgba(5, 150, 105, 0.12)' }, 车辆调拨: { accent: COLOR_PRIMARY_DEEP, soft: COLOR_PRIMARY_SOFT }, 车辆异动: { accent: '#14B8A6', soft: 'rgba(20, 184, 166, 0.12)' }, }; const buildMockTasks = () => [ { id: 'ap-1', flowType: '合同审批', bizNo: 'HT-ZL-2025-088', summary: '嘉兴氢能示范项目 · 正式合同', initiator: '张明辉', initiateTime: '2026-05-28 09:15', arriveTime: '2026-05-28 14:20', finishTime: '', currentNode: '法务审核', currentAssignee: '王法务', status: '审批中', ccUsers: ['李晓彤'], handledBy: [] }, { id: 'ap-2', flowType: '提车应收款', bizNo: 'TC-2026-0312', summary: '上海迅杰物流 · 3 台提车收款', customerName: '上海迅杰物流有限公司', projectName: '上海氢能城际物流项目', vehicleCount: 3, actualAmount: '186800.00', initiator: '李晓彤', initiateTime: '2026-05-30 10:00', arriveTime: '2026-05-30 11:30', finishTime: '', currentNode: '财务审核', currentAssignee: '张明辉', status: '审批中', ccUsers: ['张明辉', '陈高伟'], handledBy: [] }, { id: 'ap-3', flowType: '租赁账单', bizNo: 'ZD-2026-06-001', summary: '2026年6月 · 粤B58888F 等 5 车', initiator: '陈高伟', initiateTime: '2026-06-01 08:40', arriveTime: '2026-06-01 09:10', finishTime: '', currentNode: '业管主管', currentAssignee: '张明辉', status: '审批中', ccUsers: [], handledBy: [] }, { id: 'ap-4', flowType: '还车应结款', bizNo: 'HC-2026-0520', summary: '沪A03561F 还车结算', plateNo: '沪A03561F', customerName: '上海迅杰物流有限公司', projectName: '上海氢能城际物流项目', pendingSettle: '927.50', depositAmount: '5000.00', refundTotal: '4072.50', payTotal: '0.00', actualRent: '0.00', initiator: '张明辉', initiateTime: '2026-05-20 16:00', arriveTime: '2026-05-21 09:00', finishTime: '2026-05-21 15:30', currentNode: '—', currentAssignee: '', status: '已通过', ccUsers: ['李晓彤'], handledBy: ['张明辉', '财务-赵敏'] }, { id: 'ap-5', flowType: '氢费对账单(对站)', bizNo: 'H2-ST-202605', summary: '平湖加氢站 · 2026年5月对账', initiator: '能源部-周工', initiateTime: '2026-05-25 11:00', arriveTime: '2026-05-26 08:30', finishTime: '2026-05-26 17:00', currentNode: '—', currentAssignee: '', status: '已通过', ccUsers: ['张明辉'], handledBy: ['张明辉'] }, { id: 'ap-6', flowType: '氢费对账单(对客)', bizNo: 'H2-CU-202605', summary: '嘉兴某某物流 · 5月氢费账单', initiator: '李晓彤', initiateTime: '2026-05-27 14:20', arriveTime: '2026-05-28 09:00', finishTime: '', currentNode: 'CEO审批', currentAssignee: 'CEO办公室', status: '审批中', ccUsers: ['张明辉', '陈高伟'], handledBy: ['张明辉'] }, { id: 'ap-8', flowType: '车辆调拨', bizNo: 'DB-2026-018', summary: '粤B58888F · 深圳 → 杭州', initiator: '王东东', initiateTime: '2026-06-01 08:00', arriveTime: '2026-06-01 08:45', finishTime: '', currentNode: '运维主管', currentAssignee: '张明辉', status: '审批中', ccUsers: ['张明辉'], handledBy: [] }, { id: 'ap-9', flowType: '车辆异动', bizNo: 'YD-2026-042', summary: '浙F06900F · 保养至检测站', initiator: '张明辉', initiateTime: '2026-05-18 13:20', arriveTime: '2026-05-18 14:00', finishTime: '2026-05-18 16:10', currentNode: '—', currentAssignee: '', status: '已驳回', ccUsers: ['李晓彤'], handledBy: ['运维主管-刘强'] }, { id: 'ap-10', flowType: '合同审批', bizNo: 'HT-ZL-2024-066', summary: '上海迅杰物流 · 续签合同', initiator: '张明辉', initiateTime: '2026-04-10 10:00', arriveTime: '2026-04-10 11:00', finishTime: '2026-04-12 09:30', currentNode: '—', currentAssignee: '', status: '已通过', ccUsers: ['李晓彤', '王法务'], handledBy: ['法务-王法务', 'CEO办公室'] }, { id: 'ap-11', flowType: '租赁账单', bizNo: 'ZD-2026-05-008', summary: '2026年5月 · 批量租赁账单', initiator: '陈高伟', initiateTime: '2026-05-05 09:00', arriveTime: '2026-05-05 09:30', finishTime: '2026-05-05 18:00', currentNode: '—', currentAssignee: '', status: '已通过', ccUsers: ['张明辉'], handledBy: ['张明辉'] }, { id: 'ap-12', flowType: '提车应收款', bizNo: 'TC-2026-0228', summary: '嘉兴某某物流 · 2 台提车', customerName: '嘉兴某某物流有限公司', projectName: '嘉兴氢能示范项目', vehicleCount: 2, actualAmount: '98600.00', initiator: '张明辉', initiateTime: '2026-02-28 15:00', arriveTime: '2026-03-01 09:00', finishTime: '2026-03-01 11:20', currentNode: '—', currentAssignee: '', status: '已通过', ccUsers: ['财务-赵敏'], handledBy: ['财务-赵敏'] }, { id: 'ap-13', flowType: '车辆调拨', bizNo: 'DB-2026-003', summary: '苏E33333 · 苏州 → 南京', initiator: '王东东', initiateTime: '2026-03-15 10:30', arriveTime: '2026-03-15 11:00', finishTime: '', currentNode: '业务审批', currentAssignee: '李晓彤', status: '审批中', ccUsers: ['张明辉'], handledBy: ['张明辉'] }, { id: 'ap-14', flowType: '还车应结款', bizNo: 'HC-2026-0418', summary: '粤B58888F 还车应结', plateNo: '粤B58888F', customerName: '嘉兴某某物流有限公司', projectName: '嘉兴腾4.5T租赁', pendingSettle: '927.50', depositAmount: '5000.00', refundTotal: '4072.50', payTotal: '0.00', actualRent: '0.00', initiator: '李晓彤', initiateTime: '2026-04-18 09:00', arriveTime: '2026-04-18 09:30', finishTime: '', currentNode: '财务审核', currentAssignee: '张明辉', status: '审批中', ccUsers: ['张明辉'], handledBy: ['业管主管-陈高伟'] }, { id: 'ap-16', flowType: '氢费对账单(对站)', bizNo: 'H2-ST-202604', summary: '嘉兴港区加氢站 · 4月对账', initiator: '能源部-周工', initiateTime: '2026-04-20 10:00', arriveTime: '2026-04-21 09:00', finishTime: '2026-04-22 11:00', currentNode: '—', currentAssignee: '', status: '已通过', ccUsers: ['张明辉', '陈高伟'], handledBy: ['能源部-周工'], ccTime: '2026-04-21 09:05' }, { id: 'ap-17', flowType: '车辆异动', bizNo: 'YD-2026-028', summary: '沪A03561F · 年审至检测站', initiator: '王东东', initiateTime: '2026-02-24 08:00', arriveTime: '2026-02-24 08:30', finishTime: '2026-02-24 12:00', currentNode: '—', currentAssignee: '', status: '已通过', ccUsers: ['张明辉'], handledBy: ['张明辉'], ccTime: '2026-02-24 08:35' }, ]; const PAGE_STYLE = ` .ac-mini-root { min-height: 100dvh; background: linear-gradient(165deg, #e8ebef 0%, ${COLOR_PAGE} 40%); display: flex; justify-content: center; padding: 16px 12px 32px; box-sizing: border-box; font-family: ${FONT_FAMILY}; -webkit-font-smoothing: antialiased; } .ac-phone { width: 100%; max-width: 390px; min-height: 844px; background: ${COLOR_PAGE}; border-radius: 28px; overflow: hidden; box-shadow: 0 24px 48px rgba(15, 23, 42, 0.14), 0 0 0 1px rgba(15, 23, 42, 0.05); display: flex; flex-direction: column; } .ac-chrome { flex-shrink: 0; background: ${COLOR_BG}; } .ac-status-bar { height: 44px; padding: 14px 24px 0; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; } .ac-status-time { font-size: 15px; font-weight: 600; color: ${COLOR_TEXT}; letter-spacing: -0.02em; } .ac-mp-navbar { height: 48px; display: flex; align-items: center; justify-content: center; padding: 0 16px; border-bottom: 1px solid rgba(0,0,0,.05); position: relative; background: ${COLOR_BG}; } .ac-mp-navbar-title { font-size: 17px; font-weight: 700; color: ${COLOR_TEXT}; } .ac-tab-seg { display: flex; gap: 6px; padding: 10px 14px 8px; background: ${COLOR_BG}; flex-shrink: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; } .ac-tab-seg::-webkit-scrollbar { display: none; } .ac-tab-seg-btn { flex: 1 0 auto; min-width: 72px; min-height: 44px; padding: 8px 10px; border: none; border-radius: 10px; background: ${COLOR_PAGE}; color: ${COLOR_MUTED}; font-size: 13px; font-weight: 500; cursor: pointer; touch-action: manipulation; transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; position: relative; } .ac-tab-seg-btn:active { transform: scale(0.97); } .ac-tab-seg-btn.active { background: ${COLOR_BG}; color: ${COLOR_PRIMARY_DEEP}; font-weight: 700; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); } .ac-tab-seg-btn:focus-visible { outline: 2px solid ${COLOR_PRIMARY}; outline-offset: 2px; } .ac-tab-count { display: block; font-size: 11px; font-weight: 600; margin-top: 2px; opacity: 0.85; font-variant-numeric: tabular-nums; } .ac-toolbar { padding: 0 14px 10px; flex-shrink: 0; background: ${COLOR_BG}; border-bottom: 1px solid ${COLOR_LINE}; } .ac-search-wrap { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 12px; background: ${COLOR_PAGE}; border-radius: 12px; border: 1px solid transparent; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .ac-search-wrap:focus-within { border-color: rgba(22, 209, 161, 0.45); box-shadow: 0 0 0 3px rgba(22, 209, 161, 0.12); background: ${COLOR_BG}; } .ac-search-wrap svg { flex-shrink: 0; color: ${COLOR_MUTED}; } .ac-search-input { flex: 1; border: none; background: transparent; font-size: 15px; color: ${COLOR_TEXT}; outline: none; min-width: 0; } .ac-search-input::placeholder { color: ${COLOR_MUTED}; } .ac-filter-scroll { display: flex; gap: 8px; margin-top: 10px; overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; scrollbar-width: none; } .ac-filter-scroll::-webkit-scrollbar { display: none; } .ac-filter-chip { flex-shrink: 0; min-height: 36px; padding: 0 14px; border: 1px solid ${COLOR_LINE}; background: ${COLOR_BG}; border-radius: 999px; font-size: 13px; color: ${COLOR_TEXT_SEC}; cursor: pointer; touch-action: manipulation; transition: all 0.2s ease; white-space: nowrap; } .ac-filter-chip:active { transform: scale(0.96); } .ac-filter-chip.active { border-color: ${COLOR_PRIMARY}; color: ${COLOR_PRIMARY_DEEP}; background: ${COLOR_PRIMARY_SOFT}; font-weight: 600; } .ac-filter-chip:focus-visible { outline: 2px solid ${COLOR_PRIMARY}; outline-offset: 2px; } .ac-filter-more { border-style: dashed; color: ${COLOR_PRIMARY_DEEP}; font-weight: 600; } .ac-list-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px 4px; font-size: 12px; color: ${COLOR_MUTED}; } .ac-list { flex: 1; overflow-y: auto; padding: 4px 14px 28px; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } .ac-card { position: relative; background: ${COLOR_BG}; border-radius: 14px; padding: 14px 14px 12px 16px; margin-bottom: 12px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.05); border: 1px solid rgba(0,0,0,.04); cursor: pointer; touch-action: manipulation; transition: transform 0.18s ease, box-shadow 0.18s ease; animation: ac-card-in 0.35s ease both; } .ac-card::before { content: ''; position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px; border-radius: 0 3px 3px 0; background: var(--ac-accent, ${COLOR_PRIMARY}); } .ac-card:active { transform: scale(0.985); box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06); } .ac-card:focus-visible { outline: 2px solid ${COLOR_PRIMARY}; outline-offset: 2px; } @keyframes ac-card-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { .ac-card { animation: none; transition: none; } .ac-card:active { transform: none; } .ac-tab-seg-btn:active { transform: none; } .ac-filter-chip:active { transform: none; } } .ac-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 8px; } .ac-card-title-row { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; } .ac-card-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: var(--ac-icon-bg, ${COLOR_PRIMARY_SOFT}); color: var(--ac-accent, ${COLOR_PRIMARY_DEEP}); } .ac-card-title { font-size: 16px; font-weight: 700; color: ${COLOR_TEXT}; line-height: 1.25; } .ac-card-bizno { font-size: 12px; color: ${COLOR_MUTED}; margin-top: 3px; font-variant-numeric: tabular-nums; } .ac-status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; } .ac-summary { font-size: 14px; color: ${COLOR_TEXT_SEC}; margin-bottom: 10px; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .ac-meta-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; padding-top: 8px; border-top: 1px dashed ${COLOR_LINE}; } .ac-meta-item { min-width: 0; } .ac-meta-label { font-size: 11px; color: ${COLOR_MUTED}; margin-bottom: 2px; } .ac-meta-value { font-size: 13px; color: ${COLOR_TEXT}; font-weight: 500; overflow: hidden; text-overflow: ellipsis; } .ac-biz-grid { margin-bottom: 10px; padding-top: 0; border-top: none; } .ac-biz-grid .ac-meta-value.ac-amount { color: #F97316; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; } .ac-card-foot { margin-top: 10px; padding-top: 10px; border-top: 1px solid ${COLOR_LINE}; display: flex; align-items: center; justify-content: space-between; gap: 8px; } .ac-card-action { min-height: 36px; padding: 0 16px; border: none; border-radius: 8px; background: linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%); color: #fff; font-size: 13px; font-weight: 700; cursor: pointer; touch-action: manipulation; box-shadow: 0 4px 12px rgba(0, 191, 165, 0.25); } .ac-card-action:active { opacity: 0.92; transform: scale(0.98); } .ac-empty { text-align: center; padding: 56px 24px 32px; } .ac-empty-icon { width: 64px; height: 64px; margin: 0 auto 16px; border-radius: 50%; background: ${COLOR_PAGE}; display: flex; align-items: center; justify-content: center; color: ${COLOR_MUTED}; } .ac-empty-title { font-size: 15px; font-weight: 600; color: ${COLOR_TEXT}; margin-bottom: 6px; } .ac-empty-desc { font-size: 13px; color: ${COLOR_MUTED}; line-height: 1.55; } .ac-drawer-types { display: flex; flex-direction: column; gap: 8px; } .ac-drawer-type-btn { min-height: 48px; padding: 0 16px; border: 1px solid ${COLOR_LINE}; border-radius: 12px; background: ${COLOR_BG}; text-align: left; font-size: 14px; color: ${COLOR_TEXT}; cursor: pointer; touch-action: manipulation; } .ac-drawer-type-btn.active { border-color: ${COLOR_PRIMARY}; background: ${COLOR_PRIMARY_SOFT}; color: ${COLOR_PRIMARY_DEEP}; font-weight: 600; } .tc-scroll { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; padding-bottom: 88px; } .tc-hero { margin: 12px 14px 0; padding: 18px 16px 16px; border-radius: 16px; background: linear-gradient(135deg, #F97316 0%, #EA580C 100%); color: #fff; box-shadow: 0 10px 28px rgba(249, 115, 22, 0.35); } .tc-hero-label { font-size: 13px; opacity: 0.92; margin-bottom: 6px; } .tc-hero-amount { font-size: 36px; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; } .tc-hero-sub { margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,.22); display: flex; align-items: center; justify-content: space-between; gap: 12px; } .tc-hero-compare { font-size: 12px; opacity: 0.9; line-height: 1.45; } .tc-hero-detail-btn { flex-shrink: 0; min-height: 32px; padding: 0 12px; border: 1px solid rgba(255,255,255,.45); border-radius: 999px; background: rgba(255,255,255,.14); color: #fff; font-size: 12px; font-weight: 600; cursor: pointer; } .tc-hero-meta { display: flex; gap: 16px; margin-top: 10px; font-size: 12px; opacity: 0.88; } .tc-section { margin: 12px 14px 0; background: ${COLOR_BG}; border-radius: 14px; overflow: hidden; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); border: 1px solid rgba(0,0,0,.04); } .tc-section-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid ${COLOR_LINE}; } .tc-section-title { font-size: 15px; font-weight: 700; color: ${COLOR_TEXT}; } .tc-section-badge { font-size: 11px; font-weight: 600; color: #F97316; background: rgba(249, 115, 22, 0.12); padding: 2px 8px; border-radius: 999px; } .tc-kv-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 14px; padding: 12px 14px 14px; } .tc-kv-item { min-width: 0; } .tc-kv-label { font-size: 11px; color: ${COLOR_MUTED}; margin-bottom: 3px; } .tc-kv-value { font-size: 13px; color: ${COLOR_TEXT}; font-weight: 500; line-height: 1.4; word-break: break-all; } .tc-kv-value--full { grid-column: 1 / -1; } .tc-vehicle-card { margin: 0 14px 10px; padding: 12px 14px; background: ${COLOR_PAGE}; border-radius: 12px; border: 1px solid ${COLOR_LINE}; } .tc-vehicle-card:last-child { margin-bottom: 14px; } .tc-vehicle-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; } .tc-vehicle-plate { font-size: 15px; font-weight: 700; color: ${COLOR_TEXT}; } .tc-vehicle-model { font-size: 12px; color: ${COLOR_MUTED}; margin-top: 2px; } .tc-vehicle-idx { font-size: 11px; font-weight: 700; color: #F97316; background: rgba(249, 115, 22, 0.12); padding: 2px 8px; border-radius: 999px; flex-shrink: 0; } .tc-vehicle-amount-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 10px 12px; background: ${COLOR_BG}; border-radius: 10px; margin-bottom: 8px; } .tc-vehicle-amount-label { font-size: 12px; color: ${COLOR_MUTED}; } .tc-vehicle-amount-val { font-size: 18px; font-weight: 800; color: #F97316; font-variant-numeric: tabular-nums; } .tc-vehicle-kv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 10px; } .tc-vehicle-kv-item { font-size: 12px; color: ${COLOR_TEXT_SEC}; } .tc-vehicle-kv-item span { color: ${COLOR_MUTED}; } .tc-service-toggle { width: 100%; margin-top: 8px; min-height: 32px; border: none; background: transparent; color: ${COLOR_PRIMARY_DEEP}; font-size: 12px; font-weight: 600; cursor: pointer; text-align: left; padding: 0; } .tc-service-list { margin-top: 8px; padding-top: 8px; border-top: 1px dashed ${COLOR_LINE}; } .tc-service-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 8px; font-size: 12px; padding: 6px 0; border-bottom: 1px solid rgba(0,0,0,.04); } .tc-service-name { color: ${COLOR_TEXT}; font-weight: 500; } .tc-service-amt { color: #F97316; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; } .tc-service-sub { grid-column: 1 / -1; color: ${COLOR_MUTED}; font-size: 11px; } .tc-timeline { padding: 4px 14px 14px; } .tc-step { display: flex; gap: 12px; position: relative; padding-bottom: 16px; } .tc-step:not(:last-child)::before { content: ''; position: absolute; left: 9px; top: 22px; bottom: 0; width: 2px; background: ${COLOR_LINE}; } .tc-step-dot { width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; margin-top: 1px; } .tc-step-dot.done { background: ${COLOR_SUCCESS}; color: #fff; } .tc-step-dot.wait { background: rgba(249, 115, 22, 0.12); color: #F97316; border: 2px solid #F97316; box-sizing: border-box; } .tc-step-title { font-size: 14px; font-weight: 600; color: ${COLOR_TEXT}; } .tc-step-meta { font-size: 12px; color: ${COLOR_MUTED}; margin-top: 4px; line-height: 1.5; } .tc-action-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 20; display: flex; gap: 8px; padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px)); background: rgba(255,255,255,.96); border-top: 1px solid ${COLOR_LINE}; backdrop-filter: blur(8px); } .tc-action-bar--view { justify-content: flex-end; } .tc-btn { flex: 1; min-height: 44px; border-radius: 12px; font-size: 14px; font-weight: 700; cursor: pointer; border: none; touch-action: manipulation; } .tc-btn-comment { flex: 0 0 auto; min-width: 64px; background: ${COLOR_BG}; color: ${COLOR_TEXT_SEC}; border: 1px solid ${COLOR_LINE}; } .tc-btn-terminate { background: ${COLOR_PAGE}; color: ${COLOR_WARN}; border: 1px solid rgba(255, 125, 0, 0.3); } .tc-btn-reject { background: ${COLOR_PAGE}; color: ${COLOR_DANGER}; border: 1px solid rgba(245, 63, 63, 0.25); } .tc-btn-approve { background: linear-gradient(135deg, ${COLOR_PRIMARY} 0%, ${COLOR_PRIMARY_DEEP} 100%); color: #fff; box-shadow: 0 4px 14px rgba(0, 191, 165, 0.3); } .tc-btn:active { opacity: 0.92; transform: scale(0.98); } .tc-approval-form { display: flex; flex-direction: column; gap: 16px; padding: 4px 0 8px; } .tc-form-field { display: flex; flex-direction: column; gap: 8px; } .tc-form-label { font-size: 14px; color: ${COLOR_TEXT}; font-weight: 500; } .tc-form-label-required::before { content: '*'; color: ${COLOR_DANGER}; margin-right: 4px; } .tc-notify-group { display: flex; flex-wrap: wrap; gap: 16px; } .tc-notify-item { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; color: ${COLOR_TEXT_SEC}; cursor: pointer; min-height: 32px; } .tc-notify-item input { width: 16px; height: 16px; accent-color: ${COLOR_PRIMARY}; } .tc-notify-item.disabled { opacity: 0.55; cursor: not-allowed; } .tc-upload-btn { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 14px; border: 1px dashed ${COLOR_LINE}; border-radius: 8px; background: ${COLOR_PAGE}; color: ${COLOR_TEXT_SEC}; font-size: 13px; cursor: pointer; } .tc-upload-hint { font-size: 12px; color: ${COLOR_MUTED}; line-height: 1.55; margin-top: 4px; } .tc-file-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; } .tc-file-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 10px; background: ${COLOR_PAGE}; border-radius: 8px; font-size: 13px; color: ${COLOR_TEXT}; } .tc-file-remove { border: none; background: transparent; color: ${COLOR_MUTED}; font-size: 12px; cursor: pointer; padding: 4px; } .tc-select-person { min-height: 44px; padding: 0 14px; border: 1px solid ${COLOR_LINE}; border-radius: 10px; background: ${COLOR_BG}; color: ${COLOR_MUTED}; font-size: 14px; text-align: left; cursor: pointer; width: 100%; } .tc-select-person.has-value { color: ${COLOR_TEXT}; } .tc-cc-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; } .tc-cc-chip { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 999px; background: ${COLOR_PRIMARY_SOFT}; color: ${COLOR_PRIMARY_DEEP}; font-size: 12px; font-weight: 600; } .tc-cc-chip button { border: none; background: transparent; color: inherit; cursor: pointer; padding: 0 2px; font-size: 14px; line-height: 1; } .tc-textarea { width: 100%; min-height: 96px; padding: 10px 12px; border: 1px solid ${COLOR_LINE}; border-radius: 10px; font-size: 14px; color: ${COLOR_TEXT}; resize: vertical; box-sizing: border-box; font-family: inherit; outline: none; } .tc-textarea:focus { border-color: rgba(22, 209, 161, 0.55); box-shadow: 0 0 0 3px rgba(22, 209, 161, 0.12); } .tc-textarea-counter { text-align: right; font-size: 12px; color: ${COLOR_MUTED}; margin-top: 4px; } .tc-drawer-foot { display: flex; gap: 10px; padding-top: 12px; margin-top: 4px; border-top: 1px solid ${COLOR_LINE}; } .tc-drawer-foot-btn { flex: 1; min-height: 44px; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; border: none; } .tc-drawer-foot-cancel { background: ${COLOR_BG}; color: ${COLOR_TEXT_SEC}; border: 1px solid ${COLOR_LINE}; } .tc-drawer-foot-confirm { background: #1677ff; color: #fff; } .tc-person-list { display: flex; flex-direction: column; gap: 8px; } .tc-person-item { min-height: 48px; padding: 0 14px; border: 1px solid ${COLOR_LINE}; border-radius: 10px; background: ${COLOR_BG}; font-size: 14px; color: ${COLOR_TEXT}; text-align: left; cursor: pointer; } .tc-person-item.selected { border-color: ${COLOR_PRIMARY}; background: ${COLOR_PRIMARY_SOFT}; color: ${COLOR_PRIMARY_DEEP}; font-weight: 600; } .tc-drawer-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; border-bottom: 1px solid ${COLOR_LINE}; font-size: 14px; } .tc-drawer-row-val.highlight { color: #F97316; font-weight: 700; } .tc-drawer-total { margin-top: 8px; padding-top: 12px; border-top: 2px solid ${COLOR_LINE}; display: flex; justify-content: space-between; font-size: 15px; font-weight: 700; } .tc-drawer-total span:last-child { color: #F97316; font-size: 18px; font-variant-numeric: tabular-nums; } .ac-biz-grid .ac-meta-value.ac-amount-hc { color: #8B5CF6; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; } .tc-hero--return { background: linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%); box-shadow: 0 10px 28px rgba(139, 92, 246, 0.35); } .tc-section-badge--purple { color: #8B5CF6; background: rgba(139, 92, 246, 0.12); } .hc-stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 12px 14px 14px; } .hc-stat-card { padding: 12px; background: ${COLOR_PAGE}; border-radius: 10px; border: 1px solid ${COLOR_LINE}; } .hc-stat-label { font-size: 11px; color: ${COLOR_MUTED}; margin-bottom: 6px; } .hc-stat-val { font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; color: ${COLOR_TEXT}; } .hc-stat-val.warn { color: #F97316; } .hc-stat-val.success { color: ${COLOR_SUCCESS}; } .hc-stat-val.danger { color: ${COLOR_DANGER}; } .hc-group-block { padding: 0 14px 12px; } .hc-group-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 0; cursor: pointer; touch-action: manipulation; } .hc-group-title { font-size: 14px; font-weight: 700; color: ${COLOR_TEXT}; } .hc-group-meta { font-size: 11px; color: ${COLOR_MUTED}; margin-top: 2px; } .hc-fee-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; background: ${COLOR_PAGE}; border-radius: 8px; margin-bottom: 6px; font-size: 13px; } .hc-fee-row-name { color: ${COLOR_TEXT_SEC}; flex: 1; min-width: 0; } .hc-fee-row-amt { font-weight: 700; color: #8B5CF6; font-variant-numeric: tabular-nums; flex-shrink: 0; } .hc-sub-block { margin-top: 8px; padding-top: 8px; border-top: 1px dashed ${COLOR_LINE}; } .hc-violation-card { margin: 0 0 8px; padding: 10px 12px; background: ${COLOR_PAGE}; border-radius: 10px; border: 1px solid ${COLOR_LINE}; font-size: 12px; color: ${COLOR_TEXT_SEC}; line-height: 1.55; } .ac-mp-back { position: absolute; left: 8px; width: 40px; height: 40px; border: none; background: transparent; color: ${COLOR_TEXT}; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 2; } .ac-mp-back:active { background: rgba(0,0,0,.05); } .ac-phone--detail { position: relative; } `; const IconSearch = () => ( ); const IconChevron = () => ( ); const IconBack = () => ( ); const IconEmpty = () => ( ); const FlowTypeIcon = ({ flowType }) => { const common = { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }; const map = { 合同审批: , 提车应收款: , 租赁账单: , 还车应结款: , '氢费对账单(对站)': , '氢费对账单(对客)': , 车辆调拨: , 车辆异动: , }; return map[flowType] || map['合同审批']; }; const IphoneStatusBar = () => { const time = moment ? moment().format('HH:mm') : '9:41'; return (
{time}
); }; const formatMoney = (val) => { const n = parseFloat(val); if (Number.isNaN(n)) return val || '—'; return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; const formatYuan = (val) => { const n = parseFloat(val); if (Number.isNaN(n)) return '—'; return `${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} 元`; }; const buildPickupDetail = (task) => { const isShanghai = task?.bizNo === 'TC-2026-0312'; const projectInfo = isShanghai ? { collectCode: 'HT-ZL-2025-088TC0001', contractCode: 'HT-ZL-2025-088', contractType: '正式合同', projectName: '上海氢能城际物流项目', customerName: '上海迅杰物流有限公司', paymentMethod: '预付', paymentCycle: '6个月', contractStart: '2025-03-01', contractEnd: '2026-02-28', businessDept: '业务2部', businessPerson: '李晓彤', } : { collectCode: 'HT-ZL-2025-001TC0001', contractCode: 'HT-ZL-2025-001', contractType: '正式合同', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', paymentMethod: '预付', paymentCycle: '6个月', contractStart: '2025-01-15', contractEnd: '2026-01-14', businessDept: '业务1部', businessPerson: '张经理', }; const vehicles = isShanghai ? [ { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '沪A88123', receivableRent: 62000, actualRent: '61200.00', rentRemark: '首期六期一次性付清', receivableDeposit: 12000, receivableService: 880, actualService: '860.00', discountAmount: '200.00', discountRemark: '首月优惠', serviceItems: [{ name: '代处理费用', receivable: 280, actual: '280.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 600, actual: '580.00', discount: '20.00', remark: '客户协商' }] }, { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '沪A88234', receivableRent: 58000, actualRent: '58000.00', rentRemark: '', receivableDeposit: 10000, receivableService: 500, actualService: '500.00', discountAmount: '0.00', discountRemark: '', serviceItems: [{ name: '保养费用', receivable: 500, actual: '500.00', discount: '0.00', remark: '含首保' }] }, { key: 'v3', index: 3, brand: '重汽', model: 'ZZ1187', plateNo: '沪A88345', receivableRent: 64000, actualRent: '63600.00', rentRemark: '按合同约定', receivableDeposit: 12000, receivableService: 720, actualService: '700.00', discountAmount: '150.00', discountRemark: '批量提车减免', serviceItems: [{ name: '代处理费用', receivable: 220, actual: '220.00', discount: '0.00', remark: '' }, { name: '上牌服务', receivable: 500, actual: '480.00', discount: '20.00', remark: '已含上牌' }] }, ] : [ { key: 'v1', index: 1, brand: '东风', model: 'DFH1180', plateNo: '浙A12345', receivableRent: 30000, actualRent: '29800.00', rentRemark: '首期六期一次性付清', receivableDeposit: 10000, receivableService: 700, actualService: '680.00', discountAmount: '200.00', discountRemark: '首月优惠', serviceItems: [{ name: '代处理费用', receivable: 200, actual: '200.00', discount: '0.00', remark: '' }, { name: '保险上浮', receivable: 500, actual: '480.00', discount: '20.00', remark: '客户协商' }] }, { key: 'v2', index: 2, brand: '福田', model: 'BJ1180', plateNo: '浙A23456', receivableRent: 27000, actualRent: '27000.00', rentRemark: '', receivableDeposit: 8000, receivableService: 300, actualService: '300.00', discountAmount: '0.00', discountRemark: '', serviceItems: [{ name: '保养费用', receivable: 300, actual: '300.00', discount: '0.00', remark: '含首保' }] }, ]; const hasHydrogenPrepay = true; const hydrogen = isShanghai ? { receivable: '4200.00', actual: '4100.00', discount: '100.00', discountRemark: '预付款批量减免' } : { receivable: '3580.00', actual: '3500.00', discount: '80.00', discountRemark: '预付款批量减免' }; const approvalSteps = isShanghai ? [ { title: '业务部主管', department: '业务2部', status: '已通过', person: '李晓彤', approveTime: '2026-05-30 10:30' }, { title: '财务部', department: '财务部', status: '待审批', person: '张明辉', approveTime: '—' }, ] : [ { title: '业务部主管', department: '业务1部', status: '已通过', person: '张经理', approveTime: '2026-02-28 09:30' }, { title: '财务部', department: '财务部', status: '已通过', person: '李财务', approveTime: '2026-02-28 10:15' }, { title: '事业部主管', department: '事业部', status: '已通过', person: '王总', approveTime: '2026-03-01 11:20' }, ]; return { projectInfo, vehicles, hasHydrogenPrepay, hydrogen, approvalSteps, invoiceMethod: '先开票后付款', invoiceRemark: isShanghai ? '增值税专用发票,税率13%,开票项目:*现代服务*车辆租赁费;备注:上海氢能城际物流项目-提车首付款' : '增值税专用发票,税率13%,开票项目:*现代服务*车辆租赁费;备注:嘉兴氢能示范项目-提车首付款', }; }; const calcPickupTotals = (vehicles, hasHydrogenPrepay, hydrogen) => { let receivableRent = 0; let actualRent = 0; let receivableDeposit = 0; let receivableService = 0; let actualService = 0; let discountTotal = 0; vehicles.forEach((v) => { receivableRent += Number(v.receivableRent) || 0; actualRent += parseFloat(v.actualRent) || 0; receivableDeposit += Number(v.receivableDeposit) || 0; receivableService += Number(v.receivableService) || 0; actualService += parseFloat(v.actualService) || 0; discountTotal += parseFloat(v.discountAmount) || 0; }); const hydrogenReceivable = hasHydrogenPrepay ? (parseFloat(hydrogen.receivable) || 0) : 0; const hydrogenActual = hasHydrogenPrepay ? (parseFloat(hydrogen.actual) || 0) : 0; return { receivableRent: receivableRent.toFixed(2), actualRent: actualRent.toFixed(2), receivableDeposit: receivableDeposit.toFixed(2), receivableService: receivableService.toFixed(2), actualService: actualService.toFixed(2), discountTotal: discountTotal.toFixed(2), receivableTotal: (receivableRent + receivableDeposit + receivableService + hydrogenReceivable).toFixed(2), actualTotal: (actualRent + receivableDeposit + actualService - discountTotal + hydrogenActual).toFixed(2), }; }; const statusMeta = (status) => { if (status === '已通过') return { color: 'success', label: status }; if (status === '已驳回') return { color: 'error', label: status }; if (status === '已撤回') return { color: 'default', label: status }; return { color: 'warning', label: status }; }; const isPendingStatus = (status) => status === '审批中' || status === '待审批'; const MiniProgramChrome = ({ title, showBack, onBack }) => (
{showBack ? ( ) : null} {title}
); const TcInfoRow = ({ label, value, full }) => (
{label}
{value || '—'}
); const TcVehicleCard = ({ vehicle }) => { const [serviceOpen, setServiceOpen] = useState(false); const vehicleActual = ( parseFloat(vehicle.actualRent || 0) + parseFloat(vehicle.receivableDeposit || 0) + parseFloat(vehicle.actualService || 0) - parseFloat(vehicle.discountAmount || 0) ).toFixed(2); return (
{vehicle.plateNo || '待上牌'}
{vehicle.brand} · {vehicle.model}
#{vehicle.index}
本车实收合计 {formatMoney(vehicleActual)}
实收月租金 {formatYuan(vehicle.actualRent)}
应收保证金 {formatYuan(vehicle.receivableDeposit)}
实收服务费 {formatYuan(vehicle.actualService)}
减免金额 {formatYuan(vehicle.discountAmount)}
{(vehicle.serviceItems || []).length > 0 && ( <> {serviceOpen && (
{(vehicle.serviceItems || []).map((s, i) => (
{s.name} {formatMoney(s.actual)} 应收 {formatYuan(s.receivable)} · 减免 {formatYuan(s.discount)}
))}
)} )}
); }; const APPROVAL_DECISION_META = { approve: { title: '审批通过', success: '审批已通过(原型)' }, reject: { title: '审批驳回', success: '已驳回(原型)' }, terminate: { title: '审批终止', success: '已终止(原型)' }, }; const MOCK_CC_CANDIDATES = ['李晓彤', '陈高伟', '王法务', '财务-赵敏']; const UPLOAD_ACCEPT = '.png,.jpg,.jpeg,.doc,.docx,.xlsx,.xls,.ppt,.pdf'; const UPLOAD_HINT = '请上传不超过 20MB 的 png, jpg, jpeg, doc, docx, xlsx, xls, ppt, pdf 格式文件'; const emptyDecisionForm = () => ({ notifyEmail: false, notifySms: false, ccUsers: [], opinion: '', attachments: [], }); const ApprovalDecisionDrawer = ({ open, actionType, onClose, onConfirm }) => { const meta = APPROVAL_DECISION_META[actionType] || APPROVAL_DECISION_META.approve; const [form, setForm] = useState(emptyDecisionForm); const [ccPickerOpen, setCcPickerOpen] = useState(false); const fileInputRef = useRef(null); useEffect(() => { if (open) { setForm(emptyDecisionForm()); setCcPickerOpen(false); } }, [open, actionType]); const handleDrawerClose = () => { if (ccPickerOpen) { setCcPickerOpen(false); return; } onClose(); }; const toggleCcUser = (name) => { setForm((p) => ({ ...p, ccUsers: p.ccUsers.includes(name) ? p.ccUsers.filter((n) => n !== name) : [...p.ccUsers, name], })); }; const handleUpload = (e) => { const files = Array.from(e.target.files || []); if (!files.length) return; setForm((p) => ({ ...p, attachments: [ ...p.attachments, ...files.map((f) => ({ id: `${f.name}-${Date.now()}`, name: f.name, size: f.size })), ], })); e.target.value = ''; }; const handleConfirm = () => { onConfirm?.({ actionType, ...form, notifyInternal: true }); onClose(); }; if (!Drawer) return null; return ( {ccPickerOpen ? ( <>
{MOCK_CC_CANDIDATES.map((name) => ( ))}
) : ( <>
通知方式
附件上传
{UPLOAD_HINT}
{form.attachments.length > 0 && (
{form.attachments.map((f) => (
{f.name}
))}
)}
抄送人
{form.ccUsers.length > 0 && (
{form.ccUsers.map((name) => ( {name} ))}
)}
审批意见