CSAPP/src/index.ts

451 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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