城市合伙人H5端结算审核

This commit is contained in:
2026-07-17 07:56:43 +08:00
parent 7fa2c6fbed
commit b36f2ea63a
14 changed files with 418 additions and 82 deletions
+148 -49
View File
@@ -1,42 +1,96 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import type { PartnerBillDto } from '@dukang/shared-types';
import { isLoggedIn, request } from '../lib/api';
import { toastError, toastSuccess } from '../lib/toast';
function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function billStatusLabel(status: string) {
switch (status) {
case 'DRAFT': return '待确认';
case 'CONFIRMED': return '审核中';
case 'PAID': return '已结算';
case 'REJECTED': return '已驳回';
default: return status;
}
}
function canApplyPayment(status: string) {
return status === 'DRAFT' || status === 'REJECTED';
}
export default function BillsPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [bills, setBills] = useState<Array<Record<string, unknown>>>([]);
const [bills, setBills] = useState<PartnerBillDto[]>([]);
const [confirmed, setConfirmed] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(true);
async function loadBills() {
setLoading(true);
try {
const list = await request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills');
setBills(Array.isArray(list) ? list : []);
} catch {
setBills([]);
} finally {
setLoading(false);
}
}
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
void loadBills();
}, [navigate]);
const billId = searchParams.get('id');
const actionable = useMemo(
() => bills.filter((b) => canApplyPayment(b.status)),
[bills],
);
const bill = billId
? bills.find((b) => String(b.id) === billId) ?? bills[0]
: bills[0];
? bills.find((b) => String(b.id) === billId) ?? actionable[0] ?? bills[0]
: actionable[0] ?? bills[0];
function confirmBill() {
if (!confirmed) return;
const status = String(bill?.status || '');
const showApplyForm = !!bill && canApplyPayment(status);
const isReviewing = status === 'CONFIRMED';
const isRejected = status === 'REJECTED';
const isPaid = status === 'PAID';
async function confirmBill() {
if (!bill || !confirmed || !canApplyPayment(String(bill.status))) return;
setSubmitting(true);
setTimeout(() => {
try {
await request('PARTNER_H5', `/partner/settlement/bills/${bill.id}/confirm`, {
method: 'POST',
});
toastSuccess('申请已提交,请耐心等待总部打款审核');
setConfirmed(false);
await loadBills();
} catch (e) {
toastError(e instanceof Error ? e.message : '提交失败');
} finally {
setSubmitting(false);
alert('申请已提交,请耐心等待打款审核。(preV1 演示)');
}, 1500);
}
}
return (
<div className="partner-bills-page">
<PageHeader title="待确认账单" onBack={() => navigate('/center/settlement')} />
<PageHeader title="账单确认" onBack={() => navigate('/center/settlement')} />
<div className="partner-bill-stepper">
<div className="partner-stepper-inner">
<div className="partner-stepper-line" aria-hidden>
<div className="partner-stepper-line-fill" style={{ width: '50%' }} />
<div
className="partner-stepper-line-fill"
style={{ width: isPaid ? '100%' : isReviewing || isRejected ? '75%' : '50%' }}
/>
</div>
<div className="partner-step">
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--done">
@@ -45,17 +99,28 @@ export default function BillsPage() {
<span className="partner-step-label partner-step-label--active"></span>
</div>
<div className="partner-step">
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--active">2</div>
<div className={`partner-step-circle partner-step-circle--sm${showApplyForm || isReviewing || isRejected || isPaid ? ' partner-step-circle--done' : ' partner-step-circle--active'}`}>
{showApplyForm && !confirmed ? '2' : (
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
)}
</div>
<span className="partner-step-label partner-step-label--active"></span>
</div>
<div className="partner-step">
<div className="partner-step-circle partner-step-circle--sm">3</div>
<span className="partner-step-label"></span>
<div className={`partner-step-circle partner-step-circle--sm${isPaid ? ' partner-step-circle--done' : isReviewing || isRejected ? ' partner-step-circle--active' : ''}`}>
{isPaid ? (
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
) : '3'}
</div>
<span className={`partner-step-label${isReviewing || isRejected || isPaid ? ' partner-step-label--active' : ''}`}>
{isRejected ? '已驳回' : isPaid ? '已打款' : '申请打款'}
</span>
</div>
</div>
</div>
{!bill && <div className="empty"></div>}
{loading && <div className="empty"></div>}
{!loading && !bill && <div className="empty"></div>}
{bill && (
<section className="partner-bill-card">
@@ -66,25 +131,46 @@ export default function BillsPage() {
<p className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>SETTLEMENT PERIOD</p>
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '月度结算账单')}</h2>
</div>
<span className="partner-status-pill partner-status-pill--paused">{String(bill.status || '待确认')}</span>
<span className={`partner-status-pill${isRejected ? ' partner-status-pill--closed' : isReviewing ? ' partner-status-pill--paused' : isPaid ? ' partner-status-pill--open' : ' partner-status-pill--paused'}`}>
{billStatusLabel(status)}
</span>
</div>
{isRejected && bill.rejectReason && (
<div className="partner-info-banner" style={{ marginBottom: 16, background: 'rgba(166,29,36,0.06)' }}>
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>error</span>
<div>
<p className="body-md text-primary" style={{ fontWeight: 600, marginBottom: 4 }}></p>
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>{bill.rejectReason}</p>
</div>
</div>
)}
{isReviewing && (
<div className="partner-info-banner" style={{ marginBottom: 16 }}>
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>hourglass_top</span>
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
</p>
</div>
)}
<div className="partner-bill-amount">
<p className="text-muted body-md" style={{ marginBottom: 8 }}></p>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 4 }}>
<span className="text-primary" style={{ fontWeight: 700, fontSize: 20 }}>¥</span>
<span className="amount-xl">{Number(bill.totalAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
<span className="amount-xl">{fmtMoney(Number(bill.totalAmount || 0))}</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="partner-info-row">
<span className="text-variant body-md"></span>
<span className="body-md" style={{ fontWeight: 700 }}>¥ {(Number(bill.totalAmount || 0) * 0.82).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
<span className="body-md" style={{ fontWeight: 700 }}>¥ {fmtMoney(Number(bill.orderCommission || 0))}</span>
</div>
<div className="partner-info-row">
<span className="text-variant body-md"></span>
<span className="body-md" style={{ fontWeight: 700 }}>¥ {(Number(bill.totalAmount || 0) * 0.18).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</span>
<span className="body-md" style={{ fontWeight: 700 }}>¥ {fmtMoney(Number(bill.redeemCommission || 0))}</span>
</div>
</div>
</div>
@@ -93,45 +179,58 @@ export default function BillsPage() {
{bills.length > 1 && (
<div style={{ padding: '0 20px' }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}></h3>
{bills.slice(1).map((b) => (
<div key={String(b.id)} className="partner-store-card" style={{ margin: '0 0 12px' }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}></h3>
{bills.map((b) => (
<button
key={String(b.id)}
type="button"
className="partner-store-card"
style={{ margin: '0 0 12px', width: '100%', textAlign: 'left', cursor: 'pointer' }}
onClick={() => navigate(`/center/bills?id=${b.id}`)}
>
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
<div>
<p className="body-md">{String(b.billNo)}</p>
<p className="label-md text-muted">{String(b.status)}</p>
<p className="body-md">{b.billNo}</p>
<p className="label-md text-muted">{billStatusLabel(b.status)}</p>
{b.status === 'REJECTED' && b.rejectReason ? (
<p className="label-md text-primary" style={{ marginTop: 4 }}>{b.rejectReason}</p>
) : null}
</div>
<span className="amount-lg" style={{ fontSize: 18 }}>¥{Number(b.totalAmount).toFixed(2)}</span>
</div>
</div>
</button>
))}
</div>
)}
<div className="partner-info-banner" style={{ marginTop: 16 }}>
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>info</span>
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
</p>
</div>
{showApplyForm && (
<>
<div className="partner-info-banner" style={{ marginTop: 16 }}>
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>info</span>
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
{isRejected
? '请根据驳回理由核对后重新确认并申请打款。'
: '账单确认后将正式进入打款审核。如有异议,请在确认前联系城市运营经理核实数据。'}
</p>
</div>
{bill && (
<footer className="partner-bill-footer">
<label className="partner-checkbox-row" style={{ marginBottom: 16 }}>
<input type="checkbox" checked={confirmed} onChange={(e) => setConfirmed(e.target.checked)} />
<span></span>
</label>
<button
type="button"
className="partner-btn-primary"
disabled={!confirmed || submitting}
style={{ opacity: confirmed ? 1 : 0.5 }}
onClick={confirmBill}
>
{submitting ? '正在提交...' : '确认并申请打款'}
{!submitting && <span className="material-symbols-outlined">payments</span>}
</button>
</footer>
<footer className="partner-bill-footer">
<label className="partner-checkbox-row" style={{ marginBottom: 16 }}>
<input type="checkbox" checked={confirmed} onChange={(e) => setConfirmed(e.target.checked)} />
<span></span>
</label>
<button
type="button"
className="partner-btn-primary"
disabled={!confirmed || submitting}
style={{ opacity: confirmed ? 1 : 0.5 }}
onClick={() => void confirmBill()}
>
{submitting ? '正在提交...' : isRejected ? '重新申请打款' : '确认并申请打款'}
{!submitting && <span className="material-symbols-outlined">payments</span>}
</button>
</footer>
</>
)}
</div>
);