diff --git a/electron/main/index.ts b/electron/main/index.ts index 97872cce84ab6b84ef6c35230f201a387841fce9..e33a816eb775ef0e92ba91ad8ea9ad0b6d9c78f6 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -128,6 +128,37 @@ function registerIpcListener() { ipcMain.handle('copilot:system', () => { nativeTheme.themeSource = 'system'; }); + + // 添加窗口控制命令处理程序 + ipcMain.handle('copilot:window-control', (event, command) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) return; + + switch (command) { + case 'minimize': + win.minimize(); + break; + case 'maximize': + if (win.isMaximized()) { + win.unmaximize(); + } else { + win.maximize(); + } + break; + case 'close': + win.close(); + break; + } + }); + + // 添加获取窗口最大化状态的处理程序 + ipcMain.handle('copilot:window-is-maximized', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + return win.isMaximized(); + } + return false; + }); } function getUserDefinedConf(dir: string) { diff --git a/electron/main/window/create.ts b/electron/main/window/create.ts index 4cf0b933a5a0877557ad2a0e8ae1f9546fd994fa..3400e993725e820d45f6213f06130c5551e950fc 100644 --- a/electron/main/window/create.ts +++ b/electron/main/window/create.ts @@ -10,10 +10,15 @@ import * as electron from 'electron'; import { BrowserWindow, app, globalShortcut, ipcMain } from 'electron'; import { options as allWindow } from './options'; import { updateConf } from '../common/conf'; +import { isLinux } from '../common/platform'; + +// 存储所有创建的窗口实例,用于全局访问 +const windowInstances: Map = new Map(); export function createWindow( options: Electron.BrowserWindowConstructorOptions, hash: string, + id?: string, ): BrowserWindow { const win = new BrowserWindow({ ...options, @@ -24,15 +29,49 @@ export function createWindow( }, }); + // 存储窗口实例以便全局访问 + if (id) { + windowInstances.set(id, win); + } + if (app.isPackaged) { win.loadFile(path.join(__dirname, `../index.html`), { hash }); } else { win.loadURL(`http://localhost:${process.env.PORT}/#${hash}`); } + // 无论平台如何,都设置窗口控制事件处理 + // 这样确保即使在其他平台添加自定义标题栏也能工作 + setupWindowControls(win); + return win; } +/** + * 为窗口设置控制事件(适用于所有平台) + */ +function setupWindowControls(win: BrowserWindow) { + // 监听窗口最大化/还原事件 + win.on('maximize', () => { + if (win.webContents) { + win.webContents.send('window-maximized-change', true); + } + }); + + win.on('unmaximize', () => { + if (win.webContents) { + win.webContents.send('window-maximized-change', false); + } + }); + + // 添加关闭前确认 + win.on('close', (e) => { + if (win === defaultWindow) { + // 可以在这里添加关闭确认逻辑 + } + }); +} + let defaultWindow: BrowserWindow | null = null; /** @@ -57,8 +96,12 @@ export function createDefaultWindow(): BrowserWindow { const defaultWindowOptions = allWindow.mainWindow.window; const theme = process.env.EULERCOPILOT_THEME || 'light'; - defaultWindowOptions.titleBarOverlay = getDefaultTitleBarOverlay(theme); - defaultWindow = createWindow(defaultWindowOptions, hash); + // 仅在非Linux平台设置titleBarOverlay + if (!isLinux) { + defaultWindowOptions.titleBarOverlay = getDefaultTitleBarOverlay(theme); + } + + defaultWindow = createWindow(defaultWindowOptions, hash, 'mainWindow'); return defaultWindow; } @@ -70,8 +113,12 @@ export function createChatWindow(): BrowserWindow { const chatWindowOptions = allWindow.chatWindow.window; const theme = process.env.EULERCOPILOT_THEME || 'light'; - chatWindowOptions.titleBarOverlay = getDefaultTitleBarOverlay(theme); - chatWindow = createWindow(chatWindowOptions, hash); + // 仅在非Linux平台设置titleBarOverlay + if (!isLinux) { + chatWindowOptions.titleBarOverlay = getDefaultTitleBarOverlay(theme); + } + + chatWindow = createWindow(chatWindowOptions, hash, 'chatWindow'); const shortcutKey = process.platform === 'darwin' ? 'Cmd+Option+O' : 'Ctrl+Alt+O'; @@ -83,20 +130,79 @@ export function createChatWindow(): BrowserWindow { return chatWindow; } +// 全局设置IPC事件处理 +ipcMain.on('window-control', (e, command) => { + console.log('Received window control command:', command); + + // 确保命令来自正确的窗口 + const webContents = e.sender; + const win = BrowserWindow.fromWebContents(webContents); + + if (!win) { + console.error('Cannot find window for the command'); + return; + } + + switch (command) { + case 'minimize': + console.log('Minimizing window'); + win.minimize(); + break; + case 'maximize': + if (win.isMaximized()) { + console.log('Unmaximizing window'); + win.unmaximize(); + } else { + console.log('Maximizing window'); + win.maximize(); + } + break; + case 'close': + console.log('Closing window'); + win.close(); + break; + default: + console.error('Unknown window command:', command); + } +}); + +// 添加查询窗口最大化状态的处理程序 +ipcMain.handle('window-is-maximized', (e) => { + const win = BrowserWindow.fromWebContents(e.sender); + if (win) { + return win.isMaximized(); + } + return false; +}); + ipcMain.handle('copilot:theme', (e, args) => { electron.nativeTheme.themeSource = args.theme; - if (chatWindow) { - chatWindow.setTitleBarOverlay({ - color: args.backgroundColor, - symbolColor: args.theme === 'dark' ? 'white' : 'black', - }); + + // 仅在非Linux平台上更新titleBarOverlay + if (!isLinux) { + if (chatWindow) { + chatWindow.setTitleBarOverlay({ + color: args.backgroundColor, + symbolColor: args.theme === 'dark' ? 'white' : 'black', + }); + } + + if (defaultWindow) { + defaultWindow.setTitleBarOverlay({ + color: args.backgroundColor, + symbolColor: args.theme === 'dark' ? 'white' : 'black', + }); + } } - if (defaultWindow) { - defaultWindow.setTitleBarOverlay({ - color: args.backgroundColor, - symbolColor: args.theme === 'dark' ? 'white' : 'black', - }); + // 通知渲染进程主题已更改,以更新Linux自定义标题栏 + if (isLinux) { + if (chatWindow && chatWindow.webContents) { + chatWindow.webContents.send('theme-updated', args); + } + if (defaultWindow && defaultWindow.webContents) { + defaultWindow.webContents.send('theme-updated', args); + } } updateConf({ diff --git a/electron/main/window/index.ts b/electron/main/window/index.ts index 92fceac3ab7b60417ef216d239843b2ada12ec53..21a4390f99c572b070edceeb60a2c5a978096dcf 100644 --- a/electron/main/window/index.ts +++ b/electron/main/window/index.ts @@ -5,5 +5,72 @@ // IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR // PURPOSE. // See the Mulan PSL v2 for more details. -export { createWindow, createDefaultWindow, createChatWindow } from './create'; -export { createTray } from './tray'; + +import { app, BrowserWindow, ipcMain } from 'electron'; +import { createDefaultWindow, createChatWindow } from './create'; +import { createTray } from './tray'; // 导入createTray函数 + +// 重新导出以便在index.ts中使用 +export { createDefaultWindow, createChatWindow, createTray }; + +// 存储所有窗口引用 +const allWindows: BrowserWindow[] = []; + +export function createWindows() { + // 创建默认窗口 + const defaultWindow = createDefaultWindow(); + allWindows.push(defaultWindow); + + // 添加窗口最大化状态变化事件监听 + defaultWindow.on('maximize', () => { + if (defaultWindow.webContents) { + defaultWindow.webContents.send('window-maximized-change', true); + } + }); + + defaultWindow.on('unmaximize', () => { + if (defaultWindow.webContents) { + defaultWindow.webContents.send('window-maximized-change', false); + } + }); + + // 窗口关闭时退出应用 + defaultWindow.on('closed', () => { + app.quit(); + }); + + // 聊天窗口 + const chatWindow = createChatWindow(); + allWindows.push(chatWindow); + + // 添加聊天窗口最大化状态变化事件监听 + chatWindow.on('maximize', () => { + if (chatWindow.webContents) { + chatWindow.webContents.send('window-maximized-change', true); + } + }); + + chatWindow.on('unmaximize', () => { + if (chatWindow.webContents) { + chatWindow.webContents.send('window-maximized-change', false); + } + }); + + // 添加IPC处理程序,用于查询窗口是否最大化 + ipcMain.handle('window-is-maximized', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + return win.isMaximized(); + } + return false; + }); +} + +export function destroyWindows() { + // 关闭所有窗口 + allWindows.forEach((window) => { + if (!window.isDestroyed()) { + window.close(); + } + }); +} diff --git a/electron/main/window/options.ts b/electron/main/window/options.ts index 378be7dfb0ef2e4630b9988a2bec0332e4e6a301..ac16fe618c1066b9edac2949a8d7d5f8a7c52029 100644 --- a/electron/main/window/options.ts +++ b/electron/main/window/options.ts @@ -5,6 +5,8 @@ // IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR // PURPOSE. // See the Mulan PSL v2 for more details. +import { isLinux } from '../common/platform'; + export interface allWindowType { [propName: string]: { id: string; @@ -13,6 +15,17 @@ export interface allWindowType { }; } +// Linux平台专用窗口配置,添加圆角支持 +const getLinuxSpecificOptions = + (): Partial => { + if (!isLinux) return {}; + + return { + transparent: true, + backgroundColor: '#00000000', + }; + }; + export const options: allWindowType = { mainWindow: { id: 'mainWindow', @@ -27,6 +40,7 @@ export const options: allWindowType = { alwaysOnTop: false, useContentSize: true, icon: 'dist/favicon.ico', + ...getLinuxSpecificOptions(), }, hash: '/', }, @@ -35,14 +49,16 @@ export const options: allWindowType = { window: { width: 680, height: 960, + minWidth: 680, + minHeight: 810, resizable: true, show: false, - alwaysOnTop: false, + skipTaskbar: true, + alwaysOnTop: true, useContentSize: true, - minWidth: 680, - minHeight: 810, titleBarStyle: 'hidden', icon: 'dist/favicon.ico', + ...getLinuxSpecificOptions(), }, hash: '/chat', }, diff --git a/electron/main/window/tray.ts b/electron/main/window/tray.ts index 65c2aae92a3e34d3177b5ea22b882d491c016239..1c07b916e439755b7c9e16026e0aea6c56675f17 100644 --- a/electron/main/window/tray.ts +++ b/electron/main/window/tray.ts @@ -6,15 +6,65 @@ // PURPOSE. // See the Mulan PSL v2 for more details. import path from 'node:path'; -import { app, Tray, Menu } from 'electron'; +import { app, Tray, Menu, BrowserWindow } from 'electron'; import type { MenuItemConstructorOptions } from 'electron'; +import { createDefaultWindow, createChatWindow } from './create'; + +// 保存对主窗口和聊天窗口的引用,方便在托盘菜单中使用 +let defaultWindow: BrowserWindow | null = null; +let chatWindow: BrowserWindow | null = null; export function createTray(): Tray { let appTray: Tray | null = null; if (appTray) return appTray; + // 获取窗口引用 + defaultWindow = + BrowserWindow.getAllWindows().find((win) => + win.webContents.getURL().includes('main'), + ) || null; + + chatWindow = + BrowserWindow.getAllWindows().find((win) => + win.webContents.getURL().includes('chat'), + ) || null; + + // 如果没有找到窗口,尝试创建它们 + if (!defaultWindow) { + defaultWindow = createDefaultWindow(); + } + + if (!chatWindow) { + chatWindow = createChatWindow(); + } + const trayMenus: MenuItemConstructorOptions[] = [ + { + label: '显示主窗口', + click: () => { + if (defaultWindow) { + defaultWindow.show(); + defaultWindow.focus(); + } else { + defaultWindow = createDefaultWindow(); + defaultWindow.show(); + } + }, + }, + { + label: '启动快捷问答', + click: () => { + if (chatWindow) { + chatWindow.show(); + chatWindow.focus(); + } else { + chatWindow = createChatWindow(); + chatWindow.show(); + } + }, + }, + { type: 'separator' }, { label: '退出', click: () => { diff --git a/env.d.ts b/env.d.ts index 9c99de5f506a16492093727f42abff3a1b65e499..4c00dafc4f90054125e8989dde6ef7ba8c559737 100644 --- a/env.d.ts +++ b/env.d.ts @@ -11,6 +11,14 @@ declare interface Window { onHtmlEventDispatch: any; eulercopilot: any; + // 添加electronProcess属性定义 + electronProcess?: { + platform: 'win32' | 'darwin' | 'linux'; + versions: { + electron: string; + }; + env?: Record; + }; } declare interface ImportMetaEnv { diff --git a/src/App.vue b/src/App.vue index 2ee3fe88b8f70589abab4758eaabb77203091684..6500ab760eb14fc11e3dcbc74e545232345fd2d7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,19 @@ @@ -17,5 +32,15 @@ import '@vue-flow/core/dist/theme-default.css'; .eulercopilot-main { width: 100vw; height: 100vh; + position: relative; +} + +.linux-with-titlebar { + display: flex; + flex-direction: column; + + :deep(router-view) { + flex: 1; + } } diff --git a/src/assets/base.css b/src/assets/base.css index 18e8cf0ccedaaf60f7653be727f45287668a196e..277876353644dd7851a11997cd3c266f3818a9b2 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -33,7 +33,7 @@ html { } body { - min-width: 1366px; + min-width: 100vw; min-height: 100vh; transition: color 0.5s, diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss index 8b4c0fe40ef60d20753d20b0adad8ed3557c6e5d..ec56b6cbb33f8776fd8c5905df05ba4cd752bf6e 100644 --- a/src/assets/styles/main.scss +++ b/src/assets/styles/main.scss @@ -9,4 +9,18 @@ body { will-change: transform; transition: transform 0.3s ease; -} \ No newline at end of file +} + +// Linux平台窗口圆角样式 +html.platform-linux { + body { + background: transparent; + border-radius: 16px; + overflow: hidden; + } + + #app { + border-radius: 16px; + overflow: hidden; + } +} diff --git a/src/components/LinuxTitleBar.vue b/src/components/LinuxTitleBar.vue new file mode 100644 index 0000000000000000000000000000000000000000..82f07a0c117e02ed951754c5b466ea14d6286122 --- /dev/null +++ b/src/components/LinuxTitleBar.vue @@ -0,0 +1,229 @@ + + + + + + + diff --git a/src/components/dialoguePanel/DialoguePanel.vue b/src/components/dialoguePanel/DialoguePanel.vue index 98b2302ee4447ecdd37671cc3c68484e26129587..b13f913f14add0fcb3297af299b838fb880c4246 100644 --- a/src/components/dialoguePanel/DialoguePanel.vue +++ b/src/components/dialoguePanel/DialoguePanel.vue @@ -934,8 +934,6 @@ const searchAppName = (appId) => { } } .dialogue-panel { - // padding-right: 25px; - // padding: 0px 15%; width: 1000px; &__user { position: relative; diff --git a/src/main.ts b/src/main.ts index e0f78aec27a499cbc9c54992ac9d4fba10a29d28..a3853b3b8cc751fd0f025bb4ccffeab103576fb6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,9 +31,21 @@ import { QiankunProps, } from 'vite-plugin-qiankun/dist/helper'; +// 添加平台检测逻辑,为HTML添加平台特定类名 +const setPlatformClass = () => { + const electronProcess = window.electronProcess; + if (electronProcess) { + const platform = electronProcess.platform; + document.documentElement.classList.add(`platform-${platform}`); + } +}; + let app: AppInstance | null = null; const render = (props: any = {}) => { + // 在渲染前设置平台类名 + setPlatformClass(); + let selector: string = '#app'; if (props && props.container) { const { container } = props; diff --git a/src/utils/electron.ts b/src/utils/electron.ts index e1f95bb60d2a18fdd9264bff966a5dface280336..5bc4da7299fdbc7d6a136e416bd3340fe783f1b4 100644 --- a/src/utils/electron.ts +++ b/src/utils/electron.ts @@ -9,6 +9,11 @@ export const electronProcess = window.eulercopilot ? window.eulercopilot.process : undefined; +// 为了向后兼容,设置window.electronProcess +if (window.eulercopilot && !window.electronProcess) { + window.electronProcess = window.eulercopilot.process; +} + export const ipcRenderer = window.eulercopilot ? window.eulercopilot.ipcRenderer : undefined; diff --git a/src/views/chat/index.vue b/src/views/chat/index.vue index 5916923821c1252108c98c048b69905a86ae10e1..7e0c56fe6429ecba0d558b2213d9867540d6ca66 100644 --- a/src/views/chat/index.vue +++ b/src/views/chat/index.vue @@ -18,9 +18,13 @@ const handleKeydown = (evt: any): any => { }; const headerStyles = computed(() => { - return window.eulercopilot.process.platform === 'darwin' - ? { paddingLeft: 'calc(50% - 60px)' } - : { paddingRight: '150px' }; + if (window.eulercopilot.process.platform === 'win32') { + return { paddingRight: '145px' }; + } else if (window.eulercopilot.process.platform === 'linux') { + return { paddingRight: '120px' }; + } else if (window.eulercopilot.process.platform === 'darwin') { + return { paddingLeft: 'calc(50% - 60px)' }; + } }); function storageListener(e: StorageEvent) { diff --git a/src/views/dialogue/components/InitalPanel.vue b/src/views/dialogue/components/InitalPanel.vue index befc078f24a31891565124a1a96a3fdc75819a42..cd66462344e2851f65c31c56696d400c2e871053 100644 --- a/src/views/dialogue/components/InitalPanel.vue +++ b/src/views/dialogue/components/InitalPanel.vue @@ -149,7 +149,6 @@ onMounted(() => { /* 禁用水平滚动 */ padding-top: 60px; /* 将内容从顶部放置 60px */ - // transition: 0.5s; /* 0.5 秒过渡效果在侧导航中滑动 */ } /* 导航菜单链接 */ diff --git a/src/views/dialogue/components/TitleBar.vue b/src/views/dialogue/components/TitleBar.vue index 470e4cafcf10d97a11fbc0b2336bbfeec61c8b69..6cc9b2491fd57cfb0fa86fc3a68ec4a20217c404 100644 --- a/src/views/dialogue/components/TitleBar.vue +++ b/src/views/dialogue/components/TitleBar.vue @@ -71,8 +71,7 @@ const headerStyles = computed(() => { } else if (platform === 'win32') { styles.paddingRight = '145px'; } else if (platform === 'linux') { - styles.paddingRight = '110px'; - styles.paddingLeft = 'calc(50% - 60px)'; + styles.paddingRight = '120px'; } } return styles; @@ -145,7 +144,7 @@ const headerStyles = computed(() => { justify-content: space-between; align-items: center; height: 48px; - padding: 0 24px; + padding: 0 20px; background-color: var(--o-bg-color-base); -webkit-app-region: drag; &-left {