From e157fc0ba5ff23d877199bafbd11b114c0f43415 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 15 Jul 2026 13:56:13 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=81=9A=E5=B9=B3=E5=8F=B0=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=BF=90=E7=BB=B4=E9=A1=B5=E5=B8=83=E5=B1=80=E4=B8=8E?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一 56px 顶栏与现代卡片分区:服务健康、资源占用示意、操作日志列表与公告管理。 --- web/src/pages/admin/Ops.tsx | 454 +++++++++++++++++++++++++----------- 1 file changed, 315 insertions(+), 139 deletions(-) diff --git a/web/src/pages/admin/Ops.tsx b/web/src/pages/admin/Ops.tsx index 358eacd..b720b2d 100644 --- a/web/src/pages/admin/Ops.tsx +++ b/web/src/pages/admin/Ops.tsx @@ -1,33 +1,71 @@ -import { useEffect, useState } from 'react' -import { Card, Table, Tag, Button, Badge, Progress, Space, message, Modal, Form, Input, Select, Spin, Empty, Popconfirm } from 'antd' -import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons' +import { useEffect, useState, type ReactNode } from 'react' +import { + Button, message, Modal, Form, Input, Select, Spin, Empty, Popconfirm, Pagination, +} from 'antd' +import { + PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, + CloudServerOutlined, ApiOutlined, DatabaseOutlined, ClusterOutlined, + GlobalOutlined, CheckCircleFilled, WarningFilled, CloseCircleFilled, +} from '@ant-design/icons' import { createAnnouncement, deleteAnnouncement, getAdminLogs, getAnnouncements, updateAnnouncement, type Announcement, type OperationLog, } from '@/services/api' -const services = [ - { name: 'API 服务', status: 'normal' as const }, - { name: 'WebSocket 服务', status: 'normal' as const }, - { name: '数据库', status: 'normal' as const }, - { name: '消息队列', status: 'normal' as const }, - { name: 'CDN', status: 'normal' as const }, +type SvcStatus = 'normal' | 'warning' | 'error' + +const services: { name: string; status: SvcStatus; icon: ReactNode; hint: string }[] = [ + { name: 'API 服务', status: 'normal', icon: , hint: 'HTTP / REST' }, + { name: 'WebSocket', status: 'normal', icon: , hint: '实时消息' }, + { name: '数据库', status: 'normal', icon: , hint: 'PostgreSQL' }, + { name: '对象存储', status: 'normal', icon: , hint: 'MinIO / S3' }, + { name: '静态资源', status: 'normal', icon: , hint: '前端与 Widget' }, ] -const statusDisplay = { - normal: { color: 'green', text: '正常' }, - warning: { color: 'orange', text: '告警' }, - error: { color: 'red', text: '故障' }, +const statusUi: Record = { + normal: { + text: '正常', + bg: 'bg-emerald-50', + color: 'text-emerald-700', + icon: , + }, + warning: { + text: '告警', + bg: 'bg-amber-50', + color: 'text-amber-700', + icon: , + }, + error: { + text: '故障', + bg: 'bg-red-50', + color: 'text-red-700', + icon: , + }, } -const actionLabel: Record = { - create_tenant: '开通租户', - suspend_tenant: '暂停租户', - resume_tenant: '恢复租户', - update_tenant: '更新租户', - update_plan: '更新套餐', - create_announcement: '创建公告', - delete_announcement: '删除公告', +const actionMeta: Record = { + create_tenant: { text: '开通租户', bg: '#dbeafe', color: '#2563eb' }, + suspend_tenant: { text: '暂停租户', bg: '#fef2f2', color: '#dc2626' }, + resume_tenant: { text: '恢复租户', bg: '#f0fdf4', color: '#16a34a' }, + update_tenant: { text: '更新租户', bg: '#ecfeff', color: '#0891b2' }, + update_plan: { text: '更新套餐', bg: '#f3e8ff', color: '#7c3aed' }, + create_plan: { text: '创建套餐', bg: '#f3e8ff', color: '#7c3aed' }, + create_announcement: { text: '创建公告', bg: '#fffbeb', color: '#d97706' }, + delete_announcement: { text: '删除公告', bg: '#f1f5f9', color: '#64748b' }, + update_announcement: { text: '更新公告', bg: '#fffbeb', color: '#d97706' }, +} + +function formatTime(iso?: string) { + if (!iso) return '—' + return new Date(iso).toLocaleString('zh-CN', { + month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', + }) +} + +/** 示意负载:按分钟轻微波动,非真实监控 */ +function mockLoad(seed: number) { + const t = Math.floor(Date.now() / 60000) + return 22 + ((t * 7 + seed * 13) % 35) } const Ops = () => { @@ -42,11 +80,12 @@ const Ops = () => { const [saving, setSaving] = useState(false) const [form] = Form.useForm() const [refreshedAt, setRefreshedAt] = useState(new Date()) + const [loads, setLoads] = useState({ cpu: 28, mem: 51, disk: 36 }) const loadLogs = async (page = logPage) => { setLoadingLogs(true) try { - const res = await getAdminLogs({ page, pageSize: 8 }) + const res = await getAdminLogs({ page, pageSize: 10 }) setLogs(res.list || []) setLogTotal(res.total) } catch { @@ -123,143 +162,280 @@ const Ops = () => { } const refreshHealth = () => { + setLoads({ + cpu: mockLoad(1), + mem: mockLoad(2), + disk: mockLoad(3), + }) setRefreshedAt(new Date()) - message.success('已刷新状态') + loadLogs(logPage) + loadAnnouncements() + message.success('已刷新') } + const loadItems = [ + { label: 'CPU', percent: loads.cpu }, + { label: '内存', percent: loads.mem }, + { label: '磁盘', percent: loads.disk }, + ] + return ( -
-

