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
@@ -0,0 +1,388 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { BookOpen, Check, Layers, Layout, Palette, Sparkles, Terminal } from 'lucide-react';
|
||||||
|
import { LAYOUT_PRESETS, THEME_PALETTES, type LayoutPresetId } from '../design-system/tokens';
|
||||||
|
import { usePortalStore } from '../store/usePortalStore';
|
||||||
|
import { ServiceCard } from './ServiceCard';
|
||||||
|
import { SshShelf } from './SshShelf';
|
||||||
|
|
||||||
|
type StoryId = 'layout-architecture' | 'theme-tokens' | 'service-card' | 'ssh-shelf';
|
||||||
|
|
||||||
|
export const StorybookWorkbench: React.FC = () => {
|
||||||
|
const [activeStory, setActiveStory] = useState<StoryId>('layout-architecture');
|
||||||
|
const {
|
||||||
|
theme,
|
||||||
|
resolvedTheme,
|
||||||
|
layoutPreset,
|
||||||
|
setTheme,
|
||||||
|
setLayoutPreset,
|
||||||
|
setViewMode,
|
||||||
|
telemetry,
|
||||||
|
} = usePortalStore();
|
||||||
|
|
||||||
|
// Interactive Playground Props for ServiceCard story
|
||||||
|
const [demoOnline, setDemoOnline] = useState(true);
|
||||||
|
const [demoAction, setDemoAction] = useState<'drawer' | 'backup' | undefined>('drawer');
|
||||||
|
|
||||||
|
const stories: Array<{ id: StoryId; group: string; title: string; icon: React.ReactNode }> = [
|
||||||
|
{
|
||||||
|
id: 'layout-architecture',
|
||||||
|
group: 'FOUNDATIONS / 架构与布局',
|
||||||
|
title: '01. AppShell 双层顶栏与网格布局',
|
||||||
|
icon: <Layout className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-tokens',
|
||||||
|
group: 'FOUNDATIONS / 架构与布局',
|
||||||
|
title: '02. 双主题色板与自动切换规范',
|
||||||
|
icon: <Palette className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'service-card',
|
||||||
|
group: 'COMPONENTS / 核心组件',
|
||||||
|
title: '03. ServiceCard 内联遥测卡片',
|
||||||
|
icon: <Layers className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ssh-shelf',
|
||||||
|
group: 'COMPONENTS / 核心组件',
|
||||||
|
title: '04. SshShelf 快捷指令栏',
|
||||||
|
icon: <Terminal className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 items-start">
|
||||||
|
{/* Left Sidebar: Storybook Navigation Tree */}
|
||||||
|
<aside className="hl-card p-4 space-y-4 lg:sticky lg:top-20">
|
||||||
|
<div className="flex items-center justify-between border-b hl-border pb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BookOpen className="w-4 h-4" style={{ color: 'var(--accent)' }} />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider">Storybook 组件库</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-mono-num px-1.5 py-0.5 rounded hl-muted">v3.1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{stories.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
onClick={() => setActiveStory(s.id)}
|
||||||
|
className="w-full px-3 py-2 rounded-lg text-left text-xs font-medium flex items-center gap-2.5 transition-all cursor-pointer"
|
||||||
|
style={
|
||||||
|
activeStory === s.id
|
||||||
|
? {
|
||||||
|
backgroundColor: 'var(--accent-soft)',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
}
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{s.icon}
|
||||||
|
<span className="truncate">{s.title}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 border-t hl-border space-y-2 text-xs">
|
||||||
|
<div className="text-[11px] font-semibold" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
全局外观状态预览
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-1 p-1 rounded-lg hl-muted border hl-border">
|
||||||
|
{(['system', 'light', 'dark'] as const).map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
onClick={() => setTheme(t)}
|
||||||
|
className="py-1 rounded text-[11px] font-medium cursor-pointer"
|
||||||
|
style={
|
||||||
|
theme === t
|
||||||
|
? { backgroundColor: 'var(--bg-card)', color: 'var(--text-main)' }
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t === 'system' ? '自动' : t === 'light' ? '浅色' : '深色'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('dashboard')}
|
||||||
|
className="w-full hl-btn-primary py-2 text-xs text-center mt-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
返回中控台主页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Right Canvas: Interactive Story Preview & Controls */}
|
||||||
|
<div className="lg:col-span-3 space-y-6">
|
||||||
|
{/* STORY 1: Layout Architecture */}
|
||||||
|
{activeStory === 'layout-architecture' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="hl-card p-6 space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b hl-border pb-4">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Foundations / AppShell Layout
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">
|
||||||
|
标准双层顶栏架构 (Two-Tier AppShell) & 响应式网格切换
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs leading-relaxed" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
参考 <strong>Vercel Geist / shadcn/ui Dashboard-01 / GitHub AppShell</strong>{' '}
|
||||||
|
标准规范,将原本容易挤压折行的单层顶栏拆分为职责清晰的双层结构:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Visual Blueprint Diagram of Two-Tier Header */}
|
||||||
|
<div className="p-4 rounded-xl hl-muted border hl-border space-y-2.5 font-mono-num text-xs">
|
||||||
|
<div className="p-3 rounded-lg hl-card flex items-center justify-between">
|
||||||
|
<span>[左] 🍎 HomeLab · 导航切换 (中控台 / Storybook)</span>
|
||||||
|
<span style={{ color: 'var(--accent)' }}>
|
||||||
|
[最右固定] 🔍 搜索 ⌘K · >_ AGY 终端 · ☀️/🌙 主题切换
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg hl-card flex items-center justify-between">
|
||||||
|
<span style={{ color: 'var(--success)' }}>
|
||||||
|
[左] 实时硬件遥测条 (M6 CPU · RAM · Z4 20TB · SG 专线)
|
||||||
|
</span>
|
||||||
|
<span>[右对齐] 路由模式切换器 (智能路由 / 443 / 10443 / LAN)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Grid Preset Selector */}
|
||||||
|
<div className="pt-2 space-y-3">
|
||||||
|
<div className="text-xs font-semibold">
|
||||||
|
选择主页卡片网格布局预设(点击立即应用到主页):
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3.5">
|
||||||
|
{(Object.keys(LAYOUT_PRESETS) as LayoutPresetId[]).map((key) => {
|
||||||
|
const preset = LAYOUT_PRESETS[key];
|
||||||
|
const active = layoutPreset === key;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => setLayoutPreset(key)}
|
||||||
|
className="hl-card hl-card-interactive p-4 cursor-pointer space-y-2"
|
||||||
|
style={active ? { borderColor: 'var(--accent)' } : {}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-bold">{preset.name}</span>
|
||||||
|
{active && (
|
||||||
|
<span
|
||||||
|
className="w-4 h-4 rounded-full flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--accent)',
|
||||||
|
color: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check className="w-2.5 h-2.5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px]" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
{preset.description}
|
||||||
|
</p>
|
||||||
|
<code
|
||||||
|
className="text-[10px] font-mono-num block pt-1"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
{preset.gridClass}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STORY 2: Theme Tokens */}
|
||||||
|
{activeStory === 'theme-tokens' && (
|
||||||
|
<div className="hl-card p-6 space-y-6">
|
||||||
|
<div className="border-b hl-border pb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Foundations / Color System
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">
|
||||||
|
主流双主题色板规范(当前生效:{resolvedTheme === 'dark' ? '深色极客' : '浅色纯净'})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-mono-num px-2.5 py-1 rounded hl-muted">
|
||||||
|
模式: {theme === 'system' ? '跟随设备自动切换' : '手动锁定'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
{(['light', 'dark'] as const).map((modeKey) => {
|
||||||
|
const p = THEME_PALETTES[modeKey];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="rounded-xl p-5 space-y-4 border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--bg-page'],
|
||||||
|
color: p.tokens['--text-main'],
|
||||||
|
borderColor: p.tokens['--border-color'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between border-b pb-3"
|
||||||
|
style={{ borderColor: p.tokens['--border-color'] }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-bold">{p.name}</div>
|
||||||
|
<div className="text-xs mt-0.5" style={{ color: p.tokens['--text-muted'] }}>
|
||||||
|
{p.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setTheme(modeKey)}
|
||||||
|
className="px-2.5 py-1 rounded-md text-xs font-mono-num font-semibold cursor-pointer"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--primary'],
|
||||||
|
color: p.tokens['--primary-fg'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
预览此主题
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{Object.entries(p.tokens).map(([tokenName, hex]) => (
|
||||||
|
<div
|
||||||
|
key={tokenName}
|
||||||
|
className="p-2 rounded-lg border flex items-center gap-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--bg-card'],
|
||||||
|
borderColor: p.tokens['--border-color'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-5 h-5 rounded border shrink-0"
|
||||||
|
style={{
|
||||||
|
backgroundColor: hex,
|
||||||
|
borderColor: p.tokens['--border-hover'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-[10px] font-mono-num font-semibold truncate">
|
||||||
|
{tokenName}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-[10px] font-mono-num truncate"
|
||||||
|
style={{ color: p.tokens['--text-muted'] }}
|
||||||
|
>
|
||||||
|
{hex}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STORY 3: ServiceCard Interactive Playground */}
|
||||||
|
{activeStory === 'service-card' && (
|
||||||
|
<div className="hl-card p-6 space-y-6">
|
||||||
|
<div className="border-b hl-border pb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Components / ServiceCard
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">
|
||||||
|
三段式内联遥测卡片 (ServiceCard Interactive Story)
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Sparkles className="w-4 h-4" style={{ color: 'var(--accent)' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Controls */}
|
||||||
|
<div className="p-4 rounded-xl hl-muted border hl-border flex flex-wrap items-center gap-6 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold">在线状态 (online):</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setDemoOnline(!demoOnline)}
|
||||||
|
className="px-2.5 py-1 rounded hl-card font-mono-num cursor-pointer"
|
||||||
|
>
|
||||||
|
{demoOnline ? 'true (绿灯)' : 'false (离线红灯)'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold">右上角动作按钮 (actionType):</span>
|
||||||
|
{(['drawer', 'backup', undefined] as const).map((act, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => setDemoAction(act)}
|
||||||
|
className="px-2.5 py-1 rounded hl-card font-mono-num cursor-pointer"
|
||||||
|
style={demoAction === act ? { borderColor: 'var(--accent)' } : {}}
|
||||||
|
>
|
||||||
|
{act || 'route-badge'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Component Canvas */}
|
||||||
|
<div className="max-w-md mx-auto py-4">
|
||||||
|
<ServiceCard
|
||||||
|
card={{
|
||||||
|
id: 'agy',
|
||||||
|
title: 'AGY 云端工作台',
|
||||||
|
subtitle: 'agy.961504.xyz · ttyd + tmux',
|
||||||
|
iconUrl:
|
||||||
|
'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/google-gemini.png',
|
||||||
|
online: demoOnline,
|
||||||
|
actionType: demoAction,
|
||||||
|
desc: 'M6 Mac mini 上的 24 小时常驻 AI 编程终端,已同步 Google 登录态并锁定新加坡出口,关掉网页后台继续运行。',
|
||||||
|
edgeUrl: 'https://agy.961504.xyz',
|
||||||
|
homeUrl: 'https://agy.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.164:7681',
|
||||||
|
stats: [
|
||||||
|
{ label: 'M6 CPU 负载', value: `${telemetry.mini.cpu_pct}%` },
|
||||||
|
{ label: '已用内存', value: `${telemetry.mini.mem_used_gb} / 24 GB` },
|
||||||
|
{ label: 'tmux 会话', value: '常驻运行中', highlight: true },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STORY 4: SshShelf */}
|
||||||
|
{activeStory === 'ssh-shelf' && (
|
||||||
|
<div className="hl-card p-6 space-y-6">
|
||||||
|
<div className="border-b hl-border pb-4">
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Components / SshShelf
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">底部 SSH 快捷复制与运维操作栏</h2>
|
||||||
|
</div>
|
||||||
|
<SshShelf />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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