diff --git a/src/index.ts b/src/index.ts index 2055ef5..8c8bb07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,214 +1,308 @@ -import { WebviewTag } from 'electron'; +import { PageTitleUpdatedEvent } from 'electron'; import { TabGroup, Tab } from 'electron-tabs'; import { MenuItem, ApiResponse, EIACDesktopApi } from './EIAC_Desktop_Api'; const tabGroup: TabGroup = document.querySelector('tab-group') as TabGroup; -// Check login status -function checkLoginStatus() { - const cookie = window.electronAPI.getSessionStorage('cookie'); - if (!cookie) { - window.location.href = 'login.html'; - return; - } - - // Show user information - const userInfo = document.getElementById('userInfo'); - if (userInfo) { - userInfo.textContent = '欢迎使用'; - } -} - -// Handle logout -function handleLogout() { - window.electronAPI.removeSessionStorage('cookie'); +/** + * 检查登录状态 + */ +function checkLoginStatus(): void { + const cookie = window.electronAPI.getSessionStorage('cookie'); + if (!cookie) { window.location.href = 'login.html'; + return; + } + + // Show user information + const userInfo = document.getElementById('userInfo'); + if (userInfo) { + userInfo.textContent = '欢迎使用'; + } } -// Get menu list -async function getMenuList(): Promise { - try { - const response: ApiResponse = await EIACDesktopApi.Menu.GetMenuAsync(); - if (response.status === 0) { - const menuList: MenuItem[] = response.data; - return menuList; - } else { - throw new Error(response.msg || '获取菜单列表失败'); - } - } catch (error) { - console.error('获取菜单列表失败:', error); - throw error; - } -} - -// Modify the createMenuItem function to handle new tab creation -function createMenuItem(item: MenuItem, menuList: MenuItem[]): HTMLLIElement { - const li = document.createElement('li'); - li.className = 'menu-item'; - - const icon = document.createElement('img'); - icon.src = item.IconConfig._1x.Default; - icon.alt = item.ShowName; - icon.className = 'menu-icon'; - - const span = document.createElement('span'); - span.textContent = item.ShowName; - - li.appendChild(icon); - li.appendChild(span); - - if (item.Url) { - li.addEventListener('click', async () => { - // Remove active state from other menu items - document.querySelectorAll('.menu-item').forEach(menuItem => { - menuItem.classList.remove('active'); - const menuIcon = menuItem.querySelector('.menu-icon') as HTMLImageElement; - if (menuIcon) { - const menuItemData = menuList.find((m: MenuItem) => m.ShowName === menuItem.querySelector('span')?.textContent); - if (menuItemData) { - menuIcon.src = menuItemData.IconConfig._1x.Default; - } - } - }); - - // Add active state to current menu item - li.classList.add('active'); - icon.src = item.IconConfig._1x.Selected; - - // Create new tab - await addTab(tabGroup, item); - }); - } - - return li; -} - -// Render menu -function renderMenu(menuList: MenuItem[]) { - const menuContainer = document.getElementById('menuList'); - if (!menuContainer) return; - - menuList.forEach(item => { - const menuItem = createMenuItem(item, menuList); - menuContainer.appendChild(menuItem); - - if (item.Children) { - const subMenu = document.createElement('ul'); - subMenu.className = 'submenu'; - item.Children.forEach(child => { - const childItem = createMenuItem(child, menuList); - subMenu.appendChild(childItem); - }); - menuContainer.appendChild(subMenu); - } - }); -} - -// Show error modal -function showErrorModal(message: string) { - const errorModal = document.getElementById('errorModal') as HTMLDivElement; - const errorMessage = document.getElementById('errorMessage') as HTMLParagraphElement; - errorMessage.textContent = message; - errorModal.style.display = 'block'; -} - -async function addTab(tabGroup: TabGroup, menuItem: MenuItem): Promise { - const url = menuItem.Url.startsWith("http") ? menuItem.Url : `http://${menuItem.Url}`; - const result = await window.electronAPI.checkUrlAvailable(url); - if (result.ok && result.status >= 200 && result.status < 400) { - console.log('✅ URL 可访问:', result.status); - const cookies: string = window.electronAPI.getSessionStorage('cookie'); - await window.electronAPI.setWebviewCookie(url, cookies); +/** + * 获取菜单列表 + * @returns 菜单列表 + */ +async function getMenuListAsync(): Promise { + try { + const response: ApiResponse = await EIACDesktopApi.Menu.GetMenuAsync(); + if (response.status === 0) { + return response.data; } else { - console.warn('❌ URL 不可访问:', result.error ?? `status ${result.status}`); - showErrorModal(`无法访问 ${url}\r\n异常原因:${result.error ?? `status ${result.status}`}\r\n请联系10000技术支持。`); + throw new Error(response.msg || '获取菜单列表失败'); } - - const tab: Tab = tabGroup.addTab({ - active: true, - closable: true, - title: menuItem.ShowName, - src: url, - iconURL: menuItem.IconConfig._1x.Default, - webviewAttributes: { - 'webpreferences': 'contextIsolation=yes, nodeIntegration=no', - 'autosize': 'on', - 'allowpopups': true, - }, - ready: (tab: Tab) => { - // 在加载完成后,获取标题 - tab.once('webview-dom-ready', (detail: string) => { - console.log('webview-dom-ready detail:', detail); - const webview = tab.webview as WebviewTag; - const title = webview.getTitle(); - tab.setTitle(title); - console.log('webview-dom-ready title:', title); - }); - } - }); - - return tab; + } catch (error) { + console.error(error); + throw error; + } } -async function bindLogoClickEvent(tabGroup: TabGroup, menuItem: MenuItem) { - const logo = document.getElementById('logo') as HTMLImageElement; - logo.addEventListener('click', async () => { - console.log('logo clicked'); +/** + * 创建菜单项 + * @param item 菜单项 + * @param menuList 菜单列表 + * @returns 菜单项 + */ +function createMenuItem(item: MenuItem, menuList: MenuItem[]): HTMLLIElement { + const li: HTMLLIElement = document.createElement('li'); + li.className = 'menu-item'; - const tab: Tab = await addTab(tabGroup, menuItem); - tab.setPosition(0); + const icon: HTMLImageElement = document.createElement('img'); + icon.src = item.IconConfig._1x.Default; + icon.alt = item.ShowName; + icon.className = 'menu-icon'; + + const span: HTMLSpanElement = document.createElement('span'); + span.textContent = item.ShowName; + + li.appendChild(icon); + li.appendChild(span); + + if (item.Url) { + li.addEventListener('click', async () => { + // Remove active state from other menu items + document.querySelectorAll('.menu-item').forEach(menuItem => { + menuItem.classList.remove('active'); + const menuIcon = menuItem.querySelector('.menu-icon') as HTMLImageElement; + if (menuIcon) { + const menuItemData = menuList.find((m: MenuItem) => m.ShowName === menuItem.querySelector('span')?.textContent); + if (menuItemData) { + menuIcon.src = menuItemData.IconConfig._1x.Default; + } + } + }); + + // Add active state to current menu item + li.classList.add('active'); + icon.src = item.IconConfig._1x.Selected; + + // Create new tab + await addTabAsync(tabGroup, item); }); + } + + return li; } -// Modify the initialize function to create the first tab -async function initialize() { - // Check login status - checkLoginStatus(); +/** + * 渲染菜单 + * @param menuList 菜单列表 + */ +function renderMenu(menuList: MenuItem[]): void { + const menuContainer: HTMLUListElement = document.getElementById('menuList') as HTMLUListElement; + if (!menuContainer) return; - try { - const menuList = await getMenuList(); - renderMenu(menuList); + menuList.forEach(item => { + const menuItem: HTMLLIElement = createMenuItem(item, menuList); + menuContainer.appendChild(menuItem); - // Create initial tab - const firstMenuItem = menuList[0]; - await addTab(tabGroup, firstMenuItem); - - // Bind logout event - const logoutBtn = document.getElementById('btnLogout'); - if (logoutBtn) { - logoutBtn.addEventListener('click', handleLogout); - } - - const errorModal = document.getElementById('errorModal') as HTMLDivElement; - const closeErrorModal = document.getElementById('closeErrorModal') as HTMLButtonElement; - // Close button click event - closeErrorModal.addEventListener('click', (event: Event) => { - errorModal.style.display = 'none'; - }); - - // Click outside to close - window.addEventListener('click', (event: Event) => { - if (event.target === errorModal) { - errorModal.style.display = 'none'; - } - }); - - // Listen logo click event - const lastMenuItem = menuList[menuList.length - 1]; - await bindLogoClickEvent(tabGroup, lastMenuItem); - } catch (error) { - console.error('初始化失败:', error); + if (item.Children) { + const subMenu: HTMLUListElement = document.createElement('ul'); + subMenu.className = 'submenu'; + item.Children.forEach(child => { + const childItem: HTMLLIElement = createMenuItem(child, menuList); + subMenu.appendChild(childItem); + }); + menuContainer.appendChild(subMenu); } + }); +} + +/** + * 添加标签页 + * @param tabGroup 标签组 + * @param menuItem 菜单项 + * @returns 标签页 + */ +async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise { + const url: string = menuItem.Url.startsWith("http") ? menuItem.Url : `http://${menuItem.Url}`; + const result: { ok: boolean; status: number; message?: string } = await window.electronAPI.checkUrlAvailable(url); + if (result.ok && result.status >= 200 && result.status < 400) { + console.log('✅ URL 可访问:', result.status); + const cookies: string = window.electronAPI.getSessionStorage('cookie'); + await window.electronAPI.setWebviewCookie(url, cookies); + } else { + console.warn('❌ URL 不可访问:', result.message ?? `status ${result.status}`); + showErrorModal(`无法访问 ${url}\r\n异常原因:${result.message ?? `status ${result.status}`}\r\n请联系10000技术支持。`); + } + + const tab: Tab = tabGroup.addTab({ + active: true, + closable: true, + title: menuItem.ShowName, + src: url, + iconURL: menuItem.IconConfig._1x.Default, + webviewAttributes: { + 'webpreferences': 'contextIsolation=yes, nodeIntegration=no', + 'autosize': 'on', + 'allowpopups': true, + }, + ready: (tab: Tab) => { + // 在webview加载完成后,获取并设置标签页的标题和图标 + const webview: Electron.WebviewTag = tab.webview as Electron.WebviewTag; + listenWebviewTitleChange(webview, tab); + } + }); + + return tab; +} + +/** + * 监听webview的标题变化,并更新标签页的标题和图标 + * @param webview webview + * @param tab 标签页 + */ +function listenWebviewTitleChange(webview: Electron.WebviewTag, tab: Tab): void { + // 在webview加载完成后,获取并设置标签页的标题和图标 + webview.addEventListener('did-finish-load', async () => { + const title: string = webview.getTitle(); + tab.setTitle(title); + console.log('did-finish-load title:', title); + + const faviconUrl: string = await getFaviconUrl(webview); + console.log('did-finish-load iconUrl:', faviconUrl); + if (faviconUrl) { + tab.setIcon(faviconUrl, null); + } + }); + + // 监听webview的标题变化,并更新标签页的标题和图标 + webview.addEventListener('page-title-updated', async (event: PageTitleUpdatedEvent) => { + console.log('title-changed title:', event.title); + tab.setTitle(event.title); + + const faviconUrl: string = await getFaviconUrl(webview); + console.log('title-changed iconUrl:', faviconUrl); + if (faviconUrl) { + tab.setIcon(faviconUrl, null); + } + }); +} + +/** + * 获取图标URL + * @param webview webview + * @returns 图标URL + */ +async function getFaviconUrl(webview: Electron.WebviewTag): Promise { + const iconUrl: string | null = await webview.executeJavaScript(` + (() => { + const relList = ['icon', 'shortcut icon', 'apple-touch-icon']; + const links = Array.from(document.getElementsByTagName('link')); + const iconLink = links.find(link => relList.includes(link.rel)); + return iconLink ? iconLink.href : null; + })(); + `); + + if (iconUrl) { + return iconUrl; + } + + const url: string = webview.getURL(); + const defaultFaviconUrl: string = new URL(url).origin + '/favicon.ico'; + return defaultFaviconUrl; +} + +/** + * 绑定logo点击事件 + */ +function bindLogoClickEvent(tabGroup: TabGroup, menuItem: MenuItem): void { + const logo: HTMLImageElement = document.getElementById('logo') as HTMLImageElement; + logo.addEventListener('click', async () => { + console.log('logo clicked'); + + const tab: Tab = await addTabAsync(tabGroup, menuItem); + tab.setPosition(0); + }); +} + +/** + * 绑定登出事件 + */ +function bindLogoutEvent(): void { + const logoutBtn: HTMLButtonElement = document.getElementById('btnLogout') as HTMLButtonElement; + if (logoutBtn) { + logoutBtn.addEventListener('click', () => { + window.electronAPI.removeSessionStorage('cookie'); + window.location.href = 'login.html'; + }); + } +} + +/** + * 绑定关闭错误弹窗事件 + */ +function bindCloseErrorModalEvent(): void { + const errorModal: HTMLDivElement = document.getElementById('errorModal') as HTMLDivElement; + const closeErrorModal: HTMLButtonElement = document.getElementById('closeErrorModal') as HTMLButtonElement; + + // Close button click event + closeErrorModal.addEventListener('click', () => { + errorModal.style.display = 'none'; + }); + + // Click outside to close + window.addEventListener('click', (event: Event) => { + if (event.target === errorModal) { + errorModal.style.display = 'none'; + } + }); +} + +/** + * 显示错误弹窗 + * @param message 错误消息 + */ +function showErrorModal(message: string): void { + const errorModal: HTMLDivElement = document.getElementById('errorModal') as HTMLDivElement; + const errorMessage: HTMLParagraphElement = document.getElementById('errorMessage') as HTMLParagraphElement; + errorMessage.textContent = message; + errorModal.style.display = 'block'; +} + +/** + * 初始化 + */ +async function initialize(): Promise { + // Check login status + checkLoginStatus(); + + try { + const menuList: MenuItem[] = await getMenuListAsync(); + renderMenu(menuList); + + // Create initial tab + const firstMenuItem: MenuItem = menuList[0]; + await addTabAsync(tabGroup, firstMenuItem); + + // Bind logo click event + const lastMenuItem: MenuItem = menuList[menuList.length - 1]; + const logoMenuItem: MenuItem = lastMenuItem.Children[lastMenuItem.Children.length - 1]; + bindLogoClickEvent(tabGroup, logoMenuItem); + + // Bind logout event + bindLogoutEvent(); + + // Bind close error modal event + bindCloseErrorModalEvent(); + } catch (error) { + console.error('初始化失败:', error); + } } // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', initialize); +// 监听新标签页打开事件 window.electronAPI.onOpenTab((webContentId: number, url: string) => { - addTab(tabGroup, { - Url: url, - ShowName: '新标签页', - IconConfig: { _1x: { Default: '', Selected: '' }, _2x: { Default: '', Selected: '' } }, - Children: null - }); + console.log(new URL(url)); + const defaultFaviconUrl = new URL(url).origin + '/favicon.ico'; + addTabAsync(tabGroup, { + Url: url, + ShowName: '新标签页', + IconConfig: { _1x: { Default: defaultFaviconUrl, Selected: defaultFaviconUrl }, _2x: { Default: defaultFaviconUrl, Selected: defaultFaviconUrl } }, + Children: null + }); }); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 4a45ec0..107348f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,13 @@ -import { app, BrowserWindow, globalShortcut, ipcMain, Menu, Tray, session, screen } from 'electron'; +import { app, BrowserWindow, globalShortcut, ipcMain, Menu, session, screen, Tray } from 'electron'; import path from 'node:path'; import started from 'electron-squirrel-startup'; -import * as http from 'http'; -import * as https from 'https'; -import { URL } from 'url'; +import http from 'http'; +import https from 'https'; -const isDevelopment = process.env.NODE_ENV === 'development'; +const isDevelopment: boolean = process.env.NODE_ENV === 'development'; // Ensure only one instance is running -const gotTheLock = app.requestSingleInstanceLock(); +const gotTheLock: boolean = app.requestSingleInstanceLock(); if (!gotTheLock) { app.exit(0); // 使用 exit 而不是 quit,确保立即退出 } else { @@ -69,10 +68,10 @@ ipcMain.handle('check-url-available', async (event, rawUrl: string) => { getReq.destroy() }); getReq.on('error', (err) => { - resolve({ ok: false, error: err.message }); + resolve({ ok: false, status: -1, message: err.message }); }); getReq.on('timeout', () => { - resolve({ ok: false, error: 'Timeout' }); + resolve({ ok: false, status: -1, message: 'GET Timeout' }); getReq.destroy(); }); } else { @@ -83,18 +82,18 @@ ipcMain.handle('check-url-available', async (event, rawUrl: string) => { ); headReq.on('error', (err) => { - resolve({ ok: false, error: err.message }); + resolve({ ok: false, status: -1, message: err.message }); }); headReq.on('timeout', () => { - resolve({ ok: false, error: 'Timeout' }); + resolve({ ok: false, status: -1, message: 'HEAD Timeout' }); headReq.destroy(); }); headReq.end(); }) } catch (e) { - return { ok: false, error: 'Invalid URL' }; + return { ok: false, status: -1, message: 'Invalid URL' }; } }); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index e9947ce..9206fae 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -14,7 +14,7 @@ export interface ElectronAPI { * @param url 要检查的URL * @returns 是否可用 */ - checkUrlAvailable: (url: string) => Promise<{ ok: boolean; status: number; error?: string }>; + checkUrlAvailable: (url: string) => Promise<{ ok: boolean; status: number; message?: string }>; /** * 设置webview的cookie * @param url 要设置cookie的URL