Compare commits
10 Commits
89ee08789d
...
fdc008f33c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fdc008f33c | ||
![]() |
0f30e712ed | ||
![]() |
0b1148e6ea | ||
![]() |
46285fb5a2 | ||
![]() |
dcdacf0694 | ||
![]() |
dc6386f671 | ||
![]() |
7da3a62852 | ||
![]() |
3c23ea4c74 | ||
![]() |
032f3eefd0 | ||
![]() |
d1f0d2e719 |
113
README.md
Normal file
113
README.md
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# EIAC Desktop Application
|
||||||
|
|
||||||
|
一个基于 Electron 框架的跨平台桌面应用程序,用于访问和管理企业内部应用系统。
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
EIAC Desktop Application 是一个基于 Electron 框架的桌面应用程序。
|
||||||
|
|
||||||
|
主要功能包括:
|
||||||
|
|
||||||
|
- 🏢 统一的企业应用访问入口
|
||||||
|
- 📑 多标签页管理
|
||||||
|
- 🔍 自定义页面缩放
|
||||||
|
- 🔐 统一的用户认证
|
||||||
|
- 🐛 故障上报功能
|
||||||
|
- 🖥️ 跨平台支持(Windows、macOS、Linux)
|
||||||
|
|
||||||
|
本项目使用 `npx create-electron-app@latest china-telecom-app --template=vite-typescript` 创建。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Node.js >= 22.11.0
|
||||||
|
- npm >= 11.3.0
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### 开发环境设置
|
||||||
|
|
||||||
|
1. 克隆仓库
|
||||||
|
```bash
|
||||||
|
git clone [repository-url]
|
||||||
|
cd china-telecom-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装依赖
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动开发服务器
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发指南
|
||||||
|
|
||||||
|
- 使用 TypeScript 进行开发
|
||||||
|
- 遵循 ESLint 规范
|
||||||
|
- 使用 Prettier 进行代码格式化
|
||||||
|
- 主要开发文件位于 `src` 目录下
|
||||||
|
- 使用 IPC 通信进行主进程和渲染进程的通信
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
### 构建命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建配置
|
||||||
|
|
||||||
|
- 构建配置位于 `forge.config.js`
|
||||||
|
- 支持自定义应用图标
|
||||||
|
- 支持自定义应用名称
|
||||||
|
- 支持自定义构建目标平台
|
||||||
|
|
||||||
|
## 发布
|
||||||
|
|
||||||
|
### 发布流程
|
||||||
|
|
||||||
|
1. 更新版本号
|
||||||
|
|
||||||
|
在确认仓库没有任何未提交的更改后,执行以下命令更新版本号:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm version [patch|minor|major]
|
||||||
|
```
|
||||||
|
|
||||||
|
如有未更改的提交,执行以上命令会报错:`npm error Git working directory not clean.`
|
||||||
|
|
||||||
|
或者手工编辑 `package.json` 文件,将 `version` 字段更新为新版本号。
|
||||||
|
|
||||||
|
2. 构建版本
|
||||||
|
```bash
|
||||||
|
npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 打包版本
|
||||||
|
```bash
|
||||||
|
npm run make
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 发布到发布服务器
|
||||||
|
```bash
|
||||||
|
npm run publish
|
||||||
|
```
|
||||||
|
|
||||||
|
### 发布注意事项
|
||||||
|
|
||||||
|
- 确保版本号正确更新
|
||||||
|
- 确保所有依赖都是最新的稳定版本
|
||||||
|
- 确保构建配置正确
|
||||||
|
- 测试发布版本的功能完整性
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
- [Electron](https://www.electronjs.org/) - 跨平台桌面应用框架
|
||||||
|
- [Electron Forge](https://www.electronforge.io/) - Electron 应用打包工具
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/) - JavaScript 的超集
|
||||||
|
- [electron-tabs](https://github.com/brrd/electron-tabs) - Electron 标签页管理
|
||||||
|
- [vite](https://vite.dev/) - 现代前端构建工具
|
BIN
assets/help.png
Normal file
BIN
assets/help.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
@ -13,10 +13,25 @@ const config: ForgeConfig = {
|
|||||||
extraResource: [
|
extraResource: [
|
||||||
'assets/'
|
'assets/'
|
||||||
],
|
],
|
||||||
icon: 'assets/icon.ico',
|
icon: 'assets/icon',
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {},
|
||||||
makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin']), new MakerRpm({}), new MakerDeb({})],
|
makers: [
|
||||||
|
new MakerSquirrel({
|
||||||
|
authors: 'Allen Cai',
|
||||||
|
owners: 'Allen Cai',
|
||||||
|
exe: 'china-telecom-app.exe',
|
||||||
|
name: 'china-telecom-app',
|
||||||
|
version: require('./package.json').version,
|
||||||
|
description: 'China Telecom App',
|
||||||
|
copyright: 'Copyright © 2025 Allen Cai',
|
||||||
|
noMsi: true,
|
||||||
|
setupIcon: 'assets/icon.ico',
|
||||||
|
}),
|
||||||
|
new MakerZIP({}, ['darwin']),
|
||||||
|
new MakerRpm({}),
|
||||||
|
new MakerDeb({})
|
||||||
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
new VitePlugin({
|
new VitePlugin({
|
||||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<span id="userInfo"></span>
|
<span id="userInfo"></span>
|
||||||
<button id="btnLogout" class="logout-btn">退出登录</button>
|
<button id="btnExit" class="exit-btn">退出</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -48,7 +48,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="node_modules/electron-tabs/dist/electron-tabs.js"></script>
|
<!-- 帮助图标 -->
|
||||||
|
<img id="helpIcon" src="./assets/help.png" alt="帮助图标" class="help-icon">
|
||||||
|
<script type="module">
|
||||||
|
import 'electron-tabs';
|
||||||
|
</script>
|
||||||
<script type="module" src="./src/index.ts"></script>
|
<script type="module" src="./src/index.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "china-telecom-app",
|
"name": "china-telecom-app",
|
||||||
"productName": "china-telecom-app",
|
"productName": "china-telecom-app",
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"description": "My Electron application description",
|
"description": "China Telecom App",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development electron-forge start",
|
"start": "cross-env NODE_ENV=development electron-forge start",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow, ipcMain, screen, session } from "electron";
|
import { BrowserWindow, ipcMain, screen, session, app } from "electron";
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
@ -8,6 +8,11 @@ import { ApiResponse, MenuItem, TagResolutionConfig, EIACDesktopApi, SpecialPUrl
|
|||||||
const memoryCache = new Map<string, any>();
|
const memoryCache = new Map<string, any>();
|
||||||
|
|
||||||
export function initialize(): void {
|
export function initialize(): void {
|
||||||
|
// Close app
|
||||||
|
ipcMain.handle('app:close', (): void => {
|
||||||
|
app.exit(0); // 使用 exit 而不是 quit,确保立即退出
|
||||||
|
});
|
||||||
|
|
||||||
// Set cache
|
// Set cache
|
||||||
ipcMain.handle('cache:set', (_event, key: string, value: any): void => {
|
ipcMain.handle('cache:set', (_event, key: string, value: any): void => {
|
||||||
memoryCache.set(key, value);
|
memoryCache.set(key, value);
|
||||||
@ -40,7 +45,7 @@ export function initialize(): void {
|
|||||||
return helperDescrip ? helperDescrip.Descrip : null;
|
return helperDescrip ? helperDescrip.Descrip : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get zoom factor
|
// Get zoom factor by url
|
||||||
ipcMain.handle('get-zoom-factor-by-url', async (event, url: string): Promise<number> => {
|
ipcMain.handle('get-zoom-factor-by-url', async (event, url: string): Promise<number> => {
|
||||||
const display: Electron.Display = screen.getPrimaryDisplay();
|
const display: Electron.Display = screen.getPrimaryDisplay();
|
||||||
const physicalSize: Electron.Size = {
|
const physicalSize: Electron.Size = {
|
||||||
@ -113,7 +118,13 @@ export function initialize(): void {
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
console.log('check-url-available HEAD', url.toString(), res.statusCode, res.statusMessage);
|
console.log('check-url-available HEAD', url.toString(), res.statusCode, res.statusMessage);
|
||||||
if (res.statusCode === 403 || res.statusCode === 404) {
|
/**
|
||||||
|
* 403: 禁止访问
|
||||||
|
* 404: 未找到
|
||||||
|
* 405: 方法不允许
|
||||||
|
*/
|
||||||
|
const requiresRetryStatusCodes: number[] = [403, 404, 405];
|
||||||
|
if (requiresRetryStatusCodes.includes(res.statusCode)) {
|
||||||
headReq.destroy()
|
headReq.destroy()
|
||||||
const getReq = lib.get({
|
const getReq = lib.get({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -153,7 +164,7 @@ export function initialize(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set webview‘s cookie
|
// Set webview's cookie
|
||||||
ipcMain.handle('set-webview-cookie', async (event, url: string, cookie: string): Promise<boolean> => {
|
ipcMain.handle('set-webview-cookie', async (event, url: string, cookie: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
@ -188,12 +199,24 @@ export function initialize(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base64 = await captureWindowAsBase64(win);
|
const base64 = await captureWindowAsBase64(win);
|
||||||
console.log('base64:', base64);
|
console.debug('base64:', base64);
|
||||||
|
|
||||||
|
const account: string = memoryCache.get('Account');
|
||||||
|
if (!account) {
|
||||||
|
throw new Error('Not found account');
|
||||||
|
}
|
||||||
|
console.log('account:', account);
|
||||||
|
|
||||||
|
const ip: string = getLocalIPAddress();
|
||||||
|
if (!ip) {
|
||||||
|
throw new Error('Not found ip');
|
||||||
|
}
|
||||||
|
console.log('ip:', ip);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: ApiResponse<FaultReportingResponse> = await EIACDesktopApi.Help.FaultReportingAsync({
|
const response: ApiResponse<FaultReportingResponse> = await EIACDesktopApi.Help.FaultReportingAsync({
|
||||||
Account: memoryCache.get('Account'),
|
Account: account,
|
||||||
IP: getLocalIPAddress(),
|
IP: ip,
|
||||||
Url: url,
|
Url: url,
|
||||||
ImgBase64: base64,
|
ImgBase64: base64,
|
||||||
Explain: `message: ${message}, status: ${status}`
|
Explain: `message: ${message}, status: ${status}`
|
||||||
|
@ -44,7 +44,7 @@ body {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
.exit-btn {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background-color: #f44336;
|
background-color: #f44336;
|
||||||
color: white;
|
color: white;
|
||||||
@ -53,7 +53,7 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn:hover {
|
.exit-btn:hover {
|
||||||
background-color: #d32f2f;
|
background-color: #d32f2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,3 +191,24 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Help icon */
|
||||||
|
.help-icon {
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
max-width: 64px;
|
||||||
|
max-height: 64px;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
right: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 9999;
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-icon:hover {
|
||||||
|
transform: translateY(-50%) scale(1.2) rotate(360deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
111
src/index.ts
111
src/index.ts
@ -116,21 +116,23 @@ function renderMenu(menuList: MenuItem[]): void {
|
|||||||
* 添加标签页
|
* 添加标签页
|
||||||
* @param tabGroup 标签组
|
* @param tabGroup 标签组
|
||||||
* @param menuItem 菜单项
|
* @param menuItem 菜单项
|
||||||
|
* @param allowCloseTab 是否允许关闭标签页
|
||||||
* @returns 标签页
|
* @returns 标签页
|
||||||
*/
|
*/
|
||||||
async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab | null> {
|
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 url: string = menuItem.Url.startsWith("http") ? menuItem.Url : `http://${menuItem.Url}`;
|
||||||
const result: { ok: boolean; status: number; message?: string } = await window.electronAPI.checkUrlAvailable(url);
|
const result: { ok: boolean; status: number; message?: string } = 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 ${url} 可访问:`, result.status);
|
console.log(`✅ URL ${url} 可访问:`, result.status);
|
||||||
lastInvalidUrlResult = null;
|
lastInvalidUrlResult = null;
|
||||||
|
|
||||||
const cookies: string = window.electronAPI.getSessionStorage('cookies');
|
const cookies: string = window.electronAPI.getSessionStorage('cookies');
|
||||||
await window.electronAPI.setWebviewCookie(url, cookies);
|
await window.electronAPI.setWebviewCookie(url, cookies);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`❌ URL ${url} 不可访问:`, result.message ?? `status ${result.status}`);
|
console.warn(`❌ URL ${url} 不可访问:`, result.message ?? `status ${result.status}`);
|
||||||
lastInvalidUrlResult = { url, message: 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${helpDescrip ?? ''}`;
|
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));
|
showErrorModal(helpDescrip.replace('{URL}', url));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -139,7 +141,7 @@ async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab
|
|||||||
|
|
||||||
const tab: Tab = tabGroup.addTab({
|
const tab: Tab = tabGroup.addTab({
|
||||||
active: true,
|
active: true,
|
||||||
closable: true,
|
closable: allowCloseTab,
|
||||||
title: menuItem.ShowName,
|
title: menuItem.ShowName,
|
||||||
src: url,
|
src: url,
|
||||||
iconURL: menuItem.IconConfig._1x.Default,
|
iconURL: menuItem.IconConfig._1x.Default,
|
||||||
@ -153,7 +155,9 @@ async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab
|
|||||||
const webview: Electron.WebviewTag = tab.webview as Electron.WebviewTag;
|
const webview: Electron.WebviewTag = tab.webview as Electron.WebviewTag;
|
||||||
listenWebviewTitleChange(webview, tab);
|
listenWebviewTitleChange(webview, tab);
|
||||||
|
|
||||||
|
// 监听 webview 的 DOM 加载完成事件
|
||||||
tab.once('webview-dom-ready', () => {
|
tab.once('webview-dom-ready', () => {
|
||||||
|
// 设置 webview 的缩放比例
|
||||||
const webview: Electron.WebviewTag = tab.webview as Electron.WebviewTag;
|
const webview: Electron.WebviewTag = tab.webview as Electron.WebviewTag;
|
||||||
const defaultZoomFactor: number = webview.getZoomFactor();
|
const defaultZoomFactor: number = webview.getZoomFactor();
|
||||||
console.log('Default zoom factor:', defaultZoomFactor);
|
console.log('Default zoom factor:', defaultZoomFactor);
|
||||||
@ -164,6 +168,18 @@ async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab
|
|||||||
} else {
|
} else {
|
||||||
console.log('Default zoom factor is the same as the zoom factor:', zoomFactor);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -177,7 +193,31 @@ async function addTabAsync(tabGroup: TabGroup, menuItem: MenuItem): Promise<Tab
|
|||||||
* @param tab 标签页
|
* @param tab 标签页
|
||||||
*/
|
*/
|
||||||
function listenWebviewTitleChange(webview: Electron.WebviewTag, tab: Tab): void {
|
function listenWebviewTitleChange(webview: Electron.WebviewTag, tab: Tab): void {
|
||||||
// 在webview加载完成后,获取并设置标签页的标题和图标
|
// 监听 URL 变化事件
|
||||||
|
webview.addEventListener('did-navigate', async (event: Electron.DidNavigateEvent) => {
|
||||||
|
const url: string = event.url;
|
||||||
|
const zoomFactor: number = await window.electronAPI.getZoomFactorByUrl(url);
|
||||||
|
const currentZoomFactor: number = webview.getZoomFactor();
|
||||||
|
|
||||||
|
if (currentZoomFactor !== zoomFactor) {
|
||||||
|
webview.setZoomFactor(zoomFactor);
|
||||||
|
console.log('URL changed, modify zoom factor:', zoomFactor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 URL 在同一个页面内的变化事件(如 hash 变化)
|
||||||
|
webview.addEventListener('did-navigate-in-page', async (event: Electron.DidNavigateInPageEvent) => {
|
||||||
|
const url: string = event.url;
|
||||||
|
const zoomFactor: number = await window.electronAPI.getZoomFactorByUrl(url);
|
||||||
|
const currentZoomFactor: number = webview.getZoomFactor();
|
||||||
|
|
||||||
|
if (currentZoomFactor !== zoomFactor) {
|
||||||
|
webview.setZoomFactor(zoomFactor);
|
||||||
|
console.log('URL in-page changed, modify zoom factor:', zoomFactor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听webview的标题变化,并更新标签页的标题和图标
|
||||||
webview.addEventListener('did-finish-load', async () => {
|
webview.addEventListener('did-finish-load', async () => {
|
||||||
const title: string = webview.getTitle();
|
const title: string = webview.getTitle();
|
||||||
tab.setTitle(title);
|
tab.setTitle(title);
|
||||||
@ -227,6 +267,29 @@ async function getFaviconUrl(webview: Electron.WebviewTag): Promise<string> {
|
|||||||
return defaultFaviconUrl;
|
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点击事件
|
* 绑定logo点击事件
|
||||||
*/
|
*/
|
||||||
@ -249,18 +312,19 @@ function bindLogoClickEvent(tabGroup: TabGroup, menuItem: MenuItem): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绑定登出事件
|
* 绑定退出事件
|
||||||
*/
|
*/
|
||||||
function bindLogoutEvent(): void {
|
function bindExitEvent(): void {
|
||||||
const logoutBtn: HTMLButtonElement = document.getElementById('btnLogout') as HTMLButtonElement;
|
const exitBtn: HTMLButtonElement = document.getElementById('btnExit') as HTMLButtonElement;
|
||||||
if (logoutBtn) {
|
if (exitBtn) {
|
||||||
logoutBtn.addEventListener('click', () => {
|
exitBtn.addEventListener('click', async () => {
|
||||||
window.electronAPI.removeSessionStorage('cookies');
|
window.electronAPI.removeSessionStorage('cookies');
|
||||||
window.location.href = 'login.html';
|
await window.electronAPI.closeApp();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let closeCount: number = 0;
|
||||||
/**
|
/**
|
||||||
* 绑定错误弹窗事件
|
* 绑定错误弹窗事件
|
||||||
*/
|
*/
|
||||||
@ -269,17 +333,31 @@ function bindErrorModalEvent(): void {
|
|||||||
const closeErrorModal: HTMLButtonElement = document.getElementById('closeErrorModal') as HTMLButtonElement;
|
const closeErrorModal: HTMLButtonElement = document.getElementById('closeErrorModal') as HTMLButtonElement;
|
||||||
const faultReporting: HTMLButtonElement = document.getElementById('faultReporting') as HTMLButtonElement;
|
const faultReporting: HTMLButtonElement = document.getElementById('faultReporting') as HTMLButtonElement;
|
||||||
|
|
||||||
faultReporting.addEventListener('click', () => faultReportingAsync());
|
faultReporting.addEventListener('click', async () => await faultReportingAsync());
|
||||||
|
|
||||||
// Close button click event
|
// Close button click event
|
||||||
closeErrorModal.addEventListener('click', () => {
|
closeErrorModal.addEventListener('click', () => {
|
||||||
errorModal.style.display = 'none';
|
errorModal.style.display = 'none';
|
||||||
|
closeCount++;
|
||||||
|
|
||||||
|
// 如果关闭次数大于等于2,则重置计数并且清空lastInvalidUrlResult
|
||||||
|
if (closeCount >= 2) {
|
||||||
|
closeCount = 0;
|
||||||
|
lastInvalidUrlResult = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click outside to close
|
// Click outside to close
|
||||||
window.addEventListener('click', (event: Event) => {
|
window.addEventListener('click', (event: Event) => {
|
||||||
if (event.target === errorModal) {
|
if (event.target === errorModal) {
|
||||||
errorModal.style.display = 'none';
|
errorModal.style.display = 'none';
|
||||||
|
closeCount++;
|
||||||
|
|
||||||
|
// 如果关闭次数大于等于2,则重置计数并且清空lastInvalidUrlResult
|
||||||
|
if (closeCount >= 2) {
|
||||||
|
closeCount = 0;
|
||||||
|
lastInvalidUrlResult = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -316,6 +394,7 @@ async function faultReportingAsync(): Promise<void> {
|
|||||||
|
|
||||||
// 显示请求结果
|
// 显示请求结果
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
lastInvalidUrlResult = null;
|
||||||
faultReportingBtn.textContent = '上报成功';
|
faultReportingBtn.textContent = '上报成功';
|
||||||
faultReportingBtn.style.backgroundColor = '#4CAF50';
|
faultReportingBtn.style.backgroundColor = '#4CAF50';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -361,14 +440,18 @@ async function initialize(): Promise<void> {
|
|||||||
|
|
||||||
// Create initial tab
|
// Create initial tab
|
||||||
const firstMenuItem: MenuItem = menuList[0];
|
const firstMenuItem: MenuItem = menuList[0];
|
||||||
await addTabAsync(tabGroup, firstMenuItem);
|
await addTabAsync(tabGroup, firstMenuItem, false);
|
||||||
|
|
||||||
|
// Bind help icon click event
|
||||||
|
const helpMenuItem: MenuItem = menuList[menuList.length - 2];
|
||||||
|
bindHelpIconClickEvent(helpMenuItem);
|
||||||
|
|
||||||
// Bind logo click event
|
// Bind logo click event
|
||||||
const logoMenuItem: MenuItem = menuList[menuList.length - 1];
|
const logoMenuItem: MenuItem = menuList[menuList.length - 1];
|
||||||
bindLogoClickEvent(tabGroup, logoMenuItem);
|
bindLogoClickEvent(tabGroup, logoMenuItem);
|
||||||
|
|
||||||
// Bind logout event
|
// Bind exit event
|
||||||
bindLogoutEvent();
|
bindExitEvent();
|
||||||
|
|
||||||
// Bind error modal event
|
// Bind error modal event
|
||||||
bindErrorModalEvent();
|
bindErrorModalEvent();
|
||||||
|
@ -5,43 +5,48 @@ import { contextBridge, ipcRenderer } from 'electron';
|
|||||||
import { ApiResponse, MenuItem, TagResolutionConfig } from './EIAC_Desktop_Api';
|
import { ApiResponse, MenuItem, TagResolutionConfig } from './EIAC_Desktop_Api';
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
/**
|
||||||
|
* 关闭应用程序
|
||||||
|
*/
|
||||||
|
closeApp: (): Promise<void> => ipcRenderer.invoke('app:close'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取缓存
|
* 获取缓存
|
||||||
* @param key 键
|
* @param key 键
|
||||||
* @returns 缓存
|
* @returns 缓存
|
||||||
*/
|
*/
|
||||||
getCacheAsync: (key: string) => ipcRenderer.invoke('cache:get', key) as Promise<any>,
|
getCacheAsync: (key: string): Promise<any> => ipcRenderer.invoke('cache:get', key),
|
||||||
/**
|
/**
|
||||||
* 设置缓存
|
* 设置缓存
|
||||||
* @param key 键
|
* @param key 键
|
||||||
* @param value 值
|
* @param value 值
|
||||||
*/
|
*/
|
||||||
setCacheAsync: (key: string, value: any) => ipcRenderer.invoke('cache:set', key, value) as Promise<void>,
|
setCacheAsync: (key: string, value: any): Promise<void> => ipcRenderer.invoke('cache:set', key, value),
|
||||||
/**
|
/**
|
||||||
* 获取菜单缓存
|
* 获取菜单缓存
|
||||||
* @returns 菜单缓存
|
* @returns 菜单缓存
|
||||||
*/
|
*/
|
||||||
getMenuCacheAsync: () => ipcRenderer.invoke('get-menu-cache') as Promise<ApiResponse<MenuItem[]>>,
|
getMenuCacheAsync: (): Promise<ApiResponse<MenuItem[]>> => ipcRenderer.invoke('get-menu-cache'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取帮助描述
|
* 获取帮助描述
|
||||||
* @param code 帮助描述代码
|
* @param code 帮助描述代码
|
||||||
* @returns 帮助描述
|
* @returns 帮助描述
|
||||||
*/
|
*/
|
||||||
getHelperDescripAsync: (code: string) => ipcRenderer.invoke('get-helper-descrip', code) as Promise<string | null>,
|
getHelperDescripAsync: (code: string): Promise<string | null> => ipcRenderer.invoke('get-helper-descrip', code),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定URL的缩放比例
|
* 获取指定URL的缩放比例
|
||||||
* @param url 要获取缩放比例的URL
|
* @param url 要获取缩放比例的URL
|
||||||
* @returns 缩放比例
|
* @returns 缩放比例
|
||||||
*/
|
*/
|
||||||
getZoomFactorByUrl: (url: string) => ipcRenderer.invoke('get-zoom-factor-by-url', url) as Promise<number>,
|
getZoomFactorByUrl: (url: string): Promise<number> => ipcRenderer.invoke('get-zoom-factor-by-url', url),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在新标签页打开URL
|
* 在新标签页打开URL
|
||||||
* @param callback 回调函数,参数为webContentId和url。其中webContentId是请求打开URL的webview的id。
|
* @param callback 回调函数,参数为webContentId和url。其中webContentId是请求打开URL的webview的id。
|
||||||
*/
|
*/
|
||||||
onOpenTab: (callback: (webContentId: number, url: string) => void) => {
|
onOpenTab: (callback: (webContentId: number, url: string) => void): void => {
|
||||||
ipcRenderer.on('webview-new-window', (_event, webContentId, url) => callback(webContentId, url));
|
ipcRenderer.on('webview-new-window', (_event, webContentId, url) => callback(webContentId, url));
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -50,14 +55,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
* @param url 要检查的URL
|
* @param url 要检查的URL
|
||||||
* @returns 是否可用
|
* @returns 是否可用
|
||||||
*/
|
*/
|
||||||
checkUrlAvailable: (url: string) => ipcRenderer.invoke('check-url-available', url) as Promise<boolean>,
|
checkUrlAvailable: (url: string): Promise<boolean> => ipcRenderer.invoke('check-url-available', url),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置webview的cookie
|
* 设置webview的cookie
|
||||||
* @param url 要设置cookie的URL
|
* @param url 要设置cookie的URL
|
||||||
* @param cookie cookie字符串
|
* @param cookie cookie字符串
|
||||||
*/
|
*/
|
||||||
setWebviewCookie: (url: string, cookie: string) => ipcRenderer.invoke('set-webview-cookie', url, cookie) as Promise<boolean>,
|
setWebviewCookie: (url: string, cookie: string): Promise<boolean> => ipcRenderer.invoke('set-webview-cookie', url, cookie),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 故障上报
|
* 故障上报
|
||||||
@ -65,14 +70,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
* @param message 要上报的消息
|
* @param message 要上报的消息
|
||||||
* @param status 要上报的状态
|
* @param status 要上报的状态
|
||||||
*/
|
*/
|
||||||
faultReporting: (url: string, message: string, status: number) => ipcRenderer.invoke('fault-reporting', url, message, status) as Promise<{ ok: boolean; status: number; message?: string }>,
|
faultReporting: (url: string, message: string, status: number): Promise<{ ok: boolean; status: number; message?: string }> => ipcRenderer.invoke('fault-reporting', url, message, status),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按键将值设置到sessionStorage
|
* 按键将值设置到sessionStorage
|
||||||
* @param key 键
|
* @param key 键
|
||||||
* @param value 值
|
* @param value 值
|
||||||
*/
|
*/
|
||||||
setSessionStorage: (key: string, value: string) => {
|
setSessionStorage: (key: string, value: string): void => {
|
||||||
window.sessionStorage.setItem(key, value);
|
window.sessionStorage.setItem(key, value);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -81,7 +86,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
* @param key 键
|
* @param key 键
|
||||||
* @returns 值
|
* @returns 值
|
||||||
*/
|
*/
|
||||||
getSessionStorage: (key: string) => {
|
getSessionStorage: (key: string): string | null => {
|
||||||
return window.sessionStorage.getItem(key);
|
return window.sessionStorage.getItem(key);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -89,14 +94,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
* 从sessionStorage中删除指定键的值
|
* 从sessionStorage中删除指定键的值
|
||||||
* @param key 键
|
* @param key 键
|
||||||
*/
|
*/
|
||||||
removeSessionStorage: (key: string) => {
|
removeSessionStorage: (key: string): void => {
|
||||||
window.sessionStorage.removeItem(key);
|
window.sessionStorage.removeItem(key);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空sessionStorage
|
* 清空sessionStorage
|
||||||
*/
|
*/
|
||||||
clearSessionStorage: () => {
|
clearSessionStorage: (): void => {
|
||||||
window.sessionStorage.clear();
|
window.sessionStorage.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
4
src/types/electron.d.ts
vendored
4
src/types/electron.d.ts
vendored
@ -1,6 +1,10 @@
|
|||||||
import { ApiResponse, MenuItem, TagResolutionConfig } from '../EIAC_Desktop_Api';
|
import { ApiResponse, MenuItem, TagResolutionConfig } from '../EIAC_Desktop_Api';
|
||||||
|
|
||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
|
/**
|
||||||
|
* 关闭应用程序
|
||||||
|
*/
|
||||||
|
closeApp: () => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* 获取缓存
|
* 获取缓存
|
||||||
* @param key 键
|
* @param key 键
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "commonjs",
|
"module": "ESNext",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
@ -20,8 +20,17 @@ export default defineConfig({
|
|||||||
input: {
|
input: {
|
||||||
login: 'login.html',
|
login: 'login.html',
|
||||||
index: 'index.html'
|
index: 'index.html'
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
// 确保 electron-tabs.js 被正确打包
|
||||||
|
manualChunks: {
|
||||||
|
'electron-tabs': ['electron-tabs']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['electron-tabs']
|
||||||
},
|
},
|
||||||
// 使用 Vite 的 env 配置
|
// 使用 Vite 的 env 配置
|
||||||
envPrefix: ['EIAC_DESKTOP_API_HOST', 'NODE_ENV'],
|
envPrefix: ['EIAC_DESKTOP_API_HOST', 'NODE_ENV'],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user