diff --git a/pkg/agent/service/agent.go b/pkg/agent/service/agent.go index 317b7a9..6ca3500 100644 --- a/pkg/agent/service/agent.go +++ b/pkg/agent/service/agent.go @@ -190,28 +190,40 @@ func (a *Agent) runOnce(ctx context.Context, onConnected func()) error { a.setActiveConn(nil) }() - // 创建完成通道 + // 创建完成通道和错误通道 done := make(chan struct{}) - errChan := make(chan error, 3) + errChan := make(chan error, 1) // 只需要接收第一个错误 + // 创建 WaitGroup 用于等待所有 goroutine 退出 var wg conc.WaitGroup + // 启动读取循环(处理服务端的 Ping/Pong 等控制消息) wg.Go(func() { if err := a.readLoop(rawConn, done); err != nil { - errChan <- fmt.Errorf("读取失败: %w", err) + select { + case errChan <- fmt.Errorf("读取失败: %w", err): + default: + } } }) // 启动心跳和数据发送 wg.Go(func() { if err := a.heartbeatLoop(ctx, conn, done); err != nil { - errChan <- fmt.Errorf("心跳失败: %w", err) + select { + case errChan <- fmt.Errorf("心跳失败: %w", err): + default: + } } }) + // 启动指标采集循环 wg.Go(func() { if err := a.metricsLoop(ctx, conn, collectorManager, done); err != nil { - errChan <- fmt.Errorf("数据采集失败: %w", err) + select { + case errChan <- fmt.Errorf("数据采集失败: %w", err): + default: + } } }) @@ -225,23 +237,32 @@ func (a *Agent) runOnce(ctx context.Context, onConnected func()) error { a.tamperAlertLoop(ctx, conn, done) }) - // 等待错误或上下文取消 + // 等待第一个错误或上下文取消 + var returnErr error select { case err := <-errChan: - close(done) // 连接已建立,无论什么原因断开都标记为已建立状态 log.Printf("连接断开: %v", err) - return ErrConnectionEstablished + returnErr = ErrConnectionEstablished case <-ctx.Done(): - close(done) - // 优雅关闭连接 - closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") - if err := conn.WriteMessage(websocket.CloseMessage, closeMsg); err != nil { - log.Printf("⚠️ 关闭连接失败: %v", err) - } - time.Sleep(time.Second) - return ctx.Err() // 返回上下文错误 + // 收到取消信号 + log.Println("收到停止信号,准备关闭连接") + returnErr = ctx.Err() } + + // 关闭 done channel,通知所有 goroutine 退出 + close(done) + + // 发送 WebSocket 关闭消息 + closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + if err := conn.WriteMessage(websocket.CloseMessage, closeMsg); err != nil { + log.Printf("⚠️ 发送关闭消息失败: %v", err) + } + + // 等待所有 goroutine 优雅退出 + wg.Wait() + + return returnErr } // readLoop 读取服务端消息(主要用于处理 Ping/Pong 和指令) diff --git a/web/src/components/CyberCard.tsx b/web/src/components/CyberCard.tsx new file mode 100644 index 0000000..c919d1c --- /dev/null +++ b/web/src/components/CyberCard.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import {cn} from "@/lib/utils.ts"; + +interface Props { + className?: string + children: React.ReactNode; + animation?: boolean + hover?: boolean +} + +const CyberCard = ({className, children, animation, hover}: Props) => { + return ( +
+ {/* 装饰性边框 */} +
+
+
+
+ {animation && +
+ } + +
+ {children} +
+
+ ); +}; + +export default CyberCard; \ No newline at end of file diff --git a/web/src/components/NetworkStatCard.tsx b/web/src/components/NetworkStatCard.tsx new file mode 100644 index 0000000..6b46fc1 --- /dev/null +++ b/web/src/components/NetworkStatCard.tsx @@ -0,0 +1,53 @@ +import type {FC} from 'react'; +import {ArrowDown, ArrowUp, Network} from 'lucide-react'; +import {formatBytes, formatSpeed} from '@/utils/util'; + +interface NetworkStatCardProps { + uploadRate: number; + downloadRate: number; + uploadTotal: number; + downloadTotal: number; +} + +const NetworkStatCard: FC = ({ + uploadRate, + downloadRate, + uploadTotal, + downloadTotal +}) => { + return ( +
+
+ +
+
+
+
+ 网络统计 +
+
+
+ + {formatSpeed(uploadRate)} + + ({formatBytes(uploadTotal)}) + +
+
+ + {formatSpeed(downloadRate)} + + ({formatBytes(downloadTotal)}) + +
+
+
+
+ +
+
+
+ ); +}; + +export default NetworkStatCard; diff --git a/web/src/components/PublicHeader.tsx b/web/src/components/PublicHeader.tsx index 68aa441..d62c734 100644 --- a/web/src/components/PublicHeader.tsx +++ b/web/src/components/PublicHeader.tsx @@ -1,11 +1,12 @@ import {useEffect, useState} from 'react'; -import {Activity, LogIn, ServerIcon, Settings} from 'lucide-react'; +import {Activity, LogIn, ServerIcon, Settings, Menu, X} from 'lucide-react'; import {getCurrentUser} from '../api/auth'; import {Link, useLocation} from "react-router-dom"; const PublicHeader = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [currentTime, setCurrentTime] = useState(new Date()); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); let location = useLocation(); useEffect(() => { @@ -47,89 +48,159 @@ const PublicHeader = () => { } return ( -
-
-
-
-
- {'logo'} { - e.currentTarget.src = '/logo.png'; - }} - /> -
-
-

- {window.SystemConfig?.SystemNameEn} -

-

- {window.SystemConfig?.SystemNameZh} -

+ <> +
+
+
+ +
+
+ {'logo'} { + e.currentTarget.src = '/logo.png'; + }} + /> +
+
+

+ {window.SystemConfig?.SystemNameEn} +

+

+ {window.SystemConfig?.SystemNameZh} +

+
+
+ + + {/* HUD Navigation - Desktop Only */} +
+ {[ + {id: 'servers', icon: ServerIcon, label: '设备监控', to: '/'}, + {id: 'monitors', icon: Activity, label: '服务监控', to: '/monitors'} + ].map(tab => ( + + + + ))}
- {/* HUD Navigation - Simplified & No Border */} -
+ {/* Desktop Right Section */} +
+
+ {currentTime.toLocaleTimeString()} + {currentTime.toLocaleDateString()} +
+
+ + {/* 登录/管理后台按钮 - Desktop */} + {isLoggedIn ? ( + + + Admin + + ) : ( + + + Login + + )} +
+ + {/* Mobile Menu Button */} + +
+
+
+ + {/* Mobile Menu */} + {mobileMenuOpen && ( +
+
+ {/* Mobile Navigation */} {[ {id: 'servers', icon: ServerIcon, label: '设备监控', to: '/'}, {id: 'monitors', icon: Activity, label: '服务监控', to: '/monitors'} ].map(tab => ( - - + setMobileMenuOpen(false)} + className={` + flex items-center gap-3 p-4 rounded-lg border transition-all + ${activeTab === tab.id + ? 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400' + : 'bg-cyan-500/5 border-cyan-500/20 text-slate-400 hover:bg-cyan-500/10 hover:border-cyan-500/30' + } + `} + > + + {tab.label} ))} + + {/* Divider */} +
+ + {/* Mobile Login/Admin Button */} + {isLoggedIn ? ( + + + 管理后台 + + ) : ( + + + 登录 + + )}
- -
-
- {currentTime.toLocaleTimeString()} - {currentTime.toLocaleDateString()} -
-
- - {/* 登录/管理后台按钮 */} - {isLoggedIn ? ( - - - Admin - - ) : ( - - - Login - - )} -
-
-
-
+ )} + ); }; diff --git a/web/src/components/ServerCard.tsx b/web/src/components/ServerCard.tsx new file mode 100644 index 0000000..fb40c05 --- /dev/null +++ b/web/src/components/ServerCard.tsx @@ -0,0 +1,181 @@ +import type {FC} from 'react'; +import { + AlertTriangle, + ArrowDown, + ArrowUp, + Calendar, + Cpu, + HardDrive, + MemoryStick +} from 'lucide-react'; +import type {Agent, LatestMetrics} from '@/types'; +import CompactResourceBar from '@/components/CompactResourceBar'; +import {formatBytes, formatSpeed} from '@/utils/util'; + +interface AgentWithMetrics extends Agent { + metrics?: LatestMetrics; +} + +interface ServerCardProps { + server: AgentWithMetrics; + onClick: (serverId: string) => void; +} + +const calculateNetworkSpeed = (metrics?: LatestMetrics) => { + if (!metrics?.network) { + return {upload: 0, download: 0}; + } + return { + upload: metrics.network.totalBytesSentRate, + download: metrics.network.totalBytesRecvRate + }; +}; + +const calculateDiskUsage = (metrics?: LatestMetrics) => { + if (!metrics?.disk) { + return 0; + } + return metrics.disk.usagePercent; +}; + +const ServerCard: FC = ({server, onClick}) => { + const isOnline = server.status === 1; + const cpuUsage = server.metrics?.cpu?.usagePercent ?? 0; + const memoryUsage = server.metrics?.memory?.usagePercent ?? 0; + const memoryTotal = server.metrics?.memory?.total ?? 0; + const memoryUsed = server.metrics?.memory?.used ?? 0; + const diskUsage = calculateDiskUsage(server.metrics); + const diskTotal = server.metrics?.disk?.total ?? 0; + const diskUsed = server.metrics?.disk?.used ?? 0; + const {upload, download} = calculateNetworkSpeed(server.metrics); + + return ( +
onClick(server.id)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + onClick(server.id); + } + }} + className="group bg-[#0f1016]/80 backdrop-blur-md border border-cyan-500/20 shadow-[0_0_15px_rgba(6,182,212,0.05)] hover:border-cyan-500/50 hover:shadow-[0_0_25px_rgba(6,182,212,0.15)] hover:bg-[#0f1016]/90 transition-all duration-300 cursor-pointer overflow-hidden relative" + > + {/* 装饰性边框 */} +
+
+
+
+
+ +
+ {/* 顶部:名称和状态 */} +
+
+
+ {server.name || server.hostname} +
+
+ {server.os} + + {server.arch} +
+
+ {server.tags && server.tags.length > 0 && ( +
+ {server.tags.slice(0, 2).map(tag => ( + + #{tag} + + ))} + {server.tags.length > 2 && ( + + +{server.tags.length - 2} + + )} +
+ )} +
+ + {/* 资源使用情况 */} + {isOnline ? ( +
+ + + +
+ ) : ( +
+ + 离线 +
+ )} + + {/* 网络和流量 */} +
+
+ + + {formatSpeed(download)} + + + + {formatSpeed(upload)} + +
+ {server.expireTime > 0 && ( +
+ + {new Date(server.expireTime).toLocaleDateString('zh-CN')} +
+ )} +
+ + {/* 流量限制 */} + {server.trafficLimit > 0 && ( +
+
+ 流量使用 + {Math.round((server.trafficUsed || 0) / server.trafficLimit * 100)}% +
+
+
+
+
+ )} +
+
+ ); +}; + +export default ServerCard; diff --git a/web/src/components/common/Card.tsx b/web/src/components/common/Card.tsx index 396445d..d68587f 100644 --- a/web/src/components/common/Card.tsx +++ b/web/src/components/common/Card.tsx @@ -1,5 +1,6 @@ import type {ReactNode} from 'react'; import {cn} from '@/lib/utils'; +import CyberCard from "@/components/CyberCard.tsx"; interface CardProps { title?: string; @@ -19,12 +20,7 @@ export const Card = ({ const isDark = variant === 'dark'; return ( -
+ {(title || description || action) && (
)}
{children}
-
+ ); }; diff --git a/web/src/components/common/CustomTooltip.tsx b/web/src/components/common/CustomTooltip.tsx index 47453a7..0a15fbd 100644 --- a/web/src/components/common/CustomTooltip.tsx +++ b/web/src/components/common/CustomTooltip.tsx @@ -33,7 +33,7 @@ export const CustomTooltip = ({active, payload, label, variant = 'light', unit = return (
- {/* 装饰性边框 */} -
-
-
-
+ +
+

探针监控详情

+

各探针的当前状态和统计数据

+
-
-
-

探针监控详情

-

各探针的当前状态和统计数据

-
- -
- - - - - - - - {monitorType === 'https' && ( - - )} +
+
- 探针名称 - - 状态 - - 响应时间 - - 最后检测 - - 证书信息 -
+ + + + + + + {monitorType === 'https' && ( - - - - {monitorStats.map((stat, index) => { - const color = AGENT_COLORS[index % AGENT_COLORS.length]; - return ( - - + + + + {monitorStats.map((stat, index) => { + const color = AGENT_COLORS[index % AGENT_COLORS.length]; + return ( + + - - + + - - {monitorType === 'https' && ( - - )} + + + + {monitorType === 'https' && ( - - ); - })} - -
+ 探针名称 + + 状态 + + 响应时间 + + 最后检测 + - 错误信息 + 证书信息
-
+ )} +
+ 错误信息 +
+
-
- - +
+ + {stat.agentName || stat.agentId.substring(0, 8)} -
-
- - -
- - +
+
+ + +
+ + {formatTime(stat.responseTime)} -
-
- {formatDateTime(stat.checkedAt)} - - {stat.certExpiryTime ? ( - - ) : ( - - - )} - + {formatDateTime(stat.checkedAt)} + - {stat.status === 'down' && stat.message ? ( -
- - - {stat.message} - -
+ {stat.certExpiryTime ? ( + ) : ( - )}
-
+ )} + + {stat.status === 'down' && stat.message ? ( +
+ + + {stat.message} + +
+ ) : ( + - + )} + + + ); + })} + +
-
+ ); }; diff --git a/web/src/components/monitor/MonitorHero.tsx b/web/src/components/monitor/MonitorHero.tsx index 9f4ada4..50f092d 100644 --- a/web/src/components/monitor/MonitorHero.tsx +++ b/web/src/components/monitor/MonitorHero.tsx @@ -4,6 +4,7 @@ import {StatusBadge} from './StatusBadge'; import {CertBadge} from './CertBadge'; import LittleStatCard from '@/components/common/LittleStatCard'; import type {PublicMonitor} from '@/types'; +import CyberCard from "@/components/CyberCard.tsx"; interface MonitorHeroProps { monitor: PublicMonitor; @@ -16,83 +17,68 @@ interface MonitorHeroProps { */ export const MonitorHero = ({monitor, onBack}: MonitorHeroProps) => { return ( -
- {/* 装饰性边框 */} -
-
-
-
-
+ + {/* 返回按钮 */} + -
- {/* 返回按钮 */} - - - {/* 监控信息 */} -
-
-
- -
-
-
- MONITOR_ID: {monitor.id.toString().substring(0, 8)} -
-
-

{monitor.name}

- -
-

- {monitor.showTargetPublic ? monitor.target : '******'} -

-
+ {/* 监控信息 */} +
+
+
+
- - {/* 统计卡片 */} -
- - - - +
+
+ MONITOR_ID: {monitor.id.toString().substring(0, 8)} +
+
+

{monitor.name}

+ +
+

+ {monitor.showTargetPublic ? monitor.target : '******'} +

- {/* 证书信息(如果是 HTTPS)*/} - {monitor.type === 'https' && monitor.certExpiryTime && ( -
- SSL 证书: - -
- )} + {/* 统计卡片 */} +
+ + + + +
-
+ + {/* 证书信息(如果是 HTTPS)*/} + {monitor.type === 'https' && monitor.certExpiryTime && ( +
+ SSL 证书: + +
+ )} + ); }; diff --git a/web/src/components/monitor/ResponseTimeChart.tsx b/web/src/components/monitor/ResponseTimeChart.tsx index c15e69d..533051a 100644 --- a/web/src/components/monitor/ResponseTimeChart.tsx +++ b/web/src/components/monitor/ResponseTimeChart.tsx @@ -1,86 +1,25 @@ import {useEffect, useMemo, useState} from 'react'; import {useQuery} from '@tanstack/react-query'; -import {Shield} from 'lucide-react'; import {Area, AreaChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; import {type GetMetricsResponse, getMonitorHistory} from '@/api/monitor'; import {AGENT_COLORS} from '@/constants/colors'; import {MONITOR_TIME_RANGE_OPTIONS} from '@/constants/time'; -import {cn} from '@/lib/utils'; import type {AgentMonitorStat} from '@/types'; +import CyberCard from "@/components/CyberCard.tsx"; +import {ChartPlaceholder, CustomTooltip, TimeRangeSelector} from "@/components/common"; interface ResponseTimeChartProps { monitorId: string; monitorStats: AgentMonitorStat[]; } -// 自定义 Tooltip 组件 -const CustomTooltip = ({active, payload, label}: any) => { - if (!active || !payload || payload.length === 0) return null; - - return ( -
-

{label}

-
- {payload.map((entry: any, index: number) => ( -
- - - {entry.name}: {entry.value} ms - -
- ))} -
-
- ); -}; - -// 图表占位符组件 -const ChartPlaceholder = ({subtitle, heightClass}: { subtitle: string; heightClass: string }) => ( -
-
- -

{subtitle}

-
-
-); - -// 时间范围选择器组件 -const TimeRangeSelector = ({ - value, - onChange, - options - }: { - value: string; - onChange: (value: string) => void; - options: Array<{ value: string; label: string }>; -}) => ( -
- {options.map(option => ( - - ))} -
-); - /** * 响应时间趋势图表组件 * 显示监控各探针的响应时间变化 */ export const ResponseTimeChart = ({monitorId, monitorStats}: ResponseTimeChartProps) => { const [selectedAgent, setSelectedAgent] = useState('all'); - const [timeRange, setTimeRange] = useState('1d'); + const [timeRange, setTimeRange] = useState('12h'); // 获取历史数据 const {data: historyData} = useQuery({ @@ -156,94 +95,40 @@ export const ResponseTimeChart = ({monitorId, monitorStats}: ResponseTimeChartPr }, [historyData, selectedAgent]); return ( -
- {/* 装饰性边框 */} -
-
-
-
- -
-
-
-

响应时间趋势

-

监控各探针的响应时间变化

-
-
- - {availableAgents.length > 0 && ( - - )} -
+ +
+
+

响应时间趋势

+

监控各探针的响应时间变化

+
+ + {availableAgents.length > 0 && ( + + )} +
+
- {chartData.length > 0 ? ( - - - - {monitorStats - .filter(stat => { - const agentIdentifier = stat.agentName || stat.agentId; - return selectedAgent === 'all' || agentIdentifier === selectedAgent; - }) - .map((stat) => { - const originalIndex = monitorStats.findIndex(s => s.agentId === stat.agentId); - const agentIdentifier = stat.agentName || stat.agentId; - const agentKey = `agent_${agentIdentifier}`; - const color = AGENT_COLORS[originalIndex % AGENT_COLORS.length]; - return ( - - - - - ); - })} - - - - `${value}ms`} - /> - }/> - + {chartData.length > 0 ? ( + + + {monitorStats .filter(stat => { const agentIdentifier = stat.agentName || stat.agentId; @@ -254,30 +139,73 @@ export const ResponseTimeChart = ({monitorId, monitorStats}: ResponseTimeChartPr const agentIdentifier = stat.agentName || stat.agentId; const agentKey = `agent_${agentIdentifier}`; const color = AGENT_COLORS[originalIndex % AGENT_COLORS.length]; - const agentLabel = stat.agentName || stat.agentId.substring(0, 8); return ( - + + + + ); })} - - - ) : ( - - )} -
-
+ + + + `${value}ms`} + /> + }/> + + {monitorStats + .filter(stat => { + const agentIdentifier = stat.agentName || stat.agentId; + return selectedAgent === 'all' || agentIdentifier === selectedAgent; + }) + .map((stat) => { + const originalIndex = monitorStats.findIndex(s => s.agentId === stat.agentId); + const agentIdentifier = stat.agentName || stat.agentId; + const agentKey = `agent_${agentIdentifier}`; + const color = AGENT_COLORS[originalIndex % AGENT_COLORS.length]; + const agentLabel = stat.agentName || stat.agentId.substring(0, 8); + return ( + + ); + })} + + + ) : ( + + )} + ); }; diff --git a/web/src/components/server/ServerHero.tsx b/web/src/components/server/ServerHero.tsx index d6366a7..37388c4 100644 --- a/web/src/components/server/ServerHero.tsx +++ b/web/src/components/server/ServerHero.tsx @@ -1,8 +1,9 @@ -import {ArrowLeft, Server} from 'lucide-react'; +import {ArrowLeft} from 'lucide-react'; import {cn} from '@/lib/utils'; import {formatBytes, formatDateTime, formatUptime} from '@/utils/util'; import type {Agent, LatestMetrics} from '@/types'; import LittleStatCard from '@/components/common/LittleStatCard'; +import CyberCard from "@/components/CyberCard.tsx"; interface ServerHeroProps { agent: Agent; @@ -41,7 +42,7 @@ export const ServerHero = ({agent, latestMetrics, onBack}: ServerHeroProps) => { ]; return ( -
+
@@ -91,6 +92,6 @@ export const ServerHero = ({agent, latestMetrics, onBack}: ServerHeroProps) => { 网络累计:{networkSummary}
-
+ ); }; diff --git a/web/src/components/server/SystemInfoSection.tsx b/web/src/components/server/SystemInfoSection.tsx index 162c1f5..557a7df 100644 --- a/web/src/components/server/SystemInfoSection.tsx +++ b/web/src/components/server/SystemInfoSection.tsx @@ -4,6 +4,7 @@ import {Card} from '@/components/common'; import {InfoGrid, SnapshotSection, type SnapshotCardData} from '@/components/server'; import {formatBytes, formatDateTime, formatPercentValue, formatUptime} from '@/utils/util'; import type {Agent, LatestMetrics} from '@/types'; +import CyberCard from "@/components/CyberCard.tsx"; type AccentVariant = 'blue' | 'emerald' | 'purple' | 'amber'; @@ -154,26 +155,26 @@ export const SystemInfoSection = ({agent, latestMetrics}: SystemInfoSectionProps } return ( - +
-
+

运行环境

来自最近一次探针上报的硬件与系统信息

-
-
+ +

运行状态

关键时间与网络指标,帮助快速判断主机健康状况

-
+
- +
); }; diff --git a/web/src/constants/colors.ts b/web/src/constants/colors.ts new file mode 100644 index 0000000..45b28ae --- /dev/null +++ b/web/src/constants/colors.ts @@ -0,0 +1,15 @@ +// 探针颜色方案(用于 MonitorDetail 区分不同探针) +export const AGENT_COLORS = [ + '#3b82f6', // blue + '#10b981', // emerald + '#f59e0b', // amber + '#8b5cf6', // violet + '#ec4899', // pink + '#06b6d4', // cyan + '#f97316', // orange + '#14b8a6', // teal +]; + +// 重新导出其他颜色配置 +export {INTERFACE_COLORS, TEMPERATURE_COLORS, ACCENT_THEMES} from './server'; +export type {TimeRangeOption} from '@/api/property.ts'; diff --git a/web/src/constants/server.ts b/web/src/constants/server.ts new file mode 100644 index 0000000..950171a --- /dev/null +++ b/web/src/constants/server.ts @@ -0,0 +1,43 @@ +// 网卡颜色配置(上行和下行使用不同的色调) +export const INTERFACE_COLORS = [ + {upload: '#6FD598', download: '#2C70F6'}, // 绿/蓝 + {upload: '#f59e0b', download: '#8b5cf6'}, // 橙/紫 + {upload: '#ec4899', download: '#06b6d4'}, // 粉/青 + {upload: '#10b981', download: '#f97316'}, // 翠绿/深橙 + {upload: '#14b8a6', download: '#2563eb'}, // 青绿/深蓝 +]; + +// 温度类型颜色映射 +export const TEMPERATURE_COLORS: Record = { + 'CPU': '#f97316', // 橙色 + 'GPU': '#8b5cf6', // 紫色 + 'DISK': '#06b6d4', // 青色 + 'BATTERY': '#10b981', // 绿色 + 'CHIPSET': '#f59e0b', // 琥珀色 + 'SYSTEM': '#6366f1', // 靛蓝色 + 'PSU': '#ec4899', // 粉色 +}; + +// 主题配色方案 +export const ACCENT_THEMES: Record<'blue' | 'emerald' | 'purple' | 'amber', { icon: string; badge: string; highlight: string }> = { + blue: { + icon: 'text-blue-400', + badge: 'text-blue-400', + highlight: 'text-blue-400', + }, + emerald: { + icon: 'text-emerald-400', + badge: 'text-emerald-400', + highlight: 'text-emerald-400', + }, + purple: { + icon: 'text-purple-400', + badge: 'text-purple-400', + highlight: 'text-purple-400', + }, + amber: { + icon: 'text-amber-400', + badge: 'text-amber-400', + highlight: 'text-amber-400', + }, +}; diff --git a/web/src/constants/time.ts b/web/src/constants/time.ts new file mode 100644 index 0000000..fd8fa55 --- /dev/null +++ b/web/src/constants/time.ts @@ -0,0 +1,22 @@ +import type {TimeRangeOption} from '@/api/property.ts'; + +// 服务器详情页时间范围选项 +export const SERVER_TIME_RANGE_OPTIONS: TimeRangeOption[] = [ + {label: '15分钟', value: '15m'}, + {label: '30分钟', value: '30m'}, + {label: '1小时', value: '1h'}, + {label: '3小时', value: '3h'}, + {label: '6小时', value: '6h'}, + {label: '12小时', value: '12h'}, + {label: '1天', value: '1d'}, + {label: '3天', value: '3d'}, + {label: '7天', value: '7d'}, +]; + +// 监控详情页时间范围选项 +export const MONITOR_TIME_RANGE_OPTIONS: TimeRangeOption[] = [ + {label: '12小时', value: '12h'}, + {label: '1天', value: '1d'}, + {label: '3天', value: '3d'}, + {label: '7天', value: '7d'}, +]; diff --git a/web/src/pages/Public/MonitorList.tsx b/web/src/pages/Public/MonitorList.tsx index 859fb9b..7e70346 100644 --- a/web/src/pages/Public/MonitorList.tsx +++ b/web/src/pages/Public/MonitorList.tsx @@ -1,5 +1,5 @@ import {useEffect, useMemo, useState} from 'react'; -import {useNavigate} from 'react-router-dom'; +import {Link, useNavigate} from 'react-router-dom'; import {useQuery} from '@tanstack/react-query'; import { Activity, @@ -24,6 +24,7 @@ import {StatusBadge} from '@/components/monitor/StatusBadge'; import {CertBadge} from '@/components/monitor/CertBadge'; import {MiniChart} from '@/components/monitor/MiniChart'; import StatCard from "@/components/StatCard.tsx"; +import CyberCard from "@/components/CyberCard.tsx"; const LoadingSpinner = () => (
@@ -62,10 +63,9 @@ const TypeIcon = ({type}: { type: string }) => { }; // 监控卡片组件 -const MonitorCard = ({monitor, displayMode, onClick}: { +const MonitorCard = ({monitor, displayMode}: { monitor: PublicMonitor; displayMode: DisplayMode; - onClick: () => void; }) => { // 为每个监控卡片查询历史数据 const {data: historyData} = useQuery({ @@ -119,91 +119,74 @@ const MonitorCard = ({monitor, displayMode, onClick}: { const displayLabel = displayMode === 'avg' ? '平均延迟' : '最差节点延迟'; return ( -
- {/* 装饰性边框 */} -
-
-
-
-
- -
- {/* 头部 */} -
-
-
- -
-
-
- TARGET_ID: {monitor.id.toString().substring(0, 8)} -
-

- {monitor.name} -

-
+ + {/* 头部 */} +
+
+
+
-
- +
+
+ TARGET_ID: {monitor.id.toString().substring(0, 8)} +
+

+ {monitor.name} +

+
+ +
+
- {/* 指标信息 */} -
-
-

- {displayLabel} - {monitor.agentCount > 0 && ( - + {/* 指标信息 */} +

+
+

+ {displayLabel} + {monitor.agentCount > 0 && ( + {monitor.agentCount} 节点 - )} -

-

200 ? 'text-amber-400' : 'text-cyan-100' - )}> - {displayValue} ms -

-
-
- {monitor.type === 'https' && monitor.certExpiryTime ? ( - <> -

SSL 证书

- - - ) : ( - <> -

上次检测

-

- {formatDateTime(monitor.lastCheckTime)} -

- )} -
+

+

200 ? 'text-amber-400' : 'text-cyan-100' + )}> + {displayValue} ms +

