From 07743ea524b5a708d9448689c822b246304b980f Mon Sep 17 00:00:00 2001 From: JSR Date: Sat, 18 Jul 2026 09:47:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E8=87=AA=E5=AE=9A=E4=B9=89=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/main.css | 31 +++++++++ src/components/Home.tsx | 151 +++++++++++++++++++++++++++++++++------- 2 files changed, 158 insertions(+), 24 deletions(-) diff --git a/src/assets/main.css b/src/assets/main.css index 8748d2e..973d702 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -305,6 +305,15 @@ html.platform-macos body { background: #fff; } +.ss-home-operation-morph-modal .ant-modal-body { + scrollbar-width: none; +} + +.ss-home-operation-morph-modal .ant-modal-body::-webkit-scrollbar { + width: 0; + height: 0; +} + .ss-home-operation-morph-modal.ss-operation-morph-active .ant-modal-container, .ss-home-operation-morph-modal.ss-operation-morph-active .ant-modal-content { animation: ss-operation-shell-in 480ms cubic-bezier(0.2, 0, 0, 1) both; @@ -373,6 +382,28 @@ html.platform-macos body { display: none; } +.ss-operation-designed-scrollbar { + position: fixed; + z-index: 1002; + width: 5px; + pointer-events: none; + animation: ss-operation-scrollbar-in 160ms ease both; +} + +.ss-operation-designed-scrollbar > span { + position: absolute; + right: 0; + width: 5px; + min-height: 28px; + border-radius: 999px; + background: rgba(38, 38, 38, 0.42); +} + +@keyframes ss-operation-scrollbar-in { + from { opacity: 0; } + to { opacity: 1; } +} + .ss-home-operation-morph-modal.ss-operation-morph-active .ss-operation-designed-name, .ss-home-operation-morph-modal.ss-operation-morph-closing .ss-operation-designed-name { overflow: visible; diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 55b20a5..fc6e3d1 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -72,6 +72,15 @@ interface operationMorphElementRects { surfaceBorderRadius: string | null } +interface operationScrollbarState { + visible: boolean + left: number + top: number + height: number + thumbTop: number + thumbHeight: number +} + const getOperationElementMorph = ( targetRect: DOMRect, sourceRect: DOMRect @@ -166,6 +175,14 @@ export const Home: React.FC = ({ const [batchMode, setBatchMode] = useState(false) const [selectedStudentIds, setSelectedStudentIds] = useState([]) const [operationVisible, setOperationVisible] = useState(false) + const [operationScrollbar, setOperationScrollbar] = useState({ + visible: false, + left: 0, + top: 0, + height: 0, + thumbTop: 0, + thumbHeight: 0, + }) const [operationOriginRect, setOperationOriginRect] = useState(null) const [operationOriginElements, setOperationOriginElements] = useState(null) @@ -205,6 +222,70 @@ export const Home: React.FC = ({ const operationMorphEasing = "cubic-bezier(0.2, 0, 0, 1)" const operationMorphCloseEasing = "cubic-bezier(0.4, 0, 0.2, 1)" + const syncOperationScrollbar = useCallback(() => { + if (isPortraitMode || !operationVisible) { + setOperationScrollbar((previous) => (previous.visible ? { ...previous, visible: false } : previous)) + return + } + const modalBody = document.querySelector( + `.${operationModalClass} .ant-modal-body` + ) as HTMLElement | null + if (!modalBody) return + + const computedStyle = window.getComputedStyle(modalBody) + const canScroll = + computedStyle.overflowY !== "visible" && + modalBody.scrollHeight - modalBody.clientHeight > 1 && + modalBody.clientHeight > 0 + if (!canScroll) { + setOperationScrollbar((previous) => (previous.visible ? { ...previous, visible: false } : previous)) + return + } + + const bodyRect = modalBody.getBoundingClientRect() + const trackHeight = Math.max(0, modalBody.clientHeight - 16) + const thumbHeight = Math.min( + trackHeight, + Math.max(28, (modalBody.clientHeight * trackHeight) / modalBody.scrollHeight) + ) + const thumbTop = + (modalBody.scrollTop / Math.max(1, modalBody.scrollHeight - modalBody.clientHeight)) * + Math.max(0, trackHeight - thumbHeight) + setOperationScrollbar({ + visible: true, + left: bodyRect.right - 12, + top: bodyRect.top + 8, + height: trackHeight, + thumbTop, + thumbHeight, + }) + }, [isPortraitMode, operationModalClass, operationVisible]) + + useEffect(() => { + if (isPortraitMode || !operationVisible) { + setOperationScrollbar((previous) => (previous.visible ? { ...previous, visible: false } : previous)) + return + } + const modalBody = document.querySelector( + `.${operationModalClass} .ant-modal-body` + ) as HTMLElement | null + if (!modalBody) return + + const frame = window.requestAnimationFrame(syncOperationScrollbar) + const delayedSync = window.setTimeout(syncOperationScrollbar, operationMorphOpenDuration + 24) + modalBody.addEventListener("scroll", syncOperationScrollbar, { passive: true }) + const resizeObserver = + typeof ResizeObserver === "undefined" ? null : new ResizeObserver(syncOperationScrollbar) + resizeObserver?.observe(modalBody) + + return () => { + window.cancelAnimationFrame(frame) + window.clearTimeout(delayedSync) + modalBody.removeEventListener("scroll", syncOperationScrollbar) + resizeObserver?.disconnect() + } + }, [isPortraitMode, operationMorphOpenDuration, operationVisible, operationModalClass, syncOperationScrollbar]) + const emitDataUpdated = (category: "events" | "students" | "reasons" | "all") => { window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category } })) } @@ -1019,6 +1100,7 @@ export const Home: React.FC = ({ operationMorphRafRef.current = null } operationClosingRef.current = false + setOperationScrollbar((previous) => (previous.visible ? { ...previous, visible: false } : previous)) setOperationVisible(false) setOperationOriginRect(null) setOperationOriginElements(null) @@ -1032,6 +1114,7 @@ export const Home: React.FC = ({ playState: operationMorphAnimationRef.current?.playState, currentTime: operationMorphAnimationRef.current?.currentTime, }) + setOperationScrollbar((previous) => (previous.visible ? { ...previous, visible: false } : previous)) if (operationMorphRafRef.current !== null) { window.cancelAnimationFrame(operationMorphRafRef.current) operationMorphRafRef.current = null @@ -4181,30 +4264,50 @@ export const Home: React.FC = ({ {operationPanelContent} ) : ( - - {operationPanelContent} - + <> + + {operationPanelContent} + + {operationScrollbar.visible && ( + + )} + )} )