diff --git a/web/src/pages/admin/Ops.tsx b/web/src/pages/admin/Ops.tsx index b720b2d..31fba93 100644 --- a/web/src/pages/admin/Ops.tsx +++ b/web/src/pages/admin/Ops.tsx @@ -1,11 +1,11 @@ -import { useEffect, useState, type ReactNode } from 'react' +import { useEffect, useMemo, useState } 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, + CloudServerOutlined, AlertOutlined, FileTextOutlined, + NotificationOutlined, } from '@ant-design/icons' import { createAnnouncement, deleteAnnouncement, getAdminLogs, getAnnouncements, updateAnnouncement, @@ -14,35 +14,14 @@ import { 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 services: { name: string; status: SvcStatus }[] = [ + { name: 'API服务', status: 'normal' }, + { name: 'WebSocket', status: 'normal' }, + { name: '数据库', status: 'normal' }, + { name: '消息队列', status: 'normal' }, + { name: 'CDN', status: 'normal' }, ] -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 actionMeta: Record = { create_tenant: { text: '开通租户', bg: '#dbeafe', color: '#2563eb' }, suspend_tenant: { text: '暂停租户', bg: '#fef2f2', color: '#dc2626' }, @@ -62,16 +41,47 @@ function formatTime(iso?: string) { }) } -/** 示意负载:按分钟轻微波动,非真实监控 */ -function mockLoad(seed: number) { - const t = Math.floor(Date.now() / 60000) - return 22 + ((t * 7 + seed * 13) % 35) +function formatRelative(d: Date) { + const sec = Math.max(0, Math.floor((Date.now() - d.getTime()) / 1000)) + if (sec < 60) return '刚刚' + if (sec < 3600) return `${Math.floor(sec / 60)} 分钟前` + return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) +} + +/** 近 24 小时示意请求量(工作时段高峰) */ +function genApiTrend(seed: number) { + const bars: { hour: number; value: number }[] = [] + for (let h = 0; h < 24; h++) { + const work = h >= 8 && h <= 18 + const base = work ? 55 + Math.sin((h - 8) / 10 * Math.PI) * 40 : 8 + (h % 5) * 2 + const noise = ((seed * 17 + h * 31) % 15) - 7 + bars.push({ hour: h, value: Math.max(5, Math.round(base + noise)) }) + } + return bars +} + +/** 近 24 小时示意错误率 0–5% */ +function genErrorRate(seed: number) { + const points: number[] = [] + for (let h = 0; h < 24; h++) { + const spike = h === 9 || h === 10 ? 2.2 : 0 + const v = 0.4 + spike + ((seed + h * 3) % 10) / 20 + points.push(Math.min(4.8, Math.max(0.1, Number(v.toFixed(2))))) + } + return points +} + +function loadColor(p: number) { + if (p >= 80) return { text: '#dc2626', bar: '#dc2626' } + if (p >= 55) return { text: '#d97706', bar: '#d97706' } + return { text: '#16a34a', bar: '#16a34a' } } const Ops = () => { const [logs, setLogs] = useState([]) const [logTotal, setLogTotal] = useState(0) const [logPage, setLogPage] = useState(1) + const [logSearch, setLogSearch] = useState('') const [announcements, setAnnouncements] = useState([]) const [loadingLogs, setLoadingLogs] = useState(false) const [loadingAnn, setLoadingAnn] = useState(false) @@ -80,7 +90,31 @@ 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 [seed, setSeed] = useState(0) + const [loads, setLoads] = useState({ cpu: 23, mem: 61, disk: 45 }) + + const apiTrend = useMemo(() => genApiTrend(seed), [seed]) + const errorRates = useMemo(() => genErrorRate(seed), [seed]) + const maxApi = Math.max(...apiTrend.map(b => b.value), 1) + const maxErr = 5 + + const errorPolyline = useMemo(() => { + const w = 200 + const h = 100 + return errorRates + .map((v, i) => { + const x = (i / 23) * w + const y = h - (v / maxErr) * (h - 8) - 4 + return `${x},${y}` + }) + .join(' ') + }, [errorRates, maxErr]) + + const errorArea = useMemo(() => { + const w = 200 + const h = 100 + return `${errorPolyline} ${w},${h} 0,${h}` + }, [errorPolyline]) const loadLogs = async (page = logPage) => { setLoadingLogs(true) @@ -116,6 +150,16 @@ const Ops = () => { loadLogs(logPage) }, [logPage]) + const filteredLogs = useMemo(() => { + const q = logSearch.trim().toLowerCase() + if (!q) return logs + return logs.filter(l => + (l.detail || '').toLowerCase().includes(q) + || (l.action || '').toLowerCase().includes(q) + || (actionMeta[l.action]?.text || '').includes(q), + ) + }, [logs, logSearch]) + const openCreate = () => { setEditing(null) form.resetFields() @@ -161,11 +205,13 @@ const Ops = () => { } } - const refreshHealth = () => { + const refreshAll = () => { + const s = seed + 1 + setSeed(s) setLoads({ - cpu: mockLoad(1), - mem: mockLoad(2), - disk: mockLoad(3), + cpu: 18 + ((s * 11) % 40), + mem: 40 + ((s * 7) % 35), + disk: 35 + ((s * 5) % 30), }) setRefreshedAt(new Date()) loadLogs(logPage) @@ -173,239 +219,334 @@ const Ops = () => { message.success('已刷新') } - const loadItems = [ - { label: 'CPU', percent: loads.cpu }, - { label: '内存', percent: loads.mem }, - { label: '磁盘', percent: loads.disk }, + const loadCards = [ + { label: 'CPU', percent: loads.cpu, sub: '8核 / 使用中' }, + { label: '内存', percent: loads.mem, sub: '16GB / 使用中' }, + { label: '磁盘', percent: loads.disk, sub: '500GB / 使用中' }, ] return ( -
-
-
-

系统运维

-
- -
- -
-
-

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

+ {/* 页头:对齐设计 — 标题区 + 状态 */} +
+
+
+

系统运维

+

+ 监控平台运行状态、查看操作日志、管理平台公告 +

+
+
+ + + 系统正常运行 -

+ + 最后更新:{formatRelative(refreshedAt)} + + +
+
+
- {/* 服务 + 负载 */} -
-
-
-

服务状态

- - - 全部正常 - -
-
- {services.map(s => { - const st = statusUi[s.status] - return ( -
-
- {s.icon} -
-
-
{s.name}
-
{s.hint}
-
- - {st.icon} - {st.text} +
+
+ + {/* 1. 服务状态 + 系统负载 */} +
+
+ + 服务状态 +
+
+ {services.map(s => ( +
+ + {s.name} + 正常 +
+ ))} +
+ +
+ 系统负载 + 开发环境示意 +
+
+ {loadCards.map(item => { + const c = loadColor(item.percent) + return ( +
+
+ {item.label} + + {item.percent}%
- ) - })} +
+
+
+

{item.sub}

+
+ ) + })} +
+
+ + {/* 2. API 趋势 + 错误率 */} +
+
+
+
+ + API请求量趋势 +
+ 近24小时 · 示意 +
+
+
+ + + 0 +
+
+ {apiTrend.map(b => ( +
+
maxApi * 0.55 ? '#2563eb' : '#93c5fd', + minHeight: 2, + }} + /> +
+ ))} +
+
+ 00 + 06 + 12 + 18 + 23 +
-
-

资源占用

-
- {loadItems.map(item => { - const color = item.percent > 80 ? '#dc2626' : item.percent > 60 ? '#d97706' : '#16a34a' - return ( -
-
- {item.label} - {item.percent}% -
-
-
-
-
- ) - })} +
+
+
+ + 错误率监控 +
+ 近24小时 · 示意
-

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

+
+ 5% + 2.5% + 0% +
+ + {/* 阈值线 2% ≈ y=60 */} + + + + +
+ 00 + 06 + 12 + 18 + 23 +
+
+

+ 虚线为 2% 告警阈值 · 当前峰值约 {Math.max(...errorRates).toFixed(1)}%

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

操作日志

-

平台管理审计记录

-
- 共 {logTotal} 条 + {/* 3. 操作日志(全宽表格) */} +
+
+
+ + 操作日志 + 共 {logTotal} 条
-
- {loadingLogs ? ( -
- ) : logs.length === 0 ? ( - - ) : ( -
    - {logs.map(log => { +
    + setLogSearch(e.target.value)} + className="bg-transparent border-0 outline-none text-sm text-neutral-800 w-36 placeholder:text-neutral-400" + /> +
    +
+
+ {loadingLogs ? ( +
+ ) : filteredLogs.length === 0 ? ( + + ) : ( + + + + + + + + + + + {filteredLogs.map(log => { const meta = actionMeta[log.action] || { text: log.action, bg: '#f1f5f9', color: '#64748b', } return ( -
  • -
    +
  • + + + + + ) })} - - )} - - {logTotal > 10 && ( -
    - setLogPage(p)} - showSizeChanger={false} - /> -
    + +
    时间操作详情IP
    + {formatTime(log.created_at)} + {meta.text} - - {formatTime(log.created_at)} - - -

    {log.detail || '—'}

    - {log.ip && ( -

    {log.ip}

    - )} - +
    + {log.detail || '—'} + + {log.ip || '—'} +
    )}
    - -
    -
    -
    -

    平台公告

    -

    面向租户的系统通知

    -
    - + current={logPage} + total={logTotal} + pageSize={10} + onChange={p => setLogPage(p)} + showSizeChanger={false} + />
    -
    - {loadingAnn ? ( -
    - ) : announcements.length === 0 ? ( - - - - ) : ( -
    - {announcements.map(a => ( -
    -
    -

    - {a.title} -

    - + + {/* 4. 平台公告 */} +
    +
    +
    + + 平台公告 +
    + +
    +
    + {loadingAnn ? ( +
    + ) : announcements.length === 0 ? ( + + + + ) : ( +
    + {announcements.map(a => ( +
    +
    +

    + {a.title} +

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

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

    +
    + {formatTime(a.created_at)} +
    +
    -

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

    -
    - - {formatTime(a.created_at)} - -
    + + + handleDelete(a.id)}> - handleDelete(a.id)}> - - -
    +
    -
    - ))} -
    - )} -
    +
    +
    + ))} +
    + )}