1132 lines
43 KiB
JavaScript
1132 lines
43 KiB
JavaScript
// 【重要】必须使用 const Component 作为组件变量名
|
||
// ONE-OS 小程序 - 提车应收款审批办理(参照 web 端提车应收款-查看,适配移动端)
|
||
|
||
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_ACCENT = '#F97316';
|
||
const COLOR_ACCENT_DEEP = '#EA580C';
|
||
const COLOR_ACCENT_SOFT = 'rgba(249, 115, 22, 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_SUCCESS = '#00B42A';
|
||
const COLOR_DANGER = '#F53F3F';
|
||
const COLOR_WARN = '#FF7D00';
|
||
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 formatMoney = (val, withSymbol = true) => {
|
||
const n = parseFloat(val);
|
||
if (Number.isNaN(n)) return val || '—';
|
||
const s = n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
return withSymbol ? `¥${s}` : s;
|
||
};
|
||
|
||
const formatYuan = (val) => {
|
||
const n = parseFloat(val);
|
||
if (Number.isNaN(n)) return '—';
|
||
return `${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} 元`;
|
||
};
|
||
|
||
/** 根据审批任务构建详情 mock(与 web 端字段对齐) */
|
||
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%,开票项目:*现代服务*车辆租赁费;备注:嘉兴氢能示范项目-提车首付款',
|
||
task: task || { bizNo: 'TC-2026-0312', status: '审批中' },
|
||
};
|
||
};
|
||
|
||
const calcTotals = (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;
|
||
const receivableTotal = (
|
||
receivableRent + receivableDeposit + receivableService + hydrogenReceivable
|
||
).toFixed(2);
|
||
const actualTotal = (
|
||
actualRent + receivableDeposit + actualService - discountTotal + hydrogenActual
|
||
).toFixed(2);
|
||
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,
|
||
actualTotal,
|
||
};
|
||
};
|
||
|
||
const PAGE_STYLE = `
|
||
.tc-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;
|
||
}
|
||
.tc-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;
|
||
position: relative;
|
||
}
|
||
.tc-chrome { flex-shrink: 0; background: ${COLOR_BG}; }
|
||
.tc-status-bar {
|
||
height: 44px;
|
||
padding: 14px 24px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-sizing: border-box;
|
||
}
|
||
.tc-status-time { font-size: 15px; font-weight: 600; color: ${COLOR_TEXT}; }
|
||
.tc-mp-navbar {
|
||
height: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 8px 0 4px;
|
||
border-bottom: 1px solid rgba(0,0,0,.05);
|
||
position: relative;
|
||
background: ${COLOR_BG};
|
||
}
|
||
.tc-mp-back {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: none;
|
||
background: transparent;
|
||
color: ${COLOR_TEXT};
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.tc-mp-back:active { background: rgba(0,0,0,.05); }
|
||
.tc-mp-navbar-title {
|
||
position: absolute;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
color: ${COLOR_TEXT};
|
||
max-width: 56%;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.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, ${COLOR_ACCENT} 0%, ${COLOR_ACCENT_DEEP} 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-detail-btn:active { background: rgba(255,255,255,.24); }
|
||
.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: ${COLOR_ACCENT};
|
||
background: ${COLOR_ACCENT_SOFT};
|
||
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};
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.tc-vehicle-model { font-size: 12px; color: ${COLOR_MUTED}; margin-top: 2px; }
|
||
.tc-vehicle-idx {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: ${COLOR_ACCENT};
|
||
background: ${COLOR_ACCENT_SOFT};
|
||
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: ${COLOR_ACCENT};
|
||
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-row:last-child { border-bottom: none; }
|
||
.tc-service-name { color: ${COLOR_TEXT}; font-weight: 500; }
|
||
.tc-service-amt { color: ${COLOR_ACCENT}; 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:last-child { padding-bottom: 0; }
|
||
.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: ${COLOR_ACCENT_SOFT}; color: ${COLOR_ACCENT}; border: 2px solid ${COLOR_ACCENT}; box-sizing: border-box; }
|
||
.tc-step-body { flex: 1; min-width: 0; }
|
||
.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;
|
||
touch-action: manipulation;
|
||
border: none;
|
||
}
|
||
.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-rows { display: flex; flex-direction: column; gap: 0; }
|
||
.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:last-child { border-bottom: none; }
|
||
.tc-drawer-row-label { color: ${COLOR_TEXT_SEC}; }
|
||
.tc-drawer-row-val { font-weight: 700; color: ${COLOR_TEXT}; font-variant-numeric: tabular-nums; }
|
||
.tc-drawer-row-val.highlight { color: ${COLOR_ACCENT}; font-size: 16px; }
|
||
.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: ${COLOR_ACCENT}; font-size: 18px; font-variant-numeric: tabular-nums; }
|
||
`;
|
||
|
||
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 InfoRow = ({ 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 VehicleCard = ({ 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.rentRemark ? (
|
||
<div style={{ fontSize: 12, color: COLOR_MUTED, marginTop: 8 }}>租金备注:{vehicle.rentRemark}</div>
|
||
) : null}
|
||
{(vehicle.serviceItems || []).length > 0 && (
|
||
<>
|
||
<button type="button" className="tc-service-toggle" onClick={() => setServiceOpen((o) => !o)}>
|
||
{serviceOpen ? '收起' : '查看'}服务费明细({(vehicle.serviceItems || []).length} 项)
|
||
</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)}{s.remark ? ` · ${s.remark}` : ''}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const AmountBreakdownDrawer = ({ open, onClose, title, rows, totalLabel, totalValue, highlightTotal }) => (
|
||
Drawer ? (
|
||
<Drawer title={title} placement="bottom" height="auto" open={open} onClose={onClose} styles={{ body: { padding: '8px 20px 24px' } }}>
|
||
<div className="tc-drawer-rows">
|
||
{rows.map((r) => (
|
||
<div className="tc-drawer-row" key={r.label}>
|
||
<span className="tc-drawer-row-label">{r.label}</span>
|
||
<span className={`tc-drawer-row-val${r.highlight ? ' highlight' : ''}`}>{r.value}</span>
|
||
</div>
|
||
))}
|
||
<div className="tc-drawer-total">
|
||
<span>{totalLabel}</span>
|
||
<span>{totalValue}</span>
|
||
</div>
|
||
</div>
|
||
</Drawer>
|
||
) : null
|
||
);
|
||
|
||
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" 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 PickupReceivableApproveView = ({ task, mode = 'approve', onBack }) => {
|
||
const detail = useMemo(() => buildPickupDetail(task), [task]);
|
||
const { projectInfo, vehicles, hasHydrogenPrepay, hydrogen, approvalSteps, invoiceMethod, invoiceRemark } = detail;
|
||
const totals = useMemo(
|
||
() => calcTotals(vehicles, hasHydrogenPrepay, hydrogen),
|
||
[vehicles, hasHydrogenPrepay, hydrogen]
|
||
);
|
||
const [actualDrawerOpen, setActualDrawerOpen] = useState(false);
|
||
const [receivableDrawerOpen, setReceivableDrawerOpen] = useState(false);
|
||
const [decisionOpen, setDecisionOpen] = useState(false);
|
||
const [decisionType, setDecisionType] = useState('approve');
|
||
const [commentOpen, setCommentOpen] = useState(false);
|
||
const showActions = mode === 'approve' || mode === 'view';
|
||
const displayActualTotal = task?.actualAmount || totals.actualTotal;
|
||
|
||
const actualBreakdownRows = useMemo(() => {
|
||
const rows = [
|
||
{ label: '总计实收车辆月租金', value: formatYuan(totals.actualRent) },
|
||
{ label: '总计应收车辆保证金', value: formatYuan(totals.receivableDeposit) },
|
||
{ label: '总计实收服务费', value: formatYuan(totals.actualService) },
|
||
{ label: '总计减免金额', value: `- ${formatYuan(totals.discountTotal)}` },
|
||
];
|
||
if (hasHydrogenPrepay) {
|
||
rows.push({ label: '氢费预充值实收金额', value: formatYuan(hydrogen.actual), highlight: true });
|
||
}
|
||
return rows;
|
||
}, [totals, hasHydrogenPrepay, hydrogen.actual]);
|
||
|
||
const receivableBreakdownRows = useMemo(() => {
|
||
const rows = [
|
||
{ label: '总计应收车辆月租金', value: formatYuan(totals.receivableRent) },
|
||
{ label: '总计应收车辆保证金', value: formatYuan(totals.receivableDeposit) },
|
||
{ label: '总计应收服务费', value: formatYuan(totals.receivableService) },
|
||
];
|
||
if (hasHydrogenPrepay) {
|
||
rows.push({ label: '氢费预充值应收金额', value: formatYuan(hydrogen.receivable) });
|
||
}
|
||
return rows;
|
||
}, [totals, hasHydrogenPrepay, hydrogen.receivable]);
|
||
|
||
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('评论已添加(原型)');
|
||
};
|
||
|
||
const diff = (parseFloat(totals.receivableTotal) - parseFloat(displayActualTotal)).toFixed(2);
|
||
|
||
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">
|
||
<InfoRow label="提车收款单编码" value={projectInfo.collectCode} full />
|
||
<InfoRow label="合同编码" value={projectInfo.contractCode} />
|
||
<InfoRow label="合同类型" value={projectInfo.contractType} />
|
||
<InfoRow label="项目名称" value={projectInfo.projectName} full />
|
||
<InfoRow label="客户企业名称" value={projectInfo.customerName} full />
|
||
<InfoRow label="付款方式" value={projectInfo.paymentMethod} />
|
||
<InfoRow label="付款周期" value={projectInfo.paymentCycle} />
|
||
<InfoRow label="合同生效" value={projectInfo.contractStart} />
|
||
<InfoRow label="合同结束" value={projectInfo.contractEnd} />
|
||
<InfoRow label="业务部门" value={projectInfo.businessDept} />
|
||
<InfoRow 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) => (
|
||
<VehicleCard 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">
|
||
<InfoRow label="应收金额" value={formatYuan(hydrogen.receivable)} />
|
||
<InfoRow label="实收金额" value={formatYuan(hydrogen.actual)} />
|
||
<InfoRow label="减免金额" value={formatYuan(hydrogen.discount)} />
|
||
<InfoRow 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">
|
||
<InfoRow label="开票方式" value={invoiceMethod} />
|
||
<InfoRow 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 className="tc-step-body">
|
||
<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}
|
||
/>
|
||
|
||
<AmountBreakdownDrawer
|
||
open={actualDrawerOpen}
|
||
onClose={() => setActualDrawerOpen(false)}
|
||
title="实收款明细"
|
||
rows={actualBreakdownRows}
|
||
totalLabel="实收款总额"
|
||
totalValue={formatMoney(displayActualTotal)}
|
||
highlightTotal
|
||
/>
|
||
<AmountBreakdownDrawer
|
||
open={receivableDrawerOpen}
|
||
onClose={() => setReceivableDrawerOpen(false)}
|
||
title="应收款明细"
|
||
rows={receivableBreakdownRows}
|
||
totalLabel="应收款总额"
|
||
totalValue={formatMoney(totals.receivableTotal)}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const MiniProgramChrome = ({ title, showBack, onBack }) => {
|
||
const time = moment ? moment().format('HH:mm') : '9:41';
|
||
return (
|
||
<div className="tc-chrome">
|
||
<div className="tc-status-bar">
|
||
<span className="tc-status-time">{time}</span>
|
||
</div>
|
||
<div className="tc-mp-navbar">
|
||
{showBack ? (
|
||
<button type="button" className="tc-mp-back" onClick={onBack} aria-label="返回">
|
||
<IconBack />
|
||
</button>
|
||
) : (
|
||
<span style={{ width: 40, flexShrink: 0 }} />
|
||
)}
|
||
<span className="tc-mp-navbar-title">{title}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const DEFAULT_TASK = {
|
||
id: 'ap-2',
|
||
flowType: '提车应收款',
|
||
bizNo: 'TC-2026-0312',
|
||
customerName: '上海迅杰物流有限公司',
|
||
projectName: '上海氢能城际物流项目',
|
||
vehicleCount: 3,
|
||
actualAmount: '186800.00',
|
||
status: '审批中',
|
||
};
|
||
|
||
const Component = function PickupReceivableApprovePage() {
|
||
const task = window.__pickupApproveTask || DEFAULT_TASK;
|
||
const mode = window.__pickupApproveMode || 'approve';
|
||
|
||
const handleBack = () => {
|
||
if (window.__pickupApproveBack) window.__pickupApproveBack();
|
||
else message.info('返回审批中心(原型)');
|
||
};
|
||
|
||
return (
|
||
<div className="tc-mini-root">
|
||
<style>{PAGE_STYLE}</style>
|
||
<div className="tc-phone">
|
||
<MiniProgramChrome title="提车应收款审批" showBack onBack={handleBack} />
|
||
<PickupReceivableApproveView task={task} mode={mode} onBack={handleBack} />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
if (typeof window !== 'undefined') {
|
||
window.Component = Component;
|
||
window.PickupReceivableApproveView = PickupReceivableApproveView;
|
||
window.buildPickupDetail = buildPickupDetail;
|
||
window.__tcPickupStyles = PAGE_STYLE;
|
||
}
|
||
|
||
export default Component;
|