feat: 调整项目多标签切换方案

This commit is contained in:
DEV_DSW
2026-01-19 16:16:56 +08:00
parent bd54a3e88a
commit c6e07ed414
22 changed files with 42 additions and 1209 deletions

View File

@@ -1,9 +1,9 @@
import { app, BrowserWindow } from 'electron'
import { setupWindows } from '@main/wins'
import { CONFIG_KEYS } from '@common/constants'
import started from 'electron-squirrel-startup'
import configManager from '@main/service/config-service'
import logManager from '@main/service/logger'
import { CONFIG_KEYS } from '@common/constants'
import path from "path";
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
@@ -18,19 +18,32 @@ process.on('unhandledRejection', (reason, promise) => {
logManager.error('unhandledRejection', reason, promise);
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
setupWindows();
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
setupWindows();
}
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();
});
// Quit when all windows are closed, except on macOS. There, it's common
@@ -43,5 +56,10 @@ app.on('window-all-closed', () => {
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

View File

@@ -1,49 +0,0 @@
import { IPC_EVENTS, WINDOW_NAMES } from '@common/constants'
import { BrowserWindow, ipcMain } from 'electron'
import { windowManager } from '@main/service/window-service'
export function setupDialogWindow() {
let dialogWindow: BrowserWindow | void;
let params: CreateDialogProps | void
let feedback: string | void
ipcMain.handle(WINDOW_NAMES.DIALOG + 'get-params',(e)=>{
if(BrowserWindow.fromWebContents(e.sender) !== dialogWindow) return
return {
winId: e.sender.id,
...params
}
});
['confirm','cancel'].forEach(_feedback => {
ipcMain.on(WINDOW_NAMES.DIALOG + _feedback,(e,winId:number)=> {
if(e.sender.id !== winId) return
feedback = _feedback;
windowManager.close(BrowserWindow.fromWebContents(e.sender));
});
});
ipcMain.handle(`${IPC_EVENTS.OPEN_WINDOW}:${WINDOW_NAMES.DIALOG}`, (e, _params) => {
params = _params;
dialogWindow = windowManager.create(
WINDOW_NAMES.DIALOG,
{
width: 350, height: 200,
minWidth: 350, minHeight: 200,
maxWidth: 400, maxHeight: 300,
},
{
parent: BrowserWindow.fromWebContents(e.sender) as BrowserWindow,
resizable: false
}
);
return new Promise<string | void>((resolve) => dialogWindow?.on('closed', () => {
resolve(feedback);
feedback = void 0;
}))
})
}
export default setupDialogWindow

View File

@@ -1,9 +0,0 @@
import { setupMainWindow } from './main';
import { setupDialogWindow } from './dialog';
import { setupSetttingWindow } from './setting';
export function setupWindows() {
setupMainWindow();
setupSetttingWindow();
setupDialogWindow();
}

View File

@@ -1,185 +0,0 @@
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'
import path from 'node:path'
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()
let tabsInitialized = false
mainWindow.on('closed', () => {
tabManager.destroy()
})
const getPageUrl = (page: string) => {
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
return `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/html/${page}.html`
}
return `file://${path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/html/${page}.html`)}`
}
const initTabs = () => {
if (tabsInitialized) return
tabsInitialized = true
tabManager.create(getPageUrl('home'), true)
tabManager.create(getPageUrl('knowledge'), false)
tabManager.create(getPageUrl('task'), false)
tabManager.create(getPageUrl('setting'), false)
}
mainWindow.webContents.on('did-finish-load', () => {
const url = mainWindow.webContents.getURL()
const isDevRoot = MAIN_WINDOW_VITE_DEV_SERVER_URL && (url === MAIN_WINDOW_VITE_DEV_SERVER_URL || url === `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/`)
if (url.includes('/html/index.html') || url.endsWith('index.html') || isDevRoot) {
initTabs()
}
})
});
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);
}
})
}

View File

@@ -1,20 +0,0 @@
import { IPC_EVENTS, WINDOW_NAMES } from '@common/constants'
import { ipcMain } from 'electron'
import { windowManager } from '@main/service/window-service'
export function setupSetttingWindow() {
ipcMain.on(`${IPC_EVENTS.OPEN_WINDOW}:${WINDOW_NAMES.SETTING}`, () => {
const settingWindow = windowManager.get(WINDOW_NAMES.SETTING);
if (settingWindow && !settingWindow.isDestroyed())
return windowManager.focus(settingWindow);
windowManager.create(WINDOW_NAMES.SETTING, {
width: 800,
height: 600,
minHeight: 600,
minWidth: 800,
});
})
}
export default setupSetttingWindow;

View File

@@ -1,5 +1,9 @@
<template>
<router-view />
<router-view v-slot="{ Component }">
<keep-alive :include="['home']">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<script setup lang="ts"></script>

View File

@@ -1,15 +0,0 @@
import '@renderer/styles/index.css'
import errorHandler from '@utils/errorHandler'
import i18n from '@renderer/i18n'
import HeaderBar from '@renderer/components/HeaderBar/index.vue'
import DragRegion from '@renderer/components/DragRegion/index.vue'
import Dialog from './index.vue'
createApp(Dialog)
.use(i18n)
.use(errorHandler)
.component('HeaderBar', HeaderBar)
.component('DragRegion', DragRegion)
.mount('#app')

View File

@@ -1,40 +0,0 @@
import { createApp, type Plugin } from "vue"
import errorHandler from '@utils/errorHandler'
// 引入 Element Plus 组件库
import ElementPlus from 'element-plus'
import locale from 'element-plus/es/locale/lang/zh-cn'
// 引入 i18n 插件
import i18n from '@renderer/i18n'
import Home from './index.vue'
// 样式文件隔离
import '@renderer/styles/index.css'
import 'element-plus/dist/index.css'
// 引入全局组件
import HeaderBar from '@components/HeaderBar/index.vue'
import DragRegion from '@components/DragRegion/index.vue'
import Layout from '@components/Layout/index.vue'
const components: Plugin = (app) => {
app.component('HeaderBar', HeaderBar);
app.component('DragRegion', DragRegion);
app.component('Layout', Layout);
}
// 创建 Vue 应用实例
const app = createApp(Home);
const pinia = createPinia();
// 使用 Pinia 状态管理
app.use(pinia);
app.use(ElementPlus, { locale })
app.use(components)
app.use(i18n)
app.use(errorHandler)
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -1,40 +0,0 @@
import { createApp, type Plugin } from "vue"
import errorHandler from '@utils/errorHandler'
// 引入 Element Plus 组件库
import ElementPlus from 'element-plus'
import locale from 'element-plus/es/locale/lang/zh-cn'
// 引入 i18n 插件
import i18n from '@renderer/i18n'
import Home from './HomeTab.vue'
// 样式文件隔离
import '@renderer/styles/index.css'
import 'element-plus/dist/index.css'
// 引入全局组件
import HeaderBar from '@components/HeaderBar/index.vue'
import DragRegion from '@components/DragRegion/index.vue'
import Layout from '@components/Layout/index.vue'
const components: Plugin = (app) => {
app.component('HeaderBar', HeaderBar);
app.component('DragRegion', DragRegion);
app.component('Layout', Layout);
}
// 创建 Vue 应用实例
const app = createApp(Home);
const pinia = createPinia();
// 使用 Pinia 状态管理
app.use(pinia);
app.use(ElementPlus, { locale })
app.use(components)
app.use(i18n)
app.use(errorHandler)
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -1,38 +0,0 @@
import { createApp, type Plugin } from "vue"
import errorHandler from '@utils/errorHandler'
// 引入 Element Plus 组件库
import ElementPlus from 'element-plus'
import locale from 'element-plus/es/locale/lang/zh-cn'
// 引入 i18n 插件
import i18n from '@renderer/i18n'
import Knowledge from './index.vue'
// 样式文件隔离
import '@renderer/styles/index.css'
import 'element-plus/dist/index.css'
// 引入全局组件
import HeaderBar from '@components/HeaderBar/index.vue'
import DragRegion from '@components/DragRegion/index.vue'
const components: Plugin = (app) => {
app.component('HeaderBar', HeaderBar);
app.component('DragRegion', DragRegion);
}
// 创建 Vue 应用实例
const app = createApp(Knowledge);
const pinia = createPinia();
// 使用 Pinia 状态管理
app.use(pinia);
app.use(ElementPlus, { locale })
app.use(components)
app.use(i18n)
app.use(errorHandler)
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -1,38 +0,0 @@
import { createApp, type Plugin } from "vue"
import errorHandler from '@utils/errorHandler'
// 引入 Element Plus 组件库
import ElementPlus from 'element-plus'
import locale from 'element-plus/es/locale/lang/zh-cn'
// 引入 i18n 插件
import i18n from '@renderer/i18n'
import Login from './index.vue'
// 样式文件隔离
import '@renderer/styles/index.css'
import 'element-plus/dist/index.css'
// 引入全局组件
import HeaderBar from '@components/HeaderBar/index.vue'
import DragRegion from '@components/DragRegion/index.vue'
const components: Plugin = (app) => {
app.component('HeaderBar', HeaderBar);
app.component('DragRegion', DragRegion);
}
// 创建 Vue 应用实例
const app = createApp(Login);
const pinia = createPinia();
// 使用 Pinia 状态管理
app.use(pinia);
app.use(ElementPlus, { locale })
app.use(components)
app.use(i18n)
app.use(errorHandler)
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -1,15 +0,0 @@
import '@renderer/styles/index.css'
import errorHandler from '@renderer/utils/errorHandler'
import i18n from '@renderer/i18n'
import HeaderBar from '@renderer/components/HeaderBar/index.vue'
import DragRegion from '@renderer/components/DragRegion/index.vue'
import Setting from './index.vue'
createApp(Setting)
.use(i18n)
.use(errorHandler)
.component('HeaderBar', HeaderBar)
.component('DragRegion', DragRegion)
.mount('#app')

View File

@@ -1,38 +0,0 @@
import { createApp, type Plugin } from "vue"
import errorHandler from '@utils/errorHandler'
// 引入 Element Plus 组件库
import ElementPlus from 'element-plus'
import locale from 'element-plus/es/locale/lang/zh-cn'
// 引入 i18n 插件
import i18n from '@renderer/i18n'
import Task from './index.vue'
// 样式文件隔离
import '@renderer/styles/index.css'
import 'element-plus/dist/index.css'
// 引入全局组件
import HeaderBar from '@components/HeaderBar/index.vue'
import DragRegion from '@components/DragRegion/index.vue'
const components: Plugin = (app) => {
app.component('HeaderBar', HeaderBar);
app.component('DragRegion', DragRegion);
}
// 创建 Vue 应用实例
const app = createApp(Task);
const pinia = createPinia();
// 使用 Pinia 状态管理
app.use(pinia);
app.use(ElementPlus, { locale })
app.use(components)
app.use(i18n)
app.use(errorHandler)
// 挂载应用到 DOM
app.mount("#app");