Phase 6: Complete admin pages (Analytics, Monitor, Logs) and Phase 7: Asset management foundation
Phase 6 completion: - Add Analytics page with charts (user growth, revenue, retention) - Add SystemMonitor page (CPU, memory, services health) - Add LogViewer page (log search, filtering, details) - Update sidebar navigation with Admin submenu - Add recharts to package.json for data visualization Phase 7 Day 1-2: - Create Asset and AssetLibrary domain entities - Define AssetRepository and AssetLibraryRepository interfaces - Implement InMemory repositories for testing - Implement PostgreSQL repository adapter - Update SQLAlchemy models for assets and asset_libraries - Add database migration script 004_asset_management.sql Architecture: Strict Clean Architecture compliance Testing: InMemory adapters ready for unit tests Database: Migration script with indexes and foreign keys
This commit is contained in:
@@ -9,6 +9,10 @@ import {
|
||||
TeamOutlined,
|
||||
CrownOutlined,
|
||||
UserOutlined,
|
||||
DashboardOutlined,
|
||||
BarChartOutlined,
|
||||
MonitorOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -48,6 +52,43 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
||||
label: '订阅管理',
|
||||
onClick: () => navigate('/subscription'),
|
||||
},
|
||||
{
|
||||
key: '/admin',
|
||||
icon: <DashboardOutlined />,
|
||||
label: 'Admin',
|
||||
children: [
|
||||
{
|
||||
key: '/admin',
|
||||
icon: <DashboardOutlined />,
|
||||
label: 'Dashboard',
|
||||
onClick: () => navigate('/admin'),
|
||||
},
|
||||
{
|
||||
key: '/admin/users',
|
||||
icon: <TeamOutlined />,
|
||||
label: '用户管理',
|
||||
onClick: () => navigate('/admin/users'),
|
||||
},
|
||||
{
|
||||
key: '/admin/analytics',
|
||||
icon: <BarChartOutlined />,
|
||||
label: '数据分析',
|
||||
onClick: () => navigate('/admin/analytics'),
|
||||
},
|
||||
{
|
||||
key: '/admin/monitor',
|
||||
icon: <MonitorOutlined />,
|
||||
label: '系统监控',
|
||||
onClick: () => navigate('/admin/monitor'),
|
||||
},
|
||||
{
|
||||
key: '/admin/logs',
|
||||
icon: <FileTextOutlined />,
|
||||
label: '日志查看',
|
||||
onClick: () => navigate('/admin/logs'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '/profile',
|
||||
icon: <UserOutlined />,
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 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 {
|
||||
TrendingUpOutlined,
|
||||
UserAddOutlined,
|
||||
DollarOutlined,
|
||||
ProjectOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
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: '#8884d8' },
|
||||
{ name: 'Pro', value: 120, color: '#82ca9d' },
|
||||
{ name: 'Enterprise', value: 15, color: '#ffc658' },
|
||||
];
|
||||
|
||||
// 活跃度统计
|
||||
const activityStats = [
|
||||
{ metric: '日活跃用户 (DAU)', value: 892, growth: '+12.5%', icon: <UserAddOutlined /> },
|
||||
{ metric: '月活跃用户 (MAU)', value: 3456, growth: '+8.3%', icon: <TrendingUpOutlined /> },
|
||||
{ metric: '本月收入', value: 13365, prefix: '¥', growth: '+51.6%', icon: <DollarOutlined /> },
|
||||
{ metric: '活跃项目数', value: 2341, growth: '+18.2%', icon: <ProjectOutlined /> },
|
||||
];
|
||||
|
||||
// 用户留存表格
|
||||
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' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1>数据分析</h1>
|
||||
<Space>
|
||||
<RangePicker />
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 关键指标 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
{activityStats.map((stat, index) => (
|
||||
<Col span={6} key={index}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={stat.metric}
|
||||
value={stat.value}
|
||||
prefix={stat.prefix}
|
||||
suffix={stat.icon}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8, color: '#52c41a', fontSize: 14 }}>
|
||||
{stat.growth} vs 上月
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 图表区域 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
{/* 用户增长趋势 */}
|
||||
<Col span={16}>
|
||||
<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="#8884d8" name="总用户" />
|
||||
<Line type="monotone" dataKey="paid" stroke="#82ca9d" name="付费用户" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 订阅计划分布 */}
|
||||
<Col span={8}>
|
||||
<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>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 营收趋势 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={24}>
|
||||
<Card title="营收趋势">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={revenueData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="revenue" fill="#ffc658" name="收入 (¥)" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 用户留存表 */}
|
||||
<Card title="用户留存率 (Cohort Analysis)">
|
||||
<Table
|
||||
columns={retentionColumns}
|
||||
dataSource={retentionData}
|
||||
rowKey="cohort"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics;
|
||||
|
||||
export const Component = Analytics;
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 日志查看器
|
||||
*/
|
||||
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';
|
||||
|
||||
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: 'MinIO',
|
||||
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: 'Workspace Created',
|
||||
details: 'New workspace "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 "workspace:123" not found, fetching from DB',
|
||||
request_id: 'req_vwx234',
|
||||
},
|
||||
];
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
INFO: 'blue',
|
||||
WARN: 'orange',
|
||||
ERROR: 'red',
|
||||
DEBUG: 'default',
|
||||
};
|
||||
return colors[level] || 'default';
|
||||
};
|
||||
|
||||
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 color={getLevelColor(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: 'MinIO', value: 'MinIO' },
|
||||
{ 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 style={{ padding: '24px' }}>
|
||||
<h1>日志查看器</h1>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Space size="middle" wrap>
|
||||
<Search
|
||||
placeholder="搜索日志内容..."
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder="日志级别"
|
||||
style={{ width: 120 }}
|
||||
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="服务"
|
||||
style={{ width: 120 }}
|
||||
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="MinIO">MinIO</Option>
|
||||
<Option value="Redis">Redis</Option>
|
||||
</Select>
|
||||
|
||||
<RangePicker showTime />
|
||||
|
||||
<Button icon={<FilterOutlined />}>应用过滤</Button>
|
||||
<Button icon={<DownloadOutlined />}>导出日志</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={logs}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
total: logs.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条日志`,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 日志详情抽屉 */}
|
||||
<Drawer
|
||||
title="日志详情"
|
||||
placement="right"
|
||||
width={600}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
>
|
||||
{selectedLog && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>时间:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.timestamp}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>级别:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Tag color={getLevelColor(selectedLog.level)}>{selectedLog.level}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>服务:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.service}</div>
|
||||
</div>
|
||||
|
||||
{selectedLog.user && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>用户:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.user}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>操作:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.action}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>详细信息:</strong>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 4,
|
||||
color: '#595959',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{selectedLog.details}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedLog.ip && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>IP 地址:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.ip}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedLog.request_id && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>Request ID:</strong>
|
||||
<div style={{ marginTop: 4, fontFamily: 'monospace', color: '#595959' }}>
|
||||
{selectedLog.request_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogViewer;
|
||||
|
||||
export const Component = LogViewer;
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* 系统监控页面
|
||||
*/
|
||||
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';
|
||||
|
||||
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: 'MinIO', 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/workspaces', 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: 'MinIO', 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, icon, text } = config[status] || config.healthy;
|
||||
return <Badge status={color as any} icon={icon} 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 color={errors > 10 ? 'red' : errors > 5 ? 'orange' : 'green'}>{errors}</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const errorColumns = [
|
||||
{ title: '时间', dataIndex: 'time', key: 'time', width: 180 },
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
width: 100,
|
||||
render: (level: string) => (
|
||||
<Tag color={level === 'ERROR' ? 'red' : 'orange'}>{level}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '服务', dataIndex: 'service', key: 'service', width: 120 },
|
||||
{ title: '错误信息', dataIndex: 'message', key: 'message' },
|
||||
];
|
||||
|
||||
// 模拟实时更新
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
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());
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const getProgressColor = (value: number) => {
|
||||
if (value > 80) return '#ff4d4f';
|
||||
if (value > 60) return '#faad14';
|
||||
return '#52c41a';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1>系统监控</h1>
|
||||
<Space>
|
||||
<span style={{ color: '#8c8c8c' }}>上次更新: {refreshTime.toLocaleTimeString()}</span>
|
||||
<Button icon={<ReloadOutlined />}>刷新</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 系统资源监控 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<ThunderboltOutlined style={{ fontSize: 32, color: '#1890ff', marginBottom: 8 }} />
|
||||
<div style={{ marginBottom: 16 }}>CPU 使用率</div>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={systemMetrics.cpu}
|
||||
strokeColor={getProgressColor(systemMetrics.cpu)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<DatabaseOutlined style={{ fontSize: 32, color: '#52c41a', marginBottom: 8 }} />
|
||||
<div style={{ marginBottom: 16 }}>内存使用率</div>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={systemMetrics.memory}
|
||||
strokeColor={getProgressColor(systemMetrics.memory)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<CloudServerOutlined style={{ fontSize: 32, color: '#faad14', marginBottom: 8 }} />
|
||||
<div style={{ marginBottom: 16 }}>磁盘使用率</div>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={systemMetrics.disk}
|
||||
strokeColor={getProgressColor(systemMetrics.disk)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<ApiOutlined style={{ fontSize: 32, color: '#722ed1', marginBottom: 8 }} />
|
||||
<div style={{ marginBottom: 16 }}>网络使用率</div>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={systemMetrics.network}
|
||||
strokeColor={getProgressColor(systemMetrics.network)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 服务健康状态 */}
|
||||
<Card title="服务健康状态" style={{ marginBottom: 24 }}>
|
||||
<Table
|
||||
columns={serviceColumns}
|
||||
dataSource={services}
|
||||
rowKey="name"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* API 请求统计 */}
|
||||
<Card title="API 请求统计 (最近 1 小时)" style={{ marginBottom: 24 }}>
|
||||
<Table
|
||||
columns={apiColumns}
|
||||
dataSource={apiStats}
|
||||
rowKey="endpoint"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 最近错误 */}
|
||||
<Card title="最近错误日志">
|
||||
<Table
|
||||
columns={errorColumns}
|
||||
dataSource={recentErrors}
|
||||
rowKey="time"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemMonitor;
|
||||
|
||||
export const Component = SystemMonitor;
|
||||
@@ -81,6 +81,18 @@ export const router = createBrowserRouter([
|
||||
path: 'users',
|
||||
lazy: () => import('@/pages/admin/UserManagement'),
|
||||
},
|
||||
{
|
||||
path: 'analytics',
|
||||
lazy: () => import('@/pages/admin/Analytics'),
|
||||
},
|
||||
{
|
||||
path: 'monitor',
|
||||
lazy: () => import('@/pages/admin/SystemMonitor'),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
lazy: () => import('@/pages/admin/LogViewer'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user