89fd333702
Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload. Co-authored-by: Cursor <cursoragent@cursor.com>
287 lines
12 KiB
TypeScript
287 lines
12 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
|
import type { PartnerBillDto } from '@dukang/shared-types';
|
|
import { isLoggedIn, request } from '../lib/api';
|
|
import { toastError, toastSuccess } from '../lib/toast';
|
|
import { usePartnerPageView } from '../lib/usePageView';
|
|
|
|
function fmtMoney(n: number) {
|
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
function billStatusLabel(status: string) {
|
|
switch (status) {
|
|
case 'AWAITING_CONFIRM': return '待确认';
|
|
case 'UNPAID': return '未打款';
|
|
case 'PAID': return '已打款';
|
|
case 'REJECTED': return '已驳回';
|
|
default: return status;
|
|
}
|
|
}
|
|
|
|
function canConfirm(status: string) {
|
|
return status === 'AWAITING_CONFIRM';
|
|
}
|
|
|
|
export default function BillsPage() {
|
|
usePartnerPageView('partner_bills_view');
|
|
const navigate = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const [bills, setBills] = useState<PartnerBillDto[]>([]);
|
|
const [confirmed, setConfirmed] = useState(false);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
|
|
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; }
|
|
void loadBills();
|
|
}, [navigate]);
|
|
|
|
const billId = searchParams.get('id');
|
|
const actionable = useMemo(
|
|
() => bills.filter((b) => canConfirm(b.status)),
|
|
[bills],
|
|
);
|
|
const bill = billId
|
|
? bills.find((b) => String(b.id) === billId) ?? actionable[0] ?? bills[0]
|
|
: actionable[0] ?? bills[0];
|
|
|
|
const status = String(bill?.status || '');
|
|
const showApplyForm = !!bill && canConfirm(status);
|
|
const isUnpaid = status === 'UNPAID';
|
|
const isRejected = status === 'REJECTED';
|
|
const isPaid = status === 'PAID';
|
|
|
|
function askConfirm(ids: string[]) {
|
|
if (!window.confirm(`确认 ${ids.length} 笔账单无误并提交?确认后状态将变为「未打款」,等待总部打款。`)) {
|
|
return;
|
|
}
|
|
void doConfirm(ids);
|
|
}
|
|
|
|
async function doConfirm(ids: string[]) {
|
|
setSubmitting(true);
|
|
try {
|
|
if (ids.length === 1) {
|
|
await request('PARTNER_H5', `/partner/settlement/bills/${ids[0]}/confirm`, { method: 'POST' });
|
|
} else {
|
|
await request('PARTNER_H5', '/partner/settlement/bills/batch-confirm', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ids }),
|
|
});
|
|
}
|
|
toastSuccess('已确认,等待总部打款');
|
|
setConfirmed(false);
|
|
setSelectedIds([]);
|
|
await loadBills();
|
|
} catch (e) {
|
|
toastError(e instanceof Error ? e.message : '提交失败');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
function toggleSelect(id: string) {
|
|
setSelectedIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
|
}
|
|
|
|
return (
|
|
<PullToRefresh onRefresh={loadBills} className="partner-bills-page">
|
|
<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: isPaid ? '100%' : isUnpaid || isRejected ? '75%' : '50%' }}
|
|
/>
|
|
</div>
|
|
<div className="partner-step">
|
|
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--done">
|
|
<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${showApplyForm || isUnpaid || 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${isPaid ? ' partner-step-circle--done' : isUnpaid || 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${isUnpaid || isRejected || isPaid ? ' partner-step-label--active' : ''}`}>
|
|
{isRejected ? '已驳回' : isPaid ? '已打款' : '总部打款'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{loading && <div className="empty">加载中…</div>}
|
|
{!loading && !bill && <div className="empty">暂无待确认账单</div>}
|
|
|
|
{bill && (
|
|
<section className="partner-bill-card">
|
|
<div className="partner-bill-card-bar" />
|
|
<div style={{ padding: 16 }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
|
|
<div>
|
|
<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${isRejected ? ' partner-status-pill--closed' : isUnpaid ? ' 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>
|
|
)}
|
|
|
|
{isUnpaid && (
|
|
<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">{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 }}>¥ {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 }}>¥ {fmtMoney(Number(bill.redeemCommission || 0))}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{bills.length > 0 && (
|
|
<div style={{ padding: '0 20px' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
|
<h3 className="headline-md" style={{ margin: 0 }}>全部账单</h3>
|
|
{actionable.length > 1 && (
|
|
<button
|
|
type="button"
|
|
className="partner-fill-max"
|
|
style={{ fontSize: 13 }}
|
|
disabled={!selectedIds.length || submitting}
|
|
onClick={() => askConfirm(selectedIds)}
|
|
>
|
|
批量确认 ({selectedIds.length})
|
|
</button>
|
|
)}
|
|
</div>
|
|
{bills.map((b) => {
|
|
const id = String(b.id);
|
|
const selectable = canConfirm(b.status);
|
|
return (
|
|
<div
|
|
key={id}
|
|
className="partner-store-card"
|
|
style={{ margin: '0 0 12px', display: 'flex', gap: 10, alignItems: 'flex-start' }}
|
|
>
|
|
{selectable ? (
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedIds.includes(id)}
|
|
onChange={() => toggleSelect(id)}
|
|
style={{ marginTop: 4 }}
|
|
/>
|
|
) : (
|
|
<span style={{ width: 16 }} />
|
|
)}
|
|
<button
|
|
type="button"
|
|
style={{ flex: 1, textAlign: 'left', background: 'none', border: 'none', padding: 0, cursor: 'pointer' }}
|
|
onClick={() => navigate(`/center/bills?id=${id}`)}
|
|
>
|
|
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
|
|
<div>
|
|
<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>
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</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 }}>
|
|
确认后账单变为「未打款」,由总部完成打款。如有异议请先联系城市运营经理。
|
|
</p>
|
|
</div>
|
|
|
|
<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={() => askConfirm([String(bill!.id)])}
|
|
>
|
|
{submitting ? '正在提交...' : '确认账单'}
|
|
{!submitting && <span className="material-symbols-outlined">payments</span>}
|
|
</button>
|
|
</footer>
|
|
</>
|
|
)}
|
|
</PullToRefresh>
|
|
);
|
|
}
|