diff --git a/src/pages/batchupdate/main.tsx b/src/pages/batchupdate/main.tsx
index f87ac8a8a..59d7c7647 100644
--- a/src/pages/batchupdate/main.tsx
+++ b/src/pages/batchupdate/main.tsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { AppProvider } from "../store/AppContext.tsx";
+import { fixArcoIssues } from "@App/pages/fix.ts";
import MainLayout from "../components/layout/MainLayout.tsx";
import LoggerCore from "@App/app/logger/core.ts";
import { message } from "../store/global.ts";
@@ -27,6 +28,8 @@ const Root = (
);
+fixArcoIssues();
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
process.env.NODE_ENV === "development" ? {Root} : Root
);
diff --git a/src/pages/confirm/main.tsx b/src/pages/confirm/main.tsx
index 51985671b..14d20dde8 100644
--- a/src/pages/confirm/main.tsx
+++ b/src/pages/confirm/main.tsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { AppProvider } from "../store/AppContext.tsx";
+import { fixArcoIssues } from "@App/pages/fix.ts";
import MainLayout from "../components/layout/MainLayout.tsx";
import LoggerCore from "@App/app/logger/core.ts";
import { message } from "../store/global.ts";
@@ -26,6 +27,8 @@ const Root = (
);
+fixArcoIssues();
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
process.env.NODE_ENV === "development" ? {Root} : Root
);
diff --git a/src/pages/fix.ts b/src/pages/fix.ts
new file mode 100644
index 000000000..05debc2f2
--- /dev/null
+++ b/src/pages/fix.ts
@@ -0,0 +1,118 @@
+// 修复 Arco Design 在 React 17+ 环境下 focusin / focusout 事件重复触发导致的 UI 卡顿问题
+// 参考 PR:https://github.com/scriptscat/scriptcat/pull/1224
+// 核心思路:将 focusin/focusout 的事件监听器执行延迟到下一个 macrotask,避免在同一渲染帧内被 Arco 多次触发
+
+let actived = false; // 防止多次调用 fixArcoIssues 导致重复 patch
+
+export const fixArcoIssues = () => {
+ if (actived) return; // 已修复过则直接返回
+ actived = true;
+
+ // 保存原生的 addEventListener / removeEventListener 方法
+ const originalAddEventListener = HTMLElement.prototype.addEventListener;
+ const originalRemoveEventListener = HTMLElement.prototype.removeEventListener;
+
+ // 用来暂存需要延迟执行的事件对象(同一 tick 内的事件会被合并)
+ const stackedEvents = new Set();
+
+ // 记录每个事件对应的 thisArg 和 listener(因为我们会包一层 handler)
+ const bindInfoMap = new WeakMap();
+
+ // 真正执行被延迟的事件回调
+ const executorFn = () => {
+ if (!stackedEvents.size) return;
+
+ // 复制一份后清空,避免在执行期间又有新事件进来
+ const events = [...stackedEvents];
+ stackedEvents.clear();
+
+ for (const ev of events) {
+ const bi = bindInfoMap.get(ev);
+ if (!bi) continue;
+
+ // 使用完成后立即清理,减少 WeakMap 的引用存活时间
+ bindInfoMap.delete(ev);
+
+ // 如果事件已被 preventDefault,则不再执行原回调
+ // 保持浏览器原生事件行为一致
+ if (ev.defaultPrevented) continue;
+
+ try {
+ // 使用原来的 this 和 listener 执行
+ bi.listener.call(bi.thisArg, ev);
+ } catch (err) {
+ // 捕获异常,避免影响后续事件执行
+ console.error("Failed to execute delayed callback.", err);
+ }
+ }
+ };
+
+ // 使用 postMessage + message 事件来模拟 macrotask
+ // 相比 setTimeout(0),更稳定且调度开销更小
+ self.addEventListener("message", (ev) => {
+ if (typeof ev.data === "object" && ev.data?.processNextTick === "addEventListenerHack") {
+ executorFn();
+ }
+ });
+
+ // 记录原始 listener 与包装后 handler 的映射关系
+ // 以便 removeEventListener 时能正确移除
+ const handlerMap = new WeakMap();
+
+ // 自定义的 addEventListener
+ // 只针对 focusin / focusout 且 options 为简单 boolean 的情况生效
+ const addEventListenerHack = function (
+ this: Element,
+ type: K,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions
+ ): void {
+ // 只拦截 focusin / focusout,且 listener 是函数,且 options 是简单的 capture/bubble 设定
+ // (排除 once、passive 等进阶选项,避免破坏其他使用方式)
+ if (
+ (type === "focusin" || type === "focusout") &&
+ typeof listener === "function" &&
+ typeof (options ?? false) === "boolean" // 只接受 boolean 或 undefined 的 options
+ ) {
+ // 包装一层 handler,收集事件并推迟执行
+ const handler = (ev: Event) => {
+ stackedEvents.add(ev);
+ bindInfoMap.set(ev, { thisArg: this, listener });
+ // 发送 macrotask 讯号,让 executor 在下一个事件循环执行
+ self.postMessage({ processNextTick: "addEventListenerHack" }, "*");
+ };
+
+ // 保存原 listener 与包装 handler 的对应关系
+ handlerMap.set(listener, handler);
+
+ // 实际注册的是包装后的 handler
+ return originalAddEventListener.call(this, type, handler, options);
+ }
+
+ // 其他事件保持原生行为,不做任何干预
+ return originalAddEventListener.call(this, type, listener, options);
+ };
+
+ // 自定义的 removeEventListener
+ // 如果 listener 曾被包装过,这里需要移除对应的 handler
+ const removeEventListenerHack = function (
+ this: Element,
+ type: K,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions
+ ): void {
+ const handler = typeof listener === "function" && handlerMap.get(listener);
+ return originalRemoveEventListener.call(this, type, handler || listener, options);
+ };
+
+ // 针对 body 打补丁(Arco 大量事件绑在 document 或 body 上)
+ document.body.addEventListener = addEventListenerHack;
+ document.body.removeEventListener = removeEventListenerHack;
+
+ // 也针对 React 根节点 #root 打补丁(部分组件可能绑在根元素)
+ const root = document.querySelector("div#root");
+ if (root) {
+ root.addEventListener = addEventListenerHack;
+ root.removeEventListener = removeEventListenerHack;
+ }
+};
diff --git a/src/pages/import/main.tsx b/src/pages/import/main.tsx
index fe51ddedd..2802dd082 100644
--- a/src/pages/import/main.tsx
+++ b/src/pages/import/main.tsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { AppProvider } from "../store/AppContext.tsx";
+import { fixArcoIssues } from "@App/pages/fix.ts";
import MainLayout from "../components/layout/MainLayout.tsx";
import LoggerCore from "@App/app/logger/core.ts";
import { message } from "../store/global.ts";
@@ -26,6 +27,8 @@ const Root = (
);
+fixArcoIssues();
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
process.env.NODE_ENV === "development" ? {Root} : Root
);
diff --git a/src/pages/install/main.tsx b/src/pages/install/main.tsx
index 7f87023e2..95f4faf2f 100644
--- a/src/pages/install/main.tsx
+++ b/src/pages/install/main.tsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import { AppProvider } from "../store/AppContext.tsx";
+import { fixArcoIssues } from "@App/pages/fix.ts";
import MainLayout from "../components/layout/MainLayout.tsx";
import LoggerCore from "@App/app/logger/core.ts";
import { message } from "../store/global.ts";
@@ -38,6 +39,8 @@ const Root = (
);
+fixArcoIssues();
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
process.env.NODE_ENV === "development" ? {Root} : Root
);
diff --git a/src/pages/options/main.tsx b/src/pages/options/main.tsx
index 096203c58..bbd927b77 100644
--- a/src/pages/options/main.tsx
+++ b/src/pages/options/main.tsx
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
import MainLayout from "../components/layout/MainLayout.tsx";
import Sider from "../components/layout/Sider.tsx";
import { AppProvider } from "../store/AppContext.tsx";
+import { fixArcoIssues } from "@App/pages/fix.ts";
import "@arco-design/web-react/dist/css/arco.css";
import "@App/locales/locales";
import "@App/index.css";
@@ -33,6 +34,8 @@ const Root = (
);
+fixArcoIssues();
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
process.env.NODE_ENV === "development" ? {Root} : Root
);
diff --git a/src/pages/popup/main.tsx b/src/pages/popup/main.tsx
index 05b3e94c1..d0b21556c 100644
--- a/src/pages/popup/main.tsx
+++ b/src/pages/popup/main.tsx
@@ -10,6 +10,7 @@ import "@App/index.css";
import "./index.css";
import PopupLayout from "../components/layout/PopupLayout.tsx";
import { AppProvider } from "../store/AppContext.tsx";
+import { fixArcoIssues } from "@App/pages/fix.ts";
// 初始化日志组件
const loggerCore = new LoggerCore({
@@ -27,6 +28,8 @@ const Root = (
);
+fixArcoIssues();
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
process.env.NODE_ENV === "development" ? {Root} : Root
);