fix(P1-2): generation.py _download_library_assets 改用 Worker SessionLocal 替代直接 create_engine
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
This commit is contained in:
@@ -1,245 +0,0 @@
|
||||
/**
|
||||
* Admin Analytics 数据分析页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, DatePicker, Space } from 'antd';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import {
|
||||
ArrowUpOutlined,
|
||||
UserAddOutlined,
|
||||
DollarOutlined,
|
||||
ProjectOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './Admin.css';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
// V21 配色
|
||||
const V21_COLORS = {
|
||||
primary: '#4f46e5',
|
||||
primaryLight: '#6366f1',
|
||||
success: '#10b981',
|
||||
successLight: '#34d399',
|
||||
warning: '#f59e0b',
|
||||
warningLight: '#fbbf24',
|
||||
purple: '#8b5cf6',
|
||||
purpleLight: '#a78bfa',
|
||||
};
|
||||
|
||||
const Analytics: React.FC = () => {
|
||||
// 模拟用户增长数据
|
||||
const userGrowthData = [
|
||||
{ month: '1月', users: 120, paid: 15 },
|
||||
{ month: '2月', users: 180, paid: 28 },
|
||||
{ month: '3月', users: 240, paid: 42 },
|
||||
{ month: '4月', users: 320, paid: 58 },
|
||||
{ month: '5月', users: 450, paid: 89 },
|
||||
{ month: '6月', users: 620, paid: 135 },
|
||||
];
|
||||
|
||||
// 模拟营收数据
|
||||
const revenueData = [
|
||||
{ month: '1月', revenue: 1480 },
|
||||
{ month: '2月', revenue: 2770 },
|
||||
{ month: '3月', revenue: 4160 },
|
||||
{ month: '4月', revenue: 5740 },
|
||||
{ month: '5月', revenue: 8810 },
|
||||
{ month: '6月', revenue: 13365 },
|
||||
];
|
||||
|
||||
// 订阅计划分布
|
||||
const planDistribution = [
|
||||
{ name: 'Free', value: 485, color: '#a78bfa' },
|
||||
{ name: 'Pro', value: 120, color: '#34d399' },
|
||||
{ name: 'Enterprise', value: 15, color: '#fbbf24' },
|
||||
];
|
||||
|
||||
// 活跃度统计
|
||||
const activityStats = [
|
||||
{ metric: '日活跃用户 (DAU)', value: 892, growth: '+12.5%', icon: <UserAddOutlined />, colorClass: 'primary' },
|
||||
{ metric: '月活跃用户 (MAU)', value: 3456, growth: '+8.3%', icon: <ArrowUpOutlined />, colorClass: 'success' },
|
||||
{ metric: '本月收入', value: 13365, prefix: '¥', growth: '+51.6%', icon: <DollarOutlined />, colorClass: 'warning' },
|
||||
{ metric: '活跃项目数', value: 2341, growth: '+18.2%', icon: <ProjectOutlined />, colorClass: 'purple' },
|
||||
];
|
||||
|
||||
// 用户留存表格
|
||||
const retentionData = [
|
||||
{ cohort: '2024-01', day1: '100%', day7: '68%', day30: '45%', day90: '28%' },
|
||||
{ cohort: '2024-02', day1: '100%', day7: '72%', day30: '48%', day90: '31%' },
|
||||
{ cohort: '2024-03', day1: '100%', day7: '75%', day30: '52%', day90: '-' },
|
||||
{ cohort: '2024-04', day1: '100%', day7: '78%', day30: '55%', day90: '-' },
|
||||
{ cohort: '2024-05', day1: '100%', day7: '80%', day30: '-', day90: '-' },
|
||||
];
|
||||
|
||||
const retentionColumns = [
|
||||
{ title: 'Cohort', dataIndex: 'cohort', key: 'cohort' },
|
||||
{ title: 'Day 1', dataIndex: 'day1', key: 'day1' },
|
||||
{ title: 'Day 7', dataIndex: 'day7', key: 'day7' },
|
||||
{ title: 'Day 30', dataIndex: 'day30', key: 'day30' },
|
||||
{ title: 'Day 90', dataIndex: 'day90', key: 'day90' },
|
||||
];
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
primary: '#4f46e5',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
purple: '#8b5cf6',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="analytics-page">
|
||||
<div className="xx-page-head">
|
||||
<div className="xx-page-head-content">
|
||||
<h2>数据分析</h2>
|
||||
<p>用户增长、营收趋势与留存分析</p>
|
||||
</div>
|
||||
<div className="xx-page-head-actions">
|
||||
<Space>
|
||||
<RangePicker className="xx-date-picker" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关键指标 */}
|
||||
<Row gutter={24} className="xx-grid-4">
|
||||
{activityStats.map((stat, index) => (
|
||||
<Col span={6} key={index}>
|
||||
<div className="xx-stat-card">
|
||||
<div className="xx-stat-card-header">
|
||||
<div className={`xx-stat-card-icon ${stat.colorClass}`}>
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="xx-stat-card-label">{stat.metric}</div>
|
||||
<div
|
||||
className="xx-stat-card-value"
|
||||
style={{ color: colorMap[stat.colorClass] }}
|
||||
>
|
||||
{stat.prefix}{stat.value.toLocaleString()}
|
||||
</div>
|
||||
<div className="xx-stat-card-growth">
|
||||
{stat.growth} vs 上月
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 图表区域 */}
|
||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||
{/* 用户增长趋势 */}
|
||||
<Col span={16}>
|
||||
<div className="xx-chart-container">
|
||||
<Card title="用户增长趋势">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={userGrowthData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="users"
|
||||
stroke={V21_COLORS.primary}
|
||||
name="总用户"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="paid"
|
||||
stroke={V21_COLORS.success}
|
||||
name="付费用户"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* 订阅计划分布 */}
|
||||
<Col span={8}>
|
||||
<div className="xx-chart-container">
|
||||
<Card title="订阅计划分布">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={planDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={(entry) => `${entry.name}: ${entry.value}`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{planDistribution.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 营收趋势 */}
|
||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||
<Col span={24}>
|
||||
<div className="xx-chart-container">
|
||||
<Card title="营收趋势">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={revenueData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar
|
||||
dataKey="revenue"
|
||||
fill={V21_COLORS.warning}
|
||||
name="收入 (¥)"
|
||||
radius={[8, 8, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 用户留存表 */}
|
||||
<div className="xx-table-wrapper">
|
||||
<Card title="用户留存率 (Cohort Analysis)">
|
||||
<Table
|
||||
columns={retentionColumns}
|
||||
dataSource={retentionData}
|
||||
rowKey="cohort"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics;
|
||||
|
||||
export const Component = Analytics;
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Admin Dashboard 仪表盘
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic, Table } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
CrownOutlined,
|
||||
DollarOutlined,
|
||||
RiseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './Admin.css';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
// 模拟数据
|
||||
const stats = [
|
||||
{ title: '总用户数', value: 1234, icon: <UserOutlined />, colorClass: 'primary' },
|
||||
{ title: '付费用户', value: 156, icon: <CrownOutlined />, colorClass: 'success' },
|
||||
{ title: '月收入', value: 15444, prefix: '¥', icon: <DollarOutlined />, colorClass: 'warning' },
|
||||
{ title: '活跃用户', value: 892, icon: <RiseOutlined />, colorClass: 'purple' },
|
||||
];
|
||||
|
||||
const recentUsers = [
|
||||
{ id: '1', username: 'user1', email: 'user1@example.com', created_at: '2024-06-17' },
|
||||
{ id: '2', username: 'user2', email: 'user2@example.com', created_at: '2024-06-16' },
|
||||
{ id: '3', username: 'user3', email: 'user3@example.com', created_at: '2024-06-15' },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '注册时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
];
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
primary: '#4f46e5',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
purple: '#8b5cf6',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dashboard-page">
|
||||
<div className="xx-page-head-simple">
|
||||
<h2>Dashboard</h2>
|
||||
<p>系统数据总览与关键指标</p>
|
||||
</div>
|
||||
|
||||
<Row gutter={24} className="xx-grid-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Col span={6} key={index}>
|
||||
<div className="xx-stat-card">
|
||||
<div className="xx-stat-card-header">
|
||||
<div className={`xx-stat-card-icon ${stat.colorClass}`}>
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="xx-stat-card-label">{stat.title}</div>
|
||||
<div
|
||||
className="xx-stat-card-value"
|
||||
style={{ color: colorMap[stat.colorClass] }}
|
||||
>
|
||||
{stat.prefix}{stat.value.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<div className="xx-table-wrapper">
|
||||
<Card title="最近注册用户" className="xx-card">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={recentUsers}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
export const Component = Dashboard;
|
||||
@@ -1,344 +0,0 @@
|
||||
/**
|
||||
* 日志查看器
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Table, Tag, Input, Select, DatePicker, Space, Button, Drawer } from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
DownloadOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import './Admin.css';
|
||||
|
||||
const { Search } = Input;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
|
||||
interface LogEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
|
||||
service: string;
|
||||
user?: string;
|
||||
action: string;
|
||||
details: string;
|
||||
ip?: string;
|
||||
request_id?: string;
|
||||
}
|
||||
|
||||
const LogViewer: React.FC = () => {
|
||||
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null);
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [filterLevel, setFilterLevel] = useState<string>('all');
|
||||
const [filterService, setFilterService] = useState<string>('all');
|
||||
|
||||
// 模拟日志数据
|
||||
const logs: LogEntry[] = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-06-17 17:15:32',
|
||||
level: 'INFO',
|
||||
service: 'API',
|
||||
user: 'user@example.com',
|
||||
action: 'User Login',
|
||||
details: 'User successfully logged in',
|
||||
ip: '192.168.1.100',
|
||||
request_id: 'req_abc123',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-06-17 17:14:28',
|
||||
level: 'WARN',
|
||||
service: 'API',
|
||||
user: 'admin@example.com',
|
||||
action: 'Failed Login Attempt',
|
||||
details: 'Invalid password provided (attempt 2/5)',
|
||||
ip: '192.168.1.101',
|
||||
request_id: 'req_def456',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-06-17 17:12:45',
|
||||
level: 'ERROR',
|
||||
service: 'Database',
|
||||
action: 'Connection Timeout',
|
||||
details: 'PostgreSQL connection pool exhausted, timeout after 5s',
|
||||
request_id: 'req_ghi789',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timestamp: '2024-06-17 17:10:15',
|
||||
level: 'INFO',
|
||||
service: 'Celery',
|
||||
action: 'Task Completed',
|
||||
details: 'Video processing task completed successfully',
|
||||
request_id: 'task_jkl012',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timestamp: '2024-06-17 17:08:52',
|
||||
level: 'WARN',
|
||||
service: 'ObjectStorage',
|
||||
action: 'Slow Upload',
|
||||
details: 'File upload took 3.2s (>1s threshold)',
|
||||
ip: '192.168.1.102',
|
||||
request_id: 'req_mno345',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
timestamp: '2024-06-17 17:05:33',
|
||||
level: 'ERROR',
|
||||
service: 'API',
|
||||
user: 'user2@example.com',
|
||||
action: 'Permission Denied',
|
||||
details: 'User attempted to access admin endpoint without permission',
|
||||
ip: '192.168.1.103',
|
||||
request_id: 'req_pqr678',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
timestamp: '2024-06-17 17:03:21',
|
||||
level: 'INFO',
|
||||
service: 'API',
|
||||
user: 'user3@example.com',
|
||||
action: 'Project Created', details: 'New project "My Project" created',
|
||||
ip: '192.168.1.104',
|
||||
request_id: 'req_stu901',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
timestamp: '2024-06-17 17:01:08',
|
||||
level: 'DEBUG',
|
||||
service: 'Redis',
|
||||
action: 'Cache Miss',
|
||||
details: 'Cache key "project:123" not found, fetching from DB',
|
||||
request_id: 'req_vwx234',
|
||||
},
|
||||
];
|
||||
|
||||
const getLevelTagClass = (level: string) => {
|
||||
const classes: Record<string, string> = {
|
||||
INFO: 'xx-tag info',
|
||||
WARN: 'xx-tag warning',
|
||||
ERROR: 'xx-tag error',
|
||||
DEBUG: 'xx-tag debug',
|
||||
};
|
||||
return classes[level] || 'xx-tag debug';
|
||||
};
|
||||
|
||||
const handleViewDetails = (log: LogEntry) => {
|
||||
setSelectedLog(log);
|
||||
setDrawerVisible(true);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<LogEntry> = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
width: 180,
|
||||
sorter: (a, b) => a.timestamp.localeCompare(b.timestamp),
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
width: 100,
|
||||
render: (level: string) => <Tag className={getLevelTagClass(level)}>{level}</Tag>,
|
||||
filters: [
|
||||
{ text: 'INFO', value: 'INFO' },
|
||||
{ text: 'WARN', value: 'WARN' },
|
||||
{ text: 'ERROR', value: 'ERROR' },
|
||||
{ text: 'DEBUG', value: 'DEBUG' },
|
||||
],
|
||||
onFilter: (value, record) => record.level === value,
|
||||
},
|
||||
{
|
||||
title: '服务',
|
||||
dataIndex: 'service',
|
||||
key: 'service',
|
||||
width: 120,
|
||||
filters: [
|
||||
{ text: 'API', value: 'API' },
|
||||
{ text: 'Database', value: 'Database' },
|
||||
{ text: 'Celery', value: 'Celery' },
|
||||
{ text: 'ObjectStorage', value: 'ObjectStorage' },
|
||||
{ text: 'Redis', value: 'Redis' },
|
||||
],
|
||||
onFilter: (value, record) => record.service === value,
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'user',
|
||||
key: 'user',
|
||||
width: 180,
|
||||
render: (user?: string) => user || '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '详情',
|
||||
dataIndex: 'details',
|
||||
key: 'details',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetails(record)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="log-viewer-page">
|
||||
<div className="xx-page-head-simple">
|
||||
<h2>日志查看器</h2>
|
||||
<p>系统日志查询与监控</p>
|
||||
</div>
|
||||
|
||||
<div className="xx-filter-bar">
|
||||
<Search
|
||||
placeholder="搜索日志内容..."
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
className="xx-search-input"
|
||||
style={{ width: 280 }}
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder="日志级别"
|
||||
className="xx-select"
|
||||
style={{ width: 130 }}
|
||||
value={filterLevel}
|
||||
onChange={setFilterLevel}
|
||||
>
|
||||
<Option value="all">全部级别</Option>
|
||||
<Option value="INFO">INFO</Option>
|
||||
<Option value="WARN">WARN</Option>
|
||||
<Option value="ERROR">ERROR</Option>
|
||||
<Option value="DEBUG">DEBUG</Option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="服务"
|
||||
className="xx-select"
|
||||
style={{ width: 140 }}
|
||||
value={filterService}
|
||||
onChange={setFilterService}
|
||||
>
|
||||
<Option value="all">全部服务</Option>
|
||||
<Option value="API">API</Option>
|
||||
<Option value="Database">Database</Option>
|
||||
<Option value="Celery">Celery</Option>
|
||||
<Option value="ObjectStorage">Object Storage</Option>
|
||||
<Option value="Redis">Redis</Option>
|
||||
</Select>
|
||||
|
||||
<RangePicker showTime className="xx-date-picker" />
|
||||
|
||||
<Button icon={<FilterOutlined />} disabled className="xx-ghost-btn">
|
||||
应用过滤暂未开放
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} disabled className="xx-ghost-btn">
|
||||
导出日志暂未开放
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="xx-table-wrapper">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={logs}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
total: logs.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条日志`,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 日志详情抽屉 */}
|
||||
<Drawer
|
||||
title="日志详情"
|
||||
placement="right"
|
||||
width={600}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
className="xx-drawer"
|
||||
>
|
||||
{selectedLog && (
|
||||
<div>
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">时间</div>
|
||||
<div className="xx-log-detail-value">{selectedLog.timestamp}</div>
|
||||
</div>
|
||||
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">级别</div>
|
||||
<div className="xx-log-detail-value">
|
||||
<Tag className={getLevelTagClass(selectedLog.level)}>{selectedLog.level}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">服务</div>
|
||||
<div className="xx-log-detail-value">{selectedLog.service}</div>
|
||||
</div>
|
||||
|
||||
{selectedLog.user && (
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">用户</div>
|
||||
<div className="xx-log-detail-value">{selectedLog.user}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">操作</div>
|
||||
<div className="xx-log-detail-value">{selectedLog.action}</div>
|
||||
</div>
|
||||
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">详细信息</div>
|
||||
<div className="xx-log-detail-code">{selectedLog.details}</div>
|
||||
</div>
|
||||
|
||||
{selectedLog.ip && (
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">IP 地址</div>
|
||||
<div className="xx-log-detail-value">{selectedLog.ip}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedLog.request_id && (
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">Request ID</div>
|
||||
<div className="xx-log-detail-code">{selectedLog.request_id}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogViewer;
|
||||
|
||||
export const Component = LogViewer;
|
||||
@@ -1,224 +0,0 @@
|
||||
/**
|
||||
* 系统监控页面
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Row, Col, Progress, Badge, Table, Tag, Button, Space } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
SyncOutlined,
|
||||
ReloadOutlined,
|
||||
DatabaseOutlined,
|
||||
ApiOutlined,
|
||||
CloudServerOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './Admin.css';
|
||||
|
||||
const SystemMonitor: React.FC = () => {
|
||||
const [refreshTime, setRefreshTime] = useState(new Date());
|
||||
|
||||
// 模拟实时数据
|
||||
const [systemMetrics, setSystemMetrics] = useState({
|
||||
cpu: 35,
|
||||
memory: 62,
|
||||
disk: 48,
|
||||
network: 28,
|
||||
});
|
||||
|
||||
// 服务健康状态
|
||||
const services = [
|
||||
{ name: 'API 服务', status: 'healthy', uptime: '99.98%', responseTime: '45ms' },
|
||||
{ name: 'PostgreSQL', status: 'healthy', uptime: '99.99%', responseTime: '8ms' },
|
||||
{ name: 'Redis', status: 'healthy', uptime: '99.97%', responseTime: '2ms' },
|
||||
{ name: 'Celery Worker', status: 'healthy', uptime: '99.95%', responseTime: '-' },
|
||||
{ name: 'Object Storage', status: 'warning', uptime: '99.85%', responseTime: '120ms' },
|
||||
];
|
||||
|
||||
// API 请求统计
|
||||
const apiStats = [
|
||||
{ endpoint: 'POST /api/v1/auth/login', requests: 15420, avgTime: '48ms', errors: 12 },
|
||||
{ endpoint: 'GET /api/v1/projects', requests: 89340, avgTime: '32ms', errors: 5 },
|
||||
{ endpoint: 'POST /api/v1/projects', requests: 6780, avgTime: '156ms', errors: 8 },
|
||||
{ endpoint: 'GET /api/v1/assets', requests: 124560, avgTime: '68ms', errors: 23 },
|
||||
{ endpoint: 'POST /api/v1/subscriptions/subscribe', requests: 234, avgTime: '890ms', errors: 2 },
|
||||
];
|
||||
|
||||
// 错误日志
|
||||
const recentErrors = [
|
||||
{ time: '2024-06-17 16:45:32', level: 'ERROR', service: 'API', message: 'Database connection timeout' },
|
||||
{ time: '2024-06-17 16:42:15', level: 'WARN', service: 'Object Storage', message: 'Slow response detected (>1s)' },
|
||||
{ time: '2024-06-17 16:38:41', level: 'ERROR', service: 'Celery', message: 'Task retry limit exceeded' },
|
||||
];
|
||||
|
||||
const serviceColumns = [
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string) => <><CloudServerOutlined /> {text}</>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const config: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
||||
healthy: { color: 'success', icon: <CheckCircleOutlined />, text: '正常' },
|
||||
warning: { color: 'warning', icon: <SyncOutlined spin />, text: '警告' },
|
||||
error: { color: 'error', icon: <CloseCircleOutlined />, text: '故障' },
|
||||
};
|
||||
const { color, text } = config[status] || config.healthy;
|
||||
return <Badge status={color as any} text={text} />;
|
||||
},
|
||||
},
|
||||
{ title: '可用性', dataIndex: 'uptime', key: 'uptime' },
|
||||
{ title: '响应时间', dataIndex: 'responseTime', key: 'responseTime' },
|
||||
];
|
||||
|
||||
const apiColumns = [
|
||||
{ title: 'API 端点', dataIndex: 'endpoint', key: 'endpoint' },
|
||||
{ title: '请求数', dataIndex: 'requests', key: 'requests', sorter: (a: any, b: any) => a.requests - b.requests },
|
||||
{ title: '平均响应时间', dataIndex: 'avgTime', key: 'avgTime' },
|
||||
{
|
||||
title: '错误数',
|
||||
dataIndex: 'errors',
|
||||
key: 'errors',
|
||||
render: (errors: number) => (
|
||||
<Tag className={errors > 10 ? 'xx-tag error' : errors > 5 ? 'xx-tag warning' : 'xx-tag success'}>{errors}</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const errorColumns = [
|
||||
{ title: '时间', dataIndex: 'time', key: 'time', width: 180 },
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
width: 100,
|
||||
render: (level: string) => (
|
||||
<Tag className={level === 'ERROR' ? 'xx-tag error' : 'xx-tag warning'}>{level}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '服务', dataIndex: 'service', key: 'service', width: 120 },
|
||||
{ title: '错误信息', dataIndex: 'message', key: 'message' },
|
||||
];
|
||||
|
||||
const refreshMetrics = () => {
|
||||
setSystemMetrics({
|
||||
cpu: Math.floor(Math.random() * 30) + 30,
|
||||
memory: Math.floor(Math.random() * 20) + 55,
|
||||
disk: Math.floor(Math.random() * 10) + 45,
|
||||
network: Math.floor(Math.random() * 40) + 20,
|
||||
});
|
||||
setRefreshTime(new Date());
|
||||
};
|
||||
|
||||
// 模拟实时更新
|
||||
useEffect(() => {
|
||||
const interval = setInterval(refreshMetrics, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const getProgressColor = (value: number) => {
|
||||
if (value > 80) return '#ef4444';
|
||||
if (value > 60) return '#f59e0b';
|
||||
return '#10b981';
|
||||
};
|
||||
|
||||
const getProgressClass = (value: number) => {
|
||||
if (value > 80) return 'xx-progress-warning';
|
||||
if (value > 60) return 'xx-progress-warning';
|
||||
return 'xx-progress-success';
|
||||
};
|
||||
|
||||
const resourceMetrics = [
|
||||
{ key: 'cpu', label: 'CPU 使用率', value: systemMetrics.cpu, icon: <ThunderboltOutlined />, bg: 'linear-gradient(135deg, #6366f1, #4f46e5)' },
|
||||
{ key: 'memory', label: '内存使用率', value: systemMetrics.memory, icon: <DatabaseOutlined />, bg: 'linear-gradient(135deg, #34d399, #10b981)' },
|
||||
{ key: 'disk', label: '磁盘使用率', value: systemMetrics.disk, icon: <CloudServerOutlined />, bg: 'linear-gradient(135deg, #fbbf24, #f59e0b)' },
|
||||
{ key: 'network', label: '网络使用率', value: systemMetrics.network, icon: <ApiOutlined />, bg: 'linear-gradient(135deg, #a78bfa, #8b5cf6)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="system-monitor-page">
|
||||
<div className="xx-page-head">
|
||||
<div className="xx-page-head-content">
|
||||
<h2>系统监控</h2>
|
||||
<p>服务器资源、服务健康与 API 统计</p>
|
||||
</div>
|
||||
<div className="xx-page-head-actions">
|
||||
<Space>
|
||||
<span className="xx-refresh-time">上次更新: {refreshTime.toLocaleTimeString()}</span>
|
||||
<Button icon={<ReloadOutlined />} onClick={refreshMetrics} className="xx-primary-btn">
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统资源监控 */}
|
||||
<Row gutter={24} className="xx-grid-4">
|
||||
{resourceMetrics.map((metric) => (
|
||||
<Col span={6} key={metric.key}>
|
||||
<div className="xx-resource-card">
|
||||
<div
|
||||
className="xx-resource-card-icon"
|
||||
style={{ background: metric.bg, color: 'white' }}
|
||||
>
|
||||
{metric.icon}
|
||||
</div>
|
||||
<div className="xx-resource-card-label">{metric.label}</div>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={metric.value}
|
||||
strokeColor={getProgressColor(metric.value)}
|
||||
className={getProgressClass(metric.value)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 服务健康状态 */}
|
||||
<div className="xx-table-wrapper" style={{ marginBottom: 24 }}>
|
||||
<Card title="服务健康状态" className="xx-card">
|
||||
<Table
|
||||
columns={serviceColumns}
|
||||
dataSource={services}
|
||||
rowKey="name"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* API 请求统计 */}
|
||||
<div className="xx-table-wrapper" style={{ marginBottom: 24 }}>
|
||||
<Card title="API 请求统计 (最近 1 小时)" className="xx-card">
|
||||
<Table
|
||||
columns={apiColumns}
|
||||
dataSource={apiStats}
|
||||
rowKey="endpoint"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 最近错误 */}
|
||||
<div className="xx-table-wrapper">
|
||||
<Card title="最近错误日志" className="xx-card">
|
||||
<Table
|
||||
columns={errorColumns}
|
||||
dataSource={recentErrors}
|
||||
rowKey="time"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemMonitor;
|
||||
|
||||
export const Component = SystemMonitor;
|
||||
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* Admin 用户管理页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Table, Button, Space, Tag, Input, Card } from 'antd';
|
||||
import { SearchOutlined, LockOutlined, UnlockOutlined } from '@ant-design/icons';
|
||||
import './Admin.css';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
const UserManagement: React.FC = () => {
|
||||
// 模拟数据
|
||||
const users = [
|
||||
{
|
||||
id: '1',
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
is_email_verified: true,
|
||||
status: 'active',
|
||||
created_at: '2024-01-15',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
username: 'user2',
|
||||
email: 'user2@example.com',
|
||||
is_email_verified: false,
|
||||
status: 'active',
|
||||
created_at: '2024-02-20',
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '邮箱验证',
|
||||
dataIndex: 'is_email_verified',
|
||||
key: 'is_email_verified',
|
||||
render: (verified: boolean) => (
|
||||
<Tag className={verified ? 'xx-tag success' : 'xx-tag debug'}>
|
||||
{verified ? '已验证' : '未验证'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag className={status === 'active' ? 'xx-tag success' : 'xx-tag error'}>
|
||||
{status === 'active' ? '正常' : '已封禁'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" disabled>
|
||||
详情暂未开放
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger={record.status === 'active'}
|
||||
icon={record.status === 'active' ? <LockOutlined /> : <UnlockOutlined />}
|
||||
disabled
|
||||
>
|
||||
{record.status === 'active' ? '封禁暂未开放' : '解封暂未开放'}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="user-management-page">
|
||||
<div className="xx-page-head-simple">
|
||||
<h2>用户管理</h2>
|
||||
<p>管理系统用户与账户状态</p>
|
||||
</div>
|
||||
|
||||
<div className="xx-table-wrapper">
|
||||
<Card title="用户列表" className="xx-card">
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Search
|
||||
placeholder="搜索用户名或邮箱"
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
className="xx-search-input"
|
||||
style={{ width: 320 }}
|
||||
/>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={users} rowKey="id" />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagement;
|
||||
|
||||
export const Component = UserManagement;
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* 账号安全设置页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Alert } from 'antd';
|
||||
import { LockOutlined, MailOutlined } from '@ant-design/icons';
|
||||
import './ProfileSettings.css';
|
||||
|
||||
const AccountSecurity: React.FC = () => {
|
||||
const [passwordForm] = Form.useForm();
|
||||
const [emailForm] = Form.useForm();
|
||||
|
||||
const onPasswordSubmit = () => undefined;
|
||||
|
||||
const onEmailSubmit = () => undefined;
|
||||
|
||||
|
||||
return (
|
||||
<div className="xx-settings-page">
|
||||
<div className="xx-settings-card">
|
||||
<h3>修改密码</h3>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 20 }}
|
||||
message="账号安全设置暂未开放"
|
||||
description="当前后端尚未提供登录态修改密码/邮箱接口,避免假成功,入口暂时禁用。"
|
||||
/>
|
||||
<Form form={passwordForm} layout="vertical" onFinish={onPasswordSubmit}>
|
||||
<Form.Item
|
||||
name="currentPassword"
|
||||
label="当前密码"
|
||||
rules={[{ required: true, message: '请输入当前密码' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 8, message: '密码至少 8 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认新密码"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled>
|
||||
修改密码暂未开放
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="xx-settings-card">
|
||||
<h3>修改邮箱</h3>
|
||||
<Form form={emailForm} layout="vertical" onFinish={onEmailSubmit}>
|
||||
<Form.Item
|
||||
name="newEmail"
|
||||
label="新邮箱地址"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<MailOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="确认密码"
|
||||
rules={[{ required: true, message: '请输入密码以确认修改' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled>
|
||||
修改邮箱暂未开放
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountSecurity;
|
||||
|
||||
export const Component = AccountSecurity;
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* 通知设置页面
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Form, Switch, Button, Divider, Alert } from 'antd';
|
||||
import './ProfileSettings.css';
|
||||
|
||||
|
||||
const NotificationSettings: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = () => undefined;
|
||||
|
||||
|
||||
return (
|
||||
<div className="xx-settings-page">
|
||||
<div className="xx-settings-card">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 20 }}
|
||||
message="通知偏好暂未开放"
|
||||
description="当前后端尚未提供通知偏好保存接口,避免假成功,保存入口暂时禁用。"
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
emailNotifications: true,
|
||||
inviteNotifications: true,
|
||||
projectNotifications: false,
|
||||
marketingEmails: false,
|
||||
}}
|
||||
>
|
||||
<h3 className="xx-section-title">邮件通知</h3>
|
||||
<Form.Item
|
||||
name="emailNotifications"
|
||||
label="启用邮件通知"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
|
||||
<h3 className="xx-section-title">通知类型</h3>
|
||||
<Form.Item
|
||||
name="inviteNotifications"
|
||||
label="工作空间邀请通知"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="projectNotifications"
|
||||
label="项目更新通知"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="subscriptionNotifications"
|
||||
label="订阅和账单通知"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
|
||||
<h3 className="xx-section-title">营销</h3>
|
||||
<Form.Item
|
||||
name="marketingEmails"
|
||||
label="接收产品更新和优惠信息"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled>
|
||||
保存设置暂未开放
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationSettings;
|
||||
|
||||
export const Component = NotificationSettings;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Result } from 'antd';
|
||||
import './ProfileSettings.css';
|
||||
|
||||
const SessionManagement: React.FC = () => {
|
||||
return (
|
||||
<div className="xx-settings-page">
|
||||
<div className="xx-result-page">
|
||||
<Result
|
||||
status="info"
|
||||
title="会话管理暂未开放"
|
||||
subTitle="当前版本未接入会话列表、设备管理和远程登出后端服务,因此不展示模拟设备,也不会伪造登出操作。"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionManagement;
|
||||
|
||||
export const Component = SessionManagement;
|
||||
@@ -1,196 +0,0 @@
|
||||
/**
|
||||
* 工作空间详情页面
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Tabs, Button, Descriptions, Card, List, Modal, Form, Input, message, Alert } from 'antd';
|
||||
import { PlusOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
import { useWorkspace } from '@/hooks/useWorkspace';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { createProject, getProjects } from '@/api/projects';
|
||||
import MemberList from '@/components/business/MemberList';
|
||||
import InviteMemberModal from '@/components/business/InviteMemberModal';
|
||||
import PermissionMatrix from '@/components/business/PermissionMatrix';
|
||||
|
||||
const WorkspaceDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data: workspace, isLoading } = useWorkspace(id!);
|
||||
const queryClient = useQueryClient();
|
||||
const [inviteModalOpen, setInviteModalOpen] = useState(false);
|
||||
const [createProjectOpen, setCreateProjectOpen] = useState(false);
|
||||
const [projectForm] = Form.useForm<{ name: string; description?: string }>();
|
||||
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: ['projects', id],
|
||||
queryFn: () => getProjects(),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const createProjectMutation = useMutation({
|
||||
mutationFn: createProject,
|
||||
onSuccess: (project) => {
|
||||
message.success('项目创建成功');
|
||||
setCreateProjectOpen(false);
|
||||
projectForm.resetFields();
|
||||
queryClient.invalidateQueries({ queryKey: ['projects', id] });
|
||||
navigate(`/projects/${project.id}/assets`, { state: { workspaceId: id } });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
message.error(error.response?.data?.detail || '项目创建失败,请稍后重试');
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateProject = async () => {
|
||||
const values = await projectForm.validateFields();
|
||||
createProjectMutation.mutate({
|
||||
name: values.name,
|
||||
description: values.description || '',
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return <div>加载中...</div>;
|
||||
if (!workspace) return <div>工作空间不存在</div>;
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: '概览',
|
||||
children: (
|
||||
<div>
|
||||
<Card
|
||||
title="工作空间信息"
|
||||
style={{ marginBottom: 16 }}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => setCreateProjectOpen(true)}>
|
||||
创建项目
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="名称">{workspace.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="订阅计划">{workspace.subscription_plan}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{workspace.subscription_status}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{workspace.created_at
|
||||
? new Date(workspace.created_at).toLocaleDateString()
|
||||
: '暂未返回'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card title="项目" style={{ marginBottom: 16 }}>
|
||||
{projectsQuery.isError && (
|
||||
<Alert type="error" showIcon message="项目列表加载失败" style={{ marginBottom: 16 }} />
|
||||
)}
|
||||
<List
|
||||
loading={projectsQuery.isLoading}
|
||||
dataSource={projectsQuery.data || []}
|
||||
locale={{ emptyText: '还没有项目,请先创建项目再上传素材' }}
|
||||
renderItem={(project) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button key="assets" type="link" onClick={() => navigate(`/projects/${project.id}/assets`, { state: { workspaceId: id } })}>
|
||||
素材管理
|
||||
</Button>,
|
||||
<Button key="titles" type="link" onClick={() => navigate(`/projects/${project.id}/titles`, { state: { workspaceId: id } })}>
|
||||
标题库
|
||||
</Button>,
|
||||
<Button key="generation" type="link" onClick={() => navigate(`/projects/${project.id}/generation`, { state: { workspaceId: id } })}>
|
||||
视频生成
|
||||
</Button>,
|
||||
<Button key="tasks" type="link" onClick={() => navigate(`/projects/${project.id}/tasks`, { state: { workspaceId: id } })}>
|
||||
任务中心
|
||||
</Button>,
|
||||
<Button key="results" type="link" onClick={() => navigate(`/projects/${project.id}/results`, { state: { workspaceId: id } })}>
|
||||
生成结果
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta title={project.name} description={project.description || '暂无描述'} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="配额与订阅功能暂未开放"
|
||||
description="当前版本未接入订阅和配额后端服务,因此不展示模拟配额数据,也不会调用不存在的配额接口。"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'members',
|
||||
label: (
|
||||
<>
|
||||
<TeamOutlined /> 成员管理
|
||||
</>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setInviteModalOpen(true)}
|
||||
>
|
||||
邀请成员
|
||||
</Button>
|
||||
</div>
|
||||
<MemberList workspaceId={id!} />
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<PermissionMatrix />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: '设置',
|
||||
children: <div>工作空间设置(待开发)</div>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{workspace.name}</h1>
|
||||
<Tabs items={items} />
|
||||
<Modal
|
||||
title="创建项目"
|
||||
open={createProjectOpen}
|
||||
okText="创建"
|
||||
cancelText="取消"
|
||||
confirmLoading={createProjectMutation.isPending}
|
||||
onOk={handleCreateProject}
|
||||
onCancel={() => setCreateProjectOpen(false)}
|
||||
>
|
||||
<Form form={projectForm} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="项目名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入项目名称' },
|
||||
{ max: 100, message: '项目名称最多 100 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:618 宣传片" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="项目描述">
|
||||
<Input.TextArea placeholder="可选" rows={3} maxLength={500} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<InviteMemberModal
|
||||
workspaceId={id!}
|
||||
open={inviteModalOpen}
|
||||
onClose={() => setInviteModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceDetail;
|
||||
export const Component = WorkspaceDetail;
|
||||
@@ -17,8 +17,8 @@
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path alias */
|
||||
|
||||
@@ -138,17 +138,9 @@ def _download_library_assets(
|
||||
# 导入模型和会话
|
||||
try:
|
||||
from packages.adapters.sqlalchemy_impl.models import AssetModel
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from worker_app.db import SessionLocal
|
||||
|
||||
db_url = os.getenv("DATABASE_URL")
|
||||
if not db_url:
|
||||
logger.warning("DATABASE_URL not set, cannot fetch assets")
|
||||
return []
|
||||
|
||||
engine = create_engine(db_url)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
session = SessionLocal()
|
||||
|
||||
try:
|
||||
# 查询素材库中的视频素材
|
||||
|
||||
Reference in New Issue
Block a user