feat: iterate approval center cards and billing detail form.
Add lease billing and return settlement cards, insurance and billing detail pages, and card footer variants.
This commit is contained in:
@@ -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 (
|
||||
<article
|
||||
@@ -75,11 +75,23 @@ export function ApprovalCard({ item, selected = false, onClick }: ApprovalCardPr
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{item.extraTags && item.extraTags.length > 0 ? (
|
||||
<div className="ap-card__extra-tags">
|
||||
{item.extraTags.map((tag) => (
|
||||
<span key={`${item.id}-${tag}`} className="ap-card__extra-tag">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="ap-card__footer">
|
||||
<div className="ap-card__footer-meta">
|
||||
{item.showInitiatorInFooter ? (
|
||||
{item.initiatorSuffixInFooter ? (
|
||||
<span className="ap-card__footer-text">{item.initiatedBy}发起</span>
|
||||
) : item.showInitiatorInFooter ? (
|
||||
<span className="ap-card__footer-text">发起人 {item.initiatedBy}</span>
|
||||
) : null}
|
||||
<span className="ap-card__footer-text">
|
||||
|
||||
@@ -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 <InsuranceComparisonDetail item={item} onRefresh={onRefresh} />;
|
||||
}
|
||||
|
||||
if (item.type === 'billing') {
|
||||
return <LeaseBillingDetail item={item} onRefresh={onRefresh} />;
|
||||
}
|
||||
|
||||
const statusLabel = formatStatusLabel(item);
|
||||
const typeLabel = getApprovalTypeLabel(item.type);
|
||||
|
||||
|
||||
@@ -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 [
|
||||
|
||||
290
src/common/oneos-web-approval/components/LeaseBillingDetail.tsx
Normal file
290
src/common/oneos-web-approval/components/LeaseBillingDetail.tsx
Normal file
@@ -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<BillDetailRow> = [
|
||||
{ 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 (
|
||||
<div className="ap-detail ap-detail--billing">
|
||||
<div className="ap-detail__toolbar">
|
||||
<Space size={8}>
|
||||
<Text type="secondary">编号:</Text>
|
||||
<Text copyable={{ icon: <CopyOutlined />, text: billNo }}>{billNo}</Text>
|
||||
</Space>
|
||||
<Button type="text" icon={<ReloadOutlined />} onClick={onRefresh}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="ap-detail__header">
|
||||
<div className="ap-detail__header-main">
|
||||
<Title level={4} className="ap-detail__title">
|
||||
{typeLabel}审批
|
||||
</Title>
|
||||
<span className="ap-detail__badge ap-detail__badge--pending">{statusLabel}</span>
|
||||
</div>
|
||||
<div className="ap-detail__meta">
|
||||
<Space size={16} wrap>
|
||||
<Space size={6}>
|
||||
<Avatar size={24} icon={<UserOutlined />} />
|
||||
<Text>{item.initiatedBy}</Text>
|
||||
</Space>
|
||||
<Space size={6}>
|
||||
<Text type="secondary">流程分类</Text>
|
||||
<Text>{typeLabel}</Text>
|
||||
</Space>
|
||||
<Space size={6}>
|
||||
<ClockCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<Text type="secondary">提交时间</Text>
|
||||
<Text>{item.initiatedAt}</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="ap-detail__tabs"
|
||||
items={[
|
||||
{
|
||||
key: 'detail',
|
||||
label: '审批详情',
|
||||
children: (
|
||||
<div className="ap-detail__content">
|
||||
<Text strong className="ap-detail__section-title">
|
||||
账单信息
|
||||
</Text>
|
||||
<table className="ap-insurance-summary ap-billing-summary">
|
||||
<tbody>
|
||||
{Array.from({ length: Math.ceil(billInfoRows.length / 3) }).map((_, rowIndex) => {
|
||||
const cells = billInfoRows.slice(rowIndex * 3, rowIndex * 3 + 3);
|
||||
return (
|
||||
<tr key={rowIndex}>
|
||||
{cells.map((cell) => (
|
||||
<React.Fragment key={cell.label}>
|
||||
<th>{cell.label}</th>
|
||||
<td>{cell.value}</td>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{cells.length < 3
|
||||
? Array.from({ length: 3 - cells.length }).map((__, fillIndex) => (
|
||||
<React.Fragment key={`fill-${fillIndex}`}>
|
||||
<th />
|
||||
<td />
|
||||
</React.Fragment>
|
||||
))
|
||||
: null}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="ap-billing-detail-section">
|
||||
<Text strong className="ap-detail__section-title">
|
||||
账单明细
|
||||
</Text>
|
||||
<div className="ap-billing-totals">
|
||||
<span>
|
||||
应收款总额 <Text className="ap-billing-totals__value">{receivableTotal} 元</Text>
|
||||
</span>
|
||||
<span>
|
||||
实收款总额 <Text className="ap-billing-totals__value">{receivedTotal} 元</Text>
|
||||
</span>
|
||||
<span>
|
||||
开票总额 <Text className="ap-billing-totals__value">{invoicedTotal} 元</Text>
|
||||
</span>
|
||||
<span className="ap-billing-totals__meta">
|
||||
待开票 {pendingInvoice} 元
|
||||
</span>
|
||||
</div>
|
||||
<Table
|
||||
className="ap-insurance-table ap-billing-table"
|
||||
columns={detailColumns}
|
||||
dataSource={billDetailRows}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 1100 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ap-billing-timeline-section">
|
||||
<Text strong className="ap-detail__section-title">
|
||||
审批记录
|
||||
</Text>
|
||||
<Timeline
|
||||
className="ap-detail__timeline"
|
||||
items={[
|
||||
{
|
||||
color: 'gray',
|
||||
children: (
|
||||
<div className="ap-billing-timeline-item">
|
||||
<div className="ap-billing-timeline-head">
|
||||
<Text strong>财务经理审批</Text>
|
||||
<Tag>待审批</Tag>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
color: 'green',
|
||||
dot: <CheckCircleOutlined style={{ fontSize: 14 }} />,
|
||||
children: (
|
||||
<div className="ap-billing-timeline-item">
|
||||
<div className="ap-billing-timeline-head">
|
||||
<Text strong>发起审批</Text>
|
||||
<Tag color="success">通过</Tag>
|
||||
</div>
|
||||
<Text type="secondary">{item.initiatedBy}</Text>
|
||||
<Text type="secondary" className="ap-detail__timeline-time">
|
||||
{item.initiatedAt}
|
||||
</Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'flow',
|
||||
label: '审批流程图',
|
||||
children: (
|
||||
<div className="ap-detail__flow-placeholder">
|
||||
<Empty description="流程图占位,后续接入 BPM 流程引擎" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="ap-detail__actions">
|
||||
<Button>评论</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user