451 lines
15 KiB
TypeScript
451 lines
15 KiB
TypeScript
import { PageTitleUpdatedEvent } from 'electron';
|
||
import { TabGroup, Tab } from 'electron-tabs';
|
||
import { MenuItem, ApiResponse } from './EIAC_Desktop_Api';
|
||
|
||
// 保存最后一个无效URL的结果
|
||
let lastInvalidUrlResult: { url: string, message: string, status: number } | null = null;
|
||
const tabGroup: TabGroup = document.querySelector('tab-group') as TabGroup;
|
||
|
||
/**
|
||
* 检查登录状态
|
||
*/
|
||
function checkLoginStatus(): void {
|
||
const cookie = window.electronAPI.getSessionStorage('cookies');
|
||
if (!cookie) {
|
||
window.location.href = 'login.html';
|
||
return;
|
||
}
|
||
|
||
// Show user information
|
||
const userInfo = document.getElementById('userInfo');
|
||
if (userInfo) {
|
||
userInfo.textContent = '欢迎使用';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取菜单列表
|
||
* @returns 菜单列表
|
||
*/
|
||
async function getMenuListAsync(): Promise<MenuItem[]> {
|
||
try {
|
||
const response: ApiResponse<MenuItem[]> = await window.electronAPI.getMenuCacheAsync();
|
||
if (response.status === 0) {
|
||
return response.data;
|
||
} else {
|
||
throw new Error(response.msg || '获取菜单列表失败');
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建菜单项
|
||
* @param item 菜单项
|
||
* @param menuList 菜单列表
|
||
* @returns 菜单项
|
||
*/
|
||
function createMenuItem(item: MenuItem, menuList: MenuItem[]): HTMLLIElement {
|
||
const li: HTMLLIElement = document.createElement('li');
|
||
li.className = 'menu-item';
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 渲染菜单
|
||
* @param menuList 菜单列表
|
||
*/
|
||
function renderMenu(menuList: MenuItem[]): void {
|
||
const menuContainer: HTMLUListElement = document.getElementById('menuList') as HTMLUListElement;
|
||
if (!menuContainer) return;
|
||
|
||
menuList.forEach(item => {
|
||
const menuItem: HTMLLIElement = createMenuItem(item, menuList);
|
||
menuContainer.appendChild(menuItem);
|
||
|
||
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 菜单项
|
||
* @param allowCloseTab 是否允许关闭标签页
|
||
* @returns 标签页
|
||
*/
|
||
async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem, allowCloseTab: boolean = true): Promise<Tab | null> {
|
||
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 ${url} 可访问:`, result.status);
|
||
lastInvalidUrlResult = null;
|
||
|
||
const cookies: string = window.electronAPI.getSessionStorage('cookies');
|
||
await window.electronAPI.setWebviewCookie(url, cookies);
|
||
} else {
|
||
console.warn(`❌ URL ${url} 不可访问:`, result.message ?? `status ${result.status}`);
|
||
lastInvalidUrlResult = { url, message: result.message, status: result.status };
|
||
|
||
const helpDescrip: string = await window.electronAPI.getHelperDescripAsync(result.status.toString()) ?? `无法访问{URL}\r\n异常原因:${result.message ?? `status ${result.status}`}\r\n请联系技术支持。`;
|
||
showErrorModal(helpDescrip.replace('{URL}', url));
|
||
return null;
|
||
}
|
||
|
||
const zoomFactor: number = await window.electronAPI.getZoomFactorByUrl(url);
|
||
|
||
const tab: Tab = tabGroup.addTab({
|
||
active: true,
|
||
closable: allowCloseTab,
|
||
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);
|
||
|
||
// 监听 webview 的 DOM 加载完成事件
|
||
tab.once('webview-dom-ready', () => {
|
||
// 设置 webview 的缩放比例
|
||
const webview: Electron.WebviewTag = tab.webview as Electron.WebviewTag;
|
||
const defaultZoomFactor: number = webview.getZoomFactor();
|
||
console.log('Default zoom factor:', defaultZoomFactor);
|
||
|
||
if (defaultZoomFactor != zoomFactor) {
|
||
webview.setZoomFactor(zoomFactor);
|
||
console.log('Modify zoom factor:', zoomFactor);
|
||
} else {
|
||
console.log('Default zoom factor is the same as the zoom factor:', zoomFactor);
|
||
}
|
||
|
||
// 监听 webview 的关闭事件
|
||
webview.addEventListener('destroyed', (_event: Event) => {
|
||
console.log('Webview destroyed, closing tab:', tab.title);
|
||
tab.close(true);
|
||
});
|
||
|
||
// 监听 webview 的关闭事件(当页面调用 window.close() 时触发)
|
||
webview.addEventListener('close', (_event: Event) => {
|
||
console.log('Webview close event triggered, closing tab:', tab.title);
|
||
tab.close(true);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 绑定Help图标点击事件
|
||
*/
|
||
function bindHelpIconClickEvent(menuItem: MenuItem): void {
|
||
const helpIcon: HTMLImageElement = document.getElementById('helpIcon') as HTMLImageElement;
|
||
helpIcon.src = menuItem.IconConfig._1x.Default;
|
||
helpIcon.addEventListener('mouseenter', (event) => helpIcon.src = menuItem.IconConfig._1x.Selected);
|
||
helpIcon.addEventListener('mouseleave', () => helpIcon.src = menuItem.IconConfig._1x.Default);
|
||
helpIcon.addEventListener('click', async (event) => {
|
||
if (lastInvalidUrlResult) {
|
||
const helpDescrip: string = await window.electronAPI.getHelperDescripAsync(lastInvalidUrlResult.status.toString()) ?? `无法访问{URL}\r\n异常原因:${lastInvalidUrlResult.message ?? `status ${lastInvalidUrlResult.status}`}\r\n请联系技术支持。`;
|
||
showErrorModal(helpDescrip.replace('{URL}', lastInvalidUrlResult.url));
|
||
} else {
|
||
const tab: Tab | null = tabGroup.tabs.find(tab => (tab.webview as Electron.WebviewTag).getURL() === menuItem.Url);
|
||
if (tab) {
|
||
tab.activate();
|
||
} else {
|
||
await addTabAsync(tabGroup, menuItem);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 绑定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 | null = tabGroup.tabs.find(tab => (tab.webview as Electron.WebviewTag).getURL() === menuItem.Url);
|
||
if (tab) {
|
||
tab.activate();
|
||
} else {
|
||
const newTab: Tab | null = await addTabAsync(tabGroup, menuItem);
|
||
if (newTab) {
|
||
newTab.setPosition(0);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 绑定退出事件
|
||
*/
|
||
function bindExitEvent(): void {
|
||
const exitBtn: HTMLButtonElement = document.getElementById('btnExit') as HTMLButtonElement;
|
||
if (exitBtn) {
|
||
exitBtn.addEventListener('click', async () => {
|
||
window.electronAPI.removeSessionStorage('cookies');
|
||
await window.electronAPI.closeApp();
|
||
});
|
||
}
|
||
}
|
||
|
||
let closeCount: number = 0;
|
||
/**
|
||
* 绑定错误弹窗事件
|
||
*/
|
||
function bindErrorModalEvent(): void {
|
||
const errorModal: HTMLDivElement = document.getElementById('errorModal') as HTMLDivElement;
|
||
const closeErrorModal: HTMLButtonElement = document.getElementById('closeErrorModal') as HTMLButtonElement;
|
||
const faultReporting: HTMLButtonElement = document.getElementById('faultReporting') as HTMLButtonElement;
|
||
|
||
faultReporting.addEventListener('click', async () => await faultReportingAsync());
|
||
|
||
// Close button click event
|
||
closeErrorModal.addEventListener('click', () => {
|
||
errorModal.style.display = 'none';
|
||
closeCount++;
|
||
|
||
// 如果关闭次数大于等于2,则重置计数并且清空lastInvalidUrlResult
|
||
if (closeCount >= 2) {
|
||
closeCount = 0;
|
||
lastInvalidUrlResult = null;
|
||
}
|
||
});
|
||
|
||
// Click outside to close
|
||
window.addEventListener('click', (event: Event) => {
|
||
if (event.target === errorModal) {
|
||
errorModal.style.display = 'none';
|
||
closeCount++;
|
||
|
||
// 如果关闭次数大于等于2,则重置计数并且清空lastInvalidUrlResult
|
||
if (closeCount >= 2) {
|
||
closeCount = 0;
|
||
lastInvalidUrlResult = null;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 显示错误弹窗
|
||
* @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 faultReportingAsync(): Promise<void> {
|
||
if (lastInvalidUrlResult) {
|
||
const faultReportingBtn = document.getElementById('faultReporting') as HTMLButtonElement;
|
||
const originalText = faultReportingBtn.textContent;
|
||
|
||
try {
|
||
// 显示请求中状态
|
||
faultReportingBtn.disabled = true;
|
||
faultReportingBtn.textContent = '上报中...';
|
||
|
||
const result: { ok: boolean; status: number; message?: string } = await window.electronAPI.faultReporting(
|
||
lastInvalidUrlResult.url,
|
||
lastInvalidUrlResult.message,
|
||
lastInvalidUrlResult.status
|
||
);
|
||
|
||
// 显示请求结果
|
||
if (result.ok) {
|
||
lastInvalidUrlResult = null;
|
||
faultReportingBtn.textContent = '上报成功';
|
||
faultReportingBtn.style.backgroundColor = '#4CAF50';
|
||
setTimeout(() => {
|
||
faultReportingBtn.textContent = originalText;
|
||
faultReportingBtn.style.backgroundColor = '';
|
||
faultReportingBtn.disabled = false;
|
||
// 关闭错误弹窗
|
||
const errorModal = document.getElementById('errorModal') as HTMLDivElement;
|
||
errorModal.style.display = 'none';
|
||
}, 2000);
|
||
} else {
|
||
faultReportingBtn.textContent = '上报失败';
|
||
faultReportingBtn.style.backgroundColor = '#f44336';
|
||
setTimeout(() => {
|
||
faultReportingBtn.textContent = originalText;
|
||
faultReportingBtn.style.backgroundColor = '';
|
||
faultReportingBtn.disabled = false;
|
||
}, 2000);
|
||
}
|
||
} catch (error) {
|
||
console.error('故障上报异常:', error);
|
||
faultReportingBtn.textContent = '上报异常';
|
||
faultReportingBtn.style.backgroundColor = '#f44336';
|
||
setTimeout(() => {
|
||
faultReportingBtn.textContent = originalText;
|
||
faultReportingBtn.style.backgroundColor = '';
|
||
faultReportingBtn.disabled = false;
|
||
}, 2000);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化
|
||
*/
|
||
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, false);
|
||
|
||
// Bind help icon click event
|
||
const helpMenuItem: MenuItem = menuList[menuList.length - 2];
|
||
bindHelpIconClickEvent(helpMenuItem);
|
||
|
||
// Bind logo click event
|
||
const logoMenuItem: MenuItem = menuList[menuList.length - 1];
|
||
bindLogoClickEvent(tabGroup, logoMenuItem);
|
||
|
||
// Bind exit event
|
||
bindExitEvent();
|
||
|
||
// Bind error modal event
|
||
bindErrorModalEvent();
|
||
} catch (error) {
|
||
console.error('初始化失败:', error);
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', initialize);
|
||
|
||
// 监听新标签页打开事件
|
||
window.electronAPI.onOpenTab((webContentId: number, url: string) => {
|
||
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
|
||
});
|
||
}); |