From 8ee5dd9d35d9641bcbbd9a22a88be5ba4da5c276 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 15 Jun 2026 15:53:51 +0800 Subject: [PATCH] fix(grid): unify data grid scrollbars --- apps/desktop/src/components/grid/DataGrid.vue | 162 +++++++++++++++--- 1 file changed, 134 insertions(+), 28 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index c5ec8bf87..2a41d578f 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -1397,16 +1397,25 @@ const gridRef = ref(); const headerRef = ref(); const gridScrollbarGutter = ref(0); const gridHorizontalScrollbarTrackRef = ref(); +const gridVerticalScrollbarTrackRef = ref(); const hasGridHorizontalOverflow = ref(false); +const hasGridVerticalOverflow = ref(false); const gridHorizontalScrollbarThumbLeftPercent = ref(0); const gridHorizontalScrollbarThumbWidthPercent = ref(100); +const gridVerticalScrollbarThumbTopPercent = ref(0); +const gridVerticalScrollbarThumbHeightPercent = ref(100); const gridHorizontalScrollbarDragging = ref(false); +const gridVerticalScrollbarDragging = ref(false); let gridHorizontalScrollbarFrame = 0; let gridHorizontalScrollbarResizeObserver: ResizeObserver | null = null; let gridHorizontalScrollbarDragState: { trackRect: DOMRect; thumbOffsetPx: number; } | null = null; +let gridVerticalScrollbarDragState: { + trackRect: DOMRect; + thumbOffsetPx: number; +} | null = null; const hiddenColumnIndexes = ref>(new Set()); const nullColumnsHidden = ref(false); const autoHiddenNullColumnIndexes = ref>(new Set()); @@ -1565,6 +1574,7 @@ function updateGridHorizontalViewport(element: HTMLElement) { gridHorizontalScrollLeft.value = element.scrollLeft; gridViewportWidth.value = element.clientWidth; updateGridHorizontalScrollbar(element); + updateGridVerticalScrollbar(element); } function gridScrollerElement(): HTMLElement | null { @@ -1589,11 +1599,30 @@ function updateGridHorizontalScrollbar(element: HTMLElement | null = gridScrolle gridHorizontalScrollbarThumbLeftPercent.value = maxScrollLeft > 0 ? (element.scrollLeft / maxScrollLeft) * thumbTravel : 0; } +function updateGridVerticalScrollbar(element: HTMLElement | null = gridScrollerElement()) { + if (!element) { + hasGridVerticalOverflow.value = false; + gridVerticalScrollbarThumbTopPercent.value = 0; + gridVerticalScrollbarThumbHeightPercent.value = 100; + return; + } + + const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight); + hasGridVerticalOverflow.value = maxScrollTop > 1; + + const rawThumbHeight = element.scrollHeight > 0 ? (element.clientHeight / element.scrollHeight) * 100 : 100; + const thumbHeight = Math.min(100, Math.max(6, rawThumbHeight)); + const thumbTravel = Math.max(0, 100 - thumbHeight); + gridVerticalScrollbarThumbHeightPercent.value = thumbHeight; + gridVerticalScrollbarThumbTopPercent.value = maxScrollTop > 0 ? (element.scrollTop / maxScrollTop) * thumbTravel : 0; +} + function scheduleGridHorizontalScrollbarUpdate() { if (gridHorizontalScrollbarFrame) return; const update = () => { gridHorizontalScrollbarFrame = 0; updateGridHorizontalScrollbar(); + updateGridVerticalScrollbar(); }; if (typeof requestAnimationFrame === "function") { gridHorizontalScrollbarFrame = requestAnimationFrame(update); @@ -1651,6 +1680,16 @@ function stopGridHorizontalScrollbarDrag() { document.body.style.userSelect = ""; } +function stopGridVerticalScrollbarDrag() { + if (!gridVerticalScrollbarDragState) return; + gridVerticalScrollbarDragState = null; + gridVerticalScrollbarDragging.value = false; + window.removeEventListener("pointermove", onGridVerticalScrollbarPointerMove, true); + window.removeEventListener("pointerup", stopGridVerticalScrollbarDrag, true); + window.removeEventListener("pointercancel", stopGridVerticalScrollbarDrag, true); + document.body.style.userSelect = ""; +} + function startGridHorizontalScrollbarDrag(event: PointerEvent) { const scroller = gridScrollerElement(); const track = gridHorizontalScrollbarTrackRef.value; @@ -1675,11 +1714,62 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) { applyGridHorizontalScrollbarDrag(event.clientX); } +function applyGridVerticalScrollbarDrag(clientY: number) { + const scroller = gridScrollerElement(); + const dragState = gridVerticalScrollbarDragState; + if (!scroller || !dragState) return; + + const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); + if (maxScrollTop <= 1) return; + + const thumbHeightPx = dragState.trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100); + const maxThumbTopPx = Math.max(1, dragState.trackRect.height - thumbHeightPx); + const thumbTopPx = Math.min(maxThumbTopPx, Math.max(0, clientY - dragState.trackRect.top - dragState.thumbOffsetPx)); + scroller.scrollTop = (thumbTopPx / maxThumbTopPx) * maxScrollTop; + updateGridVerticalScrollbar(scroller); + if (useCanvasGridRows.value) syncCanvasViewport(); +} + +function onGridVerticalScrollbarPointerMove(event: PointerEvent) { + if (!gridVerticalScrollbarDragState) return; + event.preventDefault(); + applyGridVerticalScrollbarDrag(event.clientY); +} + +function startGridVerticalScrollbarDrag(event: PointerEvent) { + const scroller = gridScrollerElement(); + const track = gridVerticalScrollbarTrackRef.value; + if (!scroller || !track || !hasGridVerticalOverflow.value) return; + + const trackRect = track.getBoundingClientRect(); + const thumbTopPx = trackRect.height * (gridVerticalScrollbarThumbTopPercent.value / 100); + const thumbHeightPx = trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100); + const pointerY = event.clientY - trackRect.top; + const pointerInsideThumb = pointerY >= thumbTopPx && pointerY <= thumbTopPx + thumbHeightPx; + + gridVerticalScrollbarDragState = { + trackRect, + thumbOffsetPx: pointerInsideThumb ? pointerY - thumbTopPx : thumbHeightPx / 2, + }; + gridVerticalScrollbarDragging.value = true; + document.body.style.userSelect = "none"; + window.addEventListener("pointermove", onGridVerticalScrollbarPointerMove, true); + window.addEventListener("pointerup", stopGridVerticalScrollbarDrag, true); + window.addEventListener("pointercancel", stopGridVerticalScrollbarDrag, true); + event.preventDefault(); + applyGridVerticalScrollbarDrag(event.clientY); +} + const gridHorizontalScrollbarThumbStyle = computed(() => ({ width: `${gridHorizontalScrollbarThumbWidthPercent.value}%`, left: `${gridHorizontalScrollbarThumbLeftPercent.value}%`, })); +const gridVerticalScrollbarThumbStyle = computed(() => ({ + height: `${gridVerticalScrollbarThumbHeightPercent.value}%`, + top: `${gridVerticalScrollbarThumbTopPercent.value}%`, +})); + function updateGridScrollbarGutter(element: HTMLElement) { gridScrollbarGutter.value = scrollbarGutterWidth(element); } @@ -3585,6 +3675,7 @@ function onCanvasScroll(event: Event) { if (gridHorizontalScrollLeft.value !== scrollLeft || gridViewportWidth.value !== viewportWidth) { updateGridHorizontalViewport(scroller); } + updateGridVerticalScrollbar(scroller); const gutter = scrollbarGutterWidth(scroller); if (gridScrollbarGutter.value !== gutter) gridScrollbarGutter.value = gutter; if (headerRef.value && headerRef.value.scrollLeft !== scrollLeft) headerRef.value.scrollLeft = scrollLeft; @@ -3936,6 +4027,7 @@ onUnmounted(() => { pauseCanvasGridWork(); gridHorizontalScrollbarResizeObserver?.disconnect(); stopGridHorizontalScrollbarDrag(); + stopGridVerticalScrollbarDrag(); if (gridHorizontalScrollbarFrame && typeof cancelAnimationFrame === "function") { cancelAnimationFrame(gridHorizontalScrollbarFrame); } @@ -6908,6 +7000,9 @@ const gridContextMenuItems = computed(() => {
+
+
+
@@ -7705,6 +7800,9 @@ const gridContextMenuItems = computed(() => { --data-grid-row-number-deleted-bg: rgb(255 244 244); --data-grid-row-number-active-bg: rgb(232 232 232); --data-grid-row-number-selected-bg: rgb(226 226 226); + --data-grid-scrollbar-thumb: color-mix(in oklch, var(--foreground) 30%, transparent); + --data-grid-scrollbar-thumb-hover: color-mix(in oklch, var(--foreground) 48%, transparent); + --data-grid-scrollbar-track: transparent; } :global(.dark) [data-grid-root] { @@ -7761,34 +7859,11 @@ const gridContextMenuItems = computed(() => { scrollbar-gutter: stable; will-change: scroll-position; contain: layout style paint; - overflow-x: hidden; - scrollbar-width: thin; - scrollbar-color: color-mix(in oklch, var(--foreground) 30%, transparent) transparent; + scrollbar-width: none; } .data-grid-scroller::-webkit-scrollbar { - width: 10px; - height: 0; -} - -.data-grid-scroller::-webkit-scrollbar:horizontal { - height: 0; -} - -.data-grid-scroller::-webkit-scrollbar-track { - background: transparent; -} - -.data-grid-scroller::-webkit-scrollbar-thumb { - border: 3px solid transparent; - border-radius: 999px; - background: color-mix(in oklch, var(--foreground) 30%, transparent); - background-clip: content-box; -} - -.data-grid-scroller::-webkit-scrollbar-thumb:hover { - background: color-mix(in oklch, var(--foreground) 48%, transparent); - background-clip: content-box; + display: none; } .data-grid-scroller :deep(.vue-recycle-scroller__item-wrapper) { @@ -7821,7 +7896,7 @@ const gridContextMenuItems = computed(() => { top: 4px; height: 2px; border-radius: 999px; - background: color-mix(in oklch, var(--foreground) 7%, transparent); + background: var(--data-grid-scrollbar-track); } .data-grid-horizontal-scrollbar__thumb { @@ -7830,7 +7905,7 @@ const gridContextMenuItems = computed(() => { height: 4px; min-width: 24px; border-radius: 999px; - background: color-mix(in oklch, var(--foreground) 24%, transparent); + background: var(--data-grid-scrollbar-thumb); transition: height 120ms ease, background-color 120ms ease, @@ -7841,7 +7916,38 @@ const gridContextMenuItems = computed(() => { .data-grid-horizontal-scrollbar--dragging .data-grid-horizontal-scrollbar__thumb { top: 2px; height: 6px; - background: color-mix(in oklch, var(--foreground) 42%, transparent); + background: var(--data-grid-scrollbar-thumb-hover); +} + +.data-grid-vertical-scrollbar { + position: absolute; + top: 10px; + right: 2px; + bottom: 14px; + z-index: 30; + width: 10px; + cursor: pointer; + touch-action: none; +} + +.data-grid-vertical-scrollbar__thumb { + position: absolute; + left: 3px; + width: 4px; + min-height: 24px; + border-radius: 999px; + background: var(--data-grid-scrollbar-thumb); + transition: + background-color 120ms ease, + left 120ms ease, + width 120ms ease; +} + +.data-grid-vertical-scrollbar:hover .data-grid-vertical-scrollbar__thumb, +.data-grid-vertical-scrollbar--dragging .data-grid-vertical-scrollbar__thumb { + left: 2px; + width: 6px; + background: var(--data-grid-scrollbar-thumb-hover); } .canvas-grid-surface {