fix: SelectModelPopup sticky header (#5795)

* fix: remove console logs

* refactor: use onScroll instead of onItemsRendered
This commit is contained in:
one 2025-05-09 16:43:22 +08:00 committed by GitHub
parent 472123ff8d
commit 65d01805d4

View File

@ -59,9 +59,11 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
const [pinnedModels, setPinnedModels] = useState<string[]>([]) const [pinnedModels, setPinnedModels] = useState<string[]>([])
const [_focusedItemKey, setFocusedItemKey] = useState<string>('') const [_focusedItemKey, setFocusedItemKey] = useState<string>('')
const focusedItemKey = useDeferredValue(_focusedItemKey) const focusedItemKey = useDeferredValue(_focusedItemKey)
const [currentStickyGroup, setCurrentStickyGroup] = useState<FlatListItem | null>(null) const [_stickyGroup, setStickyGroup] = useState<FlatListItem | null>(null)
const stickyGroup = useDeferredValue(_stickyGroup)
const firstGroupRef = useRef<FlatListItem | null>(null) const firstGroupRef = useRef<FlatListItem | null>(null)
const scrollTriggerRef = useRef<ScrollTrigger>('initial') const scrollTriggerRef = useRef<ScrollTrigger>('initial')
const lastScrollOffsetRef = useRef(0)
// 当前选中的模型ID // 当前选中的模型ID
const currentModelId = model ? getModelUniqId(model) : '' const currentModelId = model ? getModelUniqId(model) : ''
@ -220,6 +222,45 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
return items return items
}, [providers, getFilteredModels, pinnedModels, searchText, t, createModelItem]) }, [providers, getFilteredModels, pinnedModels, searchText, t, createModelItem])
// 基于滚动位置更新sticky分组标题
const updateStickyGroup = useCallback(
(scrollOffset?: number) => {
if (listItems.length === 0) {
setStickyGroup(null)
return
}
// 基于滚动位置计算当前可见的第一个项的索引
const estimatedIndex = Math.floor((scrollOffset ?? lastScrollOffsetRef.current) / ITEM_HEIGHT)
// 从该索引向前查找最近的分组标题
for (let i = estimatedIndex - 1; i >= 0; i--) {
if (i < listItems.length && listItems[i]?.type === 'group') {
setStickyGroup(listItems[i])
return
}
}
// 找不到则使用第一个分组标题
setStickyGroup(firstGroupRef.current ?? null)
},
[listItems]
)
// 在listItems变化时更新sticky group
useEffect(() => {
updateStickyGroup()
}, [listItems, updateStickyGroup])
// 处理列表滚动事件更新lastScrollOffset并更新sticky分组
const handleScroll = useCallback(
({ scrollOffset }) => {
lastScrollOffsetRef.current = scrollOffset
updateStickyGroup(scrollOffset)
},
[updateStickyGroup]
)
// 获取可选择的模型项(过滤掉分组标题) // 获取可选择的模型项(过滤掉分组标题)
const modelItems = useMemo(() => { const modelItems = useMemo(() => {
return listItems.filter((item) => item.type === 'model') return listItems.filter((item) => item.type === 'model')
@ -257,9 +298,6 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
const alignment = scrollTriggerRef.current === 'keyboard' ? 'auto' : 'center' const alignment = scrollTriggerRef.current === 'keyboard' ? 'auto' : 'center'
listRef.current?.scrollToItem(index, alignment) listRef.current?.scrollToItem(index, alignment)
console.log('focusedItemKey', focusedItemKey)
console.log('scrollToFocusedItem', index, alignment)
// 滚动后重置触发器 // 滚动后重置触发器
scrollTriggerRef.current = 'none' scrollTriggerRef.current = 'none'
}, [focusedItemKey, listItems]) }, [focusedItemKey, listItems])
@ -365,41 +403,19 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
if (!open) return if (!open) return
setTimeout(() => inputRef.current?.focus(), 0) setTimeout(() => inputRef.current?.focus(), 0)
scrollTriggerRef.current = 'initial' scrollTriggerRef.current = 'initial'
lastScrollOffsetRef.current = 0
}, [open]) }, [open])
// 初始化sticky分组标题
useEffect(() => {
if (firstGroupRef.current) {
setCurrentStickyGroup(firstGroupRef.current)
}
}, [listItems])
const handleItemsRendered = useCallback(
({ visibleStartIndex }: { visibleStartIndex: number; visibleStopIndex: number }) => {
// 从可见区域的起始位置向前查找最近的分组标题
for (let i = visibleStartIndex - 1; i >= 0; i--) {
if (listItems[i]?.type === 'group') {
setCurrentStickyGroup(listItems[i])
return
}
}
// 找不到则使用第一个分组标题
setCurrentStickyGroup(firstGroupRef.current ?? null)
},
[listItems]
)
const RowData = useMemo( const RowData = useMemo(
(): VirtualizedRowData => ({ (): VirtualizedRowData => ({
listItems, listItems,
focusedItemKey, focusedItemKey,
setFocusedItemKey, setFocusedItemKey,
currentStickyGroup, stickyGroup,
handleItemClick, handleItemClick,
togglePin togglePin
}), }),
[currentStickyGroup, focusedItemKey, handleItemClick, listItems, togglePin] [stickyGroup, focusedItemKey, handleItemClick, listItems, togglePin]
) )
const listHeight = useMemo(() => { const listHeight = useMemo(() => {
@ -456,7 +472,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
{listItems.length > 0 ? ( {listItems.length > 0 ? (
<ListContainer onMouseMove={() => setIsMouseOver(true)}> <ListContainer onMouseMove={() => setIsMouseOver(true)}>
{/* Sticky Group Banner它会替换第一个分组名称 */} {/* Sticky Group Banner它会替换第一个分组名称 */}
<StickyGroupBanner>{currentStickyGroup?.name}</StickyGroupBanner> <StickyGroupBanner>{stickyGroup?.name}</StickyGroupBanner>
<FixedSizeList <FixedSizeList
ref={listRef} ref={listRef}
height={listHeight} height={listHeight}
@ -466,7 +482,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
itemData={RowData} itemData={RowData}
itemKey={(index, data) => data.listItems[index].key} itemKey={(index, data) => data.listItems[index].key}
overscanCount={4} overscanCount={4}
onItemsRendered={handleItemsRendered} onScroll={handleScroll}
style={{ pointerEvents: isMouseOver ? 'auto' : 'none' }}> style={{ pointerEvents: isMouseOver ? 'auto' : 'none' }}>
{VirtualizedRow} {VirtualizedRow}
</FixedSizeList> </FixedSizeList>
@ -484,7 +500,7 @@ interface VirtualizedRowData {
listItems: FlatListItem[] listItems: FlatListItem[]
focusedItemKey: string focusedItemKey: string
setFocusedItemKey: (key: string) => void setFocusedItemKey: (key: string) => void
currentStickyGroup: FlatListItem | null stickyGroup: FlatListItem | null
handleItemClick: (item: FlatListItem) => void handleItemClick: (item: FlatListItem) => void
togglePin: (modelId: string) => void togglePin: (modelId: string) => void
} }
@ -494,7 +510,7 @@ interface VirtualizedRowData {
*/ */
const VirtualizedRow = React.memo( const VirtualizedRow = React.memo(
({ data, index, style }: { data: VirtualizedRowData; index: number; style: React.CSSProperties }) => { ({ data, index, style }: { data: VirtualizedRowData; index: number; style: React.CSSProperties }) => {
const { listItems, focusedItemKey, setFocusedItemKey, handleItemClick, togglePin, currentStickyGroup } = data const { listItems, focusedItemKey, setFocusedItemKey, handleItemClick, togglePin, stickyGroup } = data
const item = listItems[index] const item = listItems[index]
@ -505,7 +521,7 @@ const VirtualizedRow = React.memo(
return ( return (
<div style={style}> <div style={style}>
{item.type === 'group' ? ( {item.type === 'group' ? (
<GroupItem $isSticky={item.key === currentStickyGroup?.key}>{item.name}</GroupItem> <GroupItem $isSticky={item.key === stickyGroup?.key}>{item.name}</GroupItem>
) : ( ) : (
<ModelItem <ModelItem
className={classNames({ className={classNames({