278 lines
8.0 KiB
TypeScript
278 lines
8.0 KiB
TypeScript
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|