mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-09 11:16:37 +08:00
feat(ui): redesign with transparent liquid glass (#414)
* docs: add design spec for iOS 26 Liquid Glass UI redesign * docs: add implementation plan for iOS 26 Liquid Glass UI redesign * feat(ui): add liquid glass CSS variables and update root layouts * feat(ui): update Card and Modal components to iOS 26 Liquid Glass style * feat(ui): redesign dashboard page to Liquid Glass style * feat(ui): redesign node management page cards and charts * feat(ui): redesign monitor page to Liquid Glass style * feat(ui): redesign group and panel-sharing cards to Liquid Glass * feat(ui): redesign settings and user components to Liquid Glass * feat(ui): redesign profile and auth pages to Liquid Glass * fix(ui): revert to LineChart to fix compilation issues * refactor(ui): extract dashboard user header and cleanup admin layout * feat(ui): refine monitor page hero metrics and fix build * fix(ui): real data for monitor hero metrics and card padding adjustments * fix(ui): apply global card padding and fix index layouts * feat(ui): implement liquid-glass-react for all Card components * fix(ui): resolve liquid-glass-react rendering issues causing invisible text and broken layout * fix(ui): native CSS liquid-glass effect replacing NPM package * fix(ui): cleanup duplicate closing tags and complete liquid glass tables * fix(ui): apply perfect rounded corners to list views and table headers in non-compact mode * fix(ui): apply liquid glass styling to grouped rules view in forward page * fix(ui): re-implement liquid-glass natively to prevent text distortion and remove package dependency * fix(ui): remove mouse distortion and preserve proper liquid glass texture * fix(ui): remove svg filter causing text distortion and replace with pure css liquid glass * fix(ui): apply properly scoped border radius to table headers * fix(ui): remove hardcoded bg-default-50/60 from TableHeader * fix(ui): apply global liquid glass styling and transparent overlays to all modals and dialogs * fix(ui): remove absolute/relative class conflicts hiding Dialog modales and implement pure transparent layout * feat(ui): apply global liquid glass styling to cards, buttons, and diagnosis modals
This commit is contained in:
@@ -10,6 +10,7 @@ import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||
import { getMonitorNodes } from "@/api";
|
||||
import { MonitorView } from "@/pages/node/monitor-view";
|
||||
import { TunnelMonitorView } from "@/pages/node/tunnel-monitor-view";
|
||||
import { useNodeRealtime } from "@/pages/node/use-node-realtime";
|
||||
|
||||
type MonitorNode = {
|
||||
id: number;
|
||||
@@ -20,6 +21,16 @@ type MonitorNode = {
|
||||
|
||||
type MonitorTab = "nodes" | "tunnels";
|
||||
|
||||
const formatBytesPerSecond = (bytesPerSecond: number): string => {
|
||||
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
|
||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
export default function MonitorPage() {
|
||||
const [nodes, setNodes] = useState<MonitorNodeApiItem[]>([]);
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
@@ -27,6 +38,55 @@ export default function MonitorPage() {
|
||||
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
|
||||
const [activeTab, setActiveTab] = useState<MonitorTab>("nodes");
|
||||
|
||||
const [realtimeNodeStatus, setRealtimeNodeStatus] = useState<Record<number, "online" | "offline">>({});
|
||||
const [realtimeNodeMetrics, setRealtimeNodeMetrics] = useState<Record<number, any>>({});
|
||||
|
||||
const handleRealtimeMessage = useCallback((message: any) => {
|
||||
const nodeId = Number(message?.id ?? 0);
|
||||
if (!nodeId || Number.isNaN(nodeId)) return;
|
||||
|
||||
const type = String(message?.type ?? "");
|
||||
const payload = message?.data;
|
||||
|
||||
if (type === "status") {
|
||||
const status = Number(payload);
|
||||
setRealtimeNodeStatus((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: status === 1 ? "online" : "offline",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "metric") {
|
||||
let raw = payload;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
raw = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!raw || typeof raw !== "object") return;
|
||||
const metric = raw as any;
|
||||
|
||||
setRealtimeNodeMetrics((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: {
|
||||
cpuUsage: Number(metric.cpuUsage ?? metric.cpu_usage ?? 0),
|
||||
netInSpeed: Number(metric.netInSpeed ?? metric.net_in_speed ?? 0),
|
||||
netOutSpeed: Number(metric.netOutSpeed ?? metric.net_out_speed ?? 0),
|
||||
tcpConns: Number(metric.tcpConns ?? metric.tcp_conns ?? 0),
|
||||
udpConns: Number(metric.udpConns ?? metric.udp_conns ?? 0),
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useNodeRealtime({
|
||||
onMessage: handleRealtimeMessage,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const loadNodes = useCallback(async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) setNodesLoading(true);
|
||||
@@ -73,22 +133,101 @@ export default function MonitorPage() {
|
||||
.map((n) => ({
|
||||
id: Number(n.id),
|
||||
name: String(n.name ?? ""),
|
||||
connectionStatus: n.status === 1 ? "online" : "offline",
|
||||
connectionStatus: realtimeNodeStatus[Number(n.id)] || (n.status === 1 ? "online" : "offline"),
|
||||
version: n.version,
|
||||
}));
|
||||
|
||||
return new Map<number, MonitorNode>(list.map((n) => [n.id, n]));
|
||||
}, [nodes]);
|
||||
}, [nodes, realtimeNodeStatus]);
|
||||
|
||||
const aggregateMetrics = useMemo(() => {
|
||||
let totalCpu = 0;
|
||||
let totalConns = 0;
|
||||
let totalNetIn = 0;
|
||||
let totalNetOut = 0;
|
||||
let onlineCount = 0;
|
||||
|
||||
nodeMap.forEach((node) => {
|
||||
if (node.connectionStatus === "online") {
|
||||
onlineCount++;
|
||||
const metric = realtimeNodeMetrics[node.id];
|
||||
if (metric) {
|
||||
totalCpu += metric.cpuUsage;
|
||||
totalConns += metric.tcpConns + metric.udpConns;
|
||||
totalNetIn += metric.netInSpeed;
|
||||
totalNetOut += metric.netOutSpeed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const avgCpu = onlineCount > 0 ? totalCpu / onlineCount : 0;
|
||||
return {
|
||||
avgCpu,
|
||||
totalConns,
|
||||
totalBandwidth: totalNetIn + totalNetOut,
|
||||
};
|
||||
}, [nodeMap, realtimeNodeMetrics]);
|
||||
|
||||
return (
|
||||
<AnimatedPage className="px-3 lg:px-6 py-8">
|
||||
{/* 顶部英雄数据指标 (Hero Metrics) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||
<div className="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">System Load</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.avgCpu.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-primary/40 h-2 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-3 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-1.5 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-4 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-2.5 rounded-t-sm" />
|
||||
<div className="w-full bg-primary h-5 rounded-t-sm shadow-[0_0_10px_rgba(0,122,255,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||
<div className="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">Active Connections</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.totalConns}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-success/40 h-3.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-4.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-2.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-6 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-5.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success h-7 rounded-t-sm shadow-[0_0_10px_rgba(52,199,89,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||
<div className="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">Bandwidth</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{formatBytesPerSecond(aggregateMetrics.totalBandwidth)}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-secondary/40 h-2 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-3 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-5 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-8 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-7 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary h-10 rounded-t-sm shadow-[0_0_10px_rgba(175,82,222,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-xl font-semibold truncate">监控</h2>
|
||||
<div className="text-xs text-default-500 truncate">
|
||||
实时节点状态 + 隧道质量检测 + 历史指标图表 + 服务监控(TCP/ICMP)
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold truncate">监控面板</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -114,11 +253,11 @@ export default function MonitorPage() {
|
||||
</div>
|
||||
|
||||
{/* Tab Switcher */}
|
||||
<div className="flex items-center gap-1 p-1 rounded-xl bg-default-100 dark:bg-default-50/10 w-fit">
|
||||
<div className="flex items-center gap-1 p-1 rounded-2xl bg-white/40 dark:bg-black/40 backdrop-blur-lg border border-white/50 dark:border-white/10 w-fit shadow-sm">
|
||||
<button
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||
activeTab === "nodes"
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
|
||||
: "text-default-500 hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setActiveTab("nodes")}
|
||||
@@ -127,9 +266,9 @@ export default function MonitorPage() {
|
||||
节点
|
||||
</button>
|
||||
<button
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||
activeTab === "tunnels"
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
|
||||
: "text-default-500 hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setActiveTab("tunnels")}
|
||||
|
||||
Reference in New Issue
Block a user