From b070b4f659559fa11804ef9451e35525536de1e8 Mon Sep 17 00:00:00 2001 From: aits2026 Date: Fri, 6 Mar 2026 19:37:27 +0800 Subject: [PATCH] fix(export): support dragging session table header horizontally --- src/pages/ExportPage.scss | 19 ++++--- src/pages/ExportPage.tsx | 106 +++++++++++++++++++++++++------------- 2 files changed, 80 insertions(+), 45 deletions(-) diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 632ec42..e592061 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -1878,6 +1878,15 @@ font-weight: 600; letter-spacing: 0.01em; flex-shrink: 0; + + &.is-draggable { + cursor: grab; + } + + &.is-dragging { + cursor: grabbing; + user-select: none; + } } .contacts-list-header-select { @@ -1975,7 +1984,6 @@ } } - .table-top-scrollbar, .table-bottom-scrollbar { flex: 0 0 auto; overflow-x: auto; @@ -1998,11 +2006,6 @@ } } - .table-top-scrollbar { - height: 14px; - border-bottom: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); - } - .table-bottom-scrollbar-inner { height: 1px; } @@ -2012,10 +2015,6 @@ border-top: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); } - .table-top-scrollbar-inner { - height: 1px; - } - .selection-clear-btn { border: 1px solid var(--border-color); border-radius: 8px; diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 31fd25f..38d01a5 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type UIEvent, type WheelEvent } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent, type UIEvent, type WheelEvent } from 'react' import { useLocation } from 'react-router-dom' import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso' import { createPortal } from 'react-dom' @@ -1512,6 +1512,7 @@ function ExportPage() { const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false) const [nowTick, setNowTick] = useState(Date.now()) const [isContactsListAtTop, setIsContactsListAtTop] = useState(true) + const [isContactsHeaderDragging, setIsContactsHeaderDragging] = useState(false) const [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({ viewportWidth: 0, contentWidth: 0 @@ -1537,11 +1538,16 @@ function ExportPage() { const contactsAvatarCacheRef = useRef>({}) const contactsVirtuosoRef = useRef(null) const sessionTableSectionRef = useRef(null) - const contactsTopScrollbarRef = useRef(null) const contactsHorizontalViewportRef = useRef(null) const contactsHorizontalContentRef = useRef(null) const contactsBottomScrollbarRef = useRef(null) - const contactsScrollSyncSourceRef = useRef<'viewport' | 'top' | 'bottom' | null>(null) + const contactsScrollSyncSourceRef = useRef<'viewport' | 'bottom' | null>(null) + const contactsHeaderDragStateRef = useRef({ + pointerId: -1, + startClientX: 0, + startScrollLeft: 0, + didDrag: false + }) const sessionFormatDropdownRef = useRef(null) const detailRequestSeqRef = useRef(0) const sessionsRef = useRef([]) @@ -5666,18 +5672,13 @@ function ExportPage() { row.mutualFriends.statusLabel.startsWith('加载中') )) ), [sessionLoadDetailRows]) - const syncContactsHorizontalScroll = useCallback((source: 'viewport' | 'top' | 'bottom', scrollLeft: number) => { + const syncContactsHorizontalScroll = useCallback((source: 'viewport' | 'bottom', scrollLeft: number) => { if (contactsScrollSyncSourceRef.current && contactsScrollSyncSourceRef.current !== source) return contactsScrollSyncSourceRef.current = source - const topScrollbar = contactsTopScrollbarRef.current const viewport = contactsHorizontalViewportRef.current const bottomScrollbar = contactsBottomScrollbarRef.current - if (source !== 'top' && topScrollbar && Math.abs(topScrollbar.scrollLeft - scrollLeft) > 1) { - topScrollbar.scrollLeft = scrollLeft - } - if (source !== 'viewport' && viewport && Math.abs(viewport.scrollLeft - scrollLeft) > 1) { viewport.scrollLeft = scrollLeft } @@ -5695,12 +5696,63 @@ function ExportPage() { const handleContactsHorizontalViewportScroll = useCallback((event: UIEvent) => { syncContactsHorizontalScroll('viewport', event.currentTarget.scrollLeft) }, [syncContactsHorizontalScroll]) - const handleContactsTopScrollbarScroll = useCallback((event: UIEvent) => { - syncContactsHorizontalScroll('top', event.currentTarget.scrollLeft) - }, [syncContactsHorizontalScroll]) const handleContactsBottomScrollbarScroll = useCallback((event: UIEvent) => { syncContactsHorizontalScroll('bottom', event.currentTarget.scrollLeft) }, [syncContactsHorizontalScroll]) + const resetContactsHeaderDrag = useCallback((currentTarget?: HTMLDivElement | null) => { + const dragState = contactsHeaderDragStateRef.current + if (currentTarget && dragState.pointerId >= 0 && currentTarget.hasPointerCapture(dragState.pointerId)) { + currentTarget.releasePointerCapture(dragState.pointerId) + } + dragState.pointerId = -1 + dragState.startClientX = 0 + dragState.startScrollLeft = 0 + dragState.didDrag = false + setIsContactsHeaderDragging(false) + }, []) + const handleContactsHeaderPointerDown = useCallback((event: PointerEvent) => { + if (!hasContactsHorizontalOverflow || event.pointerType === 'touch') return + if (event.button !== 0) return + if (event.target instanceof Element && event.target.closest('button, a, input, textarea, select, label, [role="button"]')) { + return + } + + contactsHeaderDragStateRef.current = { + pointerId: event.pointerId, + startClientX: event.clientX, + startScrollLeft: contactsHorizontalViewportRef.current?.scrollLeft ?? 0, + didDrag: false + } + event.currentTarget.setPointerCapture(event.pointerId) + setIsContactsHeaderDragging(true) + }, [hasContactsHorizontalOverflow]) + const handleContactsHeaderPointerMove = useCallback((event: PointerEvent) => { + const dragState = contactsHeaderDragStateRef.current + if (dragState.pointerId !== event.pointerId) return + + const viewport = contactsHorizontalViewportRef.current + const content = contactsHorizontalContentRef.current + if (!viewport || !content) return + + const deltaX = event.clientX - dragState.startClientX + if (!dragState.didDrag && Math.abs(deltaX) < 4) return + + dragState.didDrag = true + const maxScrollLeft = Math.max(0, content.scrollWidth - viewport.clientWidth) + const nextScrollLeft = Math.max(0, Math.min(dragState.startScrollLeft - deltaX, maxScrollLeft)) + + viewport.scrollLeft = nextScrollLeft + syncContactsHorizontalScroll('viewport', nextScrollLeft) + event.preventDefault() + }, [syncContactsHorizontalScroll]) + const handleContactsHeaderPointerUp = useCallback((event: PointerEvent) => { + if (contactsHeaderDragStateRef.current.pointerId !== event.pointerId) return + resetContactsHeaderDrag(event.currentTarget) + }, [resetContactsHeaderDrag]) + const handleContactsHeaderPointerCancel = useCallback((event: PointerEvent) => { + if (contactsHeaderDragStateRef.current.pointerId !== event.pointerId) return + resetContactsHeaderDrag(event.currentTarget) + }, [resetContactsHeaderDrag]) useEffect(() => { const viewport = contactsHorizontalViewportRef.current const content = contactsHorizontalContentRef.current @@ -5723,17 +5775,6 @@ function ExportPage() { viewport.scrollLeft = clampedScrollLeft } - const topScrollbar = contactsTopScrollbarRef.current - if (topScrollbar) { - const nextScrollLeft = Math.min(topScrollbar.scrollLeft, maxScrollLeft) - if (Math.abs(topScrollbar.scrollLeft - nextScrollLeft) > 1) { - topScrollbar.scrollLeft = nextScrollLeft - } - if (Math.abs(nextScrollLeft - clampedScrollLeft) > 1) { - topScrollbar.scrollLeft = clampedScrollLeft - } - } - const bottomScrollbar = contactsBottomScrollbarRef.current if (bottomScrollbar) { const nextScrollLeft = Math.min(bottomScrollbar.scrollLeft, maxScrollLeft) @@ -6337,19 +6378,14 @@ function ExportPage() { )} - {hasFilteredContacts && hasContactsHorizontalOverflow && ( -
-
-
- )} - {hasFilteredContacts && ( -
+