- TopBar / Nav 的 hash 形式管理入口(#/admin、#/admin/system)**删除**(design/07 §0.2:用户端不提供任何 管理入口、bundle 不含该路径串);面板只经 ADMIN_PATH 路径直入 - AdminApp 三处 window.location.hash = "" 改调 router 导出的 navigateUserShell() - 复核:tsc 0 错 + vite build 成功;用户端源码 grep "#/admin" 0 命中(仅在 channels/useAdapterNav.ts 尚有模块内闭环的 hash 字面量,属另一决策,仅报告未改) - 局限:无 playwright/puppeteer 且不许加依赖 ⇒ 「布局未塌」为结构+静态样式依赖推断(无子元素数量相关样式)
101 lines
4.5 KiB
TypeScript
101 lines
4.5 KiB
TypeScript
import { useState } from "react";
|
|
import { useAppStore } from "../store/useAppStore";
|
|
import { useCurrentMood, useCurrentPersona } from "../store/personaSelectors";
|
|
import { useModeStore } from "../store/useModeStore";
|
|
import { applyTheme, readTheme, type ThemeMode } from "../lib/theme";
|
|
import { personaAvatar } from "../lib/persona";
|
|
import { labelFor } from "../lib/enums";
|
|
import type { Mode, WsStatus } from "../lib/types";
|
|
import { UiModeSwitch } from "../design/UiModeSwitch";
|
|
import { Tag } from "./primitives";
|
|
import { Icon, type IconName } from "./Icon";
|
|
import { AwakenBadge } from "./status";
|
|
|
|
const MODE_LABEL: Record<Mode, string> = {
|
|
chat: "对话", code: "代码", home: "家居", work: "工作", docs: "知识库",
|
|
voice: "语音", vision: "视觉", agents: "子代理", settings: "设置",
|
|
};
|
|
const MODE_ICON: Record<Mode, IconName> = {
|
|
chat: "chat", code: "terminal", home: "home", work: "folder", docs: "docs",
|
|
voice: "mic", vision: "vision", agents: "agent", settings: "settings",
|
|
};
|
|
|
|
const wsMeta: Record<WsStatus, { label: string; tone: "ok" | "warn" | "neutral" | "err" }> = {
|
|
idle: { label: "未连接", tone: "neutral" },
|
|
connecting: { label: "连接中", tone: "warn" },
|
|
connected: { label: "实时", tone: "ok" },
|
|
reconnecting: { label: "重连中", tone: "warn" },
|
|
disconnected: { label: "已断开", tone: "err" },
|
|
expired: { label: "登录过期", tone: "err" },
|
|
};
|
|
|
|
export function TopBar({ onMenu }: { onMenu: () => void }): React.ReactNode {
|
|
const wsStatus = useAppStore((s) => s.wsStatus);
|
|
// Both follow the *active session's* role (design/22 §4.6): switching sessions
|
|
// switches the avatar/name, and the mood is the one the active role reported
|
|
// (mood frames are role-scoped).
|
|
const mood = useCurrentMood();
|
|
const currentPersona = useCurrentPersona();
|
|
const mode = useModeStore((s) => s.mode);
|
|
const [theme, setTheme] = useState<ThemeMode>(readTheme);
|
|
|
|
const toggleTheme = () => {
|
|
const next: ThemeMode = theme === "dark" ? "light" : "dark";
|
|
applyTheme(next);
|
|
setTheme(next);
|
|
};
|
|
|
|
const meta = wsMeta[wsStatus];
|
|
|
|
return (
|
|
<header className="glass sticky top-0 z-30 flex h-[var(--mu-topbar-h)] items-center gap-2 border-b border-[var(--mu-glass-border)] px-3 sm:px-4">
|
|
<button
|
|
onClick={onMenu}
|
|
aria-label="打开菜单"
|
|
title="菜单"
|
|
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-[var(--mu-radius)] text-[var(--mu-text-mid)] transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-4)] active:scale-95 md:hidden"
|
|
>
|
|
<Icon name="menu" size={20} />
|
|
</button>
|
|
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<img src={personaAvatar(currentPersona)} alt={currentPersona?.name ?? "Cyrene"} className="h-7 w-7 shrink-0 rounded-full object-cover" />
|
|
<div className="min-w-0 leading-tight">
|
|
<div className="truncate text-sm font-semibold">{currentPersona?.name ?? "昔涟"}</div>
|
|
<div className="truncate text-[11px] text-[var(--mu-text-lo)]">
|
|
<span className="inline-flex items-center gap-1">
|
|
<Icon name={MODE_ICON[mode]} size={12} />
|
|
{MODE_LABEL[mode]}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1" />
|
|
|
|
{mood ? <Tag tone="accent" className="hidden sm:inline-flex">心情 · {labelFor("mood", mood)}</Tag> : null}
|
|
|
|
<AwakenBadge />
|
|
|
|
<Tag tone={meta.tone} className="hidden sm:inline-flex">
|
|
<span className={`inline-block h-1.5 w-1.5 rounded-full ${wsStatus === "connected" ? "bg-[var(--mu-ok)]" : wsStatus === "expired" ? "bg-[var(--mu-err)]" : "mu-blink"}`} />
|
|
{meta.label}
|
|
</Tag>
|
|
|
|
{/* 双档 UI 档位(裁决 16.4.4):胶囊形态,尺寸对齐本栏其余 h-8 按钮。
|
|
窄屏(<sm)本栏已被头像/状态位占满,再塞 88px 会把角色名挤没甚至横向溢出,
|
|
故窄屏收起——移动端改由侧栏抽屉里的分段开关切档(Nav.tsx)。 */}
|
|
<span className="hidden shrink-0 sm:inline-flex">
|
|
<UiModeSwitch variant="compact" />
|
|
</span>
|
|
<button
|
|
onClick={toggleTheme}
|
|
aria-label="切换主题"
|
|
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-[var(--mu-radius)] bg-[var(--mu-surface-4)] text-sm transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-5)] active:scale-95"
|
|
>
|
|
<Icon name={theme === "dark" ? "sun" : "moon"} size={18} />
|
|
</button>
|
|
</header>
|
|
);
|
|
}
|