| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- 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<string>} 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<string>} [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();
- }
- });
|