弹窗通知模块

Electron弹窗通知模块实现(主进程+渲染进程IPC)

#tech / dev / pm #type / snippet #status / growing

代码实现

旧代码(ai 生成的代码)

一个主进程文件 创建窗口、注册 ipc
三个渲染进程文件 调用 ipc、生成页面样式

主进程代码

主进程的代码
import { BrowserWindow, ipcMain, screen } from "electron";
import path from "node:path";

const notificationWindows = new Map<string, BrowserWindow>();

const NOTIFICATION_WIDTH = 620;
const NOTIFICATION_HEIGHT = 920;
const NOTIFICATION_MARGIN = 10;

function getNotificationPosition(): { x: number; y: number } {
  const primaryDisplay = screen.getPrimaryDisplay();
  const { width: screenWidth } = primaryDisplay.workAreaSize;
  const x = screenWidth - NOTIFICATION_WIDTH - NOTIFICATION_MARGIN;
  const y =
    NOTIFICATION_MARGIN +
    notificationWindows.size * (NOTIFICATION_HEIGHT + NOTIFICATION_MARGIN);
  return { x, y };
}

function reorderNotifications() {
  let index = 0;
  for (const [, window] of notificationWindows) {
    const y =
      NOTIFICATION_MARGIN + index * (NOTIFICATION_HEIGHT + NOTIFICATION_MARGIN);
    window.setPosition(window.getPosition()[0], y);
    index++;
  }
}

export function setupNotificationService(
  mainWindow: BrowserWindow,
  MAIN_DIST: string,
  RENDERER_DIST: string,
  VITE_DEV_SERVER_URL: string | undefined
) {
  ipcMain.handle(
    "show-notification",
    async (
      _event,
      options: {
        id: string;
        title: string;
        body: string;
        icon?: string;
        urgency?: "normal" | "critical" | "low";
        actions?: Array<{
          text: string;
          type: "confirm" | "cancel" | "action";
        }>;
      }
    ) => {
      if (!mainWindow) {
        return;
      }

      if (notificationWindows.has(options.id)) {
        const existingWindow = notificationWindows.get(options.id);
        existingWindow?.close();
        notificationWindows.delete(options.id);
        reorderNotifications();
      }

      const { x, y } = getNotificationPosition();

      const notificationWindow = new BrowserWindow({
        width: NOTIFICATION_WIDTH,
        height: NOTIFICATION_HEIGHT,
        x,
        y,
        frame: false,
        transparent: true,
        resizable: false,
        skipTaskbar: true,
        alwaysOnTop: true,
        show: false,
        backgroundColor: "#00000000",
        webPreferences: {
          preload: path.join(MAIN_DIST, "main_preload.mjs"),
          contextIsolation: true,
          nodeIntegration: true,
          webSecurity: false,
        },
      });

      notificationWindow.webContents.session.webRequest.onHeadersReceived(
        (details, callback) => {
          callback({
            responseHeaders: {
              ...details.responseHeaders,
              "Content-Security-Policy": [
                "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
              ],
            },
          });
        }
      );

      notificationWindows.set(options.id, notificationWindow);

      notificationWindow.on("closed", () => {
        notificationWindows.delete(options.id);
        reorderNotifications();
      });

      const queryParams = new URLSearchParams({
        id: options.id,
        title: options.title,
        body: options.body,
        urgency: options.urgency || "normal",
      });

      if (options.icon) {
        queryParams.append("icon", options.icon);
      }

      if (options.actions) {
        queryParams.append(
          "actions",
          encodeURIComponent(JSON.stringify(options.actions))
        );
      }

      const notificationUrl = VITE_DEV_SERVER_URL
        ? `${VITE_DEV_SERVER_URL}#/notification?${queryParams.toString()}`
        : `file://${RENDERER_DIST}/index.html#/notification?${queryParams.toString()}`;

      await notificationWindow.loadURL(notificationUrl);

      notificationWindow.show();

      return options.id;
    }
  );

  ipcMain.on("close-notification", (_event, id: string) => {
    const window = notificationWindows.get(id);
    if (window && !window.isDestroyed()) {
      window.close();
    }
  });

  ipcMain.on(
    "notification-action",
    (_event, id: string, action: { text: string; type: string }) => {
      const window = notificationWindows.get(id);
      if (window && !window.isDestroyed()) {
        const serializedAction = {
          text: action.text,
          type: action.type,
        };
        if (action.type === "confirm" || action.type === "cancel") {
          window.close();
        }
        mainWindow.webContents.send(
          "notification-action-received",
          id,
          serializedAction
        );
      }
    }
  );
}
前端的服务代码
interface NotificationOptions {
  title: string;
  body: string;
  icon?: string;
  urgency?: "normal" | "critical" | "low";
  actions?: Array<{
    text: string;
    type: "confirm" | "cancel" | "action";
  }>;
}

