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 { 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<MenuItem[]> {
try {
const response: ApiResponse<MenuItem[]> = 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<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);
/**
*
* @returns
*/
async function getMenuListAsync(): Promise<MenuItem[]> {
try {
const response: ApiResponse<MenuItem[]> = 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<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);
// 监听新标签页打开事件
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
});
});

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 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' };
}
});

View File

@ -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