feat(portal): migrate V3 console to React 19 + Vite + TypeScript + Zustand + Tailwind v4
This commit is contained in:
1 parent
518f1e757e
commit
eae4596aa3
20 files changed
+4162
-3
No files matched your search
@@ -0,0 +1,180 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Search, Terminal } from 'lucide-react';
|
||||||
|
import { usePortalStore } from '../store/usePortalStore';
|
||||||
|
import type { RouteModeId, ThemeId } from '../types/telemetry';
|
||||||
|
|
||||||
|
const THEMES: Array<{ id: ThemeId; label: string; dot: string }> = [
|
||||||
|
{ id: 'zinc-dark', label: '深色极客', dot: '#18181b' },
|
||||||
|
{ id: 'zinc-light', label: '浅色纯净', dot: '#f8fafc' },
|
||||||
|
{ id: 'catppuccin', label: 'Catppuccin', dot: '#cba6f7' },
|
||||||
|
{ id: 'nord', label: 'Nord', dot: '#88c0d0' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MODES: Array<{ id: RouteModeId; label: string }> = [
|
||||||
|
{ id: 'auto', label: '智能路由' },
|
||||||
|
{ id: 'edge443', label: '443 免端口' },
|
||||||
|
{ id: 'home10443', label: '10443 家宽' },
|
||||||
|
{ id: 'lan', label: 'LAN 局域网' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TopRibbon: React.FC = () => {
|
||||||
|
const {
|
||||||
|
theme,
|
||||||
|
routeMode,
|
||||||
|
telemetry,
|
||||||
|
setTheme,
|
||||||
|
setRouteMode,
|
||||||
|
setShowAgyModal,
|
||||||
|
setShowCmdPalette,
|
||||||
|
} = usePortalStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className="border-b hl-border sticky top-0 z-30 backdrop-blur-md"
|
||||||
|
style={{ backgroundColor: 'color-mix(in srgb, var(--bg-page) 86%, transparent)' }}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
{/* Left: Brand + Live System Micro-Gauges Ribbon */}
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5 sm:gap-3">
|
||||||
|
<div className="flex items-center gap-2.5 pr-1">
|
||||||
|
<img
|
||||||
|
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png"
|
||||||
|
alt="Mac mini"
|
||||||
|
className="w-6 h-6 rounded"
|
||||||
|
/>
|
||||||
|
<span className="font-semibold text-sm tracking-tight">HomeLab</span>
|
||||||
|
<span
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: 'var(--success)' }}
|
||||||
|
title="全屋 4 节点在线"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden xl:flex items-center gap-2 text-xs font-mono-num">
|
||||||
|
{/* CPU Gauge */}
|
||||||
|
<div className="hl-pill px-2.5 py-1 flex items-center gap-2" title="Apple M6 12核处理器负载">
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>M6 CPU</span>
|
||||||
|
<div className="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full hl-bar-fill"
|
||||||
|
style={{ width: `${Math.max(5, telemetry.mini.cpu_pct)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="font-medium">{telemetry.mini.cpu_pct}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RAM Gauge */}
|
||||||
|
<div className="hl-pill px-2.5 py-1 flex items-center gap-2" title="Mac mini 24GB 统一内存">
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>RAM</span>
|
||||||
|
<div className="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full hl-bar-fill"
|
||||||
|
style={{ width: `${Math.max(5, telemetry.mini.mem_pct)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="font-medium">{telemetry.mini.mem_used_gb}G</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ZSpace 20TB Gauge */}
|
||||||
|
<div className="hl-pill px-2.5 py-1 flex items-center gap-2" title="极空间 Z4 20TB 存储阵列">
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>NAS 余量</span>
|
||||||
|
<div className="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full hl-bar-fill-green"
|
||||||
|
style={{ width: `${Math.max(5, telemetry.nas.pct)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="font-medium" style={{ color: 'var(--success)' }}>
|
||||||
|
{telemetry.nas.free_tb} TB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SG AI Line Gauge */}
|
||||||
|
<div className="hl-pill px-2.5 py-1 flex items-center gap-1.5" title="AGY 锁定新加坡 Azure 专线出口">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: 'var(--success)' }} />
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>SG 专线</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{telemetry.network.sg_delay_ms > 0 ? `${telemetry.network.sg_delay_ms}ms` : '直连'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Search (Cmd+K), Route Switcher, Theme Switcher, Terminal Button */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCmdPalette(true)}
|
||||||
|
className="hl-btn-outline px-2.5 py-1 text-xs flex items-center gap-3 cursor-pointer"
|
||||||
|
title="按 Cmd+K 唤起全局命令面板"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
<Search className="w-3.5 h-3.5" />
|
||||||
|
<span className="hidden sm:inline">搜索或执行指令...</span>
|
||||||
|
</span>
|
||||||
|
<kbd
|
||||||
|
className="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
|
>
|
||||||
|
⌘K
|
||||||
|
</kbd>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="hidden md:flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
|
||||||
|
{MODES.map((m) => (
|
||||||
|
<button
|
||||||
|
key={m.id}
|
||||||
|
onClick={() => setRouteMode(m.id)}
|
||||||
|
className="px-2 py-1 rounded-md font-medium transition-all cursor-pointer"
|
||||||
|
style={
|
||||||
|
routeMode === m.id
|
||||||
|
? {
|
||||||
|
backgroundColor: 'var(--bg-card)',
|
||||||
|
color: 'var(--text-main)',
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
|
||||||
|
}
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{m.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
|
||||||
|
{THEMES.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => setTheme(t.id)}
|
||||||
|
className="px-2 py-1 rounded-md font-medium transition-all flex items-center gap-1.5 cursor-pointer"
|
||||||
|
style={
|
||||||
|
theme === t.id
|
||||||
|
? {
|
||||||
|
backgroundColor: 'var(--bg-card)',
|
||||||
|
color: 'var(--text-main)',
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
|
||||||
|
}
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
title={t.label}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-2 h-2 rounded-full border border-black/10"
|
||||||
|
style={{ backgroundColor: t.dot }}
|
||||||
|
/>
|
||||||
|
<span className="hidden lg:inline">{t.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAgyModal(true)}
|
||||||
|
className="hl-btn-primary px-3 py-1.5 text-xs flex items-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Terminal className="w-3.5 h-3.5" />
|
||||||
|
<span>AGY 终端</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in new issue
Block a user