diff --git a/src/index.css b/src/index.css index 6eb4944b4..6b2ef8509 100644 --- a/src/index.css +++ b/src/index.css @@ -2,22 +2,28 @@ @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; +} + +#root { + overscroll-behavior: none; + contain: content; + overflow: auto; } :root { diff --git a/src/pages/components/layout/MainLayout.tsx b/src/pages/components/layout/MainLayout.tsx index e7a6b6480..34c92742d 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 ScrollBoundary from "@App/pages/components/layout/ScrollBoundary"; // --- 工具函数移出组件外,避免每次 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/components/layout/ScrollBoundary.tsx b/src/pages/components/layout/ScrollBoundary.tsx new file mode 100644 index 000000000..aaf65a152 --- /dev/null +++ b/src/pages/components/layout/ScrollBoundary.tsx @@ -0,0 +1,55 @@ +import type { ReactNode } from "react"; + +type ScrollBoundaryProps = { + children: ReactNode; + parentNodeSelector: string; +}; + +/** + * 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.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}; +}