main.cjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. const { app, BrowserWindow, nativeTheme, shell, protocol, net } = require('electron');
  2. const fs = require('node:fs');
  3. const path = require('node:path');
  4. const { pathToFileURL } = require('node:url');
  5. const { registerIpcHandlers } = require('./ipc/index.cjs');
  6. const { setupAutoUpdate, checkAndDownloadUpdate, triggerUpdateDownload, quitAndInstall } = require('./services/updateService.cjs');
  7. const { getGeneratedImagesDir, getImportedImagesDir } = require('./utils/paths.cjs');
  8. const rendererUrl = process.env.ELECTRON_RENDERER_URL;
  9. const iconPath = path.join(__dirname, '../assets/icon.ico');
  10. const packagedIndexUrl = pathToFileURL(path.join(__dirname, '../dist/index.html')).toString();
  11. protocol.registerSchemesAsPrivileged([{
  12. scheme: 'yibiao-asset',
  13. privileges: { standard: true, secure: true, supportFetchAPI: true },
  14. }]);
  15. function registerAssetProtocol() {
  16. protocol.handle('yibiao-asset', (request) => {
  17. try {
  18. const url = new URL(request.url);
  19. const assetRoots = {
  20. 'generated-images': getGeneratedImagesDir(app),
  21. 'imported-images': getImportedImagesDir(app),
  22. };
  23. const rootDir = assetRoots[url.hostname];
  24. if (!rootDir) {
  25. return new Response('Not found', { status: 404 });
  26. }
  27. const relativePath = decodeURIComponent(url.pathname.replace(/^\/+/, ''));
  28. if (!relativePath) {
  29. return new Response('Not found', { status: 404 });
  30. }
  31. const baseDir = path.resolve(rootDir);
  32. const filePath = path.resolve(baseDir, relativePath);
  33. if (filePath !== baseDir && !filePath.startsWith(`${baseDir}${path.sep}`)) {
  34. return new Response('Forbidden', { status: 403 });
  35. }
  36. if (!fs.existsSync(filePath)) {
  37. return new Response('Not found', { status: 404 });
  38. }
  39. return net.fetch(pathToFileURL(filePath).toString());
  40. } catch {
  41. return new Response('Invalid asset url', { status: 400 });
  42. }
  43. });
  44. }
  45. function normalizeExternalUrl(value) {
  46. const raw = String(value || '').trim();
  47. if (!raw) return null;
  48. const candidate = /^www\./i.test(raw) ? `https://${raw}` : raw;
  49. try {
  50. const url = new URL(candidate);
  51. return ['http:', 'https:'].includes(url.protocol) ? url.toString() : null;
  52. } catch {
  53. return null;
  54. }
  55. }
  56. function isAllowedAppNavigation(value) {
  57. try {
  58. const url = new URL(value);
  59. if (rendererUrl) {
  60. return url.origin === new URL(rendererUrl).origin;
  61. }
  62. const indexUrl = new URL(packagedIndexUrl);
  63. return url.protocol === 'file:' && url.pathname === indexUrl.pathname;
  64. } catch {
  65. return false;
  66. }
  67. }
  68. async function openExternalUrl(value) {
  69. const externalUrl = normalizeExternalUrl(value);
  70. if (!externalUrl) return;
  71. try {
  72. await shell.openExternal(externalUrl);
  73. } catch (error) {
  74. const preview = externalUrl.length > 300 ? `${externalUrl.slice(0, 300)}...` : externalUrl;
  75. console.warn('[electron] 打开外部链接失败', { url: preview, message: error.message || String(error) });
  76. }
  77. }
  78. function createMainWindow() {
  79. const mainWindow = new BrowserWindow({
  80. width: 1440,
  81. height: 920,
  82. minWidth: 1040,
  83. minHeight: 720,
  84. backgroundColor: '#f8fafd',
  85. title: '易标投标工具箱',
  86. icon: fs.existsSync(iconPath) ? iconPath : undefined,
  87. titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
  88. webPreferences: {
  89. preload: path.join(__dirname, 'preload.cjs'),
  90. contextIsolation: true,
  91. nodeIntegration: false,
  92. sandbox: false,
  93. },
  94. });
  95. mainWindow.setMenuBarVisibility(false);
  96. if (rendererUrl) {
  97. mainWindow.loadURL(rendererUrl);
  98. } else {
  99. mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
  100. }
  101. mainWindow.webContents.setWindowOpenHandler(({ url }) => {
  102. void openExternalUrl(url);
  103. return { action: 'deny' };
  104. });
  105. mainWindow.webContents.on('will-navigate', (event, url) => {
  106. if (isAllowedAppNavigation(url)) {
  107. return;
  108. }
  109. event.preventDefault();
  110. void openExternalUrl(url);
  111. });
  112. return mainWindow;
  113. }
  114. app.whenReady().then(() => {
  115. nativeTheme.themeSource = 'light';
  116. registerAssetProtocol();
  117. const mainWindow = createMainWindow();
  118. registerIpcHandlers({ app, mainWindow, checkAndDownloadUpdate, triggerUpdateDownload, quitAndInstall });
  119. setupAutoUpdate({ app, mainWindow });
  120. app.on('activate', () => {
  121. if (BrowserWindow.getAllWindows().length === 0) {
  122. createMainWindow();
  123. }
  124. });
  125. });
  126. app.on('window-all-closed', () => {
  127. if (process.platform !== 'darwin') {
  128. app.quit();
  129. }
  130. });