mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +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-message-col-width: 120px;
|
||||||
--contacts-media-col-width: 72px;
|
--contacts-media-col-width: 72px;
|
||||||
--contacts-action-col-width: 140px;
|
--contacts-action-col-width: 140px;
|
||||||
|
--contacts-table-min-width: 1200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -1566,6 +1567,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-wrap {
|
.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 {
|
.session-table-sticky {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -1575,6 +1596,7 @@
|
|||||||
|
|
||||||
.loading-state,
|
.loading-state,
|
||||||
.empty-state {
|
.empty-state {
|
||||||
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1590,6 +1612,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.load-issue-state {
|
.load-issue-state {
|
||||||
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -1766,6 +1789,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contacts-list {
|
.contacts-list {
|
||||||
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: var(--contacts-default-list-height);
|
min-height: var(--contacts-default-list-height);
|
||||||
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 {
|
.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 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 { 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'
|
||||||
@@ -1487,6 +1487,10 @@ 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 [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({
|
||||||
|
viewportWidth: 0,
|
||||||
|
contentWidth: 0
|
||||||
|
})
|
||||||
const tabCounts = useContactTypeCountsStore(state => state.tabCounts)
|
const tabCounts = useContactTypeCountsStore(state => state.tabCounts)
|
||||||
const isSharedTabCountsLoading = useContactTypeCountsStore(state => state.isLoading)
|
const isSharedTabCountsLoading = useContactTypeCountsStore(state => state.isLoading)
|
||||||
const isSharedTabCountsReady = useContactTypeCountsStore(state => state.isReady)
|
const isSharedTabCountsReady = useContactTypeCountsStore(state => state.isReady)
|
||||||
@@ -1508,6 +1512,10 @@ 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 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 sessionFormatDropdownRef = useRef<HTMLDivElement | null>(null)
|
||||||
const detailRequestSeqRef = useRef(0)
|
const detailRequestSeqRef = useRef(0)
|
||||||
const sessionsRef = useRef<SessionRow[]>([])
|
const sessionsRef = useRef<SessionRow[]>([])
|
||||||
@@ -5563,6 +5571,21 @@ function ExportPage() {
|
|||||||
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length
|
const taskQueuedCount = tasks.filter(task => task.status === 'queued').length
|
||||||
const taskCenterAlertCount = taskRunningCount + taskQueuedCount
|
const taskCenterAlertCount = taskRunningCount + taskQueuedCount
|
||||||
const hasFilteredContacts = filteredContacts.length > 0
|
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(() => {
|
const sessionLoadDetailUpdatedAt = useMemo(() => {
|
||||||
let latest = 0
|
let latest = 0
|
||||||
for (const row of sessionLoadDetailRows) {
|
for (const row of sessionLoadDetailRows) {
|
||||||
@@ -5588,6 +5611,82 @@ function ExportPage() {
|
|||||||
row.mutualFriends.statusLabel.startsWith('加载中')
|
row.mutualFriends.statusLabel.startsWith('加载中')
|
||||||
))
|
))
|
||||||
), [sessionLoadDetailRows])
|
), [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(() => {
|
const closeTaskCenter = useCallback(() => {
|
||||||
setIsTaskCenterOpen(false)
|
setIsTaskCenterOpen(false)
|
||||||
setExpandedPerfTaskId(null)
|
setExpandedPerfTaskId(null)
|
||||||
@@ -6115,7 +6214,13 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="session-table-section" ref={sessionTableSectionRef}>
|
<div className="session-table-section" ref={sessionTableSectionRef}>
|
||||||
<div className="session-table-layout">
|
<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="session-table-sticky">
|
||||||
<div className="table-toolbar">
|
<div className="table-toolbar">
|
||||||
<div className="table-tabs" role="tablist" aria-label="会话类型">
|
<div className="table-tabs" role="tablist" aria-label="会话类型">
|
||||||
@@ -6269,6 +6374,19 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 && (
|
{showSessionLoadDetailModal && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user