1
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}}>
|
||||
<div style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 14,
|
||||
background: gradient,
|
||||
display: 'flex',
|
||||
}> = ({ 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',
|
||||
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={{
|
||||
display: 'flex',
|
||||
<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 }}>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user