feat: 添加操作弹窗自定义滚动条

This commit is contained in:
JSR
2026-07-18 09:47:29 +08:00
parent fa162b2b57
commit 07743ea524
2 changed files with 158 additions and 24 deletions
+31
View File
@@ -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;
+103
View File
@@ -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<HomeProps> = ({
const [batchMode, setBatchMode] = useState(false)
const [selectedStudentIds, setSelectedStudentIds] = useState<number[]>([])
const [operationVisible, setOperationVisible] = useState(false)
const [operationScrollbar, setOperationScrollbar] = useState<operationScrollbarState>({
visible: false,
left: 0,
top: 0,
height: 0,
thumbTop: 0,
thumbHeight: 0,
})
const [operationOriginRect, setOperationOriginRect] = useState<DOMRect | null>(null)
const [operationOriginElements, setOperationOriginElements] =
useState<operationMorphElementRects | null>(null)
@@ -205,6 +222,70 @@ export const Home: React.FC<HomeProps> = ({
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<HomeProps> = ({
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<HomeProps> = ({
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,6 +4264,7 @@ export const Home: React.FC<HomeProps> = ({
{operationPanelContent}
</Drawer>
) : (
<>
<Modal
title={null}
open={operationVisible}
@@ -4205,6 +4289,25 @@ export const Home: React.FC<HomeProps> = ({
>
{operationPanelContent}
</Modal>
{operationScrollbar.visible && (
<div
className="ss-operation-designed-scrollbar"
aria-hidden="true"
style={{
left: operationScrollbar.left,
top: operationScrollbar.top,
height: operationScrollbar.height,
}}
>
<span
style={{
top: operationScrollbar.thumbTop,
height: operationScrollbar.thumbHeight,
}}
/>
</div>
)}
</>
)}
</div>
)