feat: implement custom window controls and replace header bar with title bar

- 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.
This commit is contained in:
duanshuwen
2026-04-14 23:38:42 +08:00
parent 6fd51d04dd
commit b5a67ff650
20 changed files with 642 additions and 340 deletions

View File

@@ -0,0 +1,423 @@
# 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**:前端自定义最小化、最大化/恢复、关闭三个按钮,通过 `invoke` IPC 调用主进程 `BrowserWindow` API。
- **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 核心差异
1. **macOS**ClawX 使用原生 traffic lightszn-ai 使用自定义按钮(将原生按钮移出可视区域),行为不完整。
2. **Linux**ClawX 保留原生边框zn-ai 隐藏了原生边框,存在 IME 兼容性风险。
3. **IPC 模式**ClawX 使用 `ipcMain.handle` + `invokeIpc`(请求-响应zn-ai 使用 `ipcMain.on` + `ipcRenderer.send`(事件驱动),模式不统一。
4. **组件层级**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`
```ts
// constants.ts 中废弃以下事件
WINDOW_MINIMIZE = 'window-minimize'
WINDOW_MAXIMIZE = 'window-maximize'
WINDOW_CLOSE = 'window-close'
IS_WINDOW_MAXIMIZED = 'is-window-maximized'
```
**废弃后替换为 ClawX 风格的 `invoke` channel**
```ts
// 新增(对齐 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**
```ts
// 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`,方便模块化):
```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` 风格)
```ts
// 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 建议新增该辅助函数以保持一致风格:
```ts
// 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`
```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`
```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
**依赖变更**
```bash
# 安装
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`
- **内容**
1. 引入 `isMac` / `isWindows` 平台判断。
2. 重写 `SHARED_WINDOW_OPTIONS``frame``titleBarStyle``trafficLightPosition`
3. 移除 `trafficLightPosition: { x: -100, y: -100 }`
- **验收标准**
- macOS 窗口出现原生 traffic lights。
- Windows 窗口无原生标题栏。
- Linux 窗口保留原生标题栏。
### Task 2新增 IPC Handlers对齐 ClawX
- **新建文件**`electron/ipc/window-handlers.ts`(或写入现有 ipc 文件)
- **内容**
1. 使用 `ipcMain.handle` 注册 `window:minimize``window:maximize``window:close``window:isMaximized`
2.`electron/wins/index.ts``electron/main.ts` 中引入并调用 `registerWindowHandlers(mainWindow)`
- **验收标准**
- 通过 DevTools Console 测试 `await window.api.windowIsMaximized()` 返回布尔值。
- `windowMinimize` / `windowMaximize` / `windowClose` 调用后窗口行为正确。
### Task 3Preload 重写
- **文件**`electron/preload/index.ts`
- **内容**
1. 移除旧的 `minimizeWindow``maximizeWindow``closeWindow``onWindowMaximized``isWindowMaximized`
2. 新增 `platform``windowMinimize``windowMaximize``windowClose``windowIsMaximized`
3. 同步更新 `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`(对齐 ClawX `TitleBar.tsx`
- **重写文件**
- `src/components/Layout/index.vue`(改用 `TitleBar`Linux 下隐藏)
- `src/main.ts`(移除 `HeaderBar` / `DragRegion` 全局注册)
- **验收标准**
- macOS 仅显示顶部 40px 拖拽区域,无自定义按钮。
- Windows 显示自定义最小化/最大化/关闭按钮,功能正常。
- Linux 不渲染任何自定义标题栏元素。
### Task 5依赖清理
- **文件**`package.json``global.d.ts`
- **内容**
1. 安装 `@lucide/vue`
2. 卸载 `@iconify/vue``@iconify-json/material-symbols`
3. 删除 `global.d.ts``@iconify/vue` 的类型声明。
- **验收标准**
- `pnpm install` 后无 iconify 包残留。
- 项目编译通过。
### Task 6跨平台回归测试
- **内容**
1. **macOS**:验证 traffic lights 位置、拖拽行为、窗口最大化/恢复、关闭。
2. **Windows**:验证自定义按钮 hover 样式、最大化/恢复状态切换、最小化到任务栏、关闭。
3. **Linux**:验证原生边框存在、无自定义标题栏残留、窗口操作正常。
---
## 4. 工作量评估Sub Agent 分配)
本任务涉及 **Electron 主进程**、**前端组件重建**、**跨平台回归** 三个独立领域,推荐由 **3 个 Sub Agent** 并行/串行完成:
### Agent 1Electron 主进程专家
- **职责**Task 1 + Task 2 + Task 3
- **技能要求**:熟悉 Electron BrowserWindow 选项、IPC `handle/invoke` 模式、preload 安全模型。
- **预计耗时**23 小时
- **产出**`window-service` + `ipc-handlers` + `preload` 的改造 PR。
### Agent 2Vue 前端组件专家
- **职责**Task 4 + Task 5
- **技能要求**:熟悉 Vue 3、组件重构、Tailwind CSS、依赖清理。
- **预计耗时**24 小时
- **产出**`TitleBar` / `Layout` 重建 PR + 依赖清理补丁。
### Agent 3跨平台测试与集成专家
- **职责**Task 6回归测试+ 边缘情况修复
- **技能要求**:能在 macOS / Windows / Linux 环境下运行 Electron 应用,熟悉 frameless 窗口陷阱。
- **预计耗时**24 小时(取决于平台覆盖度)
- **产出**:测试报告 + 对 Agent 1/2 PR 的修复补丁。
---
## 5. 风险与注意事项
1. **macOS traffic light 位置**`hiddenInset` 默认内边距可能因 Electron 版本略有差异,建议通过 `trafficLightPosition` 微调至 `{x: 16, y: 16}`(与 ClawX 一致)。
2. **Linux 原生边框与主题**:若 zn-ai 深色模式下 Linux 原生标题栏颜色不匹配,可后续通过 GTK 主题或 `darkTheme` 选项优化,但不在本次核心迁移范围内。
3. **Windows 最大化白边**`frame: false` 的窗口在 Windows 最大化时可能出现 1px 白边。ClawX 未做特殊处理,可后续按需优化。
4. **关闭行为一致性**zn-ai 主窗口关闭逻辑原会根据 `MINIMIZE_TO_TRAY` 配置决定 `hide()``close()`。改造后 `window:close` 直接调用 `mainWindow.close()`,若需保留托盘最小化逻辑,可在 `window:close` handler 中判断 `MINIMIZE_TO_TRAY` 并调用 `mainWindow.hide()` 阻止默认关闭。
5. **旧常量清理**:若 `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`