feat: 项目多标签方案调整
This commit is contained in:
11
global.d.ts
vendored
11
global.d.ts
vendored
@@ -73,17 +73,6 @@ declare global {
|
||||
setFrameless: (route?: 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}>,
|
||||
logger: {
|
||||
debug: (message: string, ...meta?: any[]) => void;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
162
src/main/wins/index.ts
Normal file
162
src/main/wins/index.ts
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -21,107 +21,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { menus, type MenuItem } from '@constant/menus'
|
||||
|
||||
const currentId = ref(1)
|
||||
const tabMap = new Map<number, string>()
|
||||
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 router = useRouter()
|
||||
|
||||
const handleClick = async (item: MenuItem) => {
|
||||
console.log("🚀 ~ handleClick ~ item:", item)
|
||||
currentId.value = item.id
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
router.push(item.url)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user