export class NotificationService {
  private static instance: NotificationService;
  private notificationCount = 0;

  private constructor() {
    // 添加空值检查
    if (!window.shared?.ipcRenderer) {
      console.error("Electron IPC Renderer is not available");
      return;
    }
    // 监听通知动作
    window.shared.ipcRenderer.on(
      "notification-action",
      (_event: any, id: string, action: any) => {}
    );
  }

  public static getInstance(): NotificationService {
    if (!NotificationService.instance) {
      NotificationService.instance = new NotificationService();
    }
    return NotificationService.instance;
  }

  private generateId(): string {
    const id = `notification-${Date.now()}-${this.notificationCount++}`;

    return id;
  }

  /**
   * 显示桌面通知,关键 show 函数
   * @param options 通知选项
   */
  public async show(options: NotificationOptions): Promise<string> {
    const id = this.generateId();
    try {
      const result = await window.shared.ipcRenderer.invoke(
        "show-notification",
        {
          id,
          ...options,
        }
      );
      return result;
    } catch (error) {
      console.error("显示通知失败:", error);
      return id;
    }
  }

  /**
   * 显示简单通知
   * @param title 标题
   * @param message 消息内容
   */
  public async showSimple(title: string, message: string): Promise<string> {
    return await this.show({
      title,
      body: message,
      urgency: "normal",
    });
  }

  /**
   * 显示警告通知
   * @param title 标题
   * @param message 消息内容
   */
  public async showWarning(title: string, message: string): Promise<string> {
    return await this.show({
      title,
      body: message,
      urgency: "critical",
      actions: [{ text: "我知道了", type: "confirm" }],
    });
  }
}

// 导出单例实例
export const notification = NotificationService.getInstance();

上面的旧代码把 ipc 层、service 层、window 层的代码都放在一起了,不够干净
并且初始化时需要传入主进程路径、渲染进程路径等参数,但实际上可以直接从主 main.ts 文件中导入写好

渲染进程代码

NotificationPage 组件
<template>
  <NotificationWindow
    v-if="notificationData"
    v-bind="notificationData"
    @action="handleAction"
    @close="handleClose"
  />
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import NotificationWindow from './NotificationWindow.vue';

const route = useRoute();
const notificationData = ref<any>(null);

onMounted(() => {
  // 从URL参数中获取通知数据
  const params = Object.fromEntries(new URLSearchParams(route.query as any));
  notificationData.value = params;
});

const handleAction = (action: any) => {
  window.shared.send('notification-action', notificationData.value?.id, action);
};

const handleClose = () => {
  window.shared.send('close-notification', notificationData.value?.id);
};
</script>

<style scoped>

</style>
NotificationWindow 组件
<template>
  <div class="notification-window" :class="urgency">
    <div class="notification-content">
      <div class="notification-header">
        <img v-if="icon" :src="icon" class="notification-icon" />
        <span class="notification-title">{{ title }}</span>
        <button class="close-btn" @click="close">×</button>
      </div>
      <div class="notification-body">{{ body }}</div>
      <div v-if="actions && actions.length" class="notification-actions">
        <button
          v-for="action in actions"
          :key="action.text"
          @click="handleAction({ text: action.text, type: action.type })"
          :class="action.type"
        >
          {{ action.text }}
        </button>
      </div>
    </div>
    <div class="progress-bar" :style="{ width: `${progressWidth}%` }"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { useRoute } from "vue-router";