+
+
+ {monitor.type === 'https' && monitor.certExpiryTime ? ( + <> +

SSL 证书

+ + + ) : ( + <> +

上次检测

+

+ {formatDateTime(monitor.lastCheckTime)} +

+ + )}
- - {/* 迷你走势图 */} -
-
+ + {/* 迷你走势图 */} + + ); }; @@ -382,12 +365,13 @@ const MonitorList = () => { ) : (
{filteredMonitors.map(monitor => ( - navigate(`/monitors/${encodeURIComponent(monitor.id)}`)} - /> + + + ))}
)} diff --git a/web/src/pages/Public/ServerList.tsx b/web/src/pages/Public/ServerList.tsx index 20a2281..30e5904 100644 --- a/web/src/pages/Public/ServerList.tsx +++ b/web/src/pages/Public/ServerList.tsx @@ -12,7 +12,6 @@ import { HardDrive, Loader2, MemoryStick, - Network, Server, XCircle } from 'lucide-react'; @@ -21,6 +20,8 @@ import type {Agent, LatestMetrics} from '@/types'; import {cn} from '@/lib/utils'; import CompactResourceBar from "@/components/CompactResourceBar.tsx"; import StatCard from "@/components/StatCard.tsx"; +import ServerCard from "@/components/ServerCard.tsx"; +import NetworkStatCard from "@/components/NetworkStatCard.tsx"; import {formatBytes, formatSpeed} from "@/utils/util.ts"; interface AgentWithMetrics extends Agent { @@ -160,9 +161,9 @@ const ServerList = () => { } return ( -
+
{/* 统计卡片 */} -
+
{ icon={XCircle} color="rose" /> -
-
- -
-
-
-
网络统计 -
-
-
- - {formatSpeed(stats.uploadRate)} - ({formatBytes(stats.uploadTotal)}) -
-
- - {formatSpeed(stats.downloadRate)} - ({formatBytes(stats.downloadTotal)}) -
-
-
-
- -
-
-
+
{/* 标签过滤器 */} {allTags.length > 1 && ( -
-
+
+
- FILTERS: + FILTERS:
{allTags.map(tag => { const tagKey = tag === 'ALL' ? '' : tag; @@ -245,158 +225,171 @@ const ServerList = () => {
)} - {/* 服务器表格 */} + {/* 服务器列表 */} {displayAgents.length === 0 ? ( ) : ( -
- - - - - - - - - - - {displayAgents.map(server => { - const isOnline = server.status === 1; - const cpuUsage = server.metrics?.cpu?.usagePercent ?? 0; - const memoryUsage = server.metrics?.memory?.usagePercent ?? 0; - const memoryTotal = server.metrics?.memory?.total ?? 0; - const memoryUsed = server.metrics?.memory?.used ?? 0; - const diskUsage = calculateDiskUsage(server.metrics); - const diskTotal = server.metrics?.disk?.total ?? 0; - const diskUsed = server.metrics?.disk?.used ?? 0; - const {upload, download} = calculateNetworkSpeed(server.metrics); + <> + {/* 桌面端表格布局 */} +
+
System IdentityResource TelemetryNetwork I/OMeta / Status
+ + + + + + + + + + {displayAgents.map(server => { + const isOnline = server.status === 1; + const cpuUsage = server.metrics?.cpu?.usagePercent ?? 0; + const memoryUsage = server.metrics?.memory?.usagePercent ?? 0; + const memoryTotal = server.metrics?.memory?.total ?? 0; + const memoryUsed = server.metrics?.memory?.used ?? 0; + const diskUsage = calculateDiskUsage(server.metrics); + const diskTotal = server.metrics?.disk?.total ?? 0; + const diskUsed = server.metrics?.disk?.used ?? 0; + const {upload, download} = calculateNetworkSpeed(server.metrics); - return ( - handleNavigate(server.id)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - handleNavigate(server.id); - } - }} - className="group hover:bg-cyan-500/5 transition-colors cursor-pointer" - > - {/* Identity */} - handleNavigate(server.id)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleNavigate(server.id); + } + }} + className="group hover:bg-cyan-500/5 transition-colors cursor-pointer" + > + {/* Identity */} + + + {/* Resources */} + + + {/* Network */} + + + {/* Meta */} + + + + ); + })} + +
System IdentityResource TelemetryNetwork I/OMeta / Status
-
-
+ return ( +
+
+
+
+ {server.name || server.hostname} +
+
+ {server.os} + + {server.arch} +
+
+
+
+ {isOnline ? ( +
+ + + +
+ ) : (
- {server.name || server.hostname} + className="text-xs text-rose-500 font-mono flex items-center gap-2 py-4"> + + CONNECTION_LOST // RECONNECTING... +
+ )} +
+
+ + + IN: {formatSpeed(download)} + + + + OUT: {formatSpeed(upload)} + +
+ {server.trafficLimit > 0 ? ( +
+
+ 流量使用 + {Math.round((server.trafficUsed || 0) / server.trafficLimit * 100)}% +
+
+
+
+
+ ) : ( +
-- NO METERING --
+ )} +
+
+
+ {server.tags && server.tags.length > 0 && server.tags.map(tag => ( + + #{tag} + + ))}
- {server.os} - - {server.arch} + className={`text-[10px] font-mono flex items-center gap-1 ${server.expireTime && server.expireTime > 0 ? 'text-cyan-600' : 'text-emerald-500/60'}`}> + + {server.expireTime > 0 + ? `EXP: ${new Date(server.expireTime).toLocaleDateString('zh-CN')}` + : '' + }
- -
+
- {/* Resources */} - - {isOnline ? ( -
- - - -
- ) : ( -
- - CONNECTION_LOST // RECONNECTING... -
- )} - - - {/* Network */} - -
- - - IN: {formatSpeed(download)} - - - - OUT: {formatSpeed(upload)} - -
- {server.trafficLimit > 0 ? ( -
-
- TRAFFIC - {Math.round((server.trafficUsed || 0) / server.trafficLimit * 100)}% -
-
-
-
-
- ) : ( -
-- NO METERING --
- )} - - - {/* Meta */} - -
-
- {server.tags && server.tags.length > 0 && server.tags.map(tag => ( - - #{tag} - - ))} -
-
0 ? 'text-cyan-600' : 'text-emerald-500/60'}`}> - - {server.expireTime && server.expireTime > 0 - ? `EXP: ${new Date(server.expireTime).toLocaleDateString('zh-CN')}` - : 'PERMANENT_NODE' - } -
-
- - - ); - })} - - -
+ {/* 移动端卡片布局 */} +
+ {displayAgents.map(server => ( + + ))} +
+ )}
);