Skip to content

Commit fd2984d

Browse files
committed
feat: Windows 采用自定义标题栏,样式优化符合主题样式
1 parent 2a722b6 commit fd2984d

8 files changed

Lines changed: 147 additions & 21 deletions

File tree

apps/electron/src/main/index.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, BrowserWindow, Menu, screen, shell } from 'electron'
1+
import { app, BrowserWindow, Menu, nativeTheme, screen, shell } from 'electron'
22
import { join } from 'path'
33
import { existsSync } from 'fs'
44

@@ -15,6 +15,7 @@ if (!app.requestSingleInstanceLock()) {
1515
}
1616

1717
import { getSettings } from './lib/settings-service'
18+
import { resolveOverlayColors } from './lib/titlebar-overlay'
1819

1920
// 处理 EPIPE 错误:当 stdout/stderr 管道被关闭时(如 electronmon 重启),忽略写入错误
2021
// 这在开发环境热重载时经常发生,不影响应用功能
@@ -163,22 +164,43 @@ function createWindow(): void {
163164
console.warn('App icon not found at:', iconPath)
164165
}
165166

167+
const isMac = process.platform === 'darwin'
168+
const isWindows = process.platform === 'win32'
169+
170+
const titleBarOptions = isMac
171+
? {
172+
titleBarStyle: 'hiddenInset' as const,
173+
trafficLightPosition: { x: 18, y: 18 },
174+
vibrancy: 'under-window' as const,
175+
visualEffectState: 'followWindow' as const,
176+
}
177+
: isWindows
178+
? (() => {
179+
const settings = getSettings()
180+
return {
181+
titleBarStyle: 'hidden' as const,
182+
titleBarOverlay: resolveOverlayColors(
183+
settings.themeMode,
184+
settings.themeStyle,
185+
nativeTheme.shouldUseDarkColors
186+
),
187+
}
188+
})()
189+
: {}
190+
166191
mainWindow = new BrowserWindow({
167192
width: 1400,
168193
height: 900,
169194
minWidth: 800,
170195
minHeight: 600,
171196
icon: iconExists ? iconPath : undefined,
172-
show: false, // Don't show until ready
197+
show: false,
173198
webPreferences: {
174199
preload: join(__dirname, 'preload.cjs'),
175200
contextIsolation: true,
176201
nodeIntegration: false,
177202
},
178-
titleBarStyle: 'hiddenInset', // macOS style
179-
trafficLightPosition: { x: 18, y: 18 },
180-
vibrancy: 'under-window', // macOS glass effect
181-
visualEffectState: 'followWindow',
203+
...titleBarOptions,
182204
})
183205

184206
// Load the renderer

apps/electron/src/main/ipc.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ import { extractTextFromAttachment } from './lib/document-parser'
123123
import { getTutorialContent, createWelcomeConversation } from './lib/tutorial-service'
124124
import { getUserProfile, updateUserProfile } from './lib/user-profile-service'
125125
import { getSettings, updateSettings } from './lib/settings-service'
126+
import { updateWindowTitleBarOverlay } from './lib/titlebar-overlay'
126127
import { checkEnvironment } from './lib/environment-checker'
127128
import { fetchInstallerManifest, findInstallerSource } from './lib/installer-manifest'
128129
import {
@@ -686,6 +687,8 @@ export function registerIpcHandlers(): void {
686687
if (win.webContents.id !== event.sender.id) {
687688
win.webContents.send(SETTINGS_IPC_CHANNELS.ON_THEME_SETTINGS_CHANGED, payload)
688689
}
690+
// Windows: 更新标题栏 overlay 颜色
691+
updateWindowTitleBarOverlay(win)
689692
})
690693
}
691694

@@ -721,6 +724,12 @@ export function registerIpcHandlers(): void {
721724
BrowserWindow.getAllWindows().forEach((win) => {
722725
win.webContents.send(SETTINGS_IPC_CHANNELS.ON_SYSTEM_THEME_CHANGED, isDark)
723726
})
727+
// Windows: system 模式下同步更新标题栏 overlay
728+
if (process.platform === 'win32' && getSettings().themeMode === 'system') {
729+
BrowserWindow.getAllWindows().forEach((win) => {
730+
updateWindowTitleBarOverlay(win)
731+
})
732+
}
724733
})
725734

726735
// ===== 应用图标切换 =====

apps/electron/src/main/lib/file-preview-service.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
type FSWatcher,
2828
} from 'node:fs'
2929
import { tmpdir } from 'node:os'
30+
import { resolveOverlayColors } from './titlebar-overlay'
31+
import { getSettings } from './settings-service'
3032

