Files
makemd/client/src/pages/Billing/index.tsx
wurenzhi 0dac26d781 feat: 添加MSW模拟服务和数据源集成
refactor: 重构页面组件移除冗余Layout组件

feat: 实现WebSocket和事件总线系统

feat: 添加队列和调度系统

docs: 更新架构文档和服务映射

style: 清理重复接口定义使用数据源

chore: 更新依赖项配置

feat: 添加运行时系统和领域引导

ci: 配置ESLint边界检查规则

build: 添加Redis和WebSocket依赖

test: 添加MSW浏览器环境入口

perf: 优化数据获取逻辑使用统一数据源

fix: 修复类型定义和状态管理问题
2026-03-19 01:39:34 +08:00

278 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { Table, Tag, Spin, Alert, Typography, Card, Button, Modal, Descriptions } from 'antd';
import { useRequest } from 'umi';
const { Title, Text } = Typography;
interface BillingRecord {
id: string;
merchantId: string;
totalAmount: number;
status: string;
createdAt: Date;
paidAt?: Date;
}
interface BillingItem {
id: string;
billingId: string;
feature: string;
amount: number;
quantity: number;
unitPrice: number;
}
const statusColorMap: Record<string, string> = {
pending: 'orange',
paid: 'green'
};
const statusTextMap: Record<string, string> = {
pending: '待支付',
paid: '已支付'
};
const featureMap: Record<string, string> = {
AI_OPTIMIZE: 'AI优化',
ADS_AUTO: '自动广告',
SYNC_INVENTORY: '库存同步',
CALCULATE_PROFIT: '利润计算'
};
export default function Billing() {
const [bills, setBills] = useState<BillingRecord[]>([]);
const [loading, setLoading] = useState(true);
const [selectedBill, setSelectedBill] = useState<BillingRecord | null>(null);
const [billItems, setBillItems] = useState<BillingItem[]>([]);
const [itemsLoading, setItemsLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
// 获取账单列表
const { run: fetchBills } = useRequest(async () => {
try {
// 从本地存储获取merchantId实际应用中应该从认证信息中获取
const merchantId = localStorage.getItem('merchantId') || 'anonymous';
const response = await fetch(`/api/billing?merchantId=${merchantId}`);
const data = await response.json();
setBills(data.list || []);
} catch (error) {
console.error('Error fetching bills:', error);
} finally {
setLoading(false);
}
}, {
manual: true
});
// 获取账单明细
const { run: fetchBillItems } = useRequest(async (billingId: string) => {
try {
setItemsLoading(true);
const response = await fetch(`/api/billing/items?billingId=${billingId}`);
const data = await response.json();
setBillItems(data.list || []);
} catch (error) {
console.error('Error fetching bill items:', error);
} finally {
setItemsLoading(false);
}
}, {
manual: true
});
// 初始加载账单列表
useEffect(() => {
fetchBills();
}, [fetchBills]);
// 查看账单明细
const handleViewDetails = async (bill: BillingRecord) => {
setSelectedBill(bill);
await fetchBillItems(bill.id);
setModalVisible(true);
};
// 标记账单为已支付
const handleMarkAsPaid = async (bill: BillingRecord) => {
try {
const response = await fetch(`/api/billing/paid`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ billingId: bill.id })
});
if (response.ok) {
// 刷新账单列表
fetchBills();
}
} catch (error) {
console.error('Error marking bill as paid:', error);
}
};
// 列定义
const columns = [
{
title: '账单ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '总金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
render: (amount: number) => `$${amount.toFixed(2)}`,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={statusColorMap[status]}>
{statusTextMap[status]}
</Tag>
),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date: Date) => new Date(date).toLocaleString(),
},
{
title: '支付时间',
dataIndex: 'paidAt',
key: 'paidAt',
render: (date: Date) => date ? new Date(date).toLocaleString() : '-',
},
{
title: '操作',
key: 'action',
render: (_: any, record: BillingRecord) => (
<div>
<Button
type="link"
onClick={() => handleViewDetails(record)}
style={{ marginRight: '8px' }}
>
</Button>
{record.status === 'pending' && (
<Button
type="primary"
onClick={() => handleMarkAsPaid(record)}
>
</Button>
)}
</div>
),
},
];
// 计算总金额
const totalAmount = bills.reduce((sum, bill) => sum + bill.totalAmount, 0);
const pendingAmount = bills.filter(bill => bill.status === 'pending').reduce((sum, bill) => sum + bill.totalAmount, 0);
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Card style={{ marginBottom: '24px' }}>
<Descriptions bordered>
<Descriptions.Item label="总账单金额">${totalAmount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="待支付金额">${pendingAmount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="已支付金额">${(totalAmount - pendingAmount).toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="账单数量">{bills.length}</Descriptions.Item>
</Descriptions>
</Card>
{loading ? (
<div style={{ textAlign: 'center', padding: '48px' }}>
<Spin size="large" />
</div>
) : bills.length === 0 ? (
<Alert message="暂无账单记录" type="info" />
) : (
<Table
rowKey="id"
columns={columns}
dataSource={bills}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
}}
scroll={{ x: 1000 }}
/>
)}
{/* 账单明细 modal */}
<Modal
title="账单明细"
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={[
<Button key="close" onClick={() => setModalVisible(false)}>
</Button>
]}
width={800}
>
{selectedBill && (
<div>
<Descriptions bordered style={{ marginBottom: '24px' }}>
<Descriptions.Item label="账单ID">{selectedBill.id}</Descriptions.Item>
<Descriptions.Item label="总金额">${selectedBill.totalAmount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={statusColorMap[selectedBill.status]}>
{statusTextMap[selectedBill.status]}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="创建时间">{new Date(selectedBill.createdAt).toLocaleString()}</Descriptions.Item>
<Descriptions.Item label="支付时间">{selectedBill.paidAt ? new Date(selectedBill.paidAt).toLocaleString() : '-'}</Descriptions.Item>
</Descriptions>
<Title level={4}></Title>
{itemsLoading ? (
<Spin />
) : billItems.length === 0 ? (
<Alert message="暂无明细记录" type="info" />
) : (
<Table
rowKey="id"
columns={[
{
title: '功能',
dataIndex: 'feature',
render: (feature: string) => featureMap[feature] || feature,
},
{
title: '数量',
dataIndex: 'quantity',
},
{
title: '单价',
dataIndex: 'unitPrice',
render: (price: number) => `$${price.toFixed(2)}`,
},
{
title: '金额',
dataIndex: 'amount',
render: (amount: number) => `$${amount.toFixed(2)}`,
},
]}
dataSource={billItems}
pagination={false}
/>
)}
</div>
)}
</Modal>
</div>
);
}