mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
fix(export): support dragging session table header horizontally
This commit is contained in:
@@ -1878,6 +1878,15 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.is-draggable {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.contacts-list-header-select {
|
.contacts-list-header-select {
|
||||||
@@ -1975,7 +1984,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-top-scrollbar,
|
|
||||||
.table-bottom-scrollbar {
|
.table-bottom-scrollbar {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
overflow-x: 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 {
|
.table-bottom-scrollbar-inner {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
@@ -2012,10 +2015,6 @@
|
|||||||
border-top: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
border-top: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-top-scrollbar-inner {
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-clear-btn {
|
.selection-clear-btn {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -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 { useLocation } from 'react-router-dom'
|
||||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
@@ -1512,6 +1512,7 @@ function ExportPage() {
|
|||||||
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
|
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
|
||||||
const [nowTick, setNowTick] = useState(Date.now())
|
const [nowTick, setNowTick] = useState(Date.now())
|
||||||
const [isContactsListAtTop, setIsContactsListAtTop] = useState(true)
|
const [isContactsListAtTop, setIsContactsListAtTop] = useState(true)
|
||||||
|
const [isContactsHeaderDragging, setIsContactsHeaderDragging] = useState(false)
|
||||||
const [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({
|
const [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({
|
||||||
viewportWidth: 0,
|
viewportWidth: 0,
|
||||||
contentWidth: 0
|
contentWidth: 0
|
||||||
@@ -1537,11 +1538,16 @@ function ExportPage() {
|
|||||||
const contactsAvatarCacheRef = useRef<Record<string, configService.ContactsAvatarCacheEntry>>({})
|
const contactsAvatarCacheRef = useRef<Record<string, configService.ContactsAvatarCacheEntry>>({})
|
||||||
const contactsVirtuosoRef = useRef<VirtuosoHandle | null>(null)
|
const contactsVirtuosoRef = useRef<VirtuosoHandle | null>(null)
|
||||||
const sessionTableSectionRef = useRef<HTMLDivElement | null>(null)
|
const sessionTableSectionRef = useRef<HTMLDivElement | null>(null)
|
||||||
const contactsTopScrollbarRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
const contactsHorizontalViewportRef = useRef<HTMLDivElement | null>(null)
|
const contactsHorizontalViewportRef = useRef<HTMLDivElement | null>(null)
|
||||||
const contactsHorizontalContentRef = useRef<HTMLDivElement | null>(null)
|
const contactsHorizontalContentRef = useRef<HTMLDivElement | null>(null)
|
||||||
const contactsBottomScrollbarRef = useRef<HTMLDivElement | null>(null)
|
const contactsBottomScrollbarRef = useRef<HTMLDivElement | null>(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<HTMLDivElement | null>(null)
|
const sessionFormatDropdownRef = useRef<HTMLDivElement | null>(null)
|
||||||
const detailRequestSeqRef = useRef(0)
|
const detailRequestSeqRef = useRef(0)
|
||||||
const sessionsRef = useRef<SessionRow[]>([])
|
const sessionsRef = useRef<SessionRow[]>([])
|
||||||
@@ -5666,18 +5672,13 @@ function ExportPage() {
|
|||||||
row.mutualFriends.statusLabel.startsWith('加载中')
|
row.mutualFriends.statusLabel.startsWith('加载中')
|
||||||
))
|
))
|
||||||
), [sessionLoadDetailRows])
|
), [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
|
if (contactsScrollSyncSourceRef.current && contactsScrollSyncSourceRef.current !== source) return
|
||||||
|
|
||||||
contactsScrollSyncSourceRef.current = source
|
contactsScrollSyncSourceRef.current = source
|
||||||
const topScrollbar = contactsTopScrollbarRef.current
|
|
||||||
const viewport = contactsHorizontalViewportRef.current
|
const viewport = contactsHorizontalViewportRef.current
|
||||||
const bottomScrollbar = contactsBottomScrollbarRef.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) {
|
if (source !== 'viewport' && viewport && Math.abs(viewport.scrollLeft - scrollLeft) > 1) {
|
||||||
viewport.scrollLeft = scrollLeft
|
viewport.scrollLeft = scrollLeft
|
||||||
}
|
}
|
||||||
@@ -5695,12 +5696,63 @@ function ExportPage() {
|
|||||||
const handleContactsHorizontalViewportScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
const handleContactsHorizontalViewportScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
||||||
syncContactsHorizontalScroll('viewport', event.currentTarget.scrollLeft)
|
syncContactsHorizontalScroll('viewport', event.currentTarget.scrollLeft)
|
||||||
}, [syncContactsHorizontalScroll])
|
}, [syncContactsHorizontalScroll])
|
||||||
const handleContactsTopScrollbarScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
|
||||||
syncContactsHorizontalScroll('top', event.currentTarget.scrollLeft)
|
|
||||||
}, [syncContactsHorizontalScroll])
|
|
||||||
const handleContactsBottomScrollbarScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
const handleContactsBottomScrollbarScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
||||||
syncContactsHorizontalScroll('bottom', event.currentTarget.scrollLeft)
|
syncContactsHorizontalScroll('bottom', event.currentTarget.scrollLeft)
|
||||||
}, [syncContactsHorizontalScroll])
|
}, [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<HTMLDivElement>) => {
|
||||||
|
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<HTMLDivElement>) => {
|
||||||
|
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<HTMLDivElement>) => {
|
||||||
|
if (contactsHeaderDragStateRef.current.pointerId !== event.pointerId) return
|
||||||
|
resetContactsHeaderDrag(event.currentTarget)
|
||||||
|
}, [resetContactsHeaderDrag])
|
||||||
|
const handleContactsHeaderPointerCancel = useCallback((event: PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (contactsHeaderDragStateRef.current.pointerId !== event.pointerId) return
|
||||||
|
resetContactsHeaderDrag(event.currentTarget)
|
||||||
|
}, [resetContactsHeaderDrag])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const viewport = contactsHorizontalViewportRef.current
|
const viewport = contactsHorizontalViewportRef.current
|
||||||
const content = contactsHorizontalContentRef.current
|
const content = contactsHorizontalContentRef.current
|
||||||
@@ -5723,17 +5775,6 @@ function ExportPage() {
|
|||||||
viewport.scrollLeft = clampedScrollLeft
|
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
|
const bottomScrollbar = contactsBottomScrollbarRef.current
|
||||||
if (bottomScrollbar) {
|
if (bottomScrollbar) {
|
||||||
const nextScrollLeft = Math.min(bottomScrollbar.scrollLeft, maxScrollLeft)
|
const nextScrollLeft = Math.min(bottomScrollbar.scrollLeft, maxScrollLeft)
|
||||||
@@ -6337,19 +6378,14 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasFilteredContacts && hasContactsHorizontalOverflow && (
|
|
||||||
<div
|
|
||||||
ref={contactsTopScrollbarRef}
|
|
||||||
className="table-top-scrollbar"
|
|
||||||
onScroll={handleContactsTopScrollbarScroll}
|
|
||||||
aria-label="会话列表顶部横向滚动条"
|
|
||||||
>
|
|
||||||
<div className="table-top-scrollbar-inner" style={contactsBottomScrollbarInnerStyle} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasFilteredContacts && (
|
{hasFilteredContacts && (
|
||||||
<div className="contacts-list-header">
|
<div
|
||||||
|
className={`contacts-list-header ${hasContactsHorizontalOverflow ? 'is-draggable' : ''} ${isContactsHeaderDragging ? 'is-dragging' : ''}`}
|
||||||
|
onPointerDown={handleContactsHeaderPointerDown}
|
||||||
|
onPointerMove={handleContactsHeaderPointerMove}
|
||||||
|
onPointerUp={handleContactsHeaderPointerUp}
|
||||||
|
onPointerCancel={handleContactsHeaderPointerCancel}
|
||||||
|
>
|
||||||
<span className="contacts-list-header-select">
|
<span className="contacts-list-header-select">
|
||||||
<button
|
<button
|
||||||
className={`select-icon-btn ${isAllVisibleSelected ? 'checked' : ''}`}
|
className={`select-icon-btn ${isAllVisibleSelected ? 'checked' : ''}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user