From 50c7282ff2b3095a483067f9b5a813673639ebdd Mon Sep 17 00:00:00 2001 From: DEV_DSW <562304744@qq.com> Date: Thu, 25 Dec 2025 08:47:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- global.d.ts | 36 ++++++--- html/home.html | 16 ++++ src/main/service/tab-manager/index.ts | 90 +++++++++++++++++++-- src/main/wins/main.ts | 33 ++++++++ src/renderer/components/SideMenus/index.vue | 75 +++++++++++++++-- src/renderer/constant/menus.ts | 2 +- src/renderer/views/home/HomeTab.vue | 8 ++ src/renderer/views/task/index.vue | 8 +- vite.renderer.config.ts | 1 + 9 files changed, 236 insertions(+), 33 deletions(-) create mode 100644 html/home.html create mode 100644 src/renderer/views/home/HomeTab.vue diff --git a/global.d.ts b/global.d.ts index 1948951..5da4f5d 100644 --- a/global.d.ts +++ b/global.d.ts @@ -42,6 +42,16 @@ declare global { } } + type TabId = string + interface TabInfo { + id: TabId + url: string + title: string + isLoading: boolean + canGoBack: boolean + canGoForward: boolean + } + // 定义IPC API 接口 interface WindowApi { invoke(channel: T, ...args: IPCTypings[T]['params']): IPCTypings[T]['return'], @@ -51,7 +61,7 @@ declare global { getCurrentWindowId(): number, versions: NodeJS.ProcessVersions, external: { - open: (url: string) => void + open: (url: string) => Promise }, minimizeWindow: () => void, maximizeWindow: () => void, @@ -60,19 +70,19 @@ declare global { isWindowMaximized: () => Promise, viewIsReady: () => void app: { - setFrameless: (route?: string) => void, - loadPage: (page: string) => void + setFrameless: (route?: string) => Promise, + loadPage: (page: string) => Promise }, tabs: { - create: (url?: string) => void, - list: () => void, - navigate: (tabId: string, url: string) => void, - reload: (tabId: string) => void, - back: (tabId: string) => void, - forward: (tabId: string) => void, - switch: (tabId: string) => void, - close: (tabId: string) => void, - on: (event: 'tab-updated' | 'tab-created' | 'tab-closed' | 'tab-switched', handler: (payload: any) => void) => void + 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: { @@ -83,7 +93,7 @@ declare global { }, } - declare interface Window { + interface Window { api: WindowApi; } diff --git a/html/home.html b/html/home.html new file mode 100644 index 0000000..1eb750b --- /dev/null +++ b/html/home.html @@ -0,0 +1,16 @@ + + + + + NIANXX + + + + +
+ + + \ No newline at end of file diff --git a/src/main/service/tab-manager/index.ts b/src/main/service/tab-manager/index.ts index 8bcbad5..374f485 100644 --- a/src/main/service/tab-manager/index.ts +++ b/src/main/service/tab-manager/index.ts @@ -1,5 +1,7 @@ -import { BrowserView, BrowserWindow } from 'electron' +import { BrowserView, BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron' import { randomUUID } from 'crypto' +import { IPC_EVENTS } from '@common/constants' +import path from 'node:path' type TabId = string type TabInfo = { id: TabId; url: string; title: string; isLoading: boolean; canGoBack: boolean; canGoForward: boolean } @@ -16,6 +18,42 @@ export class TabManager { constructor(win: BrowserWindow) { this.win = win this.win.on('resize', () => this.updateActiveBounds()) + this._setupIpcEvents() + } + + private _setupIpcEvents() { + ipcMain.handle(IPC_EVENTS.TAB_CREATE, (_e, url?: string) => { + const info = this.create(url) + return info + }) + + ipcMain.handle(IPC_EVENTS.TAB_LIST, () => { + return this.list() + }) + + ipcMain.handle(IPC_EVENTS.TAB_NAVIGATE, (_e, { tabId, url }: { tabId: TabId; url: string }) => { + this.navigate(tabId, url) + }) + + ipcMain.handle(IPC_EVENTS.TAB_RELOAD, (_e, tabId: TabId) => { + this.reload(tabId) + }) + + ipcMain.handle(IPC_EVENTS.TAB_BACK, (_e, tabId: TabId) => { + this.goBack(tabId) + }) + + ipcMain.handle(IPC_EVENTS.TAB_FORWARD, (_e, tabId: TabId) => { + this.goForward(tabId) + }) + + ipcMain.handle(IPC_EVENTS.TAB_SWITCH, (_e, tabId: TabId) => { + this.switch(tabId) + }) + + ipcMain.handle(IPC_EVENTS.TAB_CLOSE, (_e, tabId: TabId) => { + this.close(tabId) + }) } enable() { @@ -30,15 +68,40 @@ export class TabManager { if (view) this.win.removeBrowserView(view) } + destroy() { + this.disable() + this.views.forEach(view => { + // @ts-ignore + view.webContents.destroy() + }) + this.views.clear() + + ipcMain.removeHandler(IPC_EVENTS.TAB_CREATE) + ipcMain.removeHandler(IPC_EVENTS.TAB_LIST) + ipcMain.removeHandler(IPC_EVENTS.TAB_NAVIGATE) + ipcMain.removeHandler(IPC_EVENTS.TAB_RELOAD) + ipcMain.removeHandler(IPC_EVENTS.TAB_BACK) + ipcMain.removeHandler(IPC_EVENTS.TAB_FORWARD) + ipcMain.removeHandler(IPC_EVENTS.TAB_SWITCH) + ipcMain.removeHandler(IPC_EVENTS.TAB_CLOSE) + } + list(): TabInfo[] { return Array.from(this.views.entries()).map(([id, view]) => this.info(id, view)) } - create(url?: string): TabInfo { + create(url?: string, active = true): TabInfo { const id = randomUUID() - const view = new BrowserView({ webPreferences: { sandbox: true } }) + const view = new BrowserView({ + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + preload: path.join(__dirname, 'preload.js'), + }, + }) this.views.set(id, view) - if (this.enabled) this.attach(id) + if (this.enabled && active) this.attach(id) const target = url && url.length > 0 ? url : 'about:blank' view.webContents.loadURL(target) this.bindEvents(id, view) @@ -110,8 +173,23 @@ export class TabManager { if (!this.enabled || !this.activeId) return const view = this.views.get(this.activeId) if (!view) return - const [width, height] = this.win.getContentSize() - view.setBounds({ x: 0, y: UI_HEIGHT, width, height: Math.max(0, height - UI_HEIGHT) }) + const [winWidth, winHeight] = this.win.getContentSize() + + const HEADER_HEIGHT = 88 + const PADDING = 8 + const RIGHT_PANEL_WIDTH = 392 + 80 + 8 // TaskList + SideMenu + Gap + + const x = PADDING + const y = HEADER_HEIGHT + PADDING + const width = winWidth - RIGHT_PANEL_WIDTH - PADDING + const height = winHeight - HEADER_HEIGHT - (PADDING * 2) + + view.setBounds({ + x, + y, + width: Math.max(0, width), + height: Math.max(0, height) + }) } private bindEvents(id: TabId, view: BrowserView): void { diff --git a/src/main/wins/main.ts b/src/main/wins/main.ts index a7f2b84..5928bff 100644 --- a/src/main/wins/main.ts +++ b/src/main/wins/main.ts @@ -7,6 +7,8 @@ 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) { @@ -107,6 +109,37 @@ export function setupMainWindow() { }); 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() + if (url.includes('/html/index.html') || url.endsWith('index.html')) { + initTabs() + } + }) }); windowManager.create(WINDOW_NAMES.MAIN, MAIN_WIN_SIZE); diff --git a/src/renderer/components/SideMenus/index.vue b/src/renderer/components/SideMenus/index.vue index bc37197..d725bf6 100644 --- a/src/renderer/components/SideMenus/index.vue +++ b/src/renderer/components/SideMenus/index.vue @@ -21,17 +21,80 @@ diff --git a/src/renderer/constant/menus.ts b/src/renderer/constant/menus.ts index f6be361..111e94e 100644 --- a/src/renderer/constant/menus.ts +++ b/src/renderer/constant/menus.ts @@ -33,7 +33,7 @@ export const menus: MenuItem[] = [ icon: RiApps2AiLine, color: '#525866', activeColor: '#2B7FFF', - url: '/stock', + url: '/task', }, { id: 4, diff --git a/src/renderer/views/home/HomeTab.vue b/src/renderer/views/home/HomeTab.vue new file mode 100644 index 0000000..a1a4f82 --- /dev/null +++ b/src/renderer/views/home/HomeTab.vue @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/src/renderer/views/task/index.vue b/src/renderer/views/task/index.vue index 785fc87..ffce193 100644 --- a/src/renderer/views/task/index.vue +++ b/src/renderer/views/task/index.vue @@ -1,12 +1,6 @@ diff --git a/vite.renderer.config.ts b/vite.renderer.config.ts index 3448754..fd42ba1 100644 --- a/vite.renderer.config.ts +++ b/vite.renderer.config.ts @@ -26,6 +26,7 @@ export default defineConfig(async () => { resolve(__dirname, 'html/dialog.html'), resolve(__dirname, 'html/setting.html'), resolve(__dirname, 'html/login.html'), + resolve(__dirname, 'html/home.html'), resolve(__dirname, 'html/task.html'), resolve(__dirname, 'html/knowledge.html'), ]