mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
fix(export): add horizontal scrollbar for narrow session table
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user