- Add window handlers for minimize, maximize, close, and check if maximized in ipcMain. - Update preload script to use new window control IPC events. - Refactor window service to remove old IPC event handlers and use new handlers. - Remove old HeaderBar and DragRegion components, replacing them with a new TitleBar component. - Update Layout component to use TitleBar instead of HeaderBar. - Remove useWinManager hook as its functionality is now integrated into TitleBar. - Update login page to remove HeaderBar and adjust layout accordingly. - Update constants to remove old window IPC events. - Update package dependencies to replace @iconify/vue with @lucide/vue.
17 KiB
Window Controls 迁移计划(对齐 ClawX)
目标:将 zn-ai 的窗口控制体系全面对齐 ClawX,废弃 zn-ai 现有相关代码,按平台重建跨平台标题栏方案。UI 视觉延用 zn-ai 设计,图标库统一使用
@lucide/vue。
1. 现状分析
1.1 ClawX 实现思路
| 平台 | 方案 | 关键配置 |
|---|---|---|
| macOS | 原生 traffic lights | titleBarStyle: 'hiddenInset' |
| Windows | 自定义 React 标题栏 | titleBarStyle: 'hidden' + frame: false |
| Linux | 保留原生窗口边框 | frame: true(默认) |
- macOS:前端仅渲染一个
drag-region(高度 40px),红绿灯由系统原生绘制。 - Windows:前端自定义最小化、最大化/恢复、关闭三个按钮,通过
invokeIPC 调用主进程BrowserWindowAPI。 - Linux:出于 IME 兼容性考虑,不隐藏原生边框,前端不渲染任何自定义标题栏。
1.2 zn-ai 现状(待废弃)
| 项 | 现状 | 决策 |
|---|---|---|
| 主进程窗口配置 | 所有平台统一 frame: false + titleBarStyle: 'hidden' |
废弃,重写 |
| 前端组件 | HeaderBar/index.vue 同时包含 macOS 自定义按钮 + Windows 自定义按钮 |
废弃,删除 |
| 窗口管理 Hook | useWinManager.ts 封装 ref + onMounted 状态管理 |
废弃,删除 |
| 拖拽区域组件 | DragRegion/index.vue 独立组件 |
废弃,删除(直接内联到 TitleBar) |
| Layout 集成 | Layout/index.vue 嵌套 header-bar + drag-region |
重写 |
| IPC 通信 | WINDOW_CLOSE / WINDOW_MINIMIZE / WINDOW_MAXIMIZE / IS_WINDOW_MAXIMIZED(send/on 混合) |
废弃,改为 invoke/handle 对齐 ClawX |
| 图标库 | @iconify/vue + @iconify-json/material-symbols |
废弃,替换为 @lucide/vue |
1.3 核心差异
- macOS:ClawX 使用原生 traffic lights;zn-ai 使用自定义按钮(将原生按钮移出可视区域),行为不完整。
- Linux:ClawX 保留原生边框;zn-ai 隐藏了原生边框,存在 IME 兼容性风险。
- IPC 模式:ClawX 使用
ipcMain.handle+invokeIpc(请求-响应),zn-ai 使用ipcMain.on+ipcRenderer.send(事件驱动),模式不统一。 - 组件层级:ClawX 的
TitleBar是一个自包含组件,内部自带drag-region/no-drag逻辑;zn-ai 将其拆成了HeaderBar+DragRegion+useWinManager三个文件,过度拆分。
2. 迁移方案
2.1 设计原则
- 全面对齐 ClawX:架构、IPC 模式、平台分支逻辑直接复刻 ClawX 实现。
- zn-ai 既有代码可抛弃:
HeaderBar、DragRegion、useWinManager及相关 IPC 直接删除,不用做兼容改造。 - UI 视觉延用 zn-ai:Windows 按钮的 hover 颜色、尺寸保持 zn-ai 现有设计(
#999/#ff0000)。 - 图标统一使用
@lucide/vue:废弃@iconify/vue。
2.2 废弃清单(Delete List)
| 文件/目录 | 说明 |
|---|---|
src/components/HeaderBar/index.vue |
既有自定义按钮组件,完全废弃 |
src/components/DragRegion/index.vue |
拖拽区域组件,逻辑内联到新的 TitleBar |
src/hooks/useWinManager.ts |
窗口管理 Hook,逻辑内联到新的 TitleBar |
src/main.ts 中 HeaderBar / DragRegion 的全局注册 |
删除全局组件注册 |
2.3 废弃的 IPC 与常量
zn-ai 现有窗口控制 IPC(基于 send/on):
// constants.ts 中废弃以下事件
WINDOW_MINIMIZE = 'window-minimize'
WINDOW_MAXIMIZE = 'window-maximize'
WINDOW_CLOSE = 'window-close'
IS_WINDOW_MAXIMIZED = 'is-window-maximized'
废弃后替换为 ClawX 风格的 invoke channel:
// 新增(对齐 ClawX)
'window:minimize'
'window:maximize'
'window:close'
'window:isMaximized'
若项目中其他模块也使用了旧的
WINDOW_MINIMIZE等常量,需要一并迁移。经排查,旧常量仅在HeaderBar/useWinManager/window-service/preload中使用,可随本次重构一并删除。
2.4 新建/重写清单(Create/Rewrite List)
| 新建/重写项 | 说明 |
|---|---|
src/components/layout/TitleBar/index.vue |
新建。对齐 ClawX TitleBar.tsx,Vue 实现。 |
src/components/layout/Layout/index.vue |
重写。移除 header-bar/drag-region 引用,改用 TitleBar。 |
electron/service/window-service/index.ts |
重写 SHARED_WINDOW_OPTIONS。按平台设置 frame/titleBarStyle/trafficLightPosition。 |
electron/main.ts 或新建 electron/main/ipc-handlers.ts |
新增/重写。注册 window:* 的 ipcMain.handle。 |
electron/preload/index.ts |
重写。移除旧 send/on API,新增 invoke('window:*') 和 platform。 |
src/lib/api-client.ts(或类似文件) |
新增 invokeIpc 辅助函数。对齐 ClawX 的 invokeIpc 调用风格。 |
src/styles/index.css |
保留 .drag-region / .no-drag,供 TitleBar 使用。 |
2.5 主进程窗口配置改造
目标代码(对齐 ClawX):
// electron/service/window-service/index.ts
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
const useCustomTitleBar = isWindows;
const SHARED_WINDOW_OPTIONS = {
frame: isMac || !useCustomTitleBar,
titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default',
trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined,
show: false,
title: 'NIANXX',
darkTheme: themeManager.isDark,
backgroundColor: themeManager.isDark ? '#2C2C2C' : '#FFFFFF',
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
backgroundThrottling: false,
preload: MAIN_WINDOW_VITE_DEV_SERVER_URL
? path.join(process.cwd(), 'dist-electron/preload/preload.js')
: path.join(__dirname, 'preload.js'),
},
} as BrowserWindowConstructorOptions;
注意:
- 删除旧的
trafficLightPosition: { x: -100, y: -100 }。 - macOS 使用
hiddenInset后,红绿灯由系统原生绘制,前端无需硬编码左侧占位。
2.6 新增 IPC Handlers(对齐 ClawX)
新建或复用文件(建议新建 electron/ipc/window-handlers.ts,方便模块化):
import { ipcMain, BrowserWindow } from 'electron';
export function registerWindowHandlers(mainWindow: BrowserWindow): void {
ipcMain.handle('window:minimize', () => {
mainWindow.minimize();
});
ipcMain.handle('window:maximize', () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
});
ipcMain.handle('window:close', () => {
mainWindow.close();
});
ipcMain.handle('window:isMaximized', () => {
return mainWindow.isMaximized();
});
}
在 electron/main.ts 或 electron/wins/index.ts 的适当时机调用 registerWindowHandlers(mainWindow)。
2.7 Preload 改造(对齐 ClawX invokeIpc 风格)
// electron/preload/index.ts
const api: WindowApi = {
// ... 保留既有 API
platform: process.platform,
// 窗口控制 — 对齐 ClawX 的 invoke 风格
windowMinimize: () => ipcRenderer.invoke('window:minimize'),
windowMaximize: () => ipcRenderer.invoke('window:maximize'),
windowClose: () => ipcRenderer.invoke('window:close'),
windowIsMaximized: () => ipcRenderer.invoke('window:isMaximized'),
// 移除旧的 minimizeWindow / maximizeWindow / closeWindow / onWindowMaximized / isWindowMaximized
}
2.8 前端 invokeIpc 辅助函数(可选,强烈建议)
ClawX 使用统一的 invokeIpc 函数调用主进程,zn-ai 建议新增该辅助函数以保持一致风格:
// src/lib/api-client.ts
export function invokeIpc(channel: string, ...args: any[]): Promise<any> {
return window.api.invoke(channel, ...args);
}
如果 zn-ai 已有类似封装(如
window.api.invoke),可直接使用,无需额外文件。
2.9 新建 TitleBar 组件(对齐 ClawX)
新建文件:src/components/layout/TitleBar/index.vue
<template>
<!-- macOS: 仅渲染拖拽区域 -->
<div v-if="platform === 'darwin'" class="drag-region h-10 shrink-0 border-b bg-background" />
<!-- Linux: 不渲染任何自定义标题栏 -->
<template v-else-if="platform !== 'win32'" />
<!-- Windows: 自定义标题栏 -->
<div v-else class="drag-region flex h-10 shrink-0 items-center justify-end border-b bg-background">
<div class="no-drag flex h-full">
<button
@click="handleMinimize"
class="flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-[#999] hover:text-white transition-colors"
title="Minimize"
>
<Minus class="h-4 w-4" />
</button>
<button
@click="handleMaximize"
class="flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-[#999] hover:text-white transition-colors"
:title="maximized ? 'Restore' : 'Maximize'"
>
<Copy v-if="maximized" class="h-3.5 w-3.5" />
<Square v-else class="h-3.5 w-3.5" />
</button>
<button
@click="handleClose"
class="flex h-full w-11 items-center justify-center text-muted-foreground hover:bg-[#ff0000] hover:text-white transition-colors"
title="Close"
>
<X class="h-4 w-4" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Minus, Square, Copy, X } from '@lucide/vue'
const platform = window.api?.platform ?? '';
const maximized = ref(false);
onMounted(async () => {
maximized.value = await window.api.windowIsMaximized();
});
const handleMinimize = () => {
window.api.windowMinimize();
};
const handleMaximize = async () => {
await window.api.windowMaximize();
maximized.value = await window.api.windowIsMaximized();
};
const handleClose = () => {
window.api.windowClose();
};
</script>
样式说明:
.drag-region/.no-drag复用src/styles/index.css中已有定义。bg-background/text-muted-foreground等 Token 若 zn-ai 未定义,可替换为具体色值(如bg-white、text-[#525866]),保持视觉一致即可。
2.10 Layout 重写
重写文件:src/components/Layout/index.vue
<template>
<div class="bg-color h-screen flex flex-col">
<!-- Linux 不渲染 TitleBar -->
<title-bar v-if="platform !== 'linux'" />
<main class="box-border w-full flex pt-2 pb-2 pl-2"
:style="{ height: platform === 'linux' ? '100vh' : 'calc(100vh - 40px)' }">
<div class="flex-1 flex">
<slot />
</div>
<SideMenus />
</main>
</div>
</template>
<script setup lang="ts" name="Layout">
import { computed } from 'vue'
import SideMenus from '@src/components/SideMenus/index.vue'
import TitleBar from '@components/layout/TitleBar/index.vue'
const platform = computed(() => window.api?.platform ?? '')
</script>
注意:src/main.ts 中删除 HeaderBar 和 DragRegion 的全局注册。
2.11 Icon 体系迁移:iconify → @lucide/vue
依赖变更:
# 安装
pnpm add @lucide/vue
# 卸载(确认项目内无其他 iconify 引用后执行)
pnpm remove @iconify/vue @iconify-json/material-symbols
图标映射关系:
| 功能 | 原 iconify icon | lucide-vue 替代 |
|---|---|---|
| 最小化 | material-symbols:check-indeterminate-small |
Minus |
| 最大化 | material-symbols:chrome-maximize-outline-sharp |
Square |
| 恢复 | material-symbols:chrome-restore-outline-sharp |
Copy |
| 关闭 | material-symbols:close |
X |
类型声明清理:
删除 global.d.ts 中 @iconify/vue 的 declare module 段落(约第 230-240 行)。
3. 改造清单(Task Breakdown)
Task 1:主进程窗口配置重写
- 文件:
electron/service/window-service/index.ts - 内容:
- 引入
isMac/isWindows平台判断。 - 重写
SHARED_WINDOW_OPTIONS的frame、titleBarStyle、trafficLightPosition。 - 移除
trafficLightPosition: { x: -100, y: -100 }。
- 引入
- 验收标准:
- macOS 窗口出现原生 traffic lights。
- Windows 窗口无原生标题栏。
- Linux 窗口保留原生标题栏。
Task 2:新增 IPC Handlers(对齐 ClawX)
- 新建文件:
electron/ipc/window-handlers.ts(或写入现有 ipc 文件) - 内容:
- 使用
ipcMain.handle注册window:minimize、window:maximize、window:close、window:isMaximized。 - 在
electron/wins/index.ts或electron/main.ts中引入并调用registerWindowHandlers(mainWindow)。
- 使用
- 验收标准:
- 通过 DevTools Console 测试
await window.api.windowIsMaximized()返回布尔值。 windowMinimize/windowMaximize/windowClose调用后窗口行为正确。
- 通过 DevTools Console 测试
Task 3:Preload 重写
- 文件:
electron/preload/index.ts - 内容:
- 移除旧的
minimizeWindow、maximizeWindow、closeWindow、onWindowMaximized、isWindowMaximized。 - 新增
platform、windowMinimize、windowMaximize、windowClose、windowIsMaximized。 - 同步更新
global.d.ts中的WindowApi类型定义。
- 移除旧的
- 验收标准:
- 渲染进程 TypeScript 无报错。
window.api.platform能正确读取当前平台。
Task 4:前端组件废弃与重建
- 废弃文件:
src/components/HeaderBar/index.vue(删除)src/components/DragRegion/index.vue(删除)src/hooks/useWinManager.ts(删除)
- 新建文件:
src/components/layout/TitleBar/index.vue(对齐 ClawXTitleBar.tsx)
- 重写文件:
src/components/Layout/index.vue(改用TitleBar,Linux 下隐藏)src/main.ts(移除HeaderBar/DragRegion全局注册)
- 验收标准:
- macOS 仅显示顶部 40px 拖拽区域,无自定义按钮。
- Windows 显示自定义最小化/最大化/关闭按钮,功能正常。
- Linux 不渲染任何自定义标题栏元素。
Task 5:依赖清理
- 文件:
package.json、global.d.ts - 内容:
- 安装
@lucide/vue。 - 卸载
@iconify/vue和@iconify-json/material-symbols。 - 删除
global.d.ts中@iconify/vue的类型声明。
- 安装
- 验收标准:
pnpm install后无 iconify 包残留。- 项目编译通过。
Task 6:跨平台回归测试
- 内容:
- macOS:验证 traffic lights 位置、拖拽行为、窗口最大化/恢复、关闭。
- Windows:验证自定义按钮 hover 样式、最大化/恢复状态切换、最小化到任务栏、关闭。
- Linux:验证原生边框存在、无自定义标题栏残留、窗口操作正常。
4. 工作量评估(Sub Agent 分配)
本任务涉及 Electron 主进程、前端组件重建、跨平台回归 三个独立领域,推荐由 3 个 Sub Agent 并行/串行完成:
Agent 1:Electron 主进程专家
- 职责:Task 1 + Task 2 + Task 3
- 技能要求:熟悉 Electron BrowserWindow 选项、IPC
handle/invoke模式、preload 安全模型。 - 预计耗时:2~3 小时
- 产出:
window-service+ipc-handlers+preload的改造 PR。
Agent 2:Vue 前端组件专家
- 职责:Task 4 + Task 5
- 技能要求:熟悉 Vue 3、组件重构、Tailwind CSS、依赖清理。
- 预计耗时:2~4 小时
- 产出:
TitleBar/Layout重建 PR + 依赖清理补丁。
Agent 3:跨平台测试与集成专家
- 职责:Task 6(回归测试)+ 边缘情况修复
- 技能要求:能在 macOS / Windows / Linux 环境下运行 Electron 应用,熟悉 frameless 窗口陷阱。
- 预计耗时:2~4 小时(取决于平台覆盖度)
- 产出:测试报告 + 对 Agent 1/2 PR 的修复补丁。
5. 风险与注意事项
- macOS traffic light 位置:
hiddenInset默认内边距可能因 Electron 版本略有差异,建议通过trafficLightPosition微调至{x: 16, y: 16}(与 ClawX 一致)。 - Linux 原生边框与主题:若 zn-ai 深色模式下 Linux 原生标题栏颜色不匹配,可后续通过 GTK 主题或
darkTheme选项优化,但不在本次核心迁移范围内。 - Windows 最大化白边:
frame: false的窗口在 Windows 最大化时可能出现 1px 白边。ClawX 未做特殊处理,可后续按需优化。 - 关闭行为一致性:zn-ai 主窗口关闭逻辑原会根据
MINIMIZE_TO_TRAY配置决定hide()或close()。改造后window:close直接调用mainWindow.close(),若需保留托盘最小化逻辑,可在window:closehandler 中判断MINIMIZE_TO_TRAY并调用mainWindow.hide()阻止默认关闭。 - 旧常量清理:若
IPC_EVENTS中WINDOW_MINIMIZE/WINDOW_MAXIMIZE/WINDOW_CLOSE/IS_WINDOW_MAXIMIZED被其他未发现的模块引用,删除后会导致编译失败。建议在删除前用全局搜索再次确认。
6. 参考资料
- ClawX 主窗口创建:
ClawX/electron/main/index.ts - ClawX TitleBar 组件:
ClawX/src/components/layout/TitleBar.tsx - ClawX 窗口 IPC:
ClawX/electron/main/ipc-handlers.ts(registerWindowHandlers) - zn-ai 现有窗口服务:
zn-ai/electron/service/window-service/index.ts - zn-ai 现有 HeaderBar:
zn-ai/src/components/HeaderBar/index.vue