为了可拖拽tab的效果,引入electron-tabs包

This commit is contained in:
Allen 2025-05-12 00:56:15 +08:00
parent 346f88d21e
commit f99c1bb88e
7 changed files with 10147 additions and 248 deletions

View File

@ -1,17 +1,20 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" /> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<title>中国电信-工作台</title> <title>中国电信-工作台</title>
<link rel="stylesheet" href="./src/index.css"> <link rel="stylesheet" href="./src/index.css">
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
<header class="app-header"> <header class="app-header">
<div class="header-left"> <div class="header-left">
<img src="./assets/logo.png" alt="Logo" class="logo"> <img id="logo" src="./assets/logo.png" alt="Logo" class="logo" />
<h1>中国电信-工作台</h1> <h1>中国电信-工作台</h1>
</div> </div>
<div class="header-right"> <div class="header-right">
@ -21,7 +24,7 @@
</header> </header>
<main class="app-main"> <main class="app-main">
<nav class="sidebar"> <nav class="sidebar" style="display: none;">
<ul id="menuList" class="menu-list"> <ul id="menuList" class="menu-list">
<!-- 菜单项将通过JavaScript动态加载 --> <!-- 菜单项将通过JavaScript动态加载 -->
</ul> </ul>
@ -29,14 +32,7 @@
<div class="content-area"> <div class="content-area">
<div class="tabs-container"> <div class="tabs-container">
<div class="tabs-header"> <tab-group sortable="true"></tab-group>
<ul id="tabsList" class="tabs-list">
<!-- Tabs will be added here dynamically -->
</ul>
</div>
<div id="tabsContent" class="tabs-content">
<!-- Webviews will be added here dynamically -->
</div>
</div> </div>
</div> </div>
</main> </main>
@ -51,6 +47,8 @@
</div> </div>
</div> </div>
<script src="node_modules/electron-tabs/dist/electron-tabs.js"></script>
<script type="module" src="./src/index.ts"></script> <script type="module" src="./src/index.ts"></script>
</body> </body>
</html> </html>

View File

@ -2,7 +2,8 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" /> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<title>登录</title> <title>登录</title>
<link rel="stylesheet" href="./src/login.css"> <link rel="stylesheet" href="./src/login.css">

10062
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@
"@electron-forge/plugin-vite": "^7.8.0", "@electron-forge/plugin-vite": "^7.8.0",
"@electron/fuses": "^1.8.0", "@electron/fuses": "^1.8.0",
"@types/electron-squirrel-startup": "^1.0.2", "@types/electron-squirrel-startup": "^1.0.2",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^5.62.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -39,6 +40,8 @@
"vite": "^5.4.18" "vite": "^5.4.18"
}, },
"dependencies": { "dependencies": {
"electron-squirrel-startup": "^1.0.1" "electron-squirrel-startup": "^1.0.1",
"electron-tabs": "^1.0.4",
"sortablejs": "^1.15.6"
} }
} }

View File

