Tab页增强,捕获webview的网页标题和图标

This commit is contained in:
Allen 2025-05-17 21:13:07 +08:00
parent ba77eaae59
commit aad96a0866
3 changed files with 293 additions and 200 deletions

View File

@ -1,214 +1,308 @@
import { WebviewTag } from 'electron'; import { PageTitleUpdatedEvent } from 'electron';
import { TabGroup, Tab } from 'electron-tabs'; import { TabGroup, Tab } from 'electron-tabs';
import { MenuItem, ApiResponse, EIACDesktopApi } from './EIAC_Desktop_Api'; import { MenuItem, ApiResponse, EIACDesktopApi } from './EIAC_Desktop_Api';
const tabGroup: TabGroup = document.querySelector('tab-group') as TabGroup; const tabGroup: TabGroup = document.querySelector('tab-group') as TabGroup;
// Check login status /**
function checkLoginStatus() { *
const cookie = window.electronAPI.getSessionStorage('cookie'); */
if (!cookie) { function checkLoginStatus(): void {
window.location.href = 'login.html'; const cookie = window.electronAPI.getSessionStorage('cookie');
return; if (!cookie) {
}
// Show user information
const userInfo = document.getElementById('userInfo');
if (userInfo) {
userInfo.textContent = '欢迎使用';
}
}
// Handle logout
function handleLogout() {
window.electronAPI.removeSessionStorage('cookie');
window.location.href = 'login.html'; 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<MenuItem[]> { *
try { * @returns
const response: ApiResponse<MenuItem[]> = await EIACDesktopApi.Menu.GetMenuAsync(); */
if (response.status === 0) { async function getMenuListAsync(): Promise<MenuItem[]> {
const menuList: MenuItem[] = response.data; try {
return menuList; const response: ApiResponse<MenuItem[]> = await EIACDesktopApi.Menu.GetMenuAsync();
} else { if (response.status === 0) {
throw new Error(response.msg || '获取菜单列表失败'); return response.data;
}
} 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<Tab> {
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);
} else { } else {
console.warn('❌ URL 不可访问:', result.error ?? `status ${result.status}`); throw new Error(response.msg || '获取菜单列表失败');
showErrorModal(`无法访问 ${url}\r\n异常原因${result.error ?? `status ${result.status}`}\r\n请联系10000技术支持。`);
} }
} catch (error) {
const tab: Tab = tabGroup.addTab({ console.error(error);
active: true, throw error;
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;
} }
async function bindLogoClickEvent(tabGroup: TabGroup, menuItem: MenuItem) { /**
const logo = document.getElementById('logo') as HTMLImageElement; *
logo.addEventListener('click', async () => { * @param item
console.log('logo clicked'); * @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); const icon: HTMLImageElement = document.createElement('img');
tab.setPosition(0); 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 * @param menuList
checkLoginStatus(); */
function renderMenu(menuList: MenuItem[]): void {
const menuContainer: HTMLUListElement = document.getElementById('menuList') as HTMLUListElement;
if (!menuContainer) return;
try { menuList.forEach(item => {
const menuList = await getMenuList(); const menuItem: HTMLLIElement = createMenuItem(item, menuList);
renderMenu(menuList); menuContainer.appendChild(menuItem);
// Create initial tab if (item.Children) {
const firstMenuItem = menuList[0]; const subMenu: HTMLUListElement = document.createElement('ul');
await addTab(tabGroup, firstMenuItem); subMenu.className = 'submenu';
item.Children.forEach(child => {
// Bind logout event const childItem: HTMLLIElement = createMenuItem(child, menuList);
const logoutBtn = document.getElementById('btnLogout'); subMenu.appendChild(childItem);
if (logoutBtn) { });
logoutBtn.addEventListener('click', handleLogout); menuContainer.appendChild(subMenu);
}
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);
} }
});
}
/**
*
* @param tabGroup
* @param menuItem
* @returns
*/
async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab> {
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<string> {
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<void> {
// 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); document.addEventListener('DOMContentLoaded', initialize);
// 监听新标签页打开事件
window.electronAPI.onOpenTab((webContentId: number, url: string) => { window.electronAPI.onOpenTab((webContentId: number, url: string) => {
addTab(tabGroup, { console.log(new URL(url));
Url: url, const defaultFaviconUrl = new URL(url).origin + '/favicon.ico';
ShowName: '新标签页', addTabAsync(tabGroup, {
IconConfig: { _1x: { Default: '', Selected: '' }, _2x: { Default: '', Selected: '' } }, Url: url,
Children: null ShowName: '新标签页',
}); IconConfig: { _1x: { Default: defaultFaviconUrl, Selected: defaultFaviconUrl }, _2x: { Default: defaultFaviconUrl, Selected: defaultFaviconUrl } },
Children: null
});
}); });

View File

@ -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 path from 'node:path';
import started from 'electron-squirrel-startup'; import started from 'electron-squirrel-startup';
import * as http from 'http'; import http from 'http';
import * as https from 'https'; import https from 'https';
import { URL } from 'url';
const isDevelopment = process.env.NODE_ENV === 'development'; const isDevelopment: boolean = process.env.NODE_ENV === 'development';
// Ensure only one instance is running // Ensure only one instance is running
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock: boolean = app.requestSingleInstanceLock();
if (!gotTheLock) { if (!gotTheLock) {
app.exit(0); // 使用 exit 而不是 quit确保立即退出 app.exit(0); // 使用 exit 而不是 quit确保立即退出
} else { } else {
@ -69,10 +68,10 @@ ipcMain.handle('check-url-available', async (event, rawUrl: string) => {
getReq.destroy() getReq.destroy()
}); });
getReq.on('error', (err) => { getReq.on('error', (err) => {
resolve({ ok: false, error: err.message }); resolve({ ok: false, status: -1, message: err.message });
}); });
getReq.on('timeout', () => { getReq.on('timeout', () => {
resolve({ ok: false, error: 'Timeout' }); resolve({ ok: false, status: -1, message: 'GET Timeout' });
getReq.destroy(); getReq.destroy();
}); });
} else { } else {
@ -83,18 +82,18 @@ ipcMain.handle('check-url-available', async (event, rawUrl: string) => {
); );
headReq.on('error', (err) => { headReq.on('error', (err) => {
resolve({ ok: false, error: err.message }); resolve({ ok: false, status: -1, message: err.message });
}); });
headReq.on('timeout', () => { headReq.on('timeout', () => {
resolve({ ok: false, error: 'Timeout' }); resolve({ ok: false, status: -1, message: 'HEAD Timeout' });
headReq.destroy(); headReq.destroy();
}); });
headReq.end(); headReq.end();
}) })
} catch (e) { } catch (e) {
return { ok: false, error: 'Invalid URL' }; return { ok: false, status: -1, message: 'Invalid URL' };
} }
}); });

View File

@ -14,7 +14,7 @@ export interface ElectronAPI {
* @param url URL * @param url URL
* @returns * @returns
*/ */
checkUrlAvailable: (url: string) => Promise<{ ok: boolean; status: number; error?: string }>; checkUrlAvailable: (url: string) => Promise<{ ok: boolean; status: number; message?: string }>;
/** /**
* webview的cookie * webview的cookie
* @param url cookie的URL * @param url cookie的URL