feat: V21 design system alignment for admin pages
CI/CD Pipeline / Validate Code Quality And Tests (push) Waiting to run
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Waiting to run

- Create unified Admin.css with V21 design tokens
- Update Dashboard with stat cards and V21 color scheme
- Update Analytics with V21 chart colors and metrics
- Update UserManagement with V21 table and search styles
- Update LogViewer with V21 filter bar and log level tags
- Update SystemMonitor with V21 progress and resource cards
- Update AdminComingSoon with V21 result styling
- Replace all inline styles with V21 CSS classes
- Apply Indigo/Green/Amber/Purple V21 color palette
This commit is contained in:
CI Test
2026-06-26 21:40:50 +08:00
parent 43d5f6e73f
commit 704d381919
7 changed files with 902 additions and 306 deletions
+517
View File
@@ -0,0 +1,517 @@
/* V21 Admin 页面样式 */
/* 页面容器 */
.dashboard-page,
.analytics-page,
.user-management-page,
.log-viewer-page,
.system-monitor-page,
.admin-coming-soon-page {
padding: 32px;
max-width: 1400px;
margin: 0 auto;
}
/* 页面头部 */
.xx-page-head {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09);
padding: 32px 40px;
margin-bottom: 32px;
display: flex;
justify-content: space-between;
align-items: center;
}
.xx-page-head-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.xx-page-head h2 {
font-size: 28px;
font-weight: 900;
color: var(--slate, #0f172a);
margin: 0;
letter-spacing: -0.02em;
}
.xx-page-head p {
font-size: 15px;
color: var(--muted, #64748b);
margin: 0;
}
.xx-page-head-actions {
display: flex;
gap: 12px;
align-items: center;
}
/* 简化页面头部(无操作按钮) */
.xx-page-head-simple {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09);
padding: 32px 40px;
margin-bottom: 32px;
}
.xx-page-head-simple h2 {
font-size: 28px;
font-weight: 900;
color: var(--slate, #0f172a);
margin: 0 0 8px;
letter-spacing: -0.02em;
}
.xx-page-head-simple p {
font-size: 15px;
color: var(--muted, #64748b);
margin: 0;
}
/* V21 卡片 */
.xx-card {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 22px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
padding: 24px;
transition: all 0.3s;
}
.xx-card:hover {
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
}
.xx-card .ant-card-head {
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
padding: 20px 24px;
}
.xx-card .ant-card-head-title {
font-weight: 800;
font-size: 17px;
color: var(--slate, #0f172a);
}
.xx-card .ant-card-body {
padding: 24px;
}
/* 统计卡片网格 - 4列 */
.xx-grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 32px;
}
@media (max-width: 1200px) {
.xx-grid-4 {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.xx-grid-4 {
grid-template-columns: 1fr;
}
}
/* 统计卡片 */
.xx-stat-card {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 22px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
padding: 24px;
transition: all 0.3s;
}
.xx-stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
}
.xx-stat-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.xx-stat-card-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.xx-stat-card-icon.primary {
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white;
}
.xx-stat-card-icon.success {
background: linear-gradient(135deg, #34d399, #10b981);
color: white;
}
.xx-stat-card-icon.warning {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: white;
}
.xx-stat-card-icon.purple {
background: linear-gradient(135deg, #a78bfa, #8b5cf6);
color: white;
}
.xx-stat-card-icon.info {
background: linear-gradient(135deg, #60a5fa, #3b82f6);
color: white;
}
.xx-stat-card-icon.orange {
background: linear-gradient(135deg, #fb923c, #f97316);
color: white;
}
.xx-stat-card-label {
font-size: 14px;
color: var(--muted, #64748b);
font-weight: 500;
margin-bottom: 8px;
}
.xx-stat-card-value {
font-size: 32px;
font-weight: 900;
color: var(--slate, #0f172a);
line-height: 1.2;
letter-spacing: -0.02em;
}
.xx-stat-card-value.primary {
color: var(--indigo, #4f46e5);
}
.xx-stat-card-value.success {
color: var(--green, #10b981);
}
.xx-stat-card-value.warning {
color: var(--amber, #f59e0b);
}
.xx-stat-card-value.purple {
color: #8b5cf6;
}
.xx-stat-card-growth {
font-size: 13px;
color: var(--green, #10b981);
font-weight: 600;
margin-top: 8px;
display: flex;
align-items: center;
gap: 4px;
}
/* 数据表格 */
.xx-table-wrapper {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 22px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
overflow: hidden;
}
.xx-table-wrapper .ant-table {
background: transparent;
}
.xx-table-wrapper .ant-table-thead > tr > th {
background: rgba(248, 250, 252, 0.8);
font-weight: 800;
font-size: 13px;
color: var(--slate, #0f172a);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 16px 20px;
}
.xx-table-wrapper .ant-table-tbody > tr > td {
padding: 16px 20px;
font-size: 14px;
}
.xx-table-wrapper .ant-table-tbody > tr:hover > td {
background: rgba(79, 70, 229, 0.03);
}
/* V21 按钮 */
.xx-primary-btn {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
color: white !important;
border: none !important;
border-radius: 14px !important;
font-weight: 700 !important;
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.25) !important;
transition: all 0.2s !important;
}
.xx-primary-btn:hover {
box-shadow: 0 12px 28px rgba(79, 70, 229, 0.3) !important;
transform: translateY(-1px);
}
.xx-ghost-btn {
background: white !important;
border: 1px solid rgba(226, 232, 240, 0.95) !important;
color: var(--slate, #0f172a) !important;
border-radius: 14px !important;
font-weight: 600 !important;
transition: all 0.2s !important;
}
.xx-ghost-btn:hover {
border-color: var(--indigo, #4f46e5) !important;
color: var(--indigo, #4f46e5) !important;
}
/* V21 输入框 */
.xx-search-input {
border-radius: 14px !important;
border: 1px solid rgba(226, 232, 240, 0.95) !important;
padding: 8px 16px !important;
}
.xx-search-input:hover,
.xx-search-input:focus {
border-color: var(--indigo, #4f46e5) !important;
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important;
}
/* V21 Select */
.xx-select {
border-radius: 14px !important;
}
.xx-select:hover,
.xx-select:focus {
border-color: var(--indigo, #4f46e5) !important;
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important;
}
/* V21 Tag */
.xx-tag {
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 12px !important;
padding: 4px 10px !important;
}
.xx-tag.info {
background: rgba(59, 130, 246, 0.1) !important;
color: #3b82f6 !important;
border: 1px solid rgba(59, 130, 246, 0.2) !important;
}
.xx-tag.success {
background: rgba(16, 185, 129, 0.1) !important;
color: #10b981 !important;
border: 1px solid rgba(16, 185, 129, 0.2) !important;
}
.xx-tag.warning {
background: rgba(245, 158, 11, 0.1) !important;
color: #f59e0b !important;
border: 1px solid rgba(245, 158, 11, 0.2) !important;
}
.xx-tag.error {
background: rgba(239, 68, 68, 0.1) !important;
color: #ef4444 !important;
border: 1px solid rgba(239, 68, 68, 0.2) !important;
}
.xx-tag.debug {
background: rgba(100, 116, 139, 0.1) !important;
color: #64748b !important;
border: 1px solid rgba(100, 116, 139, 0.2) !important;
}
/* V21 Progress */
.xx-progress-primary .ant-progress-circle .ant-progress-text {
color: var(--indigo, #4f46e5) !important;
font-weight: 700 !important;
}
.xx-progress-success .ant-progress-circle .ant-progress-text {
color: var(--green, #10b981) !important;
font-weight: 700 !important;
}
.xx-progress-warning .ant-progress-circle .ant-progress-text {
color: var(--amber, #f59e0b) !important;
font-weight: 700 !important;
}
/* 图表容器 */
.xx-chart-container {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 22px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
padding: 24px;
}
.xx-chart-container .ant-card-head {
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
padding: 20px 24px;
}
.xx-chart-container .ant-card-head-title {
font-weight: 800;
font-size: 17px;
color: var(--slate, #0f172a);
}
/* 筛选器区域 */
.xx-filter-bar {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 22px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
padding: 20px 24px;
margin-bottom: 24px;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
/* 资源监控卡片 */
.xx-resource-card {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: 22px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
padding: 24px;
text-align: center;
}
.xx-resource-card-icon {
width: 64px;
height: 64px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
font-size: 28px;
}
.xx-resource-card-label {
font-size: 14px;
color: var(--muted, #64748b);
font-weight: 600;
margin-bottom: 16px;
}
/* 日期选择器 */
.xx-date-picker {
border-radius: 14px !important;
}
/* Drawer */
.xx-drawer .ant-drawer-header {
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
padding: 20px 24px;
}
.xx-drawer .ant-drawer-title {
font-weight: 800;
font-size: 18px;
color: var(--slate, #0f172a);
}
/* 日志详情 */
.xx-log-detail {
padding: 4px 0;
}
.xx-log-detail-label {
font-size: 13px;
font-weight: 700;
color: var(--slate, #0f172a);
margin-bottom: 4px;
}
.xx-log-detail-value {
font-size: 14px;
color: var(--muted, #64748b);
background: rgba(248, 250, 252, 0.8);
padding: 12px;
border-radius: 10px;
margin-bottom: 16px;
}
.xx-log-detail-code {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
background: rgba(248, 250, 252, 0.8);
padding: 12px;
border-radius: 10px;
white-space: pre-wrap;
word-break: break-all;
}
/* Result 页面居中 */
.xx-result-center {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.xx-result-center .ant-result {
padding: 48px;
}
/* 刷新时间显示 */
.xx-refresh-time {
font-size: 13px;
color: var(--muted, #64748b);
margin-right: 12px;
}
/* 图表配色覆盖 */
.recharts-text {
fill: #64748b !important;
font-size: 12px !important;
}
.recharts-cartesian-grid-horizontal line,
.recharts-cartesian-grid-vertical line {
stroke: rgba(226, 232, 240, 0.8) !important;
}
/* 图表 Legend */
.recharts-legend-wrapper {
padding-top: 16px !important;
}
.recharts-legend-item-text {
color: #64748b !important;
font-size: 13px !important;
}
+21 -13
View File
@@ -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>
);
};
+135 -80
View File
@@ -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>
);
};
+42 -24
View File
@@ -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>
);
};
+85 -88
View File
@@ -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>
+79 -86
View File
@@ -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>
);
};
+23 -15
View File
@@ -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>
);
};