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)
This commit is contained in:
258
src/pages/panels/BillingInfo.tsx
Normal file
258
src/pages/panels/BillingInfo.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user