- 调整设备在线查询逻辑,确保只返回活跃绑定的设备 - 优化试用期逻辑,精确计算过期时间和类型- 添加账号管理弹窗和相关状态注入 -修复跟卖精灵按钮加载状态显示问题 - 增强文件上传区域UI,显示选中文件名 - 调整分页组件样式,优化界面展示效果- 优化反馈日志存储路径逻辑,默认使用用户目录 - 移除冗余代码和无用导入,提升代码整洁度
805 lines
28 KiB
TypeScript
805 lines
28 KiB
TypeScript
import {app, BrowserWindow, ipcMain, Menu, screen, dialog} from 'electron';
|
|
import {existsSync, createWriteStream, promises as fs, mkdirSync, copyFileSync, readdirSync, writeFileSync} from 'fs';
|
|
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';
|
|
|
|
let springProcess: ChildProcess | null = null;
|
|
let mainWindow: BrowserWindow | null = null;
|
|
let splashWindow: BrowserWindow | null = null;
|
|
let appOpened = false;
|
|
let downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
|
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;
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
isDev
|
|
? mainWindow.loadURL(`http://localhost:${process.argv[2] || 8083}`)
|
|
: mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
|
|
|
mainWindow.webContents.once('did-finish-load', () => {
|
|
setTimeout(() => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
const config = loadConfig();
|
|
const shouldMinimize = config.launchMinimized || false;
|
|
if (!shouldMinimize) {
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
if (isDev) mainWindow.webContents.openDevTools();
|
|
}
|
|
if (splashWindow && !splashWindow.isDestroyed()) {
|
|
splashWindow.close();
|
|
splashWindow = null;
|
|
}
|
|
}, 500);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 通用资源路径获取函数
|
|
function getResourcePath(devPath: string, prodPath: string, fallbackPath?: string): string {
|
|
if (isDev) return join(__dirname, devPath);
|
|
const bundledPath = join(process.resourcesPath, 'app.asar.unpacked', prodPath);
|
|
if (existsSync(bundledPath)) return bundledPath;
|
|
return fallbackPath ? join(__dirname, fallbackPath) : join(__dirname, prodPath);
|
|
}
|
|
|
|
function getJavaExecutablePath(): string {
|
|
if (isDev) return 'java';
|
|
const javaPath = getResourcePath('', 'public/jre/bin/java.exe');
|
|
return existsSync(javaPath) ? javaPath : 'java';
|
|
}
|
|
|
|
function findJarFile(directory: string): string {
|
|
if (!existsSync(directory)) return '';
|
|
const jarFile = readdirSync(directory).find((f: string) => f.startsWith('erp_client_sb-') && f.endsWith('.jar'));
|
|
return jarFile ? join(directory, jarFile) : '';
|
|
}
|
|
|
|
function getJarFilePath(): string {
|
|
if (isDev) return findJarFile(join(__dirname, '../../public'));
|
|
return findJarFile(process.resourcesPath);
|
|
}
|
|
|
|
const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/splash.html');
|
|
const getIconPath = () => getResourcePath('../../public/icon/icon.png', 'public/icon/icon.png', '../renderer/icon/icon.png');
|
|
const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml');
|
|
|
|
function getDataDirectoryPath(): string {
|
|
const dataDir = join(app.getPath('userData'), 'data');
|
|
if (!existsSync(dataDir)) mkdirSync(dataDir, {recursive: true});
|
|
return dataDir;
|
|
}
|
|
|
|
function getUpdateDirectoryPath(): string {
|
|
const updateDir = join(app.getPath('userData'), 'updates');
|
|
if (!existsSync(updateDir)) mkdirSync(updateDir, {recursive: true});
|
|
return updateDir;
|
|
}
|
|
|
|
function getLogDirectoryPath(): string {
|
|
const logDir = join(app.getPath('userData'), 'logs');
|
|
if (!existsSync(logDir)) mkdirSync(logDir, {recursive: true});
|
|
return logDir;
|
|
}
|
|
|
|
interface AppConfig {
|
|
closeAction?: 'quit' | 'minimize' | 'tray';
|
|
autoLaunch?: boolean;
|
|
launchMinimized?: boolean;
|
|
}
|
|
|
|
function getConfigPath(): string {
|
|
return join(app.getPath('userData'), 'config.json');
|
|
}
|
|
|
|
function loadConfig(): AppConfig {
|
|
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: AppConfig) {
|
|
try {
|
|
const configPath = getConfigPath();
|
|
require('fs').writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
} catch (error) {
|
|
console.error('保存配置失败:', error);
|
|
}
|
|
}
|
|
|
|
function migrateDataFromPublic(): void {
|
|
if (!isDev) return;
|
|
|
|
const oldDataPath = join(__dirname, '../../public/data');
|
|
if (!existsSync(oldDataPath)) return;
|
|
|
|
const newDataPath = getDataDirectoryPath();
|
|
try {
|
|
readdirSync(oldDataPath).forEach(file => {
|
|
const destFile = join(newDataPath, file);
|
|
if (!existsSync(destFile)) {
|
|
copyFileSync(join(oldDataPath, file), destFile);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.log('数据迁移失败,使用默认配置');
|
|
}
|
|
}
|
|
|
|
|
|
function startSpringBoot() {
|
|
migrateDataFromPublic();
|
|
const jarPath = getJarFilePath();
|
|
const javaPath = getJavaExecutablePath();
|
|
const dataDir = getDataDirectoryPath();
|
|
const logDir = getLogDirectoryPath();
|
|
const logbackConfigPath = getLogbackConfigPath();
|
|
if (!existsSync(jarPath)) {
|
|
dialog.showErrorBox('启动失败', `JAR 文件不存在:\n${jarPath}`);
|
|
app.quit();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const springArgs = [
|
|
'-jar', jarPath,
|
|
`--spring.datasource.url=jdbc:sqlite:${dataDir}/erp-cache.db`,
|
|
`--server.port=8081`,
|
|
`--logging.config=file:${logbackConfigPath}`,
|
|
`--logging.file.path=${logDir}`
|
|
];
|
|
|
|
springProcess = spawn(javaPath, springArgs, {
|
|
cwd: dataDir,
|
|
detached: false,
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
});
|
|
|
|
let startupCompleted = false;
|
|
|
|
springProcess.stdout?.on('data', (data) => {
|
|
console.log('[Spring Boot]', data.toString().trim());
|
|
});
|
|
|
|
springProcess.stderr?.on('data', (data) => {
|
|
console.log('[Spring Boot]', data.toString().trim());
|
|
});
|
|
|
|
springProcess.on('close', (code) => {
|
|
mainWindow ? mainWindow.close() : app.quit();
|
|
});
|
|
|
|
springProcess.on('error', (error) => {
|
|
let errorMessage = '启动 Java 应用失败';
|
|
if (error.message.includes('ENOENT')) {
|
|
errorMessage = '找不到 Java 运行环境\n请确保应用包含 JRE 或系统已安装 Java';
|
|
}
|
|
dialog.showErrorBox('启动失败', errorMessage);
|
|
app.quit();
|
|
});
|
|
|
|
const checkHealth = () => {
|
|
if (startupCompleted) return;
|
|
|
|
http.get('http://127.0.0.1:8081', (res) => {
|
|
if (!startupCompleted) {
|
|
startupCompleted = true;
|
|
console.log('[Spring Boot] 服务已就绪');
|
|
openAppIfNotOpened();
|
|
}
|
|
}).on('error', () => {
|
|
setTimeout(checkHealth, 200);
|
|
});
|
|
};
|
|
|
|
setTimeout(checkHealth, 1000);
|
|
|
|
setTimeout(() => {
|
|
if (!startupCompleted) {
|
|
console.log('[Spring Boot] 启动超时,强制打开窗口');
|
|
openAppIfNotOpened();
|
|
}
|
|
}, 15000);
|
|
|
|
} catch (error) {
|
|
dialog.showErrorBox('启动异常', `无法启动应用: ${error}`);
|
|
app.quit();
|
|
}
|
|
}
|
|
startSpringBoot();
|
|
function stopSpringBoot() {
|
|
if (!springProcess) return;
|
|
try {
|
|
if (process.platform === 'win32') {
|
|
const pid = springProcess.pid;
|
|
if (pid) {
|
|
spawn('taskkill', ['/pid', String(pid), '/f', '/t']);
|
|
} else {
|
|
springProcess.kill();
|
|
}
|
|
} else {
|
|
springProcess.kill('SIGTERM');
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to stop Spring Boot:', e);
|
|
} finally {
|
|
springProcess = null;
|
|
}
|
|
}
|
|
|
|
function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 800,
|
|
show: false, //
|
|
autoHideMenuBar: true,
|
|
icon: getIconPath(),
|
|
backgroundColor: '#f5f5f5',
|
|
webPreferences: {
|
|
preload: join(__dirname, 'preload.js'),
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
}
|
|
});
|
|
|
|
Menu.setApplicationMenu(null);
|
|
mainWindow.setMenuBarVisibility(false);
|
|
|
|
// 阻止默认的文件拖拽导航行为,让渲染进程的 JavaScript 处理拖拽上传
|
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
|
// 允许开发模式下的热重载导航
|
|
if (isDev && url.startsWith('http://localhost')) return;
|
|
// 阻止所有其他导航(包括拖拽文件)
|
|
event.preventDefault();
|
|
});
|
|
|
|
// 同样阻止新窗口的文件拖拽
|
|
mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
|
|
// 拦截关闭事件
|
|
mainWindow.on('close', (event) => {
|
|
if (isQuitting) return;
|
|
|
|
const config = loadConfig();
|
|
const closeAction = config.closeAction || 'quit';
|
|
|
|
if (closeAction === 'quit') {
|
|
isQuitting = true;
|
|
app.quit();
|
|
} else if (closeAction === 'tray' || closeAction === 'minimize') {
|
|
event.preventDefault();
|
|
mainWindow?.hide();
|
|
}
|
|
});
|
|
|
|
// 监听窗口关闭事件,确保正确清理引用
|
|
mainWindow.on('closed', () => {
|
|
mainWindow = null;
|
|
});
|
|
|
|
mainWindow.webContents.once('did-finish-load', () => {
|
|
setTimeout(() => checkPendingUpdate(), 500);
|
|
});
|
|
|
|
}
|
|
|
|
// 单实例锁定
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
|
|
if (!gotTheLock) {
|
|
app.quit();
|
|
} else {
|
|
app.on('second-instance', () => {
|
|
if (mainWindow) {
|
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
// 应用开机自启动配置
|
|
const config = loadConfig();
|
|
const shouldMinimize = config.launchMinimized || false;
|
|
|
|
if (config.autoLaunch !== undefined) {
|
|
app.setLoginItemSettings({
|
|
openAtLogin: config.autoLaunch,
|
|
openAsHidden: shouldMinimize
|
|
});
|
|
}
|
|
|
|
createWindow();
|
|
createTray(mainWindow);
|
|
|
|
// 只有在不需要最小化启动时才显示 splash 窗口
|
|
if (!shouldMinimize) {
|
|
splashWindow = new BrowserWindow({
|
|
width: 1200,
|
|
height: 675,
|
|
frame: false,
|
|
transparent: false,
|
|
resizable: false,
|
|
alwaysOnTop: false,
|
|
show: true,
|
|
center: true,
|
|
icon: getIconPath(),
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
}
|
|
});
|
|
|
|
// 监听启动窗口关闭事件
|
|
splashWindow.on('closed', () => {
|
|
splashWindow = null;
|
|
});
|
|
|
|
const splashPath = getSplashPath();
|
|
if (existsSync(splashPath)) {
|
|
splashWindow.loadFile(splashPath);
|
|
}
|
|
}
|
|
|
|
// setTimeout(() => {
|
|
// openAppIfNotOpened();
|
|
// }, 2000);
|
|
|
|
app.on('activate', () => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.show();
|
|
} else if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
createTray(mainWindow);
|
|
}
|
|
});
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
// 允许在后台运行,不自动退出
|
|
if (process.platform !== 'darwin' && isQuitting) {
|
|
stopSpringBoot();
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('before-quit', () => {
|
|
isQuitting = true;
|
|
stopSpringBoot();
|
|
destroyTray();
|
|
});
|
|
|
|
ipcMain.on('message', (event, message) => {
|
|
console.log(message);
|
|
});
|
|
|
|
ipcMain.handle('get-jar-version', () => {
|
|
const jarPath = getJarFilePath();
|
|
const match = jarPath ? basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/) : null;
|
|
return match?.[1] || '';
|
|
});
|
|
|
|
function checkPendingUpdate() {
|
|
try {
|
|
const updateDir = getUpdateDirectoryPath();
|
|
const asarUpdatePath = join(updateDir, 'app.asar.update');
|
|
const jarUpdatePath = readdirSync(updateDir).find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update'));
|
|
|
|
if (!existsSync(asarUpdatePath) && !jarUpdatePath) return;
|
|
|
|
const appDir = dirname(process.execPath);
|
|
const helperPath = join(appDir, 'update-helper.bat');
|
|
if (!existsSync(helperPath)) return;
|
|
|
|
const appAsarPath = join(process.resourcesPath, 'app.asar');
|
|
const vbsPath = join(app.getPath('temp'), 'update-silent.vbs');
|
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")
|
|
WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath ? join(updateDir, jarUpdatePath).replace(/\\/g, '\\\\') : ''}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${updateDir.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`;
|
|
|
|
writeFileSync(vbsPath, vbsContent);
|
|
spawn('wscript.exe', [vbsPath], {detached: true, stdio: 'ignore', shell: false});
|
|
setTimeout(() => app.quit(), 1000);
|
|
} catch (error: unknown) {
|
|
console.error('[UPDATE] 更新失败:', error);
|
|
}
|
|
}
|
|
|
|
ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string, jarUrl?: string, latestVersion?: string}) => {
|
|
if (isDownloading) return {success: false, error: '正在下载中'};
|
|
if (downloadedFilePath === 'completed') return {success: true, filePath: 'already-completed'};
|
|
|
|
isDownloading = true;
|
|
currentDownloadAbortController = new AbortController();
|
|
let totalDownloaded = 0;
|
|
let combinedTotalSize = 0;
|
|
|
|
try {
|
|
// 预先获取文件大小,计算总下载大小
|
|
let asarSize = 0;
|
|
let jarSize = 0;
|
|
if (downloadUrls.asarUrl) {
|
|
asarSize = await getFileSize(downloadUrls.asarUrl);
|
|
}
|
|
if (downloadUrls.jarUrl) {
|
|
jarSize = await getFileSize(downloadUrls.jarUrl);
|
|
}
|
|
combinedTotalSize = asarSize + jarSize;
|
|
|
|
if (downloadUrls.asarUrl && !currentDownloadAbortController.signal.aborted) {
|
|
const updateDir = getUpdateDirectoryPath();
|
|
const asarUpdatePath = join(updateDir, 'app.asar.update');
|
|
await downloadFile(downloadUrls.asarUrl, asarUpdatePath, (progress) => {
|
|
const combinedProgress = {
|
|
percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0,
|
|
current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`,
|
|
total: combinedTotalSize > 0 ? `${(combinedTotalSize / 1024 / 1024).toFixed(1)} MB` : '0 MB'
|
|
};
|
|
downloadProgress = combinedProgress;
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('download-progress', combinedProgress);
|
|
}
|
|
});
|
|
|
|
if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消');
|
|
downloadedAsarPath = asarUpdatePath;
|
|
totalDownloaded = asarSize;
|
|
}
|
|
|
|
if (downloadUrls.jarUrl && !currentDownloadAbortController.signal.aborted) {
|
|
const jarFileName = downloadUrls.latestVersion
|
|
? `erp_client_sb-${downloadUrls.latestVersion}.jar`
|
|
: basename(downloadUrls.jarUrl);
|
|
const updateDir = getUpdateDirectoryPath();
|
|
const jarUpdatePath = join(updateDir, jarFileName + '.update');
|
|
await downloadFile(downloadUrls.jarUrl, jarUpdatePath, (progress) => {
|
|
const combinedProgress = {
|
|
percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0,
|
|
current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`,
|
|
total: combinedTotalSize > 0 ? `${(combinedTotalSize / 1024 / 1024).toFixed(1)} MB` : '0 MB'
|
|
};
|
|
downloadProgress = combinedProgress;
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('download-progress', combinedProgress);
|
|
}
|
|
});
|
|
|
|
if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消');
|
|
downloadedJarPath = jarUpdatePath;
|
|
}
|
|
|
|
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;
|
|
return {success: false, error: error instanceof Error ? error.message : '下载失败'};
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('get-download-progress', () => {
|
|
return {...downloadProgress, isDownloading};
|
|
});
|
|
|
|
ipcMain.handle('install-update', async () => {
|
|
try {
|
|
const updateDir = getUpdateDirectoryPath();
|
|
const asarUpdatePath = join(updateDir, 'app.asar.update');
|
|
const jarUpdateFile = readdirSync(updateDir).find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update'));
|
|
const jarUpdatePath = jarUpdateFile ? join(updateDir, jarUpdateFile) : '';
|
|
|
|
if (!existsSync(asarUpdatePath) && !jarUpdatePath) {
|
|
return {success: false, error: '更新文件不存在'};
|
|
}
|
|
|
|
const appDir = dirname(process.execPath);
|
|
const helperPath = join(appDir, 'update-helper.bat');
|
|
if (!existsSync(helperPath)) {
|
|
return {success: false, error: '更新助手不存在'};
|
|
}
|
|
|
|
const appAsarPath = join(process.resourcesPath, 'app.asar');
|
|
const vbsPath = join(app.getPath('temp'), 'update-install.vbs');
|
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")
|
|
WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${updateDir.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`;
|
|
|
|
writeFileSync(vbsPath, vbsContent);
|
|
spawn('wscript.exe', [vbsPath], {detached: true, stdio: 'ignore', shell: false});
|
|
|
|
setTimeout(() => {
|
|
downloadedFilePath = null;
|
|
downloadedAsarPath = null;
|
|
downloadedJarPath = null;
|
|
app.quit();
|
|
}, 500);
|
|
|
|
return {success: true};
|
|
} catch (error: unknown) {
|
|
return {success: false, error: error instanceof Error ? error.message : '重启失败'};
|
|
}
|
|
});
|
|
|
|
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};
|
|
});
|
|
|
|
ipcMain.handle('get-update-status', () => {
|
|
return {downloadedFilePath, isDownloading, downloadProgress, isPackaged: app.isPackaged};
|
|
});
|
|
|
|
ipcMain.handle('check-pending-update', () => {
|
|
try {
|
|
const updateDir = getUpdateDirectoryPath();
|
|
const asarUpdatePath = join(updateDir, 'app.asar.update');
|
|
const jarUpdateFile = readdirSync(updateDir).find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update'));
|
|
const jarUpdatePath = jarUpdateFile ? join(updateDir, jarUpdateFile) : '';
|
|
|
|
return {
|
|
hasPendingUpdate: existsSync(asarUpdatePath) || !!jarUpdatePath,
|
|
asarUpdatePath: existsSync(asarUpdatePath) ? asarUpdatePath : null,
|
|
jarUpdatePath: jarUpdatePath || null
|
|
};
|
|
} catch (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 updateDir = getUpdateDirectoryPath();
|
|
const files = readdirSync(updateDir);
|
|
for (const file of files) {
|
|
await fs.unlink(join(updateDir, file)).catch(() => {});
|
|
}
|
|
} catch (error) {}
|
|
}
|
|
|
|
// 添加文件保存对话框处理器
|
|
ipcMain.handle('show-save-dialog', async (event, options) => {
|
|
return await dialog.showSaveDialog(mainWindow!, options);
|
|
});
|
|
|
|
// 添加文件夹选择对话框处理器
|
|
ipcMain.handle('show-open-dialog', async (event, options) => {
|
|
return await dialog.showOpenDialog(mainWindow!, options);
|
|
});
|
|
|
|
// 添加文件写入处理器
|
|
ipcMain.handle('write-file', async (event, filePath: string, data: Uint8Array) => {
|
|
await fs.writeFile(filePath, Buffer.from(data));
|
|
return { success: true };
|
|
});
|
|
|
|
// 获取日志日期列表
|
|
ipcMain.handle('get-log-dates', async () => {
|
|
try {
|
|
const logDir = getLogDirectoryPath();
|
|
const files = await fs.readdir(logDir);
|
|
const dates: string[] = [];
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
files.forEach(file => {
|
|
if (file === 'spring-boot.log') {
|
|
dates.push(today);
|
|
} else if (file.startsWith('spring-boot-') && file.endsWith('.log')) {
|
|
const date = file.replace('spring-boot-', '').replace('.log', '');
|
|
dates.push(date);
|
|
}
|
|
});
|
|
|
|
dates.sort().reverse();
|
|
return { dates };
|
|
} catch (error) {
|
|
return { dates: [] };
|
|
}
|
|
});
|
|
|
|
// 读取指定日期的日志文件
|
|
ipcMain.handle('read-log-file', async (event, logDate: string) => {
|
|
try {
|
|
const logDir = getLogDirectoryPath();
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const fileName = logDate === today ? 'spring-boot.log' : `spring-boot-${logDate}.log`;
|
|
const logFilePath = join(logDir, fileName);
|
|
|
|
if (!existsSync(logFilePath)) {
|
|
return { success: false, error: '日志文件不存在' };
|
|
}
|
|
|
|
const content = await fs.readFile(logFilePath, 'utf-8');
|
|
return { success: true, content };
|
|
} catch (error) {
|
|
return { success: false, error: error instanceof Error ? error.message : '读取失败' };
|
|
}
|
|
});
|
|
|
|
// 获取关闭行为配置
|
|
ipcMain.handle('get-close-action', () => {
|
|
const config = loadConfig();
|
|
return config.closeAction;
|
|
});
|
|
|
|
// 保存关闭行为配置
|
|
ipcMain.handle('set-close-action', (event, action: 'quit' | 'minimize' | 'tray') => {
|
|
const config = loadConfig();
|
|
config.closeAction = action;
|
|
saveConfig(config);
|
|
return { success: true };
|
|
});
|
|
|
|
// 清理缓存
|
|
ipcMain.handle('clear-cache', async () => {
|
|
try {
|
|
const response = await fetch('http://127.0.0.1:8081/api/system/cache/clear', {
|
|
method: 'POST'
|
|
});
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error: any) {
|
|
console.error('清理缓存失败:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
});
|
|
|
|
// 获取启动配置
|
|
ipcMain.handle('get-launch-config', () => {
|
|
const config = loadConfig();
|
|
const loginSettings = app.getLoginItemSettings();
|
|
return {
|
|
autoLaunch: config.autoLaunch !== undefined ? config.autoLaunch : loginSettings.openAtLogin,
|
|
launchMinimized: config.launchMinimized || false
|
|
};
|
|
});
|
|
|
|
// 设置启动配置
|
|
ipcMain.handle('set-launch-config', (event, launchConfig: { autoLaunch: boolean; launchMinimized: boolean }) => {
|
|
const config = loadConfig();
|
|
config.autoLaunch = launchConfig.autoLaunch;
|
|
config.launchMinimized = launchConfig.launchMinimized;
|
|
saveConfig(config);
|
|
|
|
// 立即应用开机自启动设置
|
|
app.setLoginItemSettings({
|
|
openAtLogin: launchConfig.autoLaunch,
|
|
openAsHidden: launchConfig.launchMinimized
|
|
});
|
|
|
|
return { success: true };
|
|
});
|
|
|
|
// 刷新页面
|
|
ipcMain.handle('reload', () => mainWindow?.webContents.reload());
|
|
|
|
|
|
async function getFileSize(url: string): Promise<number> {
|
|
return new Promise((resolve) => {
|
|
const protocol = url.startsWith('https') ? https : http;
|
|
|
|
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, total: number}) => void): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const protocol = url.startsWith('https') ? https : http;
|
|
|
|
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, total: totalSize});
|
|
});
|
|
|
|
response.pipe(fileStream);
|
|
|
|
fileStream.on('finish', () => {
|
|
fileStream.close();
|
|
resolve();
|
|
});
|
|
|
|
fileStream.on('error', (error) => {
|
|
fs.unlink(filePath).catch(() => {});
|
|
reject(error);
|
|
});
|
|
|
|
}).on('error', reject);
|
|
});
|
|
}
|