221 lines
8.2 KiB
TypeScript
221 lines
8.2 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Button, Card, Select, Space, Table, Tag, Typography, message, Input, DatePicker,
|
|
} from 'antd';
|
|
import {
|
|
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, RollbackOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getCreditRecords } from '../api';
|
|
import { formatDate } from '../utils/formatDate';
|
|
import dayjs from 'dayjs';
|
|
|
|
interface CreditRecord {
|
|
id: string;
|
|
username: string;
|
|
type: string;
|
|
amount: number;
|
|
balanceAfter: number;
|
|
description: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
const CREDIT_TYPE_MAP: Record<string, { text: string; color: string; icon: React.ReactNode }> = {
|
|
recharge: { text: '充值', color: 'green', icon: <ArrowUpOutlined /> },
|
|
consume: { text: '消费', color: 'red', icon: <ArrowDownOutlined /> },
|
|
refund: { text: '退回', color: 'blue', icon: <RollbackOutlined /> },
|
|
};
|
|
|
|
const AdminCreditRecords: React.FC = () => {
|
|
const [records, setRecords] = useState<CreditRecord[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(10);
|
|
const [typeFilter, setTypeFilter] = useState<string>('');
|
|
const [userNameFilter, setUserNameFilter] = useState<string>('');
|
|
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const filters: { type?: string; user_name?: string; start_date?: string; end_date?: string; page?: number; page_size?: number } = {};
|
|
if (typeFilter) filters.type = typeFilter;
|
|
if (userNameFilter) filters.user_name = userNameFilter;
|
|
if (dateRange[0]) filters.start_date = dateRange[0].format('YYYY-MM-DD');
|
|
if (dateRange[1]) filters.end_date = dateRange[1].format('YYYY-MM-DD');
|
|
filters.page = page;
|
|
filters.page_size = pageSize;
|
|
const res = await getCreditRecords(Object.keys(filters).length > 0 ? filters : undefined);
|
|
setRecords(res.items || []);
|
|
setTotal(res.total || 0);
|
|
} catch {
|
|
message.error('加载积分记录失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { load(); }, [page, pageSize, typeFilter, userNameFilter, dateRange]);
|
|
|
|
const handlePageChange = (p: number, ps: number) => {
|
|
setPage(p);
|
|
setPageSize(ps);
|
|
};
|
|
|
|
useEffect(() => { load(); }, []);
|
|
|
|
const handleSearch = () => {
|
|
load();
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setTypeFilter('');
|
|
setUserNameFilter('');
|
|
setDateRange([null, null]);
|
|
load();
|
|
};
|
|
|
|
const totalRecharge = records.filter(r => r.type === 'recharge').reduce((s, r) => s + r.amount, 0);
|
|
const totalConsume = records.filter(r => r.type === 'consume').reduce((s, r) => s + Math.abs(r.amount), 0);
|
|
|
|
const columns = [
|
|
{
|
|
title: '用户', dataIndex: 'username', width: 120,
|
|
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '类型', dataIndex: 'type', width: 100,
|
|
render: (v: string) => {
|
|
const cfg = CREDIT_TYPE_MAP[v] || { text: v || '-', color: 'default', icon: null };
|
|
return (
|
|
<Tag color={cfg.color} icon={cfg.icon}>
|
|
{cfg.text}
|
|
</Tag>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '变动积分', dataIndex: 'amount', width: 120, sorter: (a: CreditRecord, b: CreditRecord) => a.amount - b.amount,
|
|
render: (v: number) => (
|
|
<Typography.Text strong style={{ color: v > 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
|
|
{v > 0 ? '+' : ''}{v.toLocaleString()}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{
|
|
title: '变动后余额', dataIndex: 'balanceAfter', width: 120,
|
|
render: (v: number) => <Typography.Text type="secondary">{v.toLocaleString()}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '说明', dataIndex: 'description', ellipsis: true,
|
|
},
|
|
{
|
|
title: '时间', dataIndex: 'createdAt', width: 160,
|
|
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text>,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
{/* Summary Cards */}
|
|
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
|
|
<Card bordered={false} style={{ flex: 1, borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div style={{
|
|
width: 44, height: 44, borderRadius: 10,
|
|
background: 'rgba(16,185,129,0.08)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: 20, color: '#10b981',
|
|
}}><ArrowUpOutlined /></div>
|
|
<div>
|
|
<div style={{ color: '#94a3b8', fontSize: 12 }}>总充值</div>
|
|
<div style={{ fontSize: 22, fontWeight: 800, color: '#10b981' }}>+{totalRecharge.toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
<Card bordered={false} style={{ flex: 1, borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div style={{
|
|
width: 44, height: 44, borderRadius: 10,
|
|
background: 'rgba(239,68,68,0.08)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: 20, color: '#ef4444',
|
|
}}><ArrowDownOutlined /></div>
|
|
<div>
|
|
<div style={{ color: '#94a3b8', fontSize: 12 }}>总消费</div>
|
|
<div style={{ fontSize: 22, fontWeight: 800, color: '#ef4444' }}>-{totalConsume.toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
<Card bordered={false} style={{ flex: 1, borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div style={{
|
|
width: 44, height: 44, borderRadius: 10,
|
|
background: 'rgba(99,102,241,0.08)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: 20, color: '#6366f1',
|
|
}}><WalletOutlined /></div>
|
|
<div>
|
|
<div style={{ color: '#94a3b8', fontSize: 12 }}>交易笔数</div>
|
|
<div style={{ fontSize: 22, fontWeight: 800, color: '#1a1a2e' }}>{total}</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
|
|
<Space wrap>
|
|
<Select
|
|
value={typeFilter}
|
|
onChange={(value) => setTypeFilter(value)}
|
|
style={{ width: 120 }}
|
|
options={[
|
|
{ value: '', label: '全部类型' },
|
|
{ value: 'recharge', label: '充值' },
|
|
{ value: 'consume', label: '消费' },
|
|
{ value: 'refund', label: '退回' },
|
|
]}
|
|
/>
|
|
<Input
|
|
placeholder="用户名搜索"
|
|
value={userNameFilter}
|
|
onChange={(e) => setUserNameFilter(e.target.value)}
|
|
style={{ width: 180 }}
|
|
allowClear
|
|
/>
|
|
<DatePicker.RangePicker
|
|
value={dateRange}
|
|
onChange={(dates) => { if (dates) setDateRange([dates[0], dates[1]]); }}
|
|
placeholder={['开始日期', '结束日期']}
|
|
style={{ width: 250 }}
|
|
/>
|
|
</Space>
|
|
<Space>
|
|
<Button type="primary" onClick={handleSearch}>搜索</Button>
|
|
<Button onClick={handleReset}>重置</Button>
|
|
<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>
|
|
</Space>
|
|
</div>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={records}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
pageSize: pageSize,
|
|
total: total,
|
|
onChange: handlePageChange,
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 条记录`,
|
|
}}
|
|
scroll={{ x: 800 }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminCreditRecords;
|