Files
makemd/client/src/pages/Billing/index.tsx

278 lines
8.0 KiB
TypeScript
Raw Normal View History

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>
);
}