feat: 添加MSW模拟服务和数据源集成
refactor: 重构页面组件移除冗余Layout组件 feat: 实现WebSocket和事件总线系统 feat: 添加队列和调度系统 docs: 更新架构文档和服务映射 style: 清理重复接口定义使用数据源 chore: 更新依赖项配置 feat: 添加运行时系统和领域引导 ci: 配置ESLint边界检查规则 build: 添加Redis和WebSocket依赖 test: 添加MSW浏览器环境入口 perf: 优化数据获取逻辑使用统一数据源 fix: 修复类型定义和状态管理问题
This commit is contained in:
277
client/src/pages/Billing/index.tsx
Normal file
277
client/src/pages/Billing/index.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user