416 lines
13 KiB
TypeScript
416 lines
13 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Card, Col, Row, Typography, DatePicker, Button, Space } from 'antd';
|
|
import {
|
|
UserOutlined,
|
|
ProjectOutlined,
|
|
PlayCircleOutlined,
|
|
FileTextOutlined,
|
|
DollarOutlined,
|
|
WalletOutlined,
|
|
ArrowUpOutlined,
|
|
CalendarOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getAdminStats, getSystemConfigs } from '../api';
|
|
import type { AdminStats, SystemConfig } from '../types';
|
|
import dayjs from 'dayjs';
|
|
import 'dayjs/locale/zh-cn';
|
|
|
|
dayjs.locale('zh-cn');
|
|
|
|
const AdminDashboard: React.FC = () => {
|
|
const [stats, setStats] = useState<AdminStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [startDate, setStartDate] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([dayjs().startOf('day'), dayjs()]);
|
|
const [siteName, setSiteName] = useState<string>('数据概览');
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const start = startDate[0]?.format('YYYY-MM-DD') || undefined;
|
|
const end = startDate[1]?.format('YYYY-MM-DD') || undefined;
|
|
const data = await getAdminStats(start, end);
|
|
setStats(data);
|
|
} catch { /* auth error handled by client */ }
|
|
setLoading(false);
|
|
};
|
|
|
|
const loadSiteName = async () => {
|
|
try {
|
|
const configs = await getSystemConfigs();
|
|
const siteConfig = configs.find((c: SystemConfig) => c.key === 'site_name');
|
|
if (siteConfig) {
|
|
const title = `${siteConfig.value} 管理后台`;
|
|
setSiteName(title);
|
|
document.title = title;
|
|
}
|
|
} catch {
|
|
setSiteName('数据概览');
|
|
document.title = '数据概览';
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
load();
|
|
loadSiteName();
|
|
}, []);
|
|
|
|
const handleToday = () => {
|
|
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [dayjs().startOf('day'), dayjs()];
|
|
setStartDate(dates);
|
|
loadWithDates(dates);
|
|
};
|
|
|
|
const handleYesterday = () => {
|
|
const yesterday = dayjs().subtract(1, 'day');
|
|
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [yesterday.startOf('day'), yesterday.endOf('day')];
|
|
setStartDate(dates);
|
|
loadWithDates(dates);
|
|
};
|
|
|
|
const handleWeek = () => {
|
|
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [dayjs().startOf('week'), dayjs()];
|
|
setStartDate(dates);
|
|
loadWithDates(dates);
|
|
};
|
|
|
|
const handleMonth = () => {
|
|
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [dayjs().startOf('month'), dayjs()];
|
|
setStartDate(dates);
|
|
loadWithDates(dates);
|
|
};
|
|
|
|
const loadWithDates = (dates: [dayjs.Dayjs, dayjs.Dayjs]) => {
|
|
const start = dates[0]?.format('YYYY-MM-DD') || undefined;
|
|
const end = dates[1]?.format('YYYY-MM-DD') || undefined;
|
|
setLoading(true);
|
|
getAdminStats(start, end).then(data => {
|
|
setStats(data);
|
|
setLoading(false);
|
|
}).catch(() => {
|
|
setLoading(false);
|
|
});
|
|
};
|
|
|
|
const handleDateChange = (dates: any) => {
|
|
if (dates) {
|
|
setStartDate([dates[0], dates[1]]);
|
|
loadWithDates([dates[0], dates[1]]);
|
|
}
|
|
};
|
|
|
|
const baseStats = stats ? [
|
|
{
|
|
title: '用户数量',
|
|
value: stats.totalUsers,
|
|
lastPeriodValue: stats.lastPeriodUsers,
|
|
icon: <UserOutlined />,
|
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
prefix: '',
|
|
suffix: '位用户',
|
|
description: '平台注册用户总数'
|
|
},
|
|
{
|
|
title: '总项目数',
|
|
value: stats.totalProjects,
|
|
lastPeriodValue: stats.lastPeriodProjects,
|
|
icon: <ProjectOutlined />,
|
|
gradient: 'linear-gradient(135deg, #00d4ff 0%, #0099cc 100%)',
|
|
prefix: '',
|
|
suffix: '个项目',
|
|
description: '创建的项目总数'
|
|
},
|
|
{
|
|
title: '项目记录',
|
|
value: stats.totalRecords,
|
|
lastPeriodValue: stats.lastPeriodRecords,
|
|
icon: <FileTextOutlined />,
|
|
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
|
prefix: '',
|
|
suffix: '条记录',
|
|
description: '项目记录总数'
|
|
},
|
|
{
|
|
title: '创作记录',
|
|
value: stats.totalGenerations,
|
|
lastPeriodValue: stats.lastPeriodGenerations,
|
|
icon: <PlayCircleOutlined />,
|
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
|
prefix: '',
|
|
suffix: '次创作',
|
|
description: 'AI创作记录总数'
|
|
},
|
|
] : [];
|
|
|
|
const financeStats = stats ? [
|
|
{
|
|
title: '支付宝收入',
|
|
value: stats.todayAlipayRevenue,
|
|
icon: <WalletOutlined />,
|
|
gradient: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
|
|
prefix: '¥',
|
|
suffix: '',
|
|
description: '支付宝收款',
|
|
tag: '支付宝'
|
|
},
|
|
{
|
|
title: '微信收入',
|
|
value: stats.todayWechatRevenue,
|
|
icon: <DollarOutlined />,
|
|
gradient: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
|
prefix: '¥',
|
|
suffix: '',
|
|
description: '微信收款',
|
|
tag: '微信支付'
|
|
},
|
|
{
|
|
title: '总收入',
|
|
value: stats.totalRevenue,
|
|
lastPeriodValue: stats.lastPeriodRevenue,
|
|
icon: <ArrowUpOutlined />,
|
|
gradient: 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)',
|
|
prefix: '¥',
|
|
suffix: '',
|
|
description: '平台总收入',
|
|
tag: '总收入'
|
|
},
|
|
{
|
|
title: '消耗积分',
|
|
value: stats.creditsConsumedToday,
|
|
lastPeriodValue: stats.lastPeriodCreditsConsumed,
|
|
icon: <DollarOutlined />,
|
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
|
prefix: '',
|
|
suffix: '积分',
|
|
description: '用户消耗积分',
|
|
tag: '积分消耗'
|
|
},
|
|
] : [];
|
|
|
|
const StatCard: React.FC<{
|
|
title: string;
|
|
value: number;
|
|
lastPeriodValue?: number;
|
|
icon: React.ReactNode;
|
|
gradient: string;
|
|
prefix?: string;
|
|
suffix?: string;
|
|
description?: string;
|
|
tag?: string;
|
|
}> = ({ title, value, lastPeriodValue, icon, gradient, prefix = '', suffix = '', description, tag }) => {
|
|
const change = lastPeriodValue !== undefined && lastPeriodValue > 0
|
|
? ((value - lastPeriodValue) / lastPeriodValue * 100).toFixed(1)
|
|
: null;
|
|
const isPositive = change !== null && parseFloat(change) >= 0;
|
|
|
|
return (
|
|
<Card
|
|
bordered={false}
|
|
loading={loading}
|
|
hoverable
|
|
style={{
|
|
borderRadius: 16,
|
|
border: '1px solid rgba(0,0,0,0.04)',
|
|
background: '#ffffff',
|
|
boxShadow: '0 4px 20px rgba(0,0,0,0.05)',
|
|
transition: 'all 0.3s ease',
|
|
overflow: 'hidden'
|
|
}}
|
|
>
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 16,
|
|
padding: '16px 0'
|
|
}}>
|
|
<div style={{
|
|
width: 52,
|
|
height: 52,
|
|
borderRadius: 14,
|
|
background: gradient,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: 24,
|
|
color: '#fff',
|
|
flexShrink: 0,
|
|
boxShadow: '0 8px 24px rgba(0,0,0,0.1)',
|
|
}}>
|
|
{icon}
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
marginBottom: 4
|
|
}}>
|
|
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}>{title}</Typography.Text>
|
|
{tag && (
|
|
<span style={{
|
|
fontSize: 10,
|
|
padding: '2px 8px',
|
|
borderRadius: 10,
|
|
background: 'rgba(99,102,241,0.1)',
|
|
color: '#6366f1',
|
|
fontWeight: 500
|
|
}}>
|
|
{tag}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div style={{
|
|
fontSize: 28,
|
|
fontWeight: 800,
|
|
color: '#1e293b',
|
|
letterSpacing: -0.5,
|
|
marginBottom: 2
|
|
}}>
|
|
{prefix}{typeof value === 'number' ? value.toLocaleString() : value}{suffix}
|
|
</div>
|
|
{description && (
|
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 11 }}>
|
|
{description}
|
|
</Typography.Text>
|
|
)}
|
|
{change !== null && lastPeriodValue !== undefined && (
|
|
<div style={{
|
|
marginTop: 8,
|
|
paddingTop: 8,
|
|
borderTop: '1px solid #f1f5f9',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8
|
|
}}>
|
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 11 }}>
|
|
上一周期: {prefix}{lastPeriodValue.toLocaleString()}{suffix}
|
|
</Typography.Text>
|
|
<span style={{
|
|
fontSize: 11,
|
|
fontWeight: 500,
|
|
color: isPositive ? '#10b981' : '#ef4444',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 2
|
|
}}>
|
|
{isPositive ? '↑' : '↓'} {Math.abs(parseFloat(change))}%
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={{ padding: 0 }}>
|
|
<div style={{
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
padding: '24px 24px 32px',
|
|
borderRadius: 0,
|
|
marginBottom: -24,
|
|
position: 'relative',
|
|
overflow: 'hidden'
|
|
}}>
|
|
<div style={{
|
|
position: 'absolute',
|
|
top: -50,
|
|
right: -50,
|
|
width: 200,
|
|
height: 200,
|
|
background: 'rgba(255,255,255,0.1)',
|
|
borderRadius: '50%'
|
|
}} />
|
|
<div style={{
|
|
position: 'absolute',
|
|
bottom: -30,
|
|
left: -30,
|
|
width: 150,
|
|
height: 150,
|
|
background: 'rgba(255,255,255,0.08)',
|
|
borderRadius: '50%'
|
|
}} />
|
|
<div style={{ position: 'relative', zIndex: 1 }}>
|
|
<Typography.Title level={2} style={{ color: '#fff', marginBottom: 4, fontWeight: 700 }}>
|
|
{siteName}
|
|
</Typography.Title>
|
|
<Typography.Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: 14 }}>
|
|
欢迎回来,查看平台运营数据统计
|
|
</Typography.Text>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ marginTop: 40 }}>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 16 }}>
|
|
<Space>
|
|
<Button
|
|
type={!startDate[0] || !startDate[1] || startDate[0]?.isSame(dayjs().startOf('day')) && startDate[1]?.isSame(dayjs(), 'day') ? 'primary' : 'default'}
|
|
size="small"
|
|
onClick={handleToday}
|
|
>
|
|
今日
|
|
</Button>
|
|
<Button
|
|
type="default"
|
|
size="small"
|
|
onClick={handleYesterday}
|
|
>
|
|
昨日
|
|
</Button>
|
|
<Button
|
|
type="default"
|
|
size="small"
|
|
onClick={handleWeek}
|
|
>
|
|
本周
|
|
</Button>
|
|
<Button
|
|
type="default"
|
|
size="small"
|
|
onClick={handleMonth}
|
|
>
|
|
本月
|
|
</Button>
|
|
</Space>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: '#fff', border: '1px solid #e2e8f0', padding: '6px 12px', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
|
<CalendarOutlined style={{ color: '#64748b', fontSize: 14 }} />
|
|
<DatePicker.RangePicker
|
|
value={startDate}
|
|
onChange={handleDateChange}
|
|
placeholder={['开始日期', '结束日期']}
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div style={{ marginBottom: 16, paddingLeft: 4 }}>
|
|
<Typography.Text strong style={{ color: '#1e293b', fontSize: 15 }}>核心数据</Typography.Text>
|
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 8 }}>平台基础运营指标</Typography.Text>
|
|
</div>
|
|
<Row gutter={[16, 16]}>
|
|
{baseStats.map((s) => (
|
|
<Col xs={12} sm={8} lg={6} key={s.title}>
|
|
<StatCard {...s} />
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</div>
|
|
|
|
<div style={{ marginTop: 24 }}>
|
|
<div style={{ marginBottom: 16, paddingLeft: 4 }}>
|
|
<Typography.Text strong style={{ color: '#1e293b', fontSize: 15 }}>财务统计</Typography.Text>
|
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 8 }}>收入与消耗数据</Typography.Text>
|
|
</div>
|
|
<Row gutter={[16, 16]}>
|
|
{financeStats.map((s) => (
|
|
<Col xs={12} sm={8} lg={6} key={s.title}>
|
|
<StatCard {...s} />
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminDashboard;
|