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,11 +1,13 @@
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() { *
*/
function checkLoginStatus(): void {
const cookie = window.electronAPI.getSessionStorage('cookie'); const cookie = window.electronAPI.getSessionStorage('cookie');
if (!cookie) { if (!cookie) {
window.location.href = 'login.html'; window.location.href = 'login.html';
@ -19,39 +21,40 @@ function checkLoginStatus() {
} }
} }
// Handle logout /**
function handleLogout() { *
window.electronAPI.removeSessionStorage('cookie'); * @returns
window.location.href = 'login.html'; */
} async function getMenuListAsync(): Promise<MenuItem[]> {
// Get menu list
async function getMenuList(): Promise<MenuItem[]> {
try { try {
const response: ApiResponse<MenuItem[]> = await EIACDesktopApi.Menu.GetMenuAsync(); const response: ApiResponse<MenuItem[]> = await EIACDesktopApi.Menu.GetMenuAsync();
if (response.status === 0) { if (response.status === 0) {
const menuList: MenuItem[] = response.data; return response.data;
return menuList;
} else { } else {
throw new Error(response.msg || '获取菜单列表失败'); throw new Error(response.msg || '获取菜单列表失败');
} }
} catch (error) { } catch (error) {
console.error('获取菜单列表失败:', error); console.error(error);
throw error; throw error;
} }
} }
// Modify the createMenuItem function to handle new tab creation /**
*
* @param item
* @param menuList
* @returns
*/
function createMenuItem(item: MenuItem, menuList: MenuItem[]): HTMLLIElement { function createMenuItem(item: MenuItem, menuList: MenuItem[]): HTMLLIElement {
const li = document.createElement('li'); const li: HTMLLIElement = document.createElement('li');
li.className = 'menu-item'; li.className = 'menu-item';
const icon = document.createElement('img'); const icon: HTMLImageElement = document.createElement('img');
icon.src = item.IconConfig._1x.Default; icon.src = item.IconConfig._1x.Default;
icon.alt = item.ShowName; icon.alt = item.ShowName;
icon.className = 'menu-icon'; icon.className = 'menu-icon';
const span = document.createElement('span'); const span: HTMLSpanElement = document.createElement('span');
span.textContent = item.ShowName; span.textContent = item.ShowName;
li.appendChild(icon); li.appendChild(icon);
@ -76,27 +79,30 @@ function createMenuItem(item: MenuItem, menuList: MenuItem[]): HTMLLIElement {
icon.src = item.IconConfig._1x.Selected; icon.src = item.IconConfig._1x.Selected;
// Create new tab // Create new tab
await addTab(tabGroup, item); await addTabAsync(tabGroup, item);
}); });
} }
return li; return li;
} }
// Render menu /**
function renderMenu(menuList: MenuItem[]) { *
const menuContainer = document.getElementById('menuList'); * @param menuList
*/
function renderMenu(menuList: MenuItem[]): void {
const menuContainer: HTMLUListElement = document.getElementById('menuList') as HTMLUListElement;
if (!menuContainer) return; if (!menuContainer) return;
menuList.forEach(item => { menuList.forEach(item => {
const menuItem = createMenuItem(item, menuList); const menuItem: HTMLLIElement = createMenuItem(item, menuList);
menuContainer.appendChild(menuItem); menuContainer.appendChild(menuItem);
if (item.Children) { if (item.Children) {
const subMenu = document.createElement('ul'); const subMenu: HTMLUListElement = document.createElement('ul');
subMenu.className = 'submenu'; subMenu.className = 'submenu';
item.Children.forEach(child => { item.Children.forEach(child => {
const childItem = createMenuItem(child, menuList); const childItem: HTMLLIElement = createMenuItem(child, menuList);
subMenu.appendChild(childItem); subMenu.appendChild(childItem);
}); });
menuContainer.appendChild(subMenu); menuContainer.appendChild(subMenu);
@ -104,24 +110,22 @@ function renderMenu(menuList: MenuItem[]) {
}); });
} }
// Show error modal /**
function showErrorModal(message: string) { *
const errorModal = document.getElementById('errorModal') as HTMLDivElement; * @param tabGroup
const errorMessage = document.getElementById('errorMessage') as HTMLParagraphElement; * @param menuItem
errorMessage.textContent = message; * @returns
errorModal.style.display = 'block'; */
} async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab> {
const url: string = menuItem.Url.startsWith("http") ? menuItem.Url : `http://${menuItem.Url}`;
async function addTab(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab> { const result: { ok: boolean; status: number; message?: string } = await window.electronAPI.checkUrlAvailable(url);
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) { if (result.ok && result.status >= 200 && result.status < 400) {
console.log('✅ URL 可访问:', result.status); console.log('✅ URL 可访问:', result.status);
const cookies: string = window.electronAPI.getSessionStorage('cookie'); const cookies: string = window.electronAPI.getSessionStorage('cookie');
await window.electronAPI.setWebviewCookie(url, cookies); await window.electronAPI.setWebviewCookie(url, cookies);
} else { } else {
console.warn('❌ URL 不可访问:', result.error ?? `status ${result.status}`); console.warn('❌ URL 不可访问:', result.message ?? `status ${result.status}`);
showErrorModal(`无法访问 ${url}\r\n异常原因${result.error ?? `status ${result.status}`}\r\n请联系10000技术支持。`); showErrorModal(`无法访问 ${url}\r\n异常原因${result.message ?? `status ${result.status}`}\r\n请联系10000技术支持。`);
} }
const tab: Tab = tabGroup.addTab({ const tab: Tab = tabGroup.addTab({
@ -136,53 +140,106 @@ async function addTab(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab> {
'allowpopups': true, 'allowpopups': true,
}, },
ready: (tab: Tab) => { ready: (tab: Tab) => {
// 在加载完成后,获取标题 // 在webview加载完成后获取并设置标签页的标题和图标
tab.once('webview-dom-ready', (detail: string) => { const webview: Electron.WebviewTag = tab.webview as Electron.WebviewTag;
console.log('webview-dom-ready detail:', detail); listenWebviewTitleChange(webview, tab);
const webview = tab.webview as WebviewTag;
const title = webview.getTitle();
tab.setTitle(title);
console.log('webview-dom-ready title:', title);
});
} }
}); });
return tab; return tab;
} }
async function bindLogoClickEvent(tabGroup: TabGroup, menuItem: MenuItem) { /**
const logo = document.getElementById('logo') as HTMLImageElement; * 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 () => { logo.addEventListener('click', async () => {
console.log('logo clicked'); console.log('logo clicked');
const tab: Tab = await addTab(tabGroup, menuItem); const tab: Tab = await addTabAsync(tabGroup, menuItem);
tab.setPosition(0); tab.setPosition(0);
}); });
} }
// Modify the initialize function to create the first tab /**
async function initialize() { *
// Check login status */
checkLoginStatus(); function bindLogoutEvent(): void {
const logoutBtn: HTMLButtonElement = document.getElementById('btnLogout') as HTMLButtonElement;
try {
const menuList = await getMenuList();
renderMenu(menuList);
// Create initial tab
const firstMenuItem = menuList[0];
await addTab(tabGroup, firstMenuItem);
// Bind logout event
const logoutBtn = document.getElementById('btnLogout');
if (logoutBtn) { if (logoutBtn) {
logoutBtn.addEventListener('click', handleLogout); logoutBtn.addEventListener('click', () => {
window.electronAPI.removeSessionStorage('cookie');
window.location.href = 'login.html';
});
}
} }
const errorModal = document.getElementById('errorModal') as HTMLDivElement; /**
const closeErrorModal = document.getElementById('closeErrorModal') as HTMLButtonElement; *
*/
function bindCloseErrorModalEvent(): void {
const errorModal: HTMLDivElement = document.getElementById('errorModal') as HTMLDivElement;
const closeErrorModal: HTMLButtonElement = document.getElementById('closeErrorModal') as HTMLButtonElement;
// Close button click event // Close button click event
closeErrorModal.addEventListener('click', (event: Event) => { closeErrorModal.addEventListener('click', () => {
errorModal.style.display = 'none'; errorModal.style.display = 'none';
}); });
@ -192,10 +249,44 @@ async function initialize() {
errorModal.style.display = 'none'; errorModal.style.display = 'none';
} }
}); });
}
// Listen logo click event /**
const lastMenuItem = menuList[menuList.length - 1]; *
await bindLogoClickEvent(tabGroup, lastMenuItem); * @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) { } catch (error) {
console.error('初始化失败:', error); console.error('初始化失败:', error);
} }
@ -204,11 +295,14 @@ async function initialize() {
// 页面加载完成后初始化 // 页面加载完成后初始化
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));
const defaultFaviconUrl = new URL(url).origin + '/favicon.ico';
addTabAsync(tabGroup, {
Url: url, Url: url,
ShowName: '新标签页', ShowName: '新标签页',
IconConfig: { _1x: { Default: '', Selected: '' }, _2x: { Default: '', Selected: '' } }, IconConfig: { _1x: { Default: defaultFaviconUrl, Selected: defaultFaviconUrl }, _2x: { Default: defaultFaviconUrl, Selected: defaultFaviconUrl } },
Children: null 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