feat(electron): 实现系统托盘和关闭行为配置功能
- 添加系统托盘创建和销毁逻辑- 实现窗口关闭行为配置(退出/最小化/托盘) - 添加配置文件读写功能 - 实现下载取消和清理功能 - 添加待更新文件检查机制 - 优化文件下载进度和错误处理 - 添加自动更新配置选项- 实现平滑滚动动画效果 - 添加试用期过期类型检查 -优化VIP状态刷新逻辑
This commit is contained in:
@@ -4,6 +4,7 @@ import {join, dirname, basename} from 'path';
|
||||
import {spawn, ChildProcess} from 'child_process';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { createTray, destroyTray } from './tray';
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
@@ -16,6 +17,8 @@ let isDownloading = false;
|
||||
let downloadedFilePath: string | null = null;
|
||||
let downloadedAsarPath: string | null = null;
|
||||
let downloadedJarPath: string | null = null;
|
||||
let isQuitting = false;
|
||||
let currentDownloadAbortController: AbortController | null = null;
|
||||
function openAppIfNotOpened() {
|
||||
if (appOpened) return;
|
||||
appOpened = true;
|
||||
@@ -75,6 +78,31 @@ function getDataDirectoryPath(): string {
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
function getConfigPath(): string {
|
||||
return join(app.getPath('userData'), 'config.json');
|
||||
}
|
||||
|
||||
function loadConfig(): { closeAction?: 'quit' | 'minimize' | 'tray' } {
|
||||
try {
|
||||
const configPath = getConfigPath();
|
||||
if (existsSync(configPath)) {
|
||||
return JSON.parse(require('fs').readFileSync(configPath, 'utf-8'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveConfig(config: any) {
|
||||
try {
|
||||
const configPath = getConfigPath();
|
||||
require('fs').writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateDataFromPublic(): void {
|
||||
if (!isDev) return;
|
||||
|
||||
@@ -171,7 +199,7 @@ function startSpringBoot() {
|
||||
}
|
||||
}
|
||||
|
||||
startSpringBoot();
|
||||
// startSpringBoot();
|
||||
|
||||
function stopSpringBoot() {
|
||||
if (!springProcess) return;
|
||||
@@ -210,6 +238,22 @@ function createWindow() {
|
||||
Menu.setApplicationMenu(null);
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
|
||||
// 拦截关闭事件
|
||||
mainWindow.on('close', (event) => {
|
||||
if (isQuitting) return;
|
||||
|
||||
const config = loadConfig();
|
||||
const closeAction = config.closeAction || 'tray';
|
||||
|
||||
if (closeAction === 'quit') {
|
||||
isQuitting = true;
|
||||
app.quit();
|
||||
} else if (closeAction === 'tray' || closeAction === 'minimize') {
|
||||
event.preventDefault();
|
||||
mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听窗口关闭事件,确保正确清理引用
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
@@ -223,6 +267,7 @@ function createWindow() {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
createTray(mainWindow);
|
||||
|
||||
const {width: sw, height: sh} = screen.getPrimaryDisplay().workAreaSize;
|
||||
splashWindow = new BrowserWindow({
|
||||
@@ -251,24 +296,32 @@ app.whenReady().then(() => {
|
||||
splashWindow.loadFile(splashPath);
|
||||
}
|
||||
|
||||
// setTimeout(() => {
|
||||
// openAppIfNotOpened();
|
||||
// }, 2000);
|
||||
setTimeout(() => {
|
||||
openAppIfNotOpened();
|
||||
}, 2000);
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.show();
|
||||
} else if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
createTray(mainWindow);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
stopSpringBoot();
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
// 允许在后台运行,不自动退出
|
||||
if (process.platform !== 'darwin' && isQuitting) {
|
||||
stopSpringBoot();
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
stopSpringBoot();
|
||||
destroyTray();
|
||||
});
|
||||
|
||||
ipcMain.on('message', (event, message) => {
|
||||
@@ -330,41 +383,46 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
||||
if (downloadedFilePath === 'completed') return {success: true, filePath: 'already-completed'};
|
||||
|
||||
isDownloading = true;
|
||||
currentDownloadAbortController = new AbortController();
|
||||
let totalDownloaded = 0;
|
||||
let totalSize = 0;
|
||||
let combinedTotalSize = 0;
|
||||
|
||||
try {
|
||||
// 获取总大小
|
||||
const sizes = await Promise.all([
|
||||
downloadUrls.asarUrl ? getFileSize(downloadUrls.asarUrl) : 0,
|
||||
downloadUrls.jarUrl ? getFileSize(downloadUrls.jarUrl) : 0
|
||||
]);
|
||||
totalSize = sizes[0] + sizes[1];
|
||||
|
||||
// 下载asar文件
|
||||
// 预先获取文件大小,计算总下载大小
|
||||
let asarSize = 0;
|
||||
let jarSize = 0;
|
||||
if (downloadUrls.asarUrl) {
|
||||
const tempAsarPath = join(app.getPath('temp'), 'app.asar.new');
|
||||
const asarSize = sizes[0];
|
||||
asarSize = await getFileSize(downloadUrls.asarUrl);
|
||||
}
|
||||
if (downloadUrls.jarUrl) {
|
||||
jarSize = await getFileSize(downloadUrls.jarUrl);
|
||||
}
|
||||
combinedTotalSize = asarSize + jarSize;
|
||||
|
||||
if (downloadUrls.asarUrl && !currentDownloadAbortController.signal.aborted) {
|
||||
const tempAsarPath = join(app.getPath('temp'), 'app.asar.new');
|
||||
await downloadFile(downloadUrls.asarUrl, tempAsarPath, (progress) => {
|
||||
const combinedProgress = {
|
||||
percentage: Math.round(((totalDownloaded + progress.downloaded) / totalSize) * 100),
|
||||
percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0,
|
||||
current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`,
|
||||
total: `${(totalSize / 1024 / 1024).toFixed(1)} MB`
|
||||
total: combinedTotalSize > 0 ? `${(combinedTotalSize / 1024 / 1024).toFixed(1)} MB` : '0 MB'
|
||||
};
|
||||
downloadProgress = combinedProgress;
|
||||
if (mainWindow) mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消');
|
||||
|
||||
const asarUpdatePath = join(process.resourcesPath, 'app.asar.update');
|
||||
await fs.copyFile(tempAsarPath, asarUpdatePath);
|
||||
await fs.unlink(tempAsarPath);
|
||||
downloadedAsarPath = asarUpdatePath;
|
||||
totalDownloaded += asarSize;
|
||||
totalDownloaded = asarSize;
|
||||
}
|
||||
|
||||
// 下载jar文件
|
||||
if (downloadUrls.jarUrl) {
|
||||
if (downloadUrls.jarUrl && !currentDownloadAbortController.signal.aborted) {
|
||||
let jarFileName = basename(downloadUrls.jarUrl);
|
||||
if (!jarFileName.match(/^erp_client_sb-[\d.]+\.jar$/)) {
|
||||
const currentJar = getJarFilePath();
|
||||
@@ -379,17 +437,20 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
||||
}
|
||||
|
||||
const tempJarPath = join(app.getPath('temp'), jarFileName);
|
||||
|
||||
await downloadFile(downloadUrls.jarUrl, tempJarPath, (progress) => {
|
||||
const combinedProgress = {
|
||||
percentage: Math.round(((totalDownloaded + progress.downloaded) / totalSize) * 100),
|
||||
percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0,
|
||||
current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`,
|
||||
total: `${(totalSize / 1024 / 1024).toFixed(1)} MB`
|
||||
total: combinedTotalSize > 0 ? `${(combinedTotalSize / 1024 / 1024).toFixed(1)} MB` : '0 MB'
|
||||
};
|
||||
downloadProgress = combinedProgress;
|
||||
if (mainWindow) mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消');
|
||||
|
||||
const jarUpdatePath = join(process.resourcesPath, jarFileName + '.update');
|
||||
await fs.copyFile(tempJarPath, jarUpdatePath);
|
||||
await fs.unlink(tempJarPath);
|
||||
@@ -398,10 +459,13 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
||||
|
||||
downloadedFilePath = 'completed';
|
||||
isDownloading = false;
|
||||
|
||||
currentDownloadAbortController = null;
|
||||
|
||||
return {success: true, asarPath: downloadedAsarPath, jarPath: downloadedJarPath};
|
||||
} catch (error: unknown) {
|
||||
await cleanupDownloadFiles();
|
||||
isDownloading = false;
|
||||
currentDownloadAbortController = null;
|
||||
downloadedFilePath = null;
|
||||
downloadedAsarPath = null;
|
||||
downloadedJarPath = null;
|
||||
@@ -466,12 +530,21 @@ WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " &
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('cancel-download', () => {
|
||||
ipcMain.handle('cancel-download', async () => {
|
||||
if (currentDownloadAbortController) {
|
||||
currentDownloadAbortController.abort();
|
||||
currentDownloadAbortController = null;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await cleanupDownloadFiles();
|
||||
|
||||
isDownloading = false;
|
||||
downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
||||
downloadedFilePath = null;
|
||||
downloadedAsarPath = null;
|
||||
downloadedJarPath = null;
|
||||
|
||||
return {success: true};
|
||||
});
|
||||
|
||||
@@ -479,6 +552,77 @@ ipcMain.handle('get-update-status', () => {
|
||||
return {downloadedFilePath, isDownloading, downloadProgress, isPackaged: app.isPackaged};
|
||||
});
|
||||
|
||||
ipcMain.handle('check-pending-update', () => {
|
||||
try {
|
||||
const asarUpdatePath = join(process.resourcesPath, 'app.asar.update');
|
||||
const hasAsarUpdate = existsSync(asarUpdatePath);
|
||||
|
||||
// 查找jar更新文件
|
||||
let jarUpdatePath = '';
|
||||
if (process.resourcesPath && existsSync(process.resourcesPath)) {
|
||||
const files = readdirSync(process.resourcesPath);
|
||||
const jarUpdateFile = files.find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update'));
|
||||
if (jarUpdateFile) {
|
||||
jarUpdatePath = join(process.resourcesPath, jarUpdateFile);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasPendingUpdate: hasAsarUpdate || !!jarUpdatePath,
|
||||
asarUpdatePath: hasAsarUpdate ? asarUpdatePath : null,
|
||||
jarUpdatePath: jarUpdatePath || null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('检查待安装更新失败:', error);
|
||||
return {
|
||||
hasPendingUpdate: false,
|
||||
asarUpdatePath: null,
|
||||
jarUpdatePath: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-update-files', async () => {
|
||||
try {
|
||||
await cleanupDownloadFiles();
|
||||
|
||||
// 重置下载状态
|
||||
downloadedFilePath = null;
|
||||
downloadedAsarPath = null;
|
||||
downloadedJarPath = null;
|
||||
isDownloading = false;
|
||||
downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
||||
|
||||
return {success: true};
|
||||
} catch (error: unknown) {
|
||||
return {success: false, error: error instanceof Error ? error.message : '清除失败'};
|
||||
}
|
||||
});
|
||||
|
||||
async function cleanupDownloadFiles() {
|
||||
try {
|
||||
const tempAsarPath = join(app.getPath('temp'), 'app.asar.new');
|
||||
if (existsSync(tempAsarPath)) await fs.unlink(tempAsarPath).catch(() => {});
|
||||
|
||||
const asarUpdatePath = join(process.resourcesPath, 'app.asar.update');
|
||||
if (existsSync(asarUpdatePath)) await fs.unlink(asarUpdatePath).catch(() => {});
|
||||
|
||||
if (process.resourcesPath && existsSync(process.resourcesPath)) {
|
||||
const files = readdirSync(process.resourcesPath);
|
||||
const jarUpdateFiles = files.filter(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update'));
|
||||
for (const file of jarUpdateFiles) {
|
||||
await fs.unlink(join(process.resourcesPath, file)).catch(() => {});
|
||||
}
|
||||
|
||||
const tempJarFiles = files.filter(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar') && !f.includes('update'));
|
||||
for (const file of tempJarFiles) {
|
||||
const tempJarPath = join(app.getPath('temp'), file);
|
||||
if (existsSync(tempJarPath)) await fs.unlink(tempJarPath).catch(() => {});
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// 添加文件保存对话框处理器
|
||||
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||||
return await dialog.showSaveDialog(mainWindow!, options);
|
||||
@@ -552,33 +696,79 @@ ipcMain.handle('read-log-file', async (event, logDate: string) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取关闭行为配置
|
||||
ipcMain.handle('get-close-action', () => {
|
||||
const config = loadConfig();
|
||||
return config.closeAction || 'tray';
|
||||
});
|
||||
|
||||
// 保存关闭行为配置
|
||||
ipcMain.handle('set-close-action', (event, action: 'quit' | 'minimize' | 'tray') => {
|
||||
const config = loadConfig();
|
||||
config.closeAction = action;
|
||||
saveConfig(config);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
|
||||
async function getFileSize(url: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
protocol.get(url, {method: 'HEAD'}, (response) => {
|
||||
|
||||
const request = protocol.get(url, {method: 'HEAD'}, (response) => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl) {
|
||||
getFileSize(redirectUrl).then(resolve).catch(() => resolve(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const size = parseInt(response.headers['content-length'] || '0', 10);
|
||||
resolve(size);
|
||||
}).on('error', () => resolve(0));
|
||||
|
||||
request.setTimeout(10000, () => {
|
||||
request.destroy();
|
||||
resolve(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, filePath: string, onProgress: (progress: {downloaded: number}) => void): Promise<void> {
|
||||
async function downloadFile(url: string, filePath: string, onProgress: (progress: {downloaded: number, total: number}) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
protocol.get(url, (response) => {
|
||||
const request = protocol.get(url, (response) => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl) {
|
||||
downloadFile(redirectUrl, filePath, onProgress).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
||||
let downloadedBytes = 0;
|
||||
const fileStream = createWriteStream(filePath);
|
||||
|
||||
if (currentDownloadAbortController) {
|
||||
currentDownloadAbortController.signal.addEventListener('abort', () => {
|
||||
request.destroy();
|
||||
response.destroy();
|
||||
fileStream.destroy();
|
||||
reject(new Error('下载已取消'));
|
||||
});
|
||||
}
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedBytes += chunk.length;
|
||||
onProgress({downloaded: downloadedBytes});
|
||||
onProgress({downloaded: downloadedBytes, total: totalSize});
|
||||
});
|
||||
|
||||
response.pipe(fileStream);
|
||||
|
||||
Reference in New Issue
Block a user