218 lines
9.0 KiB
TypeScript
218 lines
9.0 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { ChevronLeft, ChevronRight, Plus, Search } from 'lucide-react';
|
|
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Textarea, Tag, type DateRangeValue } from '@/components/ui';
|
|
import { adminApi, type AccountTransaction, type RechargeOrder, type TenantAccount, type TenantOption } from '@/api/adminApi';
|
|
|
|
type ManualRechargeForm = {
|
|
tenantId: string;
|
|
amount: string;
|
|
smsUnits: string;
|
|
operator: string;
|
|
remark: string;
|
|
};
|
|
|
|
function getDate(value: string) {
|
|
return value.slice(0, 10);
|
|
}
|
|
|
|
function formatAmount(value?: number) {
|
|
if (value === undefined) {
|
|
return '';
|
|
}
|
|
|
|
return value.toLocaleString('zh-CN', {
|
|
maximumFractionDigits: 2,
|
|
minimumFractionDigits: Number.isInteger(value) ? 0 : 2,
|
|
});
|
|
}
|
|
|
|
function RemarkCell({ value }: { value?: string }) {
|
|
return (
|
|
<div className={['admin-remark-cell', value ? '' : 'admin-remark-cell--empty'].filter(Boolean).join(' ')}>
|
|
{value || '暂无备注'}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AdminRechargeRecordsPage() {
|
|
const [records, setRecords] = useState<RechargeOrder[]>([]);
|
|
const [accounts, setAccounts] = useState<TenantAccount[]>([]);
|
|
const [transactions, setTransactions] = useState<AccountTransaction[]>([]);
|
|
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
|
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 [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
|
|
async function loadData() {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const [nextTenants, nextRecords, nextAccounts, nextTransactions] = await Promise.all([
|
|
adminApi.listTenants(),
|
|
adminApi.listManualRecharges(),
|
|
adminApi.listAccounts(),
|
|
adminApi.listTransactions(),
|
|
]);
|
|
setTenants(nextTenants);
|
|
setRecords(nextRecords);
|
|
setAccounts(nextAccounts);
|
|
setTransactions(nextTransactions);
|
|
setForm((current) => ({ ...current, tenantId: current.tenantId || nextTenants[0]?.id || '' }));
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : '充值记录加载失败');
|
|
setRecords([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void loadData();
|
|
}, []);
|
|
|
|
const filteredRows = useMemo(
|
|
() => records.filter((item) => {
|
|
const rechargeDate = getDate(item.paidAt ?? item.createdAt);
|
|
const tenantName = item.tenant?.name ?? tenants.find((tenant) => tenant.id === item.tenantId)?.name ?? item.tenantId;
|
|
const matchesEnterprise = !enterpriseKeyword || tenantName.includes(enterpriseKeyword);
|
|
const matchesStartDate = !dateRange.start || rechargeDate >= dateRange.start;
|
|
const matchesEndDate = !dateRange.end || rechargeDate <= dateRange.end;
|
|
return matchesEnterprise && matchesStartDate && matchesEndDate;
|
|
}),
|
|
[dateRange.end, dateRange.start, enterpriseKeyword, records, tenants],
|
|
);
|
|
|
|
function resetFilters() {
|
|
setEnterpriseKeyword('');
|
|
setDateRange({});
|
|
}
|
|
|
|
function updateForm<K extends keyof ManualRechargeForm>(key: K, value: ManualRechargeForm[K]) {
|
|
setForm((current) => ({ ...current, [key]: value }));
|
|
}
|
|
|
|
async function submitManualRecharge() {
|
|
const amount = Number(form.amount);
|
|
const smsUnits = Number(form.smsUnits || 0);
|
|
if (!form.tenantId || !Number.isFinite(amount) || amount <= 0 || !Number.isFinite(smsUnits) || smsUnits < 0) {
|
|
return;
|
|
}
|
|
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: '' });
|
|
}
|
|
|
|
return (
|
|
<section className="page-stack admin-recharge-page">
|
|
<div className="page-heading">
|
|
<div>
|
|
<Breadcrumb items={['数据详单', '充值记录']} />
|
|
<h1>充值记录</h1>
|
|
</div>
|
|
<Button icon={<Plus size={16} />} onClick={() => setManualOpen(true)}>人工充值</Button>
|
|
</div>
|
|
|
|
<div className="surface admin-recharge-filter">
|
|
<Input label="企业名称" onChange={(event) => setEnterpriseKeyword(event.target.value)} value={enterpriseKeyword} />
|
|
<DateRangeInput label="充值日期" onChange={setDateRange} value={dateRange} />
|
|
<div className="admin-recharge-filter__actions">
|
|
<Button icon={<Search size={16} />}>查询</Button>
|
|
<Button onClick={resetFilters} variant="ghost">重置</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="surface admin-recharge-table-card">
|
|
<div className="ui-table-wrap">
|
|
<table className="ui-table admin-recharge-table">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: '240px' }}>企业名称</th>
|
|
<th style={{ width: '180px' }}>充值时间</th>
|
|
<th style={{ width: '130px' }}>充值金额</th>
|
|
<th style={{ width: '140px' }}>充值后余额</th>
|
|
<th style={{ width: '120px' }}>充值类型</th>
|
|
<th style={{ width: '140px' }}>操作人</th>
|
|
<th style={{ width: '300px' }}>备注</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{error ? (
|
|
<tr><td className="ui-table__empty" colSpan={7}>{error}</td></tr>
|
|
) : loading ? (
|
|
<tr><td className="ui-table__empty" colSpan={7}>正在加载真实充值记录...</td></tr>
|
|
) : filteredRows.length === 0 ? (
|
|
<tr><td className="ui-table__empty" colSpan={7}>暂无真实充值记录</td></tr>
|
|
) : filteredRows.map((record) => {
|
|
const account = accounts.find((item) => item.tenantId === record.tenantId);
|
|
const transaction = transactions.find((item) => item.relatedId === record.id);
|
|
const tenantName = record.tenant?.name ?? tenants.find((tenant) => tenant.id === record.tenantId)?.name ?? record.tenantId;
|
|
return (
|
|
<tr key={record.id}>
|
|
<td><strong>{tenantName}</strong></td>
|
|
<td>{new Date(record.paidAt ?? record.createdAt).toLocaleString('zh-CN')}</td>
|
|
<td>{formatAmount(record.amountCents / 100)}</td>
|
|
<td>{formatAmount((transaction?.balanceAfter ?? account?.balanceCents ?? 0) / 100)}</td>
|
|
<td><Tag tone="warning">人工充值</Tag></td>
|
|
<td>{record.operatorId || '运营'}</td>
|
|
<td><RemarkCell value={record.remark ?? undefined} /></td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="admin-recharge-pagination">
|
|
<Select options={[{ label: '10条/页', value: '10' }, { label: '20条/页', value: '20' }]} value="10" />
|
|
<div>
|
|
<Button icon={<ChevronLeft size={16} />} iconOnly variant="ghost">上一页</Button>
|
|
<Button size="sm" variant="ghost">24</Button>
|
|
<Button size="sm">25</Button>
|
|
<Button size="sm" variant="ghost">26</Button>
|
|
<span>...</span>
|
|
<Button size="sm" variant="ghost">63</Button>
|
|
<Button icon={<ChevronRight size={16} />} iconOnly variant="ghost">下一页</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{manualOpen ? (
|
|
<Modal
|
|
footer={(
|
|
<>
|
|
<Button onClick={() => setManualOpen(false)} variant="ghost">取消</Button>
|
|
<Button onClick={submitManualRecharge}>确认充值</Button>
|
|
</>
|
|
)}
|
|
onClose={() => setManualOpen(false)}
|
|
open
|
|
size="md"
|
|
title="企业人工充值"
|
|
>
|
|
<div className="admin-system-modal-form">
|
|
<Select
|
|
label="企业名称"
|
|
onChange={(event) => updateForm('tenantId', event.target.value)}
|
|
options={tenants.map((tenant) => ({ label: tenant.name, value: tenant.id }))}
|
|
value={form.tenantId}
|
|
/>
|
|
<Input label="充值金额" onChange={(event) => updateForm('amount', event.target.value)} prefix="¥" 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)} value={form.operator} />
|
|
<Textarea className="admin-system-modal-form__wide" label="充值备注" onChange={(event) => updateForm('remark', event.target.value)} rows={4} value={form.remark} />
|
|
</div>
|
|
</Modal>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|