fix(export): add horizontal scrollbar for narrow session table

This commit is contained in:
aits2026
2026-03-06 18:05:31 +08:00
parent a0dda0b866
commit 1d1b38210a
2 changed files with 314 additions and 144 deletions

View File

@@ -1555,6 +1555,7 @@
--contacts-message-col-width: 120px;
--contacts-media-col-width: 72px;
--contacts-action-col-width: 140px;
--contacts-table-min-width: 1200px;
overflow: hidden;
border: 1px solid var(--border-color);
border-radius: 10px;
@@ -1566,6 +1567,26 @@
}
.table-wrap {
.table-scroll-viewport {
flex: 1;
min-height: 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.table-scroll-content {
min-width: max(100%, var(--contacts-table-min-width));
width: max-content;
min-height: 100%;
display: flex;
flex-direction: column;
}
.session-table-sticky {
position: sticky;
top: 0;
@@ -1575,6 +1596,7 @@
.loading-state,
.empty-state {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
@@ -1590,6 +1612,7 @@
}
.load-issue-state {
width: 100%;
flex: 1;
padding: 14px;
overflow-y: auto;
@@ -1766,6 +1789,7 @@
}
.contacts-list {
width: 100%;
flex: 1;
min-height: var(--contacts-default-list-height);
height: var(--contacts-default-list-height);
@@ -1787,6 +1811,34 @@
}
}
.table-bottom-scrollbar {
flex: 0 0 auto;
height: 16px;
overflow-x: auto;
overflow-y: hidden;
border-top: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary));
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--text-tertiary) 70%, transparent) transparent;
&::-webkit-scrollbar {
height: 10px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 999px;
background: color-mix(in srgb, var(--text-tertiary) 70%, transparent);
}
}
.table-bottom-scrollbar-inner {
height: 1px;
}
.selection-clear-btn {
border: 1px solid var(--border-color);
border-radius: 8px;

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState, type WheelEvent } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type UIEvent, type WheelEvent } from 'react'
import { useLocation } from 'react-router-dom'
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
import { createPortal } from 'react-dom'
@@ -1487,6 +1487,10 @@ function ExportPage() {
const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false)
const [nowTick, setNowTick] = useState(Date.now())
const [isContactsListAtTop, setIsContactsListAtTop] = useState(true)
const [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({
viewportWidth: 0,
contentWidth: 0
})
const tabCounts = useContactTypeCountsStore(state => state.tabCounts)
const isSharedTabCountsLoading = useContactTypeCountsStore(state => state.isLoading)
const isSharedTabCountsReady = useContactTypeCountsStore(state => state.isReady)
@@ -1508,6 +1512,10 @@ function ExportPage() {
const contactsAvatarCacheRef = useRef<Record<string, configService.ContactsAvatarCacheEntry>>({})
const contactsVirtuosoRef = useRef<VirtuosoHandle | null>(null)
const sessionTableSectionRef = useRef<HTMLDivElement | null>(null)
const contactsHorizontalViewportRef = useRef<HTMLDivElement | null>(null)
const contactsHorizontalContentRef = useRef<HTMLDivElement | null>(null)
const contactsBottomScrollbarRef = useRef<HTMLDivElement | null>(null)
const contactsScrollSyncSourceRef = useRef<'viewport' | 'bottom' | null>(null)
const sessionFormatDropdownRef = useRef<HTMLDivElement | null>(null)
const detailRequestSeqRef = useRef(0)
const sessionsRef = useRef<SessionRow[]>([])
@@ -5563,6 +5571,21 @@ function ExportPage() {
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length
const taskCenterAlertCount = taskRunningCount + taskQueuedCount
const hasFilteredContacts = filteredContacts.length > 0
const contactsTableMinWidth = useMemo(() => {
const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + 140 + (8 * 12)
const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0
const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0
return baseWidth + snsWidth + mutualFriendsWidth
}, [shouldShowMutualFriendsColumn, shouldShowSnsColumn])
const contactsTableStyle = useMemo(() => (
{
['--contacts-table-min-width' as const]: `${contactsTableMinWidth}px`
} as CSSProperties
), [contactsTableMinWidth])
const hasContactsHorizontalOverflow = contactsHorizontalScrollMetrics.contentWidth - contactsHorizontalScrollMetrics.viewportWidth > 1
const contactsBottomScrollbarInnerStyle = useMemo<CSSProperties>(() => ({
width: `${Math.max(contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth)}px`
}), [contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth])
const sessionLoadDetailUpdatedAt = useMemo(() => {
let latest = 0
for (const row of sessionLoadDetailRows) {
@@ -5588,6 +5611,82 @@ function ExportPage() {
row.mutualFriends.statusLabel.startsWith('加载中')
))
), [sessionLoadDetailRows])
const syncContactsHorizontalScroll = useCallback((source: 'viewport' | 'bottom', scrollLeft: number) => {
if (contactsScrollSyncSourceRef.current && contactsScrollSyncSourceRef.current !== source) return
contactsScrollSyncSourceRef.current = source
const viewport = contactsHorizontalViewportRef.current
const bottomScrollbar = contactsBottomScrollbarRef.current
if (source !== 'viewport' && viewport && Math.abs(viewport.scrollLeft - scrollLeft) > 1) {
viewport.scrollLeft = scrollLeft
}
if (source !== 'bottom' && bottomScrollbar && Math.abs(bottomScrollbar.scrollLeft - scrollLeft) > 1) {
bottomScrollbar.scrollLeft = scrollLeft
}
window.requestAnimationFrame(() => {
if (contactsScrollSyncSourceRef.current === source) {
contactsScrollSyncSourceRef.current = null
}
})
}, [])
const handleContactsHorizontalViewportScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
syncContactsHorizontalScroll('viewport', event.currentTarget.scrollLeft)
}, [syncContactsHorizontalScroll])
const handleContactsBottomScrollbarScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
syncContactsHorizontalScroll('bottom', event.currentTarget.scrollLeft)
}, [syncContactsHorizontalScroll])
useEffect(() => {
const viewport = contactsHorizontalViewportRef.current
const content = contactsHorizontalContentRef.current
if (!viewport || !content) return
const syncMetrics = () => {
const viewportWidth = Math.round(viewport.clientWidth)
const contentWidth = Math.round(content.scrollWidth)
setContactsHorizontalScrollMetrics((prev) => (
prev.viewportWidth === viewportWidth && prev.contentWidth === contentWidth
? prev
: { viewportWidth, contentWidth }
))
const maxScrollLeft = Math.max(0, contentWidth - viewportWidth)
const clampedScrollLeft = Math.min(viewport.scrollLeft, maxScrollLeft)
if (Math.abs(viewport.scrollLeft - clampedScrollLeft) > 1) {
viewport.scrollLeft = clampedScrollLeft
}
const bottomScrollbar = contactsBottomScrollbarRef.current
if (bottomScrollbar) {
const nextScrollLeft = Math.min(bottomScrollbar.scrollLeft, maxScrollLeft)
if (Math.abs(bottomScrollbar.scrollLeft - nextScrollLeft) > 1) {
bottomScrollbar.scrollLeft = nextScrollLeft
}
if (Math.abs(nextScrollLeft - clampedScrollLeft) > 1) {
bottomScrollbar.scrollLeft = clampedScrollLeft
}
}
}
syncMetrics()
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', syncMetrics)
return () => window.removeEventListener('resize', syncMetrics)
}
const resizeObserver = new ResizeObserver(syncMetrics)
resizeObserver.observe(viewport)
resizeObserver.observe(content)
return () => {
resizeObserver.disconnect()
}
}, [])
const closeTaskCenter = useCallback(() => {
setIsTaskCenterOpen(false)
setExpandedPerfTaskId(null)
@@ -6115,7 +6214,13 @@ function ExportPage() {
</div>
<div className="session-table-section" ref={sessionTableSectionRef}>
<div className="session-table-layout">
<div className="table-wrap">
<div className="table-wrap" style={contactsTableStyle}>
<div
ref={contactsHorizontalViewportRef}
className="table-scroll-viewport"
onScroll={handleContactsHorizontalViewportScroll}
>
<div ref={contactsHorizontalContentRef} className="table-scroll-content">
<div className="session-table-sticky">
<div className="table-toolbar">
<div className="table-tabs" role="tablist" aria-label="会话类型">
@@ -6269,6 +6374,19 @@ function ExportPage() {
</div>
)}
</div>
</div>
{hasFilteredContacts && hasContactsHorizontalOverflow && (
<div
ref={contactsBottomScrollbarRef}
className="table-bottom-scrollbar"
onScroll={handleContactsBottomScrollbarScroll}
aria-label="会话列表横向滚动条"
>
<div className="table-bottom-scrollbar-inner" style={contactsBottomScrollbarInnerStyle} />
</div>
)}
</div>
{showSessionLoadDetailModal && (
<div