'use strict'; const { Controller } = require('ee-core'); const { shell } = require('electron'); const Addon = require('ee-core/addon'); const { dialog } = require('electron'); const fs = require('fs'); const path = require('path'); const CoreWindow = require('ee-core/electron/window'); const { BrowserWindow, Menu,app } = require('electron'); const { spawn } = require('child_process'); const { readConfigFile } = require('../utils/config'); const configDeault = readConfigFile(); const errData = { msg :'请求失败,请联系管理员', code:999 } const sharp = require('sharp'); // 确保安装:npm install sharp /** * example * @class */ class UtilsController extends Controller { constructor(ctx) { super(ctx); } /** * 运行外部工具(如exe) * @param {{exeName?: string, exePath?: string, args?: string[]}} params */ async runExternalTool (params = {}) { try { const { exeName, exePath, args = [] } = params; const targetName = exePath || exeName; if (!targetName) { throw new Error('缺少可执行文件名称'); } const isPackaged = app.isPackaged; const execDir = isPackaged ? path.dirname(app.getPath('exe')) : path.join(app.getAppPath(), '.'); const resourcesDir = process.resourcesPath; const candidates = []; const pushCandidate = (candidatePath, isAbsolute = false) => { if (!candidatePath) return; const normalized = isAbsolute || path.isAbsolute(candidatePath) ? candidatePath : path.join(execDir, candidatePath); if (!candidates.includes(normalized)) { candidates.push(normalized); } }; // 允许前端显式传入候选路径数组 if (Array.isArray(params.candidatePaths)) { params.candidatePaths.forEach((p) => pushCandidate(p)); } if (path.isAbsolute(targetName)) { pushCandidate(targetName, true); } else { if (isPackaged) { pushCandidate(path.join(execDir, targetName), true); pushCandidate(path.join(resourcesDir, targetName), true); pushCandidate(path.join(resourcesDir, 'extraResources', targetName), true); } else { pushCandidate(targetName); pushCandidate(path.join('build', 'extraResources', targetName)); pushCandidate(path.join('extraResources', targetName)); } } console.log('======='); console.log(candidates); const resolvedPath = candidates.find((filePath) => filePath && fs.existsSync(filePath)); if (!resolvedPath) { throw new Error(`未找到工具:${targetName}`); } // 如果已经有正在运行的子进程,则先关闭旧进程,再重新启动新的(保证始终只有一个进程,同时刷新页面参数) if (this.app.externalToolProcess && !this.app.externalToolProcess.killed) { try { this.app.externalToolProcess.kill(); } catch (e) { console.warn('关闭旧 externalTool 进程失败(忽略继续启动新进程):', e); } this.app.externalToolProcess = null; } const child = spawn(resolvedPath, args, { cwd: path.dirname(resolvedPath), detached: true, stdio: 'ignore', windowsHide: false }); // 记录子进程,方便后续复用/判断是否已退出 this.app.externalToolProcess = child; child.on('exit', () => { if (this.app.externalToolProcess === child) { this.app.externalToolProcess = null; } }); child.unref(); return { code: 0, data: { path: resolvedPath, reused: false } }; } catch (error) { console.error('runExternalTool error:', error); return { code: 1, msg: error.message || '运行外部工具失败' }; } } /** * 所有方法接收两个参数 * @param args 前端传的参数 * @param event - ipc通信时才有值。详情见:控制器文档 */ /** * upload */ async shellFun (params) { // 如果是打开路径操作,确保路径存在 if (params.action === 'openMkPath') { try { // 确保目录存在,如果不存在就创建 if (!fs.existsSync(params.params)) { fs.mkdirSync(params.params, { recursive: true }); } shell.openPath(params.params) return; } catch (err) { console.error('创建目录失败:', err); // 即使创建目录失败,也尝试打开路径(可能已经存在) } } shell[params.action](params.params) } async openMain (config) { const { id, url } = config; if (this.app.electron[id]) { const win = this.app.electron[id]; // 切换到指定的 URL await win.loadURL(url); win.focus(); win.show(); return; } const win = new BrowserWindow({ ...config, webPreferences: { webSecurity: false, contextIsolation: false, // false -> 可在渲染进程中使用electron的api,true->需要bridge.js(contextBridge) nodeIntegration: true, // preload: path.join('../preload/preload.js','../preload/bridge.js'), }, }); await win.loadURL(config.url); // 设置窗口的 URL // 监听窗口关闭事件 if(configDeault.debug) win.webContents.openDevTools() // win.on('close', () => { delete this.app.electron[config.id]; // 删除窗口引用 }); this.app.electron[config.id] = win ; } async openDirectory(optiops={ title:"选择文件夹" }){ const filePaths = dialog.showOpenDialogSync({ title:optiops.title || '选择文件夹', properties: ['openDirectory'] }) if(filePaths[0]) return filePaths[0]; return filePaths } /** * 关闭所有子窗口 */ closeAllWindows() { try { // 获取所有窗口 const windows = this.app.electron; // 关闭除主窗口外的所有窗口 for (const [id, window] of Object.entries(windows)) { if (!['mainWindow','extra'].includes(id)) { // 保留主窗口 try { window.close(); delete this.app.electron[id]; } catch (error) { console.error(`关闭窗口 ${id} 失败:`, error); } } } console.log('所有子窗口已关闭'); return { success: true, message: '所有子窗口已关闭' }; } catch (error) { console.error('关闭所有窗口失败:', error); return { success: false, message: '关闭所有窗口失败: ' + error.message }; } } async openImage( optiops= { title:"选择图片", filters:[ { name: '支持JPG,png,gif', extensions: ['jpg','jpeg','png'] }, ], } ){ const filePaths = dialog.showOpenDialogSync({ title:optiops.title || '选择图片', properties:['openFile'], filters:optiops.filters || [ { name: '支持JPG,png,gif', extensions: ['jpg','jpeg','png'] }, ] }) const filePath = filePaths[0]; const fileBuffer = fs.readFileSync(filePath); const base64Image = fileBuffer.toString('base64'); // 获取文件扩展名 const extension = path.extname(filePath).toLowerCase().replace('.', ''); // 根据扩展名确定 MIME 类型 let mimeType = ''; switch (extension) { case 'jpg': case 'jpeg': mimeType = 'image/jpeg'; break; case 'png': mimeType = 'image/png'; break; case 'gif': mimeType = 'image/gif'; break; default: mimeType = 'application/octet-stream'; // 默认 MIME 类型 break; } // 构建 data URL const dataUrl = `data:${mimeType};base64,${base64Image}`; return { filePath:filePath, base64Image:dataUrl }; } /** * 将前端生成的详情图数据写入 EXE 同级目录下的 output 文件夹。 * @param {{ bundles: Array }} payload */ async saveGeneratedImages (payload = {}) { try { const { app } = require('electron'); const bundles = payload.bundles || []; if (!Array.isArray(bundles) || !bundles.length) { return { code: 1, msg: '无可保存的图片数据' }; } // 运行目录:打包后为 EXE 所在目录,开发环境为项目根目录 const isPackaged = app.isPackaged; const exeDir = isPackaged ? path.dirname(app.getPath('exe')) : process.cwd(); const baseOutputDir = path.join(exeDir, 'output'); if (!fs.existsSync(baseOutputDir)) { fs.mkdirSync(baseOutputDir, { recursive: true }); } let fileCount = 0; const saveDataUrlToFile = (dataUrl, targetPath) => { if (!dataUrl || typeof dataUrl !== 'string') return; const match = dataUrl.match(/^data:image\/\w+;base64,(.+)$/); const base64Data = match ? match[1] : dataUrl; const buffer = Buffer.from(base64Data, 'base64'); const dir = path.dirname(targetPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(targetPath, buffer); fileCount += 1; }; bundles.forEach(bundle => { if (!bundle) return; const styleNo = (bundle.styleNo || bundle.styleKey || 'UNKNOWN').toString().replace(/[\\\/:*?"<>|]/g, '_'); const styleDir = path.join(baseOutputDir, styleNo); if (!fs.existsSync(styleDir)) { fs.mkdirSync(styleDir, { recursive: true }); } // 单画布图片 (bundle.images || []).forEach((img, idx) => { if (!img || typeof img.dataUrl !== 'string') return; if (img.dataUrl === 'model' || img.dataUrl === 'scene') return; if (!img.dataUrl.startsWith('data:image')) return; const fileName = `canvas_${img.canvasIndex || 0}_img_${idx}.jpg`; const targetPath = path.join(styleDir, fileName); saveDataUrlToFile(img.dataUrl, targetPath); }); // 合成长图 if (bundle.combined && bundle.combined.dataUrl) { const combinedPath = path.join(styleDir, 'all_canvases.jpg'); saveDataUrlToFile(bundle.combined.dataUrl, combinedPath); } }); return { code: 0, data: { outputDir: baseOutputDir, fileCount, }, }; } catch (error) { console.error('saveGeneratedImages error:', error); return { code: 1, msg: error.message || '保存生成图片失败', }; } } async openFile(optiops= { title:"选择文件", filters:[ { name: '支持JPG', extensions: ['jpg','jpeg'] }, ], }){ const filePaths = dialog.showOpenDialogSync({ title:optiops.title || '选择文件', properties: ['openFile'], filters: optiops.filters || [ { name: '选择文件' }, ] }) if(filePaths[0]) return filePaths[0]; return filePaths } getAppConfig(){ const config = readConfigFile() const appPath = path.join(app.getAppPath(), '../..'); const pyPath = path.join(path.dirname(appPath), 'extraResources', 'py'); return { ...config, userDataPath: app.getPath('userData'), appPath: appPath, pyPath:pyPath, } } async readFileImageForPath(filePath,maxWidth=1500){ const getMimeType = (fileName)=>{ const extension = path.extname(fileName).toLowerCase().replace('.', ''); let mimeType = ''; switch (extension) { case 'jpg': case 'jpeg': mimeType = 'image/jpeg'; break; case 'png': mimeType = 'image/png'; break; case 'gif': mimeType = 'image/gif'; break; case 'webp': mimeType = 'image/webp'; break; case 'avif': mimeType = 'image/avif'; break; default: mimeType = 'application/octet-stream'; break; } return mimeType; } try { const fileName = path.basename(filePath); const image = sharp(filePath); const metadata = await image.metadata(); let mimeType = getMimeType(fileName); // 调用下面定义的私有方法获取 MIME 类型 let fileBuffer; if (metadata.width > maxWidth) { // 如果宽度大于 1500px,压缩至 1500px 宽度,保持比例 fileBuffer = await image.resize(maxWidth).toBuffer(); } else { // 否则直接读取原图 fileBuffer = fs.readFileSync(filePath); } return { fileBuffer, fileName, mimeType }; } catch (error) { console.error('Error processing image:', error); throw error; } } } UtilsController.toString = () => '[class ExampleController]'; module.exports = UtilsController;