release: bump version to 0.26.2

This commit is contained in:
rayd1o
2026-04-12 09:27:01 +08:00
parent b1d0624061
commit 2ee4773f4f
11 changed files with 494 additions and 139 deletions

View File

@@ -1 +1 @@
0.26.1
0.26.2

View File

@@ -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 Tables internal layout behavior.
## 0.26.0
Released: 2026-04-12

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.26.1",
"version": "0.26.2",
"private": true,
"packageManager": "bun@1",
"dependencies": {

View File

@@ -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"

View File

@@ -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>
)
}

View 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

View File

@@ -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 {

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
[project]
name = "planet"
version = "0.26.1"
version = "0.26.2"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]]
name = "planet"
version = "0.26.1"
version = "0.26.2"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },