feat: V21 Admin 页面样式对齐 (#31)
This commit is contained in:
@@ -1,24 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Button, Card, Result } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './Admin.css';
|
||||
|
||||
const AdminComingSoon: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<Card>
|
||||
<Result
|
||||
status="info"
|
||||
title="Admin 后台暂未开放"
|
||||
subTitle="当前版本未接入后台用户、监控、日志、分析等后端服务,因此不展示模拟运营数据,也不会提供假操作入口。"
|
||||
extra={[
|
||||
<Button key="workspaces" type="primary" onClick={() => navigate('/workspaces')}>
|
||||
返回工作空间
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<div className="admin-coming-soon-page">
|
||||
<div className="xx-result-center">
|
||||
<Card className="xx-card">
|
||||
<Result
|
||||
status="info"
|
||||
title="Admin 后台暂未开放"
|
||||
subTitle="当前版本未接入后台用户、监控、日志、分析等后端服务,因此不展示模拟运营数据,也不会提供假操作入口。"
|
||||
extra={[
|
||||
<Button
|
||||
key="workspaces"
|
||||
type="primary"
|
||||
onClick={() => navigate('/workspaces')}
|
||||
className="xx-primary-btn"
|
||||
>
|
||||
返回工作空间
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,9 +24,22 @@ import {
|
||||
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 = [
|
||||
@@ -50,17 +63,17 @@ const Analytics: React.FC = () => {
|
||||
|
||||
// 订阅计划分布
|
||||
const planDistribution = [
|
||||
{ name: 'Free', value: 485, color: '#8884d8' },
|
||||
{ name: 'Pro', value: 120, color: '#82ca9d' },
|
||||
{ name: 'Enterprise', value: 15, color: '#ffc658' },
|
||||
{ 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 /> },
|
||||
{ metric: '月活跃用户 (MAU)', value: 3456, growth: '+8.3%', icon: <ArrowUpOutlined /> },
|
||||
{ metric: '本月收入', value: 13365, prefix: '¥', growth: '+51.6%', icon: <DollarOutlined /> },
|
||||
{ metric: '活跃项目数', value: 2341, growth: '+18.2%', icon: <ProjectOutlined /> },
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
// 用户留存表格
|
||||
@@ -80,107 +93,149 @@ const Analytics: React.FC = () => {
|
||||
{ title: 'Day 90', dataIndex: 'day90', key: 'day90' },
|
||||
];
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
primary: '#4f46e5',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
purple: '#8b5cf6',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1>数据分析</h1>
|
||||
<Space>
|
||||
<RangePicker />
|
||||
</Space>
|
||||
<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={16} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={24} className="xx-grid-4">
|
||||
{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 }}>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 图表区域 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={24} 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>
|
||||
<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}>
|
||||
<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 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={16} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={24} 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>
|
||||
<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>
|
||||
|
||||
{/* 用户留存表 */}
|
||||
<Card title="用户留存率 (Cohort Analysis)">
|
||||
<Table
|
||||
columns={retentionColumns}
|
||||
dataSource={retentionData}
|
||||
rowKey="cohort"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
<div className="xx-table-wrapper">
|
||||
<Card title="用户留存率 (Cohort Analysis)">
|
||||
<Table
|
||||
columns={retentionColumns}
|
||||
dataSource={retentionData}
|
||||
rowKey="cohort"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,14 +9,15 @@ import {
|
||||
DollarOutlined,
|
||||
RiseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './Admin.css';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
// 模拟数据
|
||||
const stats = [
|
||||
{ title: '总用户数', value: 1234, icon: <UserOutlined />, color: '#1890ff' },
|
||||
{ title: '付费用户', value: 156, icon: <CrownOutlined />, color: '#52c41a' },
|
||||
{ title: '月收入', value: 15444, prefix: '¥', icon: <DollarOutlined />, color: '#faad14' },
|
||||
{ title: '活跃用户', value: 892, icon: <RiseOutlined />, color: '#722ed1' },
|
||||
{ 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 = [
|
||||
@@ -31,34 +32,51 @@ const Dashboard: React.FC = () => {
|
||||
{ title: '注册时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
];
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
primary: '#4f46e5',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
purple: '#8b5cf6',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>Dashboard</h1>
|
||||
<div className="dashboard-page">
|
||||
<div className="xx-page-head-simple">
|
||||
<h2>Dashboard</h2>
|
||||
<p>系统数据总览与关键指标</p>
|
||||
</div>
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={24} className="xx-grid-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Col span={6} key={index}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={stat.title}
|
||||
value={stat.value}
|
||||
prefix={stat.prefix}
|
||||
valueStyle={{ color: stat.color }}
|
||||
suffix={stat.icon}
|
||||
/>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
<Card title="最近注册用户">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={recentUsers}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
<div className="xx-table-wrapper">
|
||||
<Card title="最近注册用户" className="xx-card">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={recentUsers}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import './Admin.css';
|
||||
|
||||
const { Search } = Input;
|
||||
const { RangePicker } = DatePicker;
|
||||
@@ -118,14 +119,14 @@ const LogViewer: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
INFO: 'blue',
|
||||
WARN: 'orange',
|
||||
ERROR: 'red',
|
||||
DEBUG: 'default',
|
||||
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 colors[level] || 'default';
|
||||
return classes[level] || 'xx-tag debug';
|
||||
};
|
||||
|
||||
const handleViewDetails = (log: LogEntry) => {
|
||||
@@ -146,7 +147,7 @@ const LogViewer: React.FC = () => {
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
width: 100,
|
||||
render: (level: string) => <Tag color={getLevelColor(level)}>{level}</Tag>,
|
||||
render: (level: string) => <Tag className={getLevelTagClass(level)}>{level}</Tag>,
|
||||
filters: [
|
||||
{ text: 'INFO', value: 'INFO' },
|
||||
{ text: 'WARN', value: 'WARN' },
|
||||
@@ -205,53 +206,61 @@ const LogViewer: React.FC = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>日志查看器</h1>
|
||||
<div className="log-viewer-page">
|
||||
<div className="xx-page-head-simple">
|
||||
<h2>日志查看器</h2>
|
||||
<p>系统日志查询与监控</p>
|
||||
</div>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Space size="middle" wrap>
|
||||
<Search
|
||||
placeholder="搜索日志内容..."
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
<div className="xx-filter-bar">
|
||||
<Search
|
||||
placeholder="搜索日志内容..."
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
className="xx-search-input"
|
||||
style={{ width: 280 }}
|
||||
/>
|
||||
|
||||
<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="日志级别"
|
||||
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="服务"
|
||||
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="ObjectStorage">Object Storage</Option>
|
||||
<Option value="Redis">Redis</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 />
|
||||
<RangePicker showTime className="xx-date-picker" />
|
||||
|
||||
<Button icon={<FilterOutlined />} disabled>应用过滤暂未开放</Button>
|
||||
<Button icon={<DownloadOutlined />} disabled>导出日志暂未开放</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Button icon={<FilterOutlined />} disabled className="xx-ghost-btn">
|
||||
应用过滤暂未开放
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} disabled className="xx-ghost-btn">
|
||||
导出日志暂未开放
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="xx-table-wrapper">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={logs}
|
||||
@@ -264,7 +273,7 @@ const LogViewer: React.FC = () => {
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 日志详情抽屉 */}
|
||||
<Drawer
|
||||
@@ -273,67 +282,55 @@ const LogViewer: React.FC = () => {
|
||||
width={600}
|
||||
open={drawerVisible}
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
className="xx-drawer"
|
||||
>
|
||||
{selectedLog && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>时间:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.timestamp}</div>
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">时间</div>
|
||||
<div className="xx-log-detail-value">{selectedLog.timestamp}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>级别:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Tag color={getLevelColor(selectedLog.level)}>{selectedLog.level}</Tag>
|
||||
<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 style={{ marginBottom: 16 }}>
|
||||
<strong>服务:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.service}</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 style={{ marginBottom: 16 }}>
|
||||
<strong>用户:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.user}</div>
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">用户</div>
|
||||
<div className="xx-log-detail-value">{selectedLog.user}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>操作:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.action}</div>
|
||||
<div className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">操作</div>
|
||||
<div className="xx-log-detail-value">{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 className="xx-log-detail">
|
||||
<div className="xx-log-detail-label">详细信息</div>
|
||||
<div className="xx-log-detail-code">{selectedLog.details}</div>
|
||||
</div>
|
||||
|
||||
{selectedLog.ip && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>IP 地址:</strong>
|
||||
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.ip}</div>
|
||||
<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 style={{ marginBottom: 16 }}>
|
||||
<strong>Request ID:</strong>
|
||||
<div style={{ marginTop: 4, fontFamily: 'monospace', color: '#595959' }}>
|
||||
{selectedLog.request_id}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CloudServerOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './Admin.css';
|
||||
|
||||
const SystemMonitor: React.FC = () => {
|
||||
const [refreshTime, setRefreshTime] = useState(new Date());
|
||||
@@ -84,7 +85,7 @@ const SystemMonitor: React.FC = () => {
|
||||
dataIndex: 'errors',
|
||||
key: 'errors',
|
||||
render: (errors: number) => (
|
||||
<Tag color={errors > 10 ? 'red' : errors > 5 ? 'orange' : 'green'}>{errors}</Tag>
|
||||
<Tag className={errors > 10 ? 'xx-tag error' : errors > 5 ? 'xx-tag warning' : 'xx-tag success'}>{errors}</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -97,7 +98,7 @@ const SystemMonitor: React.FC = () => {
|
||||
key: 'level',
|
||||
width: 100,
|
||||
render: (level: string) => (
|
||||
<Tag color={level === 'ERROR' ? 'red' : 'orange'}>{level}</Tag>
|
||||
<Tag className={level === 'ERROR' ? 'xx-tag error' : 'xx-tag warning'}>{level}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '服务', dataIndex: 'service', key: 'service', width: 120 },
|
||||
@@ -117,111 +118,103 @@ const SystemMonitor: React.FC = () => {
|
||||
// 模拟实时更新
|
||||
useEffect(() => {
|
||||
const interval = setInterval(refreshMetrics, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const getProgressColor = (value: number) => {
|
||||
if (value > 80) return '#ff4d4f';
|
||||
if (value > 60) return '#faad14';
|
||||
return '#52c41a';
|
||||
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 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 />} onClick={refreshMetrics}>刷新</Button>
|
||||
</Space>
|
||||
<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={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>
|
||||
<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={systemMetrics.cpu}
|
||||
strokeColor={getProgressColor(systemMetrics.cpu)}
|
||||
percent={metric.value}
|
||||
strokeColor={getProgressColor(metric.value)}
|
||||
className={getProgressClass(metric.value)}
|
||||
/>
|
||||
</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>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 服务健康状态 */}
|
||||
<Card title="服务健康状态" style={{ marginBottom: 24 }}>
|
||||
<Table
|
||||
columns={serviceColumns}
|
||||
dataSource={services}
|
||||
rowKey="name"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
<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 请求统计 */}
|
||||
<Card title="API 请求统计 (最近 1 小时)" style={{ marginBottom: 24 }}>
|
||||
<Table
|
||||
columns={apiColumns}
|
||||
dataSource={apiStats}
|
||||
rowKey="endpoint"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
{/* 最近错误 */}
|
||||
<Card title="最近错误日志">
|
||||
<Table
|
||||
columns={errorColumns}
|
||||
dataSource={recentErrors}
|
||||
rowKey="time"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
<div className="xx-table-wrapper">
|
||||
<Card title="最近错误日志" className="xx-card">
|
||||
<Table
|
||||
columns={errorColumns}
|
||||
dataSource={recentErrors}
|
||||
rowKey="time"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
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;
|
||||
|
||||
@@ -44,7 +45,7 @@ const UserManagement: React.FC = () => {
|
||||
dataIndex: 'is_email_verified',
|
||||
key: 'is_email_verified',
|
||||
render: (verified: boolean) => (
|
||||
<Tag color={verified ? 'success' : 'default'}>
|
||||
<Tag className={verified ? 'xx-tag success' : 'xx-tag debug'}>
|
||||
{verified ? '已验证' : '未验证'}
|
||||
</Tag>
|
||||
),
|
||||
@@ -54,7 +55,7 @@ const UserManagement: React.FC = () => {
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={status === 'active' ? 'success' : 'error'}>
|
||||
<Tag className={status === 'active' ? 'xx-tag success' : 'xx-tag error'}>
|
||||
{status === 'active' ? '正常' : '已封禁'}
|
||||
</Tag>
|
||||
),
|
||||
@@ -87,19 +88,26 @@ const UserManagement: React.FC = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h1>用户管理</h1>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Search
|
||||
placeholder="搜索用户名或邮箱"
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={users} rowKey="id" />
|
||||
</Card>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user