Files
ele-HeiXiu/src/pages/panels/BillingInfo.tsx
YoungestSongMo 0e6d996718 refactor: 重组 components/ 和 pages/ 文件结构,按职责分层
components/ 拆为三层:
  - providers/   — 状态管理(App/Theme/SettingsProvider)
  - shell/       — 应用外壳(Layout/NavBar/PageDispatcher)
  - ui/          — 通用 UI 原子(FloatingPanel/LegalTextModal)

pages/ 重命名 + 重组:
  - headers/ → panels/        NavBar 触发的面板页
  - login/   → auth/          认证模块(含注册/忘记密码)
  - settings/blocks/ → sections/
  - home/components/ → panels/ features/ controls/ hooks/
    按职责分:panels(布局壳) + features(功能面板) + controls(表单控件) + hooks

其他清理:
  - 删除空占位 Test3.tsx
  - HomeContext 从 HomeContent 拆出独立文件
  - uploadValidation 内联到 ImageInput
  - 修复过时注释(useUI → controls)
2026-06-15 11:19:06 +08:00

259 lines
8.4 KiB
TypeScript

import {useState, useMemo} from 'react';
import {
Card,
Table,
Button,
Typography,
Skeleton,
Result,
Space,
Tag,
theme as antTheme,
} from 'antd';
import {
WalletOutlined,
RiseOutlined,
ApiOutlined,
} from '@ant-design/icons';
import {useBillingBalance, useBillingLedger} from '@/hooks/use-api';
import type {EntryTypeValue, LedgerItemResponseBody, LedgerReqeustParam} from '@/services/modules/billing';
import {EnumResolver} from '@/utils/enum-resolver';
import {centToYuan, formatDate} from '@/utils/display';
const {Text, Title} = Typography;
// ============================================================
// 余额卡片
// ============================================================
function BalanceCard() {
const {token} = antTheme.useToken();
const {data, loading, error, errorMessage, refetch} = useBillingBalance();
// ======== 加载态 ========
if (loading) {
return (
<Card size="small" className="rounded-md!">
<Skeleton active paragraph={{rows: 3}} />
</Card>
);
}
// ======== 错误态 ========
if (error || !data) {
return (
<Card size="small" className="rounded-md!">
<Result
status="error"
title="余额加载失败"
subTitle={errorMessage}
extra={
<Button type="primary" size="small" onClick={refetch}>
</Button>
}
/>
</Card>
);
}
// ======== 正常展示 ========
const totalBalance = (data.balance_cent ?? 0) + (data.gift_balance_cent ?? 0);
return (
<Card
size="small"
styles={{body: {padding: '16px 20px'}}}
className="rounded-md!"
>
<Text type="secondary" style={{fontSize: 12, marginBottom: 8, display: 'block'}}>
<WalletOutlined style={{marginRight: 6}} />
</Text>
<Title level={3} style={{margin: '0 0 12px 0'}}>
{centToYuan(totalBalance)}
<Text type="secondary" style={{fontSize: 14, marginLeft: 4}}>
{data.currency}
</Text>
</Title>
<Space size={32}>
<div>
<Text type="secondary" style={{fontSize: 12}}></Text>
<br />
<Text strong>{centToYuan(data.balance_cent ?? 0)}</Text>
</div>
<div>
<Text type="secondary" style={{fontSize: 12}}></Text>
<br />
<Text strong>{centToYuan(data.gift_balance_cent ?? 0)}</Text>
</div>
<div>
<Space size={4}>
<RiseOutlined style={{color: token.colorWarning}} />
<Text type="secondary" style={{fontSize: 12}}></Text>
</Space>
<br />
<Text strong>{centToYuan(data.today_spent_cent)}</Text>
</div>
<div>
<Space size={4}>
<ApiOutlined style={{color: token.colorPrimary}} />
<Text type="secondary" style={{fontSize: 12}}></Text>
</Space>
<br />
<Text strong>{data.today_calls} </Text>
</div>
</Space>
</Card>
);
}
function LedgerTable() {
const {token} = antTheme.useToken();
const [pagination, setPagination] = useState({page: 1, page_size: 10});
const params: LedgerReqeustParam = useMemo(() => ({
month: null,
page: pagination.page,
page_size: pagination.page_size,
}), [pagination]);
const {data, loading, error, errorMessage, refetch} = useBillingLedger(params);
const columns = [
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120,
align: 'center' as const,
render: (_: unknown, record: LedgerItemResponseBody) => (
<Text style={{fontSize: 12, whiteSpace: 'nowrap'}}>
{formatDate(record.created_at)}
</Text>
),
},
{
title: '类型',
dataIndex: 'entry_type',
key: 'entry_type',
width: 100,
align: 'center' as const,
render: (type: EntryTypeValue) => (
<Tag color={EnumResolver.billing.entryType.color(type)} style={{fontSize: 11, lineHeight: '18px'}}>
{EnumResolver.billing.entryType.label(type)}
</Tag>
),
},
{
title: '金额',
dataIndex: 'amount_cent',
key: 'amount_cent',
width: 100,
align: 'center' as const,
render: (amount: number): React.ReactNode => {
const isNegative = amount < 0;
return (
<Text
strong
style={{
fontSize: 13,
color: isNegative ? token.colorError : token.colorSuccess,
}}
>
{isNegative ? '-' : '+'}
{centToYuan(Math.abs(amount))}
</Text>
);
},
},
{
title: '余额',
dataIndex: 'balance_after_cent',
key: 'balance_after_cent',
width: 100,
align: 'center' as const,
render: (val: number) => (
<Text style={{fontSize: 12}}>{centToYuan(val)}</Text>
),
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
align: 'left' as const,
render: (desc: string) => (
<Text style={{fontSize: 12}}>{desc || '—'}</Text>
),
},
];
// ======== 错误态 ========
if (error) {
return (
<Card size="small" className="rounded-md!">
<Result
status="error"
title="账单加载失败"
subTitle={errorMessage}
extra={
<Button type="primary" size="small" onClick={refetch}>
</Button>
}
/>
</Card>
);
}
// ======== 正常 / 加载态 ========
return (
<Card
size="small"
styles={{body: {padding: 0}}}
className="rounded-md!"
>
<Table<LedgerItemResponseBody>
columns={columns}
dataSource={data ?? []}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: pagination.page,
pageSize: pagination.page_size,
total: (data ?? []).length >= pagination.page_size
? pagination.page * pagination.page_size + 1
: pagination.page * pagination.page_size,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
showTotal: (total: number, range: [number, number]) =>
`${range[0]}-${range[1]},共 ${total}+ 条`,
onChange: (page: number, pageSize: number) => {
setPagination({page, page_size: pageSize});
},
position: ['bottomCenter'],
}}
locale={{emptyText: '暂无消费记录'}}
/>
</Card>
);
}
// ============================================================
// 页面入口
// ============================================================
export function BillingInfo() {
return (
<div style={{padding: '8px 0', width: '100%'}}>
<div className="flex flex-col gap-4" style={{width: '100%'}}>
<BalanceCard />
<LedgerTable />
</div>
</div>
);
}