feat(portal): implement two-tier AppShell header (fix top-left wrapping) and interactive Storybook workbench

Browse Source
This commit is contained in:
Yin Kou committed 2026-11-07 00:17:06 +08:00
1 parent 4ca86da55f
commit 6b589fa62a
11 files changed
+1025 -268

No files matched your search

+199 -138
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Moon, Search, Sun, Terminal } from 'lucide-react'; import { BookOpen, LayoutGrid, Moon, Search, Sun, Terminal } from 'lucide-react';
import { usePortalStore } from '../store/usePortalStore'; import { usePortalStore } from '../store/usePortalStore';
import type { RouteModeId } from '../types/telemetry'; import type { RouteModeId } from '../types/telemetry';
@@ -15,171 +15,232 @@ export const TopRibbon: React.FC = () => {
theme, theme,
resolvedTheme, resolvedTheme,
routeMode, routeMode,
viewMode,
telemetry, telemetry,
setTheme, setTheme,
toggleTheme, toggleTheme,
setRouteMode, setRouteMode,
setViewMode,
setShowAgyModal, setShowAgyModal,
setShowCmdPalette, setShowCmdPalette,
} = usePortalStore(); } = usePortalStore();
return ( 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)' }} Tier 1: Standard AppShell Global Top Navbar (h-14, NEVER wraps!)
> Left: Brand + View Tabs (中控台 / Storybook)
<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"> Far Right (Red Box): Search (Cmd+K) + AGY Terminal + Theme Switcher
{/* Left: Brand + Live System Micro-Gauges Ribbon */} ===================================================================== */}
<div className="flex flex-wrap items-center gap-2.5 sm:gap-3"> <header
<div className="flex items-center gap-2.5 pr-1"> className="border-b hl-border sticky top-0 z-30 backdrop-blur-md"
<img style={{ backgroundColor: 'color-mix(in srgb, var(--bg-card) 88%, transparent)' }}
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png" >
alt="Mac mini" <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between gap-4">
className="w-6 h-6 rounded" {/* Left Group: Brand Logo + Navigation Tabs */}
/> <div className="flex items-center gap-4 shrink-0">
<span className="font-semibold text-sm tracking-tight">HomeLab</span> <div
<span onClick={() => setViewMode('dashboard')}
className="w-2 h-2 rounded-full" className="flex items-center gap-2.5 cursor-pointer"
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, AGY Terminal, Top-Right Theme Switcher */}
<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 <img
</kbd> src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png"
</button> 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 md:flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs"> <div className="h-4 w-[1px] hl-muted" />
{MODES.map((m) => (
{/* View Switcher Tabs: Dashboard vs Storybook */}
<nav className="flex items-center gap-1 text-xs">
<button <button
key={m.id} onClick={() => setViewMode('dashboard')}
onClick={() => setRouteMode(m.id)} className="px-2.5 py-1.5 rounded-md font-medium flex items-center gap-1.5 transition-all cursor-pointer"
className="px-2 py-1 rounded-md font-medium transition-all cursor-pointer"
style={ style={
routeMode === m.id viewMode === 'dashboard'
? { ? {
backgroundColor: 'var(--bg-card)', backgroundColor: 'var(--bg-muted)',
color: 'var(--text-main)', color: 'var(--text-main)',
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
} }
: { color: 'var(--text-muted)' } : { color: 'var(--text-muted)' }
} }
> >
{m.label} <LayoutGrid className="w-3.5 h-3.5" />
<span>中控台</span>
</button> </button>
))}
<button
onClick={() => setViewMode('storybook')}
className="px-2.5 py-1.5 rounded-md font-medium flex items-center gap-1.5 transition-all cursor-pointer"
style={
viewMode === 'storybook'
? {
backgroundColor: 'var(--bg-muted)',
color: 'var(--text-main)',
}
: { color: 'var(--text-muted)' }
}
>
<BookOpen className="w-3.5 h-3.5" />
<span>Storybook 设计规范</span>
</button>
</nav>
</div> </div>
<button {/* Far Right Group: Pinned to Right Edge (Search + AGY + Theme Switcher) */}
onClick={() => setShowAgyModal(true)} <div className="flex items-center gap-2.5 shrink-0 ml-auto">
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>
{/* Top-Right Mainstream Theme Switcher (Auto System + Light/Dark 2 Mainstream Themes) */}
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
<button <button
onClick={toggleTheme} onClick={() => setShowCmdPalette(true)}
className="px-2.5 py-1 rounded-md font-medium transition-all flex items-center gap-1.5 cursor-pointer" className="hl-btn-outline px-3 py-1.5 text-xs flex items-center gap-3 cursor-pointer"
style={{ title="按 Cmd+K 唤起全局命令面板"
backgroundColor: 'var(--bg-card)',
color: 'var(--text-main)',
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
}}
title="点击切换深色 / 浅色主题"
> >
{resolvedTheme === 'dark' ? ( <span className="flex items-center gap-1.5" style={{ color: 'var(--text-muted)' }}>
<> <Search className="w-3.5 h-3.5" />
<Moon className="w-3.5 h-3.5" /> <span className="hidden sm:inline">搜索服务或指令...</span>
<span className="hidden sm:inline">深色</span> </span>
</> <kbd
) : ( className="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted"
<>
<Sun className="w-3.5 h-3.5" />
<span className="hidden sm:inline">浅色</span>
</>
)}
</button>
{theme !== 'system' && (
<button
onClick={() => setTheme('system')}
className="px-2 py-1 rounded-md text-[11px] transition-all cursor-pointer"
style={{ color: 'var(--text-muted)' }} style={{ color: 'var(--text-muted)' }}
title="恢复跟随设备系统深浅色自动切换"
> >
自动 ⌘K
</kbd>
</button>
<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>
{/* Top-Right Theme Toggle (Pinned at Far Right Corner!) */}
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
<button
onClick={toggleTheme}
className="px-2.5 py-1 rounded-md font-medium transition-all flex items-center gap-1.5 cursor-pointer"
style={{
backgroundColor: 'var(--bg-card)',
color: 'var(--text-main)',
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
}}
title="点击切换深色 / 浅色主题(默认跟随系统)"
>
{resolvedTheme === 'dark' ? (
<>
<Moon className="w-3.5 h-3.5" />
<span>深色</span>
</>
) : (
<>
<Sun className="w-3.5 h-3.5" />
<span>浅色</span>
</>
)}
</button> </button>
)} {theme !== 'system' && (
<button
onClick={() => setTheme('system')}
className="px-2 py-1 rounded-md text-[11px] transition-all cursor-pointer"
style={{ color: 'var(--text-muted)' }}
title="恢复跟随设备系统深浅色自动切换"
>
自动
</button>
)}
</div>
</div> </div>
</div> </div>
</div> </header>
</header>
{/* =====================================================================
Tier 2: Subheader Telemetry & Route Mode Bar (Aligned with Grid)
Left: 4 Hardware & Network Gauges
Right: Route Mode Segmented Switcher (智能路由 / 443 / 10443 / LAN)
===================================================================== */}
{viewMode === 'dashboard' && (
<div className="border-b hl-border" style={{ backgroundColor: 'var(--bg-card)' }}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex flex-wrap items-center justify-between gap-3">
{/* Left: System Telemetry Strip */}
<div className="flex flex-wrap items-center gap-2 text-xs font-mono-num">
<div className="hl-pill px-2.5 py-1 flex items-center gap-2">
<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>
<div className="hl-pill px-2.5 py-1 flex items-center gap-2">
<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>
<div className="hl-pill px-2.5 py-1 flex items-center gap-2">
<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>
<div className="hl-pill px-2.5 py-1 flex items-center gap-1.5">
<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>
{/* Right: Route Mode Switcher (Pinned to Right Edge of Subheader) */}
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs ml-auto">
{MODES.map((m) => (
<button
key={m.id}
onClick={() => setRouteMode(m.id)}
className="px-2.5 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>
</div>
)}
</>
); );
}; };