@ -179,80 +179,3 @@ body {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.tabs-header {
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.tabs-list {
display: flex;
list-style: none;
margin: 0;
padding: 0;
height: 40px;
}
.tab-item {
display: flex;
align-items: center;
padding: 0 15px;
height: 100%;
border-right: 1px solid #ddd;
background-color: #fff;
cursor: pointer;
user-select: none;
position: relative;
}
.tab-item.active {
background-color: #fff;
border-bottom: 2px solid #1890ff;
}
.tab-item .tab-title {
margin-right: 8px;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tab-item .tab-close {
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
border-radius: 50%;
color: #999;
font-size: 12px;
}
.tab-item .tab-close:hover {
background-color: #e6e6e6;
color: #666;
}
.tabs-content {
flex: 1;
position: relative;
}
.tab-pane {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
}
.tab-pane.active {
display: block;
}
.tab-pane webview {
width: 100%;
height: 100%;
border: none;
}

View File

@ -1,4 +1,4 @@
import { ipcRenderer, WebviewTag } from "electron"; import { TabGroup, Tab } from 'electron-tabs';
// 菜单项 // 菜单项
interface MenuItem { interface MenuItem {
@ -28,122 +28,9 @@ interface ApiResponse<T> {
data: T; data: T;
} }
// Tab management const tabGroup: TabGroup = document.querySelector('tab-group') as TabGroup;
interface Tab {
id: string;
title: string;
url: string;
webview: WebviewTag;
}
let tabs: Tab[] = []; // Check login status
let activeTabId: string | null = null;
// Create a new tab
function createTab(title: string, url: string): Tab {
const id = `tab-${Date.now()}`;
const tabPane = document.createElement('div');
tabPane.className = 'tab-pane';
tabPane.id = `pane-${id}`;
const webview = document.createElement('webview');
webview.className = 'page-content';
webview.setAttribute('autosize', 'on');
webview.setAttribute('allowpopups', 'true');
webview.setAttribute('webpreferences', 'contextIsolation=yes, nodeIntegration=no');
webview.src = url;
tabPane.appendChild(webview);
document.getElementById('tabsContent')?.appendChild(tabPane);
const tab: Tab = {
id,
title,
url,
webview
};
tabs.push(tab);
return tab;
}
// Create tab header
function createTabHeader(tab: Tab): HTMLLIElement {
const li = document.createElement('li');
li.className = 'tab-item';
li.dataset.tabId = tab.id;
const titleSpan = document.createElement('span');
titleSpan.className = 'tab-title';
titleSpan.textContent = tab.title;
const closeButton = document.createElement('span');
closeButton.className = 'tab-close';
closeButton.textContent = '×';
closeButton.addEventListener('click', (e) => {
e.stopPropagation();
closeTab(tab.id);
});
li.appendChild(titleSpan);
li.appendChild(closeButton);
li.addEventListener('click', () => {
activateTab(tab.id);
});
return li;
}
// Activate a tab
function activateTab(tabId: string) {
const tab = tabs.find(t => t.id === tabId);
if (!tab) return;
// Update active states
document.querySelectorAll('.tab-item').forEach(item => {
item.classList.remove('active');
});
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('active');
});
// Activate the selected tab
const tabElement = document.querySelector(`.tab-item[data-tab-id="${tabId}"]`);
const paneElement = document.getElementById(`pane-${tabId}`);
if (tabElement && paneElement) {
tabElement.classList.add('active');
paneElement.classList.add('active');
}
activeTabId = tabId;
}
// Close a tab
function closeTab(tabId: string) {
const tabIndex = tabs.findIndex(t => t.id === tabId);
if (tabIndex === -1) return;
const tab = tabs[tabIndex];
const tabElement = document.querySelector(`.tab-item[data-tab-id="${tabId}"]`);
const paneElement = document.getElementById(`pane-${tabId}`);
if (tabElement) tabElement.remove();
if (paneElement) paneElement.remove();
tabs.splice(tabIndex, 1);
// If we closed the active tab, activate another one
if (activeTabId === tabId) {
if (tabs.length > 0) {
activateTab(tabs[Math.min(tabIndex, tabs.length - 1)].id);
} else {
activeTabId = null;
}
}
}
// 检查登录状态
function checkLoginStatus() { function checkLoginStatus() {
const cookie = window.electronAPI.getSessionStorage('cookie'); const cookie = window.electronAPI.getSessionStorage('cookie');
if (!cookie) { if (!cookie) {
@ -151,20 +38,20 @@ function checkLoginStatus() {
return; return;
} }
// 显示用户信息 // Show user information
const userInfo = document.getElementById('userInfo'); const userInfo = document.getElementById('userInfo');
if (userInfo) { if (userInfo) {
userInfo.textContent = '欢迎使用'; userInfo.textContent = '欢迎使用';
} }
} }
// 处理退出登录 // Handle logout
function handleLogout() { function handleLogout() {
window.electronAPI.removeSessionStorage('cookie'); window.electronAPI.removeSessionStorage('cookie');
window.location.href = 'login.html'; window.location.href = 'login.html';
} }
// 获取菜单列表 // Get menu list
async function getMenuList(): Promise<MenuItem[]> { async function getMenuList(): Promise<MenuItem[]> {
try { try {
const response = await fetch('http://1.12.73.211:8848/EIAC_Desktop_Api/api/Menu/GetMenu', { const response = await fetch('http://1.12.73.211:8848/EIAC_Desktop_Api/api/Menu/GetMenu', {
@ -178,10 +65,10 @@ async function getMenuList(): Promise<MenuItem[]> {
if (result.status === 0) { if (result.status === 0) {
return result.data; return result.data;
} else { } else {
throw new Error(result.msg || '获取菜单失败'); throw new Error(result.msg || '获取菜单列表失败');
} }
} catch (error) { } catch (error) {
console.error('获取菜单失败:', error); console.error('获取菜单列表失败:', error);
throw error; throw error;
} }
} }
@ -220,29 +107,15 @@ function createMenuItem(item: MenuItem, menuList: MenuItem[]): HTMLLIElement {
li.classList.add('active'); li.classList.add('active');
icon.src = item.IconConfig._1x.Selected; icon.src = item.IconConfig._1x.Selected;
const url: string = item.Url.startsWith("http") ? item.Url : `http://${item.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);
// Create new tab // Create new tab
const tab = createTab(item.ShowName, url); await addTab(tabGroup, item);
const tabHeader = createTabHeader(tab);
document.getElementById('tabsList')?.appendChild(tabHeader);
activateTab(tab.id);
} else {
console.warn('❌ URL 不可访问:', result.error ?? `status ${result.status}`);
showErrorModal(`无法访问 ${url}\r\n异常原因${result.error ?? `status ${result.status}`}\r\n请联系10000技术支持。`);
}
}); });
} }
return li; return li;
} }
// 渲染菜单 // Render menu
function renderMenu(menuList: MenuItem[]) { function renderMenu(menuList: MenuItem[]) {
const menuContainer = document.getElementById('menuList'); const menuContainer = document.getElementById('menuList');
if (!menuContainer) return; if (!menuContainer) return;
@ -263,7 +136,7 @@ function renderMenu(menuList: MenuItem[]) {
}); });
} }
// 显示故障窗口 // Show error modal
function showErrorModal(message: string) { function showErrorModal(message: string) {
const errorModal = document.getElementById('errorModal') as HTMLDivElement; const errorModal = document.getElementById('errorModal') as HTMLDivElement;
const errorMessage = document.getElementById('errorMessage') as HTMLParagraphElement; const errorMessage = document.getElementById('errorMessage') as HTMLParagraphElement;
@ -271,6 +144,44 @@ function showErrorModal(message: string) {
errorModal.style.display = 'block'; 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);
} else {
console.warn('❌ URL 不可访问:', result.error ?? `status ${result.status}`);
showErrorModal(`无法访问 ${url}\r\n异常原因${result.error ?? `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,
}
});
return tab;
}
async function bindLogoClickEvent(tabGroup: TabGroup, menuItem: MenuItem) {
const logo = document.getElementById('logo') as HTMLImageElement;
logo.addEventListener('click', async () => {
console.log('logo clicked');
const tab: Tab = await addTab(tabGroup, menuItem);
tab.setPosition(0);
});
}
// Modify the initialize function to create the first tab // Modify the initialize function to create the first tab
async function initialize() { async function initialize() {
// Check login status // Check login status
@ -281,10 +192,8 @@ async function initialize() {
renderMenu(menuList); renderMenu(menuList);
// Create initial tab // Create initial tab
const initialTab = createTab('首页', 'about:blank'); const firstMenuItem = menuList[0];
const tabHeader = createTabHeader(initialTab); await addTab(tabGroup, firstMenuItem);
document.getElementById('tabsList')?.appendChild(tabHeader);
activateTab(initialTab.id);
// Bind logout event // Bind logout event
const logoutBtn = document.getElementById('btnLogout'); const logoutBtn = document.getElementById('btnLogout');
@ -295,16 +204,20 @@ async function initialize() {
const errorModal = document.getElementById('errorModal') as HTMLDivElement; const errorModal = document.getElementById('errorModal') as HTMLDivElement;
const closeErrorModal = document.getElementById('closeErrorModal') as HTMLButtonElement; const closeErrorModal = document.getElementById('closeErrorModal') as HTMLButtonElement;
// Close button click event // Close button click event
closeErrorModal.addEventListener('click', (event) => { closeErrorModal.addEventListener('click', (event: Event) => {
errorModal.style.display = 'none'; errorModal.style.display = 'none';
}); });
// Click outside to close // Click outside to close
window.addEventListener('click', (event) => { window.addEventListener('click', (event: Event) => {
if (event.target === errorModal) { if (event.target === errorModal) {
errorModal.style.display = 'none'; errorModal.style.display = 'none';
} }
}); });
// Listen logo click event
const lastMenuItem = menuList[menuList.length - 1];
await bindLogoClickEvent(tabGroup, lastMenuItem);
} catch (error) { } catch (error) {
console.error('初始化失败:', error); console.error('初始化失败:', error);
} }

View File

@ -5,13 +5,12 @@ import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { URL } from 'url'; import { URL } from 'url';
// 确保只有一个实例在运行 // Ensure only one instance is running
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) { if (!gotTheLock) {
app.exit(0); // 使用 exit 而不是 quit确保立即退出 app.exit(0); // 使用 exit 而不是 quit确保立即退出
} else { } else {
app.on('second-instance', (event, commandLine, workingDirectory) => { app.on('second-instance', (event: Event, cmdArgs: string[], workingDirectory: string) => {
// 当运行第二个实例时,我们应该聚焦到主窗口 // 当运行第二个实例时,我们应该聚焦到主窗口
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
if (win) { if (win) {
@ -63,7 +62,7 @@ ipcMain.handle('check-url-available', async (event, rawUrl: string) => {
} }
}); });
// 设置webview的cookie // Set webviews cookie
ipcMain.handle('set-webview-cookie', async (event, url: string, cookie: string) => { ipcMain.handle('set-webview-cookie', async (event, url: string, cookie: string) => {
try { try {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
@ -109,7 +108,7 @@ const createWindow = () => {
}, },
}); });
// 隐藏顶部菜单栏 // Hide top menu bar
win.setMenuBarVisibility(false); win.setMenuBarVisibility(false);
win.setAutoHideMenuBar(true); win.setAutoHideMenuBar(true);
win.setMenu(null); win.setMenu(null);
@ -136,17 +135,17 @@ const createWindow = () => {
// }); // });
// }); // });
// 设置session // Set session
win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
callback(true); callback(true);
}); });
// 最小化到系统托盘 // Minimize to system tray
win.on('minimize', () => { win.on('minimize', () => {
win.hide(); win.hide();
}); });
// 监听窗口关闭用户点击右上角X // Listen window close (user clicks the X in the top right corner)
win.on('close', (event) => { win.on('close', (event) => {
event.preventDefault(); event.preventDefault();
win.hide(); win.hide();
@ -169,7 +168,7 @@ const createWindow = () => {
win.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/login.html`)); win.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/login.html`));
} }
// 创建托盘图标 // Create tray icon
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ label: '显示窗口', click: () => win.show() }, { label: '显示窗口', click: () => win.show() },
{ label: '退出程序', click: () => app.exit() } { label: '退出程序', click: () => app.exit() }