3133
/** 文件大小限制:50MB */
3234
const MAX_FILE_SIZE = 50 * 1024 * 1024
@@ -1309,9 +1311,30 @@ function docxPreviewHtml(filePath: string, filename: string, base64Data: string)
13091311
/** 创建预览窗口并绑定脏状态关闭确认 */
13101312
function createPreviewWindow(filename: string): BrowserWindow {
13111313
const isMac = process.platform === 'darwin'
1314+
const isWindows = process.platform === 'win32'
13121315
const { workArea } = screen.getPrimaryDisplay()
13131316
const width = Math.min(1200, workArea.width)
13141317
const height = workArea.height
1318+
1319+
const titleBarOptions = isMac
1320+
? {
1321+
titleBarStyle: 'hiddenInset' as const,
1322+
trafficLightPosition: { x: 16, y: 14 },
1323+
}
1324+
: isWindows
1325+
? (() => {
1326+
const settings = getSettings()
1327+
return {
1328+
titleBarStyle: 'hidden' as const,
1329+
titleBarOverlay: resolveOverlayColors(
1330+
settings.themeMode,
1331+
settings.themeStyle,
1332+
nativeTheme.shouldUseDarkColors
1333+
),
1334+
}
1335+
})()
1336+
: {}
1337+
13151338
const previewWindow = new BrowserWindow({
13161339
width,
13171340
height,
@@ -1320,8 +1343,7 @@ function createPreviewWindow(filename: string): BrowserWindow {
13201343
minWidth: 480,
13211344
minHeight: 360,
13221345
title: filename,
1323-
titleBarStyle: isMac ? 'hiddenInset' : 'default',
1324-
trafficLightPosition: isMac ? { x: 16, y: 14 } : undefined,
1346+
...titleBarOptions,
13251347
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1e1e1e' : '#fafaf8',
13261348
webPreferences: {
13271349
preload: join(__dirname, 'file-preview-preload.cjs'),
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { BrowserWindow, nativeTheme } from 'electron'
2+
import type { ThemeMode, ThemeStyle } from '../../types'
3+
import { getSettings } from './settings-service'
4+
5+
interface OverlayColors {
6+
color: string
7+
symbolColor: string
8+
height: number
9+
}
10+
11+
const OVERLAY_HEIGHT = 40
12+
13+
const THEME_COLORS: Record<string, { color: string; symbolColor: string }> = {
14+
'default-light': { color: '#ffffff', symbolColor: '#0a0a0a' },
15+
'default-dark': { color: '#121212', symbolColor: '#fafafa' },
16+
'ocean-light': { color: '#ecf2f7', symbolColor: '#1b2632' },
17+
'ocean-dark': { color: '#182434', symbolColor: '#e7ebef' },
18+
'forest-light': { color: '#eff5f1', symbolColor: '#1d3026' },
19+
'forest-dark': { color: '#212c26', symbolColor: '#e3e8e5' },
20+
'slate-light': { color: '#e3e1dc', symbolColor: '#312f2a' },
21+
'slate-dark': { color: '#1d1b20', symbolColor: '#e9e6e3' },
22+
}
23+
24+
export function resolveOverlayColors(
25+
themeMode: ThemeMode,
26+
themeStyle: ThemeStyle | undefined,
27+
systemIsDark: boolean
28+
): OverlayColors {
29+
let key: string
30+
31+
if (themeMode === 'special' && themeStyle && themeStyle !== 'default') {
32+
key = themeStyle
33+
} else if (themeMode === 'system') {
34+
key = systemIsDark ? 'default-dark' : 'default-light'
35+
} else if (themeMode === 'dark') {
36+
key = 'default-dark'
37+
} else {
38+
key = 'default-light'
39+
}
40+
41+
const colors = THEME_COLORS[key] ?? THEME_COLORS['default-dark']!
42+
return { color: colors.color, symbolColor: colors.symbolColor, height: OVERLAY_HEIGHT }
43+
}
44+
45+
export function updateWindowTitleBarOverlay(win: BrowserWindow): void {
46+
if (process.platform !== 'win32') return
47+
if (win.isDestroyed()) return
48+
49+
try {
50+
const settings = getSettings()
51+
const { color, symbolColor, height } = resolveOverlayColors(
52+
settings.themeMode,
53+
settings.themeStyle,
54+
nativeTheme.shouldUseDarkColors
55+
)
56+
win.setTitleBarOverlay({ color, symbolColor, height })
57+
} catch {
58+
// frameless 窗口(如 quick-task)不支持 setTitleBarOverlay,静默忽略
59+
}
60+
}

apps/electron/src/renderer/components/onboarding/OnboardingView.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,13 @@ import { ScrollArea } from '@/components/ui/scroll-area'
2222
import { TutorialViewer } from '@/components/tutorial/TutorialViewer'
2323
import { EnvironmentCheckPanel } from '@/components/environment/EnvironmentCheckPanel'
2424
import { isShellEnvironmentOkAtom } from '@/atoms/environment'
25+
import { detectIsWindows } from '@/lib/platform'
2526

2627
interface OnboardingViewProps {
2728
/** 完成回调(进入主界面) */
2829
onComplete: () => void
2930
}
3031

31-
function detectIsWindows(): boolean {
32-
const platform =
33-
typeof navigator !== 'undefined' &&
34-
(navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform
35-
if (typeof platform === 'string' && platform.toLowerCase().includes('win')) {
36-
return true
37-
}
38-
return typeof navigator !== 'undefined' && /win/i.test(navigator.platform || '')
39-
}
40-
4132
export function OnboardingView({ onComplete }: OnboardingViewProps) {
4233
const [showTutorial, setShowTutorial] = useState(false)
4334
const [step, setStep] = useState<'welcome' | 'environment'>('welcome')

apps/electron/src/renderer/components/settings/AppearanceSettings.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
applyThemeToDOM,
2525
} from '@/atoms/theme'
2626
import { cn } from '@/lib/utils'
27+
import { detectIsWindows } from '@/lib/platform'
2728
import type { ThemeMode, ThemeStyle } from '../../../types'
2829

2930
// ===== Logo 资源导入(用于图标选择器) =====
@@ -214,7 +215,7 @@ function AppIconPicker(): React.ReactElement {
214215
})
215216
}, [])
216217

217-
const isWindows = typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows')
218+
const isWindows = React.useMemo(() => detectIsWindows(), [])
218219

219220
const handleIconSelect = React.useCallback(async (variantId: string) => {
220221
if (isWindows) {

apps/electron/src/renderer/components/tabs/TabBar.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { appModeAtom } from '@/atoms/app-mode'
3030
import { TabBarItem } from './TabBarItem'
3131
import { TabCloseConfirmDialog } from './TabCloseConfirmDialog'
3232
import { useCloseTab } from '@/hooks/useCloseTab'
33+
import { detectIsWindows } from '@/lib/platform'
34+
import { cn } from '@/lib/utils'
3335

3436
export function TabBar(): React.ReactElement {
3537
const tabs = useAtomValue(tabsAtom)
@@ -153,7 +155,7 @@ function TabBarInner({
153155
const enterTimerRef = React.useRef<ReturnType<typeof setTimeout>>()
154156
const leaveTimerRef = React.useRef<ReturnType<typeof setTimeout>>()
155157
const fadeTimerRef = React.useRef<ReturnType<typeof setTimeout>>()
156-
158+
const isWindows = React.useMemo(() => detectIsWindows(), [])
157159
React.useEffect(() => {
158160
return () => {
159161
if (enterTimerRef.current) clearTimeout(enterTimerRef.current)
@@ -199,7 +201,7 @@ function TabBarInner({
199201
<div className="flex items-end h-[34px] tabbar-bg relative">
200202
<div className="absolute inset-0 titlebar-drag-region" />
201203

202-
<div className="relative flex items-end flex-1 min-w-0 overflow-x-clip titlebar-no-drag">
204+
<div className={cn("relative flex items-end flex-1 min-w-0 overflow-x-clip titlebar-no-drag", isWindows && "pr-[140px]")}>
203205
{tabs.map((tab) => (
204206
<TabBarItem
205207
key={tab.id}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function detectIsWindows(): boolean {
2+
const platform =
3+
typeof navigator !== 'undefined' &&
4+
(navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform
5+
if (typeof platform === 'string' && platform.toLowerCase().includes('win')) {
6+
return true
7+
}
8+
return typeof navigator !== 'undefined' && /win/i.test(navigator.platform || '')
9+
}
10+
11+
export function detectIsMac(): boolean {
12+
const platform =
13+
typeof navigator !== 'undefined' &&
14+
(navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform
15+
if (typeof platform === 'string' && platform.toLowerCase().includes('mac')) {
16+
return true
17+
}
18+
return typeof navigator !== 'undefined' && /mac/i.test(navigator.platform || '')
19+
}

0 commit comments

Comments
 (0)