feat: 服务器监控面板(/server) - CPU/内存/硬盘/PM2进程/PG数据库/回补状态
This commit is contained in:
parent
871da720ab
commit
930c8d3a9c
122
backend/main.py
122
backend/main.py
@ -452,3 +452,125 @@ async def get_signal_trades(
|
|||||||
status, limit
|
status, limit
|
||||||
)
|
)
|
||||||
return {"count": len(rows), "data": rows}
|
return {"count": len(rows), "data": rows}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 服务器状态监控 ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
import shutil, subprocess, psutil
|
||||||
|
|
||||||
|
@app.get("/api/server/status")
|
||||||
|
async def get_server_status(user: dict = Depends(get_current_user)):
|
||||||
|
"""服务器全状态:CPU/内存/硬盘/负载/PM2进程/PG数据库/回补进度"""
|
||||||
|
# CPU
|
||||||
|
cpu_percent = psutil.cpu_percent(interval=0.5)
|
||||||
|
cpu_count = psutil.cpu_count()
|
||||||
|
|
||||||
|
# 内存
|
||||||
|
mem = psutil.virtual_memory()
|
||||||
|
swap = psutil.swap_memory()
|
||||||
|
|
||||||
|
# 硬盘
|
||||||
|
disk = shutil.disk_usage("/")
|
||||||
|
|
||||||
|
# 负载
|
||||||
|
load1, load5, load15 = os.getloadavg()
|
||||||
|
|
||||||
|
# Uptime
|
||||||
|
boot_time = psutil.boot_time()
|
||||||
|
uptime_s = time.time() - boot_time
|
||||||
|
|
||||||
|
# 网络IO
|
||||||
|
net = psutil.net_io_counters()
|
||||||
|
|
||||||
|
# PM2进程状态
|
||||||
|
pm2_procs = []
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["npx", "pm2", "jlist"],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
import json as _json
|
||||||
|
procs = _json.loads(result.stdout)
|
||||||
|
for p in procs:
|
||||||
|
pm2_procs.append({
|
||||||
|
"name": p.get("name", ""),
|
||||||
|
"status": p.get("pm2_env", {}).get("status", "unknown"),
|
||||||
|
"cpu": p.get("monit", {}).get("cpu", 0),
|
||||||
|
"memory_mb": round(p.get("monit", {}).get("memory", 0) / 1024 / 1024, 1),
|
||||||
|
"restarts": p.get("pm2_env", {}).get("restart_time", 0),
|
||||||
|
"uptime_ms": p.get("pm2_env", {}).get("pm_uptime", 0),
|
||||||
|
"pid": p.get("pid", 0),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pm2_procs = []
|
||||||
|
|
||||||
|
# PG数据库大小 + agg_trades条数
|
||||||
|
pg_info = {}
|
||||||
|
try:
|
||||||
|
row = await async_fetchrow(
|
||||||
|
"SELECT pg_database_size(current_database()) as db_size"
|
||||||
|
)
|
||||||
|
pg_info["db_size_mb"] = round(row["db_size"] / 1024 / 1024, 1) if row else 0
|
||||||
|
|
||||||
|
row2 = await async_fetchrow("SELECT COUNT(*) as cnt FROM agg_trades")
|
||||||
|
pg_info["agg_trades_count"] = row2["cnt"] if row2 else 0
|
||||||
|
|
||||||
|
row3 = await async_fetchrow("SELECT COUNT(*) as cnt FROM rate_snapshots")
|
||||||
|
pg_info["rate_snapshots_count"] = row3["cnt"] if row3 else 0
|
||||||
|
|
||||||
|
# 各symbol最新数据时间
|
||||||
|
meta_rows = await async_fetch("SELECT symbol, last_time_ms, earliest_time_ms FROM agg_trades_meta")
|
||||||
|
pg_info["symbols"] = {}
|
||||||
|
for m in meta_rows:
|
||||||
|
sym = m["symbol"].replace("USDT", "")
|
||||||
|
pg_info["symbols"][sym] = {
|
||||||
|
"latest_ms": m["last_time_ms"],
|
||||||
|
"earliest_ms": m["earliest_time_ms"],
|
||||||
|
"span_hours": round((m["last_time_ms"] - m["earliest_time_ms"]) / 3600000, 1),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 回补进程
|
||||||
|
backfill_running = False
|
||||||
|
try:
|
||||||
|
for proc in psutil.process_iter(["pid", "cmdline"]):
|
||||||
|
cmdline = " ".join(proc.info.get("cmdline") or [])
|
||||||
|
if "backfill_agg_trades" in cmdline:
|
||||||
|
backfill_running = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"cpu": {
|
||||||
|
"percent": cpu_percent,
|
||||||
|
"cores": cpu_count,
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"total_gb": round(mem.total / 1024**3, 1),
|
||||||
|
"used_gb": round(mem.used / 1024**3, 1),
|
||||||
|
"percent": mem.percent,
|
||||||
|
"swap_percent": swap.percent,
|
||||||
|
},
|
||||||
|
"disk": {
|
||||||
|
"total_gb": round(disk.total / 1024**3, 1),
|
||||||
|
"used_gb": round(disk.used / 1024**3, 1),
|
||||||
|
"free_gb": round(disk.free / 1024**3, 1),
|
||||||
|
"percent": round(disk.used / disk.total * 100, 1),
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"load1": round(load1, 2),
|
||||||
|
"load5": round(load5, 2),
|
||||||
|
"load15": round(load15, 2),
|
||||||
|
},
|
||||||
|
"uptime_hours": round(uptime_s / 3600, 1),
|
||||||
|
"network": {
|
||||||
|
"bytes_sent_gb": round(net.bytes_sent / 1024**3, 2),
|
||||||
|
"bytes_recv_gb": round(net.bytes_recv / 1024**3, 2),
|
||||||
|
},
|
||||||
|
"pm2": pm2_procs,
|
||||||
|
"postgres": pg_info,
|
||||||
|
"backfill_running": backfill_running,
|
||||||
|
}
|
||||||
|
|||||||
@ -2,3 +2,4 @@ fastapi
|
|||||||
uvicorn
|
uvicorn
|
||||||
httpx
|
httpx
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
psutil
|
||||||
|
|||||||
286
frontend/app/server/page.tsx
Normal file
286
frontend/app/server/page.tsx
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { authFetch } from "@/lib/auth";
|
||||||
|
import { useAuth } from "@/lib/auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface PM2Proc {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
cpu: number;
|
||||||
|
memory_mb: number;
|
||||||
|
restarts: number;
|
||||||
|
uptime_ms: number;
|
||||||
|
pid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerStatus {
|
||||||
|
timestamp: number;
|
||||||
|
cpu: { percent: number; cores: number };
|
||||||
|
memory: { total_gb: number; used_gb: number; percent: number; swap_percent: number };
|
||||||
|
disk: { total_gb: number; used_gb: number; free_gb: number; percent: number };
|
||||||
|
load: { load1: number; load5: number; load15: number };
|
||||||
|
uptime_hours: number;
|
||||||
|
network: { bytes_sent_gb: number; bytes_recv_gb: number };
|
||||||
|
pm2: PM2Proc[];
|
||||||
|
postgres: {
|
||||||
|
db_size_mb: number;
|
||||||
|
agg_trades_count: number;
|
||||||
|
rate_snapshots_count: number;
|
||||||
|
symbols: Record<string, { latest_ms: number; earliest_ms: number; span_hours: number }>;
|
||||||
|
};
|
||||||
|
backfill_running: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bjtStr(ms: number) {
|
||||||
|
const d = new Date(ms + 8 * 3600 * 1000);
|
||||||
|
return `${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,"0")}-${String(d.getUTCDate()).padStart(2,"0")} ${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}:${String(d.getUTCSeconds()).padStart(2,"0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uptimeStr(ms: number) {
|
||||||
|
if (!ms) return "-";
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - ms;
|
||||||
|
const h = Math.floor(diff / 3600000);
|
||||||
|
const m = Math.floor((diff % 3600000) / 60000);
|
||||||
|
if (h > 24) return `${Math.floor(h/24)}d ${h%24}h`;
|
||||||
|
if (h > 0) return `${h}h ${m}m`;
|
||||||
|
return `${m}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressBar({ percent, color = "cyan" }: { percent: number; color?: string }) {
|
||||||
|
const colorClass = percent > 90 ? "bg-red-500" : percent > 70 ? "bg-amber-500" : color === "cyan" ? "bg-cyan-500" : "bg-emerald-500";
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-slate-700 rounded-full h-2.5 overflow-hidden">
|
||||||
|
<div className={`h-full rounded-full transition-all duration-500 ${colorClass}`} style={{ width: `${Math.min(percent, 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ status }: { status: string }) {
|
||||||
|
const isOnline = status === "online";
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${isOnline ? "bg-emerald-500/20 text-emerald-400" : "bg-red-500/20 text-red-400"}`}>
|
||||||
|
<span className={`w-1.5 h-1.5 rounded-full ${isOnline ? "bg-emerald-400" : "bg-red-400"}`} />
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberFmt(n: number) {
|
||||||
|
return n.toLocaleString("en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServerPage() {
|
||||||
|
const { token } = useAuth();
|
||||||
|
const [data, setData] = useState<ServerStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [lastUpdate, setLastUpdate] = useState(0);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await authFetch("/api/server/status");
|
||||||
|
if (!res.ok) return;
|
||||||
|
const json = await res.json();
|
||||||
|
setData(json);
|
||||||
|
setLastUpdate(Date.now());
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) return;
|
||||||
|
fetchData();
|
||||||
|
const iv = setInterval(fetchData, 10000); // 10秒刷新
|
||||||
|
return () => clearInterval(iv);
|
||||||
|
}, [token, fetchData]);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-950 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-slate-400 mb-4">请先登录</p>
|
||||||
|
<Link href="/login" className="text-cyan-400 hover:text-cyan-300 underline">登录</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-950 text-slate-200">
|
||||||
|
{/* 顶栏 */}
|
||||||
|
<header className="border-b border-slate-800 bg-slate-900/80 backdrop-blur-sm sticky top-0 z-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 h-14 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link href="/" className="text-slate-400 hover:text-slate-200 text-sm">← 返回</Link>
|
||||||
|
<h1 className="text-base font-semibold text-white">🖥️ 服务器监控</h1>
|
||||||
|
<span className="text-xs text-slate-500">GCP asia-northeast1-b</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
|
{data?.backfill_running && (
|
||||||
|
<span className="flex items-center gap-1 text-cyan-400">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||||
|
回补运行中
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>每10秒刷新</span>
|
||||||
|
{lastUpdate > 0 && <span>· 更新于 {bjtStr(lastUpdate)}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-7xl mx-auto px-4 py-6 space-y-6">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-64 text-slate-400">加载中...</div>
|
||||||
|
) : !data ? (
|
||||||
|
<div className="flex items-center justify-center h-64 text-red-400">获取失败</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 系统概览 4卡片 */}
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{/* CPU */}
|
||||||
|
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-xs text-slate-400">CPU</span>
|
||||||
|
<span className="text-xs text-slate-500">{data.cpu.cores} 核</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">{data.cpu.percent}%</div>
|
||||||
|
<ProgressBar percent={data.cpu.percent} />
|
||||||
|
<div className="mt-2 text-xs text-slate-500">
|
||||||
|
负载: {data.load.load1} / {data.load.load5} / {data.load.load15}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内存 */}
|
||||||
|
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-xs text-slate-400">内存</span>
|
||||||
|
<span className="text-xs text-slate-500">{data.memory.used_gb}G / {data.memory.total_gb}G</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">{data.memory.percent}%</div>
|
||||||
|
<ProgressBar percent={data.memory.percent} />
|
||||||
|
{data.memory.swap_percent > 0 && (
|
||||||
|
<div className="mt-2 text-xs text-amber-400">Swap: {data.memory.swap_percent}%</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 硬盘 */}
|
||||||
|
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-xs text-slate-400">硬盘</span>
|
||||||
|
<span className="text-xs text-slate-500">{data.disk.free_gb}G 可用</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">{data.disk.percent}%</div>
|
||||||
|
<ProgressBar percent={data.disk.percent} color="emerald" />
|
||||||
|
<div className="mt-2 text-xs text-slate-500">
|
||||||
|
{data.disk.used_gb}G / {data.disk.total_gb}G
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 运行时间 & 网络 */}
|
||||||
|
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-xs text-slate-400">系统</span>
|
||||||
|
<span className="text-xs text-slate-500">Uptime</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">
|
||||||
|
{data.uptime_hours > 24 ? `${Math.floor(data.uptime_hours/24)}天` : `${data.uptime_hours}h`}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 space-y-0.5">
|
||||||
|
<div>↑ 发送: {data.network.bytes_sent_gb} GB</div>
|
||||||
|
<div>↓ 接收: {data.network.bytes_recv_gb} GB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PM2 进程列表 */}
|
||||||
|
<div className="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
||||||
|
<div className="px-4 py-3 border-b border-slate-800">
|
||||||
|
<h3 className="font-semibold text-sm text-white">📦 PM2 进程</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-slate-400 border-b border-slate-800">
|
||||||
|
<th className="text-left px-4 py-2 font-medium">名称</th>
|
||||||
|
<th className="text-center px-4 py-2 font-medium">状态</th>
|
||||||
|
<th className="text-right px-4 py-2 font-medium">CPU</th>
|
||||||
|
<th className="text-right px-4 py-2 font-medium">内存</th>
|
||||||
|
<th className="text-right px-4 py-2 font-medium">重启</th>
|
||||||
|
<th className="text-right px-4 py-2 font-medium">运行时间</th>
|
||||||
|
<th className="text-right px-4 py-2 font-medium">PID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.pm2.map((p, i) => (
|
||||||
|
<tr key={i} className="border-b border-slate-800/50 hover:bg-slate-800/30">
|
||||||
|
<td className="px-4 py-2.5 font-mono text-cyan-400">{p.name}</td>
|
||||||
|
<td className="px-4 py-2.5 text-center"><StatusBadge status={p.status} /></td>
|
||||||
|
<td className="px-4 py-2.5 text-right font-mono">{p.cpu}%</td>
|
||||||
|
<td className="px-4 py-2.5 text-right font-mono">{p.memory_mb} MB</td>
|
||||||
|
<td className="px-4 py-2.5 text-right font-mono">{p.restarts}</td>
|
||||||
|
<td className="px-4 py-2.5 text-right text-slate-400">{uptimeStr(p.uptime_ms)}</td>
|
||||||
|
<td className="px-4 py-2.5 text-right text-slate-500 font-mono">{p.pid || "-"}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数据库信息 */}
|
||||||
|
<div className="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
||||||
|
<div className="px-4 py-3 border-b border-slate-800">
|
||||||
|
<h3 className="font-semibold text-sm text-white">🗄️ PostgreSQL</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400 mb-1">数据库大小</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
{data.postgres.db_size_mb > 1024
|
||||||
|
? `${(data.postgres.db_size_mb / 1024).toFixed(1)} GB`
|
||||||
|
: `${data.postgres.db_size_mb} MB`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400 mb-1">aggTrades 总条数</div>
|
||||||
|
<div className="text-lg font-bold text-cyan-400">{numberFmt(data.postgres.agg_trades_count)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400 mb-1">费率快照</div>
|
||||||
|
<div className="text-lg font-bold text-white">{numberFmt(data.postgres.rate_snapshots_count)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400 mb-1">回补状态</div>
|
||||||
|
<div className={`text-lg font-bold ${data.backfill_running ? "text-cyan-400" : "text-slate-500"}`}>
|
||||||
|
{data.backfill_running ? "🔄 运行中" : "⏸ 停止"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{data.postgres.symbols && Object.keys(data.postgres.symbols).length > 0 && (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<div className="text-xs text-slate-400 mb-2">数据覆盖范围</div>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||||
|
{Object.entries(data.postgres.symbols).map(([sym, info]) => (
|
||||||
|
<div key={sym} className="flex items-center justify-between bg-slate-800/50 rounded-lg px-3 py-2">
|
||||||
|
<span className="font-mono text-sm text-white">{sym}</span>
|
||||||
|
<div className="text-xs text-slate-400 text-right">
|
||||||
|
<div>{bjtStr(info.earliest_ms)} → {bjtStr(info.latest_ms)}</div>
|
||||||
|
<div className="text-cyan-400">{info.span_hours > 24 ? `${(info.span_hours/24).toFixed(1)} 天` : `${info.span_hours.toFixed(1)} 小时`}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,13 +7,14 @@ import { useAuth } from "@/lib/auth";
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard, Info,
|
LayoutDashboard, Info,
|
||||||
Menu, X, Zap, LogIn, UserPlus,
|
Menu, X, Zap, LogIn, UserPlus,
|
||||||
ChevronLeft, ChevronRight, Activity, LogOut, Crosshair
|
ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: "/", label: "仪表盘", icon: LayoutDashboard },
|
{ href: "/", label: "仪表盘", icon: LayoutDashboard },
|
||||||
{ href: "/trades", label: "成交流", icon: Activity },
|
{ href: "/trades", label: "成交流", icon: Activity },
|
||||||
{ href: "/signals", label: "信号引擎", icon: Crosshair },
|
{ href: "/signals", label: "信号引擎", icon: Crosshair },
|
||||||
|
{ href: "/server", label: "服务器", icon: Monitor },
|
||||||
{ href: "/about", label: "说明", icon: Info },
|
{ href: "/about", label: "说明", icon: Info },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user