feat: server ops panel + dashboard server widgets
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 7s

This commit is contained in:
WangDL 2026-05-22 13:30:37 +08:00
parent 274a27a10a
commit 26f5750046
6 changed files with 133 additions and 2 deletions

View File

@ -10,6 +10,7 @@ import PageLoading from './components/PageLoading'
import AdminLayout from './layouts/AdminLayout' import AdminLayout from './layouts/AdminLayout'
const Login = lazy(() => import('./pages/Login')) const Login = lazy(() => import('./pages/Login'))
const ServersPage = lazy(() => import("./pages/Servers"))
const AuditLogPage = lazy(() => import("./pages/AuditLog")) const AuditLogPage = lazy(() => import("./pages/AuditLog"))
const Dashboard = lazy(() => import('./pages/Dashboard')) const Dashboard = lazy(() => import('./pages/Dashboard'))
const UserManagement = lazy(() => import('./pages/UserManagement')) const UserManagement = lazy(() => import('./pages/UserManagement'))
@ -80,6 +81,14 @@ function App() {
</PermissionGuard> </PermissionGuard>
} }
/> />
<Route
path="servers"
element={
<PermissionGuard requiredRole="SUPER_ADMIN">
<Suspense fallback={<PageLoading />}><ServersPage /></Suspense>
</PermissionGuard>
}
/>
<Route <Route
path="audit" path="audit"
element={ element={

View File

@ -1,5 +1,5 @@
import type React from 'react' import type React from 'react'
import { RobotOutlined, DashboardOutlined, import { CloudServerOutlined, RobotOutlined, DashboardOutlined,
UserOutlined, UserOutlined,
BookOutlined, BookOutlined,
ImportOutlined, ImportOutlined,
@ -47,6 +47,7 @@ export const adminMenuItems: AdminMenuItem[] = [
{ path: '/ai-costs', name: 'AI 调用与成本', icon: <CloudOutlined /> }, { path: '/ai-costs', name: 'AI 调用与成本', icon: <CloudOutlined /> },
{ path: '/files', name: '文件与 COS', icon: <FileOutlined /> }, { path: '/files', name: '文件与 COS', icon: <FileOutlined /> },
{ path: '/settings', name: '系统配置', icon: <SettingOutlined />, requiredRole: 'ADMIN' }, { path: '/settings', name: '系统配置', icon: <SettingOutlined />, requiredRole: 'ADMIN' },
{ path: '/servers', name: '服务器运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN' },
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' }, { path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
] ]

View File

@ -21,6 +21,7 @@ const breadcrumbMap: Record<string, string> = {
'/ai-costs': 'AI 调用与成本', '/ai-costs': 'AI 调用与成本',
'/files': '文件与 COS', '/files': '文件与 COS',
'/settings': '系统配置', '/settings': '系统配置',
'/servers': '服务器运维',
'/audit': '审计日志', '/audit': '审计日志',
} }

View File

@ -6,11 +6,14 @@ import * as echarts from 'echarts/core'
import { LineChart, BarChart } from 'echarts/charts' import { LineChart, BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, TitleComponent, LegendComponent } from 'echarts/components' import { GridComponent, TooltipComponent, TitleComponent, LegendComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers' import { CanvasRenderer } from 'echarts/renderers'
import { UserOutlined, BookOutlined, CloudOutlined, FileOutlined } from '@ant-design/icons' import { UserOutlined, BookOutlined, CloudOutlined, FileOutlined, ClusterOutlined } from '@ant-design/icons'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import MetricCard from '@/components/MetricCard' import MetricCard from '@/components/MetricCard'
import { Progress, Space } from 'antd'
const DText = Typography.Text
import EChartsChartContainer from '@/components/EChartsChartContainer' import EChartsChartContainer from '@/components/EChartsChartContainer'
import { getDashboardStats } from '@/services/admin-api' import { getDashboardStats } from '@/services/admin-api'
import { getServerMetrics } from '@/services/server-api'
echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, CanvasRenderer]) echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, CanvasRenderer])
@ -27,6 +30,12 @@ export default function Dashboard() {
staleTime: 60_000, staleTime: 60_000,
}) })
const { data: serverData } = useQuery({
queryKey: ['servers', 'metrics'],
queryFn: getServerMetrics,
refetchInterval: 10_000,
})
const userTrendOption = useMemo(() => ({ const userTrendOption = useMemo(() => ({
grid: { top: 20, right: 20, bottom: 20, left: 40 }, grid: { top: 20, right: 20, bottom: 20, left: 40 },
tooltip: { trigger: 'axis' as const }, tooltip: { trigger: 'axis' as const },
@ -56,6 +65,25 @@ export default function Dashboard() {
<Col xs={24} lg={12}><EChartsChartContainer title="日活用户趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.userTrend?.length}><ReactEChartsCore echarts={echarts} option={userTrendOption} style={{ height: 300 }} notMerge lazyUpdate /></EChartsChartContainer></Col> <Col xs={24} lg={12}><EChartsChartContainer title="日活用户趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.userTrend?.length}><ReactEChartsCore echarts={echarts} option={userTrendOption} style={{ height: 300 }} notMerge lazyUpdate /></EChartsChartContainer></Col>
<Col xs={24} lg={12}><EChartsChartContainer title="AI 调用趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.aiCallTrend?.length}><ReactEChartsCore echarts={echarts} option={aiCallTrendOption} style={{ height: 300 }} notMerge /></EChartsChartContainer></Col> <Col xs={24} lg={12}><EChartsChartContainer title="AI 调用趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.aiCallTrend?.length}><ReactEChartsCore echarts={echarts} option={aiCallTrendOption} style={{ height: 300 }} notMerge /></EChartsChartContainer></Col>
</Row> </Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
{serverData?.servers?.map(s => (
<Col xs={24} sm={12} lg={12} key={s.hostname}>
<div style={{ background: '#fff', borderRadius: 8, padding: '12px 16px', border: '1px solid #f0f0f0' }}>
<Space style={{ marginBottom: 8 }}><ClusterOutlined style={{ color: '#1677ff' }} /><DText strong>{s.name}</DText><DText type="secondary" style={{ fontSize: 12 }}>{s.network.ip}</DText></Space>
<Row gutter={12}>
<Col span={12}>
<DText type="secondary" style={{ fontSize: 11 }}>CPU {s.cpu.usagePercent}%</DText>
<Progress percent={s.cpu.usagePercent} size="small" strokeColor={s.cpu.usagePercent > 80 ? '#ff4d4f' : '#1677ff'} showInfo={false} />
</Col>
<Col span={12}>
<DText type="secondary" style={{ fontSize: 11 }}> {s.memory.percent}%</DText>
<Progress percent={s.memory.percent} size="small" strokeColor={s.memory.percent > 80 ? '#ff4d4f' : '#52c41a'} showInfo={false} />
</Col>
</Row>
</div>
</Col>
))}
</Row>
</div> </div>
) )
} }

75
src/pages/Servers.tsx Normal file
View File

@ -0,0 +1,75 @@
import { useQuery } from '@tanstack/react-query'
import { Card, Row, Col, Progress, Table, Tag, Typography, theme, Space } from 'antd'
import { CloudServerOutlined, DashboardOutlined } from '@ant-design/icons'
import { getServerMetrics, type ServerInfo } from '@/services/server-api'
const { Text, Title } = Typography
function ServerCard({ server }: { server: ServerInfo }) {
theme.useToken()
const cpuColor = server.cpu.usagePercent > 80 ? 'red' : server.cpu.usagePercent > 50 ? 'orange' : 'green'
const memColor = server.memory.percent > 80 ? 'red' : server.memory.percent > 50 ? 'orange' : 'green'
return (
<Card title={<Space><CloudServerOutlined />{server.name}<Tag color="blue">{server.role}</Tag></Space>}
extra={<Text type="secondary">{server.network.ip}</Text>}
style={{ height: '100%' }}>
<Row gutter={[16, 16]}>
<Col span={12}>
<Text type="secondary">CPU ({server.cpu.cores})</Text>
<Progress percent={server.cpu.usagePercent} strokeColor={cpuColor} size="small" />
<Text style={{ fontSize: 11 }} type="secondary">{server.cpu.model?.slice(0, 40)}</Text>
</Col>
<Col span={12}>
<Text type="secondary"></Text>
<Progress percent={server.memory.percent} strokeColor={memColor} size="small" />
<Text style={{ fontSize: 11 }} type="secondary">{server.memory.used}/{server.memory.total}</Text>
</Col>
<Col span={12}>
<Text type="secondary"></Text>
<Progress percent={server.disk.percent} strokeColor={server.disk.percent > 80 ? 'red' : 'blue'} size="small" />
<Text style={{ fontSize: 11 }} type="secondary">{server.disk.used}/{server.disk.total}</Text>
</Col>
<Col span={12}>
<Text type="secondary"></Text>
<div><Text strong>{server.uptime}</Text></div>
</Col>
</Row>
<Table
dataSource={server.processes}
rowKey="pid"
size="small"
pagination={false}
style={{ marginTop: 16 }}
columns={[
{ title: 'PID', dataIndex: 'pid', width: 70 },
{ title: 'CPU', dataIndex: 'cpu', width: 60 },
{ title: 'MEM', dataIndex: 'mem', width: 60 },
{ title: '命令', dataIndex: 'command', ellipsis: true },
]}
locale={{ emptyText: '暂无进程数据' }}
/>
</Card>
)
}
export default function ServersPage() {
const { data } = useQuery({
queryKey: ['servers', 'metrics'],
queryFn: getServerMetrics,
refetchInterval: 10_000,
})
return (
<div>
<Title level={5} style={{ marginTop: 0 }}><DashboardOutlined /> </Title>
<Row gutter={[16, 16]}>
{(data?.servers || []).map(s => (
<Col xs={24} lg={12} key={s.hostname}>
<ServerCard server={s} />
</Col>
))}
</Row>
</div>
)
}

View File

@ -0,0 +1,17 @@
import { api } from './http-client'
export interface ServerInfo {
name: string
role: string
hostname: string
cpu: { model: string; cores: number; usagePercent: number; loadAvg: number[] }
memory: { total: string; used: string; free: string; percent: number }
disk: { total: string; used: string; free: string; percent: number }
uptime: string
network: { ip: string }
processes: { pid: number; cpu: string; mem: string; command: string }[]
}
export function getServerMetrics(): Promise<{ servers: ServerInfo[] }> {
return api.get('/admin-api/servers/metrics')
}