import { useEffect, useRef, useState } from 'react';
import * as echarts from 'echarts';
import {
getSystemStats, getTraffic, getWanInterfaces, getOnlineDevices, getDnsBlockStats,
saveFirewallRules, type SystemStats, type TrafficItem, type WanInterface, type Device, type DnsBlockCategory,
} from '../lib/api';
import { Icon } from '@/components/common/Icon';
import { showToast } from '@/stores/toastStore';
// 格式化流量速率
function fmtSpeed(bytesPerSec: number): string {
if (bytesPerSec >= 1e9) return `${(bytesPerSec / 1e9).toFixed(1)} GB/s`;
if (bytesPerSec >= 1e6) return `${(bytesPerSec / 1e6).toFixed(1)} MB/s`;
if (bytesPerSec >= 1e3) return `${(bytesPerSec / 1e3).toFixed(1)} KB/s`;
return `${bytesPerSec} B/s`;
}
// 格式化字节
function fmtBytes(bytes: number): string {
if (bytes >= 1e12) return `${(bytes / 1e12).toFixed(2)} TB`;
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(2)} MB`;
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(2)} KB`;
return `${bytes} B`;
}
export function Dashboard() {
const [stats, setStats] = useState<SystemStats | null>(null);
const [wanList, setWanList] = useState<WanInterface[]>([]);
const [onlineDevices, setOnlineDevices] = useState<Device[]>([]);
const [blockStats, setBlockStats] = useState<DnsBlockCategory[]>([]);
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
const trafficHistory = useRef<Map<string, Array<[number, number]>>>(new Map()); // name -> [ts, txSpeed]
useEffect(() => {
// 初始化 ECharts
if (chartRef.current) {
chartInstance.current = echarts.init(chartRef.current);
chartInstance.current.setOption({
tooltip: { trigger: 'axis' },
legend: { data: [] as string[], bottom: 0 },
grid: { left: 50, right: 30, top: 20, bottom: 40 },
xAxis: { type: 'time', axisLabel: { fontSize: 10 } },
yAxis: {
type: 'value',
axisLabel: { formatter: (v: number) => fmtSpeed(v) },
splitLine: { lineStyle: { color: '#f0f0f0' } },
},
series: [] as echarts.SeriesOption[],
});
}
const fetchData = async () => {
try {
const [s, wan, devices, blocks] = await Promise.all([
getSystemStats(),
getWanInterfaces(),
getOnlineDevices(),
getDnsBlockStats(),
]);
setStats(s);
setWanList(wan);
setOnlineDevices(devices);
setBlockStats(blocks);
} catch { /* 后端未就绪时静默 */ }
};
// 首次加载
fetchData();
// 流量图表: 获取各接口流量
const fetchTraffic = async () => {
try {
const data = await getTraffic();
if (data.length > 0) updateChart(data);
} catch { /* 静默 */ }
};
fetchTraffic();
const t1 = setInterval(fetchData, 5000);
const t2 = setInterval(fetchTraffic, 5000);
return () => {
clearInterval(t1);
clearInterval(t2);
chartInstance.current?.dispose();
};
}, []);
function updateChart(data: TrafficItem[]) {
const chart = chartInstance.current;
if (!chart) return;
const now = Date.now();
const history = trafficHistory.current;
const keepTime = now - 60000; // 保留最近 60 秒
for (const item of data) {
const key = `tx_${item.name}`;
if (!history.has(key)) history.set(key, []);
const arr = history.get(key)!;
arr.push([now, item.txSpeed]);
// 清理旧数据
while (arr.length > 0 && arr[0][0] < keepTime) arr.shift();
}
const series: echarts.SeriesOption[] = [];
for (const item of data) {
const key = `tx_${item.name}`;
series.push({
name: `${item.name} ↑`,
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: { opacity: 0.05 },
data: history.get(key) ?? [],
});
}
const allNames = series.map(s => s.name as string);
chart.setOption({
legend: { data: allNames },
series,
});
}
const totalBlockHits = blockStats.reduce((s, c) => s + c.count, 0);
const onlineCount = onlineDevices.length;
if (!stats) return <div className="text-center py-20 text-gray-400">加载中...</div>;
return (
<div>
<h1 className="text-2xl font-bold mb-6">📊 系统仪表盘</h1>
{/* 资源面板 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<StatCard label="CPU" value={`${stats.cpuUsage.toFixed(1)}%`} color="#5470c6" />
<StatCard label="内存" value={`${stats.memoryUsage.toFixed(1)}%`} color="#91cc75" />
<StatCard label="磁盘" value={`${stats.diskUsage.toFixed(1)}%`} color="#fac858" />
<StatCard label="连接数" value={`${stats.connectionCount}`} color="#ee6666" />
</div>
{/* 快捷操作 */}
<div className="glass-panel p-4 mb-6">
<h2 className="text-lg font-semibold mb-3">⚡ 快捷操作</h2>
<div className="flex flex-wrap gap-3">
<QuickAction icon="block" label="一键断网" variant="danger"
onClick={async () => {
try {
await saveFirewallRules();
showToast('success', '防火墙规则已保存,断网策略已生效');
} catch { showToast('error', '操作失败,请检查系统状态'); }
}}
/>
<QuickAction icon="save" label="保存防火墙" variant="secondary"
onClick={async () => {
try {
await saveFirewallRules();
showToast('success', '防火墙规则已持久化');
} catch { showToast('error', '保存失败'); }
}}
/>
<QuickAction icon="sync" label="刷新数据" variant="ghost"
onClick={() => window.location.reload()}
/>
<QuickAction icon="admin_panel_settings" label="后台管理" variant="ghost"
onClick={() => window.open('/admin', '_blank')}
/>
<QuickAction icon="settings" label="系统设置" variant="ghost"
onClick={() => window.location.href = '/settings'}
/>
</div>
</div>
{/* 实时流量图 */}
<div className="glass-panel p-4 mb-6">
<h2 className="text-lg font-semibold mb-3">📈 实时流量</h2>
<div ref={chartRef} style={{ height: 280 }} />
<div className="flex gap-4 mt-2 text-sm text-gray-500">
<span>↓ 下载 {fmtSpeed(stats.rxSpeed)}/s</span>
<span>↑ 上传 {fmtSpeed(stats.txSpeed)}/s</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* WAN 口状态 */}
<div className="glass-panel p-4">
<h2 className="text-lg font-semibold mb-3">🌐 WAN 口状态</h2>
{wanList.length === 0 ? (
<p className="text-sm text-gray-400">暂无 WAN 口配置</p>
) : (
<div className="space-y-3">
{wanList.map(w => (
<div key={w.id} className="flex items-center justify-between p-2 rounded bg-gray-50">
<div>
<span className="font-medium">{w.name}</span>
<span className="ml-2 text-xs text-gray-400">{wanKindLabel(w.kind)}</span>
</div>
<div className="text-right text-sm">
<div>
<span className={`inline-block w-2 h-2 rounded-full mr-1 ${w.enable ? 'bg-green-500' : 'bg-gray-300'}`} />
{w.enable ? '已启用' : '已停用'}
</div>
{w.ip && <div className="text-xs text-gray-400">{w.ip}</div>}
<div className="text-xs">
<span className="text-blue-500">↓{fmtSpeed(w.rxSpeed)}/s</span>
{' '}
<span className="text-orange-500">↑{fmtSpeed(w.txSpeed)}/s</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 在线设备概览 */}
<div className="glass-panel p-4">
<h2 className="text-lg font-semibold mb-3">🖥️ 在线设备 ({onlineCount})</h2>
{onlineCount === 0 ? (
<p className="text-sm text-gray-400">暂无在线设备</p>
) : (
<div className="space-y-2 max-h-64 overflow-y-auto">
{onlineDevices.slice(0, 8).map(d => (
<div key={d.id} className="flex items-center justify-between text-sm p-1.5 rounded hover:bg-gray-50">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500" />
<span className="font-mono text-xs">{d.mac}</span>
</div>
<div className="text-gray-500 text-xs">
{d.name || d.hostName || d.ip || '-'}
</div>
</div>
))}
{onlineCount > 8 && (
<div className="text-center text-xs text-gray-400">
还有 {onlineCount - 8} 台在线设备...
</div>
)}
</div>
)}
</div>
</div>
{/* 拦截统计 */}
<div className="glass-panel p-4 mb-6">
<h2 className="text-lg font-semibold mb-3">🛡️ DNS 拦截统计(累计 {totalBlockHits} 次)</h2>
{blockStats.length === 0 ? (
<p className="text-sm text-gray-400">暂无拦截数据</p>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{blockStats.map(b => (
<div key={b.category} className="text-center p-3 rounded bg-gray-50">
<div className="text-xl font-bold text-red-500">{b.count}</div>
<div className="text-xs text-gray-500">{categoryLabel(b.category)}</div>
<div className="text-xs text-gray-400">{b.count} 条规则</div>
</div>
))}
</div>
)}
</div>
{/* 运行时间 */}
<div className="glass-panel p-4 text-sm text-gray-500">
运行时间:{Math.floor(stats.uptimeSeconds / 3600)} 时 {Math.floor((stats.uptimeSeconds % 3600) / 60)} 分{' '}
{stats.rxSpeed > 0 && `| 总下载 ${fmtBytes(stats.rxSpeed * stats.uptimeSeconds)}`}
</div>
</div>
);
}
function categoryLabel(cat: string): string {
const map: Record<string, string> = {
Ad: '广告', Tracker: '追踪器', Malware: '恶意软件', Adult: '成人内容', Custom: '自定义',
};
return map[cat] || cat;
}
function wanKindLabel(kind: number): string {
const map: Record<number, string> = { 0: 'DHCP', 1: 'PPPoE', 2: '静态IP' };
return map[kind] || `类型${kind}`;
}
function StatCard({ label, value, color }: { label: string; value: string; color: string }) {
return (
<div className="glass-panel p-4 text-center">
<div className="text-2xl font-bold" style={{ color }}>{value}</div>
<div className="text-sm text-gray-500 mt-1">{label}</div>
</div>
);
}
function QuickAction({ icon, label, variant, onClick }: {
icon: string; label: string; variant: 'primary' | 'secondary' | 'ghost' | 'danger'; onClick: () => void;
}) {
const base = 'inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md active:scale-95';
const variantClass = {
primary: 'bg-[image:var(--gradient-brand)] text-white',
secondary: 'bg-[var(--color-surface-2)] text-[var(--color-text-primary)] border border-[var(--color-border-subtle)]',
ghost: 'bg-transparent text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)]',
danger: 'bg-red-50 text-red-600 border border-red-200 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800',
};
return (
<button className={`${base} ${variantClass[variant]}`} onClick={onClick}>
<Icon name={icon} size="lg" />
{label}
</button>
);
}
|