系统运维

- -
- } +
+
+
+

系统运维

+
+ +
+ +
+
+

+ 查看服务健康、操作审计与平台公告。负载指标为开发环境示意,生产可对接主机监控。 + + 更新于 {refreshedAt.toLocaleTimeString('zh-CN')} + +

+ + {/* 服务 + 负载 */} +
+
+
+

服务状态

+ + + 全部正常 +
- ))} -
-
- 健康检查基于进程存活(开发环境示意)· 刷新于 {refreshedAt.toLocaleTimeString('zh-CN')} -
- - - -
- {[ - { type: 'CPU 使用率', percent: 28 }, - { type: '内存使用率', percent: 51 }, - { type: '磁盘使用率', percent: 36 }, - ].map(l => ( -
-
- {l.type} - 80 ? 'text-red-500' : l.percent > 60 ? 'text-orange-500' : 'text-green-600'}`}>{l.percent}% -
- 80 ? '#dc2626' : l.percent > 60 ? '#d97706' : '#16a34a'} /> +
+ {services.map(s => { + const st = statusUi[s.status] + return ( +
+
+ {s.icon} +
+
+
{s.name}
+
{s.hint}
+
+ + {st.icon} + {st.text} + +
+ ) + })}
- ))} -
负载指标为示意数据,生产环境可对接主机监控。
-
- -
- -
- - setLogPage(p), - }} - size="small" - locale={{ emptyText: }} - columns={[ - { - title: '时间', dataIndex: 'created_at', key: 'created_at', width: 150, - render: (t: string) => {t ? new Date(t).toLocaleString('zh-CN') : '—'}, - }, - { - title: '操作', dataIndex: 'action', key: 'action', - render: (a: string) => actionLabel[a] || a, - }, - { - title: '详情', dataIndex: 'detail', key: 'detail', - render: (d: string) => {d}, - }, - ]} - /> - - - } onClick={openCreate}>新建公告} - > - {loadingAnn ? ( -
- ) : announcements.length === 0 ? ( - - ) : ( -
- {announcements.map(a => ( -
-
- {a.title} - - {a.status === 'published' ? '已发布' : '草稿'} - -
-

{a.content || '—'}

-
- {a.created_at ? new Date(a.created_at).toLocaleString('zh-CN') : '—'} - - - handleDelete(a.id)}> - - - -
-
- ))}
- )} -
+ +
+

资源占用

+
+ {loadItems.map(item => { + const color = item.percent > 80 ? '#dc2626' : item.percent > 60 ? '#d97706' : '#16a34a' + return ( +
+
+ {item.label} + {item.percent}% +
+
+
+
+
+ ) + })} +
+

+ 示意数据 · 点击右上角刷新会轻微波动 +

+
+ + + {/* 日志 + 公告 */} +
+
+
+
+

操作日志

+

平台管理审计记录

+
+ 共 {logTotal} 条 +
+
+ {loadingLogs ? ( +
+ ) : logs.length === 0 ? ( + + ) : ( +
    + {logs.map(log => { + const meta = actionMeta[log.action] || { + text: log.action, bg: '#f1f5f9', color: '#64748b', + } + return ( +
  • +
    + + {meta.text} + + + {formatTime(log.created_at)} + +
    +

    {log.detail || '—'}

    + {log.ip && ( +

    {log.ip}

    + )} +
  • + ) + })} +
+ )} +
+ {logTotal > 10 && ( +
+ setLogPage(p)} + showSizeChanger={false} + /> +
+ )} +
+ +
+
+
+

平台公告

+

面向租户的系统通知

+
+ +
+
+ {loadingAnn ? ( +
+ ) : announcements.length === 0 ? ( + + + + ) : ( +
+ {announcements.map(a => ( +
+
+

+ {a.title} +

+ + {a.status === 'published' ? '已发布' : '草稿'} + +
+

+ {a.content || '—'} +

+
+ + {formatTime(a.created_at)} + +
+ + handleDelete(a.id)}> + + +
+
+
+ ))} +
+ )} +
+
+
+
setModalOpen(false)} onOk={() => form.submit()} confirmLoading={saving} destroyOnClose + okText="保存" + width={520} > -
- - + + + - - + + -