diff --git a/src/common/oneos-web-approval/components/ApprovalCard.tsx b/src/common/oneos-web-approval/components/ApprovalCard.tsx index 9836c25..b52a183 100644 --- a/src/common/oneos-web-approval/components/ApprovalCard.tsx +++ b/src/common/oneos-web-approval/components/ApprovalCard.tsx @@ -21,7 +21,7 @@ function statusClassName(status: ApprovalCardItem['status']): string { export function ApprovalCard({ item, selected = false, onClick }: ApprovalCardProps) { const typeColor = getApprovalTypeColor(item.type); - const statusLabel = formatStatusLabel(item); + const statusLabel = item.statusLabel ?? formatStatusLabel(item); return (
) : null} + + {item.extraTags && item.extraTags.length > 0 ? ( +
+ {item.extraTags.map((tag) => ( + + {tag} + + ))} +
+ ) : null}
- {item.showInitiatorInFooter ? ( + {item.initiatorSuffixInFooter ? ( + {item.initiatedBy}发起 + ) : item.showInitiatorInFooter ? ( 发起人 {item.initiatedBy} ) : null} diff --git a/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx b/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx index d4c700d..36fae93 100644 --- a/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx +++ b/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx @@ -21,6 +21,7 @@ import { getApprovalTypeLabel, } from '../config/approvalTypeConfig'; import { InsuranceComparisonDetail } from './InsuranceComparisonDetail'; +import { LeaseBillingDetail } from './LeaseBillingDetail'; const { Text, Title } = Typography; @@ -56,6 +57,10 @@ export function ApprovalDetailPlaceholder({ item, onRefresh }: ApprovalDetailPla return ; } + if (item.type === 'billing') { + return ; + } + const statusLabel = formatStatusLabel(item); const typeLabel = getApprovalTypeLabel(item.type); diff --git a/src/common/oneos-web-approval/components/InsuranceComparisonDetail.tsx b/src/common/oneos-web-approval/components/InsuranceComparisonDetail.tsx index ff49360..fb96aa1 100644 --- a/src/common/oneos-web-approval/components/InsuranceComparisonDetail.tsx +++ b/src/common/oneos-web-approval/components/InsuranceComparisonDetail.tsx @@ -73,7 +73,7 @@ function buildSummaryRows(item: ApprovalCardItem, comparisonNo: string) { { label: '当前审批人', value: item.currentApprover ?? '-' }, { label: '保险公司数量', value: companyCount }, { label: '采购险种', value: insuranceType }, - { label: '中选保险公司', value: item.subtitle?.replace(/^中选:/, '') ?? '-' }, + { label: '中选保险公司', value: item.subtitle ?? '-' }, ]; } @@ -96,7 +96,7 @@ function buildVehicleRows(item: ApprovalCardItem): VehicleRow[] { } function buildQuoteRows(item: ApprovalCardItem): QuoteRow[] { - const winner = item.subtitle?.replace(/^中选:/, '') ?? '国元农业保险上海分公司'; + const winner = item.subtitle ?? '国元农业保险上海分公司'; const finalAmount = getFact(item, '最终报价').replace('¥', ''); return [ diff --git a/src/common/oneos-web-approval/components/LeaseBillingDetail.tsx b/src/common/oneos-web-approval/components/LeaseBillingDetail.tsx new file mode 100644 index 0000000..96f41d1 --- /dev/null +++ b/src/common/oneos-web-approval/components/LeaseBillingDetail.tsx @@ -0,0 +1,290 @@ +import React, { useState } from 'react'; +import { + Avatar, + Button, + Empty, + Space, + Table, + Tabs, + Tag, + Timeline, + Typography, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { + CheckCircleOutlined, + ClockCircleOutlined, + CopyOutlined, + ReloadOutlined, + UserOutlined, +} from '@ant-design/icons'; +import type { ApprovalCardItem } from '../types'; +import { formatStatusLabel, getApprovalTypeLabel } from '../config/approvalTypeConfig'; + +const { Text, Title } = Typography; + +export interface LeaseBillingDetailProps { + item: ApprovalCardItem; + onRefresh?: () => void; +} + +function getFact(item: ApprovalCardItem, label: string): string { + return item.keyFacts?.find((fact) => fact.label === label)?.value ?? '-'; +} + +function parseBillingCycle(text: string) { + const normalized = text.replace('·', ' ').trim(); + const periodMatch = normalized.match(/第\d+期/); + const period = periodMatch?.[0] ?? '-'; + const fullDateMatch = normalized.match(/(\d{4}-\d{2}-\d{2})至(\d{4}-\d{2}-\d{2})/); + if (fullDateMatch) { + return { period, startDate: fullDateMatch[1], endDate: fullDateMatch[2] }; + } + const rangeMatch = normalized.match(/(\d{2}-\d{2})至(\d{2}-\d{2})/); + const startDate = rangeMatch ? `2026-${rangeMatch[1]}` : '-'; + const endDate = rangeMatch ? `2026-${rangeMatch[2]}` : '-'; + return { period, startDate, endDate }; +} + +function parseAmount(value: string): string { + return value.replace(/[¥,]/g, '').trim() || '0.00'; +} + +function buildBillNo(item: ApprovalCardItem): string { + const seq = item.id.replace(/\D/g, '').padStart(16, '0'); + return `207${seq}994`; +} + +interface BillDetailRow { + key: string; + index: number; + brand: string; + model: string; + plateNo: string; + rentReceivable: string; + rentReceived: string; + rentRemark: string; + waivedAmount: string; + depositReceivable: string; +} + +function buildBillDetailRows(item: ApprovalCardItem): BillDetailRow[] { + const vehicleCount = Number(getFact(item, '计费车辆数量').match(/(\d+)/)?.[1] ?? 2); + const amount = parseAmount(getFact(item, '应收款总额')); + const receivedAmount = parseAmount(getFact(item, '实收款总额')); + const perVehicleReceivable = (Number(amount) / vehicleCount).toFixed(2); + const perVehicleReceived = (Number(receivedAmount) / vehicleCount).toFixed(2); + + return Array.from({ length: vehicleCount }, (_, index) => ({ + key: String(index + 1), + index: index + 1, + brand: '帕力安', + model: '4.5T 厢式货车', + plateNo: index === 0 ? '粤AGR9901' : '粤AGR9902', + rentReceivable: perVehicleReceivable, + rentReceived: perVehicleReceived, + rentRemark: '-', + waivedAmount: '0.00', + depositReceivable: '0.00', + })); +} + +export function LeaseBillingDetail({ item, onRefresh }: LeaseBillingDetailProps) { + const [activeTab, setActiveTab] = useState('detail'); + const billNo = buildBillNo(item); + const typeLabel = getApprovalTypeLabel(item.type); + const statusLabel = item.statusLabel ?? formatStatusLabel(item); + const cycle = parseBillingCycle(item.subtitle ?? ''); + const receivableTotal = parseAmount(getFact(item, '应收款总额')); + const receivedTotal = parseAmount(getFact(item, '实收款总额')); + const invoicedTotal = parseAmount(getFact(item, '开票总额')); + const pendingInvoice = Math.max(Number(receivedTotal) - Number(invoicedTotal), 0).toFixed(2); + const billDetailRows = buildBillDetailRows(item); + + const billInfoRows = [ + { label: '合同编码', value: 'AUTO_REG_20260703105722' }, + { label: '合同类型', value: '租赁合同' }, + { label: '项目名称', value: item.projectName ?? '-' }, + { label: '客户名称', value: item.title }, + { label: '交车任务编码', value: '-' }, + { label: '账单编码', value: `RS-ZD-${item.id.replace('ac-', '')}-${billNo.slice(-10)}` }, + { label: '账单期数', value: cycle.period }, + { label: '账单开始日期', value: cycle.startDate }, + { label: '账单结束日期', value: cycle.endDate }, + ]; + + const detailColumns: ColumnsType = [ + { title: '序号', dataIndex: 'index', width: 56, fixed: 'left' }, + { title: '品牌', dataIndex: 'brand', width: 80 }, + { title: '型号', dataIndex: 'model', width: 120 }, + { title: '车牌号', dataIndex: 'plateNo', width: 100 }, + { title: '应收车辆月租金', dataIndex: 'rentReceivable', width: 120 }, + { title: '实收车辆月租金', dataIndex: 'rentReceived', width: 120 }, + { title: '车辆租金备注', dataIndex: 'rentRemark', width: 110 }, + { title: '减免金额', dataIndex: 'waivedAmount', width: 90 }, + { title: '应收车辆保证金', dataIndex: 'depositReceivable', width: 120 }, + ]; + + return ( +
+
+ + 编号: + , text: billNo }}>{billNo} + + +
+ +
+
+ + {typeLabel}审批 + + {statusLabel} +
+
+ + + } /> + {item.initiatedBy} + + + 流程分类 + {typeLabel} + + + + 提交时间 + {item.initiatedAt} + + +
+
+ + + + 账单信息 + + + + {Array.from({ length: Math.ceil(billInfoRows.length / 3) }).map((_, rowIndex) => { + const cells = billInfoRows.slice(rowIndex * 3, rowIndex * 3 + 3); + return ( + + {cells.map((cell) => ( + + + + + ))} + {cells.length < 3 + ? Array.from({ length: 3 - cells.length }).map((__, fillIndex) => ( + + + ); + })} + +
{cell.label}{cell.value} + + + )) + : null} +
+ +
+ + 账单明细 + +
+ + 应收款总额 {receivableTotal} 元 + + + 实收款总额 {receivedTotal} 元 + + + 开票总额 {invoicedTotal} 元 + + + 待开票 {pendingInvoice} 元 + +
+ + + +
+ + 审批记录 + + +
+ 财务经理审批 + 待审批 +
+
+ ), + }, + { + color: 'green', + dot: , + children: ( +
+
+ 发起审批 + 通过 +
+ {item.initiatedBy} + + {item.initiatedAt} + +
+ ), + }, + ]} + /> + + + ), + }, + { + key: 'flow', + label: '审批流程图', + children: ( +
+ +
+ ), + }, + ]} + /> + +
+ +
+ + ); +} diff --git a/src/common/oneos-web-approval/data/mockApprovalCases.ts b/src/common/oneos-web-approval/data/mockApprovalCases.ts index ffddfef..509da9d 100644 --- a/src/common/oneos-web-approval/data/mockApprovalCases.ts +++ b/src/common/oneos-web-approval/data/mockApprovalCases.ts @@ -27,18 +27,55 @@ export const MOCK_APPROVAL_CASES: ApprovalCardItem[] = [ status: 'processing', listTab: 'initiated', title: '沪A51676F', - subtitle: '中选:国元农业保险上海分公司', + subtitle: '国元农业保险上海分公司', keyFacts: [ { label: '采购险种', value: '商业险' }, { label: '最终报价', value: '¥1000.00' }, { label: '保险公司数量', value: '3家' }, { label: '最晚付费日', value: '2026-08-09' }, ], - currentApprover: '财务部-赵总', + currentApprover: '赵总', showInitiatorInFooter: true, initiatedBy: CURRENT_USER, initiatedAt: '2026-06-10 14:00', }, + { + id: 'ac-24', + type: 'return_settlement', + status: 'processing', + listTab: 'initiated', + title: '粤AGR8536', + subtitle: '环迪新能源(广州)有限公司 · 荣达餐饮试-租赁帕力安4.5T', + keyFacts: [ + { label: '应退还总额', value: '¥11,610.18', emphasis: true }, + { label: '保证金总额', value: '¥10,000.00' }, + { label: '待结算总额', value: '¥-1,610.18' }, + { label: '应补缴总额', value: '¥0' }, + ], + currentApprover: '赵总', + showInitiatorInFooter: true, + initiatedBy: CURRENT_USER, + initiatedAt: '2026-06-10 14:00', + }, + { + id: 'ac-25', + type: 'billing', + status: 'processing', + listTab: 'initiated', + title: '嘉兴市振鑫物流有限公司', + subtitle: '第1期 · 2026-07-03至2026-07-31', + projectName: '浮云测试-0703-试用-1', + keyFacts: [ + { label: '计费车辆数量', value: '2辆' }, + { label: '应收款总额', value: '¥288.00', emphasis: true }, + { label: '实收款总额', value: '¥288.00' }, + { label: '开票总额', value: '¥244.00' }, + ], + currentApprover: '赵总', + showInitiatorInFooter: true, + initiatedBy: '王雨昊', + initiatedAt: '2026-07-31 16:40', + }, ]; export function filterCasesByTab( diff --git a/src/common/oneos-web-approval/styles/index.css b/src/common/oneos-web-approval/styles/index.css index dbb4288..242ab68 100644 --- a/src/common/oneos-web-approval/styles/index.css +++ b/src/common/oneos-web-approval/styles/index.css @@ -272,6 +272,7 @@ font-weight: 500; line-height: 1.45; word-break: break-word; + white-space: pre-line; } .ap-card__fact-value--emphasis { @@ -310,6 +311,25 @@ border-color: #fde68a; } +.ap-card__extra-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.ap-card__extra-tag { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + background: #f1f5f9; + color: var(--ap-muted); + border: 1px solid #e2e8f0; +} + .ap-card__footer { display: flex; align-items: flex-end; @@ -571,6 +591,56 @@ background: #f1f5f9; } +.ap-billing-summary th { + width: 10%; +} + +.ap-billing-summary td { + width: 23%; +} + +.ap-billing-detail-section { + margin-top: 8px; +} + +.ap-billing-totals { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 20px; + margin-bottom: 12px; + padding: 12px 14px; + border-radius: 8px; + background: #f8fafc; + font-size: 13px; + color: var(--ap-muted); +} + +.ap-billing-totals__value { + color: var(--ap-primary) !important; + font-weight: 600; +} + +.ap-billing-totals__meta { + margin-left: auto; + font-size: 12px; +} + +.ap-billing-timeline-section { + margin-top: 24px; +} + +.ap-billing-timeline-item { + padding-bottom: 4px; +} + +.ap-billing-timeline-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + @media (max-width: 960px) { .ap-page { height: auto; diff --git a/src/common/oneos-web-approval/types.ts b/src/common/oneos-web-approval/types.ts index 6d8bbdc..82186c9 100644 --- a/src/common/oneos-web-approval/types.ts +++ b/src/common/oneos-web-approval/types.ts @@ -50,8 +50,12 @@ export interface ApprovalCardItem { initiatedBy: string; initiatedAt: string; typeLabel?: string; + statusLabel?: string; + projectName?: string; footerText?: string; showInitiatorInFooter?: boolean; + initiatorSuffixInFooter?: boolean; + extraTags?: string[]; ccUsers?: string[]; handledBy?: string; }