refactor: 枚举移入Models目录,命名空间更新为Rainbow.Entity.Models
大石头 authored at 2026-07-02 12:54:58
11.95 KiB
RainbowBridge
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>
  );
}