mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 06:49:02 +08:00
refactor: SelectModelPopup pinning (#5855)
* refactor: focus the hovered item when toggling a pinned model * refactor: focus the selected item after loading pinned models * refactor: update sticky group after loading pinned models * fix: rapidly update sticky group * refactor: defer lastscrolloffset * refactor: rename updateOnListChange to focusOnListChange for clarity * refactor: increaset overscan count * refactor: use startTransition instead of deferred value * refactor: add guard, clean up code * refactor: simplify cleanup logic * refactor: remove unnecessary dep on pinnedModels * fix: flicker on searching * refactor: simplify tag tooltips, prevent tooltips in SelectModelPopup
This commit is contained in:
parent
63b8ca4888
commit
ff570a58d5
@ -1,6 +1,6 @@
|
|||||||
import { CloseOutlined } from '@ant-design/icons'
|
import { CloseOutlined } from '@ant-design/icons'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { FC, memo } from 'react'
|
import { FC, memo, useMemo } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface CustomTagProps {
|
interface CustomTagProps {
|
||||||
@ -14,13 +14,22 @@ interface CustomTagProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, tooltip, closable = false, onClose }) => {
|
const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, tooltip, closable = false, onClose }) => {
|
||||||
return (
|
const tagContent = useMemo(
|
||||||
<Tooltip title={tooltip} placement="top">
|
() => (
|
||||||
<Tag $color={color} $size={size} $closable={closable}>
|
<Tag $color={color} $size={size} $closable={closable}>
|
||||||
{icon && icon} {children}
|
{icon && icon} {children}
|
||||||
{closable && <CloseIcon $size={size} $color={color} onClick={onClose} />}
|
{closable && <CloseIcon $size={size} $color={color} onClick={onClose} />}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
),
|
||||||
|
[children, closable, color, icon, onClose, size]
|
||||||
|
)
|
||||||
|
|
||||||
|
return tooltip ? (
|
||||||
|
<Tooltip title={tooltip} placement="top" mouseEnterDelay={0.3}>
|
||||||
|
{tagContent}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
tagContent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ interface ModelTagsProps {
|
|||||||
showToolsCalling?: boolean
|
showToolsCalling?: boolean
|
||||||
size?: number
|
size?: number
|
||||||
showLabel?: boolean
|
showLabel?: boolean
|
||||||
|
showTooltip?: boolean
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
|
|||||||
showToolsCalling = true,
|
showToolsCalling = true,
|
||||||
size = 12,
|
size = 12,
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
|
showTooltip = true,
|
||||||
style
|
style
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -73,7 +75,7 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
|
|||||||
size={size}
|
size={size}
|
||||||
color="#00b96b"
|
color="#00b96b"
|
||||||
icon={<EyeOutlined style={{ fontSize: size }} />}
|
icon={<EyeOutlined style={{ fontSize: size }} />}
|
||||||
tooltip={t('models.type.vision')}>
|
tooltip={showTooltip ? t('models.type.vision') : undefined}>
|
||||||
{shouldShowLabel ? t('models.type.vision') : ''}
|
{shouldShowLabel ? t('models.type.vision') : ''}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
)}
|
)}
|
||||||
@ -82,7 +84,7 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
|
|||||||
size={size}
|
size={size}
|
||||||
color="#1677ff"
|
color="#1677ff"
|
||||||
icon={<GlobalOutlined style={{ fontSize: size }} />}
|
icon={<GlobalOutlined style={{ fontSize: size }} />}
|
||||||
tooltip={t('models.type.websearch')}>
|
tooltip={showTooltip ? t('models.type.websearch') : undefined}>
|
||||||
{shouldShowLabel ? t('models.type.websearch') : ''}
|
{shouldShowLabel ? t('models.type.websearch') : ''}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
)}
|
)}
|
||||||
@ -91,7 +93,7 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
|
|||||||
size={size}
|
size={size}
|
||||||
color="#6372bd"
|
color="#6372bd"
|
||||||
icon={<i className="iconfont icon-thinking" />}
|
icon={<i className="iconfont icon-thinking" />}
|
||||||
tooltip={t('models.type.reasoning')}>
|
tooltip={showTooltip ? t('models.type.reasoning') : undefined}>
|
||||||
{shouldShowLabel ? t('models.type.reasoning') : ''}
|
{shouldShowLabel ? t('models.type.reasoning') : ''}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
)}
|
)}
|
||||||
@ -100,19 +102,13 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
|
|||||||
size={size}
|
size={size}
|
||||||
color="#f18737"
|
color="#f18737"
|
||||||
icon={<ToolOutlined style={{ fontSize: size }} />}
|
icon={<ToolOutlined style={{ fontSize: size }} />}
|
||||||
tooltip={t('models.type.function_calling')}>
|
tooltip={showTooltip ? t('models.type.function_calling') : undefined}>
|
||||||
{shouldShowLabel ? t('models.type.function_calling') : ''}
|
{shouldShowLabel ? t('models.type.function_calling') : ''}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
)}
|
)}
|
||||||
{isEmbeddingModel(model) && (
|
{isEmbeddingModel(model) && <CustomTag size={size} color="#FFA500" icon={t('models.type.embedding')} />}
|
||||||
<CustomTag size={size} color="#FFA500" icon={t('models.type.embedding')} tooltip={t('models.type.embedding')} />
|
{showFree && isFreeModel(model) && <CustomTag size={size} color="#7cb305" icon={t('models.type.free')} />}
|
||||||
)}
|
{isRerankModel(model) && <CustomTag size={size} color="#6495ED" icon={t('models.type.rerank')} />}
|
||||||
{showFree && isFreeModel(model) && (
|
|
||||||
<CustomTag size={size} color="#7cb305" icon={t('models.type.free')} tooltip={t('models.type.free')} />
|
|
||||||
)}
|
|
||||||
{isRerankModel(model) && (
|
|
||||||
<CustomTag size={size} color="#6495ED" icon={t('models.type.rerank')} tooltip={t('models.type.rerank')} />
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,8 @@ export function useScrollState() {
|
|||||||
focusPage: (modelItems: FlatListItem[], currentIndex: number, step: number) =>
|
focusPage: (modelItems: FlatListItem[], currentIndex: number, step: number) =>
|
||||||
dispatch({ type: 'FOCUS_PAGE', payload: { modelItems, currentIndex, step } }),
|
dispatch({ type: 'FOCUS_PAGE', payload: { modelItems, currentIndex, step } }),
|
||||||
searchChanged: (searchText: string) => dispatch({ type: 'SEARCH_CHANGED', payload: { searchText } }),
|
searchChanged: (searchText: string) => dispatch({ type: 'SEARCH_CHANGED', payload: { searchText } }),
|
||||||
updateOnListChange: (modelItems: FlatListItem[]) =>
|
focusOnListChange: (modelItems: FlatListItem[]) =>
|
||||||
dispatch({ type: 'UPDATE_ON_LIST_CHANGE', payload: { modelItems } }),
|
dispatch({ type: 'FOCUS_ON_LIST_CHANGE', payload: { modelItems } })
|
||||||
initScroll: () => dispatch({ type: 'INIT_SCROLL' })
|
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,7 +11,16 @@ import { classNames } from '@renderer/utils/style'
|
|||||||
import { Avatar, Divider, Empty, Input, InputRef, Modal } from 'antd'
|
import { Avatar, Divider, Empty, Input, InputRef, Modal } from 'antd'
|
||||||
import { first, sortBy } from 'lodash'
|
import { first, sortBy } from 'lodash'
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
import {
|
||||||
|
startTransition,
|
||||||
|
useCallback,
|
||||||
|
useDeferredValue,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FixedSizeList } from 'react-window'
|
import { FixedSizeList } from 'react-window'
|
||||||
@ -34,7 +43,7 @@ interface Props extends PopupParams {
|
|||||||
const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const { pinnedModels, togglePinnedModel, loading: loadingPinnedModels } = usePinnedModels()
|
const { pinnedModels, togglePinnedModel, loading } = usePinnedModels()
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const inputRef = useRef<InputRef>(null)
|
const inputRef = useRef<InputRef>(null)
|
||||||
const listRef = useRef<FixedSizeList>(null)
|
const listRef = useRef<FixedSizeList>(null)
|
||||||
@ -49,29 +58,40 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
focusedItemKey,
|
focusedItemKey,
|
||||||
scrollTrigger,
|
scrollTrigger,
|
||||||
lastScrollOffset,
|
lastScrollOffset,
|
||||||
stickyGroup: _stickyGroup,
|
stickyGroup,
|
||||||
isMouseOver,
|
isMouseOver,
|
||||||
setFocusedItemKey,
|
setFocusedItemKey: _setFocusedItemKey,
|
||||||
setScrollTrigger,
|
setScrollTrigger,
|
||||||
setLastScrollOffset,
|
setLastScrollOffset: _setLastScrollOffset,
|
||||||
setStickyGroup,
|
setStickyGroup: _setStickyGroup,
|
||||||
setIsMouseOver,
|
setIsMouseOver,
|
||||||
focusNextItem,
|
focusNextItem,
|
||||||
focusPage,
|
focusPage,
|
||||||
searchChanged,
|
searchChanged,
|
||||||
updateOnListChange,
|
focusOnListChange
|
||||||
initScroll
|
|
||||||
} = useScrollState()
|
} = useScrollState()
|
||||||
|
|
||||||
const stickyGroup = useDeferredValue(_stickyGroup)
|
|
||||||
const firstGroupRef = useRef<FlatListItem | null>(null)
|
const firstGroupRef = useRef<FlatListItem | null>(null)
|
||||||
|
|
||||||
const togglePin = useCallback(
|
const setFocusedItemKey = useCallback(
|
||||||
async (modelId: string) => {
|
(key: string) => {
|
||||||
await togglePinnedModel(modelId)
|
startTransition(() => _setFocusedItemKey(key))
|
||||||
setScrollTrigger('none') // pin操作不触发滚动
|
|
||||||
},
|
},
|
||||||
[togglePinnedModel, setScrollTrigger]
|
[_setFocusedItemKey]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setLastScrollOffset = useCallback(
|
||||||
|
(offset: number) => {
|
||||||
|
startTransition(() => _setLastScrollOffset(offset))
|
||||||
|
},
|
||||||
|
[_setLastScrollOffset]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setStickyGroup = useCallback(
|
||||||
|
(group: FlatListItem | null) => {
|
||||||
|
startTransition(() => _setStickyGroup(group))
|
||||||
|
},
|
||||||
|
[_setStickyGroup]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 根据输入的文本筛选模型
|
// 根据输入的文本筛选模型
|
||||||
@ -89,14 +109,11 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
const lowerFullName = fullName.toLowerCase()
|
const lowerFullName = fullName.toLowerCase()
|
||||||
return keywords.every((keyword) => lowerFullName.includes(keyword))
|
return keywords.every((keyword) => lowerFullName.includes(keyword))
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
// 如果不是搜索状态,过滤掉已固定的模型
|
|
||||||
models = models.filter((m) => !pinnedModels.includes(getModelUniqId(m)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortBy(models, ['group', 'name'])
|
return sortBy(models, ['group', 'name'])
|
||||||
},
|
},
|
||||||
[searchText, t, pinnedModels]
|
[searchText, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 创建模型列表项
|
// 创建模型列表项
|
||||||
@ -116,7 +133,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
),
|
),
|
||||||
tags: (
|
tags: (
|
||||||
<TagsContainer>
|
<TagsContainer>
|
||||||
<ModelTagsWithLabel model={model} size={11} showLabel={false} />
|
<ModelTagsWithLabel model={model} size={11} showLabel={false} showTooltip={false} />
|
||||||
</TagsContainer>
|
</TagsContainer>
|
||||||
),
|
),
|
||||||
icon: (
|
icon: (
|
||||||
@ -137,7 +154,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
const items: FlatListItem[] = []
|
const items: FlatListItem[] = []
|
||||||
|
|
||||||
// 添加置顶模型分组(仅在无搜索文本时)
|
// 添加置顶模型分组(仅在无搜索文本时)
|
||||||
if (pinnedModels.length > 0 && searchText.length === 0) {
|
if (searchText.length === 0 && pinnedModels.length > 0) {
|
||||||
const pinnedItems = providers.flatMap((p) =>
|
const pinnedItems = providers.flatMap((p) =>
|
||||||
p.models.filter((m) => pinnedModels.includes(getModelUniqId(m))).map((m) => createModelItem(m, p, true))
|
p.models.filter((m) => pinnedModels.includes(getModelUniqId(m))).map((m) => createModelItem(m, p, true))
|
||||||
)
|
)
|
||||||
@ -158,7 +175,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
// 添加常规模型分组
|
// 添加常规模型分组
|
||||||
providers.forEach((p) => {
|
providers.forEach((p) => {
|
||||||
const filteredModels = getFilteredModels(p).filter(
|
const filteredModels = getFilteredModels(p).filter(
|
||||||
(m) => !pinnedModels.includes(getModelUniqId(m)) || searchText.length > 0
|
(m) => searchText.length > 0 || !pinnedModels.includes(getModelUniqId(m))
|
||||||
)
|
)
|
||||||
|
|
||||||
if (filteredModels.length === 0) return
|
if (filteredModels.length === 0) return
|
||||||
@ -198,48 +215,53 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
const updateStickyGroup = useCallback(
|
const updateStickyGroup = useCallback(
|
||||||
(scrollOffset?: number) => {
|
(scrollOffset?: number) => {
|
||||||
if (listItems.length === 0) {
|
if (listItems.length === 0) {
|
||||||
setStickyGroup(null)
|
stickyGroup && setStickyGroup(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let newStickyGroup: FlatListItem | null = null
|
||||||
|
|
||||||
// 基于滚动位置计算当前可见的第一个项的索引
|
// 基于滚动位置计算当前可见的第一个项的索引
|
||||||
const estimatedIndex = Math.floor((scrollOffset ?? lastScrollOffset) / ITEM_HEIGHT)
|
const estimatedIndex = Math.floor((scrollOffset ?? lastScrollOffset) / ITEM_HEIGHT)
|
||||||
|
|
||||||
// 从该索引向前查找最近的分组标题
|
// 从该索引向前查找最近的分组标题
|
||||||
for (let i = estimatedIndex - 1; i >= 0; i--) {
|
for (let i = estimatedIndex - 1; i >= 0; i--) {
|
||||||
if (i < listItems.length && listItems[i]?.type === 'group') {
|
if (i < listItems.length && listItems[i]?.type === 'group') {
|
||||||
setStickyGroup(listItems[i])
|
newStickyGroup = listItems[i]
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找不到则使用第一个分组标题
|
// 找不到则使用第一个分组标题
|
||||||
setStickyGroup(firstGroupRef.current)
|
if (!newStickyGroup) newStickyGroup = firstGroupRef.current
|
||||||
},
|
|
||||||
[listItems, lastScrollOffset, setStickyGroup]
|
|
||||||
)
|
|
||||||
|
|
||||||
// 在listItems变化时更新sticky group
|
if (stickyGroup?.key !== newStickyGroup?.key) {
|
||||||
useEffect(() => {
|
setStickyGroup(newStickyGroup)
|
||||||
updateStickyGroup()
|
}
|
||||||
}, [listItems, updateStickyGroup])
|
},
|
||||||
|
[listItems, lastScrollOffset, setStickyGroup, stickyGroup]
|
||||||
|
)
|
||||||
|
|
||||||
// 处理列表滚动事件,更新lastScrollOffset并更新sticky分组
|
// 处理列表滚动事件,更新lastScrollOffset并更新sticky分组
|
||||||
const handleScroll = useCallback(
|
const handleScroll = useCallback(
|
||||||
({ scrollOffset }) => {
|
({ scrollOffset }) => {
|
||||||
setLastScrollOffset(scrollOffset)
|
setLastScrollOffset(scrollOffset)
|
||||||
updateStickyGroup(scrollOffset)
|
|
||||||
},
|
},
|
||||||
[updateStickyGroup, setLastScrollOffset]
|
[setLastScrollOffset]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 在列表项更新时,更新焦点项
|
// 列表项更新时,更新焦点
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateOnListChange(modelItems)
|
if (!loading) focusOnListChange(modelItems)
|
||||||
}, [modelItems, updateOnListChange])
|
}, [modelItems, focusOnListChange, loading])
|
||||||
|
|
||||||
|
// 列表项更新时,更新sticky分组
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) updateStickyGroup()
|
||||||
|
}, [modelItems, updateStickyGroup, loading])
|
||||||
|
|
||||||
// 滚动到聚焦项
|
// 滚动到聚焦项
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (scrollTrigger === 'none' || !focusedItemKey) return
|
if (scrollTrigger === 'none' || !focusedItemKey) return
|
||||||
|
|
||||||
const index = listItems.findIndex((item) => item.key === focusedItemKey)
|
const index = listItems.findIndex((item) => item.key === focusedItemKey)
|
||||||
@ -302,23 +324,12 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
break
|
break
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setScrollTrigger('none')
|
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
resolve(undefined)
|
resolve(undefined)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[focusedItemKey, modelItems, handleItemClick, open, resolve, setIsMouseOver, focusNextItem, focusPage]
|
||||||
focusedItemKey,
|
|
||||||
modelItems,
|
|
||||||
handleItemClick,
|
|
||||||
open,
|
|
||||||
resolve,
|
|
||||||
setIsMouseOver,
|
|
||||||
focusNextItem,
|
|
||||||
focusPage,
|
|
||||||
setScrollTrigger
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -327,11 +338,10 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
}, [handleKeyDown])
|
}, [handleKeyDown])
|
||||||
|
|
||||||
const onCancel = useCallback(() => {
|
const onCancel = useCallback(() => {
|
||||||
setScrollTrigger('initial')
|
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}, [setScrollTrigger])
|
}, [])
|
||||||
|
|
||||||
const onClose = useCallback(async () => {
|
const onAfterClose = useCallback(async () => {
|
||||||
setScrollTrigger('initial')
|
setScrollTrigger('initial')
|
||||||
resolve(undefined)
|
resolve(undefined)
|
||||||
SelectModelPopup.hide()
|
SelectModelPopup.hide()
|
||||||
@ -339,10 +349,16 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
|
|
||||||
// 初始化焦点和滚动位置
|
// 初始化焦点和滚动位置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || loadingPinnedModels) return
|
if (!open) return
|
||||||
setTimeout(() => inputRef.current?.focus(), 0)
|
setTimeout(() => inputRef.current?.focus(), 0)
|
||||||
initScroll()
|
}, [open])
|
||||||
}, [open, initScroll, loadingPinnedModels])
|
|
||||||
|
const togglePin = useCallback(
|
||||||
|
async (modelId: string) => {
|
||||||
|
await togglePinnedModel(modelId)
|
||||||
|
},
|
||||||
|
[togglePinnedModel]
|
||||||
|
)
|
||||||
|
|
||||||
const RowData = useMemo(
|
const RowData = useMemo(
|
||||||
(): VirtualizedRowData => ({
|
(): VirtualizedRowData => ({
|
||||||
@ -365,7 +381,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
centered
|
centered
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
afterClose={onClose}
|
afterClose={onAfterClose}
|
||||||
width={600}
|
width={600}
|
||||||
transitionName="animation-move-down"
|
transitionName="animation-move-down"
|
||||||
styles={{
|
styles={{
|
||||||
@ -408,7 +424,7 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
|
|||||||
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
||||||
|
|
||||||
{listItems.length > 0 ? (
|
{listItems.length > 0 ? (
|
||||||
<ListContainer onMouseMove={() => !isMouseOver && setIsMouseOver(true)}>
|
<ListContainer onMouseMove={() => !isMouseOver && startTransition(() => setIsMouseOver(true))}>
|
||||||
{/* Sticky Group Banner,它会替换第一个分组名称 */}
|
{/* Sticky Group Banner,它会替换第一个分组名称 */}
|
||||||
<StickyGroupBanner>{stickyGroup?.name}</StickyGroupBanner>
|
<StickyGroupBanner>{stickyGroup?.name}</StickyGroupBanner>
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
@ -456,6 +472,8 @@ const VirtualizedRow = React.memo(
|
|||||||
return <div style={style} />
|
return <div style={style} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFocused = item.key === focusedItemKey
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
{item.type === 'group' ? (
|
{item.type === 'group' ? (
|
||||||
@ -463,11 +481,11 @@ const VirtualizedRow = React.memo(
|
|||||||
) : (
|
) : (
|
||||||
<ModelItem
|
<ModelItem
|
||||||
className={classNames({
|
className={classNames({
|
||||||
focused: item.key === focusedItemKey,
|
focused: isFocused,
|
||||||
selected: item.isSelected
|
selected: item.isSelected
|
||||||
})}
|
})}
|
||||||
onClick={() => handleItemClick(item)}
|
onClick={() => handleItemClick(item)}
|
||||||
onMouseEnter={() => setFocusedItemKey(item.key)}>
|
onMouseOver={() => !isFocused && setFocusedItemKey(item.key)}>
|
||||||
<ModelItemLeft>
|
<ModelItemLeft>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
{item.name}
|
{item.name}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export const scrollReducer = (state: ScrollState, action: ScrollAction): ScrollS
|
|||||||
scrollTrigger: action.payload.searchText ? 'search' : 'initial'
|
scrollTrigger: action.payload.searchText ? 'search' : 'initial'
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'UPDATE_ON_LIST_CHANGE': {
|
case 'FOCUS_ON_LIST_CHANGE': {
|
||||||
const { modelItems } = action.payload
|
const { modelItems } = action.payload
|
||||||
|
|
||||||
// 在列表变化时尝试聚焦一个模型:
|
// 在列表变化时尝试聚焦一个模型:
|
||||||
@ -96,13 +96,6 @@ export const scrollReducer = (state: ScrollState, action: ScrollAction): ScrollS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'INIT_SCROLL':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
scrollTrigger: 'initial',
|
|
||||||
lastScrollOffset: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,5 +38,4 @@ export type ScrollAction =
|
|||||||
| { type: 'FOCUS_NEXT_ITEM'; payload: { modelItems: FlatListItem[]; step: number } }
|
| { type: 'FOCUS_NEXT_ITEM'; payload: { modelItems: FlatListItem[]; step: number } }
|
||||||
| { type: 'FOCUS_PAGE'; payload: { modelItems: FlatListItem[]; currentIndex: number; step: number } }
|
| { type: 'FOCUS_PAGE'; payload: { modelItems: FlatListItem[]; currentIndex: number; step: number } }
|
||||||
| { type: 'SEARCH_CHANGED'; payload: { searchText: string } }
|
| { type: 'SEARCH_CHANGED'; payload: { searchText: string } }
|
||||||
| { type: 'UPDATE_ON_LIST_CHANGE'; payload: { modelItems: FlatListItem[] } }
|
| { type: 'FOCUS_ON_LIST_CHANGE'; payload: { modelItems: FlatListItem[] } }
|
||||||
| { type: 'INIT_SCROLL'; payload?: void }
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user