release: bump version to 0.26.2
This commit is contained in:
@@ -133,7 +133,7 @@ function AppLayout({ children }: AppLayoutProps) {
|
||||
<Text strong style={{ color: 'white', fontSize: 18 }}>智能星球</Text>
|
||||
) : null}
|
||||
</div>
|
||||
<Scrollbar className="dashboard-sider-menu-scroll" axis="y">
|
||||
<Scrollbar className="dashboard-sider-menu-scroll">
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
} from 'react'
|
||||
|
||||
type Axis = 'x' | 'y'
|
||||
type ScrollbarAxis = 'x' | 'y' | 'both'
|
||||
|
||||
interface AxisScrollbarState {
|
||||
interface AxisState {
|
||||
visible: boolean
|
||||
dragging: boolean
|
||||
thumbSize: number
|
||||
@@ -18,17 +17,14 @@ interface AxisScrollbarState {
|
||||
}
|
||||
|
||||
interface ScrollbarState {
|
||||
x: AxisScrollbarState
|
||||
y: AxisScrollbarState
|
||||
x: AxisState
|
||||
y: AxisState
|
||||
}
|
||||
|
||||
interface ScrollbarProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
minThumbSize?: number
|
||||
axis?: ScrollbarAxis
|
||||
suppressScrollX?: boolean
|
||||
suppressScrollY?: boolean
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
@@ -37,7 +33,7 @@ interface DragState {
|
||||
pointerOffset: number
|
||||
}
|
||||
|
||||
const INITIAL_AXIS_STATE: AxisScrollbarState = {
|
||||
const INITIAL_AXIS_STATE: AxisState = {
|
||||
visible: false,
|
||||
dragging: false,
|
||||
thumbSize: 0,
|
||||
@@ -53,9 +49,6 @@ function Scrollbar({
|
||||
children,
|
||||
className = '',
|
||||
minThumbSize = 28,
|
||||
axis = 'both',
|
||||
suppressScrollX = false,
|
||||
suppressScrollY = false,
|
||||
}: ScrollbarProps) {
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null)
|
||||
const trackXRef = useRef<HTMLDivElement | null>(null)
|
||||
@@ -78,35 +71,31 @@ function Scrollbar({
|
||||
scrollTop,
|
||||
} = viewport
|
||||
|
||||
const enableX = axis === 'x' || axis === 'both'
|
||||
const enableY = axis === 'y' || axis === 'both'
|
||||
const hasOverflowX = enableX && !suppressScrollX && scrollWidth - clientWidth > 1
|
||||
const hasOverflowY = enableY && !suppressScrollY && scrollHeight - clientHeight > 1
|
||||
const hasOverflowX = scrollWidth - clientWidth > 1
|
||||
const hasOverflowY = scrollHeight - clientHeight > 1
|
||||
|
||||
const nextX: AxisScrollbarState = hasOverflowX && clientWidth > 0
|
||||
const nextX: AxisState = hasOverflowX && clientWidth > 0 && trackX.clientWidth > 0
|
||||
? (() => {
|
||||
const trackSize = trackX.clientWidth
|
||||
const thumbSize = Math.max(minThumbSize, (clientWidth / scrollWidth) * trackSize)
|
||||
const maxThumbOffset = Math.max(0, trackSize - thumbSize)
|
||||
const thumbSize = Math.max(minThumbSize, (clientWidth / scrollWidth) * trackX.clientWidth)
|
||||
const maxThumbOffset = Math.max(0, trackX.clientWidth - thumbSize)
|
||||
const scrollRange = Math.max(1, scrollWidth - clientWidth)
|
||||
return {
|
||||
visible: true,
|
||||
dragging: scrollbar.x.dragging && dragStateRef.current?.axis === 'x',
|
||||
dragging: dragStateRef.current?.axis === 'x',
|
||||
thumbSize,
|
||||
thumbOffset: (scrollLeft / scrollRange) * maxThumbOffset,
|
||||
}
|
||||
})()
|
||||
: { ...INITIAL_AXIS_STATE }
|
||||
|
||||
const nextY: AxisScrollbarState = hasOverflowY && clientHeight > 0
|
||||
const nextY: AxisState = hasOverflowY && clientHeight > 0 && trackY.clientHeight > 0
|
||||
? (() => {
|
||||
const trackSize = trackY.clientHeight
|
||||
const thumbSize = Math.max(minThumbSize, (clientHeight / scrollHeight) * trackSize)
|
||||
const maxThumbOffset = Math.max(0, trackSize - thumbSize)
|
||||
const thumbSize = Math.max(minThumbSize, (clientHeight / scrollHeight) * trackY.clientHeight)
|
||||
const maxThumbOffset = Math.max(0, trackY.clientHeight - thumbSize)
|
||||
const scrollRange = Math.max(1, scrollHeight - clientHeight)
|
||||
return {
|
||||
visible: true,
|
||||
dragging: scrollbar.y.dragging && dragStateRef.current?.axis === 'y',
|
||||
dragging: dragStateRef.current?.axis === 'y',
|
||||
thumbSize,
|
||||
thumbOffset: (scrollTop / scrollRange) * maxThumbOffset,
|
||||
}
|
||||
@@ -114,7 +103,7 @@ function Scrollbar({
|
||||
: { ...INITIAL_AXIS_STATE }
|
||||
|
||||
setScrollbar({ x: nextX, y: nextY })
|
||||
}, [minThumbSize, scrollbar.x.dragging, scrollbar.y.dragging, suppressScrollX, suppressScrollY])
|
||||
}, [minThumbSize])
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = viewportRef.current
|
||||
@@ -122,23 +111,45 @@ function Scrollbar({
|
||||
const trackY = trackYRef.current
|
||||
if (!viewport || !trackX || !trackY) return
|
||||
|
||||
let rafId = 0
|
||||
const scheduleUpdate = () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = window.requestAnimationFrame(() => {
|
||||
updateScrollbar()
|
||||
})
|
||||
}
|
||||
|
||||
updateScrollbar()
|
||||
scheduleUpdate()
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateScrollbar()
|
||||
scheduleUpdate()
|
||||
})
|
||||
const mutationObserver = new MutationObserver(() => {
|
||||
scheduleUpdate()
|
||||
})
|
||||
|
||||
resizeObserver.observe(viewport)
|
||||
resizeObserver.observe(trackX)
|
||||
resizeObserver.observe(trackY)
|
||||
Array.from(viewport.children).forEach((child) => resizeObserver.observe(child))
|
||||
viewport.addEventListener('scroll', updateScrollbar, { passive: true })
|
||||
window.addEventListener('resize', updateScrollbar)
|
||||
|
||||
mutationObserver.observe(viewport, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
})
|
||||
|
||||
viewport.addEventListener('scroll', scheduleUpdate, { passive: true })
|
||||
window.addEventListener('resize', scheduleUpdate)
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
resizeObserver.disconnect()
|
||||
viewport.removeEventListener('scroll', updateScrollbar)
|
||||
window.removeEventListener('resize', updateScrollbar)
|
||||
mutationObserver.disconnect()
|
||||
viewport.removeEventListener('scroll', scheduleUpdate)
|
||||
window.removeEventListener('resize', scheduleUpdate)
|
||||
}
|
||||
}, [children, updateScrollbar])
|
||||
|
||||
@@ -177,19 +188,16 @@ function Scrollbar({
|
||||
|
||||
const progress = Math.max(0, Math.min(nextThumbOffset, maxThumbOffset)) / maxThumbOffset
|
||||
if (axis === 'x') {
|
||||
const scrollRange = Math.max(0, viewport.scrollWidth - viewport.clientWidth)
|
||||
viewport.scrollLeft = progress * scrollRange
|
||||
viewport.scrollLeft = progress * Math.max(0, viewport.scrollWidth - viewport.clientWidth)
|
||||
return
|
||||
}
|
||||
const scrollRange = Math.max(0, viewport.scrollHeight - viewport.clientHeight)
|
||||
viewport.scrollTop = progress * scrollRange
|
||||
viewport.scrollTop = progress * Math.max(0, viewport.scrollHeight - viewport.clientHeight)
|
||||
}
|
||||
|
||||
const handleThumbPointerDown = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
const axisState = scrollbar[axis]
|
||||
if (!axisState.visible) return
|
||||
if (!scrollbar[axis].visible) return
|
||||
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
dragStateRef.current = {
|
||||
axis,
|
||||
pointerId: event.pointerId,
|
||||
@@ -207,9 +215,9 @@ function Scrollbar({
|
||||
}
|
||||
|
||||
const handleThumbPointerMove = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const track = axis === 'x' ? trackXRef.current : trackYRef.current
|
||||
const dragState = dragStateRef.current
|
||||
if (!track || !dragState || dragState.pointerId !== event.pointerId || dragState.axis !== axis) return
|
||||
const track = axis === 'x' ? trackXRef.current : trackYRef.current
|
||||
if (!dragState || dragState.axis !== axis || dragState.pointerId !== event.pointerId || !track) return
|
||||
|
||||
const rect = track.getBoundingClientRect()
|
||||
const nextThumbOffset = axis === 'x'
|
||||
@@ -219,7 +227,9 @@ function Scrollbar({
|
||||
}
|
||||
|
||||
const handleThumbPointerUp = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (dragStateRef.current?.pointerId !== event.pointerId || dragStateRef.current.axis !== axis) return
|
||||
const dragState = dragStateRef.current
|
||||
if (!dragState || dragState.axis !== axis || dragState.pointerId !== event.pointerId) return
|
||||
|
||||
dragStateRef.current = null
|
||||
document.body.style.userSelect = ''
|
||||
setScrollbar((previous) => ({
|
||||
@@ -230,70 +240,60 @@ function Scrollbar({
|
||||
}
|
||||
|
||||
const handleTrackPointerDown = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const axisState = scrollbar[axis]
|
||||
if (!axisState.visible || event.target !== event.currentTarget) return
|
||||
if (!scrollbar[axis].visible || event.target !== event.currentTarget) return
|
||||
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
const nextThumbOffset = axis === 'x'
|
||||
? event.clientX - rect.left - axisState.thumbSize / 2
|
||||
: event.clientY - rect.top - axisState.thumbSize / 2
|
||||
? event.clientX - rect.left - scrollbar.x.thumbSize / 2
|
||||
: event.clientY - rect.top - scrollbar.y.thumbSize / 2
|
||||
syncScrollFromThumbOffset(axis, nextThumbOffset)
|
||||
}
|
||||
|
||||
const showCorner = scrollbar.x.visible && scrollbar.y.visible
|
||||
const scrollbarClassName = [
|
||||
'scrollbar',
|
||||
`scrollbar--${axis}`,
|
||||
className,
|
||||
].filter(Boolean).join(' ')
|
||||
const hasCorner = scrollbar.x.visible && scrollbar.y.visible
|
||||
|
||||
return (
|
||||
<div className={scrollbarClassName}>
|
||||
<div className={['scrollbar', className].filter(Boolean).join(' ')}>
|
||||
<div ref={viewportRef} className="scrollbar__viewport">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{(axis === 'y' || axis === 'both') ? (
|
||||
<div
|
||||
ref={trackYRef}
|
||||
className={`scrollbar__track scrollbar__track--y${scrollbar.y.visible ? ' scrollbar__track--visible' : ''}${scrollbar.y.dragging ? ' scrollbar__track--dragging' : ''}${showCorner ? ' scrollbar__track--has-corner' : ''}`}
|
||||
onPointerDown={handleTrackPointerDown('y')}
|
||||
>
|
||||
{scrollbar.y.visible ? (
|
||||
<div
|
||||
className="scrollbar__thumb scrollbar__thumb--y"
|
||||
style={{
|
||||
height: `${scrollbar.y.thumbSize}px`,
|
||||
transform: `translateY(${scrollbar.y.thumbOffset}px)`,
|
||||
}}
|
||||
onPointerDown={handleThumbPointerDown('y')}
|
||||
onPointerMove={handleThumbPointerMove('y')}
|
||||
onPointerUp={handleThumbPointerUp('y')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
ref={trackYRef}
|
||||
className={`scrollbar__track scrollbar__track--y${scrollbar.y.visible ? ' scrollbar__track--visible' : ''}${scrollbar.y.dragging ? ' scrollbar__track--dragging' : ''}${hasCorner ? ' scrollbar__track--has-corner' : ''}`}
|
||||
onPointerDown={handleTrackPointerDown('y')}
|
||||
>
|
||||
{scrollbar.y.visible ? (
|
||||
<div
|
||||
className="scrollbar__thumb scrollbar__thumb--y"
|
||||
style={{
|
||||
height: `${scrollbar.y.thumbSize}px`,
|
||||
transform: `translateY(${scrollbar.y.thumbOffset}px)`,
|
||||
}}
|
||||
onPointerDown={handleThumbPointerDown('y')}
|
||||
onPointerMove={handleThumbPointerMove('y')}
|
||||
onPointerUp={handleThumbPointerUp('y')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{(axis === 'x' || axis === 'both') ? (
|
||||
<div
|
||||
ref={trackXRef}
|
||||
className={`scrollbar__track scrollbar__track--x${scrollbar.x.visible ? ' scrollbar__track--visible' : ''}${scrollbar.x.dragging ? ' scrollbar__track--dragging' : ''}${showCorner ? ' scrollbar__track--has-corner' : ''}`}
|
||||
onPointerDown={handleTrackPointerDown('x')}
|
||||
>
|
||||
{scrollbar.x.visible ? (
|
||||
<div
|
||||
className="scrollbar__thumb scrollbar__thumb--x"
|
||||
style={{
|
||||
width: `${scrollbar.x.thumbSize}px`,
|
||||
transform: `translateX(${scrollbar.x.thumbOffset}px)`,
|
||||
}}
|
||||
onPointerDown={handleThumbPointerDown('x')}
|
||||
onPointerMove={handleThumbPointerMove('x')}
|
||||
onPointerUp={handleThumbPointerUp('x')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
ref={trackXRef}
|
||||
className={`scrollbar__track scrollbar__track--x${scrollbar.x.visible ? ' scrollbar__track--visible' : ''}${scrollbar.x.dragging ? ' scrollbar__track--dragging' : ''}${hasCorner ? ' scrollbar__track--has-corner' : ''}`}
|
||||
onPointerDown={handleTrackPointerDown('x')}
|
||||
>
|
||||
{scrollbar.x.visible ? (
|
||||
<div
|
||||
className="scrollbar__thumb scrollbar__thumb--x"
|
||||
style={{
|
||||
width: `${scrollbar.x.thumbSize}px`,
|
||||
transform: `translateX(${scrollbar.x.thumbOffset}px)`,
|
||||
}}
|
||||
onPointerDown={handleThumbPointerDown('x')}
|
||||
onPointerMove={handleThumbPointerMove('x')}
|
||||
onPointerUp={handleThumbPointerUp('x')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
327
frontend/src/components/Scrollbar/ScrollbarOverlay.tsx
Normal file
327
frontend/src/components/Scrollbar/ScrollbarOverlay.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import {
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
type Axis = 'x' | 'y'
|
||||
|
||||
interface AxisState {
|
||||
visible: boolean
|
||||
dragging: boolean
|
||||
thumbSize: number
|
||||
thumbOffset: number
|
||||
}
|
||||
|
||||
interface ScrollbarState {
|
||||
x: AxisState
|
||||
y: AxisState
|
||||
}
|
||||
|
||||
interface ScrollbarOverlayProps {
|
||||
containerRef: RefObject<HTMLElement | null>
|
||||
targetSelector: string
|
||||
minThumbSize?: number
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
axis: Axis
|
||||
pointerId: number
|
||||
pointerOffset: number
|
||||
}
|
||||
|
||||
const INITIAL_AXIS_STATE: AxisState = {
|
||||
visible: false,
|
||||
dragging: false,
|
||||
thumbSize: 0,
|
||||
thumbOffset: 0,
|
||||
}
|
||||
|
||||
const INITIAL_STATE: ScrollbarState = {
|
||||
x: INITIAL_AXIS_STATE,
|
||||
y: INITIAL_AXIS_STATE,
|
||||
}
|
||||
|
||||
function ScrollbarOverlay({
|
||||
containerRef,
|
||||
targetSelector,
|
||||
minThumbSize = 28,
|
||||
}: ScrollbarOverlayProps) {
|
||||
const trackXRef = useRef<HTMLDivElement | null>(null)
|
||||
const trackYRef = useRef<HTMLDivElement | null>(null)
|
||||
const dragStateRef = useRef<DragState | null>(null)
|
||||
const targetRef = useRef<HTMLElement | null>(null)
|
||||
const [scrollbar, setScrollbar] = useState<ScrollbarState>(INITIAL_STATE)
|
||||
|
||||
const bindTarget = useCallback(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) {
|
||||
targetRef.current = null
|
||||
return null
|
||||
}
|
||||
|
||||
const target = container.querySelector<HTMLElement>(targetSelector)
|
||||
targetRef.current = target
|
||||
return target
|
||||
}, [containerRef, targetSelector])
|
||||
|
||||
const updateScrollbar = useCallback(() => {
|
||||
const target = targetRef.current ?? bindTarget()
|
||||
const trackX = trackXRef.current
|
||||
const trackY = trackYRef.current
|
||||
if (!target || !trackX || !trackY) return
|
||||
|
||||
const {
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
scrollWidth,
|
||||
scrollHeight,
|
||||
scrollLeft,
|
||||
scrollTop,
|
||||
} = target
|
||||
|
||||
const hasOverflowX = scrollWidth - clientWidth > 1
|
||||
const hasOverflowY = scrollHeight - clientHeight > 1
|
||||
|
||||
const nextX: AxisState = hasOverflowX && clientWidth > 0 && trackX.clientWidth > 0
|
||||
? (() => {
|
||||
const thumbSize = Math.max(minThumbSize, (clientWidth / scrollWidth) * trackX.clientWidth)
|
||||
const maxThumbOffset = Math.max(0, trackX.clientWidth - thumbSize)
|
||||
const scrollRange = Math.max(1, scrollWidth - clientWidth)
|
||||
return {
|
||||
visible: true,
|
||||
dragging: dragStateRef.current?.axis === 'x',
|
||||
thumbSize,
|
||||
thumbOffset: (scrollLeft / scrollRange) * maxThumbOffset,
|
||||
}
|
||||
})()
|
||||
: { ...INITIAL_AXIS_STATE }
|
||||
|
||||
const nextY: AxisState = hasOverflowY && clientHeight > 0 && trackY.clientHeight > 0
|
||||
? (() => {
|
||||
const thumbSize = Math.max(minThumbSize, (clientHeight / scrollHeight) * trackY.clientHeight)
|
||||
const maxThumbOffset = Math.max(0, trackY.clientHeight - thumbSize)
|
||||
const scrollRange = Math.max(1, scrollHeight - clientHeight)
|
||||
return {
|
||||
visible: true,
|
||||
dragging: dragStateRef.current?.axis === 'y',
|
||||
thumbSize,
|
||||
thumbOffset: (scrollTop / scrollRange) * maxThumbOffset,
|
||||
}
|
||||
})()
|
||||
: { ...INITIAL_AXIS_STATE }
|
||||
|
||||
setScrollbar({ x: nextX, y: nextY })
|
||||
}, [bindTarget, minThumbSize])
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
let cleanupTargetScroll: (() => void) | null = null
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let mutationObserver: MutationObserver | null = null
|
||||
let rafId = 0
|
||||
|
||||
const scheduleUpdate = () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = window.requestAnimationFrame(() => {
|
||||
updateScrollbar()
|
||||
})
|
||||
}
|
||||
|
||||
const attachTarget = () => {
|
||||
cleanupTargetScroll?.()
|
||||
resizeObserver?.disconnect()
|
||||
|
||||
const target = bindTarget()
|
||||
if (!target) {
|
||||
targetRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
target.addEventListener('scroll', handleScroll, { passive: true })
|
||||
cleanupTargetScroll = () => {
|
||||
target.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
scheduleUpdate()
|
||||
})
|
||||
resizeObserver.observe(target)
|
||||
Array.from(target.children).forEach((child) => resizeObserver?.observe(child))
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
attachTarget()
|
||||
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
attachTarget()
|
||||
scheduleUpdate()
|
||||
})
|
||||
mutationObserver.observe(container, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
})
|
||||
|
||||
window.addEventListener('resize', scheduleUpdate)
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
cleanupTargetScroll?.()
|
||||
resizeObserver?.disconnect()
|
||||
mutationObserver?.disconnect()
|
||||
window.removeEventListener('resize', scheduleUpdate)
|
||||
}
|
||||
}, [bindTarget, containerRef, updateScrollbar])
|
||||
|
||||
useEffect(() => {
|
||||
const stopDragging = () => {
|
||||
dragStateRef.current = null
|
||||
document.body.style.userSelect = ''
|
||||
setScrollbar((previous) => ({
|
||||
x: { ...previous.x, dragging: false },
|
||||
y: { ...previous.y, dragging: false },
|
||||
}))
|
||||
}
|
||||
|
||||
window.addEventListener('pointerup', stopDragging)
|
||||
window.addEventListener('pointercancel', stopDragging)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('pointerup', stopDragging)
|
||||
window.removeEventListener('pointercancel', stopDragging)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const syncScrollFromThumbOffset = (axis: Axis, nextThumbOffset: number) => {
|
||||
const target = targetRef.current
|
||||
const track = axis === 'x' ? trackXRef.current : trackYRef.current
|
||||
const axisState = scrollbar[axis]
|
||||
if (!target || !track) return
|
||||
|
||||
const trackSize = axis === 'x' ? track.clientWidth : track.clientHeight
|
||||
const maxThumbOffset = Math.max(0, trackSize - axisState.thumbSize)
|
||||
if (maxThumbOffset <= 0) {
|
||||
if (axis === 'x') target.scrollLeft = 0
|
||||
else target.scrollTop = 0
|
||||
return
|
||||
}
|
||||
|
||||
const progress = Math.max(0, Math.min(nextThumbOffset, maxThumbOffset)) / maxThumbOffset
|
||||
if (axis === 'x') {
|
||||
target.scrollLeft = progress * Math.max(0, target.scrollWidth - target.clientWidth)
|
||||
return
|
||||
}
|
||||
target.scrollTop = progress * Math.max(0, target.scrollHeight - target.clientHeight)
|
||||
}
|
||||
|
||||
const handleThumbPointerDown = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!scrollbar[axis].visible) return
|
||||
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
dragStateRef.current = {
|
||||
axis,
|
||||
pointerId: event.pointerId,
|
||||
pointerOffset: axis === 'x' ? event.clientX - rect.left : event.clientY - rect.top,
|
||||
}
|
||||
|
||||
setScrollbar((previous) => ({
|
||||
x: { ...previous.x, dragging: axis === 'x' },
|
||||
y: { ...previous.y, dragging: axis === 'y' },
|
||||
}))
|
||||
document.body.style.userSelect = 'none'
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleThumbPointerMove = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const dragState = dragStateRef.current
|
||||
const track = axis === 'x' ? trackXRef.current : trackYRef.current
|
||||
if (!dragState || dragState.axis !== axis || dragState.pointerId !== event.pointerId || !track) return
|
||||
|
||||
const rect = track.getBoundingClientRect()
|
||||
const nextThumbOffset = axis === 'x'
|
||||
? event.clientX - rect.left - dragState.pointerOffset
|
||||
: event.clientY - rect.top - dragState.pointerOffset
|
||||
syncScrollFromThumbOffset(axis, nextThumbOffset)
|
||||
}
|
||||
|
||||
const handleThumbPointerUp = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const dragState = dragStateRef.current
|
||||
if (!dragState || dragState.axis !== axis || dragState.pointerId !== event.pointerId) return
|
||||
|
||||
dragStateRef.current = null
|
||||
document.body.style.userSelect = ''
|
||||
setScrollbar((previous) => ({
|
||||
x: { ...previous.x, dragging: false },
|
||||
y: { ...previous.y, dragging: false },
|
||||
}))
|
||||
event.currentTarget.releasePointerCapture?.(event.pointerId)
|
||||
}
|
||||
|
||||
const handleTrackPointerDown = (axis: Axis) => (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!scrollbar[axis].visible || event.target !== event.currentTarget) return
|
||||
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
const nextThumbOffset = axis === 'x'
|
||||
? event.clientX - rect.left - scrollbar.x.thumbSize / 2
|
||||
: event.clientY - rect.top - scrollbar.y.thumbSize / 2
|
||||
syncScrollFromThumbOffset(axis, nextThumbOffset)
|
||||
}
|
||||
|
||||
const hasCorner = scrollbar.x.visible && scrollbar.y.visible
|
||||
|
||||
return (
|
||||
<div className="scrollbar scrollbar--overlay">
|
||||
<div
|
||||
ref={trackYRef}
|
||||
className={`scrollbar__track scrollbar__track--y${scrollbar.y.visible ? ' scrollbar__track--visible' : ''}${scrollbar.y.dragging ? ' scrollbar__track--dragging' : ''}${hasCorner ? ' scrollbar__track--has-corner' : ''}`}
|
||||
onPointerDown={handleTrackPointerDown('y')}
|
||||
>
|
||||
{scrollbar.y.visible ? (
|
||||
<div
|
||||
className="scrollbar__thumb scrollbar__thumb--y"
|
||||
style={{
|
||||
height: `${scrollbar.y.thumbSize}px`,
|
||||
transform: `translateY(${scrollbar.y.thumbOffset}px)`,
|
||||
}}
|
||||
onPointerDown={handleThumbPointerDown('y')}
|
||||
onPointerMove={handleThumbPointerMove('y')}
|
||||
onPointerUp={handleThumbPointerUp('y')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={trackXRef}
|
||||
className={`scrollbar__track scrollbar__track--x${scrollbar.x.visible ? ' scrollbar__track--visible' : ''}${scrollbar.x.dragging ? ' scrollbar__track--dragging' : ''}${hasCorner ? ' scrollbar__track--has-corner' : ''}`}
|
||||
onPointerDown={handleTrackPointerDown('x')}
|
||||
>
|
||||
{scrollbar.x.visible ? (
|
||||
<div
|
||||
className="scrollbar__thumb scrollbar__thumb--x"
|
||||
style={{
|
||||
width: `${scrollbar.x.thumbSize}px`,
|
||||
transform: `translateX(${scrollbar.x.thumbOffset}px)`,
|
||||
}}
|
||||
onPointerDown={handleThumbPointerDown('x')}
|
||||
onPointerMove={handleThumbPointerMove('x')}
|
||||
onPointerUp={handleThumbPointerUp('x')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScrollbarOverlay
|
||||
@@ -91,11 +91,6 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scrollbar--y .scrollbar__track--x,
|
||||
.scrollbar--x .scrollbar__track--y {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar__viewport {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
@@ -117,29 +112,35 @@ body {
|
||||
|
||||
.scrollbar__track--y {
|
||||
top: 0;
|
||||
right: 8px;
|
||||
width: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scrollbar__track--x {
|
||||
left: 0;
|
||||
bottom: 8px;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.scrollbar__track--y.scrollbar__track--has-corner {
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.scrollbar__track--x.scrollbar__track--has-corner {
|
||||
width: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.scrollbar__thumb {
|
||||
position: absolute;
|
||||
min-height: 28px;
|
||||
min-width: 28px;
|
||||
border-radius: 999px;
|
||||
background: rgba(196, 208, 224, 0.46);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.18s ease, background 0.18s ease, width 0.18s ease;
|
||||
transition: opacity 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
.scrollbar__thumb::after {
|
||||
@@ -149,24 +150,26 @@ body {
|
||||
|
||||
.scrollbar__thumb--y {
|
||||
top: 8px;
|
||||
right: 0;
|
||||
width: 6px;
|
||||
min-width: 6px;
|
||||
right: 3px;
|
||||
width: 4px;
|
||||
min-width: 4px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.scrollbar__thumb--y::after {
|
||||
inset: 0 -6px;
|
||||
inset: 0 -4px;
|
||||
}
|
||||
|
||||
.scrollbar__thumb--x {
|
||||
left: 8px;
|
||||
bottom: 0;
|
||||
height: 6px;
|
||||
min-height: 6px;
|
||||
bottom: 3px;
|
||||
height: 4px;
|
||||
min-height: 4px;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.scrollbar__thumb--x::after {
|
||||
inset: -6px 0;
|
||||
inset: -4px 0;
|
||||
}
|
||||
|
||||
.scrollbar:hover .scrollbar__track--visible .scrollbar__thumb,
|
||||
@@ -175,6 +178,10 @@ body {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scrollbar__track--visible {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.scrollbar__thumb:hover {
|
||||
background: rgba(216, 226, 240, 0.68);
|
||||
}
|
||||
@@ -1555,6 +1562,7 @@ body {
|
||||
}
|
||||
|
||||
.table-scroll-region {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
@@ -1575,35 +1583,31 @@ body {
|
||||
padding: 10px 12px !important;
|
||||
}
|
||||
|
||||
.table-scroll-region .ant-table-body,
|
||||
.table-scroll-region .ant-table-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
.data-source-table-region .ant-table-body,
|
||||
.data-source-table-region .ant-table-content {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.table-scroll-region .ant-table-body::-webkit-scrollbar,
|
||||
.table-scroll-region .ant-table-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
.data-source-table-region .ant-table-body::-webkit-scrollbar,
|
||||
.data-source-table-region .ant-table-content::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.table-scroll-region .ant-table-body::-webkit-scrollbar-thumb,
|
||||
.table-scroll-region .ant-table-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.82);
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
.scrollbar--overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.table-scroll-region .ant-table-body::-webkit-scrollbar-thumb:hover,
|
||||
.table-scroll-region .ant-table-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.9);
|
||||
background-clip: padding-box;
|
||||
.scrollbar--overlay .scrollbar__track--y {
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.table-scroll-region .ant-table-body::-webkit-scrollbar-track,
|
||||
.table-scroll-region .ant-table-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
.scrollbar--overlay .scrollbar__track--x {
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
.bgp-page {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import axios, { type AxiosResponse } from 'axios'
|
||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||
import ScrollbarOverlay from '../../components/Scrollbar/ScrollbarOverlay'
|
||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||
import { useWebSocket } from '../../hooks/useWebSocket'
|
||||
|
||||
@@ -1050,6 +1051,7 @@ function DataSources() {
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
/>
|
||||
<ScrollbarOverlay containerRef={builtinTableRegionRef} targetSelector=".ant-table-body" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@@ -1084,6 +1086,7 @@ function DataSources() {
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
/>
|
||||
<ScrollbarOverlay containerRef={customTableRegionRef} targetSelector=".ant-table-body" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user