1865 lines
88 KiB
JavaScript
1865 lines
88 KiB
JavaScript
// 【重要】必须使用 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 (
|
||
<span
|
||
className={className}
|
||
style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
fontSize: 11,
|
||
lineHeight: '18px',
|
||
padding: '2px 8px',
|
||
borderRadius: 999,
|
||
fontWeight: 600,
|
||
color: p.c,
|
||
background: p.bg,
|
||
...style,
|
||
}}
|
||
>
|
||
{children}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
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 = () => (
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
||
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||
</svg>
|
||
);
|
||
|
||
const IconChevron = () => (
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
||
<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.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||
<polyline points="15 18 9 12 15 6" />
|
||
</svg>
|
||
);
|
||
|
||
const IconEmpty = () => (
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||
<polyline points="14 2 14 8 20 8" /><line x1="9" y1="13" x2="15" y2="13" />
|
||
</svg>
|
||
);
|
||
|
||
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 = {
|
||
合同审批: <svg {...common}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /></svg>,
|
||
提车应收款: <svg {...common}><line x1="12" y1="1" x2="12" y2="23" /><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" /></svg>,
|
||
租赁账单: <svg {...common}><rect x="3" y="4" width="18" height="16" rx="2" /><line x1="7" y1="9" x2="17" y2="9" /><line x1="7" y1="13" x2="13" y2="13" /></svg>,
|
||
还车应结款: <svg {...common}><rect x="1" y="3" width="15" height="13" /><polygon points="16 8 20 8 23 11 23 16 16 16 16 8" /><circle cx="5.5" cy="18.5" r="2.5" /><circle cx="18.5" cy="18.5" r="2.5" /></svg>,
|
||
'氢费对账单(对站)': <svg {...common}><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" /></svg>,
|
||
'氢费对账单(对客)': <svg {...common}><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></svg>,
|
||
车辆调拨: <svg {...common}><polyline points="17 1 21 5 17 9" /><path d="M3 11V9a4 4 0 0 1 4-4h14" /><polyline points="7 23 3 19 7 15" /><path d="M21 13v2a4 4 0 0 1-4 4H3" /></svg>,
|
||
车辆异动: <svg {...common}><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" /><circle cx="12" cy="10" r="3" /></svg>,
|
||
};
|
||
return map[flowType] || map['合同审批'];
|
||
};
|
||
|
||
const IphoneStatusBar = () => {
|
||
const time = moment ? moment().format('HH:mm') : '9:41';
|
||
return (
|
||
<div className="ac-status-bar">
|
||
<span className="ac-status-time">{time}</span>
|
||
<span style={{ display: 'flex', gap: 4, alignItems: 'center' }} aria-hidden="true">
|
||
<svg width="16" height="12" viewBox="0 0 16 12"><path fill="currentColor" d="M1 4h2v8H1V4zm4-2h2v10H5V2zm4 3h2v7H9V5zm4-5h2v12h-2V0z" /></svg>
|
||
<svg width="22" height="11" viewBox="0 0 22 11"><rect x="0.5" y="0.5" width="18" height="10" rx="2" stroke="currentColor" fill="none" /><rect x="2" y="2" width="13" height="7" rx="1" fill="currentColor" /></svg>
|
||
</span>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
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 }) => (
|
||
<div className="ac-chrome">
|
||
<IphoneStatusBar />
|
||
<div className="ac-mp-navbar">
|
||
{showBack ? (
|
||
<button type="button" className="ac-mp-back" onClick={onBack} aria-label="返回">
|
||
<IconBack />
|
||
</button>
|
||
) : null}
|
||
<span className="ac-mp-navbar-title">{title}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
const TcInfoRow = ({ label, value, full }) => (
|
||
<div className={`tc-kv-item${full ? ' tc-kv-value--full' : ''}`}>
|
||
<div className="tc-kv-label">{label}</div>
|
||
<div className="tc-kv-value">{value || '—'}</div>
|
||
</div>
|
||
);
|
||
|
||
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 (
|
||
<div className="tc-vehicle-card">
|
||
<div className="tc-vehicle-head">
|
||
<div>
|
||
<div className="tc-vehicle-plate">{vehicle.plateNo || '待上牌'}</div>
|
||
<div className="tc-vehicle-model">{vehicle.brand} · {vehicle.model}</div>
|
||
</div>
|
||
<span className="tc-vehicle-idx">#{vehicle.index}</span>
|
||
</div>
|
||
<div className="tc-vehicle-amount-row">
|
||
<span className="tc-vehicle-amount-label">本车实收合计</span>
|
||
<span className="tc-vehicle-amount-val">{formatMoney(vehicleActual)}</span>
|
||
</div>
|
||
<div className="tc-vehicle-kv">
|
||
<div className="tc-vehicle-kv-item"><span>实收月租金 </span>{formatYuan(vehicle.actualRent)}</div>
|
||
<div className="tc-vehicle-kv-item"><span>应收保证金 </span>{formatYuan(vehicle.receivableDeposit)}</div>
|
||
<div className="tc-vehicle-kv-item"><span>实收服务费 </span>{formatYuan(vehicle.actualService)}</div>
|
||
<div className="tc-vehicle-kv-item"><span>减免金额 </span>{formatYuan(vehicle.discountAmount)}</div>
|
||
</div>
|
||
{(vehicle.serviceItems || []).length > 0 && (
|
||
<>
|
||
<button type="button" className="tc-service-toggle" onClick={() => setServiceOpen((o) => !o)}>
|
||
{serviceOpen ? '收起' : '查看'}服务费明细
|
||
</button>
|
||
{serviceOpen && (
|
||
<div className="tc-service-list">
|
||
{(vehicle.serviceItems || []).map((s, i) => (
|
||
<div className="tc-service-row" key={i}>
|
||
<span className="tc-service-name">{s.name}</span>
|
||
<span className="tc-service-amt">{formatMoney(s.actual)}</span>
|
||
<span className="tc-service-sub">应收 {formatYuan(s.receivable)} · 减免 {formatYuan(s.discount)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
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 (
|
||
<Drawer
|
||
title={ccPickerOpen ? '选择抄送人' : meta.title}
|
||
placement="bottom"
|
||
height={ccPickerOpen ? 360 : 'auto'}
|
||
open={open}
|
||
onClose={handleDrawerClose}
|
||
destroyOnClose
|
||
zIndex={1000}
|
||
styles={{ body: { padding: '8px 16px 24px', maxHeight: ccPickerOpen ? undefined : '78vh', overflowY: 'auto' } }}
|
||
>
|
||
{ccPickerOpen ? (
|
||
<>
|
||
<div className="tc-person-list">
|
||
{MOCK_CC_CANDIDATES.map((name) => (
|
||
<button
|
||
key={name}
|
||
type="button"
|
||
className={`tc-person-item${form.ccUsers.includes(name) ? ' selected' : ''}`}
|
||
onClick={() => toggleCcUser(name)}
|
||
>
|
||
{name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="tc-drawer-foot">
|
||
<button type="button" className="tc-drawer-foot-btn tc-drawer-foot-cancel" onClick={() => setCcPickerOpen(false)}>返回</button>
|
||
<button type="button" className="tc-drawer-foot-btn tc-drawer-foot-confirm" onClick={() => setCcPickerOpen(false)}>确定</button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="tc-approval-form">
|
||
<div className="tc-form-field">
|
||
<div className="tc-form-label">通知方式</div>
|
||
<div className="tc-notify-group">
|
||
<label className="tc-notify-item disabled">
|
||
<input type="checkbox" checked disabled readOnly />站内信
|
||
</label>
|
||
<label className="tc-notify-item">
|
||
<input
|
||
type="checkbox"
|
||
checked={form.notifyEmail}
|
||
onChange={(e) => setForm((p) => ({ ...p, notifyEmail: e.target.checked }))}
|
||
/>
|
||
邮件
|
||
</label>
|
||
<label className="tc-notify-item">
|
||
<input
|
||
type="checkbox"
|
||
checked={form.notifySms}
|
||
onChange={(e) => setForm((p) => ({ ...p, notifySms: e.target.checked }))}
|
||
/>
|
||
短信
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div className="tc-form-field">
|
||
<div className="tc-form-label">附件上传</div>
|
||
<button type="button" className="tc-upload-btn" onClick={() => fileInputRef.current?.click()}>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" /></svg>
|
||
上传
|
||
</button>
|
||
<input ref={fileInputRef} type="file" accept={UPLOAD_ACCEPT} multiple style={{ display: 'none' }} onChange={handleUpload} />
|
||
<div className="tc-upload-hint">{UPLOAD_HINT}</div>
|
||
{form.attachments.length > 0 && (
|
||
<div className="tc-file-list">
|
||
{form.attachments.map((f) => (
|
||
<div className="tc-file-item" key={f.id}>
|
||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
|
||
<button
|
||
type="button"
|
||
className="tc-file-remove"
|
||
onClick={() => setForm((p) => ({ ...p, attachments: p.attachments.filter((a) => a.id !== f.id) }))}
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="tc-form-field">
|
||
<div className="tc-form-label">抄送人</div>
|
||
<button
|
||
type="button"
|
||
className={`tc-select-person${form.ccUsers.length ? ' has-value' : ''}`}
|
||
onClick={() => setCcPickerOpen(true)}
|
||
>
|
||
{form.ccUsers.length ? `已选 ${form.ccUsers.length} 人` : '选择人员'}
|
||
</button>
|
||
{form.ccUsers.length > 0 && (
|
||
<div className="tc-cc-chips">
|
||
{form.ccUsers.map((name) => (
|
||
<span className="tc-cc-chip" key={name}>
|
||
{name}
|
||
<button type="button" aria-label={`移除${name}`} onClick={() => toggleCcUser(name)}>×</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="tc-form-field">
|
||
<div className="tc-form-label">审批意见</div>
|
||
<textarea
|
||
className="tc-textarea"
|
||
placeholder="请输入"
|
||
value={form.opinion}
|
||
onChange={(e) => setForm((p) => ({ ...p, opinion: e.target.value }))}
|
||
maxLength={500}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="tc-drawer-foot">
|
||
<button type="button" className="tc-drawer-foot-btn tc-drawer-foot-cancel" onClick={onClose}>取消</button>
|
||
<button type="button" className="tc-drawer-foot-btn tc-drawer-foot-confirm" onClick={handleConfirm}>确认</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
);
|
||
};
|
||
|
||
const ApprovalCommentDrawer = ({ open, onClose, onConfirm }) => {
|
||
const [content, setContent] = useState('');
|
||
|
||
useEffect(() => {
|
||
if (open) setContent('');
|
||
}, [open]);
|
||
|
||
const handleConfirm = () => {
|
||
if (!content.trim()) {
|
||
message.warning('请输入评论内容');
|
||
return;
|
||
}
|
||
onConfirm?.({ content: content.trim() });
|
||
onClose();
|
||
};
|
||
|
||
if (!Drawer) return null;
|
||
|
||
return (
|
||
<Drawer
|
||
title="添加评论"
|
||
placement="bottom"
|
||
height="auto"
|
||
open={open}
|
||
onClose={onClose}
|
||
destroyOnClose
|
||
zIndex={1000}
|
||
styles={{ body: { padding: '8px 16px 24px' } }}
|
||
>
|
||
<div className="tc-form-field">
|
||
<div className="tc-form-label tc-form-label-required">评论内容</div>
|
||
<textarea
|
||
className="tc-textarea"
|
||
placeholder="请输入评论内容"
|
||
value={content}
|
||
onChange={(e) => setContent(e.target.value.slice(0, 500))}
|
||
maxLength={500}
|
||
/>
|
||
<div className="tc-textarea-counter">{content.length} / 500</div>
|
||
</div>
|
||
<div className="tc-drawer-foot">
|
||
<button type="button" className="tc-drawer-foot-btn tc-drawer-foot-cancel" onClick={onClose}>取消</button>
|
||
<button type="button" className="tc-drawer-foot-btn tc-drawer-foot-confirm" onClick={handleConfirm}>确认</button>
|
||
</div>
|
||
</Drawer>
|
||
);
|
||
};
|
||
|
||
const ApprovalActionBar = ({ mode, onComment, onTerminate, onReject, onApprove }) => {
|
||
if (mode === 'approve') {
|
||
return (
|
||
<div className="tc-action-bar">
|
||
<button type="button" className="tc-btn tc-btn-comment" onClick={onComment}>评论</button>
|
||
<button type="button" className="tc-btn tc-btn-terminate" onClick={onTerminate}>终止</button>
|
||
<button type="button" className="tc-btn tc-btn-reject" onClick={onReject}>驳回</button>
|
||
<button type="button" className="tc-btn tc-btn-approve" onClick={onApprove}>通过</button>
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div className="tc-action-bar tc-action-bar--view">
|
||
<button type="button" className="tc-btn tc-btn-comment" style={{ flex: '0 0 88px' }} onClick={onComment}>评论</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const PickupReceivableApprovePage = ({ task, mode, onBack }) => {
|
||
const detail = useMemo(() => buildPickupDetail(task), [task]);
|
||
const { projectInfo, vehicles, hasHydrogenPrepay, hydrogen, approvalSteps, invoiceMethod, invoiceRemark } = detail;
|
||
const totals = useMemo(() => calcPickupTotals(vehicles, hasHydrogenPrepay, hydrogen), [vehicles, hasHydrogenPrepay, hydrogen]);
|
||
const [actualDrawerOpen, setActualDrawerOpen] = useState(false);
|
||
const displayActualTotal = task?.actualAmount || totals.actualTotal;
|
||
const diff = (parseFloat(totals.receivableTotal) - parseFloat(displayActualTotal)).toFixed(2);
|
||
|
||
const actualRows = [
|
||
{ label: '总计实收车辆月租金', value: formatYuan(totals.actualRent) },
|
||
{ label: '总计应收车辆保证金', value: formatYuan(totals.receivableDeposit) },
|
||
{ label: '总计实收服务费', value: formatYuan(totals.actualService) },
|
||
{ label: '总计减免金额', value: `- ${formatYuan(totals.discountTotal)}` },
|
||
];
|
||
if (hasHydrogenPrepay) actualRows.push({ label: '氢费预充值实收金额', value: formatYuan(hydrogen.actual), highlight: true });
|
||
|
||
const [decisionOpen, setDecisionOpen] = useState(false);
|
||
const [decisionType, setDecisionType] = useState('approve');
|
||
const [commentOpen, setCommentOpen] = useState(false);
|
||
const showActions = mode === 'approve' || mode === 'view';
|
||
|
||
const openDecision = (type) => {
|
||
setDecisionType(type);
|
||
setDecisionOpen(true);
|
||
};
|
||
|
||
const handleDecisionConfirm = (payload) => {
|
||
const meta = APPROVAL_DECISION_META[payload.actionType];
|
||
if (payload.actionType === 'reject' || payload.actionType === 'terminate') {
|
||
message.warning(meta?.success || '操作成功(原型)');
|
||
} else {
|
||
message.success(meta?.success || '操作成功(原型)');
|
||
}
|
||
if (mode === 'approve') onBack();
|
||
};
|
||
|
||
const handleCommentConfirm = () => {
|
||
message.success('评论已添加(原型)');
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div className="tc-scroll">
|
||
<div className="tc-hero">
|
||
<div className="tc-hero-label">实收款总额</div>
|
||
<div className="tc-hero-amount">{formatMoney(displayActualTotal)}</div>
|
||
<div className="tc-hero-meta">
|
||
<span>{projectInfo.customerName}</span>
|
||
<span>{vehicles.length} 台车</span>
|
||
</div>
|
||
<div className="tc-hero-sub">
|
||
<div className="tc-hero-compare">
|
||
应收款总额 {formatMoney(totals.receivableTotal)}
|
||
<br />
|
||
较应收减免 {formatMoney(diff)}
|
||
</div>
|
||
<button type="button" className="tc-hero-detail-btn" onClick={() => setActualDrawerOpen(true)}>实收明细</button>
|
||
</div>
|
||
</div>
|
||
<div className="tc-section">
|
||
<div className="tc-section-head"><span className="tc-section-title">项目信息</span></div>
|
||
<div className="tc-kv-grid">
|
||
<TcInfoRow label="提车收款单编码" value={projectInfo.collectCode} full />
|
||
<TcInfoRow label="合同编码" value={projectInfo.contractCode} />
|
||
<TcInfoRow label="项目名称" value={projectInfo.projectName} full />
|
||
<TcInfoRow label="客户企业名称" value={projectInfo.customerName} full />
|
||
<TcInfoRow label="付款方式" value={projectInfo.paymentMethod} />
|
||
<TcInfoRow label="付款周期" value={projectInfo.paymentCycle} />
|
||
<TcInfoRow label="合同生效" value={projectInfo.contractStart} />
|
||
<TcInfoRow label="合同结束" value={projectInfo.contractEnd} />
|
||
<TcInfoRow label="业务部门" value={projectInfo.businessDept} />
|
||
<TcInfoRow label="业务负责人" value={projectInfo.businessPerson} />
|
||
</div>
|
||
</div>
|
||
<div className="tc-section">
|
||
<div className="tc-section-head">
|
||
<span className="tc-section-title">提车应收款 · 车辆明细</span>
|
||
<span className="tc-section-badge">{vehicles.length} 台</span>
|
||
</div>
|
||
{vehicles.map((v) => <TcVehicleCard key={v.key} vehicle={v} />)}
|
||
</div>
|
||
{hasHydrogenPrepay && (
|
||
<div className="tc-section">
|
||
<div className="tc-section-head"><span className="tc-section-title">氢费预付款</span></div>
|
||
<div className="tc-kv-grid">
|
||
<TcInfoRow label="应收金额" value={formatYuan(hydrogen.receivable)} />
|
||
<TcInfoRow label="实收金额" value={formatYuan(hydrogen.actual)} />
|
||
<TcInfoRow label="减免金额" value={formatYuan(hydrogen.discount)} />
|
||
<TcInfoRow label="减免备注" value={hydrogen.discountRemark} full />
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="tc-section">
|
||
<div className="tc-section-head"><span className="tc-section-title">开票信息</span></div>
|
||
<div className="tc-kv-grid">
|
||
<TcInfoRow label="开票方式" value={invoiceMethod} />
|
||
<TcInfoRow label="开票备注" value={invoiceRemark} full />
|
||
</div>
|
||
</div>
|
||
<div className="tc-section">
|
||
<div className="tc-section-head"><span className="tc-section-title">审批情况</span></div>
|
||
<div className="tc-timeline">
|
||
{approvalSteps.map((step, idx) => (
|
||
<div className="tc-step" key={idx}>
|
||
<div className={`tc-step-dot ${step.status === '已通过' ? 'done' : 'wait'}`}>{step.status === '已通过' ? '✓' : ''}</div>
|
||
<div>
|
||
<div className="tc-step-title">{step.department || step.title}</div>
|
||
<div className="tc-step-meta">状态:{step.status}<br />审批人:{step.person}<br />时间:{step.approveTime}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div style={{ height: showActions ? 16 : 24 }} />
|
||
</div>
|
||
{showActions && !decisionOpen && !commentOpen && (
|
||
<ApprovalActionBar
|
||
mode={mode}
|
||
onComment={() => setCommentOpen(true)}
|
||
onTerminate={() => openDecision('terminate')}
|
||
onReject={() => openDecision('reject')}
|
||
onApprove={() => openDecision('approve')}
|
||
/>
|
||
)}
|
||
<ApprovalDecisionDrawer
|
||
open={decisionOpen}
|
||
actionType={decisionType}
|
||
onClose={() => setDecisionOpen(false)}
|
||
onConfirm={handleDecisionConfirm}
|
||
/>
|
||
<ApprovalCommentDrawer
|
||
open={commentOpen}
|
||
onClose={() => setCommentOpen(false)}
|
||
onConfirm={handleCommentConfirm}
|
||
/>
|
||
{Drawer ? (
|
||
<Drawer title="实收款明细" placement="bottom" height="auto" open={actualDrawerOpen} onClose={() => setActualDrawerOpen(false)} styles={{ body: { padding: '8px 20px 24px' } }}>
|
||
{actualRows.map((r) => (
|
||
<div className="tc-drawer-row" key={r.label}>
|
||
<span style={{ color: COLOR_TEXT_SEC }}>{r.label}</span>
|
||
<span className={`tc-drawer-row-val${r.highlight ? ' highlight' : ''}`} style={{ fontWeight: 700 }}>{r.value}</span>
|
||
</div>
|
||
))}
|
||
<div className="tc-drawer-total"><span>实收款总额</span><span>{formatMoney(displayActualTotal)}</span></div>
|
||
</Drawer>
|
||
) : null}
|
||
</>
|
||
);
|
||
};
|
||
|
||
const buildReturnSettlementDetail = (task) => {
|
||
const isGuangzhou = task?.bizNo === 'HC-2026-0418';
|
||
const vehicle = isGuangzhou
|
||
? {
|
||
plateNo: '粤B58888F', contractCode: 'LNZLHT20251106001', projectName: '嘉兴腾4.5T租赁',
|
||
customerName: '嘉兴某某物流有限公司', deliveryTime: '2026-02-01 09:30', returnTime: '2026-02-27 16:20',
|
||
fragileInsurance: '是', tireInsurance: '否', maintenanceInsurance: '是',
|
||
}
|
||
: {
|
||
plateNo: '沪A03561F', contractCode: 'LNZLHT20251012003', projectName: '上海氢能城际物流项目',
|
||
customerName: '上海迅杰物流有限公司', deliveryTime: '2026-04-01 10:00', returnTime: '2026-05-18 15:40',
|
||
fragileInsurance: '是', tireInsurance: '是', maintenanceInsurance: '是',
|
||
};
|
||
|
||
const businessServiceRows = [
|
||
{ key: 'bs-0', feeItem: '违章处理违约金', amount: '0.00' },
|
||
{ key: 'bs-1', feeItem: '保险上浮', amount: '0.00' },
|
||
{ key: 'bs-2', feeItem: 'ETC-客户未缴费用', amount: '100.00' },
|
||
{ key: 'bs-3', feeItem: 'ETC卡缺损费', amount: '0.00' },
|
||
{ key: 'bs-4', feeItem: 'ETC设备缺损费', amount: '0.00' },
|
||
];
|
||
const billInfo = { receivedRent: '0.00', actualRent: task?.actualRent || '0.00', shouldRefundRent: '0.00' };
|
||
const energy = {
|
||
deliveryHydrogen: '85.00', returnHydrogen: '72.00', hydrogenUnitPrice: '35.00',
|
||
hydrogenSupplement: '455.00', hydrogenFee: '0.00', electricFee: '0.00', prepayRefund: '0.00', userBalance: '1200.00',
|
||
};
|
||
const operationRows = [
|
||
{ key: 'op-0', feeItem: '清洗费', amount: '0.00' },
|
||
{ key: 'op-1', feeItem: '未结算保养', amount: '372.50' },
|
||
{ key: 'op-2', feeItem: '未结算维修', amount: '0.00' },
|
||
{ key: 'op-3', feeItem: '车损费用', amount: '0.00' },
|
||
{ key: 'op-4', feeItem: '接车服务费', amount: '0.00' },
|
||
];
|
||
const violations = [{
|
||
code: 'WZ202602010001', plateNo: vehicle.plateNo, violationBehavior: '闯红灯',
|
||
violationTime: '2026-02-01', penaltyAmount: '100.00', paymentStatus: '未缴费', handleStatus: '未处理',
|
||
}];
|
||
const approvalSteps = isGuangzhou
|
||
? [
|
||
{ department: '业务服务组', status: '已通过', person: '张三', approveTime: '2026-04-17 16:00' },
|
||
{ department: '能源采购组', status: '已通过', person: '李四', approveTime: '2026-04-17 17:30' },
|
||
{ department: '运维部', status: '已通过', person: '王五', approveTime: '2026-04-18 08:20' },
|
||
{ department: '财务部', status: '待审批', person: '张明辉', approveTime: '—' },
|
||
]
|
||
: [
|
||
{ department: '业务服务组', status: '已通过', person: '张三', approveTime: '2026-05-19 14:00' },
|
||
{ department: '财务部', status: '已通过', person: '李财务', approveTime: '2026-05-21 15:30' },
|
||
];
|
||
|
||
const businessServiceTotal = businessServiceRows.reduce((s, r) => s + (parseFloat(r.amount) || 0), 0).toFixed(2);
|
||
const operationTotal = operationRows.reduce((s, r) => s + (parseFloat(r.amount) || 0), 0).toFixed(2);
|
||
const energyTotal = ((parseFloat(energy.hydrogenSupplement) || 0) + (parseFloat(energy.hydrogenFee) || 0) + (parseFloat(energy.electricFee) || 0)).toFixed(2);
|
||
const depositAmount = task?.depositAmount || '5000.00';
|
||
const pendingSettle = task?.pendingSettle || (
|
||
parseFloat(businessServiceTotal) + parseFloat(billInfo.shouldRefundRent)
|
||
+ parseFloat(energy.hydrogenSupplement) + parseFloat(energy.hydrogenFee) + parseFloat(energy.electricFee)
|
||
- parseFloat(energy.prepayRefund) + parseFloat(operationTotal)
|
||
).toFixed(2);
|
||
const diff = (parseFloat(depositAmount) || 0) - (parseFloat(pendingSettle) || 0);
|
||
const refundTotal = task?.refundTotal || (diff > 0 ? diff.toFixed(2) : '0.00');
|
||
const payTotal = task?.payTotal || (diff < 0 ? Math.abs(diff).toFixed(2) : '0.00');
|
||
|
||
const settleBreakdown = [
|
||
{ label: '业务服务组费用项总和', value: formatYuan(businessServiceTotal) },
|
||
{ label: '车辆应退租金', value: formatYuan(billInfo.shouldRefundRent) },
|
||
{ label: '氢量差补缴金额', value: formatYuan(energy.hydrogenSupplement), highlight: true },
|
||
{ label: '氢费补缴金额', value: formatYuan(energy.hydrogenFee) },
|
||
{ label: '电费补缴金额', value: formatYuan(energy.electricFee) },
|
||
{ label: '预付款退费(减)', value: `- ${formatYuan(energy.prepayRefund)}` },
|
||
{ label: '运维部费用总额', value: formatYuan(operationTotal) },
|
||
];
|
||
|
||
return {
|
||
vehicle, businessServiceRows, billInfo, energy, operationRows, violations,
|
||
approvalSteps, businessServiceTotal, operationTotal, energyTotal,
|
||
depositAmount, pendingSettle, refundTotal, payTotal, settleBreakdown,
|
||
};
|
||
};
|
||
|
||
const HcFeeGroup = ({ title, total, submitter, defaultOpen, children }) => {
|
||
const [open, setOpen] = useState(defaultOpen !== false);
|
||
return (
|
||
<div className="hc-group-block">
|
||
<div className="hc-group-head" role="button" tabIndex={0} onClick={() => setOpen((o) => !o)} onKeyDown={(e) => e.key === 'Enter' && setOpen((o) => !o)}>
|
||
<div>
|
||
<div className="hc-group-title">{title}</div>
|
||
<div className="hc-group-meta">总金额 {formatYuan(total)} · {submitter} · 已提交</div>
|
||
</div>
|
||
<span style={{ color: COLOR_MUTED, fontSize: 12 }}>{open ? '收起' : '展开'}</span>
|
||
</div>
|
||
{open && children}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const ReturnSettlementApprovePage = ({ task, mode, onBack }) => {
|
||
const detail = useMemo(() => buildReturnSettlementDetail(task), [task]);
|
||
const {
|
||
vehicle, businessServiceRows, billInfo, energy, operationRows, violations, approvalSteps,
|
||
businessServiceTotal, operationTotal, energyTotal, depositAmount, pendingSettle, refundTotal, payTotal, settleBreakdown,
|
||
} = detail;
|
||
|
||
const [settleDrawerOpen, setSettleDrawerOpen] = useState(false);
|
||
const [decisionOpen, setDecisionOpen] = useState(false);
|
||
const [decisionType, setDecisionType] = useState('approve');
|
||
const [commentOpen, setCommentOpen] = useState(false);
|
||
const showActions = mode === 'approve' || mode === 'view';
|
||
const displayPending = task?.pendingSettle || pendingSettle;
|
||
const heroSub = parseFloat(payTotal) > 0
|
||
? `应补缴 ${formatMoney(payTotal)}`
|
||
: `应退还 ${formatMoney(refundTotal)}`;
|
||
|
||
const openDecision = (type) => { setDecisionType(type); setDecisionOpen(true); };
|
||
const handleDecisionConfirm = (payload) => {
|
||
const meta = APPROVAL_DECISION_META[payload.actionType];
|
||
if (payload.actionType === 'reject' || payload.actionType === 'terminate') message.warning(meta?.success || '操作成功(原型)');
|
||
else message.success(meta?.success || '操作成功(原型)');
|
||
if (mode === 'approve') onBack();
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div className="tc-scroll">
|
||
<div className="tc-hero tc-hero--return">
|
||
<div className="tc-hero-label">待结算总额</div>
|
||
<div className="tc-hero-amount">{formatMoney(displayPending)}</div>
|
||
<div className="tc-hero-meta">
|
||
<span>{vehicle.plateNo}</span>
|
||
<span>{vehicle.customerName}</span>
|
||
</div>
|
||
<div className="tc-hero-sub">
|
||
<div className="tc-hero-compare">
|
||
保证金 {formatMoney(depositAmount)}
|
||
<br />
|
||
{heroSub}
|
||
<br />
|
||
车辆实际租金 {formatYuan(billInfo.actualRent)}
|
||
</div>
|
||
<button type="button" className="tc-hero-detail-btn" onClick={() => setSettleDrawerOpen(true)}>结算明细</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="tc-section">
|
||
<div className="tc-section-head"><span className="tc-section-title">还车车辆明细</span></div>
|
||
<div className="tc-kv-grid">
|
||
<TcInfoRow label="车牌号" value={vehicle.plateNo} />
|
||
<TcInfoRow label="合同编码" value={vehicle.contractCode} />
|
||
<TcInfoRow label="项目名称" value={vehicle.projectName} full />
|
||
<TcInfoRow label="客户企业名称" value={vehicle.customerName} full />
|
||
<TcInfoRow label="交车时间" value={vehicle.deliveryTime} />
|
||
<TcInfoRow label="还车时间" value={vehicle.returnTime} />
|
||
<TcInfoRow label="易损保" value={vehicle.fragileInsurance} />
|
||
<TcInfoRow label="轮胎保" value={vehicle.tireInsurance} />
|
||
<TcInfoRow label="养护保" value={vehicle.maintenanceInsurance} />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="tc-section">
|
||
<div className="tc-section-head"><span className="tc-section-title">结算概览</span></div>
|
||
<div className="hc-stat-grid">
|
||
<div className="hc-stat-card"><div className="hc-stat-label">保证金总额</div><div className="hc-stat-val">{formatMoney(depositAmount)}</div></div>
|
||
<div className="hc-stat-card"><div className="hc-stat-label">待结算总额</div><div className="hc-stat-val warn">{formatMoney(displayPending)}</div></div>
|
||
<div className="hc-stat-card"><div className="hc-stat-label">应退还总额</div><div className="hc-stat-val success">{formatMoney(refundTotal)}</div></div>
|
||
<div className="hc-stat-card"><div className="hc-stat-label">应补缴总额</div><div className="hc-stat-val danger">{formatMoney(payTotal)}</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="tc-section">
|
||
<div className="tc-section-head">
|
||
<span className="tc-section-title">还车费用明细</span>
|
||
<span className="tc-section-badge tc-section-badge--purple">4 组</span>
|
||
</div>
|
||
<HcFeeGroup title="业务服务组" total={businessServiceTotal} submitter="业务服务组-张三">
|
||
{businessServiceRows.filter((r) => parseFloat(r.amount) > 0).map((r) => (
|
||
<div className="hc-fee-row" key={r.key}><span className="hc-fee-row-name">{r.feeItem}</span><span className="hc-fee-row-amt">{formatYuan(r.amount)}</span></div>
|
||
))}
|
||
<div className="hc-sub-block">
|
||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8, color: COLOR_TEXT }}>车辆租金</div>
|
||
<div className="tc-kv-grid" style={{ padding: 0 }}>
|
||
<TcInfoRow label="本期已收租金" value={formatYuan(billInfo.receivedRent)} />
|
||
<TcInfoRow label="车辆实际租金" value={formatYuan(billInfo.actualRent)} />
|
||
<TcInfoRow label="车辆应退租金" value={formatYuan(billInfo.shouldRefundRent)} />
|
||
</div>
|
||
</div>
|
||
</HcFeeGroup>
|
||
<HcFeeGroup title="能源采购组" total={energyTotal} submitter="能源采购组-李四" defaultOpen={false}>
|
||
<div className="tc-kv-grid" style={{ padding: '0 0 8px' }}>
|
||
<TcInfoRow label="氢量差补缴" value={formatYuan(energy.hydrogenSupplement)} />
|
||
<TcInfoRow label="交车氢量" value={`${energy.deliveryHydrogen} MPa`} />
|
||
<TcInfoRow label="还车氢量" value={`${energy.returnHydrogen} MPa`} />
|
||
<TcInfoRow label="退还氢气单价" value={formatYuan(energy.hydrogenUnitPrice)} />
|
||
<TcInfoRow label="氢费补缴" value={formatYuan(energy.hydrogenFee)} />
|
||
<TcInfoRow label="电费补缴" value={formatYuan(energy.electricFee)} />
|
||
<TcInfoRow label="预付款退费" value={formatYuan(energy.prepayRefund)} />
|
||
<TcInfoRow label="预充值余额" value={formatYuan(energy.userBalance)} />
|
||
</div>
|
||
</HcFeeGroup>
|
||
<HcFeeGroup title="运维部" total={operationTotal} submitter="运维部-王五" defaultOpen={false}>
|
||
{operationRows.filter((r) => parseFloat(r.amount) > 0).map((r) => (
|
||
<div className="hc-fee-row" key={r.key}><span className="hc-fee-row-name">{r.feeItem}</span><span className="hc-fee-row-amt">{formatYuan(r.amount)}</span></div>
|
||
))}
|
||
</HcFeeGroup>
|
||
<HcFeeGroup title="安全组" total="0.00" submitter="安全组-赵六" defaultOpen={false}>
|
||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8, color: COLOR_TEXT }}>违章清单</div>
|
||
{violations.map((v) => (
|
||
<div className="hc-violation-card" key={v.code}>
|
||
<div><strong>{v.violationBehavior}</strong> · {v.penaltyAmount} 元</div>
|
||
<div>{v.code} · {v.violationTime} · {v.paymentStatus}</div>
|
||
</div>
|
||
))}
|
||
</HcFeeGroup>
|
||
</div>
|
||
|
||
<div className="tc-section">
|
||
<div className="tc-section-head"><span className="tc-section-title">审批情况</span></div>
|
||
<div className="tc-timeline">
|
||
{approvalSteps.map((step, idx) => (
|
||
<div className="tc-step" key={idx}>
|
||
<div className={`tc-step-dot ${step.status === '已通过' ? 'done' : 'wait'}`}>{step.status === '已通过' ? '✓' : ''}</div>
|
||
<div>
|
||
<div className="tc-step-title">{step.department}</div>
|
||
<div className="tc-step-meta">状态:{step.status}<br />审批人:{step.person}<br />时间:{step.approveTime}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div style={{ height: showActions ? 16 : 24 }} />
|
||
</div>
|
||
|
||
{showActions && !decisionOpen && !commentOpen && (
|
||
<ApprovalActionBar mode={mode} onComment={() => setCommentOpen(true)} onTerminate={() => openDecision('terminate')} onReject={() => openDecision('reject')} onApprove={() => openDecision('approve')} />
|
||
)}
|
||
<ApprovalDecisionDrawer open={decisionOpen} actionType={decisionType} onClose={() => setDecisionOpen(false)} onConfirm={handleDecisionConfirm} />
|
||
<ApprovalCommentDrawer open={commentOpen} onClose={() => setCommentOpen(false)} onConfirm={() => message.success('评论已添加(原型)')} />
|
||
|
||
{Drawer ? (
|
||
<Drawer title="待结算明细" placement="bottom" height="auto" open={settleDrawerOpen} onClose={() => setSettleDrawerOpen(false)} zIndex={1000} styles={{ body: { padding: '8px 20px 24px' } }}>
|
||
{settleBreakdown.map((r) => (
|
||
<div className="tc-drawer-row" key={r.label}>
|
||
<span style={{ color: COLOR_TEXT_SEC }}>{r.label}</span>
|
||
<span className={`tc-drawer-row-val${r.highlight ? ' highlight' : ''}`} style={{ fontWeight: 700, color: r.highlight ? '#8B5CF6' : undefined }}>{r.value}</span>
|
||
</div>
|
||
))}
|
||
<div className="tc-drawer-total"><span>待结算总额</span><span style={{ color: '#8B5CF6' }}>{formatMoney(displayPending)}</span></div>
|
||
</Drawer>
|
||
) : null}
|
||
</>
|
||
);
|
||
};
|
||
|
||
const filterByTab = (task, tabKey, user) => {
|
||
if (tabKey === 'initiated') return task.initiator === user;
|
||
if (tabKey === 'todo') return task.currentAssignee === user && isPendingStatus(task.status);
|
||
if (tabKey === 'done') return (task.handledBy || []).includes(user);
|
||
if (tabKey === 'cc') return (task.ccUsers || []).includes(user);
|
||
return false;
|
||
};
|
||
|
||
const QUICK_FILTER_TYPES = ['合同审批', '提车应收款', '租赁账单', '车辆调拨'];
|
||
|
||
const XLL_GREEN = '#7AB929';
|
||
const XLL_GREEN_DEEP = '#6AA322';
|
||
const XLL_GREEN_SOFT = 'rgba(122, 185, 41, 0.14)';
|
||
|
||
const EMBED_STYLE = `
|
||
.ac-embed-root {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background: ${COLOR_PAGE};
|
||
}
|
||
.ac-embed-root .ac-tab-seg,
|
||
.ac-embed-root .ac-toolbar,
|
||
.ac-embed-root .ac-list-head { flex-shrink: 0; }
|
||
.ac-embed-root .ac-list { flex: 1; min-height: 0; overflow-y: auto; }
|
||
.ac-phone--embed-detail {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
position: relative;
|
||
background: ${COLOR_PAGE};
|
||
}
|
||
`;
|
||
|
||
const XLL_THEME_PATCH = `
|
||
.xll-module-theme .ac-tab-seg-btn.active { color: ${XLL_GREEN_DEEP}; }
|
||
.xll-module-theme .ac-tab-seg-btn:focus-visible { outline-color: ${XLL_GREEN}; }
|
||
.xll-module-theme .ac-search-wrap:focus-within {
|
||
border-color: rgba(122, 185, 41, 0.45);
|
||
box-shadow: 0 0 0 3px ${XLL_GREEN_SOFT};
|
||
}
|
||
.xll-module-theme .ac-filter-chip.active {
|
||
border-color: ${XLL_GREEN};
|
||
color: ${XLL_GREEN_DEEP};
|
||
background: ${XLL_GREEN_SOFT};
|
||
}
|
||
.xll-module-theme .ac-filter-chip:focus-visible { outline-color: ${XLL_GREEN}; }
|
||
.xll-module-theme .ac-filter-more { color: ${XLL_GREEN_DEEP}; }
|
||
.xll-module-theme .ac-card:focus-visible { outline-color: ${XLL_GREEN}; }
|
||
.xll-module-theme .ac-card-action { color: ${XLL_GREEN}; border-color: rgba(122,185,41,.35); background: ${XLL_GREEN_SOFT}; }
|
||
.xll-module-theme .ac-drawer-type-btn.active { border-color: ${XLL_GREEN}; color: ${XLL_GREEN_DEEP}; background: ${XLL_GREEN_SOFT}; }
|
||
`;
|
||
|
||
const ApprovalCenterPanel = function ApprovalCenterPanel({ embedded = false, theme = 'default', onBack, onOpenPrd }) {
|
||
const [mainTab, setMainTab] = useState('todo');
|
||
const [flowFilter, setFlowFilter] = useState('');
|
||
const [searchKey, setSearchKey] = useState('');
|
||
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
|
||
const [detailApproveTask, setDetailApproveTask] = useState(null);
|
||
const currentUser = MOCK_CURRENT_USER;
|
||
const allTasks = useMemo(() => buildMockTasks(), []);
|
||
|
||
const tabCounts = useMemo(() => {
|
||
const counts = { initiated: 0, todo: 0, done: 0, cc: 0 };
|
||
allTasks.forEach((t) => {
|
||
TAB_ITEMS.forEach((tab) => {
|
||
if (filterByTab(t, tab.key, currentUser)) counts[tab.key] += 1;
|
||
});
|
||
});
|
||
return counts;
|
||
}, [allTasks, currentUser]);
|
||
|
||
const filteredList = useMemo(() => {
|
||
const q = searchKey.trim().toLowerCase();
|
||
let list = allTasks.filter((t) => filterByTab(t, mainTab, currentUser));
|
||
if (flowFilter) list = list.filter((t) => t.flowType === flowFilter);
|
||
if (q) {
|
||
list = list.filter(
|
||
(t) =>
|
||
t.bizNo.toLowerCase().includes(q)
|
||
|| t.summary.toLowerCase().includes(q)
|
||
|| t.flowType.toLowerCase().includes(q)
|
||
|| t.initiator.toLowerCase().includes(q)
|
||
|| (t.customerName && t.customerName.toLowerCase().includes(q))
|
||
|| (t.projectName && t.projectName.toLowerCase().includes(q))
|
||
|| (t.plateNo && t.plateNo.toLowerCase().includes(q))
|
||
);
|
||
}
|
||
return [...list].sort((a, b) => {
|
||
const ta = String(b.arriveTime || b.initiateTime || b.ccTime || '');
|
||
const tb = String(a.arriveTime || a.initiateTime || a.ccTime || '');
|
||
return ta.localeCompare(tb);
|
||
});
|
||
}, [allTasks, mainTab, flowFilter, searchKey, currentUser]);
|
||
|
||
const handleCardClick = useCallback(
|
||
(task) => {
|
||
if (task.flowType === '提车应收款' || task.flowType === '还车应结款') {
|
||
setDetailApproveTask(task);
|
||
return;
|
||
}
|
||
if (mainTab === 'todo') {
|
||
message.info(`打开「${task.flowType}」审批办理页(原型)\n单据:${task.bizNo}`);
|
||
return;
|
||
}
|
||
message.info(`查看「${task.flowType}」详情(原型)\n单据:${task.bizNo}`);
|
||
},
|
||
[mainTab]
|
||
);
|
||
|
||
const closeDetailApprove = useCallback(() => setDetailApproveTask(null), []);
|
||
const detailApproveMode = mainTab === 'todo' ? 'approve' : 'view';
|
||
const themeClass = theme === 'xll' ? ' xll-module-theme' : '';
|
||
const embedStyles = `${PAGE_STYLE}${EMBED_STYLE}${theme === 'xll' ? XLL_THEME_PATCH : ''}`;
|
||
|
||
useEffect(() => {
|
||
if (!embedded || typeof window === 'undefined') return undefined;
|
||
window.__xllAuditBack = () => {
|
||
if (detailApproveTask) {
|
||
setDetailApproveTask(null);
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
return () => { delete window.__xllAuditBack; };
|
||
}, [embedded, detailApproveTask]);
|
||
|
||
if (detailApproveTask) {
|
||
const isReturn = detailApproveTask.flowType === '还车应结款';
|
||
const DetailPage = isReturn ? ReturnSettlementApprovePage : PickupReceivableApprovePage;
|
||
if (embedded) {
|
||
return (
|
||
<div className={`ac-embed-root ac-phone--embed-detail${themeClass}`}>
|
||
<style>{embedStyles}</style>
|
||
<DetailPage task={detailApproveTask} mode={detailApproveMode} onBack={closeDetailApprove} />
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div className="ac-mini-root">
|
||
<style>{PAGE_STYLE}</style>
|
||
<div className="ac-phone ac-phone--detail">
|
||
<MiniProgramChrome title={isReturn ? '还车应结款审批' : '提车应收款审批'} showBack onBack={closeDetailApprove} />
|
||
<DetailPage task={detailApproveTask} mode={detailApproveMode} onBack={closeDetailApprove} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const renderMeta = (label, value) => (
|
||
<div className="ac-meta-item" key={label}>
|
||
<div className="ac-meta-label">{label}</div>
|
||
<div className="ac-meta-value" title={value}>{value || '—'}</div>
|
||
</div>
|
||
);
|
||
|
||
const renderCard = (task, index) => {
|
||
const theme = FLOW_THEME[task.flowType] || { accent: COLOR_PRIMARY_DEEP, soft: COLOR_PRIMARY_SOFT };
|
||
const st = statusMeta(task.status);
|
||
const showApprove = mainTab === 'todo';
|
||
|
||
const metaItems = [
|
||
renderMeta('发起人', task.initiator),
|
||
renderMeta('发起时间', task.initiateTime),
|
||
];
|
||
if (mainTab === 'todo') {
|
||
metaItems.push(renderMeta('到达时间', task.arriveTime));
|
||
if (task.currentNode && task.currentNode !== '—') metaItems.push(renderMeta('当前节点', task.currentNode));
|
||
} else if (mainTab === 'done') {
|
||
metaItems.push(renderMeta('办理时间', task.finishTime || task.arriveTime));
|
||
} else if (mainTab === 'cc') {
|
||
metaItems.push(renderMeta('抄送时间', task.ccTime || task.arriveTime || task.initiateTime));
|
||
}
|
||
if ((mainTab === 'initiated' || mainTab === 'cc') && isPendingStatus(task.status) && task.currentNode && task.currentNode !== '—') {
|
||
metaItems.push(renderMeta('当前节点', task.currentNode));
|
||
}
|
||
|
||
const isPickupReceivable = task.flowType === '提车应收款';
|
||
const isReturnSettlement = task.flowType === '还车应结款';
|
||
const cardSummary = isPickupReceivable || isReturnSettlement
|
||
? `${task.customerName || ''} · ${task.projectName || task.plateNo || ''}`
|
||
: task.summary;
|
||
|
||
return (
|
||
<div
|
||
key={task.id}
|
||
className="ac-card"
|
||
style={{
|
||
'--ac-accent': theme.accent,
|
||
'--ac-icon-bg': theme.soft,
|
||
animationDelay: `${Math.min(index, 8) * 40}ms`,
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-label={`${task.flowType},${task.status},${cardSummary}`}
|
||
onClick={() => handleCardClick(task)}
|
||
onKeyDown={(e) => e.key === 'Enter' && handleCardClick(task)}
|
||
>
|
||
<div className="ac-card-head">
|
||
<div className="ac-card-title-row">
|
||
<span className="ac-card-icon" aria-hidden="true">
|
||
<FlowTypeIcon flowType={task.flowType} />
|
||
</span>
|
||
<div style={{ minWidth: 0 }}>
|
||
<div className="ac-card-title">{task.flowType}</div>
|
||
<div className="ac-card-bizno">{task.bizNo}</div>
|
||
</div>
|
||
</div>
|
||
<Tag color={st.color}>
|
||
<span className="ac-status-dot" aria-hidden="true" />
|
||
{st.label}
|
||
</Tag>
|
||
</div>
|
||
{isPickupReceivable ? (
|
||
<div className="ac-meta-grid ac-biz-grid">
|
||
{renderMeta('客户企业名称', task.customerName)}
|
||
{renderMeta('项目名称', task.projectName)}
|
||
{renderMeta('车辆数', task.vehicleCount != null ? `${task.vehicleCount} 台` : '—')}
|
||
<div className="ac-meta-item" key="实收金额">
|
||
<div className="ac-meta-label">实收金额</div>
|
||
<div className="ac-meta-value ac-amount" title={formatMoney(task.actualAmount)}>
|
||
{formatMoney(task.actualAmount)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : isReturnSettlement ? (
|
||
<div className="ac-meta-grid ac-biz-grid">
|
||
{renderMeta('车牌号', task.plateNo)}
|
||
{renderMeta('客户企业名称', task.customerName)}
|
||
{renderMeta('项目名称', task.projectName)}
|
||
<div className="ac-meta-item" key="待结算总额">
|
||
<div className="ac-meta-label">待结算总额</div>
|
||
<div className="ac-meta-value ac-amount-hc" title={formatMoney(task.pendingSettle)}>
|
||
{formatMoney(task.pendingSettle)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="ac-summary">{task.summary}</div>
|
||
)}
|
||
<div className="ac-meta-grid">{metaItems}</div>
|
||
{showApprove && (
|
||
<div className="ac-card-foot">
|
||
<span style={{ fontSize: 12, color: COLOR_MUTED }}>待您审批</span>
|
||
<button
|
||
type="button"
|
||
className="ac-card-action"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleCardClick(task);
|
||
}}
|
||
>
|
||
去审批
|
||
</button>
|
||
</div>
|
||
)}
|
||
{!showApprove && (
|
||
<div className="ac-card-foot" style={{ justifyContent: 'flex-end', borderTop: 'none', paddingTop: 4, marginTop: 4 }}>
|
||
<span style={{ fontSize: 12, color: COLOR_MUTED, display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||
查看详情 <IconChevron />
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const activeTabLabel = TAB_ITEMS.find((t) => t.key === mainTab)?.label || '';
|
||
|
||
const listContent = (
|
||
<>
|
||
<div className="ac-tab-seg" role="tablist" aria-label="审批列表分类">
|
||
{TAB_ITEMS.map((tab) => (
|
||
<button
|
||
key={tab.key}
|
||
type="button"
|
||
role="tab"
|
||
aria-selected={mainTab === tab.key}
|
||
className={`ac-tab-seg-btn${mainTab === tab.key ? ' active' : ''}`}
|
||
onClick={() => setMainTab(tab.key)}
|
||
>
|
||
{tab.short}
|
||
<span className="ac-tab-count">{tabCounts[tab.key]} 条</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="ac-toolbar">
|
||
<div className="ac-search-wrap">
|
||
<IconSearch />
|
||
<input
|
||
className="ac-search-input"
|
||
type="search"
|
||
placeholder="搜索单据号、摘要、发起人"
|
||
value={searchKey}
|
||
onChange={(e) => setSearchKey(e.target.value)}
|
||
aria-label="搜索审批任务"
|
||
/>
|
||
</div>
|
||
<div className="ac-filter-scroll" role="group" aria-label="流程类型筛选">
|
||
<button
|
||
type="button"
|
||
className={`ac-filter-chip${!flowFilter ? ' active' : ''}`}
|
||
onClick={() => setFlowFilter('')}
|
||
>
|
||
全部
|
||
</button>
|
||
{QUICK_FILTER_TYPES.map((type) => (
|
||
<button
|
||
key={type}
|
||
type="button"
|
||
className={`ac-filter-chip${flowFilter === type ? ' active' : ''}`}
|
||
onClick={() => setFlowFilter(flowFilter === type ? '' : type)}
|
||
>
|
||
{type}
|
||
</button>
|
||
))}
|
||
<button type="button" className="ac-filter-chip ac-filter-more" onClick={() => setFilterDrawerOpen(true)}>
|
||
更多类型
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="ac-list-head">
|
||
<span>{activeTabLabel}</span>
|
||
<span>共 {filteredList.length} 条</span>
|
||
</div>
|
||
|
||
<div className="ac-list">
|
||
{filteredList.length === 0 ? (
|
||
<div className="ac-empty">
|
||
<div className="ac-empty-icon"><IconEmpty /></div>
|
||
<div className="ac-empty-title">暂无{activeTabLabel}任务</div>
|
||
<div className="ac-empty-desc">
|
||
{searchKey || flowFilter
|
||
? '试试清空搜索或切换流程类型'
|
||
: '新的审批任务到达后将在此展示'}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
filteredList.map(renderCard)
|
||
)}
|
||
</div>
|
||
|
||
{Drawer ? (
|
||
<Drawer
|
||
title="选择流程类型"
|
||
placement="bottom"
|
||
height={420}
|
||
open={filterDrawerOpen}
|
||
onClose={() => setFilterDrawerOpen(false)}
|
||
styles={{ body: { padding: '12px 16px 24px' } }}
|
||
>
|
||
<div className="ac-drawer-types">
|
||
<button
|
||
type="button"
|
||
className={`ac-drawer-type-btn${!flowFilter ? ' active' : ''}`}
|
||
onClick={() => { setFlowFilter(''); setFilterDrawerOpen(false); }}
|
||
>
|
||
全部类型
|
||
</button>
|
||
{APPROVAL_FLOW_TYPES.map((type) => (
|
||
<button
|
||
key={type}
|
||
type="button"
|
||
className={`ac-drawer-type-btn${flowFilter === type ? ' active' : ''}`}
|
||
onClick={() => { setFlowFilter(type); setFilterDrawerOpen(false); }}
|
||
>
|
||
{type}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Drawer>
|
||
) : null}
|
||
</>
|
||
);
|
||
|
||
if (embedded) {
|
||
return (
|
||
<div className={`ac-embed-root${themeClass}`}>
|
||
<style>{embedStyles}</style>
|
||
{listContent}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="ac-mini-root">
|
||
<style>{PAGE_STYLE}</style>
|
||
<div className="ac-phone">
|
||
<MiniProgramChrome title="审批中心" />
|
||
{listContent}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const Component = function ApprovalCenterMiniApp() {
|
||
return <ApprovalCenterPanel embedded={false} />;
|
||
};
|
||
|
||
if (typeof window !== 'undefined') {
|
||
window.Component = Component;
|
||
window.ONEOS_MP_EMBED = window.ONEOS_MP_EMBED || {};
|
||
window.ONEOS_MP_EMBED.ApprovalCenterPanel = ApprovalCenterPanel;
|
||
}
|
||
|
||
export default Component;
|