fix: simplify balance billing and govern operation logs
This commit is contained in:
@@ -1,76 +1,67 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CreditCard } from 'lucide-react';
|
||||
import { Button, Pagination, Tag } from '@/components/ui';
|
||||
import { clientApi, type BillingPlan } from '@/api/adminApi';
|
||||
import { WalletCards } from 'lucide-react';
|
||||
import { Pagination, Tag } from '@/components/ui';
|
||||
import { clientApi, type RechargeOrder } from '@/api/adminApi';
|
||||
import { formatCents } from '@/utils/currency';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
export function ClientBillingPage() {
|
||||
const [plans, setPlans] = useState<BillingPlan[]>([]);
|
||||
const [balanceCents, setBalanceCents] = useState(0);
|
||||
const [orders, setOrders] = useState<RechargeOrder[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const pageSize = 10;
|
||||
const totalPages = Math.max(1, Math.ceil(plans.length / pageSize));
|
||||
const totalPages = Math.max(1, Math.ceil(orders.length / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const visiblePlans = plans.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
const visibleOrders = orders.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
clientApi.listPlans()
|
||||
.then((items) => {
|
||||
setPlans(items.filter((item) => item.status !== 'disabled' && item.status !== 'deleted'));
|
||||
Promise.all([clientApi.getDashboard(), clientApi.listOrders()])
|
||||
.then(([dashboard, nextOrders]) => {
|
||||
setBalanceCents(dashboard.accounts[0]?.balanceCents ?? 0);
|
||||
setOrders(nextOrders);
|
||||
setError('');
|
||||
})
|
||||
.catch((reason: Error) => setError(reason.message || '充值套餐加载失败'))
|
||||
.catch((reason: Error) => setError(reason.message || '账户信息加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [plans.length]);
|
||||
|
||||
function createOrder(plan: BillingPlan) {
|
||||
clientApi.createOrder({ planId: plan.id, amountCents: plan.amountCents, smsUnits: plan.smsUnits, payMethod: 'manual' })
|
||||
.catch((reason: Error) => setError(reason.message || '充值订单创建失败'));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<p className="eyebrow">账户</p>
|
||||
<h1>充值套餐</h1>
|
||||
<div><p className="eyebrow">账户</p><h1>账户余额</h1></div>
|
||||
</div>
|
||||
<div className="dashboard-grid enterprise-summary-grid">
|
||||
<div className="surface mini-status-card">
|
||||
<WalletCards size={22} />
|
||||
<div><span>当前可用余额</span><strong>¥{formatCents(balanceCents)}</strong><small>短信发送仅按现金余额校验。</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface">
|
||||
{loading ? <p className="muted">正在加载充值套餐...</p> : null}
|
||||
<div className="surface section-stack">
|
||||
<div className="section-heading"><div><h2>充值记录</h2><p className="muted">如需充值,请联系平台运营人员。</p></div><Tag tone="info">{orders.length} 条</Tag></div>
|
||||
{loading ? <p className="muted">正在加载账户信息...</p> : null}
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
<div className="plan-grid">
|
||||
{visiblePlans.map((plan) => (
|
||||
<article className={['plan-card', plan.smsUnits >= 100000 ? 'plan-card--highlight' : ''].filter(Boolean).join(' ')} key={plan.id}>
|
||||
<div className="section-heading">
|
||||
<h2>{plan.name}</h2>
|
||||
{plan.smsUnits >= 100000 ? <Tag tone="accent">推荐</Tag> : null}
|
||||
</div>
|
||||
<strong>{plan.smsUnits.toLocaleString('zh-CN')} 条</strong>
|
||||
<p className="muted">{plan.description ?? '适合阶段性短信发送和活动通知。'}</p>
|
||||
<Button icon={<CreditCard size={16} />} onClick={() => createOrder(plan)} variant={plan.smsUnits >= 100000 ? 'primary' : 'ghost'}>
|
||||
¥{formatCents(plan.amountCents)} 立即充值
|
||||
</Button>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<Pagination
|
||||
nextDisabled={currentPage >= totalPages}
|
||||
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
|
||||
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
|
||||
page={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setPage}
|
||||
previousDisabled={currentPage <= 1}
|
||||
total={plans.length}
|
||||
/>
|
||||
{!loading && !error && plans.length === 0 ? <p className="muted">暂无可用充值套餐。</p> : null}
|
||||
{!loading && !error ? (
|
||||
<div className="ui-table-wrap">
|
||||
<table className="ui-table">
|
||||
<thead><tr><th>订单号</th><th>充值时间</th><th>充值金额</th><th>状态</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
{visibleOrders.length === 0 ? <tr><td className="ui-table__empty" colSpan={5}>暂无充值记录</td></tr> : visibleOrders.map((order) => (
|
||||
<tr key={order.id}>
|
||||
<td><span className="table-mono-id">{order.orderNo}</span></td>
|
||||
<td>{formatDateTime(order.paidAt ?? order.createdAt)}</td>
|
||||
<td>¥{formatCents(order.amountCents)}</td>
|
||||
<td><Tag tone={order.status === 'paid' ? 'success' : 'warning'}>{order.status === 'paid' ? '已入账' : order.status}</Tag></td>
|
||||
<td>{order.remark || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : null}
|
||||
<Pagination nextDisabled={currentPage >= totalPages} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} onPrevious={() => setPage((value) => Math.max(1, value - 1))} page={currentPage} totalPages={totalPages} onPageChange={setPage} previousDisabled={currentPage <= 1} total={orders.length} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user