feat: 全站 PageShell 设计系统与专业 UI 统一

- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
Logan committed 2026-07-01 12:45:38 +08:00
1 parent 1acb0c49af
commit e363ded445
213 files changed
+7148 -6818

No files matched your search

+133 -107
View File
@@ -8,11 +8,13 @@ import { useHostList, useHostHistory, usePortPolicy } from '@/hooks/api/useHostM
import { queryKeys } from '@/hooks/api/queryKeys';
import { Card, CardContent } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { PageShell } from '@/components/layout/PageShell';
import { PageLoading } from '@/components/layout/PageLoading';
import {
Cpu, HardDrive, Network, Activity, Server, Loader2,
Cpu, HardDrive, Network, Activity, Server,
Info, Monitor, Disc, Clock, List, HeartPulse, RefreshCw, XCircle,
} from 'lucide-react';
import {
@@ -27,7 +29,7 @@ import { PortPolicyDialog } from './host-monitor/PortPolicyDialog';
import { SystemInfoSection } from './host-monitor/SystemInfoSection';
import type { JsonData } from '@/types/common';
export function HostMonitor() {
export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
const qc = useQueryClient();
const [searchParams] = useSearchParams();
const urlHost = searchParams.get('host') || '';
@@ -95,111 +97,108 @@ export function HostMonitor() {
const currentVal = (key: string, fallback = '--') =>
getCurrentVal(latest, netInfo, diskIOInfo, swapInfo, fdInfo, key, fallback);
if (loading && !history.length) {
return <div className="flex items-center justify-center h-64"><Loader2 className="h-8 w-8 animate-spin text-slate-500" /></div>;
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-2">
<Server className="h-4 w-4 text-blue-400" />
<Select value={selectedHost} onValueChange={setSelectedHost}>
<SelectTrigger className="w-52 h-8 text-xs bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<SelectValue placeholder="选择主机..." />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
{hosts.map((h: JsonData) => (
<SelectItem key={h.hostname} value={h.hostname}>{h.hostname}</SelectItem>
))}
</SelectContent>
</Select>
{serverInfo && (
<span className="text-xs text-[var(--dg-text-muted)] ml-2">
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} 核` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
</span>
)}
</div>
<div className="flex items-center gap-2">
<Select value={aggType} onValueChange={(v) => setAggType(v as AggType)}>
<SelectTrigger className="w-24 h-7 text-xs bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
<SelectItem value="avg">平均值</SelectItem>
<SelectItem value="max">最大值</SelectItem>
<SelectItem value="min">最小值</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-1">
{TIME_RANGES.map(tr => (
<Button key={tr.hours} variant={rangeHours === tr.hours ? 'default' : 'outline'} size="sm"
className="h-7 text-xs px-2.5" onClick={() => setRangeHours(tr.hours)}>{tr.label}</Button>
const toolbar = (
<div className="flex items-center justify-between flex-wrap gap-2 w-full">
<div className="flex items-center gap-2">
<Select value={selectedHost} onValueChange={setSelectedHost}>
<SelectTrigger className="w-52 h-8 text-xs dg-input">
<SelectValue placeholder="选择主机..." />
</SelectTrigger>
<SelectContent className="dg-input">
{hosts.map((h: JsonData) => (
<SelectItem key={h.hostname} value={h.hostname}>{h.hostname}</SelectItem>
))}
</div>
</SelectContent>
</Select>
{serverInfo && (
<span className="text-xs dg-text-muted">
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} 核` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
</span>
)}
</div>
<div className="flex items-center gap-2">
<Select value={aggType} onValueChange={(v) => setAggType(v as AggType)}>
<SelectTrigger className="w-24 h-7 text-xs dg-input">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="avg">平均值</SelectItem>
<SelectItem value="max">最大值</SelectItem>
<SelectItem value="min">最小值</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-1">
{TIME_RANGES.map(tr => (
<Button key={tr.hours} variant={rangeHours === tr.hours ? 'default' : 'outline'} size="sm"
className={`h-7 text-xs px-2.5 ${rangeHours === tr.hours ? 'dg-btn-primary' : 'dg-btn-secondary'}`}
onClick={() => setRangeHours(tr.hours)}>{tr.label}</Button>
))}
</div>
</div>
</div>
);
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="h-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] gap-0 overflow-x-auto">
{[
{ id: 'basic', label: '基础监控', icon: <Monitor className="w-3 h-3" /> },
{ id: 'disk', label: '磁盘监控', icon: <HardDrive className="w-3 h-3" /> },
{ id: 'network', label: '网络监控', icon: <Network className="w-3 h-3" /> },
{ id: 'ports', label: '端口监控', icon: <Server className="w-3 h-3" /> },
{ id: 'process', label: '进程监控', icon: <List className="w-3 h-3" /> },
{ id: 'system', label: '系统信息', icon: <Info className="w-3 h-3" /> },
{ id: 'health_probes', label: '健康探针', icon: <HeartPulse className="w-3 h-3" /> },
].map(tab => (
<TabsTrigger key={tab.id} value={tab.id} className="text-[11px] h-7 gap-1 data-[state=active]:bg-[var(--dg-bg-primary)]">
{tab.icon}{tab.label}
</TabsTrigger>
))}
</TabsList>
const monitorBody = (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="h-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] gap-0 overflow-x-auto">
{[
{ id: 'basic', label: '基础监控', icon: <Monitor className="w-3 h-3" /> },
{ id: 'disk', label: '磁盘监控', icon: <HardDrive className="w-3 h-3" /> },
{ id: 'network', label: '网络监控', icon: <Network className="w-3 h-3" /> },
{ id: 'ports', label: '端口监控', icon: <Server className="w-3 h-3" /> },
{ id: 'process', label: '进程监控', icon: <List className="w-3 h-3" /> },
{ id: 'system', label: '系统信息', icon: <Info className="w-3 h-3" /> },
{ id: 'health_probes', label: '健康探针', icon: <HeartPulse className="w-3 h-3" /> },
].map(tab => (
<TabsTrigger key={tab.id} value={tab.id} className="text-[11px] h-7 gap-1 data-[state=active]:bg-[var(--dg-bg-primary)]">
{tab.icon}{tab.label}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 text-blue-400" />}
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 dg-text-blue" />}
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock className="h-4 w-4 text-orange-400" />}
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock className="h-4 w-4 dg-text-orange" />}
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 text-emerald-400" />}
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 dg-text-green" />}
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server className="h-4 w-4 text-emerald-400" />}
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server className="h-4 w-4 dg-text-green" />}
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive className="h-4 w-4 text-red-400" />}
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive className="h-4 w-4 dg-text-red" />}
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 text-purple-400" />}
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 dg-text-purple" />}
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
</TabsContent>
<TabsContent value="disk" className="mt-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricChartCard title="磁盘使用率(%)" value={currentVal('disk')} icon={<HardDrive className="h-4 w-4 text-orange-400" />}
<MetricChartCard title="磁盘使用率(%)" value={currentVal('disk')} icon={<HardDrive className="h-4 w-4 dg-text-orange" />}
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title="Inode 使用率(%)" value={(() => {
const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : [];
const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0];
return root?.inode_usage_pct != null ? `${root.inode_usage_pct.toFixed(1)}%` : '--';
})()} icon={<HardDrive className="h-4 w-4 text-purple-400" />}
})()} icon={<HardDrive className="h-4 w-4 dg-text-purple" />}
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title="磁盘读取(KB/s)" value={currentVal('ioR')} icon={<Disc className="h-4 w-4 text-blue-400" />}
<MetricChartCard title="磁盘读取(KB/s)" value={currentVal('ioR')} icon={<Disc className="h-4 w-4 dg-text-blue" />}
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 text-emerald-400" />}
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 dg-text-green" />}
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc className="h-4 w-4 text-red-400" />}
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc className="h-4 w-4 dg-text-red" />}
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
</div>
</TabsContent>
<TabsContent value="network" className="mt-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network className="h-4 w-4 text-purple-400" />}
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network className="h-4 w-4 dg-text-purple" />}
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 dg-text-yellow" />}
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP CLOSE_WAIT" value={netInfo?.tcp_close_wait != null ? String(netInfo.tcp_close_wait) : '--'} icon={<Clock className="h-4 w-4 text-red-400" />}
<MetricChartCard title="TCP CLOSE_WAIT" value={netInfo?.tcp_close_wait != null ? String(netInfo.tcp_close_wait) : '--'} icon={<Clock className="h-4 w-4 dg-text-red" />}
data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP 总连接" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network className="h-4 w-4 text-cyan-400" />}
<MetricChartCard title="TCP 总连接" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network className="h-4 w-4 dg-text-cyan" />}
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
</div>
<DualLineChart
@@ -210,9 +209,9 @@ export function HostMonitor() {
aggType={aggType} unit="KB/s"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<MetricChartCard title="网卡接收错误" value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-red-400" />}
<MetricChartCard title="网卡接收错误" value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle className="h-4 w-4 dg-text-red" />}
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="网卡发送错误" value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-orange-400" />}
<MetricChartCard title="网卡发送错误" value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle className="h-4 w-4 dg-text-orange" />}
data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />
</div>
</TabsContent>
@@ -221,15 +220,15 @@ export function HostMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<Server className="h-4 w-4 text-emerald-400" />监听端口 {portInfo.length}
<Server className="h-4 w-4 dg-text-green" />监听端口 {portInfo.length}
<span className="ml-auto flex items-center gap-2">
{portPolicyMode === 'blacklist' && portWhitelist.length > 0 && (
<Badge variant="outline" className="text-[10px] text-amber-400 border-amber-500/30">
<Badge variant="outline" className="text-[10px] dg-text-yellow border-amber-500/30">
黑名单: {portWhitelist.join(',')}
</Badge>
)}
{portPolicyMode === 'whitelist' && (
<Badge variant="outline" className="text-[10px] text-blue-400 border-blue-500/30">
<Badge variant="outline" className="text-[10px] dg-text-blue border-blue-500/30">
白名单模式
</Badge>
)}
@@ -256,7 +255,7 @@ export function HostMonitor() {
</thead>
<tbody>
{portInfo.map((p: JsonData, i: number) => (
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-slate-800/30 transition-colors">
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-[var(--dg-bg-hover)] transition-colors">
<td className="py-1.5 font-mono text-[var(--dg-text-primary)]">{p.port}</td>
<td className="py-1.5">
<Badge variant="outline" className="text-[10px] font-mono">
@@ -265,7 +264,7 @@ export function HostMonitor() {
</td>
<td className="py-1.5 font-mono text-[var(--dg-text-muted)]">{p.process || '—'}</td>
<td className="py-1.5 text-right">
<Badge variant="default" className="text-[10px] bg-emerald-500/20 text-emerald-400 border-emerald-500/30">
<Badge variant="default" className="text-[10px] bg-emerald-500/20 dg-text-green border-emerald-500/30">
LISTEN
</Badge>
</td>
@@ -283,7 +282,7 @@ export function HostMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<List className="h-4 w-4 text-cyan-400" />进程 TOP {processTop.length}
<List className="h-4 w-4 dg-text-cyan" />进程 TOP {processTop.length}
</h3>
{processTop.length === 0 ? (
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
@@ -307,14 +306,14 @@ export function HostMonitor() {
const cpup = p.cpu_pct != null ? Number(p.cpu_pct) : null;
const memp = p.mem_pct != null ? Number(p.mem_pct) : null;
return (
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-slate-800/30 transition-colors">
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-[var(--dg-bg-hover)] transition-colors">
<td className="py-1.5 pr-2">
<div className="flex items-center gap-1 group/cmd">
<span className="font-mono text-[var(--dg-text-primary)] truncate max-w-[320px] inline-block align-middle" title={cmd}>
{cmd}
</span>
<button
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:text-cyan-400 shrink-0"
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:dg-text-cyan shrink-0"
title="复制命令行"
onClick={async (e) => {
e.stopPropagation();
@@ -337,7 +336,7 @@ export function HostMonitor() {
</td>
<td className="py-1.5 text-right font-mono text-[var(--dg-text-muted)]">{p.pid}</td>
<td className="py-1.5 text-right font-mono">
<span className={memp != null && memp > 50 ? 'text-rose-400' : 'text-cyan-400'}>
<span className={memp != null && memp > 50 ? 'text-rose-400' : 'dg-text-cyan'}>
{memp != null ? memp.toFixed(2) : '--'}
</span>
</td>
@@ -361,7 +360,7 @@ export function HostMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<HardDrive className="h-4 w-4 text-violet-400" />FD 文件句柄
<HardDrive className="h-4 w-4 dg-text-purple" />FD 文件句柄
</h3>
{fdInfo?.allocated != null ? (
<>
@@ -376,7 +375,7 @@ export function HostMonitor() {
</div>
<div>
<span className="text-[var(--dg-text-muted)]">使用率 </span>
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'text-violet-400'}`}>
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'dg-text-purple'}`}>
{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
</span>
</div>
@@ -410,21 +409,21 @@ export function HostMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<RefreshCw className="h-4 w-4 text-emerald-400" />上下文切换
<RefreshCw className="h-4 w-4 dg-text-green" />上下文切换
</h3>
{csInfo?.ctx_per_sec != null ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">CTX 切换/秒</div>
<div className="text-lg font-bold font-mono text-emerald-400">
<div className="text-lg font-bold font-mono dg-text-green">
{(csInfo.ctx_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.ctx_per_sec?.toLocaleString()} 次</div>
</div>
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">IRQ 中断/秒</div>
<div className="text-lg font-bold font-mono text-amber-400">
<div className="text-lg font-bold font-mono dg-text-yellow">
{(csInfo.irq_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.irq_per_sec?.toLocaleString()} 次</div>
@@ -432,8 +431,8 @@ export function HostMonitor() {
</div>
<div className="space-y-1">
<div className="flex justify-between text-[10px]">
<span className="text-emerald-400">CTX</span>
<span className="text-amber-400">IRQ</span>
<span className="dg-text-green">CTX</span>
<span className="dg-text-yellow">IRQ</span>
</div>
<div className="h-2 bg-[var(--dg-bg-primary)] rounded-full overflow-hidden flex">
{csInfo.ctx_per_sec + csInfo.irq_per_sec > 0 && (
@@ -463,19 +462,46 @@ export function HostMonitor() {
portInfoCount={portInfo.length}
/>
<HealthProbeSection selectedHost={selectedHost} />
</Tabs>
<HealthProbeSection selectedHost={selectedHost} />
</Tabs>
);
<PortPolicyDialog
open={portPolicyDialogOpen}
onOpenChange={setPortPolicyDialogOpen}
portPolicyMode={portPolicyMode}
onModeChange={setPortPolicyMode}
portWhitelist={portWhitelist}
onWhitelistChange={setPortWhitelist}
onSave={handleSavePortPolicy}
saving={portPolicySaving}
/>
</div>
const dialogs = (
<PortPolicyDialog
open={portPolicyDialogOpen}
onOpenChange={setPortPolicyDialogOpen}
portPolicyMode={portPolicyMode}
onModeChange={setPortPolicyMode}
portWhitelist={portWhitelist}
onWhitelistChange={setPortWhitelist}
onSave={handleSavePortPolicy}
saving={portPolicySaving}
/>
);
if (embedded) {
return (
<div className="space-y-3">
{toolbar}
{loading && !history.length ? <PageLoading /> : monitorBody}
{dialogs}
</div>
);
}
return (
<PageShell
variant="workspace"
title="主机监控"
subtitle={selectedHost ? `当前主机: ${selectedHost}` : '选择主机查看监控指标'}
icon={<Server className="h-5 w-5" />}
toolbar={toolbar}
loading={loading && !history.length}
loadingLabel="加载监控数据..."
contentClassName="space-y-3"
>
{monitorBody}
{dialogs}
</PageShell>
);
}
+63 -80
View File
@@ -6,14 +6,10 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Activity, Search, Filter, BarChart3, Cpu, HardDrive, Network, Database, Plus } from 'lucide-react';
import { metricsApi } from '@/api';
import { PageShell } from '@/components/layout/PageShell';
import { PageEmpty } from '@/components/layout/PageEmpty';
import type { JsonData } from '@/types/common';
interface _MetricName {
name: string;
category: string;
data_source: string;
}
interface TargetInfo {
id: number;
name: string;
@@ -49,7 +45,6 @@ export function MetricsExplorer() {
const [loading, setLoading] = useState(true);
const [searchKw, setSearchKw] = useState('');
// Load targets
const loadTargets = useCallback(async () => {
try {
const res: JsonData = await metricsApi.getTargets({ page_size: 200 });
@@ -58,7 +53,6 @@ export function MetricsExplorer() {
finally { setLoading(false); }
}, []);
// Load metric names
const loadMetricNames = useCallback(async () => {
try {
const names: string[] = await metricsApi.getNames();
@@ -66,16 +60,15 @@ export function MetricsExplorer() {
} catch { /* empty */ }
}, []);
useEffect(() => { loadTargets(); loadMetricNames(); }, []);
useEffect(() => { loadTargets(); loadMetricNames(); }, [loadTargets, loadMetricNames]);
// Query metric data when target + metrics selected
useEffect(() => {
if (!selectedTarget || selectedMetrics.length === 0) return;
const target = targets.find(t => String(t.id) === selectedTarget);
if (!target) return;
const to = Math.floor(Date.now() / 1000);
const from = to - 3600; // last hour
const from = to - 3600;
Promise.all(
selectedMetrics.map(mn =>
@@ -88,15 +81,12 @@ export function MetricsExplorer() {
});
}, [selectedTarget, selectedMetrics, targets]);
// Filter metric names by search
const filteredMetrics = searchKw
? metricNames.filter(n => n.toLowerCase().includes(searchKw.toLowerCase()))
: metricNames;
// Selected target info
const target = targets.find(t => String(t.id) === selectedTarget);
// Group metrics by category
const grouped = filteredMetrics.reduce((acc: Record<string, string[]>, name) => {
const cat = name.split('_')[0] || 'other';
if (!acc[cat]) acc[cat] = [];
@@ -104,7 +94,6 @@ export function MetricsExplorer() {
return acc;
}, {});
// Merge query results into time-aligned data
const timeMap: Record<number, Record<string, number>> = {};
queryResults.forEach(pt => {
if (!timeMap[pt.timestamp]) timeMap[pt.timestamp] = {};
@@ -115,61 +104,68 @@ export function MetricsExplorer() {
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#f97316'];
return (
<div className="space-y-6">
{/* Toolbar */}
<div className="flex items-center gap-4 flex-wrap">
<Select value={selectedTarget} onValueChange={setSelectedTarget}>
<SelectTrigger className="w-64 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<SelectValue placeholder={loading ? '加载中...' : '选择监控目标...'} />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200 max-h-80">
{targets.map(t => (
<SelectItem key={t.id} value={String(t.id)}>
<span className="flex items-center gap-2">
<span className={`inline-block w-1.5 h-1.5 rounded-full ${t.status === 'online' ? 'bg-emerald-400' : 'bg-slate-500'}`} />
{t.name} ({t.ip || t.type})
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2.5 top-2 h-4 w-4 text-slate-500" />
<Input
className="pl-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] h-9"
placeholder="搜索指标..."
value={searchKw}
onChange={(e) => setSearchKw(e.target.value)}
/>
</div>
<Button variant="ghost" size="sm" onClick={() => { setSelectedMetrics([]); setQueryResults([]); }}>
清除
</Button>
const toolbar = (
<div className="flex items-center gap-4 flex-wrap w-full">
<Select value={selectedTarget} onValueChange={setSelectedTarget}>
<SelectTrigger className="w-64 dg-input">
<SelectValue placeholder={loading ? '加载中...' : '选择监控目标...'} />
</SelectTrigger>
<SelectContent className="dg-input max-h-80">
{targets.map(t => (
<SelectItem key={t.id} value={String(t.id)}>
<span className="flex items-center gap-2">
<span className={`inline-block w-1.5 h-1.5 rounded-full ${t.status === 'online' ? 'bg-emerald-400' : 'bg-[var(--dg-bg-active)]'}`} />
{t.name} ({t.ip || t.type})
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2.5 top-2 h-4 w-4 dg-text-muted" />
<Input
className="pl-8 dg-input h-9"
placeholder="搜索指标..."
value={searchKw}
onChange={(e) => setSearchKw(e.target.value)}
/>
</div>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => { setSelectedMetrics([]); setQueryResults([]); }}>
清除
</Button>
</div>
);
return (
<PageShell
variant="workspace"
title="指标浏览器"
subtitle="按分类/主机浏览所有指标,支持叠加对比"
icon={<Activity className="h-5 w-5" />}
toolbar={toolbar}
loading={loading}
loadingLabel="加载监控目标..."
contentClassName="space-y-6"
>
{!selectedTarget ? (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-12 text-center">
<Filter className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-[var(--dg-text-muted)]">请先选择监控目标,然后勾选要查看的指标</p>
</CardContent>
</Card>
<PageEmpty
icon={<Filter className="h-12 w-12" />}
title="请选择监控目标"
description="选择目标主机后,从左侧勾选要查看的指标"
/>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Metric selector */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] lg:col-span-1">
<Card className="dg-card lg:col-span-1">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<BarChart3 className="h-4 w-4 text-[var(--dg-text-muted)]" />
<BarChart3 className="h-4 w-4 dg-text-muted" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">指标列表</h3>
<span className="text-xs text-[var(--dg-text-muted)] ml-auto">{selectedMetrics.length} 已选</span>
<span className="text-xs dg-text-muted ml-auto">{selectedMetrics.length} 已选</span>
</div>
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
{Object.entries(grouped).map(([cat, names]) => (
<div key={cat}>
<div className="flex items-center gap-1.5 mb-1.5 text-xs text-[var(--dg-text-muted)]">
<div className="flex items-center gap-1.5 mb-1.5 text-xs dg-text-muted">
{METRIC_CATEGORIES[cat]?.icon}
<span>{METRIC_CATEGORIES[cat]?.label || cat}</span>
</div>
@@ -177,10 +173,10 @@ export function MetricsExplorer() {
{names.map(name => {
const selected = selectedMetrics.includes(name);
return (
<label key={name} className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer text-xs transition-colors ${selected ? 'bg-blue-500/10 text-blue-300' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}>
<label key={name} className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer text-xs transition-colors ${selected ? 'bg-[var(--dg-bg-active)] dg-text-blue' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}>
<input
type="checkbox"
className="rounded border-slate-600 bg-slate-800 accent-blue-500"
className="rounded border-[var(--dg-border-hover)] bg-[var(--dg-bg-tertiary)] accent-[var(--dg-blue)]"
checked={selected}
onChange={() => {
setSelectedMetrics(prev =>
@@ -199,34 +195,24 @@ export function MetricsExplorer() {
</CardContent>
</Card>
{/* Chart area */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] lg:col-span-2">
<Card className="dg-card lg:col-span-2">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
<Activity className="h-4 w-4 dg-text-muted" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">
{target?.name || ''} — 最近 1 小时
</h3>
</div>
{selectedMetrics.length === 0 ? (
<div className="h-64 flex items-center justify-center text-sm text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
<div className="text-center">
<Plus className="h-8 w-8 text-slate-600 mx-auto mb-2" />
请从左侧勾选指标
</div>
</div>
<PageEmpty icon={<Plus className="h-8 w-8" />} title="请从左侧勾选指标" className="py-16" />
) : sortedTimes.length === 0 ? (
<div className="h-64 flex items-center justify-center text-sm text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
暂无数据
</div>
<PageEmpty title="暂无数据" className="py-16" />
) : (
<div className="h-80 overflow-x-auto">
<svg viewBox={`0 0 ${Math.max(sortedTimes.length * 4, 400)} 300`} className="w-full h-full min-w-[400px]">
{/* Grid lines */}
{[0, 25, 50, 75, 100].map(pct => (
<line key={pct} x1="0" y1={300 - (pct / 100) * 260} x2={Math.max(sortedTimes.length * 4, 400)} y2={300 - (pct / 100) * 260} stroke="#1e293b" strokeDasharray="3 3" />
))}
{/* Lines per metric */}
{selectedMetrics.map((mn, mi) => {
const points = sortedTimes
.map((t, i) => ({ x: i * 4 + 2, y: 300 - ((timeMap[t][mn] ?? 0) / maxVal) * 260 }))
@@ -239,7 +225,6 @@ export function MetricsExplorer() {
</g>
);
})}
{/* X axis time labels */}
{sortedTimes.filter((_, i) => i % 15 === 0).map((t, i) => (
<text key={t} x={i * 60 + 2} y="295" fill="#475569" fontSize="9">
{new Date(t * 1000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
@@ -248,7 +233,6 @@ export function MetricsExplorer() {
</svg>
</div>
)}
{/* Legend */}
{selectedMetrics.length > 0 && (
<div className="flex flex-wrap gap-3 mt-3">
{selectedMetrics.map((mn, mi) => (
@@ -264,12 +248,11 @@ export function MetricsExplorer() {
</div>
)}
{/* Target metrics overview */}
{target && Object.keys(target.latest_metrics || {}).length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
{Object.entries(target.latest_metrics).map(([key, val]) => (
<div key={key} className="bg-[var(--dg-bg-secondary)] rounded-lg p-3 border border-[var(--dg-border)] text-center">
<p className="text-[10px] text-[var(--dg-text-muted)] truncate">{key}</p>
<p className="text-[10px] dg-text-muted truncate">{key}</p>
<p className="text-lg font-bold text-[var(--dg-text-primary)]">
{typeof val === 'number' ? val.toFixed(1) : '--'}
</p>
@@ -277,8 +260,8 @@ export function MetricsExplorer() {
))}
</div>
)}
</div>
</PageShell>
);
}
export default MetricsExplorer;
export default MetricsExplorer;
@@ -54,11 +54,11 @@ function fmtVal(v: number, precision = 0): string {
function statusColor(status: string): string {
switch (status) {
case 'online': return 'text-emerald-400';
case 'warning': return 'text-amber-400';
case 'online': return 'dg-text-green';
case 'warning': return 'dg-text-yellow';
case 'error':
case 'offline': return 'text-red-400';
default: return 'text-slate-500';
case 'offline': return 'dg-text-red';
default: return 'dg-text-muted';
}
}
@@ -68,7 +68,7 @@ function statusBg(status: string): string {
case 'warning': return 'bg-amber-400';
case 'error':
case 'offline': return 'bg-red-400';
default: return 'bg-slate-500';
default: return 'bg-[var(--dg-bg-active)]';
}
}
@@ -174,7 +174,7 @@ export function MiddlewareMonitor() {
{loading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-slate-500 mr-2" />
<Loader2 className="h-6 w-6 animate-spin dg-text-muted mr-2" />
<span className="text-sm text-[var(--dg-text-muted)]">正在加载中间件指标...</span>
</div>
)}
@@ -182,8 +182,8 @@ export function MiddlewareMonitor() {
{error && !loading && (
<Card className="bg-[var(--dg-bg-secondary)] border-red-500/30">
<CardContent className="p-6 text-center">
<Activity className="h-8 w-8 text-red-400 mx-auto mb-2" />
<p className="text-sm text-red-400">{error}</p>
<Activity className="h-8 w-8 dg-text-red mx-auto mb-2" />
<p className="text-sm dg-text-red">{error}</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-2">请确认后端服务可用</p>
</CardContent>
</Card>
@@ -192,7 +192,7 @@ export function MiddlewareMonitor() {
{!loading && !error && targets.length === 0 && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-12 text-center">
<Database className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<Database className="h-12 w-12 dg-text-muted mx-auto mb-4" />
<p className="text-sm text-[var(--dg-text-muted)]">暂无中间件监控目标</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-2">点击「添加监控」配置 Redis / MySQL 连接</p>
<Button size="sm" className="mt-4 gap-1.5" onClick={() => { setEditTarget(null); setDialogOpen(true); }}>
@@ -208,7 +208,7 @@ export function MiddlewareMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-4">
<Database className="h-4 w-4 text-red-400" />
<Database className="h-4 w-4 dg-text-red" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">Redis 监控</h3>
{redisTarget && (
<Badge variant="outline" className="ml-auto text-[10px] border-[var(--dg-border)] text-[var(--dg-text-muted)]">
@@ -237,7 +237,7 @@ export function MiddlewareMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-4">
<Server className="h-4 w-4 text-blue-400" />
<Server className="h-4 w-4 dg-text-blue" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">MySQL 监控</h3>
{mysqlTarget && (
<Badge variant="outline" className="ml-auto text-[10px] border-[var(--dg-border)] text-[var(--dg-text-muted)]">
@@ -358,7 +358,7 @@ function InstanceCard({
</div>
<Badge
variant="outline"
className={`text-[10px] capitalize ${target.status === 'online' ? 'border-emerald-400/50 text-emerald-400' : 'border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
className={`text-[10px] capitalize ${target.status === 'online' ? 'border-emerald-400/50 dg-text-green' : 'border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
>
{target.status}
</Badge>
@@ -393,7 +393,7 @@ function InstanceCard({
<Pencil className="h-3 w-3" />
编辑
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs gap-1 text-red-400 hover:text-red-300" onClick={onDelete}>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs gap-1 dg-text-red hover:dg-text-red" onClick={onDelete}>
<Trash2 className="h-3 w-3" />
删除
</Button>
+25 -54
View File
@@ -1,7 +1,7 @@
// 监控中心 — 4个监控标签页的容器
// 监控中心 — 4个监控标签页的容器 // Logan.Li
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { Cpu, Database, Radio, Globe } from 'lucide-react';
import { Cpu } from 'lucide-react';
import { PageShell } from '@/components/layout/PageShell';
import { HostMonitor } from './HostMonitor';
import { MiddlewareMonitor } from './MiddlewareMonitor';
import { SNMPMonitor } from './SNMPMonitor';
@@ -9,61 +9,32 @@ import { SiteMonitor } from './SiteMonitor';
type TabId = 'host' | 'middleware' | 'snmp' | 'site';
interface Tab {
id: TabId;
name: string;
icon: React.ElementType;
description: string;
}
const tabs: Tab[] = [
{ id: 'host', name: '主机监控', icon: Cpu, description: 'CPU / 内存 / 磁盘 / 网络 / 磁盘IO' },
{ id: 'middleware', name: '中间件监控', icon: Database, description: 'Redis / MySQL 指标采集' },
{ id: 'snmp', name: 'SNMP 监控', icon: Radio, description: '网络设备 / 服务器硬件 (OID 模板)' },
{ id: 'site', name: '站点拨测', icon: Globe, description: 'HTTP / PING / DNS / TCP / WS 拨测' },
const tabs: { id: TabId; name: string; description: string }[] = [
{ id: 'host', name: '主机监控', description: 'CPU / 内存 / 磁盘 / 网络 / 磁盘IO' },
{ id: 'middleware', name: '中间件监控', description: 'Redis / MySQL 指标采集' },
{ id: 'snmp', name: 'SNMP 监控', description: '网络设备 / 服务器硬件 (OID 模板)' },
{ id: 'site', name: '站点拨测', description: 'HTTP / PING / DNS / TCP / WS 拨测' },
];
export function MonitorCenter() {
const [activeTab, setActiveTab] = useState<TabId>('host');
const current = tabs.find((t) => t.id === activeTab);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">监控中心</h1>
<p className="text-sm text-[var(--dg-text-muted)] mt-1">全面的基础设施与应用监控</p>
</div>
{/* Tab Bar */}
<div className="flex gap-1 bg-[var(--dg-bg-secondary)] p-1 rounded-lg border border-[var(--dg-border)]">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={cn(
'flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-md text-sm font-medium transition-all',
activeTab === tab.id
? 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-primary)] shadow-sm'
: 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]'
)}
>
<tab.icon className="h-4 w-4" />
<span className="hidden sm:inline">{tab.name}</span>
</button>
))}
</div>
{/* Tab Description */}
<p className="text-sm text-[var(--dg-text-muted)] -mt-4">
{tabs.find(t => t.id === activeTab)?.description}
</p>
{/* Tab Content */}
<div className="min-h-[400px]">
{activeTab === 'host' && <HostMonitor />}
{activeTab === 'middleware' && <MiddlewareMonitor />}
{activeTab === 'snmp' && <SNMPMonitor />}
{activeTab === 'site' && <SiteMonitor />}
</div>
</div>
<PageShell
variant="list"
title="监控中心"
subtitle={current?.description ?? '全面的基础设施与应用监控'}
icon={<Cpu className="h-5 w-5" />}
tabs={tabs.map((t) => ({ id: t.id, label: t.name }))}
activeTab={activeTab}
onTabChange={(id) => setActiveTab(id as TabId)}
contentClassName="min-h-[400px]"
>
{activeTab === 'host' && <HostMonitor embedded />}
{activeTab === 'middleware' && <MiddlewareMonitor />}
{activeTab === 'snmp' && <SNMPMonitor />}
{activeTab === 'site' && <SiteMonitor />}
</PageShell>
);
}
}
+14 -14
View File
@@ -143,12 +143,12 @@ export function SNMPMonitor() {
// ---------- status badge ----------
const statusBadge = (status?: string) => {
if (status === 'online') {
return <Badge className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">在线</Badge>;
return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30">在线</Badge>;
}
if (status === 'abnormal') {
return <Badge className="bg-red-500/15 text-red-400 border-red-500/30">异常</Badge>;
return <Badge className="bg-red-500/15 dg-text-red border-red-500/30">异常</Badge>;
}
return <Badge className="bg-slate-500/15 text-slate-400 border-slate-500/30">离线</Badge>;
return <Badge className="bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]">离线</Badge>;
};
// ---------- render ----------
@@ -159,7 +159,7 @@ export function SNMPMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Router className="h-4 w-4 text-cyan-400" />
<Router className="h-4 w-4 dg-text-cyan" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">网络设备</h3>
</div>
<p className="text-xs text-[var(--dg-text-muted)] mb-3">交换机 / 路由器 / 防火墙</p>
@@ -170,7 +170,7 @@ export function SNMPMonitor() {
</div>
<div className="flex justify-between text-sm">
<span className="text-[var(--dg-text-muted)]">在线</span>
<span className="text-emerald-400 font-mono">{stats.online}</span>
<span className="dg-text-green font-mono">{stats.online}</span>
</div>
</div>
</CardContent>
@@ -179,14 +179,14 @@ export function SNMPMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<HardDrive className="h-4 w-4 text-purple-400" />
<HardDrive className="h-4 w-4 dg-text-purple" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">服务器硬件</h3>
</div>
<p className="text-xs text-[var(--dg-text-muted)] mb-3">iLO / iDRAC / IPMI</p>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-[var(--dg-text-muted)]">异常</span>
<span className="text-red-400 font-mono">{stats.abnormal}</span>
<span className="dg-text-red font-mono">{stats.abnormal}</span>
</div>
</div>
</CardContent>
@@ -221,7 +221,7 @@ export function SNMPMonitor() {
<CardContent className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Router className="h-4 w-4 text-cyan-400" />
<Router className="h-4 w-4 dg-text-cyan" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">
SNMP 设备列表
{!loading && (
@@ -235,12 +235,12 @@ export function SNMPMonitor() {
{/* Add Device Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" className="bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20 border border-cyan-500/30">
<Button size="sm" className="bg-cyan-500/10 dg-text-cyan hover:bg-cyan-500/20 border border-cyan-500/30">
<Plus className="h-4 w-4" />
添加设备
</Button>
</DialogTrigger>
<DialogContent className="bg-slate-900 border-[var(--dg-border)] text-[var(--dg-text-primary)] sm:max-w-md">
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">添加 SNMP 设备</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
@@ -307,7 +307,7 @@ export function SNMPMonitor() {
>
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
<SelectContent className="dg-input">
<SelectItem value="v2c">v2c</SelectItem>
<SelectItem value="v3">v3</SelectItem>
</SelectContent>
@@ -348,14 +348,14 @@ export function SNMPMonitor() {
<Button
variant="outline"
onClick={() => setDialogOpen(false)}
className="border-[var(--dg-border)] text-[var(--dg-text-muted)] hover:bg-slate-800"
className="border-[var(--dg-border)] text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)]"
>
取消
</Button>
<Button
onClick={handleAdd}
disabled={submitting || !form.name.trim() || !form.ip.trim()}
className="bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20 border border-cyan-500/30"
className="bg-cyan-500/10 dg-text-cyan hover:bg-cyan-500/20 border border-cyan-500/30"
>
{submitting ? '添加中...' : '确认添加'}
</Button>
@@ -410,7 +410,7 @@ export function SNMPMonitor() {
variant="ghost"
size="icon-sm"
onClick={() => handleDelete(d.id)}
className="text-slate-500 hover:text-red-400 hover:bg-red-500/10"
className="dg-text-muted hover:dg-text-red hover:bg-red-500/10"
title="删除设备"
>
<Trash2 className="h-4 w-4" />
+15 -15
View File
@@ -49,11 +49,11 @@ import type { JsonData } from '@/types/common';
// 拨测协议类型(与后端 model 对齐:http, https, tcp, dns, ping)
const probeTypes = [
{ icon: Globe, name: 'HTTP', color: 'text-blue-400', desc: 'HTTP URL 状态码 & 响应时间' },
{ icon: Globe, name: 'HTTPS', color: 'text-emerald-400', desc: 'HTTPS SSL 证书 & 响应时间' },
{ icon: Server, name: 'TCP', color: 'text-orange-400', desc: 'TCP 端口连通性检测' },
{ icon: Globe, name: 'DNS', color: 'text-cyan-400', desc: '域名解析延迟 & 结果验证' },
{ icon: ArrowUpDown, name: 'PING', color: 'text-purple-400', desc: 'ICMP 连通性 & 延迟' },
{ icon: Globe, name: 'HTTP', color: 'dg-text-blue', desc: 'HTTP URL 状态码 & 响应时间' },
{ icon: Globe, name: 'HTTPS', color: 'dg-text-green', desc: 'HTTPS SSL 证书 & 响应时间' },
{ icon: Server, name: 'TCP', color: 'dg-text-orange', desc: 'TCP 端口连通性检测' },
{ icon: Globe, name: 'DNS', color: 'dg-text-cyan', desc: '域名解析延迟 & 结果验证' },
{ icon: ArrowUpDown, name: 'PING', color: 'dg-text-purple', desc: 'ICMP 连通性 & 延迟' },
] as const;
const PROBE_TYPE_OPTIONS = ['HTTP', 'HTTPS', 'TCP', 'DNS', 'PING'] as const;
@@ -104,22 +104,22 @@ const emptyForm: CreateTaskForm = {
function statusIcon(status?: string) {
switch (status) {
case 'up':
return <CheckCircle2 className="h-4 w-4 text-emerald-400" />;
return <CheckCircle2 className="h-4 w-4 dg-text-green" />;
case 'down':
return <XCircle className="h-4 w-4 text-red-400" />;
return <XCircle className="h-4 w-4 dg-text-red" />;
default:
return <Clock className="h-4 w-4 text-gray-400" />;
return <Clock className="h-4 w-4 dg-text-secondary" />;
}
}
function statusBadge(status?: string) {
switch (status) {
case 'up':
return <Badge className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">正常</Badge>;
return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30">正常</Badge>;
case 'down':
return <Badge className="bg-red-500/15 text-red-400 border-red-500/30">异常</Badge>;
return <Badge className="bg-red-500/15 dg-text-red border-red-500/30">异常</Badge>;
default:
return <Badge className="bg-gray-500/15 text-gray-400 border-gray-500/30">未知</Badge>;
return <Badge className="dg-badge-muted">未知</Badge>;
}
}
@@ -300,9 +300,9 @@ export function SiteMonitor() {
{[
{ label: '总任务数', value: stats?.total_tasks ?? '--' },
{ label: '已启用', value: stats?.enabled_tasks ?? '--' },
{ label: '正常', value: stats?.up_count ?? '--', color: 'text-emerald-400' },
{ label: '异常', value: stats?.down_count ?? '--', color: 'text-red-400' },
{ label: '可用率', value: availStr, color: 'text-blue-400' },
{ label: '正常', value: stats?.up_count ?? '--', color: 'dg-text-green' },
{ label: '异常', value: stats?.down_count ?? '--', color: 'dg-text-red' },
{ label: '可用率', value: availStr, color: 'dg-text-blue' },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-tertiary)] rounded-lg p-3 text-center">
<p className="text-xs text-[var(--dg-text-muted)]">{item.label}</p>
@@ -407,7 +407,7 @@ export function SiteMonitor() {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-400 hover:text-red-300 hover:bg-red-500/10"
className="h-8 w-8 dg-text-red hover:dg-text-red hover:bg-red-500/10"
onClick={() => handleDelete(task)}
>
<Trash2 className="h-4 w-4" />
@@ -33,12 +33,12 @@ export function DualLineChart({
<CardContent className="p-3">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5">
<Network className="h-4 w-4 text-blue-400" />
<Network className="h-4 w-4 dg-text-blue" />
<span className="text-xs text-[var(--dg-text-primary)] font-medium">{title}</span>
</div>
<div className="flex items-center gap-3 text-xs">
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-blue-400 mr-1 align-middle" />接收 <b className="text-blue-300">{currentRx}</b></span>
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-400 mr-1 align-middle" />发送 <b className="text-emerald-300">{currentTx}</b></span>
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-400 mr-1 align-middle" />发送 <b className="dg-text-green">{currentTx}</b></span>
</div>
</div>
<div className="h-56">
@@ -110,9 +110,9 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
<CardContent className="p-3">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
{p.last_status === 'up' ? <CheckCircle className="w-4 h-4 text-emerald-400" /> :
p.last_status === 'down' ? <XCircle className="w-4 h-4 text-red-400" /> :
<Clock className="w-4 h-4 text-slate-500" />}
{p.last_status === 'up' ? <CheckCircle className="w-4 h-4 dg-text-green" /> :
p.last_status === 'down' ? <XCircle className="w-4 h-4 dg-text-red" /> :
<Clock className="w-4 h-4 dg-text-muted" />}
<span className="text-xs font-medium text-[var(--dg-text-primary)]">{String(p.name)}</span>
<Badge variant="outline" className="text-[10px] h-5 px-1.5 capitalize">
{p.probe_type === 'http_get' ? 'HTTP' : p.probe_type === 'tcp_socket' ? 'TCP' : 'Exec'}
@@ -122,7 +122,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={() => openEditDialog(p)}>
<Edit className="w-3 h-3" />
</Button>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 text-red-400 hover:text-red-300" onClick={() => handleDeleteProbe(p)}>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 dg-text-red hover:dg-text-red" onClick={() => handleDeleteProbe(p)}>
<Trash2 className="w-3 h-3" />
</Button>
</div>
@@ -138,11 +138,11 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
<div className="truncate font-mono">{String(p.exec_command)}</div>
)}
<div className="flex items-center gap-3">
<span>状态: <span className={p.last_status === 'up' ? 'text-emerald-400' : p.last_status === 'down' ? 'text-red-400' : 'text-slate-400'}>
<span>状态: <span className={p.last_status === 'up' ? 'dg-text-green' : p.last_status === 'down' ? 'dg-text-red' : 'dg-text-secondary'}>
{p.last_status ? String(p.last_status).toUpperCase() : '未检测'}
</span></span>
{Number(p.last_latency_ms) > 0 && <span>延迟: {String(p.last_latency_ms)}ms</span>}
{Number(p.fail_count) > 0 && <span className="text-red-400">连续失败: {String(p.fail_count)}次</span>}
{Number(p.fail_count) > 0 && <span className="dg-text-red">连续失败: {String(p.fail_count)}次</span>}
{p.last_checked_at && <span className="text-[10px]">{new Date(String(p.last_checked_at)).toLocaleString('zh-CN')}</span>}
</div>
</div>
@@ -165,7 +165,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
</DialogHeader>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针名称 <span className="text-red-400">*</span></label>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针名称 <span className="dg-text-red">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.name} onChange={e => setProbeForm({ ...probeForm, name: e.target.value })} placeholder="如:cex-agent 健康检查" />
</div>
<div>
@@ -183,7 +183,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
{probeForm.probe_type === 'http_get' && (
<>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="text-red-400">*</span></label>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="dg-text-red">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_url} onChange={e => setProbeForm({ ...probeForm, http_url: e.target.value })} placeholder="http://localhost:8080/health" /></div>
<div className="grid grid-cols-2 gap-2">
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望状态码</label>
@@ -204,7 +204,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
)}
{probeForm.probe_type === 'exec_command' && (
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">执行命令 <span className="text-red-400">*</span></label>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">执行命令 <span className="dg-text-red">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs font-mono text-[var(--dg-text-primary)]" value={probeForm.exec_command} onChange={e => setProbeForm({ ...probeForm, exec_command: e.target.value })} placeholder="pgrep -x mysqld" /></div>
)}
@@ -38,13 +38,13 @@ export function PortPolicyDialog({
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">过滤模式</label>
<div className="flex gap-2">
<button
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 text-blue-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 dg-text-blue' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => onModeChange('whitelist')}
>
白名单 ─ 全量上报
</button>
<button
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 text-amber-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 dg-text-yellow' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => onModeChange('blacklist')}
>
黑名单 ─ 仅上报选中
@@ -60,7 +60,7 @@ export function PortPolicyDialog({
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
{portPolicyMode === 'whitelist' && <span className="text-blue-400 ml-1">— 白名单模式无需配置</span>}
{portPolicyMode === 'whitelist' && <span className="dg-text-blue ml-1">— 白名单模式无需配置</span>}
</label>
<textarea
className={`w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono resize-none ${
@@ -55,9 +55,9 @@ export function SystemInfoSection({
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">系统负载 (Load Average)</h3>
</div>
<div className="flex items-center gap-6 text-xs">
<div><span className="text-[var(--dg-text-muted)]">1min </span><span className="text-sm font-bold text-slate-200">{latest.load1 ?? '--'}</span></div>
<div><span className="text-[var(--dg-text-muted)]">5min </span><span className="text-sm font-bold text-slate-200">{latest.load5 ?? '--'}</span></div>
<div><span className="text-[var(--dg-text-muted)]">15min </span><span className="text-sm font-bold text-slate-200">{latest.load15 ?? '--'}</span></div>
<div><span className="text-[var(--dg-text-muted)]">1min </span><span className="text-sm font-bold text-[var(--dg-text-primary)]">{latest.load1 ?? '--'}</span></div>
<div><span className="text-[var(--dg-text-muted)]">5min </span><span className="text-sm font-bold text-[var(--dg-text-primary)]">{latest.load5 ?? '--'}</span></div>
<div><span className="text-[var(--dg-text-muted)]">15min </span><span className="text-sm font-bold text-[var(--dg-text-primary)]">{latest.load15 ?? '--'}</span></div>
</div>
</CardContent>
</Card>
@@ -71,11 +71,11 @@ export function SystemInfoSection({
{parts.map((p: JsonData, i: number) => (
<div key={i} className="flex items-center gap-2 text-xs">
<span className="text-[var(--dg-text-muted)] w-24 font-mono truncate">{p.mount || p.mountpoint || p.device || `disk${i}`}</span>
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div className="flex-1 h-1.5 bg-[var(--dg-bg-active)] rounded-full overflow-hidden">
<div className="h-full rounded-full bg-blue-500" style={{ width: `${Math.min(p.usage_percent || p.used_percent || 0, 100)}%` }} />
</div>
<span className="text-[var(--dg-text-primary)] w-28 text-right tabular-nums whitespace-nowrap">{(p.used_gb ?? 0).toFixed(1)}/{(p.total_gb ?? 0).toFixed(0)}GB</span>
<span className="text-blue-400 w-14 text-right tabular-nums whitespace-nowrap">{(p.usage_percent ?? p.used_percent ?? 0).toFixed(1)}%</span>
<span className="dg-text-blue w-14 text-right tabular-nums whitespace-nowrap">{(p.usage_percent ?? p.used_percent ?? 0).toFixed(1)}%</span>
</div>
))}
</div>