mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 19:30:04 +08:00
fix: SelectModelPopup sticky header (#5795)
* fix: remove console logs * refactor: use onScroll instead of onItemsRendered
This commit is contained in:
parent
472123ff8d
commit
65d01805d4
@ -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({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user