Files
dukang/apps/h5-partner/src/pages/SettlementPage.tsx
T
jacy 550697c346 feat(settlement): 合伙人账单生成后直接同步并支持打款凭证
去掉总部发送与合伙人确认,财务打款后上传凭证,合伙人即可查看状态。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-26 13:04:03 +08:00

176 lines
6.8 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import type { PartnerBillDto, PartnerSettlementPreviewDto } from '@dukang/shared-types';
import { isLoggedIn, request } from '../lib/api';
import { formatBillPeriod, formatIssueLabel } from '../lib/settlement';
type StatusFilter = 'all' | 'unpaid' | 'paid';
function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function billStatusLabel(status: string) {
switch (status) {
case 'UNPAID': return '未打款';
case 'PAID': return '已打款';
default: return status;
}
}
function billStatusClass(status: string) {
switch (status) {
case 'UNPAID': return 'partner-settlement-status--reviewing';
case 'PAID': return 'partner-settlement-status--settled';
default: return '';
}
}
function billPeriodLabel(bill: PartnerBillDto) {
return formatBillPeriod(bill.periodStart, bill.periodEnd);
}
export default function SettlementPage() {
const navigate = useNavigate();
const [bills, setBills] = useState<PartnerBillDto[]>([]);
const [preview, setPreview] = useState<PartnerSettlementPreviewDto | null>(null);
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
const loadBills = useCallback(() => {
return Promise.all([
request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills').then(setBills),
request<PartnerSettlementPreviewDto>('PARTNER_H5', '/partner/settlement/preview')
.then(setPreview)
.catch(() => setPreview(null)),
]).then(() => undefined);
}, []);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
void loadBills();
}, [navigate, loadBills]);
const summary = useMemo(() => {
const unpaid = bills.filter((b) => b.status === 'UNPAID');
const settled = bills.filter((b) => b.status === 'PAID');
return {
pendingTotal: unpaid.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
};
}, [bills]);
const filteredBills = useMemo(() => {
return bills.filter((b) => {
if (statusFilter === 'all') return true;
if (statusFilter === 'unpaid') return b.status === 'UNPAID';
if (statusFilter === 'paid') return b.status === 'PAID';
return true;
});
}, [bills, statusFilter]);
function openBill(bill: PartnerBillDto) {
navigate(bill.id ? `/center/bills?id=${bill.id}` : '/center/bills');
}
const statusTabs: Array<{ key: StatusFilter; label: string }> = [
{ key: 'all', label: '全部' },
{ key: 'unpaid', label: '未打款' },
{ key: 'paid', label: '已打款' },
];
return (
<PullToRefresh onRefresh={loadBills} className="page-no-tab partner-settlement-page">
<PageHeader title="财务对账中心" onBack={() => navigate('/')} />
<main className="partner-settlement-body">
<section className="partner-settlement-summary">
<div className="partner-settlement-summary-deco" aria-hidden>
<span className="material-symbols-outlined">account_balance_wallet</span>
</div>
<p className="partner-settlement-summary-label">待结算总额</p>
<div className="partner-settlement-summary-amount">
<span className="partner-settlement-currency">¥</span>
{fmtMoney(summary.pendingTotal)}
</div>
<div className="partner-settlement-summary-grid">
<div>
<p className="partner-settlement-summary-sub">累计已结算</p>
<p className="partner-settlement-summary-value">¥{fmtMoney(summary.settledTotal)}</p>
</div>
<div className="partner-settlement-summary-divider" />
<div>
<p className="partner-settlement-summary-sub">本周预付款</p>
<p className="partner-settlement-summary-value partner-settlement-summary-value--accent">
¥{fmtMoney(preview?.prepaymentAmount ?? 0)}
</p>
</div>
</div>
</section>
<section className="partner-settlement-filters">
<p className="label-md text-muted" style={{ margin: 0, lineHeight: 1.5 }}>
账期 {formatBillPeriod(preview?.periodStart, preview?.periodEnd)} · {formatIssueLabel(preview)}
</p>
</section>
<div className="partner-settlement-chips">
{statusTabs.map((tab) => (
<button
key={tab.key}
type="button"
className={`partner-settlement-chip${statusFilter === tab.key ? ' partner-settlement-chip--active' : ''}`}
onClick={() => setStatusFilter(tab.key)}
>
{tab.label}
</button>
))}
</div>
<section className="partner-settlement-list-section">
<h3 className="partner-settlement-list-title">对账明细记录</h3>
{filteredBills.length === 0 && (
<div className="empty" style={{ padding: '32px 0' }}>暂无对账记录</div>
)}
<div className="partner-settlement-list">
{filteredBills.map((bill) => (
<button
key={bill.id}
type="button"
className="partner-settlement-item"
onClick={() => openBill(bill)}
>
<div className="partner-settlement-item-main">
<div className="partner-settlement-item-head">
<span className="partner-settlement-item-no">{bill.billNo}</span>
<span className={`partner-settlement-status ${billStatusClass(bill.status)}`}>
{billStatusLabel(bill.status)}
</span>
</div>
<p className="partner-settlement-item-meta">
<span className="material-symbols-outlined">event</span>
{billPeriodLabel(bill)}
<span className="partner-settlement-dot" />
订单分佣 ¥{fmtMoney(Number(bill.orderCommission || 0))}
</p>
<p className={`partner-settlement-item-amount${bill.status === 'UNPAID' ? ' text-primary' : ''}`}>
¥{fmtMoney(Number(bill.totalAmount || 0))}
</p>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</button>
))}
</div>
{filteredBills.length > 0 && (
<p className="partner-settlement-footer-note">已展示全部记录</p>
)}
</section>
</main>
</PullToRefresh>
);
}