This commit is contained in:
2026-06-15 16:42:33 +08:00
parent e1e9bf9aca
commit 937ffc4320
6 changed files with 213 additions and 82 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VideoGen.AI 管理后台</title>
<script type="module" crossorigin src="/assets/index-CshLDvYK.js"></script>
<script type="module" crossorigin src="/assets/index-C93H_elA.js"></script>
</head>
<body>
<div id="root"></div>
+129 -72
View File
@@ -39,10 +39,13 @@ const AdminDashboard: React.FC = () => {
const configs = await getSystemConfigs();
const siteConfig = configs.find((c: SystemConfig) => c.key === 'site_name');
if (siteConfig) {
setSiteName(`${siteConfig.value} 管理后台`);
const title = `${siteConfig.value} 管理后台`;
setSiteName(title);
document.title = title;
}
} catch {
setSiteName('数据概览');
document.title = '数据概览';
}
};
@@ -52,30 +55,46 @@ const AdminDashboard: React.FC = () => {
}, []);
const handleToday = () => {
setStartDate([dayjs().startOf('day'), dayjs()]);
load();
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [dayjs().startOf('day'), dayjs()];
setStartDate(dates);
loadWithDates(dates);
};
const handleYesterday = () => {
const yesterday = dayjs().subtract(1, 'day');
setStartDate([yesterday.startOf('day'), yesterday.endOf('day')]);
load();
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [yesterday.startOf('day'), yesterday.endOf('day')];
setStartDate(dates);
loadWithDates(dates);
};
const handleWeek = () => {
setStartDate([dayjs().startOf('week'), dayjs()]);
load();
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [dayjs().startOf('week'), dayjs()];
setStartDate(dates);
loadWithDates(dates);
};
const handleMonth = () => {
setStartDate([dayjs().startOf('month'), dayjs()]);
load();
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]]);
load();
loadWithDates([dates[0], dates[1]]);
}
};
@@ -83,6 +102,7 @@ const AdminDashboard: React.FC = () => {
{
title: '用户数量',
value: stats.totalUsers,
lastPeriodValue: stats.lastPeriodUsers,
icon: <UserOutlined />,
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
prefix: '',
@@ -92,6 +112,7 @@ const AdminDashboard: React.FC = () => {
{
title: '总项目数',
value: stats.totalProjects,
lastPeriodValue: stats.lastPeriodProjects,
icon: <ProjectOutlined />,
gradient: 'linear-gradient(135deg, #00d4ff 0%, #0099cc 100%)',
prefix: '',
@@ -101,6 +122,7 @@ const AdminDashboard: React.FC = () => {
{
title: '项目记录',
value: stats.totalRecords,
lastPeriodValue: stats.lastPeriodRecords,
icon: <FileTextOutlined />,
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
prefix: '',
@@ -110,6 +132,7 @@ const AdminDashboard: React.FC = () => {
{
title: '创作记录',
value: stats.totalGenerations,
lastPeriodValue: stats.lastPeriodGenerations,
icon: <PlayCircleOutlined />,
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
prefix: '',
@@ -142,6 +165,7 @@ const AdminDashboard: React.FC = () => {
{
title: '总收入',
value: stats.totalRevenue,
lastPeriodValue: stats.lastPeriodRevenue,
icon: <ArrowUpOutlined />,
gradient: 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)',
prefix: '¥',
@@ -152,6 +176,7 @@ const AdminDashboard: React.FC = () => {
{
title: '消耗积分',
value: stats.creditsConsumedToday,
lastPeriodValue: stats.lastPeriodCreditsConsumed,
icon: <DollarOutlined />,
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
prefix: '',
@@ -164,86 +189,118 @@ const AdminDashboard: React.FC = () => {
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, icon, gradient, prefix = '', suffix = '', description, tag }) => (
<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'
}}>
}> = ({ 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={{
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)',
gap: 16,
padding: '16px 0'
}}>
{icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
width: 52,
height: 52,
borderRadius: 14,
background: gradient,
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 4
justifyContent: 'center',
fontSize: 24,
color: '#fff',
flexShrink: 0,
boxShadow: '0 8px 24px rgba(0,0,0,0.1)',
}}>
<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
{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
}}>
{tag}
</span>
<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 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>
)}
</div>
</div>
</Card>
);
</Card>
);
};
return (
<div style={{ padding: 0 }}>
+6
View File
@@ -118,6 +118,12 @@ export interface AdminStats {
creditsConsumedToday: number;
todayAlipayRevenue: number;
todayWechatRevenue: number;
lastPeriodUsers: number;
lastPeriodProjects: number;
lastPeriodGenerations: number;
lastPeriodRecords: number;
lastPeriodRevenue: number;
lastPeriodCreditsConsumed: number;
}
export interface PaymentStats {
+66 -4
View File
@@ -1147,10 +1147,6 @@ async def get_stats(
start_date: str = Query(None),
end_date: str = Query(None),
):
total_users = (await db.execute(
select(func.count(User.id)).where(User.user_type == "frontend")
)).scalar() or 0
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
try:
@@ -1167,6 +1163,14 @@ async def get_stats(
date_start = today_start
date_end = datetime.now()
total_users = (await db.execute(
select(func.count(User.id)).where(
User.user_type == "frontend",
User.created_at >= date_start,
User.created_at <= date_end,
)
)).scalar() or 0
total_projects = (await db.execute(
select(func.count(Project.id)).where(
Project.deleted_at.is_(None),
@@ -1224,6 +1228,58 @@ async def get_stats(
)
)).scalar() or 0
period_duration = date_end - date_start
last_period_start = date_start - period_duration
last_period_end = date_start
last_period_users = (await db.execute(
select(func.count(User.id)).where(
User.user_type == "frontend",
User.created_at >= last_period_start,
User.created_at <= last_period_end,
)
)).scalar() or 0
last_period_projects = (await db.execute(
select(func.count(Project.id)).where(
Project.deleted_at.is_(None),
Project.created_at >= last_period_start,
Project.created_at <= last_period_end,
)
)).scalar() or 0
last_period_generations = (await db.execute(
select(func.count(ChatGenerationTask.id)).where(
ChatGenerationTask.created_at >= last_period_start,
ChatGenerationTask.created_at <= last_period_end,
)
)).scalar() or 0
last_period_records = (await db.execute(
select(func.count(GenerationRecord.id)).where(
GenerationRecord.deleted_at.is_(None),
GenerationRecord.created_at >= last_period_start,
GenerationRecord.created_at <= last_period_end,
)
)).scalar() or 0
last_period_revenue = (await db.execute(
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
PaymentOrder.status == "paid",
PaymentOrder.created_at >= last_period_start,
PaymentOrder.created_at <= last_period_end,
)
)).scalar() or 0
last_period_credits_consumed = (await db.execute(
select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
CreditRecord.type == "consume",
CreditRecord.created_at >= last_period_start,
CreditRecord.created_at <= last_period_end,
)
)).scalar() or 0
return AdminStatsOut(
total_users=total_users,
total_projects=total_projects,
@@ -1233,6 +1289,12 @@ async def get_stats(
credits_consumed_today=float(credits_consumed),
today_alipay_revenue=float(alipay_revenue),
today_wechat_revenue=float(wechat_revenue),
last_period_users=last_period_users,
last_period_projects=last_period_projects,
last_period_generations=last_period_generations,
last_period_records=last_period_records,
last_period_revenue=float(last_period_revenue),
last_period_credits_consumed=float(last_period_credits_consumed),
)
+6
View File
@@ -98,3 +98,9 @@ class AdminStatsOut(BaseModel):
credits_consumed_today: float
today_alipay_revenue: float
today_wechat_revenue: float
last_period_users: int = 0
last_period_projects: int = 0
last_period_generations: int = 0
last_period_records: int = 0
last_period_revenue: float = 0.0
last_period_credits_consumed: float = 0.0