From f3e6ecdf4bd28b97543a7accceafb61faad2fb64 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 10 May 2026 13:54:16 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix(UI):=20=E9=81=BF=E5=85=8D=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BA=92=E5=8A=A8=E8=A7=A6=E5=8F=91=20body=20?= =?UTF-8?q?=E7=9A=84=20swipe=20=E8=B7=B3=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.css | 20 +- src/pages/components/layout/MainLayout.tsx | 371 +++++++++++---------- src/pages/utility/InteractiveContainer.tsx | 57 ++++ 3 files changed, 260 insertions(+), 188 deletions(-) create mode 100644 src/pages/utility/InteractiveContainer.tsx diff --git a/src/index.css b/src/index.css index 6eb4944b4..dca7dbe5f 100644 --- a/src/index.css +++ b/src/index.css @@ -2,22 +2,34 @@ @unocss default; @unocss; +* { + scrollbar-color: inherit; +} + body { scrollbar-color: var(--color-scrollbar-thumb) var(--color-scrollbar-track); - /* 对于webkit浏览器的滚动条样式 */ - scrollbar-width: thin; } body[arco-theme='dark'] { --color-scrollbar-thumb: #6b6b6b; --color-scrollbar-track: #2d2d2d; - --color-scrollbar-thumb-hover: #8c8c8c; } body[arco-theme='light'] { --color-scrollbar-thumb: #6b6b6b; --color-scrollbar-track: #f0f0f0; - --color-scrollbar-thumb-hover: #8c8c8c; +} + +.interactive-container { + overscroll-behavior: none; + height: 100vh; + width: 100vw; + position: relative; + contain: strict; + padding: 0px; + margin: 0px; + box-sizing: border-box; + overflow: auto; } :root { diff --git a/src/pages/components/layout/MainLayout.tsx b/src/pages/components/layout/MainLayout.tsx index e7a6b6480..db20d4c76 100644 --- a/src/pages/components/layout/MainLayout.tsx +++ b/src/pages/components/layout/MainLayout.tsx @@ -36,6 +36,7 @@ import { arcoLocale } from "@App/locales/arco"; import { prepareScriptByCode } from "@App/pkg/utils/script"; import { saveHandle } from "@App/pkg/utils/filehandle-db"; import { makeBlobURL } from "@App/pkg/utils/utils"; +import InteractiveContainer from "@App/pages/utility/InteractiveContainer"; // --- 工具函数移出组件外,避免每次 Render 重新定义 --- @@ -297,215 +298,217 @@ const MainLayout: React.FC<{ }; return ( - { - return ; - }} - locale={arcoLocale(i18n.language)} - componentConfig={{ - Select: { - getPopupContainer: (node) => { - return getSafePopupParent(node as Element); + + { + return ; + }} + locale={arcoLocale(i18n.language)} + componentConfig={{ + Select: { + getPopupContainer: (node) => { + return getSafePopupParent(node as Element); + }, }, - }, - }} - getPopupContainer={(node) => { - return getSafePopupParent(node.parentNode as Element); - }} - > - {contextHolder} - - - { - setImportVisible(false); + }} + getPopupContainer={(node) => { + return getSafePopupParent(node.parentNode as Element); + }} + > + {contextHolder} + + - { - if (e.ctrlKey && e.key === "Enter") { - e.preventDefault(); - handleImport(); - } + { + setImportVisible(false); }} - /> - -
- ScriptCat - - {"ScriptCat"} - -
- - {pageName === "options" && ( - - - - {t("create_user_script")} - - - - - {t("create_background_script")} - - - - - {t("create_scheduled_script")} - - - { - if ("showOpenFilePicker" in window) { - // 使用新的文件打开接口,解决无法监听本地文件的问题 - //@ts-ignore - window - .showOpenFilePicker({ - multiple: true, - types: [ - { - description: "JavaScript", - accept: { "text/javascript": [".js"] }, - }, - ], - }) - .then((handles: any) => { - onDrop(handles as FileWithPath[]); - }); - } else { - // 旧的方式,无法监听本地文件变更 - document.getElementById("import-local")?.click(); - } - }} - > - {t("import_by_local")} - - { - setImportVisible(true); - }} - > - {t("import_link")} - - - } - position="bl" - > - - - )} - { - const theme = key as "auto" | "light" | "dark"; - updateColorTheme(theme); - }} - selectedKeys={[colorThemeState]} - > - - {t("light")} - - - {t("dark")} - - - {t("system_follow")} - - - } - position="bl" > - + + )} + { + const theme = key as "auto" | "light" | "dark"; + updateColorTheme(theme); + }} + selectedKeys={[colorThemeState]} + > + + {t("light")} + + + {t("dark")} + + + {t("system_follow")} + } + position="bl" > + /> - )} - -
- - - {/* 性能关键:抽离遮罩组件,只有 active 变化时此小组件重绘 */} - - {children} + {showLanguage && ( + + {languageList.map((value) => ( + { + if (value.key === "help") { + window.open("https://github.com/scriptscat/scriptcat/discussions/531", "_blank"); + return; + } + systemConfig.setLanguage(value.key); + Message.success(t("language_change_tip", { lng: value.key })!); + }} + > + {value.title} + + ))} + + } + > + + + )} + +
+ + + {/* 性能关键:抽离遮罩组件,只有 active 变化时此小组件重绘 */} + + {children} +
- -
+
+ ); }; diff --git a/src/pages/utility/InteractiveContainer.tsx b/src/pages/utility/InteractiveContainer.tsx new file mode 100644 index 000000000..54762f206 --- /dev/null +++ b/src/pages/utility/InteractiveContainer.tsx @@ -0,0 +1,57 @@ +import type { CSSProperties, HTMLAttributes, ReactNode } from "react"; + +type InteractiveContainerProps = { + children: ReactNode; + className?: string; + style?: CSSProperties; +} & HTMLAttributes; + +const handleContainerWheel = (evt: Event) => { + if ((evt.target as Element).closest(".monaco-editor")) { + evt.preventDefault(); + } else { + evt.stopImmediatePropagation(); + evt.stopPropagation(); + // evt.preventDefault(); + } +}; + +const attachMainHandler = (target: Node) => { + const o = { capture: false, passive: false, once: false }; + target.removeEventListener("wheel", handleContainerWheel, o); + target.addEventListener("wheel", handleContainerWheel, o); +}; + +const weakSet = new WeakSet(); + +const setRef = (div: HTMLDivElement) => { + if (!div) return; + if (weakSet.has(div)) return; + weakSet.add(div); + attachMainHandler(div); +}; + +/** + * Wraps arbitrary interactive content and intercepts wheel events at the + * container boundary. + * + * Wheel behavior: + * - Inside Monaco editor instances, prevent the browser from handling the + * event so editor scrolling remains isolated. + * - Outside Monaco, stop propagation so parent/page-level handlers do not + * react to wheel gestures from this container. + */ +export default function InteractiveContainer({ children, className, style, ...props }: InteractiveContainerProps) { + return ( +
+ {children} +
+ ); +} From afc4e148c6b9a1d750d9d5d3e7393d67127dd20f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 10 May 2026 14:11:41 +0900 Subject: [PATCH 2/5] code update --- src/index.css | 10 ++----- src/pages/utility/InteractiveContainer.tsx | 35 +++++----------------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/index.css b/src/index.css index dca7dbe5f..6b2ef8509 100644 --- a/src/index.css +++ b/src/index.css @@ -20,15 +20,9 @@ body[arco-theme='light'] { --color-scrollbar-track: #f0f0f0; } -.interactive-container { +#root { overscroll-behavior: none; - height: 100vh; - width: 100vw; - position: relative; - contain: strict; - padding: 0px; - margin: 0px; - box-sizing: border-box; + contain: content; overflow: auto; } diff --git a/src/pages/utility/InteractiveContainer.tsx b/src/pages/utility/InteractiveContainer.tsx index 54762f206..2e7350961 100644 --- a/src/pages/utility/InteractiveContainer.tsx +++ b/src/pages/utility/InteractiveContainer.tsx @@ -1,9 +1,7 @@ -import type { CSSProperties, HTMLAttributes, ReactNode } from "react"; +import type { HTMLAttributes, ReactNode } from "react"; type InteractiveContainerProps = { children: ReactNode; - className?: string; - style?: CSSProperties; } & HTMLAttributes; const handleContainerWheel = (evt: Event) => { @@ -16,19 +14,10 @@ const handleContainerWheel = (evt: Event) => { } }; -const attachMainHandler = (target: Node) => { +const attachMainHandler = (target: Node | null) => { const o = { capture: false, passive: false, once: false }; - target.removeEventListener("wheel", handleContainerWheel, o); - target.addEventListener("wheel", handleContainerWheel, o); -}; - -const weakSet = new WeakSet(); - -const setRef = (div: HTMLDivElement) => { - if (!div) return; - if (weakSet.has(div)) return; - weakSet.add(div); - attachMainHandler(div); + target?.removeEventListener("wheel", handleContainerWheel, o); + target?.addEventListener("wheel", handleContainerWheel, o); }; /** @@ -41,17 +30,7 @@ const setRef = (div: HTMLDivElement) => { * - Outside Monaco, stop propagation so parent/page-level handlers do not * react to wheel gestures from this container. */ -export default function InteractiveContainer({ children, className, style, ...props }: InteractiveContainerProps) { - return ( -
- {children} -
- ); +export default function InteractiveContainer({ children }: InteractiveContainerProps) { + attachMainHandler(document.getElementById("root")); + return <>{children}; } From c741b2e8c52ec9ab24132e93eb7b5f72b1b87d38 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 10 May 2026 14:18:37 +0900 Subject: [PATCH 3/5] InteractiveContainer -> ScrollBoundary --- src/pages/components/layout/MainLayout.tsx | 6 +- .../components/layout/ScrollBoundary.tsx | 57 +++++++++++++++++++ src/pages/utility/InteractiveContainer.tsx | 36 ------------ 3 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 src/pages/components/layout/ScrollBoundary.tsx delete mode 100644 src/pages/utility/InteractiveContainer.tsx diff --git a/src/pages/components/layout/MainLayout.tsx b/src/pages/components/layout/MainLayout.tsx index db20d4c76..34c92742d 100644 --- a/src/pages/components/layout/MainLayout.tsx +++ b/src/pages/components/layout/MainLayout.tsx @@ -36,7 +36,7 @@ import { arcoLocale } from "@App/locales/arco"; import { prepareScriptByCode } from "@App/pkg/utils/script"; import { saveHandle } from "@App/pkg/utils/filehandle-db"; import { makeBlobURL } from "@App/pkg/utils/utils"; -import InteractiveContainer from "@App/pages/utility/InteractiveContainer"; +import ScrollBoundary from "@App/pages/components/layout/ScrollBoundary"; // --- 工具函数移出组件外,避免每次 Render 重新定义 --- @@ -298,7 +298,7 @@ const MainLayout: React.FC<{ }; return ( - + { return ; @@ -508,7 +508,7 @@ const MainLayout: React.FC<{ - + ); }; diff --git a/src/pages/components/layout/ScrollBoundary.tsx b/src/pages/components/layout/ScrollBoundary.tsx new file mode 100644 index 000000000..6cc808de8 --- /dev/null +++ b/src/pages/components/layout/ScrollBoundary.tsx @@ -0,0 +1,57 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +// Props extend native div attributes so callers can pass className, style, etc. +type ScrollBoundaryProps = { + children: ReactNode; + parentNodeSelector: string; +} & HTMLAttributes; + +/** + * Handles wheel events bubbling up to the scroll boundary. + * + * - Monaco editor: prevent default so the editor's own scroll logic runs + * without interference from the browser or ancestor handlers. + * - Everywhere else: stop propagation so parent/page-level handlers ignore + * wheel gestures originating inside this boundary. + * (preventDefault is intentionally left commented out to allow native + * scrolling within non-editor children.) + */ +const handleScrollBoundaryWheel = (evt: Event) => { + if ((evt.target as Element).closest(".monaco-editor")) { + evt.preventDefault(); + } else { + evt.stopImmediatePropagation(); + evt.stopPropagation(); + // evt.preventDefault(); + } +}; + +/** + * Registers the wheel handler on the given target node. + * Removes any existing listener first to guarantee exactly one handler is + * attached, even if called multiple times (e.g. on re-renders). + * + * Options: non-capturing, non-passive (required for preventDefault to work), + * and persistent (once: false). + */ +const attachScrollBoundaryHandler = (target: Node | null) => { + const o = { capture: false, passive: false, once: false }; + target?.removeEventListener("wheel", handleScrollBoundaryWheel, o); + target?.addEventListener("wheel", handleScrollBoundaryWheel, o); +}; + +/** + * Establishes a wheel-event boundary at the root level. + * + * Wheel behavior within the boundary: + * - Inside Monaco editor instances: prevents default so editor scrolling + * stays isolated from browser and ancestor handlers. + * - Outside Monaco: stops propagation so parent/page-level handlers do not + * react to wheel gestures originating inside this boundary. + */ +export default function ScrollBoundary({ children, parentNodeSelector }: ScrollBoundaryProps) { + // Attach once per render; the handler is idempotent due to the + // remove-then-add pattern in attachScrollBoundaryHandler. + attachScrollBoundaryHandler(document.querySelector(parentNodeSelector)); + return <>{children}; +} diff --git a/src/pages/utility/InteractiveContainer.tsx b/src/pages/utility/InteractiveContainer.tsx deleted file mode 100644 index 2e7350961..000000000 --- a/src/pages/utility/InteractiveContainer.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { HTMLAttributes, ReactNode } from "react"; - -type InteractiveContainerProps = { - children: ReactNode; -} & HTMLAttributes; - -const handleContainerWheel = (evt: Event) => { - if ((evt.target as Element).closest(".monaco-editor")) { - evt.preventDefault(); - } else { - evt.stopImmediatePropagation(); - evt.stopPropagation(); - // evt.preventDefault(); - } -}; - -const attachMainHandler = (target: Node | null) => { - const o = { capture: false, passive: false, once: false }; - target?.removeEventListener("wheel", handleContainerWheel, o); - target?.addEventListener("wheel", handleContainerWheel, o); -}; - -/** - * Wraps arbitrary interactive content and intercepts wheel events at the - * container boundary. - * - * Wheel behavior: - * - Inside Monaco editor instances, prevent the browser from handling the - * event so editor scrolling remains isolated. - * - Outside Monaco, stop propagation so parent/page-level handlers do not - * react to wheel gestures from this container. - */ -export default function InteractiveContainer({ children }: InteractiveContainerProps) { - attachMainHandler(document.getElementById("root")); - return <>{children}; -} From 3de730331dba4a8ecca711e5ac96ed149cb517a4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 10 May 2026 14:45:19 +0900 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/pages/components/layout/ScrollBoundary.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/components/layout/ScrollBoundary.tsx b/src/pages/components/layout/ScrollBoundary.tsx index 6cc808de8..7d8184928 100644 --- a/src/pages/components/layout/ScrollBoundary.tsx +++ b/src/pages/components/layout/ScrollBoundary.tsx @@ -1,10 +1,9 @@ -import type { HTMLAttributes, ReactNode } from "react"; +import type { ReactNode } from "react"; -// Props extend native div attributes so callers can pass className, style, etc. type ScrollBoundaryProps = { children: ReactNode; parentNodeSelector: string; -} & HTMLAttributes; +}; /** * Handles wheel events bubbling up to the scroll boundary. From 217110bfb05c004d747fe467e8d7a9c014a53c1b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 10 May 2026 14:45:34 +0900 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/pages/components/layout/ScrollBoundary.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/components/layout/ScrollBoundary.tsx b/src/pages/components/layout/ScrollBoundary.tsx index 7d8184928..aaf65a152 100644 --- a/src/pages/components/layout/ScrollBoundary.tsx +++ b/src/pages/components/layout/ScrollBoundary.tsx @@ -19,7 +19,6 @@ const handleScrollBoundaryWheel = (evt: Event) => { if ((evt.target as Element).closest(".monaco-editor")) { evt.preventDefault(); } else { - evt.stopImmediatePropagation(); evt.stopPropagation(); // evt.preventDefault(); }