// 从路由参数中获取数据
const route = useRoute();
const title = ref(route.query.title as string);
const body = ref(route.query.body as string);
const icon = ref(route.query.icon as string);
const urgency = ref(route.query.urgency as string);
const id = ref(route.query.id as string);

// 进度条宽度
const progressWidth = ref(100);
let progressInterval: NodeJS.Timeout | null = null;

// 解析 actions 参数
let actions = ref<Array<{ text: string; type: string }>>([]);
if (route.query.actions) {
  try {
    const actionsStr = route.query.actions as string;
    actions.value = JSON.parse(decodeURIComponent(actionsStr));
  } catch (e) {
    console.error("Failed to parse actions:", e);
  }
}

const close = () => {
  if (progressInterval) {
    clearInterval(progressInterval);
    progressInterval = null;
  }
  if (window.shared?.ipcRenderer) {
    window.shared.ipcRenderer.send("close-notification", id.value);
  }
};

const handleAction = (action: { text: string; type: string }) => {
  const serializedAction = {
    text: action.text,
    type: action.type,
  };
  if (window.shared?.ipcRenderer) {
    window.shared.ipcRenderer.send(
      "notification-action",
      id.value,
      serializedAction
    );
  }
};

onMounted(() => {
  console.log("通知窗口已挂载,参数:", {
    title: title.value,
    body: body.value,
    urgency: urgency.value,
    actions: actions.value,
  });

  // 如果不是 critical 级别,3秒后自动关闭
  if (urgency.value !== "critical") {
    // 设置进度条动画
    const DURATION = 3000; // 3秒
    const INTERVAL = 50; // 50毫秒更新一次
    const STEP = (100 * INTERVAL) / DURATION;

    progressInterval = setInterval(() => {
      progressWidth.value -= STEP;
      if (progressWidth.value <= 0) {
        if (progressInterval) {
          clearInterval(progressInterval);
          progressInterval = null;
        }
        close();
      }
    }, INTERVAL);
  }
});

onUnmounted(() => {
  if (progressInterval) {
    clearInterval(progressInterval);
    progressInterval = null;
  }
});
</script>

<style scoped>
.notification-window {
  width: 100vw;
  height: 100vh;
  background: rgb(var(--v-theme-background));
  color: rgb(var(--v-theme-on-surface));
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
  animation: slide-in 0.3s ease-out;
}

.notification-content {
  flex: 1;
  padding: 16px;
  display: flex;
  flex-direction: column;
}

.notification-header {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
}

.notification-title {
  flex: 1;
  font-weight: 600;
  font-size: 16px;
}

.notification-body {
  flex: 1;
  font-size: 14px;
  line-height: 1.6;
  margin-bottom: 12px;
  opacity: 0.9;
}

.notification-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  padding-bottom: 8px;
}

/* Update progress bar positioning */
.progress-bar {
  position: absolute;
  bottom: 0;
  left: 0;
  height: 3px;
  background: #1890ff;
  transition: width 0.05s linear;
}

/* Update border styles for different urgency levels */
.notification-window.critical {
  border-top: 4px solid #ff4d4f;
}

.notification-window.normal {
  border-top: 4px solid #1890ff;
}

.notification-window.low {
  border-top: 4px solid #52c41a;
}
.notification-header {
  display: flex;
  align-items: center;
  margin-bottom: 8px;
}

.notification-icon {
  width: 20px;
  height: 20px;
  margin-right: 8px;
}

.notification-title {
  flex: 1;
  font-weight: 600;
  font-size: 14px;
}

.close-btn {
  background: transparent;
  border: none;
  color: #ffffff;
  font-size: 18px;
  cursor: pointer;
  padding: 4px;
  line-height: 1;
  opacity: 0.7;
  transition: opacity 0.2s;
}

.close-btn:hover {
  opacity: 1;
}

.notification-body {
  font-size: 13px;
  line-height: 1.5;
  margin-bottom: 8px;
  opacity: 0.9;
}

.notification-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

.notification-actions button {
  padding: 4px 12px;
  border-radius: 4px;
  border: none;
  font-size: 12px;
  cursor: pointer;
  transition: all 0.2s;
  background: rgba(255, 255, 255, 0.1);
  color: #ffffff;
}

