feat: 项目多标签方案调整

This commit is contained in:
DEV_DSW
2026-01-19 17:06:01 +08:00
parent c6e07ed414
commit 0c0f5ec430
9 changed files with 197 additions and 152 deletions

11
global.d.ts vendored
View File

@@ -73,17 +73,6 @@ declare global {
setFrameless: (route?: string) => Promise<void>, setFrameless: (route?: string) => Promise<void>,
loadPage: (page: string) => Promise<void> loadPage: (page: string) => Promise<void>
}, },
tabs: {
create: (url?: string) => Promise<TabInfo>,
list: () => Promise<TabInfo[]>,
navigate: (tabId: string, url: string) => Promise<void>,
reload: (tabId: string) => Promise<void>,
back: (tabId: string) => Promise<void>,
forward: (tabId: string) => Promise<void>,
switch: (tabId: string) => Promise<void>,
close: (tabId: string) => Promise<void>,
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}>, readFile: (filePath: string) => Promise<{success: boolean, data?: string, error?: string}>,
logger: { logger: {
debug: (message: string, ...meta?: any[]) => void; debug: (message: string, ...meta?: any[]) => void;

View File

@@ -1,9 +1,9 @@
import { app, BrowserWindow } from 'electron' import { app, BrowserWindow } from 'electron'
import { CONFIG_KEYS } from '@common/constants' import { CONFIG_KEYS } from '@common/constants'
import { setupMainWindow } from './wins';
import started from 'electron-squirrel-startup' import started from 'electron-squirrel-startup'
import configManager from '@main/service/config-service' import configManager from '@main/service/config-service'
import logManager from '@main/service/logger' import logManager from '@main/service/logger'
import path from "path";
// Handle creating/removing shortcuts on Windows when installing/uninstalling. // Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) { if (started) {
@@ -18,32 +18,8 @@ process.on('unhandledRejection', (reason, promise) => {
logManager.error('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(() => { app.whenReady().then(() => {
createWindow(); setupMainWindow();
}); });
// Quit when all windows are closed, except on macOS. There, it's common // 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. // dock icon is clicked and there are no other windows open.
app.on('activate', () => { app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); setupMainWindow();
} }
}); });

View File

@@ -219,9 +219,9 @@ class WindowService {
private _loadPage(window: BrowserWindow, pageName: string) { private _loadPage(window: BrowserWindow, pageName: string) {
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { 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) { private _loadWindowTemplate(window: BrowserWindow, name: WindowNames) {

162
src/main/wins/index.ts Normal file
View File

@@ -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);
}
})
}

View File

@@ -20,22 +20,6 @@ const api: WindowApi = {
loadPage: (page: string) => ipcRenderer.invoke(IPC_EVENTS.APP_LOAD_PAGE, page) 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 调用主进程 // 通过 IPC 调用主进程
readFile: (filePath: string) => ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath), readFile: (filePath: string) => ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath),

View File

@@ -21,107 +21,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref } from 'vue'
import { menus, type MenuItem } from '@constant/menus' import { menus, type MenuItem } from '@constant/menus'
const currentId = ref(1) const currentId = ref(1)
const tabMap = new Map<number, string>() const router = useRouter()
const cleanupListeners: (() => void)[] = []
const getHtmlPath = (menuUrl: string) => {
const cleanUrl = menuUrl.startsWith('/') ? menuUrl.slice(1) : menuUrl
let filename = ''
switch (cleanUrl) {
case 'home':
filename = 'home.html'
break
case 'knowledge':
filename = 'knowledge.html'
break
case 'task':
filename = 'task.html'
break
case 'setting':
filename = 'setting.html'
break
default:
filename = 'home.html'
}
if (import.meta.env.DEV) {
return `/html/${filename}`
}
return filename
}
onMounted(async () => {
if (window.api && window.api.tabs) {
const cleanupClosed = window.api.tabs.on('tab-closed', (payload: any) => {
const { tabId } = payload
for (const [menuId, id] of tabMap.entries()) {
if (id === tabId) {
tabMap.delete(menuId)
break
}
}
})
if (cleanupClosed) cleanupListeners.push(cleanupClosed)
const cleanupCreated = window.api.tabs.on('tab-created', (tab: any) => {
for (const menu of menus) {
const targetHtml = getHtmlPath(menu.url)
if (tab.url.includes(targetHtml)) {
tabMap.set(menu.id, tab.id)
break
}
}
})
if (cleanupCreated) cleanupListeners.push(cleanupCreated)
try {
const tabs = await window.api.tabs.list()
if (tabs && tabs.length > 0) {
for (const tab of tabs) {
for (const menu of menus) {
const targetHtml = getHtmlPath(menu.url)
if (tab.url.includes(targetHtml)) {
tabMap.set(menu.id, tab.id)
break
}
}
}
}
} catch (e) {
console.error('Failed to sync tabs', e)
}
}
})
onUnmounted(() => {
cleanupListeners.forEach(fn => fn())
})
const handleClick = async (item: MenuItem) => { const handleClick = async (item: MenuItem) => {
console.log("🚀 ~ handleClick ~ item:", item) console.log("🚀 ~ handleClick ~ item:", item)
currentId.value = item.id currentId.value = item.id
router.push(item.url)
const existingTabId = tabMap.get(item.id)
if (existingTabId) {
await window.api.tabs.switch(existingTabId)
} else {
const htmlFile = getHtmlPath(item.url)
const targetUrl = new URL(htmlFile, window.location.href).href
try {
const tabInfo = await window.api.tabs.create(targetUrl)
if (tabInfo && tabInfo.id) {
tabMap.set(item.id, tabInfo.id)
await window.api.tabs.switch(tabInfo.id)
}
} catch (e) {
console.error('Failed to create tab', e)
}
}
} }
</script> </script>

View File

@@ -10,7 +10,7 @@ import locale from 'element-plus/es/locale/lang/zh-cn'
// 引入 i18n 插件 // 引入 i18n 插件
import i18n from './i18n' import i18n from './i18n'
// import './permission' import './permission'
// 样式文件隔离 // 样式文件隔离
import "./styles/index.css"; import "./styles/index.css";

View File

@@ -5,12 +5,36 @@ const routes = [
path: '/', path: '/',
redirect: '/home' redirect: '/home'
}, },
{
path: "/login",
component: () => import("@renderer/views/login/index.vue"),
name: "Login",
meta: { requiresAuth: true },
},
{ {
path: "/home", path: "/home",
component: () => import("@renderer/views/home/index.vue"), component: () => import("@renderer/views/home/index.vue"),
name: "Home", name: "Home",
meta: { requiresAuth: true }, 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({ const router = createRouter({

View File

@@ -127,6 +127,7 @@ const getVerifyCode = async () => {
onMounted(() => getVerifyCode()) onMounted(() => getVerifyCode())
const router = useRouter()
const formRef = ref() const formRef = ref()
const onSubmit = async () => { const onSubmit = async () => {
const valid = await formRef.value.validate().catch(() => { }); // 表单校验 const valid = await formRef.value.validate().catch(() => { }); // 表单校验
@@ -136,7 +137,7 @@ const onSubmit = async () => {
try { try {
userStore.login(form).then(() => { userStore.login(form).then(() => {
window.api.app.loadPage('index'); router.push({ path: '/home' })
}) })
} finally { } finally {
getVerifyCode() getVerifyCode()