tray.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. // tray = 系统托盘
  2. import path from 'path';
  3. import { Tray, Menu, app, dialog, nativeImage, BrowserWindow, Notification, ipcMain } from 'electron';
  4. import type { IpcMainInvokeEvent } from 'electron';
  5. import {_PATHS} from '../paths';
  6. import {$env, isDev} from '../env';
  7. const TrayIcons = {
  8. // 代码逻辑说明: 【JHHB-13】桌面应用消息通知
  9. normal: nativeImage.createFromPath(
  10. process.platform === 'win32'
  11. ? path.join(_PATHS.publicRoot, 'logo.png')
  12. : path.join(_PATHS.electronRoot, './icons/mac/tray-icon.png').replace(/[\\/]dist[\\/]/, '/')
  13. ),
  14. empty: nativeImage.createEmpty(),
  15. };
  16. // 创建托盘图标
  17. export function createTray(win: BrowserWindow) {
  18. const tray = new Tray(TrayIcons.normal);
  19. const TrayUtils = useTray(tray, win);
  20. tray.setToolTip($env.VITE_GLOB_APP_TITLE! + (isDev ? ' (开发环境)' : ''));
  21. // 左键托盘图标显示主窗口
  22. tray.on('click', () => TrayUtils.showMainWindow());
  23. // 右键托盘图标显示托盘菜单
  24. tray.on('right-click', () => showTrayContextMenu());
  25. function showTrayContextMenu() {
  26. const trayContextMenu = getTrayMenus(win, TrayUtils);
  27. // 弹出托盘菜单,不使用 setContextMenu 方法是因为要实时更新菜单内容
  28. tray.popUpContextMenu(trayContextMenu);
  29. }
  30. }
  31. export function useTray(tray: Tray, win: BrowserWindow) {
  32. let isBlinking = false;
  33. let blinkTimer: NodeJS.Timeout | null = null;
  34. function showMainWindow() {
  35. win.show();
  36. }
  37. // 开始闪动
  38. function startBlink() {
  39. isBlinking = true;
  40. tray.setImage(TrayIcons.empty);
  41. blinkTimer = setTimeout(() => {
  42. tray.setImage(TrayIcons.normal);
  43. setTimeout(() => {
  44. if (isBlinking) {
  45. startBlink();
  46. }
  47. }, 500);
  48. }, 500);
  49. }
  50. // 结束闪动
  51. function stopBlink() {
  52. isBlinking = false;
  53. if (blinkTimer) {
  54. clearTimeout(blinkTimer);
  55. blinkTimer = null;
  56. }
  57. tray.setImage(TrayIcons.normal);
  58. }
  59. ipcMain.on('tray-flash', (event: IpcMainInvokeEvent) => {
  60. // 仅在 Windows 系统中闪烁
  61. if (process.platform === 'win32') {
  62. startBlink();
  63. }
  64. });
  65. ipcMain.on('tray-flash-stop', (event: IpcMainInvokeEvent) => {
  66. // 仅在 Windows 系统中停止闪烁
  67. if (process.platform === 'win32') {
  68. stopBlink();
  69. }
  70. });
  71. win.on('focus', () => {
  72. stopBlink();
  73. });
  74. // 发送桌面通知
  75. function sendDesktopNotice() {
  76. // 判断是否支持桌面通知
  77. if (!Notification.isSupported()) {
  78. // todo 实际开发中不需要提示,直接返回或者换一种提示方式
  79. dialog.showMessageBoxSync(win, {
  80. type: 'error',
  81. title: '错误',
  82. message: '当前系统不支持桌面通知',
  83. });
  84. return;
  85. }
  86. const ins = new Notification({
  87. title: '通知标题',
  88. body: '通知内容第一行\n通知内容第二行',
  89. // icon: TrayIcons.normal.resize({width: 32, height: 32}),
  90. });
  91. ins.on('click', () => {
  92. dialog.showMessageBoxSync(win, {
  93. type: 'info',
  94. title: '提示',
  95. message: '通知被点击',
  96. });
  97. });
  98. ins.show();
  99. }
  100. return {
  101. showMainWindow,
  102. startBlink,
  103. stopBlink,
  104. isBlinking: () => isBlinking,
  105. sendDesktopNotice,
  106. };
  107. }
  108. const MenuIcon = {
  109. exit: nativeImage
  110. .createFromDataURL(
  111. 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACJ0lEQVR4nH1TzWvUQBRP7fpxsWqVXsSLiAevRWhhN28msRJo981kay4WRBCF/QdEFJpbaUHw4kFBQTwUKX4gKh48KPiBBcGLJ1F0uzPZ7ibWXf0DIjObielS+mDIm/fxm9/85sWyBixN06E0CIaV3wB2XhC8puOWNZSG4Y7B+k2mi7Kl9l2n9rHnzvbWJoLRYn7r5jTViQjwzM8ynlC+AFyVgN2NU8G+Rnn6QETx3FfP223A/jeHfWqCsAUJ7Hlryh9Te0nYqiDsz9rE6VHVIABvNwEf/ADYk4OsZPeVFbwiCHtcZBVR9k4CJhJmDuUxwEVJ8H4fINOkC9Vjbeq/UTR1IgPturX3f93Z35+B7ddxgJL6dih/skF9zE9KCJ//5bDLpii1+npIuzolKTubC5gBxzarJo6vWWjrUP+etFlF+ds9lRFOXalN+NPEmxvRDS3KH34v8+PFIgNmTh0EahH+InGCwzoQEbYcuTMnlR8aYbaxGHFvRNiznssP6sA65UsxrdU1+hYnFhlpAGAkdvzlPLFu88mY8pcrVjCsxcqGapC2eYW249/tUH4xS4QaVQLeigi/YWJqPl4DlNRSrAwzSaoXIspeWUYrI9qXINglgT1qAt5JPG+kkNN5BSAJuyoJfhAVdmST4PlPBFASNs6rIgnspqC8HlF+SQAuRQTfKpYiEy6fwuIdP42P71T+t0l/TBKcE8AXm4DXBfB6w50+apgUhf4HZ5j+Z5+zNTAAAAAASUVORK5CYII='
  112. )
  113. .resize({
  114. width: 16,
  115. height: 16,
  116. }),
  117. };
  118. // 设置托盘菜单
  119. function getTrayMenus(win: BrowserWindow, TrayUtils: ReturnType<typeof useTray>) {
  120. const {startBlink, stopBlink, sendDesktopNotice} = TrayUtils;
  121. const isBlinking = TrayUtils.isBlinking();
  122. return Menu.buildFromTemplate([
  123. ...(isDev
  124. ? [
  125. {
  126. label: '开发工具',
  127. submenu: [
  128. {
  129. label: '以下菜单仅显示在开发环境',
  130. sublabel: '当前为开发环境',
  131. enabled: false,
  132. },
  133. {type: 'separator'},
  134. {
  135. label: '切换 DevTools',
  136. click: () => win.webContents.toggleDevTools(),
  137. },
  138. {
  139. label: `托盘图标${isBlinking ? '停止' : '开始'}闪烁`,
  140. sublabel: '模拟新消息提醒',
  141. click: () => (isBlinking ? stopBlink() : startBlink()),
  142. },
  143. {
  144. label: '发送桌面通知示例',
  145. click: () => sendDesktopNotice(),
  146. },
  147. ],
  148. },
  149. {type: 'separator'},
  150. ]
  151. : ([] as any)),
  152. {
  153. label: '显示主窗口',
  154. // 文件图标
  155. icon: TrayIcons.normal.resize({width: 16, height: 16}),
  156. click: () => win.show(),
  157. },
  158. {type: 'separator'},
  159. {
  160. label: '退出',
  161. // base64图标
  162. icon: MenuIcon.exit,
  163. click: () => {
  164. // 弹出是否确认退出提示框
  165. const choice = dialog.showMessageBoxSync(win, {
  166. type: 'question',
  167. title: '提示',
  168. message: '确定要退出应用吗?',
  169. buttons: ['退出', '取消'],
  170. defaultId: 1,
  171. cancelId: 1,
  172. noLink: true,
  173. });
  174. // 用户选择了退出,直接 exit
  175. if (choice === 0) {
  176. // global.isQuitting = true;
  177. app.exit(0);
  178. }
  179. },
  180. },
  181. ]);
  182. }