feat: Windows 端支持原生窗口控制并实现主题同步 (#31)

This commit is contained in:
Forrest
2026-01-13 21:17:38 +08:00
committed by GitHub
parent 4de48b1d27
commit e548f3785c
5 changed files with 69 additions and 24 deletions
+1
View File
@@ -40,6 +40,7 @@ nsis:
installerIcon: build/icon.ico
uninstallerIcon: build/icon.ico
include: installer-dpi.nsh
differentialPackage: false
# macOS 平台配置
mac:
+37 -1
View File
@@ -118,10 +118,24 @@ class MainProcess {
}
// macOS: 使用 hiddenInset 保留红绿灯按钮
// Windows/Linux: 完全移除系统标题栏
// Windows: 使用 titleBarOverlay,在自定义标题栏区域右侧显示原生窗口按钮
// Linux: 使用自定义标题栏和自定义按钮
if (platform.isMacOS) {
windowOptions.titleBarStyle = 'hiddenInset'
} else if (platform.isWindows) {
// 保留系统框架,只隐藏标题栏内容,把内容区域顶到最上方
windowOptions.titleBarStyle = 'hidden'
// 获取当前主题状态
const isDark = nativeTheme.shouldUseDarkColors
windowOptions.titleBarOverlay = {
// 背景透明
color: '#00000000',
// 图标颜色适配主题
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
height: 32,
}
} else {
// Linux 继续使用无边框 + 自定义按钮
windowOptions.frame = false
}
@@ -129,6 +143,28 @@ class MainProcess {
this.mainWindow.once('ready-to-show', () => {
this.mainWindow?.show()
// Windows 上根据当前主题设置 titleBarOverlay 颜色
if (platform.isWindows) {
const isDark = nativeTheme.shouldUseDarkColors
this.mainWindow?.setTitleBarOverlay({
color: '#00000000', // 透明背景
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
height: 32,
})
// 监听主题变化,动态更新图标颜色
nativeTheme.on('updated', () => {
if (this.mainWindow && platform.isWindows) {
const isDark = nativeTheme.shouldUseDarkColors
this.mainWindow.setTitleBarOverlay({
color: '#00000000',
symbolColor: isDark ? '#a1a1aa' : '#52525b',
height: 32,
})
}
})
}
})
// 主窗口事件
+10
View File
@@ -57,6 +57,16 @@ export function registerWindowHandlers(ctx: IpcContext): void {
ipcMain.on('window:setThemeSource', (_, mode: 'system' | 'light' | 'dark') => {
const { nativeTheme } = require('electron')
nativeTheme.themeSource = mode
// Windows 上动态更新图标颜色以匹配主题
if (process.platform === 'win32' && win) {
const isDark = nativeTheme.shouldUseDarkColors
win.setTitleBarOverlay({
color: '#00000000', // 透明背景
symbolColor: isDark ? '#a1a1aa' : '#52525b', // dark: zinc-400, light: zinc-600
height: 32,
})
}
})
// ==================== 应用信息 ====================
+15 -21
View File
@@ -13,26 +13,20 @@ declare module 'vue' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UChatPrompt: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/ChatPrompt.vue')['default']
UChatPromptSubmit: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/ChatPromptSubmit.vue')['default']
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
UContextMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/ContextMenu.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UInputTags: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/InputTags.vue')['default']
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default']
USelect: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parser@7.28.5_@floating-ui+dom@1.7.4_@tiptap+extension-drag-handl_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UContextMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/ContextMenu.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default']
UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.3.0_@babel+parse_a9a750d621383b428aaeaf8a849d7a17/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
}
}
+6 -2
View File
@@ -6,8 +6,11 @@ const isMac = ref(false)
const isMaximized = ref(false)
// 检测平台
const isWindows = ref(false)
onMounted(() => {
isMac.value = navigator.platform.toLowerCase().includes('mac')
isWindows.value = navigator.platform.toLowerCase().includes('win')
// 监听窗口状态变化
window.electron?.ipcRenderer?.on('windowState', (_: unknown, maximized: boolean) => {
@@ -37,8 +40,9 @@ function close() {
<!-- 中间拖拽区域 - 填充剩余空间 -->
<div class="drag-region" />
<!-- 右侧窗口控制按钮 - Windows/Linux -->
<div v-if="!isMac" class="window-controls">
<!-- Windows: 使用原生窗口按钮通过 titleBarOverlay只需预留空间 -->
<!-- Linux: 使用自定义窗口按钮 -->
<div v-if="!isMac && !isWindows" class="window-controls">
<!-- 最小化 -->
<button class="control-btn" @click="minimize">
<svg width="10" height="1" viewBox="0 0 10 1">