release: bump version to 0.26.2
This commit is contained in:
@@ -23,6 +23,26 @@ Released: 2026-04-12
|
||||
|
||||
- Fixed [frontend/public/earth/js/tv.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/tv.js) by refactoring repeated iframe/video reset paths into shared helpers, so TV source switching, empty-state fallback, and playback retry handling no longer duplicate cleanup logic across multiple branches.
|
||||
|
||||
## 0.26.2
|
||||
|
||||
Released: 2026-04-12
|
||||
|
||||
### Highlights
|
||||
|
||||
- Stabilized the new reusable scrollbar work by restoring reliable sidebar visibility and extending the same floating scrollbar language to the Data Sources tables without letting scrollbars squeeze layout width or regress the console navigation.
|
||||
|
||||
### Added
|
||||
|
||||
- Added [frontend/src/components/Scrollbar/ScrollbarOverlay.tsx](/home/ray/dev/linkong/planet/frontend/src/components/Scrollbar/ScrollbarOverlay.tsx) as an overlay variant that binds to existing scroll containers such as Ant Table bodies, so heavy data grids can adopt the new scrollbar visuals without replacing their built-in scrolling mechanics.
|
||||
|
||||
### Improved
|
||||
|
||||
- Improved [frontend/src/components/Scrollbar/Scrollbar.tsx](/home/ray/dev/linkong/planet/frontend/src/components/Scrollbar/Scrollbar.tsx), [frontend/src/components/AppLayout/AppLayout.tsx](/home/ray/dev/linkong/planet/frontend/src/components/AppLayout/AppLayout.tsx), and [frontend/src/index.css](/home/ray/dev/linkong/planet/frontend/src/index.css) by reverting the sidebar to a reliable vertical-first scrollbar path, then reintroducing automatic dual-axis support with independent floating tracks that no longer hide the thumb when only the sidebar needs vertical scrolling.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed [frontend/src/pages/DataSources/DataSources.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/DataSources/DataSources.tsx) and [frontend/src/index.css](/home/ray/dev/linkong/planet/frontend/src/index.css) so the built-in and custom datasource tables now use the new overlay scrollbar instead of native table scrollbars, keeping horizontal and vertical scrolling available without changing Ant Table’s internal layout behavior.
|
||||
|
||||
## 0.26.0
|
||||
|
||||
Released: 2026-04-12
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.26.1`
|
||||
- `dev` 当前开发分支历史推导到:`0.26.2`
|
||||
|
||||
## Timeline
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
| `0.25.3` | bugfix | `dev` | `pending` | refactor the Earth HUD visual system, extract the top-left Earth brand into a reusable language-driven component, and consolidate duplicated brand assets into a single canonical set |
|
||||
| `0.26.0` | feature | `dev` | `pending` | add the Earth TV live module with backend-configurable sources, a draggable/resizable TV HUD window, a first curated news channel catalog, and TV source management hooks in system settings |
|
||||
| `0.26.1` | bugfix | `dev` | `pending` | extract the dashboard sidebar scrollbar into a reusable component and clean duplicated Earth TV player reset logic after the first live-module rollout |
|
||||
| `0.26.2` | bugfix | `dev` | `pending` | stabilize the reusable sidebar scrollbar, restore automatic dual-axis floating tracks safely, and apply the same overlay scrollbar system to datasource tables |
|
||||
|
||||
## Maintenance Commits Not Counted as Version Bumps
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.26.1",
|
||||
"version": "0.26.2",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.26.1"
|
||||
version = "0.26.2"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user