Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
Vendored
+56
-56
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-CvhbmotR.js"></script>
|
<script type="module" crossorigin src="/assets/index-Dhv0zQu5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Col, Row, Typography, DatePicker, Button, Space } from 'antd';
|
import { Card, Col, Row, Typography, DatePicker, Button, Space, Spin } from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined, ProjectOutlined, PlayCircleOutlined, FileTextOutlined,
|
||||||
ProjectOutlined,
|
DollarOutlined, WalletOutlined, ArrowUpOutlined, CalendarOutlined,
|
||||||
PlayCircleOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
DollarOutlined,
|
|
||||||
WalletOutlined,
|
|
||||||
ArrowUpOutlined,
|
|
||||||
CalendarOutlined,
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getAdminStats, getSystemConfigs } from '../api';
|
import { getAdminStats, getSystemConfigs } from '../api';
|
||||||
import type { AdminStats, SystemConfig } from '../types';
|
import type { AdminStats, SystemConfig } from '../types';
|
||||||
@@ -17,10 +11,25 @@ import 'dayjs/locale/zh-cn';
|
|||||||
|
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
const MODULE_LABELS: Record<string, string> = {
|
||||||
|
'ai_creation': 'AI创作',
|
||||||
|
'generation_record': '项目生成',
|
||||||
|
'hot_opening_replicate': '爆款开头复刻',
|
||||||
|
'shot_replicate': '拆镜复刻',
|
||||||
|
'payment': '支付充值',
|
||||||
|
'admin': '后台管理',
|
||||||
|
'team': '团队管理',
|
||||||
|
'unknown': '历史未知',
|
||||||
|
'other': '其他',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLORS = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#3b82f6', '#ec4899', '#14b8a6', '#f97316'];
|
||||||
|
|
||||||
const AdminDashboard: React.FC = () => {
|
const AdminDashboard: React.FC = () => {
|
||||||
const [stats, setStats] = useState<AdminStats | null>(null);
|
const [stats, setStats] = useState<AdminStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [startDate, setStartDate] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([dayjs().startOf('day'), dayjs()]);
|
const [startDate, setStartDate] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([dayjs().startOf('day'), dayjs()]);
|
||||||
|
const [activeRange, setActiveRange] = useState<string>('today');
|
||||||
const [siteName, setSiteName] = useState<string>('数据概览');
|
const [siteName, setSiteName] = useState<string>('数据概览');
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@@ -39,377 +48,254 @@ const AdminDashboard: React.FC = () => {
|
|||||||
const configs = await getSystemConfigs();
|
const configs = await getSystemConfigs();
|
||||||
const siteConfig = configs.find((c: SystemConfig) => c.key === 'site_name');
|
const siteConfig = configs.find((c: SystemConfig) => c.key === 'site_name');
|
||||||
if (siteConfig) {
|
if (siteConfig) {
|
||||||
const title = `${siteConfig.value} 管理后台`;
|
setSiteName(`${siteConfig.value} 管理后台`);
|
||||||
setSiteName(title);
|
document.title = `${siteConfig.value} 管理后台`;
|
||||||
document.title = title;
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch { /* ignore */ }
|
||||||
setSiteName('数据概览');
|
|
||||||
document.title = '数据概览';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { load(); loadSiteName(); }, []);
|
||||||
load();
|
|
||||||
loadSiteName();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleToday = () => {
|
const loadWithDates = (dates: [dayjs.Dayjs, dayjs.Dayjs], range?: string) => {
|
||||||
const dates: [dayjs.Dayjs, dayjs.Dayjs] = [dayjs().startOf('day'), dayjs()];
|
|
||||||
setStartDate(dates);
|
setStartDate(dates);
|
||||||
loadWithDates(dates);
|
if (range) setActiveRange(range);
|
||||||
};
|
|
||||||
|
|
||||||
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 start = dates[0]?.format('YYYY-MM-DD') || undefined;
|
||||||
const end = dates[1]?.format('YYYY-MM-DD') || undefined;
|
const end = dates[1]?.format('YYYY-MM-DD') || undefined;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
getAdminStats(start, end).then(data => {
|
getAdminStats(start, end).then(data => { setStats(data); setLoading(false); }).catch(() => setLoading(false));
|
||||||
setStats(data);
|
|
||||||
setLoading(false);
|
|
||||||
}).catch(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToday = () => loadWithDates([dayjs().startOf('day'), dayjs()], 'today');
|
||||||
|
const handleYesterday = () => { const y = dayjs().subtract(1, 'day'); loadWithDates([y.startOf('day'), y.endOf('day')], 'yesterday'); };
|
||||||
|
const handleWeek = () => loadWithDates([dayjs().startOf('week'), dayjs()], 'week');
|
||||||
|
const handleMonth = () => loadWithDates([dayjs().startOf('month'), dayjs()], 'month');
|
||||||
|
|
||||||
const handleDateChange = (dates: any) => {
|
const handleDateChange = (dates: any) => {
|
||||||
if (dates) {
|
if (dates) { setActiveRange(''); loadWithDates([dates[0], dates[1]]); }
|
||||||
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 (
|
return (
|
||||||
<div style={{ padding: 0 }}>
|
<div>
|
||||||
<div style={{
|
{/* 日期筛选 */}
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 20 }}>
|
||||||
padding: '24px 24px 32px',
|
<Space>
|
||||||
borderRadius: 0,
|
<Button type={activeRange === 'today' ? 'primary' : 'default'} size="small" onClick={handleToday}>今日</Button>
|
||||||
marginBottom: -24,
|
<Button type={activeRange === 'yesterday' ? 'primary' : 'default'} size="small" onClick={handleYesterday}>昨日</Button>
|
||||||
position: 'relative',
|
<Button type={activeRange === 'week' ? 'primary' : 'default'} size="small" onClick={handleWeek}>本周</Button>
|
||||||
overflow: 'hidden'
|
<Button type={activeRange === 'month' ? 'primary' : 'default'} size="small" onClick={handleMonth}>本月</Button>
|
||||||
}}>
|
</Space>
|
||||||
<div style={{
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: '#fff', border: '1px solid #e2e8f0', padding: '4px 12px', borderRadius: 8 }}>
|
||||||
position: 'absolute',
|
<CalendarOutlined style={{ color: '#64748b' }} />
|
||||||
top: -50,
|
<DatePicker.RangePicker value={startDate} onChange={handleDateChange} size="small" />
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 40 }}>
|
{/* 核心数据 */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 16 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<Space>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Button
|
<Typography.Text strong style={{ fontSize: 15 }}>核心数据</Typography.Text>
|
||||||
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>
|
||||||
<div style={{ marginBottom: 16, paddingLeft: 4 }}>
|
<Row gutter={[12, 12]}>
|
||||||
<Typography.Text strong style={{ color: '#1e293b', fontSize: 15 }}>核心数据</Typography.Text>
|
{[
|
||||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 8 }}>平台基础运营指标</Typography.Text>
|
{ title: '新增用户数量', value: stats?.totalUsers, icon: <UserOutlined />, color: '#6366f1' },
|
||||||
</div>
|
{ title: '总收入', value: stats?.totalRevenue, icon: <WalletOutlined />, color: '#ec4899', prefix: '¥' },
|
||||||
<Row gutter={[16, 16]}>
|
{ title: '消耗积分', value: stats?.creditsConsumedToday, icon: <DollarOutlined />, color: '#ef4444' },
|
||||||
{baseStats.map((s) => (
|
].map(s => (
|
||||||
<Col xs={12} sm={8} lg={6} key={s.title}>
|
<Col xs={12} sm={8} md={4} key={s.title}>
|
||||||
<StatCard {...s} />
|
<CompactStatCard {...s} loading={loading} />
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 24 }}>
|
{/* 图表区域 */}
|
||||||
<div style={{ marginBottom: 16, paddingLeft: 4 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<Typography.Text strong style={{ color: '#1e293b', fontSize: 15 }}>财务统计</Typography.Text>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 8 }}>收入与消耗数据</Typography.Text>
|
<Typography.Text strong style={{ fontSize: 15 }}>数据统计</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{financeStats.map((s) => (
|
<Col xs={24} lg={12}>
|
||||||
<Col xs={12} sm={8} lg={6} key={s.title}>
|
<ChartCard title="每日积分消耗趋势" loading={loading}>
|
||||||
<StatCard {...s} />
|
<LineChart data={stats?.dailyCreditsByModule || []} />
|
||||||
</Col>
|
</ChartCard>
|
||||||
))}
|
</Col>
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<ChartCard title="各模块积分占比" loading={loading}>
|
||||||
|
<ModulePie data={stats?.periodCreditsByModule || []} />
|
||||||
|
</ChartCard>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<ChartCard title="团队积分消耗排行" loading={loading}>
|
||||||
|
<HorizontalBarChart data={(stats?.creditsByTeam || []).slice(0, 8)} />
|
||||||
|
</ChartCard>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<ChartCard title="模型使用次数" loading={loading}>
|
||||||
|
<HorizontalBarChart data={(stats?.modelUsage || []).slice(0, 8)} valueKey="count" />
|
||||||
|
</ChartCard>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── 图表卡片 ──
|
||||||
|
const ChartCard: React.FC<{ title: string; loading: boolean; children: React.ReactNode }> = ({ title, loading, children }) => (
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', height: '100%' }}
|
||||||
|
styles={{ body: { padding: '16px' } }}>
|
||||||
|
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Typography.Text strong style={{ fontSize: 14 }}>{title}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Spin spinning={loading}>{children}</Spin>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 核心数据小卡片 ──
|
||||||
|
const CompactStatCard: React.FC<{ title: string; value?: number; icon: React.ReactNode; color: string; prefix?: string; loading: boolean }> = ({ title, value, icon, color, prefix = '', loading }) => (
|
||||||
|
<Card bordered={false} loading={loading} style={{ borderRadius: 10, border: '1px solid #f0f0f5' }}
|
||||||
|
styles={{ body: { padding: '12px 14px' } }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<div style={{ width: 34, height: 34, borderRadius: 8, background: `${color}15`, display: 'flex', alignItems: 'center', justifyContent: 'center', color, fontSize: 16 }}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 11 }}>{title}</Typography.Text>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 700, color: '#1e293b', lineHeight: 1.3 }}>
|
||||||
|
{prefix}{typeof value === 'number' ? value.toLocaleString() : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 折线图(按日期+模块,固定展示选中日期往前7天)──
|
||||||
|
const LineChart: React.FC<{ data: { date: string; module: string; credits: number }[] }> = ({ data }) => {
|
||||||
|
// 以数据中最新日期为基准,往前推 7 天;不足 7 天按实际天数
|
||||||
|
if (!data.length) return <EmptyChart />;
|
||||||
|
const sortedDates = Array.from(new Set(data.map(d => d.date))).sort();
|
||||||
|
const maxDate = sortedDates[sortedDates.length - 1];
|
||||||
|
// 生成 [maxDate-6, maxDate] 共 7 天
|
||||||
|
const baseDayjs = dayjs(maxDate);
|
||||||
|
const sevenDays: string[] = [];
|
||||||
|
for (let i = 6; i >= 0; i--) sevenDays.push(baseDayjs.subtract(i, 'day').format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
const dateMap = new Map<string, number>();
|
||||||
|
data.forEach(d => { dateMap.set(d.date, (dateMap.get(d.date) || 0) + d.credits); });
|
||||||
|
const maxVal = Math.max(...sevenDays.map(d => dateMap.get(d) || 0), 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: 220, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<div style={{ flex: 1, display: 'flex', alignItems: 'flex-end', gap: 4, borderBottom: '1px solid #f1f5f9', paddingBottom: 4 }}>
|
||||||
|
{sevenDays.map(d => {
|
||||||
|
const val = dateMap.get(d) || 0;
|
||||||
|
const pct = (val / maxVal) * 100;
|
||||||
|
return (
|
||||||
|
<div key={d} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%', justifyContent: 'flex-end' }}>
|
||||||
|
<span style={{ fontSize: 9, color: '#6366f1', fontWeight: 600, marginBottom: 2 }}>{val > 0 ? val.toFixed(0) : ''}</span>
|
||||||
|
<div style={{ width: '65%', maxWidth: 32, height: `${Math.max(pct, 2)}%`, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: '3px 3px 0 0', minHeight: 3, transition: 'height 0.3s' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
|
||||||
|
{sevenDays.map(d => (
|
||||||
|
<div key={d} style={{ flex: 1, textAlign: 'center' }}>
|
||||||
|
<span style={{ fontSize: 9, color: '#94a3b8' }}>{d.slice(5)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 模块积分占比(饼图)──
|
||||||
|
const ModulePie: React.FC<{ data: { module: string; credits: number }[] }> = ({ data }) => {
|
||||||
|
if (!data.length) return <EmptyChart />;
|
||||||
|
const moduleMap = new Map<string, number>();
|
||||||
|
data.forEach(d => { moduleMap.set(d.module, (moduleMap.get(d.module) || 0) + d.credits); });
|
||||||
|
const modules = Array.from(moduleMap.entries()).sort((a, b) => b[1] - a[1]);
|
||||||
|
const total = modules.reduce((s, [, v]) => s + v, 0) || 1;
|
||||||
|
|
||||||
|
// 计算饼图扇形路径
|
||||||
|
const size = 160;
|
||||||
|
const cx = size / 2;
|
||||||
|
const cy = size / 2;
|
||||||
|
const r = 68;
|
||||||
|
let cumAngle = -90; // 从顶部开始
|
||||||
|
|
||||||
|
const slices = modules.map(([mod, val], i) => {
|
||||||
|
const pct = val / total;
|
||||||
|
const angle = pct * 360;
|
||||||
|
const startAngle = cumAngle;
|
||||||
|
cumAngle += angle;
|
||||||
|
const endAngle = cumAngle;
|
||||||
|
const startRad = (startAngle * Math.PI) / 180;
|
||||||
|
const endRad = (endAngle * Math.PI) / 180;
|
||||||
|
const largeArc = angle > 180 ? 1 : 0;
|
||||||
|
const x1 = cx + r * Math.cos(startRad);
|
||||||
|
const y1 = cy + r * Math.sin(startRad);
|
||||||
|
const x2 = cx + r * Math.cos(endRad);
|
||||||
|
const y2 = cy + r * Math.sin(endRad);
|
||||||
|
const d = `M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${largeArc} 1 ${x2},${y2} Z`;
|
||||||
|
return { d, color: COLORS[i % COLORS.length], label: MODULE_LABELS[mod] || mod, val, pct };
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: 220, display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ flexShrink: 0 }}>
|
||||||
|
{slices.map((s, i) => (
|
||||||
|
<path key={i} d={s.d} fill={s.color} stroke="#fff" strokeWidth={2} />
|
||||||
|
))}
|
||||||
|
<circle cx={cx} cy={cy} r={36} fill="#fff" />
|
||||||
|
<text x={cx} y={cy - 4} textAnchor="middle" fontSize={11} fill="#64748b">总计</text>
|
||||||
|
<text x={cx} y={cy + 12} textAnchor="middle" fontSize={13} fontWeight={700} fill="#1e293b">{total.toFixed(0)}</text>
|
||||||
|
</svg>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
{slices.map((s, i) => (
|
||||||
|
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#64748b' }}>
|
||||||
|
<div style={{ width: 10, height: 10, borderRadius: 3, background: s.color, flexShrink: 0 }} />
|
||||||
|
<span>{s.label}</span>
|
||||||
|
<span style={{ fontWeight: 600, color: '#1e293b' }}>{s.val.toFixed(0)}</span>
|
||||||
|
<span style={{ fontSize: 10 }}>{(s.pct * 100).toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 横向柱状图(团队/模型)──
|
||||||
|
const HorizontalBarChart: React.FC<{ data: { teamName?: string; modelName?: string; credits?: number; count?: number }[]; valueKey?: string }> = ({ data, valueKey = 'credits' }) => {
|
||||||
|
if (!data.length) return <EmptyChart />;
|
||||||
|
const maxVal = Math.max(...data.map(d => (d as any)[valueKey] || 0), 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: 220, display: 'flex', flexDirection: 'column', gap: 8, overflowY: 'auto' }}>
|
||||||
|
{data.map((d, i) => {
|
||||||
|
const label = d.teamName || d.modelName || '-';
|
||||||
|
const val = (d as any)[valueKey] || 0;
|
||||||
|
const pct = (val / maxVal) * 100;
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#475569', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '70%' }}>{label}</span>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 600, color: '#1e293b' }}>{val.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ height: 16, background: '#f1f5f9', borderRadius: 4, overflow: 'hidden' }}>
|
||||||
|
<div style={{ height: '100%', width: `${pct}%`, background: `linear-gradient(90deg, ${COLORS[i % COLORS.length]}, ${COLORS[(i + 1) % COLORS.length]})`, borderRadius: 4, transition: 'width 0.3s' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyChart = () => (
|
||||||
|
<div style={{ height: 220, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}>
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default AdminDashboard;
|
export default AdminDashboard;
|
||||||
|
|||||||
@@ -233,15 +233,15 @@ const AdminLayout: React.FC = () => {
|
|||||||
width: 42,
|
width: 42,
|
||||||
height: 42,
|
height: 42,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)',
|
background: '#ffffff',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)',
|
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}>
|
}}>
|
||||||
{siteLogo ? (
|
{siteLogo ? (
|
||||||
<img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} />
|
<img src={siteLogo} alt="logo" style={{ width: 36, height: 36, objectFit: 'contain' }} />
|
||||||
) : (
|
) : (
|
||||||
<ThunderboltOutlined style={{ fontSize: 20, color: '#ffffff' }} />
|
<ThunderboltOutlined style={{ fontSize: 20, color: '#ffffff' }} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -199,6 +199,24 @@ export interface AdminUser {
|
|||||||
privatePortraitAssetLimit: number;
|
privatePortraitAssetLimit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DailyCredit {
|
||||||
|
date: string;
|
||||||
|
module: string;
|
||||||
|
credits: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamCredit {
|
||||||
|
teamName: string;
|
||||||
|
teamId: string | null;
|
||||||
|
credits: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelUsageOut {
|
||||||
|
modelName: string;
|
||||||
|
provider: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminStats {
|
export interface AdminStats {
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
totalProjects: number;
|
totalProjects: number;
|
||||||
@@ -214,6 +232,10 @@ export interface AdminStats {
|
|||||||
lastPeriodRecords: number;
|
lastPeriodRecords: number;
|
||||||
lastPeriodRevenue: number;
|
lastPeriodRevenue: number;
|
||||||
lastPeriodCreditsConsumed: number;
|
lastPeriodCreditsConsumed: number;
|
||||||
|
dailyCreditsByModule: DailyCredit[];
|
||||||
|
periodCreditsByModule: DailyCredit[];
|
||||||
|
creditsByTeam: TeamCredit[];
|
||||||
|
modelUsage: ModelUsageOut[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentStats {
|
export interface PaymentStats {
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ from app.schemas.admin import (
|
|||||||
SystemConfigOut,
|
SystemConfigOut,
|
||||||
AdminUserOut,
|
AdminUserOut,
|
||||||
AdminStatsOut,
|
AdminStatsOut,
|
||||||
|
DailyCreditOut,
|
||||||
|
TeamCreditOut,
|
||||||
|
ModelUsageOut,
|
||||||
CreateUserRequest,
|
CreateUserRequest,
|
||||||
UpdateMenusRequest,
|
UpdateMenusRequest,
|
||||||
ResetPasswordRequest,
|
ResetPasswordRequest,
|
||||||
@@ -1850,6 +1853,103 @@ async def get_stats(
|
|||||||
)
|
)
|
||||||
)).scalar() or 0
|
)).scalar() or 0
|
||||||
|
|
||||||
|
# ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示)
|
||||||
|
from sqlalchemy import Date, cast as sa_cast
|
||||||
|
_day_expr = sa_cast(CreditRecord.created_at, Date)
|
||||||
|
# 图表固定展示 [date_end - 6天, date_end] 共7天
|
||||||
|
_chart_end_dt = date_end
|
||||||
|
_chart_start_dt = _chart_end_dt - timedelta(days=6)
|
||||||
|
_inner = (
|
||||||
|
select(
|
||||||
|
_day_expr.label('date'),
|
||||||
|
CreditRecord.source_module.label('module'),
|
||||||
|
func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0).label('credits'),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
CreditRecord.type == "consume",
|
||||||
|
CreditRecord.created_at >= _chart_start_dt,
|
||||||
|
CreditRecord.created_at <= _chart_end_dt,
|
||||||
|
)
|
||||||
|
.group_by(_day_expr, CreditRecord.source_module)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
daily_credits_rows = (await db.execute(
|
||||||
|
select(
|
||||||
|
_inner.c.date,
|
||||||
|
func.coalesce(_inner.c.module, 'other').label('module'),
|
||||||
|
_inner.c.credits,
|
||||||
|
).order_by(_inner.c.date)
|
||||||
|
)).all()
|
||||||
|
daily_credits_by_module = [
|
||||||
|
DailyCreditOut(date=str(row.date), module=row.module, credits=float(row.credits or 0))
|
||||||
|
for row in daily_credits_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── 选中周期内各模块积分占比(按 source_module 分组,不拆日期)
|
||||||
|
_period_inner = (
|
||||||
|
select(
|
||||||
|
CreditRecord.source_module.label('module'),
|
||||||
|
func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0).label('credits'),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
CreditRecord.type == "consume",
|
||||||
|
CreditRecord.created_at >= date_start,
|
||||||
|
CreditRecord.created_at <= date_end,
|
||||||
|
)
|
||||||
|
.group_by(CreditRecord.source_module)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
period_credits_rows = (await db.execute(
|
||||||
|
select(
|
||||||
|
func.coalesce(_period_inner.c.module, 'other').label('module'),
|
||||||
|
_period_inner.c.credits,
|
||||||
|
).order_by(_period_inner.c.credits.desc())
|
||||||
|
)).all()
|
||||||
|
period_credits_by_module = [
|
||||||
|
DailyCreditOut(date='', module=row.module, credits=float(row.credits or 0))
|
||||||
|
for row in period_credits_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── 各团队积分消耗(有团队 vs 无团队,使用流水中的团队快照)
|
||||||
|
team_credit_rows = (await db.execute(
|
||||||
|
select(
|
||||||
|
func.coalesce(CreditRecord.team_name_snapshot, '未分配团队').label('team_name'),
|
||||||
|
CreditRecord.team_id_snapshot.label('team_id'),
|
||||||
|
func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0).label('credits'),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
CreditRecord.type == "consume",
|
||||||
|
CreditRecord.created_at >= date_start,
|
||||||
|
CreditRecord.created_at <= date_end,
|
||||||
|
)
|
||||||
|
.group_by(CreditRecord.team_id_snapshot, CreditRecord.team_name_snapshot)
|
||||||
|
.order_by(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0).desc())
|
||||||
|
)).all()
|
||||||
|
credits_by_team = [
|
||||||
|
TeamCreditOut(team_name=row.team_name, team_id=row.team_id, credits=float(row.credits or 0))
|
||||||
|
for row in team_credit_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── 各模型使用次数(通过 engine 快照字段统计)
|
||||||
|
model_usage_rows = (await db.execute(
|
||||||
|
select(
|
||||||
|
func.coalesce(CreditRecord.engine_name, '未知').label('model_name'),
|
||||||
|
func.coalesce(CreditRecord.engine_provider, 'unknown').label('provider'),
|
||||||
|
func.count(CreditRecord.id).label('count'),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
CreditRecord.type == "consume",
|
||||||
|
CreditRecord.created_at >= date_start,
|
||||||
|
CreditRecord.created_at <= date_end,
|
||||||
|
)
|
||||||
|
.group_by(CreditRecord.engine_name, CreditRecord.engine_provider)
|
||||||
|
.order_by(func.count(CreditRecord.id).desc())
|
||||||
|
)).all()
|
||||||
|
model_usage = [
|
||||||
|
ModelUsageOut(model_name=row.model_name, provider=row.provider, count=int(row.count or 0))
|
||||||
|
for row in model_usage_rows
|
||||||
|
]
|
||||||
|
|
||||||
return AdminStatsOut(
|
return AdminStatsOut(
|
||||||
total_users=total_users,
|
total_users=total_users,
|
||||||
total_projects=total_projects,
|
total_projects=total_projects,
|
||||||
@@ -1865,6 +1965,10 @@ async def get_stats(
|
|||||||
last_period_records=last_period_records,
|
last_period_records=last_period_records,
|
||||||
last_period_revenue=float(last_period_revenue),
|
last_period_revenue=float(last_period_revenue),
|
||||||
last_period_credits_consumed=float(last_period_credits_consumed),
|
last_period_credits_consumed=float(last_period_credits_consumed),
|
||||||
|
daily_credits_by_module=daily_credits_by_module,
|
||||||
|
period_credits_by_module=period_credits_by_module,
|
||||||
|
credits_by_team=credits_by_team,
|
||||||
|
model_usage=model_usage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,24 @@ class OperationLogOut(BaseModel):
|
|||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class DailyCreditOut(BaseModel):
|
||||||
|
date: str
|
||||||
|
module: str
|
||||||
|
credits: float
|
||||||
|
|
||||||
|
|
||||||
|
class TeamCreditOut(BaseModel):
|
||||||
|
team_name: str
|
||||||
|
team_id: str | None
|
||||||
|
credits: float
|
||||||
|
|
||||||
|
|
||||||
|
class ModelUsageOut(BaseModel):
|
||||||
|
model_name: str
|
||||||
|
provider: str
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
class AdminStatsOut(BaseModel):
|
class AdminStatsOut(BaseModel):
|
||||||
total_users: int
|
total_users: int
|
||||||
total_projects: int
|
total_projects: int
|
||||||
@@ -124,6 +142,11 @@ class AdminStatsOut(BaseModel):
|
|||||||
last_period_records: int = 0
|
last_period_records: int = 0
|
||||||
last_period_revenue: float = 0.0
|
last_period_revenue: float = 0.0
|
||||||
last_period_credits_consumed: float = 0.0
|
last_period_credits_consumed: float = 0.0
|
||||||
|
# 图表数据
|
||||||
|
daily_credits_by_module: list[DailyCreditOut] = []
|
||||||
|
period_credits_by_module: list[DailyCreditOut] = []
|
||||||
|
credits_by_team: list[TeamCreditOut] = []
|
||||||
|
model_usage: list[ModelUsageOut] = []
|
||||||
|
|
||||||
|
|
||||||
class AdminCreditRecordSummaryOut(BaseModel):
|
class AdminCreditRecordSummaryOut(BaseModel):
|
||||||
|
|||||||
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-iwDaDgKC.js"></script>
|
<script type="module" crossorigin src="/assets/index-D7Lr6Blo.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -866,7 +866,7 @@ const AppLayout: React.FC = () => {
|
|||||||
|
|
||||||
>
|
>
|
||||||
{siteLogo ? (
|
{siteLogo ? (
|
||||||
<img src={siteLogo} alt="logo" style={{ width: 28, height: 28, objectFit: 'contain' }} />
|
<img src={siteLogo} alt="logo" style={{ width: 36, height: 36, objectFit: 'contain' }} />
|
||||||
) : (
|
) : (
|
||||||
<ThunderboltOutlined style={{ fontSize: 20, color: '#ffffff' }} />
|
<ThunderboltOutlined style={{ fontSize: 20, color: '#ffffff' }} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user