feat(portal): implement two-tier AppShell header (fix top-left wrapping) and interactive Storybook workbench
This commit is contained in:
1 parent
4ca86da55f
commit
6b589fa62a
11 files changed
+1025
-268
No files matched your search
@@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user