diff --git a/global.d.ts b/global.d.ts index 5da4f5d..b396180 100644 --- a/global.d.ts +++ b/global.d.ts @@ -73,17 +73,6 @@ declare global { setFrameless: (route?: string) => Promise, loadPage: (page: string) => Promise }, - tabs: { - create: (url?: string) => Promise, - list: () => Promise, - navigate: (tabId: string, url: string) => Promise, - reload: (tabId: string) => Promise, - back: (tabId: string) => Promise, - forward: (tabId: string) => Promise, - switch: (tabId: string) => Promise, - close: (tabId: string) => Promise, - on: (event: 'tab-updated' | 'tab-created' | 'tab-closed' | 'tab-switched', handler: (payload: any) => void) => () => void - }, readFile: (filePath: string) => Promise<{success: boolean, data?: string, error?: string}>, logger: { debug: (message: string, ...meta?: any[]) => void; diff --git a/src/main/main.ts b/src/main/main.ts index 5cb11bc..87cbf73 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,9 +1,9 @@ import { app, BrowserWindow } from 'electron' import { CONFIG_KEYS } from '@common/constants' +import { setupMainWindow } from './wins'; import started from 'electron-squirrel-startup' import configManager from '@main/service/config-service' import logManager from '@main/service/logger' -import path from "path"; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { @@ -18,32 +18,8 @@ process.on('unhandledRejection', (reason, promise) => { logManager.error('unhandledRejection', reason, promise); }); -const createWindow = () => { - // Create the browser window. - const mainWindow = new BrowserWindow({ - width: 1920, - height: 1080, - autoHideMenuBar: true, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - }, - }); - - // and load the index.html of the app. - if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); - } else { - mainWindow.loadFile( - path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`) - ); - } - - // Open the DevTools. - mainWindow.webContents.openDevTools(); -}; - app.whenReady().then(() => { - createWindow(); + setupMainWindow(); }); // Quit when all windows are closed, except on macOS. There, it's common @@ -60,6 +36,6 @@ app.on('window-all-closed', () => { // dock icon is clicked and there are no other windows open. app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); + setupMainWindow(); } }); diff --git a/src/main/service/window-service/index.ts b/src/main/service/window-service/index.ts index 8107596..feb9e50 100644 --- a/src/main/service/window-service/index.ts +++ b/src/main/service/window-service/index.ts @@ -219,9 +219,9 @@ class WindowService { private _loadPage(window: BrowserWindow, pageName: string) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - return window.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}/html/${pageName}.html`); + return window.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}/${pageName}.html`); } - window.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/html/${pageName}.html`)); + window.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/${pageName}.html`)); } private _loadWindowTemplate(window: BrowserWindow, name: WindowNames) { diff --git a/src/main/wins/index.ts b/src/main/wins/index.ts new file mode 100644 index 0000000..8cca791 --- /dev/null +++ b/src/main/wins/index.ts @@ -0,0 +1,162 @@ +import type { BrowserWindow } from 'electron' +import { ipcMain } from 'electron'; +import { WINDOW_NAMES, MAIN_WIN_SIZE, IPC_EVENTS, MENU_IDS, CONVERSATION_ITEM_MENU_IDS, CONVERSATION_LIST_MENU_IDS, MESSAGE_ITEM_MENU_IDS, CONFIG_KEYS } from '@common/constants' +import { createProvider } from '../providers' +import { windowManager } from '@main/service/window-service' +import { menuManager } from '@main/service/menu-service' +import { logManager } from '@main/service/logger' +import { configManager } from '@main/service/config-service' +import { trayManager } from '@main/service/tray-service' +import { TabManager } from '@service/tab-manager' + +const handleTray = (minimizeToTray: boolean) => { + if (minimizeToTray) { + trayManager.create(); + return; + } + + trayManager.destroy(); +} + +const registerMenus = (window: BrowserWindow) => { + + const conversationItemMenuItemClick = (id: string) => { + logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_ITEM}-${id}`) + window.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_ITEM}`, id); + } + + menuManager.register(MENU_IDS.CONVERSATION_ITEM, [ + { + id: CONVERSATION_ITEM_MENU_IDS.PIN, + label: 'menu.conversation.pinConversation', + click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.PIN) + }, + { + id: CONVERSATION_ITEM_MENU_IDS.RENAME, + label: 'menu.conversation.renameConversation', + click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.RENAME) + }, + { + id: CONVERSATION_ITEM_MENU_IDS.DEL, + label: 'menu.conversation.delConversation', + click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.DEL) + }, + ]) + + const conversationListMenuItemClick = (id: string) => { + logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_LIST}-${id}`) + window.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_LIST}`, id); + } + + menuManager.register(MENU_IDS.CONVERSATION_LIST, [ + { + id: CONVERSATION_LIST_MENU_IDS.NEW_CONVERSATION, + label: 'menu.conversation.newConversation', + click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.NEW_CONVERSATION) + }, + { type: 'separator' }, + { + id: CONVERSATION_LIST_MENU_IDS.SORT_BY, label: 'menu.conversation.sortBy', submenu: [ + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_CREATE_TIME, label: 'menu.conversation.sortByCreateTime', type: 'radio', checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_CREATE_TIME) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_UPDATE_TIME, label: 'menu.conversation.sortByUpdateTime', type: 'radio', checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_UPDATE_TIME) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_NAME, label: 'menu.conversation.sortByName', type: 'radio', checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_NAME) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_MODEL, label: 'menu.conversation.sortByModel', type: 'radio', checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_MODEL) }, + { type: 'separator' }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_ASCENDING, label: 'menu.conversation.sortAscending', type: 'radio', checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_ASCENDING) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_DESCENDING, label: 'menu.conversation.sortDescending', type: 'radio', checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_DESCENDING) }, + ] + }, + { + id: CONVERSATION_LIST_MENU_IDS.BATCH_OPERATIONS, + label: 'menu.conversation.batchOperations', + click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.BATCH_OPERATIONS) + } + ]) + + const messageItemMenuItemClick = (id: string) => { + logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.MESSAGE_ITEM}-${id}`) + window.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.MESSAGE_ITEM}`, id); + } + + menuManager.register(MENU_IDS.MESSAGE_ITEM, [ + { + id: MESSAGE_ITEM_MENU_IDS.COPY, + label: 'menu.message.copyMessage', + click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.COPY) + }, + { + id: MESSAGE_ITEM_MENU_IDS.SELECT, + label: 'menu.message.selectMessage', + click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.SELECT) + }, + { type: 'separator' }, + { + id: MESSAGE_ITEM_MENU_IDS.DELETE, + label: 'menu.message.deleteMessage', + click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.DELETE) + }, + ]) +} + +export function setupMainWindow() { + windowManager.onWindowCreate(WINDOW_NAMES.MAIN, (mainWindow) => { + let minimizeToTray = configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY); + + configManager.onConfigChange((config) => { + if (minimizeToTray === config[CONFIG_KEYS.MINIMIZE_TO_TRAY]) return; + minimizeToTray = config[CONFIG_KEYS.MINIMIZE_TO_TRAY]; + handleTray(minimizeToTray); + }); + + handleTray(minimizeToTray); + registerMenus(mainWindow); + + const tabManager = new TabManager(mainWindow) + tabManager.enable() + + mainWindow.on('closed', () => { + tabManager.destroy() + }) + }); + + windowManager.create(WINDOW_NAMES.MAIN, MAIN_WIN_SIZE); + + ipcMain.on(IPC_EVENTS.START_A_DIALOGUE, async (_event, props: CreateDialogueProps) => { + const { providerName, messages, messageId, selectedModel } = props; + const mainWindow = windowManager.get(WINDOW_NAMES.MAIN); + + if (!mainWindow) { + throw new Error('mainWindow not found'); + } + + try { + + const provider = createProvider(providerName); + const chunks = await provider?.chat(messages, selectedModel); + + if (!chunks) { + throw new Error('chunks or stream not found'); + } + + for await (const chunk of chunks) { + const chunkContent = { + messageId, + data: chunk + } + mainWindow.webContents.send(IPC_EVENTS.START_A_DIALOGUE + 'back' + messageId, chunkContent); + } + + } catch (error) { + const errorContent = { + messageId, + data: { + isEnd: true, + isError: true, + result: error instanceof Error ? error.message : String(error), + } + } + + mainWindow.webContents.send(IPC_EVENTS.START_A_DIALOGUE + 'back' + messageId, errorContent); + } + }) +} diff --git a/src/preload.ts b/src/preload.ts index 271aae7..1fa92ff 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -20,22 +20,6 @@ const api: WindowApi = { loadPage: (page: string) => ipcRenderer.invoke(IPC_EVENTS.APP_LOAD_PAGE, page) }, - tabs: { - create: (url?: string) => ipcRenderer.invoke(IPC_EVENTS.TAB_CREATE, url), - list: () => ipcRenderer.invoke(IPC_EVENTS.TAB_LIST), - navigate: (tabId: string, url: string) => ipcRenderer.invoke(IPC_EVENTS.TAB_NAVIGATE, { tabId, url }), - reload: (tabId: string) => ipcRenderer.invoke(IPC_EVENTS.TAB_RELOAD, tabId), - back: (tabId: string) => ipcRenderer.invoke(IPC_EVENTS.TAB_BACK, tabId), - forward: (tabId: string) => ipcRenderer.invoke(IPC_EVENTS.TAB_FORWARD, tabId), - switch: (tabId: string) => ipcRenderer.invoke(IPC_EVENTS.TAB_SWITCH, tabId), - close: (tabId: string) => ipcRenderer.invoke(IPC_EVENTS.TAB_CLOSE, tabId), - on: (event: 'tab-updated' | 'tab-created' | 'tab-closed' | 'tab-switched', handler: (payload: any) => void) => { - const listener = (_e: any, payload: any) => handler(payload) - ipcRenderer.on(event, listener) - return () => ipcRenderer.removeListener(event, listener) - } - }, - // 通过 IPC 调用主进程 readFile: (filePath: string) => ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath), diff --git a/src/renderer/components/SideMenus/index.vue b/src/renderer/components/SideMenus/index.vue index a5a5a8d..1c47e55 100644 --- a/src/renderer/components/SideMenus/index.vue +++ b/src/renderer/components/SideMenus/index.vue @@ -21,107 +21,16 @@ diff --git a/src/renderer/main.ts b/src/renderer/main.ts index b108594..8399cbf 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -10,7 +10,7 @@ import locale from 'element-plus/es/locale/lang/zh-cn' // 引入 i18n 插件 import i18n from './i18n' -// import './permission' +import './permission' // 样式文件隔离 import "./styles/index.css"; diff --git a/src/renderer/router/index.ts b/src/renderer/router/index.ts index 8d8bc14..cb2691e 100644 --- a/src/renderer/router/index.ts +++ b/src/renderer/router/index.ts @@ -5,12 +5,36 @@ const routes = [ path: '/', redirect: '/home' }, + { + path: "/login", + component: () => import("@renderer/views/login/index.vue"), + name: "Login", + meta: { requiresAuth: true }, + }, { path: "/home", component: () => import("@renderer/views/home/index.vue"), name: "Home", meta: { requiresAuth: true }, }, + { + path: "/task", + component: () => import("@renderer/views/task/index.vue"), + name: "Task", + meta: { requiresAuth: true }, + }, + { + path: "/knowledge", + component: () => import("@renderer/views/knowledge/index.vue"), + name: "Knowledge", + meta: { requiresAuth: true }, + }, + { + path: "/setting", + component: () => import("@renderer/views/setting/index.vue"), + name: "Setting", + meta: { requiresAuth: true }, + }, ]; const router = createRouter({ diff --git a/src/renderer/views/login/index.vue b/src/renderer/views/login/index.vue index a82336f..0487ddf 100644 --- a/src/renderer/views/login/index.vue +++ b/src/renderer/views/login/index.vue @@ -127,6 +127,7 @@ const getVerifyCode = async () => { onMounted(() => getVerifyCode()) +const router = useRouter() const formRef = ref() const onSubmit = async () => { const valid = await formRef.value.validate().catch(() => { }); // 表单校验 @@ -136,7 +137,7 @@ const onSubmit = async () => { try { userStore.login(form).then(() => { - window.api.app.loadPage('index'); + router.push({ path: '/home' }) }) } finally { getVerifyCode()