.notification-actions button:hover {
  background: rgba(255, 255, 255, 0.2);
}

.notification-actions button.confirm {
  background: #1890ff;
}

.notification-actions button.confirm:hover {
  background: #40a9ff;
}

.notification-actions button.cancel {
  background: rgba(255, 255, 255, 0.1);
}

.notification-actions button.cancel:hover {
  background: rgba(255, 255, 255, 0.2);
}

.notification-actions button.action {
  background: #52c41a;
}

.notification-actions button.action:hover {
  background: #73d13d;
}

@keyframes slide-in {
  from {
    transform: translateY(-100%);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}
</style>

新代码

主进程代码

window层,管理窗口(创建窗口、构建URL 等)
import { BrowserWindow, screen } from "electron";
import path from "node:path";
import type { NotificationWindowOptions } from "@/modules/notification/types/notification";
import { MAIN_DIST, RENDERER_DIST, VITE_DEV_SERVER_URL } from "../../../main";

export class NotificationWindowManager {
  private windows = new Map<string, BrowserWindow>();
  private static readonly WINDOW_CONFIG = {
    WIDTH: 620,
    HEIGHT: 920,
    MARGIN: 10,
  };

  constructor() {}

  /**
   * 创建通知窗口
   */
  createWindow(options: NotificationWindowOptions): BrowserWindow {
    // 如果窗口已存在,先关闭
    this.closeWindow(options.id);

    const position = this.calculatePosition();
    const window = this.buildWindow(position);

    this.setupWindowEvents(window, options.id);
    this.windows.set(options.id, window);
    console.log("NotificationWindowManager - Window created:", options.id);
    return window;
  }

  /**
   * 关闭指定窗口
   */
  closeWindow(id: string): boolean {
    const window = this.windows.get(id);
    if (window && !window.isDestroyed()) {
      window.close();
      return true;
    }
    return false;
  }

  /**
   * 获取窗口
   */
  getWindow(id: string): BrowserWindow | undefined {
    return this.windows.get(id);
  }

  /**
   * 获取所有窗口
   */
  getAllWindows(): Map<string, BrowserWindow> {
    return new Map(this.windows);
  }

  /**
   * 计算窗口位置
   */
  private calculatePosition(): { x: number; y: number } {
    const primaryDisplay = screen.getPrimaryDisplay();
    const { width: screenWidth } = primaryDisplay.workAreaSize;

    const x =
      screenWidth -
      NotificationWindowManager.WINDOW_CONFIG.WIDTH -
      NotificationWindowManager.WINDOW_CONFIG.MARGIN;
    const y =
      NotificationWindowManager.WINDOW_CONFIG.MARGIN +
      this.windows.size *
        (NotificationWindowManager.WINDOW_CONFIG.HEIGHT +
          NotificationWindowManager.WINDOW_CONFIG.MARGIN);

    return { x, y };
  }

  /**
   * 构建窗口
   */
  private buildWindow(position: { x: number; y: number }): BrowserWindow {
    return new BrowserWindow({
      width: NotificationWindowManager.WINDOW_CONFIG.WIDTH,
      height: NotificationWindowManager.WINDOW_CONFIG.HEIGHT,
      x: position.x,
      y: position.y,
      frame: false,
      transparent: true,
      resizable: false,
      skipTaskbar: true,
      alwaysOnTop: true,
      show: false,
      backgroundColor: "#00000000",
      webPreferences: {
        preload: path.join(MAIN_DIST, "main_preload.mjs"),
        contextIsolation: true,
        nodeIntegration: true,
        webSecurity: false,
      },
    });
  }

  /**
   * 设置窗口事件
   */
  private setupWindowEvents(window: BrowserWindow, id: string): void {
    // CSP 设置
    window.webContents.session.webRequest.onHeadersReceived(
      (details, callback) => {
        callback({
          responseHeaders: {
            ...details.responseHeaders,
            "Content-Security-Policy": [
              "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
            ],
          },
        });
      }
    );

    // 窗口关闭事件
    window.on("closed", () => {
      this.windows.delete(id);
      this.reorderWindows();
    });
  }

  /**
   * 重新排列窗口位置
   */
  reorderWindows(): void {
    let index = 0;
    for (const [, window] of this.windows) {
      if (!window.isDestroyed()) {
        const y =
          NotificationWindowManager.WINDOW_CONFIG.MARGIN +
          index *
            (NotificationWindowManager.WINDOW_CONFIG.HEIGHT +
              NotificationWindowManager.WINDOW_CONFIG.MARGIN);
        window.setPosition(window.getPosition()[0], y);
        index++;
      }
    }
  }

  /**
   * 构建通知URL
   */
  buildNotificationUrl(options: NotificationWindowOptions): string {
    const queryParams = new URLSearchParams({
      id: options.id,
      title: options.title,
      body: options.body,
      urgency: options.urgency || "normal",
    });

    if (options.icon) {
      queryParams.append("icon", options.icon);
    }

    if (options.actions) {
      queryParams.append(
        "actions",
        encodeURIComponent(JSON.stringify(options.actions))
      );
    }

    return VITE_DEV_SERVER_URL
      ? `${VITE_DEV_SERVER_URL}#/notification?${queryParams.toString()}`
      : `file://${RENDERER_DIST}/index.html#/notification?${queryParams.toString()}`;
  }
}
service层
import { BrowserWindow } from "electron";
import { NotificationWindowManager } from "../windows/notification.window";

import type { NotificationWindowOptions } from "@/modules/notification/types/notification";
import type { TResponse } from "@/shared/types/response";

export interface NotificationServiceConfig {
  mainWindow: BrowserWindow;
  MAIN_DIST: string;
  RENDERER_DIST: string;
  VITE_DEV_SERVER_URL?: string;
}

/**
 * 通知服务 - 统一管理通知功能
 */
class NotificationService {
  private windowManagementService = new NotificationWindowManager();

  constructor() {}

  public async showNotification(
    options: NotificationWindowOptions
  ): Promise<TResponse> {
    try {
      const window = this.windowManagementService.createWindow(options);
      const url = this.windowManagementService.buildNotificationUrl(options);
      // 加载 URL
      await window.loadURL(url);
      window.show();
      return {
        success: true,
        message: "Notification displayed successfully",
      };
    } catch (error) {
      console.error("NotificationService - showNotification Error:", error);
      return {
        success: false,
        message: error instanceof Error ? error.message : "Unknown error",
      };
    }
  }

  public closeNotification(id: string): TResponse {
    try {
      const response = this.windowManagementService.closeWindow(id);
      if (!response) {
        throw new Error(
          `Notification with ID ${id} not found or already closed`
        );
      }
      return {
        success: true,
        message: "Notification closed successfully",
      };
    } catch (error) {
      console.error("NotificationService - closeNotification Error:", error);
      return {
        success: false,
        message: error instanceof Error ? error.message : "Unknown error",
      };
    }
  }

  public handleNotificationAction(
    id: string,
    action: { text: string; type: string }
  ): void {
    const window = this.windowManagementService.getWindow(id);
    if (window) {
      if (action.type === "cancel" || action.type === "confirm") {
        window.close();
      }
      window.webContents.send("notification-action", { id, action });
    } else {
      console.warn(`Notification with ID ${id} not found.`);
    }
  }
}

export const notificationService = new NotificationService();
ipc层
import { ipcMain } from "electron";
import { notificationService } from "../services/notification.service";
import type { NotificationWindowOptions } from "@/modules/notification/types/notification";

export function setupNotificationHandler() {
  ipcMain.handle(
    "show-notification",
    async (_event, options: NotificationWindowOptions) => {
      try {
        return await notificationService.showNotification(options);
      } catch (error) {
        console.error("IPC Error - show-notification:", error);
        throw error;
      }
    }
  );

  // 关闭通知
  ipcMain.on("close-notification", (_event, id: string) => {
    try {
      return notificationService.closeNotification(id);
    } catch (error) {
      console.error("IPC Error - close-notification:", error);
    }
  });

  // 通知操作
  ipcMain.on(
    "notification-action",
    (_event, id: string, action: { text: string; type: string }) => {
      try {
        return notificationService.handleNotificationAction(id, action);
      } catch (error) {
        console.error("IPC Error - notification-action:", error);
      }
    }
  );
}

渲染进程代码

ipc层
import { TResponse } from "@/shared/types/response";
import { NotificationWindowOptions } from "../types/notification";

export class NotificationIpc {
  public static showNotification(
    options: NotificationWindowOptions
  ): Promise<TResponse<void>> {
    return window.shared.ipcRenderer.invoke("show-notification", options);
  }

  public static closeNotification(id: string): Promise<TResponse<void>> {
    return window.shared.ipcRenderer.invoke("close-notification", id);
  }

  public static notificationAction(
    id: string,
    action: { text: string; type: string }
  ): Promise<TResponse<void>> {
    return window.shared.ipcRenderer.invoke("notification-action", id, action);
  }

  public static onNotificationAction(
    callback: (id: string, action: { text: string; type: string }) => void
  ): () => void {
    const handler = (
      _event: any,
      id: string,
      action: { text: string; type: string }
    ) => {
      callback(id, action);
    };
    window.shared.ipcRenderer.on("notification-action-received", handler);
    return () =>
      window.shared.ipcRenderer.removeListener(
        "notification-action-received",
        handler
      );
  }
}
service层
import type { NotificationWindowOptions } from "../types/notification";
import type { TResponse } from "@/shared/types/response";

export class NotificationService {

  private generateId(): string {
    const id = `notification-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
    return id;
  }
  /**
   * 显示通知
   * @param options 通知选项
   * @returns Promise<TResponse>
   */
  public async showNotification(
    options: Omit<NotificationWindowOptions, 'id'>
  ): Promise<TResponse> {
    const id = this.generateId(); // 为通知生成唯一ID
    try {
      const fullOptions: NotificationWindowOptions = {
        id, // 将生成的ID添加到选项中
        ...options,
      };
      const response = await window.shared.ipcRenderer.invoke(
        "show-notification",
        fullOptions
      );
      return response;
    } catch (error) {
      console.error("NotificationService - showNotification Error:", error);
      return {
        success: false,
        message: error instanceof Error ? error.message : "Unknown error",
      };
    }
  }
  /**
   * 关闭通知
   * @param id 通知ID
   * @returns Promise<TResponse>
   */
  public async closeNotification(id: string): Promise<TResponse> {
    try {
      const response = await window.shared.ipcRenderer.invoke(
        "close-notification",
        id
      );
      return response;
    } catch (error) {
      console.error("NotificationService - closeNotification Error:", error);
      return {
        success: false,
        message: error instanceof Error ? error.message : "Unknown error",
      };
    }
  }

  /**
   * 发送通知操作
   */
  public notificationAction(
    id: string,
    action: { text: string; type: string }
  ): TResponse {
    try {
      window.shared.ipcRenderer.send('notification-action', id, action);
      return {
        success: true,
        message: 'Action sent successfully',
      };
    } catch (error) {
      return {
        success: false,
        message: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  /**
   * 监听通知操作
   */
  public onNotificationAction(
    callback: (id: string, action: { text: string; type: string }) => void
  ): () => void {
    const handler = (_event: any, id: string, action: { text: string; type: string }) => {
      callback(id, action);
    };

    window.shared.ipcRenderer.on('notification-action-received', handler);

    // 返回清理函数
    return () => {
      window.shared.ipcRenderer.removeListener('notification-action-received', handler);
    };
  }

  /**
   * 显示简单通知
   * @param title 标题
   * @param message 消息内容
   */
  public async showSimple(title: string, message: string): Promise<TResponse> {
    return await this.showNotification({
      title,
      body: message,
      urgency: 'normal',
    });

  }

  /**
   * 显示警告通知
   * @param title 标题
   * @param message 消息内容
   */
  public async showWarning(title: string, message: string): Promise<TResponse> {
    return await this.showNotification({
      title,
      body: message,
      urgency: 'critical',
      actions: [
        { text: '我知道了', type: 'confirm' }
      ]
    });
  }
}

export const notificationService = new NotificationService();

总结

  1. 渲染进程发送请求和参数
  2. 主进程收到请求后构造窗口、渲染路径
  3. 如果触发按钮,则返会通知渲染进程
创建于 2025/1/1 更新于 2026/3/2