const { app, BrowserWindow, ipcMain, net, shell, dialog } = require('electron'); const path = require('path'); const url = require('url'); const fs = require('fs'); const { spawn } = require('child_process'); // 用于启动子进程 const sevenBin = require('7zip-bin'); let iconv = null; // 用来在 Windows 上正确解码 7za 使用系统编码输出的中文信息 try { iconv = require('iconv-lite'); } catch (e) { iconv = null; } let win = null; // 创建窗口 const createWindow = () => { win = new BrowserWindow({ // width: 1200, // height: 800, show: false, // 是否显示窗口 autoHideMenuBar: true, // 隐藏菜单栏 webPreferences: { nodeIntegration: true, // 是否集成 Node.js // enableRemoteModule: true, // 是否启用 remote 模块 webSecurity: false, // 是否禁用同源策略 preload: path.join(__dirname, 'preload.js'), // 预加载脚本 }, }); win.loadURL( url.format({ pathname: path.join(__dirname, './dist', 'index.html'), protocol: 'file:', slashes: true, // true: file://, false: file: hash: '/login', // /image_change 图片切换页 }), ); win.once('ready-to-show', () => { win.maximize(); win.show(); }); // 拦截当前窗口的导航,防止外部链接在应用内打开 win.webContents.on('will-navigate', (event, url) => { if (url.startsWith('http://') || url.startsWith('https://')) { event.preventDefault(); shell.openExternal(url); } }); }; // 当 Electron 完成初始化并准备创建浏览器窗口时调用此方法 app.whenReady().then(() => { createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); // 检查更新 ipcMain.handle('check-update', async () => { const apiUrl = 'https://your-api.com/api/app/latest'; // <-- 替换为真实接口 const currentVersion = app.getVersion(); return new Promise((resolve, reject) => { const req = net.request(apiUrl); let body = ''; req.on('response', (res) => { res.on('data', (chunk) => (body += chunk)); res.on('end', () => { try { const latest = JSON.parse(body); const update = latest.version && latest.version !== currentVersion; resolve({ update, latest, currentVersion }); } catch (e) { reject(e); } }); }); req.on('error', (err) => reject(err)); req.end(); }); }); // 下载更新(渲染进程发起),主进程负责流式保存并推送进度 ipcMain.on('download-update', (event, downloadUrl) => { const win = BrowserWindow.getAllWindows()[0]; const filename = path.basename(downloadUrl).split('?')[0] || `update-${Date.now()}.exe`; const tmpPath = path.join(app.getPath('temp'), filename); const fileStream = fs.createWriteStream(tmpPath); const req = net.request(downloadUrl); let received = 0; let total = 0; req.on('response', (res) => { total = parseInt(res.headers['content-length'] || res.headers['Content-Length'] || '0'); res.on('data', (chunk) => { received += chunk.length; fileStream.write(chunk); win.webContents.send('update-download-progress', { received, total }); }); res.on('end', () => { fileStream.end(); win.webContents.send('update-downloaded', { path: tmpPath }); }); }); req.on('error', (err) => { win.webContents.send('update-error', { message: err.message || String(err) }); }); req.end(); }); // 安装更新(渲染进程确认安装时调用) ipcMain.on('install-update', (event, filePath) => { if (!fs.existsSync(filePath)) { event.sender.send('update-error', { message: '安装文件不存在' }); return; } if (process.platform === 'win32') { // Windows:直接执行安装程序(根据你的安装包参数添加静默/等待参数) try { spawn(filePath, [], { detached: true, stdio: 'ignore' }).unref(); app.quit(); } catch (e) { event.sender.send('update-error', { message: e.message }); } } else if (process.platform === 'darwin') { // macOS:打开 dmg 或者打开 .pkg shell.openPath(filePath).then(() => app.quit()); } else { // linux:按照需要实现 shell.openPath(filePath).then(() => app.quit()); } }); /** * 使用 7z 压缩文件/目录,支持密码和进度反馈 * @param {Object} opts 压缩选项 * @param {Array} opts.sources 待压缩的文件或目录列表 * @param {string} opts.dest 目标压缩包路径 * @param {string} [opts.format] 压缩格式,'7z' 或 'zip',默认 'zip' * @param {number} [opts.level] 压缩级别,0-9,默认 9 * @param {boolean} [opts.recurse] 是否递归子目录,默认 true * @param {string} [opts.password] 压缩密码 * @returns {Promise<{success: boolean, dest: string}>} 压缩结果 */ ipcMain.handle('compress-with-7z', async (evt, opts) => { const sender = evt.sender; // 用于发送进度消息 const sources = Array.isArray(opts.sources) ? opts.sources : []; // 待压缩的文件或目录列表 if (!sources.length) throw new Error('no sources'); const dest = path.resolve(opts.dest); // 目标压缩包路径 const format = opts.format === 'zip' ? 'zip' : '7z'; // 压缩格式 const level = Number.isInteger(opts.level) ? Math.max(0, Math.min(9, opts.level)) : 9; // 压缩级别 const recurse = opts.recurse !== false; // 是否递归子目录 // 检查源文件/目录是否存在 for (const s of sources) { if (!fs.existsSync(s)) throw new Error(`source not found: ${s}`); } // 7za 可执行路径,优先使用 seven-bin 提供的路径;若被打包进 app.asar,则尝试 app.asar.unpacked 路径 let sevenPath = sevenBin.path7za; try { // 如果 sevenPath 在 app.asar 内,映射到 app.asar.unpacked 的对应相对路径 const asarToken = `${path.sep}app.asar${path.sep}`; if (typeof sevenPath === 'string' && sevenPath.indexOf(asarToken) !== -1) { const rel = sevenPath.split(asarToken)[1]; // 例如: node_modules/7zip-bin/win/x64/7za.exe const unpackedCandidate = path.join(process.resourcesPath, 'app.asar.unpacked', rel); if (fs.existsSync(unpackedCandidate)) { sevenPath = unpackedCandidate; } } // 若上面没有找到,尝试常见的 unpacked 路径(兼容不同包结构与架构) if (!fs.existsSync(sevenPath)) { const candidates = [ path.join( process.resourcesPath, 'app.asar.unpacked', 'node_modules', '7zip-bin', 'win', 'x64', path.basename(sevenPath), ), path.join( process.resourcesPath, 'app.asar.unpacked', 'node_modules', '7zip-bin', 'win', 'x86', path.basename(sevenPath), ), path.join( process.resourcesPath, 'app.asar.unpacked', 'node_modules', '7zip-bin', 'bin', path.basename(sevenPath), ), ]; for (const c of candidates) { if (fs.existsSync(c)) { sevenPath = c; break; } } } } catch (err) { console.error('Error checking 7za path:', err); } if (!fs.existsSync(sevenPath)) { throw new Error( `7za executable not found: ${sevenPath}. If running from a packaged app, add "asarUnpack": ["node_modules/7zip-bin/**"] to build config so 7za is unpacked.`, ); } const args = ['a', `-t${format}`, `-mx=${level}`, dest]; // 基本参数 if (opts.password) { args.push(`-p${opts.password}`); // 仅当使用 7z 格式时才启用头部加密(zip 不支持 -mhe) if (format === '7z') args.push('-mhe=on'); // 加密文件名/头(仅 7z 支持) } if (recurse) args.push('-r'); // 递归 // append sources (支持通配或单个路径) args.push(...sources); // 确保目标目录存在(避免 7z 无法写入导致的失败) try { fs.mkdirSync(path.dirname(dest), { recursive: true }); } catch (e) { // 如果无法创建目标目录,尽早报错 throw new Error(`failed to create dest directory: ${e.message}`); } return await new Promise((resolve, reject) => { let stdoutAll = ''; let stderrAll = ''; const child = spawn(sevenPath, args, { windowsHide: true }); // 收集并转发 stdout/stderr,用作进度显示或日志 child.stdout.on('data', (chunk) => { const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); let s = ''; // 7za 在 Windows 上输出常用 OEM/GBK 编码,优先用 iconv-lite 解码 if (process.platform === 'win32' && iconv) { try { s = iconv.decode(buf, 'cp936'); } catch (e) { s = buf.toString('utf8'); } } else { s = buf.toString('utf8'); } stdoutAll += s; sender.send('compress-progress', s); }); child.stderr.on('data', (chunk) => { const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); let s = ''; if (process.platform === 'win32' && iconv) { try { s = iconv.decode(buf, 'cp936'); } catch (e) { s = buf.toString('utf8'); } } else { s = buf.toString('utf8'); } stderrAll += s; sender.send('compress-stderr', s); }); child.on('error', (err) => reject(err)); child.on('close', (code) => { if (code === 0) { resolve({ success: true, dest }); } else { // 包含 stdout/stderr 与命令信息以便排查 const errMsg = `7z exited with code ${code}\ncommand: ${JSON.stringify({ sevenPath, args })}\nstdout:\n${stdoutAll}\nstderr:\n${stderrAll}`; const e = new Error(errMsg); e.code = code; e.stdout = stdoutAll; e.stderr = stderrAll; e.cmd = { sevenPath, args }; return reject(e); } }); }); }); /** * 下载文件 * @param {string} url 文件 URL * @param {string} destPath 保存路径 */ ipcMain.handle('download-file', async (evt, { url, destPath }) => { return new Promise((resolve, reject) => { const fileStream = fs.createWriteStream(destPath); // 创建写入流 const req = net.request(url); req.on('response', (res) => { res.on('data', (chunk) => { fileStream.write(chunk); }); res.on('end', () => { fileStream.end(); resolve(destPath); }); }); req.on('error', (err) => { reject(err); }); req.end(); }); }); /** * 打开文件对话框 * @param {Object} opts 对话框选项 * @param {string} [opts.title] 对话框标题 * @param {Array} [opts.properties] 对话框属性数组,默认 ['openDirectory'] * @return {Promise<{canceled: boolean, filePaths: string[]}>} 文件路径 canceled 表示是否取消选择 filePaths 表示选择的文件路径数组 */ ipcMain.handle('dialog:openFiles', async (event, options = {}) => { const result = await dialog.showOpenDialog({ title: options.title || '选择文件夹', properties: options.properties || ['openDirectory'], }); return { canceled: result.canceled, filePaths: result.filePaths }; }); // 当所有窗口都已关闭时退出 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });