fix: simplify balance billing and govern operation logs
This commit is contained in:
+4
-20
@@ -200,6 +200,7 @@ export type TenantOption = {
|
||||
export type TenantManagementRow = TenantOption & {
|
||||
account?: TenantAccount | null;
|
||||
todaySpendCents: number;
|
||||
todayRefundCents: number;
|
||||
};
|
||||
|
||||
export type CaptchaResponse = {
|
||||
@@ -242,7 +243,7 @@ export type DashboardResponse = {
|
||||
today: { sent: number; delivered: number; failed: number; unknown: number; successRate: number; spendCents: number; billingUnits: number };
|
||||
uplinkCount: number;
|
||||
billing: { _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } };
|
||||
transactions: { _count: { _all: number }; _sum: { amountCents?: number | null; smsUnits?: number | null } };
|
||||
transactions: { _count: { _all: number }; _sum: { amountCents?: number | null } };
|
||||
gatewayConnections: Array<{ status: string; _count: { _all: number }; _sum: { currentConnections?: number | null; desiredConnections?: number | null } }>;
|
||||
pendingAuditCount: number;
|
||||
pendingAudits: { enterpriseCertifications: number; smsAudits: number; templates: number; signatures: number; drainageInfos: number; total: number };
|
||||
@@ -254,7 +255,7 @@ export type DashboardResponse = {
|
||||
recentFailed: number;
|
||||
alertCount: number;
|
||||
};
|
||||
accounts: Array<{ id: string; tenantId: string; balanceCents: number; smsUnits: number; creditCents: number; status: string; tenant?: TenantOption }>;
|
||||
accounts: Array<{ id: string; tenantId: string; balanceCents: number; status: string; tenant?: TenantOption }>;
|
||||
recentTasks: Array<Record<string, unknown>>;
|
||||
recentRecharges: Array<RechargeOrder>;
|
||||
};
|
||||
@@ -264,7 +265,6 @@ export type RechargeOrder = {
|
||||
tenantId: string;
|
||||
orderNo: string;
|
||||
amountCents: number;
|
||||
smsUnits: number;
|
||||
status: string;
|
||||
payMethod?: string | null;
|
||||
paidAt?: string | null;
|
||||
@@ -275,16 +275,6 @@ export type RechargeOrder = {
|
||||
tenant?: TenantOption;
|
||||
};
|
||||
|
||||
export type BillingPlan = {
|
||||
id: string;
|
||||
name: string;
|
||||
amountCents: number;
|
||||
smsUnits: number;
|
||||
unitPriceCents?: number | null;
|
||||
status: string;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
export type ClientSmsApplication = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
@@ -671,8 +661,6 @@ export type TenantAccount = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
balanceCents: number;
|
||||
smsUnits: number;
|
||||
creditCents: number;
|
||||
status: string;
|
||||
tenant?: TenantOption;
|
||||
};
|
||||
@@ -966,7 +954,7 @@ export const adminApi = {
|
||||
request<OperationLogResponse>(withQuery('/admin/system-logs', query)),
|
||||
listAccounts: () => request<TenantAccount[]>('/admin/billing/accounts'),
|
||||
listManualRecharges: (tenantId?: string) => request<RechargeOrder[]>(withQuery('/admin/billing/manual-recharges', { tenantId })),
|
||||
createManualRecharge: (body: { tenantId: string; amountCents: number; smsUnits?: number; operatorId?: string; remark?: string }) =>
|
||||
createManualRecharge: (body: { tenantId: string; amountCents: number; operatorId?: string; remark?: string }) =>
|
||||
request<RechargeOrder>('/admin/billing/manual-recharges', { method: 'POST', body: JSON.stringify(body) }),
|
||||
listEnterpriseApplications: (query: { tenantId?: string; keyword?: string; enterpriseKeyword?: string; applicationKeyword?: string; status?: string } = {}) =>
|
||||
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-applications', query)),
|
||||
@@ -1215,10 +1203,6 @@ export const clientApi = {
|
||||
request<OperationLogResponse>(withQuery('/client/operations/system-logs', query), { tenantId }),
|
||||
listOrders: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<RechargeOrder[]>('/client/billing/orders', { tenantId }),
|
||||
listPlans: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<BillingPlan[]>('/client/billing/plans', { tenantId }),
|
||||
createOrder: (body: { planId?: string; amountCents?: number; smsUnits?: number; payMethod?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<RechargeOrder>('/client/billing/orders', { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||||
listApplications: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
request<ClientSmsApplication[]>('/client/applications', { tenantId }),
|
||||
getApplicationCmppParams: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function AdminCustomerDetailPage() {
|
||||
|
||||
<div className="dashboard-grid enterprise-summary-grid">
|
||||
<div className="surface mini-status-card"><Server size={22} /><div><span>企业编码</span><strong>{tenant?.code ?? '-'}</strong><small>{tenant?.status ?? '-'}</small></div></div>
|
||||
<div className="surface mini-status-card"><MessageSquare size={22} /><div><span>短信余量</span><strong>{(account?.smsUnits ?? 0).toLocaleString('zh-CN')}</strong><small>真实账户余量</small></div></div>
|
||||
<div className="surface mini-status-card"><MessageSquare size={22} /><div><span>计费方式</span><strong>按量计费</strong><small>仅从现金余额扣费</small></div></div>
|
||||
<div className="surface mini-status-card"><FileText size={22} /><div><span>现金余额</span><strong>¥{formatCents(account?.balanceCents)}</strong><small>真实账户余额</small></div></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,10 +36,9 @@ function emptyRechargeForm(): RechargeForm {
|
||||
export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCustomersPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const [records, setRecords] = useState<CustomerRow[]>([]);
|
||||
const [queryId, setQueryId] = useState('');
|
||||
const [queryName, setQueryName] = useState('');
|
||||
const [queryStatus, setQueryStatus] = useState('all');
|
||||
const [filters, setFilters] = useState({ id: '', name: '', status: 'all' });
|
||||
const [filters, setFilters] = useState({ name: '', status: 'all' });
|
||||
const [confirmAction, setConfirmAction] = useState<{ type: 'toggle' | 'delete'; record: CustomerRow } | null>(null);
|
||||
const [rechargeTarget, setRechargeTarget] = useState<CustomerRow | null>(null);
|
||||
const [rechargeForm, setRechargeForm] = useState<RechargeForm>(emptyRechargeForm);
|
||||
@@ -61,10 +60,9 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
}, []);
|
||||
|
||||
const filteredRecords = useMemo(() => records.filter((record) => {
|
||||
const matchId = filters.id ? record.id.includes(filters.id) : true;
|
||||
const matchName = filters.name ? record.name.includes(filters.name) : true;
|
||||
const matchStatus = filters.status === 'all' ? true : record.status === filters.status;
|
||||
return matchId && matchName && matchStatus;
|
||||
return matchName && matchStatus;
|
||||
}), [filters, records]);
|
||||
|
||||
const activeCount = records.filter((record) => record.status === 'active').length;
|
||||
@@ -72,7 +70,6 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
const totalBalance = records.reduce((sum, record) => sum + (record.account?.balanceCents ?? 0), 0);
|
||||
|
||||
const columns: Array<TableColumn<CustomerRow>> = [
|
||||
{ key: 'id', title: '企业ID', width: '160px', render: (record) => <span className="table-mono-id">{record.id}</span> },
|
||||
{ key: 'name', title: '企业名称', width: '260px', render: (record) => <strong className="table-strong-text">{record.name}</strong> },
|
||||
{
|
||||
key: 'balance',
|
||||
@@ -89,8 +86,8 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: 'overdraftLimit', title: '透支限额', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.account?.creditCents ?? 0)}` },
|
||||
{ key: 'todaySpend', title: '今日消费', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todaySpendCents)}` },
|
||||
{ key: 'todayRefund', title: '今日返还', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todayRefundCents)}` },
|
||||
{ key: 'status', title: '企业状态', width: '130px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'warning'}>{record.status === 'active' ? '正常' : '已禁用'}</Tag> },
|
||||
{
|
||||
key: 'actions',
|
||||
@@ -133,7 +130,6 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
await adminApi.createManualRecharge({
|
||||
tenantId: rechargeTarget.id,
|
||||
amountCents: Math.round(amount * 100),
|
||||
smsUnits: 0,
|
||||
remark: [rechargeForm.operator, rechargeForm.remark].filter(Boolean).join(' / '),
|
||||
});
|
||||
setRechargeTarget(null);
|
||||
@@ -175,12 +171,11 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
<div className="surface ui-query-panel">
|
||||
<h2>查询条件</h2>
|
||||
<div className="ui-query-panel__grid enterprise-query-grid">
|
||||
<Input label="企业ID" onChange={(event) => setQueryId(event.target.value)} placeholder="请输入企业ID" value={queryId} />
|
||||
<Input label="企业名称" onChange={(event) => setQueryName(event.target.value)} placeholder="请输入企业名称" value={queryName} />
|
||||
<Select label="企业状态" onChange={(event) => setQueryStatus(event.target.value)} options={[{ label: '全部状态', value: 'all' }, { label: '正常', value: 'active' }, { label: '禁用', value: 'disabled' }]} value={queryStatus} />
|
||||
<div className="enterprise-query-actions">
|
||||
<Button onClick={() => setFilters({ id: queryId, name: queryName, status: queryStatus })} variant="secondary">查询</Button>
|
||||
<Button onClick={() => { setQueryId(''); setQueryName(''); setQueryStatus('all'); setFilters({ id: '', name: '', status: 'all' }); }} variant="ghost">重置</Button>
|
||||
<Button onClick={() => setFilters({ name: queryName, status: queryStatus })} variant="secondary">查询</Button>
|
||||
<Button onClick={() => { setQueryName(''); setQueryStatus('all'); setFilters({ name: '', status: 'all' }); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function AdminHome() {
|
||||
const todaySpend = Math.abs(dashboard?.recentRecharges
|
||||
.filter((item) => item.tenantId === account.tenantId)
|
||||
.reduce((sum, item) => sum + item.amountCents, 0) ?? 0) / 100;
|
||||
const availableBalance = (account.balanceCents + account.creditCents) / 100;
|
||||
const availableBalance = account.balanceCents / 100;
|
||||
return {
|
||||
id: account.tenantId,
|
||||
enterprise: account.tenant?.name ?? account.tenantId,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { formatAmount } from '@/utils/currency';
|
||||
type ManualRechargeForm = {
|
||||
tenantId: string;
|
||||
amount: string;
|
||||
smsUnits: string;
|
||||
operator: string;
|
||||
remark: string;
|
||||
};
|
||||
@@ -31,7 +30,7 @@ export function AdminRechargeRecordsPage() {
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>({});
|
||||
const [manualOpen, setManualOpen] = useState(false);
|
||||
const [form, setForm] = useState<ManualRechargeForm>({ tenantId: '', amount: '', smsUnits: '0', operator: '运营', remark: '' });
|
||||
const [form, setForm] = useState<ManualRechargeForm>({ tenantId: '', amount: '', operator: '运营', remark: '' });
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
@@ -93,9 +92,8 @@ export function AdminRechargeRecordsPage() {
|
||||
|
||||
async function submitManualRecharge() {
|
||||
const amount = Number(form.amount);
|
||||
const smsUnits = Number(form.smsUnits || 0);
|
||||
if (!form.tenantId || !Number.isFinite(amount) || !Number.isFinite(smsUnits) || (amount === 0 && smsUnits === 0)) {
|
||||
setManualError('请填写非 0 的充值金额或短信条数;金额支持负数冲正。');
|
||||
if (!form.tenantId || !Number.isFinite(amount) || amount === 0) {
|
||||
setManualError('请填写非 0 的充值金额;金额支持负数冲正。');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
@@ -104,12 +102,11 @@ export function AdminRechargeRecordsPage() {
|
||||
await adminApi.createManualRecharge({
|
||||
tenantId: form.tenantId,
|
||||
amountCents: Math.round(amount * 100),
|
||||
smsUnits,
|
||||
remark: [form.operator, form.remark].filter(Boolean).join(' / '),
|
||||
});
|
||||
await loadData();
|
||||
setManualOpen(false);
|
||||
setForm({ tenantId: tenants[0]?.id ?? '', amount: '', smsUnits: '0', operator: '运营', remark: '' });
|
||||
setForm({ tenantId: tenants[0]?.id ?? '', amount: '', operator: '运营', remark: '' });
|
||||
} catch (failure) {
|
||||
setManualError(failure instanceof Error ? failure.message : '人工充值失败');
|
||||
} finally {
|
||||
@@ -151,11 +148,11 @@ export function AdminRechargeRecordsPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{error ? (
|
||||
<tr><td className="ui-table__empty" colSpan={7}>{error}</td></tr>
|
||||
<tr><td className="ui-table__empty" colSpan={6}>{error}</td></tr>
|
||||
) : loading ? (
|
||||
<tr><td className="ui-table__empty" colSpan={7}>正在加载真实充值记录...</td></tr>
|
||||
<tr><td className="ui-table__empty" colSpan={6}>正在加载真实充值记录...</td></tr>
|
||||
) : filteredRows.length === 0 ? (
|
||||
<tr><td className="ui-table__empty" colSpan={7}>暂无真实充值记录</td></tr>
|
||||
<tr><td className="ui-table__empty" colSpan={6}>暂无真实充值记录</td></tr>
|
||||
) : visibleRows.map((record) => {
|
||||
const tenantName = record.tenant?.name ?? tenants.find((tenant) => tenant.id === record.tenantId)?.name ?? record.tenantId;
|
||||
return (
|
||||
@@ -207,7 +204,6 @@ export function AdminRechargeRecordsPage() {
|
||||
value={form.tenantId}
|
||||
/>
|
||||
<Input label="充值金额" onChange={(event) => updateForm('amount', event.target.value)} prefix="¥" required type="number" value={form.amount} />
|
||||
<Input label="短信条数" onChange={(event) => updateForm('smsUnits', event.target.value)} type="number" value={form.smsUnits} />
|
||||
<Input label="操作人" onChange={(event) => updateForm('operator', event.target.value)} required value={form.operator} />
|
||||
<Textarea className="admin-system-modal-form__wide" label="充值备注" onChange={(event) => updateForm('remark', event.target.value)} rows={4} value={form.remark} />
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function ClientHome() {
|
||||
}, []);
|
||||
|
||||
const account = dashboard?.accounts[0];
|
||||
const availableBalance = ((account?.balanceCents ?? 0) + (account?.creditCents ?? 0)) / 100;
|
||||
const availableBalance = (account?.balanceCents ?? 0) / 100;
|
||||
const todaySpend = (dashboard?.today.spendCents ?? 0) / 100;
|
||||
const todayRefund = Math.abs((dashboard?.transactions._sum.amountCents ?? 0) < 0 ? 0 : dashboard?.transactions._sum.amountCents ?? 0) / 100;
|
||||
const balanceBaseline = Math.max(availableBalance + todaySpend - todayRefund, availableBalance, 1);
|
||||
@@ -145,7 +145,7 @@ export function ClientHome() {
|
||||
</button>
|
||||
<button className="quick-action" onClick={() => navigate('/client/billing')} type="button">
|
||||
<WalletCards size={20} />
|
||||
<span>账户充值</span>
|
||||
<span>账户余额</span>
|
||||
<small>查看余额与流水</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ const pageTitleMap: Record<string, string> = {
|
||||
'/client/send': '短信发送',
|
||||
'/client/templates': '模板管理',
|
||||
'/client/signatures': '签名与引流信息',
|
||||
'/client/billing': '充值套餐',
|
||||
'/client/billing': '账户余额',
|
||||
'/client/settings': '账号设置',
|
||||
'/admin/monitor': '发送监控',
|
||||
'/admin/analytics': '数据统计',
|
||||
|
||||
@@ -64,7 +64,7 @@ export function ClientLayout() {
|
||||
{
|
||||
title: '账户',
|
||||
items: [
|
||||
{ label: '充值套餐', to: '/client/billing', icon: BadgeDollarSign },
|
||||
{ label: '账户余额', to: '/client/billing', icon: BadgeDollarSign },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BatchTask, BillingPlan, ClientOverview, Invoice, RecentMessage, Signature, SmsTemplate } from '@/mock/types';
|
||||
import type { BatchTask, ClientOverview, Invoice, RecentMessage, Signature, SmsTemplate } from '@/mock/types';
|
||||
|
||||
export const clientOverview: ClientOverview = {
|
||||
availableBalance: 28642.5,
|
||||
@@ -70,12 +70,6 @@ export const signatures: Signature[] = [
|
||||
{ id: 'SIG-103', name: '北辰出行', company: '深圳北辰出行服务有限公司', status: 'rejected' },
|
||||
];
|
||||
|
||||
export const billingPlans: BillingPlan[] = [
|
||||
{ id: 'PLAN-01', name: '基础包', messages: 10000, price: 680 },
|
||||
{ id: 'PLAN-02', name: '增长包', messages: 50000, price: 2980, highlight: true },
|
||||
{ id: 'PLAN-03', name: '企业包', messages: 200000, price: 10800 },
|
||||
];
|
||||
|
||||
export const invoices: Invoice[] = [
|
||||
{ id: 'INV-20260601', title: '增长包充值', amount: 2980, messages: 50000, status: 'paid', createdAt: '2026-06-01 10:12' },
|
||||
{ id: 'INV-20260518', title: '基础包充值', amount: 680, messages: 10000, status: 'paid', createdAt: '2026-05-18 14:26' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { batchTasks, billingPlans, clientOverview, invoices, recentMessages, signatures, smsTemplates } from '@/mock/clientData';
|
||||
import { batchTasks, clientOverview, invoices, recentMessages, signatures, smsTemplates } from '@/mock/clientData';
|
||||
import { readLocalData, writeLocalData } from '@/mock/storage';
|
||||
import type { BatchTask, RecentMessage, Signature, SmsTemplate } from '@/mock/types';
|
||||
|
||||
@@ -45,10 +45,6 @@ export const clientService = {
|
||||
writeLocalData(keys.signatures, nextSignatures);
|
||||
},
|
||||
|
||||
getBillingPlans() {
|
||||
return billingPlans;
|
||||
},
|
||||
|
||||
getInvoices() {
|
||||
return invoices;
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ export type {
|
||||
BatchSendType,
|
||||
BatchTask,
|
||||
BatchTaskStatus,
|
||||
BillingPlan,
|
||||
Channel,
|
||||
ClientOverview,
|
||||
Customer,
|
||||
|
||||
@@ -75,14 +75,6 @@ export type Channel = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type BillingPlan = {
|
||||
id: string;
|
||||
name: string;
|
||||
messages: number;
|
||||
price: number;
|
||||
highlight?: boolean;
|
||||
};
|
||||
|
||||
export type Invoice = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user