141 lines
6.0 KiB
TypeScript
141 lines
6.0 KiB
TypeScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import {
|
|
Col, Row, Space, Table, Tag, Typography,
|
|
} from 'antd';
|
|
import {
|
|
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined,
|
|
ThunderboltOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getCredits } from '../api';
|
|
import { useAuthStore } from '../store/useAuthStore';
|
|
import { formatDate } from '../utils/formatDate';
|
|
import type { CreditRecord } from '../types';
|
|
|
|
const AnimatedNumber: React.FC<{ value: number; duration?: number }> = ({ value, duration = 1200 }) => {
|
|
const [display, setDisplay] = useState(0);
|
|
const ref = useRef<number>(0);
|
|
useEffect(() => {
|
|
const start = display;
|
|
const diff = value - start;
|
|
if (diff === 0) return;
|
|
const startTime = performance.now();
|
|
const animate = (now: number) => {
|
|
const elapsed = now - startTime;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
setDisplay(start + diff * eased);
|
|
if (progress < 1) ref.current = requestAnimationFrame(animate);
|
|
};
|
|
ref.current = requestAnimationFrame(animate);
|
|
return () => cancelAnimationFrame(ref.current);
|
|
}, [value]);
|
|
return <>{display.toLocaleString()}</>;
|
|
};
|
|
|
|
const CreditsPage: React.FC = () => {
|
|
const { user } = useAuthStore();
|
|
const [records, setRecords] = useState<CreditRecord[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetch = async () => {
|
|
setLoading(true);
|
|
const data = await getCredits();
|
|
setRecords(data.records);
|
|
setLoading(false);
|
|
};
|
|
fetch();
|
|
}, []);
|
|
|
|
const totalRecharge = records.filter((r) => r.type === 'recharge').reduce((sum, r) => sum + r.amount, 0);
|
|
const totalConsume = records.filter((r) => r.type === 'consume').reduce((sum, r) => sum + Math.abs(r.amount), 0);
|
|
|
|
const columns = [
|
|
{ title: '类型', dataIndex: 'type', key: 'type', width: 100,
|
|
render: (type: string) => (
|
|
<Tag color={type === 'recharge' ? 'green' : 'orange'} icon={type === 'recharge' ? <ArrowUpOutlined /> : <ArrowDownOutlined />}>
|
|
{type === 'recharge' ? '充值' : '消费'}
|
|
</Tag>
|
|
),
|
|
},
|
|
{ title: '积分变动', dataIndex: 'amount', key: 'amount', width: 120,
|
|
render: (amount: number) => (
|
|
<Typography.Text strong style={{ color: amount > 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
|
|
{amount > 0 ? '+' : ''}{amount}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{ title: '说明', dataIndex: 'description', key: 'description' },
|
|
{ title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 160,
|
|
render: (d: string) => <span className="date-display" translate="no" style={{ color: '#94a3b8' }}>{formatDate(d)}</span>,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
{/* Balance cards */}
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
|
<Col xs={24} sm={8}>
|
|
<div className="animate-fadeInUp" style={{
|
|
borderRadius: 16, padding: 24,
|
|
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
|
position: 'relative', overflow: 'hidden',
|
|
}}>
|
|
<div style={{ position: 'absolute', right: -20, top: -20, width: 120, height: 120, borderRadius: '50%', background: 'rgba(255,255,255,0.1)' }} />
|
|
<div style={{ position: 'relative' }}>
|
|
<Space style={{ marginBottom: 10 }}>
|
|
<WalletOutlined style={{ color: 'rgba(255,255,255,0.8)', fontSize: 16 }} />
|
|
<span style={{ color: 'rgba(255,255,255,0.8)', fontSize: 14 }}>当前可用积分</span>
|
|
</Space>
|
|
<div style={{ color: '#fff', fontSize: 38, fontWeight: 800, lineHeight: 1 }}>
|
|
<AnimatedNumber value={user?.credits ?? 0} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Col>
|
|
<Col xs={12} sm={8}>
|
|
<div className="animate-fadeInUp" style={{
|
|
borderRadius: 16, padding: 24, background: '#fff', border: '1px solid #f0f0f5', animationDelay: '0.06s',
|
|
}}>
|
|
<Space style={{ marginBottom: 10 }}>
|
|
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'rgba(16,185,129,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<ArrowUpOutlined style={{ color: '#10b981', fontSize: 14 }} />
|
|
</div>
|
|
<span style={{ color: '#94a3b8', fontSize: 14 }}>累计获得</span>
|
|
</Space>
|
|
<div style={{ color: '#10b981', fontSize: 28, fontWeight: 800 }}><AnimatedNumber value={totalRecharge} /></div>
|
|
</div>
|
|
</Col>
|
|
<Col xs={12} sm={8}>
|
|
<div className="animate-fadeInUp" style={{
|
|
borderRadius: 16, padding: 24, background: '#fff', border: '1px solid #f0f0f5', animationDelay: '0.12s',
|
|
}}>
|
|
<Space style={{ marginBottom: 10 }}>
|
|
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'rgba(239,68,68,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<ArrowDownOutlined style={{ color: '#ef4444', fontSize: 14 }} />
|
|
</div>
|
|
<span style={{ color: '#94a3b8', fontSize: 14 }}>累计消耗</span>
|
|
</Space>
|
|
<div style={{ color: '#ef4444', fontSize: 28, fontWeight: 800 }}><AnimatedNumber value={totalConsume} /></div>
|
|
</div>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* Records */}
|
|
<div className="animate-fadeInUp" style={{ animationDelay: '0.15s' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
|
<ThunderboltOutlined style={{ color: '#6366f1', fontSize: 16 }} />
|
|
<Typography.Text strong style={{ fontSize: 16 }}>积分明细</Typography.Text>
|
|
</div>
|
|
<div style={{ borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }}>
|
|
<Table columns={columns} dataSource={records} rowKey="id" loading={loading}
|
|
pagination={{ pageSize: 10, size: 'small' }}
|
|
scroll={{ x: 500 }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CreditsPage;
|