From f50130bd7499ab21005a87ef6b737fd83fee8352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Fri, 11 Apr 2025 16:52:28 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Linux=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E7=AA=97=E5=8F=A3=E5=9C=86=E8=A7=92=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- electron/main/window/options.ts | 22 ++++++++++++++++--- env.d.ts | 8 +++++++ src/assets/base.css | 2 +- src/assets/styles/main.scss | 16 +++++++++++++- src/main.ts | 12 ++++++++++ src/utils/electron.ts | 5 +++++ src/views/dialogue/components/InitalPanel.vue | 1 - 7 files changed, 60 insertions(+), 6 deletions(-) diff --git a/electron/main/window/options.ts b/electron/main/window/options.ts index 378be7df..ac16fe61 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/env.d.ts b/env.d.ts index 9c99de5f..4c00dafc 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/assets/base.css b/src/assets/base.css index 18e8cf0c..27787635 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 8b4c0fe4..ec56b6cb 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/main.ts b/src/main.ts index e0f78aec..a3853b3b 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 e1f95bb6..5bc4da72 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/dialogue/components/InitalPanel.vue b/src/views/dialogue/components/InitalPanel.vue index befc078f..cd664623 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 秒过渡效果在侧导航中滑动 */ } /* 导航菜单链接 */ -- Gitee From f810e78c33bc50931ef240027ae27fb0b7702e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Fri, 11 Apr 2025 20:25:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89Linux=E5=B9=B3=E5=8F=B0=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD=EF=BC=8C=E8=A7=A3=E5=86=B3?= =?UTF-8?q?Electron=E5=86=85=E7=BD=AE=E6=8C=89=E9=92=AE=E5=9C=A8Linux?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E4=B8=8D=E6=94=AF=E6=8C=81=E5=9C=86=E8=A7=92?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- electron/main/index.ts | 31 +++ electron/main/window/create.ts | 134 ++++++++-- electron/main/window/index.ts | 71 +++++- src/App.vue | 25 ++ src/components/LinuxTitleBar.vue | 229 ++++++++++++++++++ .../dialoguePanel/DialoguePanel.vue | 2 - src/views/chat/index.vue | 10 +- src/views/dialogue/components/TitleBar.vue | 5 +- 8 files changed, 483 insertions(+), 24 deletions(-) create mode 100644 src/components/LinuxTitleBar.vue diff --git a/electron/main/index.ts b/electron/main/index.ts index 97872cce..e33a816e 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 4cf0b933..3400e993 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 92fceac3..21a4390f 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/src/App.vue b/src/App.vue index 2ee3fe88..6500ab76 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/components/LinuxTitleBar.vue b/src/components/LinuxTitleBar.vue new file mode 100644 index 00000000..82f07a0c --- /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 98b2302e..b13f913f 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/views/chat/index.vue b/src/views/chat/index.vue index 59169238..7e0c56fe 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/TitleBar.vue b/src/views/dialogue/components/TitleBar.vue index 470e4caf..6cc9b249 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 { -- Gitee From 1c5fa66076ef649756e9e4cde868395ad0453cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Fri, 11 Apr 2025 20:25:25 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=89=98?= =?UTF-8?q?=E7=9B=98=E8=8F=9C=E5=8D=95=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=98=BE=E7=A4=BA=E4=B8=BB=E7=AA=97=E5=8F=A3=E5=92=8C?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- electron/main/window/tray.ts | 52 +++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/electron/main/window/tray.ts b/electron/main/window/tray.ts index 65c2aae9..1c07b916 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: () => { -- Gitee