Files
makemd/dashboard/src/pages/Finance/Transactions.tsx
wurenzhi 1b14947e7b refactor: 优化代码结构和类型定义
feat(types): 添加express.d.ts类型引用
style: 格式化express.d.ts中的接口定义
refactor: 移除未使用的AntFC类型导入
chore: 删除自动生成的.umi-production文件
feat: 添加店铺管理相关表和初始化脚本
docs: 更新安全规则和交互指南文档
refactor: 统一使用FC类型替代React.FC
perf: 优化图表组件导入方式
style: 添加.prettierrc配置文件
refactor: 调整组件导入顺序和结构
feat: 添加平台库存管理路由
fix: 修复订单同步时的库存检查逻辑
docs: 更新RBAC设计和租户管理文档
refactor: 优化部门控制器代码
2026-03-30 01:20:57 +08:00

481 lines
16 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 {
useState,
useEffect,
useMemo,
Table,
Button,
Input,
Select,
DatePicker,
message,
Card,
Row,
Col,
Tag,
Space,
Modal,
Drawer,
Descriptions,
Typography,
Alert,
Statistic,
Tooltip,
SearchOutlined,
FilterOutlined,
ExportOutlined,
DownloadOutlined,
EyeOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
DollarOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
ReloadOutlined,
useNavigate,
Option,
RangePicker,
Search,
Title,
Text,
FC,
} from '@/imports';
import { LineChart, Line, Bar, Pie as RechartsPie, ResponsiveContainer, Legend, XAxis, YAxis, CartesianGrid, Cell } from 'recharts';
import moment from 'moment';
import { financeDataSource } from '@/services/financeDataSource';
import type { Transaction } from '@/services/financeDataSource';
const Transactions: FC = () => {
const navigate = useNavigate();
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Transaction[]>([]);
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false);
const [currentTransaction, setCurrentTransaction] = useState<Transaction | null>(null);
const [filters, setFilters] = useState({
type: '',
category: '',
status: '',
search: '',
dateRange: null as any,
minAmount: null as number | null,
maxAmount: null as number | null,
});
const fetchTransactions = async (filterParams?: any) => {
setLoading(true);
try {
// 转换过滤参数以匹配 financeDataSource 期望的格式
const params = {
type: filterParams?.type || filters.type,
startDate: filterParams?.dateRange?.[0]?.format('YYYY-MM-DD'),
endDate: filterParams?.dateRange?.[1]?.format('YYYY-MM-DD'),
};
const data = await financeDataSource.fetchTransactions(params);
setTransactions(data);
} catch (error) {
message.error('Failed to load transactions');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTransactions();
}, []); // 只在组件挂载时执行一次
const handleExport = () => {
message.success('交易记录已导出');
};
const handleBatchExport = () => {
if (selectedRows.length === 0) {
message.warning('请先选择要导出的交易');
return;
}
message.success(`成功导出 ${selectedRows.length} 条交易记录`);
};
const handleViewDetail = (record: Transaction) => {
setCurrentTransaction(record);
setDetailDrawerVisible(true);
};
const handleRefresh = () => {
fetchTransactions();
message.success('数据已刷新');
};
const stats = useMemo(() => {
const income = transactions.filter(t => t.type === 'income').reduce((sum, t) => sum + t.amount, 0);
const expense = transactions.filter(t => t.type === 'expense').reduce((sum, t) => sum + t.amount, 0);
const pending = transactions.filter(t => t.status === 'pending').reduce((sum, t) => sum + t.amount, 0);
return {
income,
expense,
profit: income - expense,
pending,
total: transactions.length,
};
}, [transactions]);
const trendData = useMemo(() => {
const trendData = Array.from({ length: 7 }, (_, i) => {
const date = moment().subtract(i, 'days').format('YYYY-MM-DD');
const dayTransactions = transactions.filter(t => moment(t.createdAt).format('YYYY-MM-DD') === date);
return {
date,
income: dayTransactions.filter(t => t.type === 'income').reduce((sum, t) => sum + t.amount, 0),
expense: dayTransactions.filter(t => t.type === 'expense').reduce((sum, t) => sum + t.amount, 0),
};
}).reverse();
return trendData;
}, [transactions]);
const categoryData = useMemo(() => {
const categoryMap: Record<string, number> = {};
transactions.filter(t => t.type === 'expense').forEach(t => {
categoryMap[t.category] = (categoryMap[t.category] || 0) + t.amount;
});
return Object.entries(categoryMap).map(([name, value]) => ({ name, value }));
}, [transactions]);
const columns = [
{
title: '交易ID',
dataIndex: 'transactionId',
key: 'transactionId',
render: (text: string) => <Text strong>{text}</Text>,
},
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
sorter: (a: Transaction, b: Transaction) => a.amount - b.amount,
render: (amount: number, record: Transaction) => (
<Text style={{ color: record.type === 'income' ? '#52c41a' : '#ff4d4f', fontWeight: 500 }}>
{record.type === 'income' ? '+' : '-'}${(amount || 0).toFixed(2)}
</Text>
),
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
filters: [
{ text: '收入', value: 'income' },
{ text: '支出', value: 'expense' },
{ text: '退款', value: 'refund' },
{ text: '费用', value: 'fee' },
],
render: (type: string) => {
const typeConfig = {
income: { text: '收入', color: 'success' },
expense: { text: '支出', color: 'error' },
refund: { text: '退款', color: 'warning' },
fee: { text: '费用', color: 'processing' },
};
const config = typeConfig[type as keyof typeof typeConfig] || { text: type, color: 'default' };
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
filters: [
{ text: '销售', value: 'Sales' },
{ text: '供应商', value: 'Suppliers' },
{ text: '营销', value: 'Marketing' },
{ text: '物流', value: 'Shipping' },
{ text: '平台费用', value: 'Platform' },
{ text: '退款', value: 'Refund' },
],
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '日期',
dataIndex: 'date',
key: 'date',
sorter: (a: Transaction, b: Transaction) => moment(a.createdAt).unix() - moment(b.createdAt).unix(),
render: (date: string) => moment(date).format('YYYY-MM-DD HH:mm'),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '已完成', value: 'completed' },
{ text: '待处理', value: 'pending' },
{ text: '失败', value: 'failed' },
],
render: (status: string) => {
const statusConfig = {
completed: { text: '已完成', color: 'success', icon: <CheckCircleOutlined /> },
pending: { text: '待处理', color: 'warning', icon: <SyncOutlined spin /> },
failed: { text: '失败', color: 'error', icon: <CloseCircleOutlined /> },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
},
},
{
title: '操作',
key: 'action',
render: (_: any, record: Transaction) => (
<Space>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
</Button>
</Space>
),
},
];
return (
<div className="transactions">
<Row justify="space-between" align="middle" style={{ marginBottom: 24 }}>
<Col>
<Title level={4}></Title>
<Text type="secondary"></Text>
</Col>
<Col>
<Space>
<Button icon={<ReloadOutlined />} onClick={handleRefresh}>
</Button>
<Button icon={<ExportOutlined />} onClick={handleExport}>
</Button>
</Space>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12} lg={6}>
<Card size="small">
<Statistic
title="总收入"
value={stats.income}
precision={2}
prefix={<ArrowUpOutlined />}
valueStyle={{ color: '#52c41a' }}
suffix="USD"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card size="small">
<Statistic
title="总支出"
value={stats.expense}
precision={2}
prefix={<ArrowDownOutlined />}
valueStyle={{ color: '#ff4d4f' }}
suffix="USD"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card size="small">
<Statistic
title="净利润"
value={stats.profit}
precision={2}
prefix={<DollarOutlined />}
valueStyle={{ color: stats.profit >= 0 ? '#52c41a' : '#ff4d4f' }}
suffix="USD"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card size="small">
<Statistic
title="待处理"
value={stats.pending}
precision={2}
prefix={<SyncOutlined />}
valueStyle={{ color: '#faad14' }}
suffix="USD"
/>
</Card>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={16}>
<Card title="收支趋势最近7天" size="small">
<ResponsiveContainer width="100%" height={250}>
<LineChart
data={trendData}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="income" name="收入" stroke="#52c41a" activeDot={{ r: 8 }} />
<Line type="monotone" dataKey="expense" name="支出" stroke="#ff4d4f" />
</LineChart>
</ResponsiveContainer>
</Card>
</Col>
<Col span={8}>
<Card title="支出分类" size="small">
<ResponsiveContainer width="100%" height={250}>
<RechartsPie
data={categoryData}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name}: ${((percent || 0) * 100).toFixed(0)}%`}
>
{categoryData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={['#1890ff', '#52c41a', '#faad14', '#ff4d4f', '#722ed1', '#eb2f96'][index % 6]} />
))}
</RechartsPie>
</ResponsiveContainer>
</Card>
</Col>
</Row>
{selectedRows.length > 0 && (
<Alert
message={`已选择 ${selectedRows.length}`}
action={
<Space>
<Button icon={<ExportOutlined />} onClick={handleBatchExport}>
</Button>
<Button onClick={() => setSelectedRows([])}>
</Button>
</Space>
}
style={{ marginBottom: 16 }}
/>
)}
<Card>
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Search
placeholder="搜索交易ID或描述"
style={{ width: 300 }}
onSearch={(value) => setFilters({ ...filters, search: value })}
allowClear
/>
<Select
placeholder="类型"
style={{ width: 120 }}
value={filters.type}
onChange={(value) => setFilters({ ...filters, type: value })}
allowClear
>
<Option value="income"></Option>
<Option value="expense"></Option>
</Select>
<Select
placeholder="分类"
style={{ width: 120 }}
value={filters.category}
onChange={(value) => setFilters({ ...filters, category: value })}
allowClear
>
<Option value="Sales"></Option>
<Option value="Suppliers"></Option>
<Option value="Marketing"></Option>
<Option value="Shipping"></Option>
<Option value="Platform"></Option>
<Option value="Refund">退</Option>
</Select>
<Select
placeholder="状态"
style={{ width: 120 }}
value={filters.status}
onChange={(value) => setFilters({ ...filters, status: value })}
allowClear
>
<Option value="completed"></Option>
<Option value="pending"></Option>
<Option value="failed"></Option>
</Select>
<RangePicker
style={{ width: 300 }}
value={filters.dateRange}
onChange={(dates) => setFilters({ ...filters, dateRange: dates })}
/>
<Button
icon={<FilterOutlined />}
onClick={() => fetchTransactions(filters)}
>
</Button>
</div>
<Table
columns={columns}
dataSource={transactions}
loading={loading}
rowKey="id"
rowSelection={{
selectedRowKeys: selectedRows.map(r => r.id),
onChange: (selectedRowKeys: React.Key[], selectedRows: Transaction[]) => {
setSelectedRows(selectedRows);
},
}}
pagination={{
pageSize: 20,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100'],
}}
scroll={{ x: 1200 }}
/>
</Card>
<Drawer
title="交易详情"
placement="right"
onClose={() => setDetailDrawerVisible(false)}
open={detailDrawerVisible}
width={600}
>
{currentTransaction && (
<Descriptions column={1} bordered>
<Descriptions.Item label="交易ID">{currentTransaction.id}</Descriptions.Item>
<Descriptions.Item label="金额">
<Text style={{ color: currentTransaction.type === 'income' ? '#52c41a' : '#ff4d4f', fontSize: 18, fontWeight: 500 }}>
{currentTransaction.type === 'income' ? '+' : '-'}${currentTransaction.amount.toFixed(2)}
</Text>
</Descriptions.Item>
<Descriptions.Item label="类型">
<Tag color={currentTransaction.type === 'income' ? 'success' : 'error'}>
{currentTransaction.type === 'income' ? '收入' : '支出'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="分类">{currentTransaction.category}</Descriptions.Item>
<Descriptions.Item label="描述">{currentTransaction.description}</Descriptions.Item>
<Descriptions.Item label="日期">{moment(currentTransaction.createdAt).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={currentTransaction.status === 'completed' ? 'success' : currentTransaction.status === 'pending' ? 'warning' : 'error'}>
{currentTransaction.status === 'completed' ? '已完成' : currentTransaction.status === 'pending' ? '待处理' : '失败'}
</Tag>
</Descriptions.Item>
</Descriptions>
)}
</Drawer>
</div>
);
};
export default Transactions;