1、修改后台数据概览页面
2、增加对应是时间范围修改
This commit is contained in:
Vendored
+14
-3
File diff suppressed because one or more lines are too long
Vendored
+13
-13
@@ -1,13 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>VideoGen.AI 管理后台</title>
|
<title>VideoGen.AI 管理后台</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CIsX_bex.js"></script>
|
<script type="module" crossorigin src="/assets/index-D8zG-fgv.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>␍
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -86,8 +86,12 @@ export async function markNotificationRead(id: string): Promise<void> {
|
|||||||
|
|
||||||
// ── Admin ─────────────────────────────────────────────────
|
// ── Admin ─────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getAdminStats(): Promise<AdminStats> {
|
export async function getAdminStats(startDate?: string, endDate?: string): Promise<AdminStats> {
|
||||||
return api.get('/admin/stats');
|
const params: Record<string, string> = {};
|
||||||
|
if (startDate) params.start_date = startDate;
|
||||||
|
if (endDate) params.end_date = endDate;
|
||||||
|
const query = new URLSearchParams(params).toString();
|
||||||
|
return api.get(`/admin/stats${query ? `?${query}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminUsers(search?: string): Promise<AdminUser[]> {
|
export async function getAdminUsers(search?: string): Promise<AdminUser[]> {
|
||||||
@@ -139,10 +143,19 @@ export async function uploadPdf(file: File, configKey: string): Promise<{ url: s
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCreditRecords(filters?: { user_id?: string; type?: string }): Promise<any> {
|
export async function getCreditRecords(filters?: {
|
||||||
|
user_id?: string;
|
||||||
|
user_name?: string;
|
||||||
|
type?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
}): Promise<any> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (filters?.user_id) params.set('user_id', filters.user_id);
|
if (filters?.user_id) params.set('user_id', filters.user_id);
|
||||||
|
if (filters?.user_name) params.set('user_name', filters.user_name);
|
||||||
if (filters?.type) params.set('type', filters.type);
|
if (filters?.type) params.set('type', filters.type);
|
||||||
|
if (filters?.start_date) params.set('start_date', filters.start_date);
|
||||||
|
if (filters?.end_date) params.set('end_date', filters.end_date);
|
||||||
const q = params.toString() ? `?${params}` : '';
|
const q = params.toString() ? `?${params}` : '';
|
||||||
return api.get(`/admin/credit-records${q}`);
|
return api.get(`/admin/credit-records${q}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button, Card, Select, Space, Table, Tag, Typography, message,
|
Button, Card, Select, Space, Table, Tag, Typography, message, Input, DatePicker,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, RollbackOutlined,
|
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, RollbackOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getCreditRecords } from '../api';
|
import { getCreditRecords } from '../api';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
interface CreditRecord {
|
interface CreditRecord {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -29,11 +30,18 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [typeFilter, setTypeFilter] = useState<string>('');
|
const [typeFilter, setTypeFilter] = useState<string>('');
|
||||||
|
const [userNameFilter, setUserNameFilter] = useState<string>('');
|
||||||
|
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
|
||||||
|
|
||||||
const load = async (type?: string) => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getCreditRecords(type ? { type } : undefined);
|
const filters: { type?: string; user_name?: string; start_date?: string; end_date?: string } = {};
|
||||||
|
if (typeFilter) filters.type = typeFilter;
|
||||||
|
if (userNameFilter) filters.user_name = userNameFilter;
|
||||||
|
if (dateRange[0]) filters.start_date = dateRange[0].format('YYYY-MM-DD');
|
||||||
|
if (dateRange[1]) filters.end_date = dateRange[1].format('YYYY-MM-DD');
|
||||||
|
const res = await getCreditRecords(Object.keys(filters).length > 0 ? filters : undefined);
|
||||||
setRecords(res.items || []);
|
setRecords(res.items || []);
|
||||||
setTotal(res.total || 0);
|
setTotal(res.total || 0);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -45,9 +53,15 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
const handleTypeFilter = (value: string) => {
|
const handleSearch = () => {
|
||||||
setTypeFilter(value);
|
load();
|
||||||
load(value || undefined);
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setTypeFilter('');
|
||||||
|
setUserNameFilter('');
|
||||||
|
setDateRange([null, null]);
|
||||||
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalRecharge = records.filter(r => r.type === 'recharge').reduce((s, r) => s + r.amount, 0);
|
const totalRecharge = records.filter(r => r.type === 'recharge').reduce((s, r) => s + r.amount, 0);
|
||||||
@@ -139,11 +153,11 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
|
||||||
<Space>
|
<Space wrap>
|
||||||
<Select
|
<Select
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
onChange={handleTypeFilter}
|
onChange={(value) => setTypeFilter(value)}
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
options={[
|
options={[
|
||||||
{ value: '', label: '全部类型' },
|
{ value: '', label: '全部类型' },
|
||||||
@@ -152,8 +166,25 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
{ value: 'refund', label: '退回' },
|
{ value: 'refund', label: '退回' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="用户名搜索"
|
||||||
|
value={userNameFilter}
|
||||||
|
onChange={(e) => setUserNameFilter(e.target.value)}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(dates) => { if (dates) setDateRange([dates[0], dates[1]]); }}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" onClick={handleSearch}>搜索</Button>
|
||||||
|
<Button onClick={handleReset}>重置</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>
|
||||||
</Space>
|
</Space>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => load(typeFilter || undefined)}>刷新</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,90 +1,348 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Col, Row, Statistic, Typography, Table, Tag } from 'antd';
|
import { Card, Col, Row, Typography, DatePicker, Button, Space } from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
ProjectOutlined,
|
ProjectOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
DollarOutlined,
|
DollarOutlined,
|
||||||
ThunderboltOutlined,
|
WalletOutlined,
|
||||||
ArrowUpOutlined,
|
ArrowUpOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getAdminStats } from '../api';
|
import { getAdminStats } from '../api';
|
||||||
import type { AdminStats } from '../types';
|
import type { AdminStats } from '../types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
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 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);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const data = await getAdminStats();
|
|
||||||
setStats(data);
|
|
||||||
} catch { /* auth error handled by client */ }
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
load();
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const statCards = stats ? [
|
const handleDateChange = () => {
|
||||||
{ title: '总用户数', value: stats.totalUsers, icon: <UserOutlined />, color: '#6366f1', bg: 'rgba(99,102,241,0.08)' },
|
load();
|
||||||
{ title: '总项目数', value: stats.totalProjects, icon: <ProjectOutlined />, color: '#06b6d4', bg: 'rgba(6,182,212,0.08)' },
|
};
|
||||||
{ title: '总生成次数', value: stats.totalGenerations, icon: <PlayCircleOutlined />, color: '#10b981', bg: 'rgba(16,185,129,0.08)' },
|
|
||||||
{ title: '总收入(元)', value: stats.totalRevenue, icon: <DollarOutlined />, color: '#f59e0b', bg: 'rgba(245,158,11,0.08)', prefix: '¥' },
|
const handleToday = () => {
|
||||||
{ title: '今日消耗积分', value: stats.creditsConsumedToday, icon: <ThunderboltOutlined />, color: '#ef4444', bg: 'rgba(239,68,68,0.08)' },
|
setStartDate([dayjs().startOf('day'), dayjs()]);
|
||||||
|
setTimeout(() => load(), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleYesterday = () => {
|
||||||
|
const yesterday = dayjs().subtract(1, 'day');
|
||||||
|
setStartDate([yesterday.startOf('day'), yesterday.endOf('day')]);
|
||||||
|
setTimeout(() => load(), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWeek = () => {
|
||||||
|
setStartDate([dayjs().startOf('week'), dayjs()]);
|
||||||
|
setTimeout(() => load(), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonth = () => {
|
||||||
|
setStartDate([dayjs().startOf('month'), dayjs()]);
|
||||||
|
setTimeout(() => load(), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseStats = stats ? [
|
||||||
|
{
|
||||||
|
title: '用户数量',
|
||||||
|
value: stats.totalUsers,
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
prefix: '',
|
||||||
|
suffix: '位用户',
|
||||||
|
description: '平台注册用户总数'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '总项目数',
|
||||||
|
value: stats.totalProjects,
|
||||||
|
icon: <ProjectOutlined />,
|
||||||
|
gradient: 'linear-gradient(135deg, #00d4ff 0%, #0099cc 100%)',
|
||||||
|
prefix: '',
|
||||||
|
suffix: '个项目',
|
||||||
|
description: '创建的项目总数'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目记录',
|
||||||
|
value: stats.totalRecords,
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
|
prefix: '',
|
||||||
|
suffix: '条记录',
|
||||||
|
description: '项目记录总数'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创作记录',
|
||||||
|
value: stats.totalGenerations,
|
||||||
|
icon: <PlayCircleOutlined />,
|
||||||
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||||
|
prefix: '',
|
||||||
|
suffix: '次创作',
|
||||||
|
description: 'AI创作记录总数'
|
||||||
|
},
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
return (
|
const financeStats = stats ? [
|
||||||
<div>
|
{
|
||||||
{/* Stats Cards */}
|
title: '支付宝收入',
|
||||||
<Row gutter={[16, 16]}>
|
value: stats.todayAlipayRevenue,
|
||||||
{statCards.map((s, i) => (
|
icon: <WalletOutlined />,
|
||||||
<Col xs={12} sm={8} lg={i < 4 ? 6 : 24} key={s.title}>
|
gradient: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
|
||||||
<Card bordered={false} loading={loading}
|
prefix: '¥',
|
||||||
style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
suffix: '',
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
description: '支付宝收款',
|
||||||
<div style={{
|
tag: '支付宝'
|
||||||
width: 44, height: 44, borderRadius: 10,
|
},
|
||||||
background: s.bg, display: 'flex',
|
{
|
||||||
alignItems: 'center', justifyContent: 'center',
|
title: '微信收入',
|
||||||
fontSize: 20, color: s.color, flexShrink: 0,
|
value: stats.todayWechatRevenue,
|
||||||
}}>
|
icon: <DollarOutlined />,
|
||||||
{s.icon}
|
gradient: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
||||||
</div>
|
prefix: '¥',
|
||||||
<div>
|
suffix: '',
|
||||||
<div style={{ color: '#94a3b8', fontSize: 12, marginBottom: 2 }}>{s.title}</div>
|
description: '微信收款',
|
||||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#1a1a2e' }}>
|
tag: '微信支付'
|
||||||
{s.prefix}{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}
|
},
|
||||||
</div>
|
{
|
||||||
</div>
|
title: '总收入',
|
||||||
</div>
|
value: stats.totalRevenue,
|
||||||
</Card>
|
icon: <ArrowUpOutlined />,
|
||||||
</Col>
|
gradient: 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)',
|
||||||
))}
|
prefix: '¥',
|
||||||
</Row>
|
suffix: '',
|
||||||
|
description: '平台总收入',
|
||||||
|
tag: '总收入'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消耗积分',
|
||||||
|
value: stats.creditsConsumedToday,
|
||||||
|
icon: <DollarOutlined />,
|
||||||
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||||
|
prefix: '',
|
||||||
|
suffix: '积分',
|
||||||
|
description: '用户消耗积分',
|
||||||
|
tag: '积分消耗'
|
||||||
|
},
|
||||||
|
] : [];
|
||||||
|
|
||||||
{/* Quick Info */}
|
const StatCard: React.FC<{
|
||||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
title: string;
|
||||||
<Col xs={24} lg={12}>
|
value: number;
|
||||||
<Card title="系统信息" bordered={false}
|
icon: React.ReactNode;
|
||||||
style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
gradient: string;
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
prefix?: string;
|
||||||
{[
|
suffix?: string;
|
||||||
{ label: '平台名称', value: 'VideoGen.AI' },
|
description?: string;
|
||||||
{ label: 'API版本', value: 'v1.0.0' },
|
tag?: string;
|
||||||
{ label: '数据库', value: 'PostgreSQL' },
|
}> = ({ title, value, icon, gradient, prefix = '', suffix = '', description, tag }) => (
|
||||||
{ label: '视频引擎', value: 'Seedance 2.0' },
|
<Card
|
||||||
].map(item => (
|
bordered={false}
|
||||||
<div key={item.label} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid #f5f6fa' }}>
|
loading={loading}
|
||||||
<Typography.Text type="secondary">{item.label}</Typography.Text>
|
hoverable
|
||||||
<Typography.Text strong>{item.value}</Typography.Text>
|
style={{
|
||||||
</div>
|
borderRadius: 16,
|
||||||
))}
|
border: '1px solid rgba(0,0,0,0.04)',
|
||||||
</div>
|
background: '#ffffff',
|
||||||
</Card>
|
boxShadow: '0 4px 20px rgba(0,0,0,0.05)',
|
||||||
</Col>
|
transition: 'all 0.3s ease',
|
||||||
</Row>
|
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>
|
||||||
|
)}
|
||||||
|
</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 }}>
|
||||||
|
数据概览
|
||||||
|
</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={(dates) => { if (dates) setStartDate([dates[0], dates[1]]); }}
|
||||||
|
onBlur={handleDateChange}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={handleDateChange}
|
||||||
|
style={{ borderRadius: 6 }}
|
||||||
|
>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -763,81 +763,76 @@ const AdminGenerationAiRecords: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24 }}>
|
<div>
|
||||||
<Card
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
title={(
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
|
||||||
<Space>
|
<Space>
|
||||||
<FileImageOutlined />
|
<PlayCircleOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||||
<span>创作记录管理</span>
|
<Typography.Text strong style={{ fontSize: 16 }}>创作记录管理</Typography.Text>
|
||||||
</Space>
|
<Tag color="purple">{total} 条记录</Tag>
|
||||||
)}
|
</Space>
|
||||||
extra={(
|
<Space>
|
||||||
<Space wrap>
|
<Select
|
||||||
<Select
|
allowClear
|
||||||
allowClear
|
placeholder="状态筛选"
|
||||||
placeholder="状态筛选"
|
value={filterStatus || undefined}
|
||||||
value={filterStatus || undefined}
|
style={{ width: 140 }}
|
||||||
style={{ width: 140 }}
|
onChange={(v) => { setFilterStatus(v || ''); setPage(1); }}
|
||||||
onChange={(v) => {
|
options={[
|
||||||
setFilterStatus(v || '');
|
{ value: 'generating', label: '生成中' },
|
||||||
setPage(1);
|
{ value: 'completed', label: '已完成' },
|
||||||
}}
|
{ value: 'failed', label: '失败' },
|
||||||
options={[
|
]}
|
||||||
{ value: 'generating', label: '生成中' },
|
/>
|
||||||
{ value: 'completed', label: '已完成' },
|
<Select
|
||||||
{ value: 'failed', label: '失败' },
|
allowClear
|
||||||
]}
|
placeholder="类型筛选"
|
||||||
/>
|
value={filterGenType || undefined}
|
||||||
<Select
|
style={{ width: 120 }}
|
||||||
allowClear
|
onChange={(v) => { setFilterGenType(v || ''); setPage(1); }}
|
||||||
placeholder="类型筛选"
|
options={[
|
||||||
value={filterGenType || undefined}
|
{ value: 'image', label: '图片' },
|
||||||
style={{ width: 120 }}
|
{ value: 'video', label: '视频' },
|
||||||
onChange={(v) => {
|
]}
|
||||||
setFilterGenType(v || '');
|
/>
|
||||||
setPage(1);
|
<Input
|
||||||
}}
|
placeholder="用户ID搜索"
|
||||||
options={[
|
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||||
{ value: 'image', label: '图片' },
|
style={{ width: 180 }}
|
||||||
{ value: 'video', label: '视频' },
|
value={inputUserId}
|
||||||
]}
|
onChange={(e) => setInputUserId(e.target.value)}
|
||||||
/>
|
onPressEnter={handleSearch}
|
||||||
<Input
|
allowClear
|
||||||
placeholder="用户ID"
|
/>
|
||||||
value={inputUserId}
|
<Input
|
||||||
onChange={(e) => setInputUserId(e.target.value)}
|
placeholder="用户名搜索"
|
||||||
onPressEnter={handleSearch}
|
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||||
style={{ width: 180 }}
|
style={{ width: 160 }}
|
||||||
allowClear
|
value={inputUserName}
|
||||||
/>
|
onChange={(e) => setInputUserName(e.target.value)}
|
||||||
<Input
|
onPressEnter={handleSearch}
|
||||||
placeholder="用户名"
|
allowClear
|
||||||
value={inputUserName}
|
/>
|
||||||
onChange={(e) => setInputUserName(e.target.value)}
|
<Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}>
|
||||||
onPressEnter={handleSearch}
|
搜索
|
||||||
style={{ width: 160 }}
|
</Button>
|
||||||
allowClear
|
</Space>
|
||||||
/>
|
</div>
|
||||||
<Button icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
bordered={false}
|
|
||||||
style={{ borderRadius: 16 }}
|
|
||||||
>
|
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns as any}
|
columns={columns as any}
|
||||||
dataSource={records}
|
dataSource={records}
|
||||||
scroll={{ x: 1180 }}
|
scroll={{ x: 1180 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
total,
|
total,
|
||||||
showSizeChanger: false,
|
showSizeChanger: false,
|
||||||
showTotal: (t) => `共 ${t} 条`,
|
showTotal: (t) => `共 ${t} 条`,
|
||||||
onChange: (p) => setPage(p),
|
onChange: (p) => setPage(p),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -113,8 +113,11 @@ export interface AdminStats {
|
|||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
totalProjects: number;
|
totalProjects: number;
|
||||||
totalGenerations: number;
|
totalGenerations: number;
|
||||||
|
totalRecords: number;
|
||||||
totalRevenue: number;
|
totalRevenue: number;
|
||||||
creditsConsumedToday: number;
|
creditsConsumedToday: number;
|
||||||
|
todayAlipayRevenue: number;
|
||||||
|
todayWechatRevenue: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentStats {
|
export interface PaymentStats {
|
||||||
|
|||||||
@@ -247,7 +247,10 @@ async def list_credit_records(
|
|||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=100),
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
user_id: str | None = Query(None),
|
user_id: str | None = Query(None),
|
||||||
|
user_name: str | None = Query(None),
|
||||||
type: str | None = Query(None),
|
type: str | None = Query(None),
|
||||||
|
start_date: str = Query(None),
|
||||||
|
end_date: str = Query(None),
|
||||||
admin: User = Depends(get_admin_user),
|
admin: User = Depends(get_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -261,9 +264,25 @@ async def list_credit_records(
|
|||||||
if user_id:
|
if user_id:
|
||||||
query = query.where(CreditRecord.user_id == user_id)
|
query = query.where(CreditRecord.user_id == user_id)
|
||||||
count_query = count_query.where(CreditRecord.user_id == user_id)
|
count_query = count_query.where(CreditRecord.user_id == user_id)
|
||||||
|
if user_name:
|
||||||
|
query = query.where(User.username.like(f'%{user_name}%'))
|
||||||
|
count_query = count_query.join(User, CreditRecord.user_id == User.id).where(User.username.like(f'%{user_name}%'))
|
||||||
if type:
|
if type:
|
||||||
query = query.where(CreditRecord.type == type)
|
query = query.where(CreditRecord.type == type)
|
||||||
count_query = count_query.where(CreditRecord.type == type)
|
count_query = count_query.where(CreditRecord.type == type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if start_date:
|
||||||
|
date_start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||||
|
query = query.where(CreditRecord.created_at >= date_start)
|
||||||
|
count_query = count_query.where(CreditRecord.created_at >= date_start)
|
||||||
|
if end_date:
|
||||||
|
date_end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||||
|
date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||||
|
query = query.where(CreditRecord.created_at <= date_end)
|
||||||
|
count_query = count_query.where(CreditRecord.created_at <= date_end)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
total = (await db.execute(count_query)).scalar() or 0
|
total = (await db.execute(count_query)).scalar() or 0
|
||||||
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
||||||
@@ -1124,40 +1143,95 @@ async def list_operation_logs(
|
|||||||
async def get_stats(
|
async def get_stats(
|
||||||
admin: User = Depends(get_admin_user),
|
admin: User = Depends(get_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
start_date: str = Query(None),
|
||||||
|
end_date: str = Query(None),
|
||||||
):
|
):
|
||||||
total_users = (await db.execute(
|
total_users = (await db.execute(
|
||||||
select(func.count(User.id)).where(User.user_type == "frontend")
|
select(func.count(User.id)).where(User.user_type == "frontend")
|
||||||
)).scalar() or 0
|
)).scalar() or 0
|
||||||
total_projects = (await db.execute(select(func.count(Project.id)).where(Project.deleted_at.is_(None)))).scalar() or 0
|
|
||||||
total_generations = (
|
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
await db.execute(select(func.count(GenerationRecord.id)).where(GenerationRecord.deleted_at.is_(None)))
|
|
||||||
).scalar() or 0
|
try:
|
||||||
total_revenue = (
|
if start_date:
|
||||||
await db.execute(
|
date_start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||||
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
|
else:
|
||||||
PaymentOrder.status == "paid"
|
date_start = today_start
|
||||||
)
|
if end_date:
|
||||||
)
|
date_end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||||
).scalar() or 0
|
date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||||
|
else:
|
||||||
|
date_end = datetime.now()
|
||||||
|
except:
|
||||||
|
date_start = today_start
|
||||||
|
date_end = datetime.now()
|
||||||
|
|
||||||
today_start = datetime.now().replace(
|
total_projects = (await db.execute(
|
||||||
hour=0, minute=0, second=0, microsecond=0
|
select(func.count(Project.id)).where(
|
||||||
)
|
Project.deleted_at.is_(None),
|
||||||
credits_today = (
|
Project.created_at >= date_start,
|
||||||
await db.execute(
|
Project.created_at <= date_end,
|
||||||
select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
|
|
||||||
CreditRecord.type == "consume",
|
|
||||||
CreditRecord.created_at >= today_start,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
).scalar() or 0
|
)).scalar() or 0
|
||||||
|
|
||||||
|
total_generations = (await db.execute(
|
||||||
|
select(func.count(GenerationAITask.id)).where(
|
||||||
|
GenerationAITask.created_at >= date_start,
|
||||||
|
GenerationAITask.created_at <= date_end,
|
||||||
|
)
|
||||||
|
)).scalar() or 0
|
||||||
|
|
||||||
|
total_records = (await db.execute(
|
||||||
|
select(func.count(GenerationRecord.id)).where(
|
||||||
|
GenerationRecord.deleted_at.is_(None),
|
||||||
|
GenerationRecord.created_at >= date_start,
|
||||||
|
GenerationRecord.created_at <= date_end,
|
||||||
|
)
|
||||||
|
)).scalar() or 0
|
||||||
|
|
||||||
|
total_revenue = (await db.execute(
|
||||||
|
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
|
||||||
|
PaymentOrder.status == "paid",
|
||||||
|
PaymentOrder.created_at >= date_start,
|
||||||
|
PaymentOrder.created_at <= date_end,
|
||||||
|
)
|
||||||
|
)).scalar() or 0
|
||||||
|
|
||||||
|
credits_consumed = (await db.execute(
|
||||||
|
select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
|
||||||
|
CreditRecord.type == "consume",
|
||||||
|
CreditRecord.created_at >= date_start,
|
||||||
|
CreditRecord.created_at <= date_end,
|
||||||
|
)
|
||||||
|
)).scalar() or 0
|
||||||
|
|
||||||
|
alipay_revenue = (await db.execute(
|
||||||
|
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
|
||||||
|
PaymentOrder.status == "paid",
|
||||||
|
PaymentOrder.payment_method == "alipay",
|
||||||
|
PaymentOrder.created_at >= date_start,
|
||||||
|
PaymentOrder.created_at <= date_end,
|
||||||
|
)
|
||||||
|
)).scalar() or 0
|
||||||
|
|
||||||
|
wechat_revenue = (await db.execute(
|
||||||
|
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
|
||||||
|
PaymentOrder.status == "paid",
|
||||||
|
PaymentOrder.payment_method == "wechat",
|
||||||
|
PaymentOrder.created_at >= date_start,
|
||||||
|
PaymentOrder.created_at <= date_end,
|
||||||
|
)
|
||||||
|
)).scalar() or 0
|
||||||
|
|
||||||
return AdminStatsOut(
|
return AdminStatsOut(
|
||||||
total_users=total_users,
|
total_users=total_users,
|
||||||
total_projects=total_projects,
|
total_projects=total_projects,
|
||||||
total_generations=total_generations,
|
total_generations=total_generations,
|
||||||
|
total_records=total_records,
|
||||||
total_revenue=float(total_revenue),
|
total_revenue=float(total_revenue),
|
||||||
credits_consumed_today=float(credits_today),
|
credits_consumed_today=float(credits_consumed),
|
||||||
|
today_alipay_revenue=float(alipay_revenue),
|
||||||
|
today_wechat_revenue=float(wechat_revenue),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+70
-28
@@ -322,13 +322,54 @@ async def _seed_data():
|
|||||||
# Seed menu configs
|
# Seed menu configs
|
||||||
from app.models.menu_config import MenuConfig
|
from app.models.menu_config import MenuConfig
|
||||||
|
|
||||||
default_menus = [
|
frontend_groups = [
|
||||||
("/projects", "我的项目", "HomeOutlined", 0, "frontend"),
|
("AI项目行业生成", "HomeOutlined", 0),
|
||||||
("/conversation", "AI创作", "StarOutlined", 1, "frontend"),
|
("AI对话生成", "HomeOutlined", 1),
|
||||||
("/records", "项目记录", "PlayCircleOutlined", 2, "frontend"),
|
("AI视频创作", "HomeOutlined", 2),
|
||||||
("/generated", "素材云", "StarOutlined", 3, "frontend"),
|
("资产管理", "HomeOutlined", 3),
|
||||||
|
("媒体关联", "HomeOutlined", 4),
|
||||||
]
|
]
|
||||||
for path, label, icon, order, target in default_menus:
|
frontend_group_ids: dict[str, str] = {}
|
||||||
|
for label, icon, order in frontend_groups:
|
||||||
|
existing = await db.execute(
|
||||||
|
select(MenuConfig).where(
|
||||||
|
MenuConfig.label == label,
|
||||||
|
MenuConfig.menu_type == "group",
|
||||||
|
MenuConfig.menu_target == "frontend",
|
||||||
|
).limit(1)
|
||||||
|
)
|
||||||
|
group = existing.scalar_one_or_none()
|
||||||
|
if group:
|
||||||
|
frontend_group_ids[label] = group.id
|
||||||
|
else:
|
||||||
|
gid = generate_id()
|
||||||
|
frontend_group_ids[label] = gid
|
||||||
|
db.add(
|
||||||
|
MenuConfig(
|
||||||
|
id=gid,
|
||||||
|
path="",
|
||||||
|
label=label,
|
||||||
|
icon=icon,
|
||||||
|
sort_order=order,
|
||||||
|
is_active=True,
|
||||||
|
parent_id=None,
|
||||||
|
menu_type="group",
|
||||||
|
menu_target="frontend",
|
||||||
|
is_default=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
frontend_pages = [
|
||||||
|
("/projects", "我的项目", "HomeOutlined", 0, "AI项目行业生成", True),
|
||||||
|
("/conversation", "AI创作", "StarOutlined", 0, "AI对话生成", True),
|
||||||
|
("/initial", "爆款开头复刻", "CodeOutlined", 0, "AI视频创作", False),
|
||||||
|
("/removelens", "拆镜复刻", "CameraOutlined", 1, "AI视频创作", False),
|
||||||
|
("/records", "项目记录", "PlayCircleOutlined", 0, "资产管理", True),
|
||||||
|
("/generated", "素材云", "CloudOutlined", 1, "资产管理", True),
|
||||||
|
("/authorization", "授权管理", "UserOutlined", 0, "媒体关联", False),
|
||||||
|
("/consume", "消耗列表", "FileTextOutlined", 1, "媒体关联", False),
|
||||||
|
]
|
||||||
|
for path, label, icon, order, group_label, is_default in frontend_pages:
|
||||||
existing = await db.execute(
|
existing = await db.execute(
|
||||||
select(MenuConfig).where(MenuConfig.path == path).limit(1)
|
select(MenuConfig).where(MenuConfig.path == path).limit(1)
|
||||||
)
|
)
|
||||||
@@ -341,30 +382,33 @@ async def _seed_data():
|
|||||||
icon=icon,
|
icon=icon,
|
||||||
sort_order=order,
|
sort_order=order,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
|
parent_id=frontend_group_ids.get(group_label),
|
||||||
menu_type="page",
|
menu_type="page",
|
||||||
menu_target=target,
|
menu_target="frontend",
|
||||||
is_default=True,
|
is_default=is_default,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Seed admin group menus first, then page menus with parent_id
|
|
||||||
admin_groups = [
|
admin_groups = [
|
||||||
|
("模型设置", "RobotOutlined", 98),
|
||||||
("模型配置", "RobotOutlined", 6),
|
("模型配置", "RobotOutlined", 6),
|
||||||
("系统设置", "SettingOutlined", 99),
|
("系统设置", "SettingOutlined", 99),
|
||||||
]
|
]
|
||||||
group_ids = {}
|
admin_group_ids: dict[str, str] = {}
|
||||||
for label, icon, order in admin_groups:
|
for label, icon, order in admin_groups:
|
||||||
existing = await db.execute(
|
existing = await db.execute(
|
||||||
select(MenuConfig).where(
|
select(MenuConfig).where(
|
||||||
MenuConfig.label == label,
|
MenuConfig.label == label,
|
||||||
MenuConfig.menu_type == "group",
|
MenuConfig.menu_type == "group",
|
||||||
MenuConfig.menu_target == "admin",
|
MenuConfig.menu_target == "admin",
|
||||||
)
|
).limit(1)
|
||||||
.limit(1)
|
|
||||||
)
|
)
|
||||||
group = existing.scalar_one_or_none()
|
group = existing.scalar_one_or_none()
|
||||||
if not group:
|
if group:
|
||||||
|
admin_group_ids[label] = group.id
|
||||||
|
else:
|
||||||
gid = generate_id()
|
gid = generate_id()
|
||||||
|
admin_group_ids[label] = gid
|
||||||
db.add(
|
db.add(
|
||||||
MenuConfig(
|
MenuConfig(
|
||||||
id=gid,
|
id=gid,
|
||||||
@@ -377,35 +421,33 @@ async def _seed_data():
|
|||||||
menu_target="admin",
|
menu_target="admin",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
group_ids[label] = gid
|
|
||||||
else:
|
|
||||||
group_ids[label] = group.id
|
|
||||||
|
|
||||||
# (path, label, icon, sort_order, parent_group_label or None)
|
admin_pages = [
|
||||||
admin_menus = [
|
|
||||||
("/", "数据概览", "DashboardOutlined", 0, None),
|
("/", "数据概览", "DashboardOutlined", 0, None),
|
||||||
("/users", "用户管理", "UserOutlined", 1, None),
|
("/users", "用户管理", "UserOutlined", 1, None),
|
||||||
("/credit-records", "交易流水", "WalletOutlined", 2, None),
|
("/credit-records", "交易流水", "WalletOutlined", 2, None),
|
||||||
("/generation-records", "生成记录", "VideoCameraOutlined", 3, None),
|
("/generation-ai", "创作记录", "BulbOutlined", 3, None),
|
||||||
|
("/generation-records", "项目记录", "VideoCameraOutlined", 3, None),
|
||||||
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
|
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
|
||||||
("/notifications", "消息推送", "BellOutlined", 5, None),
|
("/notifications", "消息推送", "BellOutlined", 5, None),
|
||||||
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型配置"),
|
("/payment-stats", "支付统计", "LineChartOutlined", 6, None),
|
||||||
("/models", "模型配置", "RobotOutlined", 1, "模型配置"),
|
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型设置"),
|
||||||
("/image-engines", "图片模型", "PictureOutlined", 2, "模型配置"),
|
("/models", "模型配置", "RobotOutlined", 1, "模型设置"),
|
||||||
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型配置"),
|
("/image-engines", "图片模型", "PictureOutlined", 2, "模型设置"),
|
||||||
|
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型设置"),
|
||||||
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
|
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
|
||||||
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
|
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
|
||||||
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
|
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
|
||||||
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
|
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
|
||||||
("/operation-logs", "操作日志", "HistoryOutlined", 5, "系统设置"),
|
("/operation-logs", "操作日志", "DatabaseOutlined", 5, "系统设置"),
|
||||||
|
("/oauthapp-list", "授权应用", "MenuOutlined", 28, "系统设置"),
|
||||||
]
|
]
|
||||||
for path, label, icon, order, parent_group in admin_menus:
|
for path, label, icon, order, parent_group in admin_pages:
|
||||||
existing = await db.execute(
|
existing = await db.execute(
|
||||||
select(MenuConfig).where(
|
select(MenuConfig).where(
|
||||||
MenuConfig.path == path,
|
MenuConfig.path == path,
|
||||||
MenuConfig.menu_target == "admin",
|
MenuConfig.menu_target == "admin",
|
||||||
)
|
).limit(1)
|
||||||
.limit(1)
|
|
||||||
)
|
)
|
||||||
if not existing.scalar_one_or_none():
|
if not existing.scalar_one_or_none():
|
||||||
db.add(
|
db.add(
|
||||||
@@ -418,7 +460,7 @@ async def _seed_data():
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
menu_type="page",
|
menu_type="page",
|
||||||
menu_target="admin",
|
menu_target="admin",
|
||||||
parent_id=group_ids.get(parent_group),
|
parent_id=admin_group_ids.get(parent_group),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -93,5 +93,8 @@ class AdminStatsOut(BaseModel):
|
|||||||
total_users: int
|
total_users: int
|
||||||
total_projects: int
|
total_projects: int
|
||||||
total_generations: int
|
total_generations: int
|
||||||
|
total_records: int
|
||||||
total_revenue: float
|
total_revenue: float
|
||||||
credits_consumed_today: float
|
credits_consumed_today: float
|
||||||
|
today_alipay_revenue: float
|
||||||
|
today_wechat_revenue: float
|
||||||
|
|||||||
Reference in New Issue
Block a user