utils.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. 'use strict';
  2. const { Controller } = require('ee-core');
  3. const { shell } = require('electron');
  4. const Addon = require('ee-core/addon');
  5. const { dialog } = require('electron');
  6. const fs = require('fs');
  7. const path = require('path');
  8. const CoreWindow = require('ee-core/electron/window');
  9. const { BrowserWindow, Menu,app } = require('electron');
  10. const { spawn } = require('child_process');
  11. const { readConfigFile } = require('../utils/config');
  12. const configDeault = readConfigFile();
  13. const errData = {
  14. msg :'请求失败,请联系管理员',
  15. code:999
  16. }
  17. const sharp = require('sharp'); // 确保安装:npm install sharp
  18. /**
  19. * example
  20. * @class
  21. */
  22. class UtilsController extends Controller {
  23. constructor(ctx) {
  24. super(ctx);
  25. }
  26. /**
  27. * 运行外部工具(如exe)
  28. * @param {{exeName?: string, exePath?: string, args?: string[]}} params
  29. */
  30. async runExternalTool (params = {}) {
  31. try {
  32. const { exeName, exePath, args = [] } = params;
  33. const targetName = exePath || exeName;
  34. if (!targetName) {
  35. throw new Error('缺少可执行文件名称');
  36. }
  37. const isPackaged = app.isPackaged;
  38. const execDir = isPackaged ? path.dirname(app.getPath('exe')) : path.join(app.getAppPath(), '.');
  39. const resourcesDir = process.resourcesPath;
  40. const candidates = [];
  41. const pushCandidate = (candidatePath, isAbsolute = false) => {
  42. if (!candidatePath) return;
  43. const normalized = isAbsolute || path.isAbsolute(candidatePath)
  44. ? candidatePath
  45. : path.join(execDir, candidatePath);
  46. if (!candidates.includes(normalized)) {
  47. candidates.push(normalized);
  48. }
  49. };
  50. // 允许前端显式传入候选路径数组
  51. if (Array.isArray(params.candidatePaths)) {
  52. params.candidatePaths.forEach((p) => pushCandidate(p));
  53. }
  54. if (path.isAbsolute(targetName)) {
  55. pushCandidate(targetName, true);
  56. } else {
  57. if (isPackaged) {
  58. pushCandidate(path.join(execDir, targetName), true);
  59. pushCandidate(path.join(resourcesDir, targetName), true);
  60. pushCandidate(path.join(resourcesDir, 'extraResources', targetName), true);
  61. } else {
  62. pushCandidate(targetName);
  63. pushCandidate(path.join('build', 'extraResources', targetName));
  64. pushCandidate(path.join('extraResources', targetName));
  65. }
  66. }
  67. console.log('=======');
  68. console.log(candidates);
  69. const resolvedPath = candidates.find((filePath) => filePath && fs.existsSync(filePath));
  70. if (!resolvedPath) {
  71. throw new Error(`未找到工具:${targetName}`);
  72. }
  73. // 如果已经有正在运行的子进程,则先关闭旧进程,再重新启动新的(保证始终只有一个进程,同时刷新页面参数)
  74. if (this.app.externalToolProcess && !this.app.externalToolProcess.killed) {
  75. try {
  76. this.app.externalToolProcess.kill();
  77. } catch (e) {
  78. console.warn('关闭旧 externalTool 进程失败(忽略继续启动新进程):', e);
  79. }
  80. this.app.externalToolProcess = null;
  81. }
  82. const child = spawn(resolvedPath, args, {
  83. cwd: path.dirname(resolvedPath),
  84. detached: true,
  85. stdio: 'ignore',
  86. windowsHide: false
  87. });
  88. // 记录子进程,方便后续复用/判断是否已退出
  89. this.app.externalToolProcess = child;
  90. child.on('exit', () => {
  91. if (this.app.externalToolProcess === child) {
  92. this.app.externalToolProcess = null;
  93. }
  94. });
  95. child.unref();
  96. return {
  97. code: 0,
  98. data: {
  99. path: resolvedPath,
  100. reused: false
  101. }
  102. };
  103. } catch (error) {
  104. console.error('runExternalTool error:', error);
  105. return {
  106. code: 1,
  107. msg: error.message || '运行外部工具失败'
  108. };
  109. }
  110. }
  111. /**
  112. * 所有方法接收两个参数
  113. * @param args 前端传的参数
  114. * @param event - ipc通信时才有值。详情见:控制器文档
  115. */
  116. /**
  117. * upload
  118. */
  119. async shellFun (params) {
  120. // 如果是打开路径操作,确保路径存在
  121. if (params.action === 'openMkPath') {
  122. try {
  123. // 确保目录存在,如果不存在就创建
  124. if (!fs.existsSync(params.params)) {
  125. fs.mkdirSync(params.params, { recursive: true });
  126. }
  127. shell.openPath(params.params)
  128. return;
  129. } catch (err) {
  130. console.error('创建目录失败:', err);
  131. // 即使创建目录失败,也尝试打开路径(可能已经存在)
  132. }
  133. }
  134. shell[params.action](params.params)
  135. }
  136. async openMain (config) {
  137. const { id, url } = config;
  138. if (this.app.electron[id]) {
  139. const win = this.app.electron[id];
  140. // 切换到指定的 URL
  141. await win.loadURL(url);
  142. win.focus();
  143. win.show();
  144. return;
  145. }
  146. const win = new BrowserWindow({
  147. ...config,
  148. webPreferences: {
  149. webSecurity: false,
  150. contextIsolation: false, // false -> 可在渲染进程中使用electron的api,true->需要bridge.js(contextBridge)
  151. nodeIntegration: true,
  152. // preload: path.join('../preload/preload.js','../preload/bridge.js'),
  153. },
  154. });
  155. await win.loadURL(config.url); // 设置窗口的 URL
  156. // 监听窗口关闭事件
  157. if(configDeault.debug) win.webContents.openDevTools()
  158. //
  159. win.on('close', () => {
  160. delete this.app.electron[config.id]; // 删除窗口引用
  161. });
  162. this.app.electron[config.id] = win ;
  163. }
  164. async openDirectory(optiops={
  165. title:"选择文件夹"
  166. }){
  167. const filePaths = dialog.showOpenDialogSync({
  168. title:optiops.title || '选择文件夹',
  169. properties: ['openDirectory']
  170. })
  171. if(filePaths[0]) return filePaths[0];
  172. return filePaths
  173. }
  174. /**
  175. * 关闭所有子窗口
  176. */
  177. closeAllWindows() {
  178. try {
  179. // 获取所有窗口
  180. const windows = this.app.electron;
  181. // 关闭除主窗口外的所有窗口
  182. for (const [id, window] of Object.entries(windows)) {
  183. if (!['mainWindow','extra'].includes(id)) { // 保留主窗口
  184. try {
  185. window.close();
  186. delete this.app.electron[id];
  187. } catch (error) {
  188. console.error(`关闭窗口 ${id} 失败:`, error);
  189. }
  190. }
  191. }
  192. console.log('所有子窗口已关闭');
  193. return { success: true, message: '所有子窗口已关闭' };
  194. } catch (error) {
  195. console.error('关闭所有窗口失败:', error);
  196. return { success: false, message: '关闭所有窗口失败: ' + error.message };
  197. }
  198. }
  199. async openImage(
  200. optiops= {
  201. title:"选择图片",
  202. filters:[
  203. { name: '支持JPG,png,gif', extensions: ['jpg','jpeg','png'] },
  204. ],
  205. }
  206. ){
  207. const filePaths = dialog.showOpenDialogSync({
  208. title:optiops.title || '选择图片',
  209. properties:['openFile'],
  210. filters:optiops.filters || [
  211. { name: '支持JPG,png,gif', extensions: ['jpg','jpeg','png'] },
  212. ]
  213. })
  214. const filePath = filePaths[0];
  215. const fileBuffer = fs.readFileSync(filePath);
  216. const base64Image = fileBuffer.toString('base64');
  217. // 获取文件扩展名
  218. const extension = path.extname(filePath).toLowerCase().replace('.', '');
  219. // 根据扩展名确定 MIME 类型
  220. let mimeType = '';
  221. switch (extension) {
  222. case 'jpg':
  223. case 'jpeg':
  224. mimeType = 'image/jpeg';
  225. break;
  226. case 'png':
  227. mimeType = 'image/png';
  228. break;
  229. case 'gif':
  230. mimeType = 'image/gif';
  231. break;
  232. default:
  233. mimeType = 'application/octet-stream'; // 默认 MIME 类型
  234. break;
  235. }
  236. // 构建 data URL
  237. const dataUrl = `data:${mimeType};base64,${base64Image}`;
  238. return {
  239. filePath:filePath,
  240. base64Image:dataUrl
  241. };
  242. }
  243. /**
  244. * 将前端生成的详情图数据写入 EXE 同级目录下的 output 文件夹。
  245. * @param {{ bundles: Array }} payload
  246. */
  247. async saveGeneratedImages (payload = {}) {
  248. try {
  249. const { app } = require('electron');
  250. const bundles = payload.bundles || [];
  251. if (!Array.isArray(bundles) || !bundles.length) {
  252. return { code: 1, msg: '无可保存的图片数据' };
  253. }
  254. // 运行目录:打包后为 EXE 所在目录,开发环境为项目根目录
  255. const isPackaged = app.isPackaged;
  256. const exeDir = isPackaged ? path.dirname(app.getPath('exe')) : process.cwd();
  257. const baseOutputDir = path.join(exeDir, 'output');
  258. if (!fs.existsSync(baseOutputDir)) {
  259. fs.mkdirSync(baseOutputDir, { recursive: true });
  260. }
  261. let fileCount = 0;
  262. const saveDataUrlToFile = (dataUrl, targetPath) => {
  263. if (!dataUrl || typeof dataUrl !== 'string') return;
  264. const match = dataUrl.match(/^data:image\/\w+;base64,(.+)$/);
  265. const base64Data = match ? match[1] : dataUrl;
  266. const buffer = Buffer.from(base64Data, 'base64');
  267. const dir = path.dirname(targetPath);
  268. if (!fs.existsSync(dir)) {
  269. fs.mkdirSync(dir, { recursive: true });
  270. }
  271. fs.writeFileSync(targetPath, buffer);
  272. fileCount += 1;
  273. };
  274. bundles.forEach(bundle => {
  275. if (!bundle) return;
  276. const styleNo = (bundle.styleNo || bundle.styleKey || 'UNKNOWN').toString().replace(/[\\\/:*?"<>|]/g, '_');
  277. const styleDir = path.join(baseOutputDir, styleNo);
  278. if (!fs.existsSync(styleDir)) {
  279. fs.mkdirSync(styleDir, { recursive: true });
  280. }
  281. // 单画布图片
  282. (bundle.images || []).forEach((img, idx) => {
  283. if (!img || typeof img.dataUrl !== 'string') return;
  284. if (img.dataUrl === 'model' || img.dataUrl === 'scene') return;
  285. if (!img.dataUrl.startsWith('data:image')) return;
  286. const fileName = `canvas_${img.canvasIndex || 0}_img_${idx}.jpg`;
  287. const targetPath = path.join(styleDir, fileName);
  288. saveDataUrlToFile(img.dataUrl, targetPath);
  289. });
  290. // 合成长图
  291. if (bundle.combined && bundle.combined.dataUrl) {
  292. const combinedPath = path.join(styleDir, 'all_canvases.jpg');
  293. saveDataUrlToFile(bundle.combined.dataUrl, combinedPath);
  294. }
  295. });
  296. return {
  297. code: 0,
  298. data: {
  299. outputDir: baseOutputDir,
  300. fileCount,
  301. },
  302. };
  303. } catch (error) {
  304. console.error('saveGeneratedImages error:', error);
  305. return {
  306. code: 1,
  307. msg: error.message || '保存生成图片失败',
  308. };
  309. }
  310. }
  311. async openFile(optiops= {
  312. title:"选择文件",
  313. filters:[
  314. { name: '支持JPG', extensions: ['jpg','jpeg'] },
  315. ],
  316. }){
  317. const filePaths = dialog.showOpenDialogSync({
  318. title:optiops.title || '选择文件',
  319. properties: ['openFile'],
  320. filters: optiops.filters || [
  321. { name: '选择文件' },
  322. ]
  323. })
  324. if(filePaths[0]) return filePaths[0];
  325. return filePaths
  326. }
  327. getAppConfig(){
  328. const config = readConfigFile()
  329. const appPath = path.join(app.getAppPath(), '../..');
  330. const pyPath = path.join(path.dirname(appPath), 'extraResources', 'py');
  331. return {
  332. ...config,
  333. userDataPath: app.getPath('userData'),
  334. appPath: appPath,
  335. pyPath:pyPath,
  336. }
  337. }
  338. async readFileImageForPath(filePath,maxWidth=1500){
  339. const getMimeType = (fileName)=>{
  340. const extension = path.extname(fileName).toLowerCase().replace('.', '');
  341. let mimeType = '';
  342. switch (extension) {
  343. case 'jpg':
  344. case 'jpeg':
  345. mimeType = 'image/jpeg';
  346. break;
  347. case 'png':
  348. mimeType = 'image/png';
  349. break;
  350. case 'gif':
  351. mimeType = 'image/gif';
  352. break;
  353. case 'webp':
  354. mimeType = 'image/webp';
  355. break;
  356. case 'avif':
  357. mimeType = 'image/avif';
  358. break;
  359. default:
  360. mimeType = 'application/octet-stream';
  361. break;
  362. }
  363. return mimeType;
  364. }
  365. try {
  366. const fileName = path.basename(filePath);
  367. const image = sharp(filePath);
  368. const metadata = await image.metadata();
  369. let mimeType = getMimeType(fileName); // 调用下面定义的私有方法获取 MIME 类型
  370. let fileBuffer;
  371. if (metadata.width > maxWidth) {
  372. // 如果宽度大于 1500px,压缩至 1500px 宽度,保持比例
  373. fileBuffer = await image.resize(maxWidth).toBuffer();
  374. } else {
  375. // 否则直接读取原图
  376. fileBuffer = fs.readFileSync(filePath);
  377. }
  378. return {
  379. fileBuffer,
  380. fileName,
  381. mimeType
  382. };
  383. } catch (error) {
  384. console.error('Error processing image:', error);
  385. throw error;
  386. }
  387. }
  388. }
  389. UtilsController.toString = () => '[class ExampleController]';
  390. module.exports = UtilsController;