MenuBar
源码:
src/components/layout/MenuBar.tsx
用途与 UX 角色
MenuBar 是 WorkspaceLayout 顶端的 32px 菜单条,由四块组成(从左到右):
- Logo + 应用名称 —
Apollo Map Studio文本与渐变图标徽标。 - 菜单组(File / Edit / View / Tools / Help)— 完全由 Action Registry 驱动,菜单项不在组件里硬编码。
- Spacer(
flex-1)。 - ModeToggle — 一对中文标签的分段按钮:
绘图/场景,绑定useUIStore.appMode。
它和 ToolStrip、CommandPalette 一起构成 Action Registry 的三个 UI 出口(详见 架构 的"Action Registry"章节)。
组件组合树
Props 接口
ts
export interface MenuBarProps {
onExecute: (actionId: ActionId) => void;
getToggleState: (actionId: ActionId) => boolean;
}1
2
3
4
2
3
4
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
onExecute | (actionId: ActionId) => void | — | 当用户点击某个菜单项时调用——通常是 useActionDispatcher().execute |
getToggleState | (actionId: ActionId) => boolean | — | 对 isToggle 属性为 true 的 action(如 toggleGrid)返回当前开关状态,用于渲染勾选 |
子组件
Menu
ts
function Menu(props: {
label: string;
actions: ActionDef[];
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
onExecute: (id: ActionId) => void;
getToggleState: (id: ActionId) => boolean;
}): JSX.Element;1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
行为:
- 点击按钮 toggle 打开/关闭。
- 打开时挂载
mousedown全局监听器,点外部即关闭(MenuBar.tsx:32-41)。 - 按
menuOrder把 actions 分组,每 10 一档插入分隔线(MenuBar.tsx:44-53)。 - 渲染勾选时使用
getToggleState(item.id) ? '✓' : ''。
ModeToggle
ts
function ModeToggle(): JSX.Element;1
- 直接读
useUIStore的appMode与setAppMode。 'drawing' | 'scene'两个按钮,每个 11px 字号、3px padding,激活时bg-cyan-500/20 text-cyan-300。
内部状态
| 钩子 | 作用 |
|---|---|
useState<string | null>(null) | openMenu — 当前打开的菜单名(互斥) |
useUIStore(s.appMode) | 当前应用模式(绘图/场景) |
useUIStore(s.setAppMode) | 切换模式 |
副作用
- Click outside:每个
Menu在isOpen=true时挂mousedown监听,点击非自身区域关闭。必须在 cleanup 中removeEventListener,否则多个菜单切换时会泄漏。 - 菜单项执行:
onExecute(item.id)由父组件的useActionDispatcher处理,包括 R1 撤销 CANCEL 修复。
渲染骨架
jsx
<div className="h-8 bg-zinc-950 border-b border-white/[0.07] flex items-center px-2 shrink-0">
<div className="flex items-center gap-2 mr-4">
<div className="w-4 h-4 rounded bg-gradient-to-br from-cyan-400 to-cyan-600" />
<span className="text-xs font-medium text-zinc-300 tracking-wide">Apollo Map Studio</span>
</div>
<div className="flex items-center">
{menuNames.map((name) => (
<Menu key={name} label={name} actions={getMenuActions(name)} … />
))}
</div>
<div className="flex-1" />
<ModeToggle />
</div>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
下拉面板:
jsx
<div className="absolute top-full left-0 mt-1 py-1 min-w-[200px] bg-zinc-900 border border-white/10 rounded-md shadow-xl z-50">
{/* 每项: ✓ 标记 / 标签 / 快捷键 */}
</div>1
2
3
2
3
性能注释
getMenuNames()每次 render 都调用,但内部基于MENU_ORDER常量数组 +Map.has,O(N),可以忽略。如果将来菜单极多,可在父组件useMemo。Menu组件未memo:因为父组件传入的onExecute/getToggleState来自useActionDispatcher,每次 render 都是新引用。Menu内部的下拉面板已在isOpen=false时不渲染,浪费可忽略。- 键盘快捷键不在此处理:
useActionDispatcher在 WorkspaceLayout 顶层挂一个全局keydown监听器;MenuBar只显示formatShortcut(item.shortcut)文本。
源码索引
| 关注点 | 文件位置 |
|---|---|
| MenuBar 主体 | MenuBar.tsx:142-177 |
Menu 子组件 | MenuBar.tsx:13-97 |
| Click outside 监听 | MenuBar.tsx:32-41 |
| 菜单分隔符插入 | MenuBar.tsx:44-53 |
| ModeToggle | MenuBar.tsx:108-140 |
| Action Registry 入口 | src/core/actions/registry.ts (getMenuNames, getMenuActions, formatShortcut) |
跨页参考
- WorkspaceLayout — 父组件
- ToolStrip / CommandPalette — 共享 Action Registry 的另外两个出口
- Action Registry →
src/core/actions/registry.ts useActionDispatcher→/api/hooks- 模式切换 →
uiStore.appMode