// main.js (ESM 版本)
import { app, BrowserWindow, Menu, ipcMain, shell, dialog } from 'electron';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { writeLog } from './src/log/log-writer.js';
import { tmpdir } from 'os';
import { writeFileSync } from 'fs';
// -------------- 构造 ESM 版 __dirname --------------
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const execAsync = promisify(exec);
let win=null;
// -------------- 单实例检查 ------------
// 1. 请求单实例锁
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
// 抢锁失败 -> 立即退出
app.quit();
}
// 2. 抢锁成功 -> 监听后续启动事件
app.on('second-instance', (ev, cmd, cwd, additionalData) => {
// 把已存在的窗口提到最前
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
// todo 也可以在这里解析 cmd 中的参数,做打开文件等操作
});
// -------------- 创建窗口 --------------
function createWindow() {
const isMac = process.platform === 'darwin';
win = new BrowserWindow({
show: false,
frame: false,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: join(__dirname, 'preload.js'),
},
});
// 去掉应用菜单栏
Menu.setApplicationMenu(null);
if (!isMac) win.removeMenu();
win.maximize();
// 加载外置 H5 页面
win.loadFile(join(process.cwd(), 'h5/index.html'));
win.once('ready-to-show', () => win.show());
//渲染进程死了,纪录日志
win.webContents.on('render-process-gone', (event, details) => {
writeLog('error',`渲染进程崩溃 ${details}`);
// writeLog('error', JSON.stringify(details));
});
}
// -------------- 应用生命周期 --------------
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
// -------------- 系统操作函数 --------------
const getSystemCommands = () => {
const platform = process.platform;
switch (platform) {
case 'win32': // Windows
return {
shutdown: 'shutdown /s /t 0'
};
case 'darwin': // macOS
return {
shutdown: 'sudo shutdown -h now'
};
case 'linux': // Linux
return {
shutdown: 'systemctl poweroff'
};
default:
return {
shutdown: null
};
}
};
const executeSystemCommand = async (command, requiresAdmin = false) => {
try {
if (!command) {
throw new Error('当前平台不支持该操作');
}
// Windows平台的权限检查
if (process.platform === 'win32' && requiresAdmin) {
// 检查是否以管理员身份运行
try {
await execAsync('net session', { timeout: 5000 });
} catch (adminCheckError) {
writeLog('warn', '操作可能需要管理员权限');
return {
success: false,
error: '该操作需要管理员权限,请以管理员身份运行程序后重试',
requiresAdmin: true
};
}
}
const { stdout, stderr } = await execAsync(command, { timeout: 10000 });
writeLog('info', `系统命令执行成功: ${command}`);
if (stderr && stderr.trim()) {
writeLog('warn', `系统命令有警告信息: ${stderr}`);
}
return { success: true, stdout, stderr };
} catch (error) {
let errorMessage = error.message;
// 特定错误类型的处理
if (error.code === 'EACCES') {
errorMessage = '权限不足,请以管理员身份运行程序';
} else if (error.code === 'ENOENT') {
errorMessage = '系统命令不存在或路径错误';
} else if (error.code === 'ETIMEDOUT') {
errorMessage = '操作超时,请重试';
}
writeLog('error', `系统命令执行失败: ${command}, 错误: ${errorMessage}`);
return { success: false, error: errorMessage };
}
};
// -------------- IPC --------------
ipcMain.handle('write-log', async (_, level, msg) => {
writeLog(level, msg);
});
// 退出应用
ipcMain.handle('exit-close', async () => {
writeLog('info', '用户选择关闭应用程序');
app.quit();
return { success: true };
});
// 关机
ipcMain.handle('exit-shutdown', async () => {
writeLog('info', '用户选择关机');
const commands = getSystemCommands();
// 关机通常需要管理员权限
const requiresAdmin = true;
return await executeSystemCommand(commands.shutdown, requiresAdmin);
});
// ========== 打印相关 IPC 处理器 ==========
/**
* 打印HTML内容
*/
// 工具:一次性事件监听
const once = (emitter, name) =>
new Promise(resolve => emitter.once(name, resolve));
// 工具:生成不二次编码的 data URL
const makeDataURL = html =>
`data:text/html;charset=utf-8,${Buffer.from(html, 'utf-8')}`;
ipcMain.handle('print-film', async (event, options) => {
let printWindow = null;
try {
writeLog('info', `开始打印,选项: ${JSON.stringify(options)}`);
const { html, orientation, paperSize, silent, margins } = options;
// 1. 创建隐藏窗口
printWindow = new BrowserWindow({
show: true, // true 仅调试时打开
webPreferences: {
nodeIntegration: false,
contextIsolation: false,
//sandbox: true
}
});
// 2. 加载 HTML
const loaded = once(printWindow.webContents, 'did-finish-load');
await printWindow.loadURL(makeDataURL(html));
await loaded;
// 3. 打印参数
const printOpts = {
silent: Boolean(silent),
printBackground: true,
color: true,
margins: {
marginType: 'custom',
top: margins?.top ?? 10,
bottom: margins?.bottom ?? 10,
left: margins?.left ?? 10,
right: margins?.right ?? 10
},
landscape: orientation === 'landscape',
pageSize: paperSize || 'A4'
};
// 4. 执行打印(带 60 秒超时)
const [success, reason] = await Promise.race([
new Promise(res => {
printWindow.webContents.print(printOpts, (ok, err) => res([ok, err]));
}),
new Promise((_, rej) =>
setTimeout(() => rej(new Error('打印超时(60s)')), 60_000)
)
]);
// 5. 清理窗口
if (printWindow && !printWindow.isDestroyed()) {
printWindow.close();
printWindow = null;
}
if (success) {
writeLog('info', '打印成功');
return { success: true };
}
throw new Error(reason || '打印失败');
} catch (err) {
writeLog('error', `打印出错: ${err.message}`);
// 确保异常时窗口也关掉
if (printWindow && !printWindow.isDestroyed()) {
printWindow.close();
}
return { success: false, error: err.message };
}
});
/**
* 打印HTML内容到文件(使用File System API)
*/
ipcMain.handle('print-film-to-file', async (event, options) => {
let printWindow = null;
const tempFiles = [];
try {
writeLog('info', `开始文件系统打印,选项: ${JSON.stringify(options)}`);
const { html, imageDataList, orientation, paperSize, silent, margins } = options;
// 1. 创建临时图片文件
const fs = await import('fs/promises');
const os = await import('os');
const path = await import('path');
const { fileURLToPath, pathToFileURL } = await import('url');
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'electron-print-'));
// 当前时间戳用于文件名
const timestamp = Date.now();
// 处理每个图片数据
const imageUrlMap = new Map();
for (let i = 0; i < imageDataList.length; i++) {
const { canvasId, data: base64Data } = imageDataList[i];
// 将base64转换为buffer
const buffer = Buffer.from(base64Data, 'base64');
// 生成临时文件路径
const tempFilePath = path.join(tempDir, `print-image-${timestamp}-${i}.png`);
// 写入文件
await fs.writeFile(tempFilePath, buffer);
tempFiles.push(tempFilePath);
writeLog('info', `临时图片文件已保存: ${tempFilePath}`);
// 使用Node.js内置方法构造file:// URL
const fileUrl = pathToFileURL(tempFilePath).href;
writeLog('info', `File URL构造: ${fileUrl}`);
writeLog('info', `原始路径: ${tempFilePath}`);
// 记录文件路径映射
imageUrlMap.set(canvasId, fileUrl);
}
// 2. 生成最终HTML,将占位符替换为文件URL
let finalHtml = html;
for (const [canvasId, fileUrl] of imageUrlMap.entries()) {
finalHtml = finalHtml.replace(
new RegExp(`]*>`, 'g'),
`
`
);
}
writeLog('info', `HTML占位符已替换完成,临时文件数量: ${tempFiles.length}`);
// 3. 包装完整的HTML文档
const fullHtml = `