refactor(ModelList): 优化模型列表的渲染和样式

- 使用startTransition优化折叠/展开性能
- 重构数据结构,将单个模型渲染改为批量渲染
- 改进组头和模型项的样式和布局
This commit is contained in:
icarus 2025-08-01 18:36:33 +08:00
parent f8c471105e
commit e18286c70e

View File

@ -10,7 +10,7 @@ import { Model, Provider } from '@renderer/types'
import { Button, Flex, Tooltip } from 'antd' import { Button, Flex, Tooltip } from 'antd'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { ChevronRight } from 'lucide-react' import { ChevronRight } from 'lucide-react'
import React, { memo, useCallback, useMemo, useState } from 'react' import React, { memo, startTransition, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -23,12 +23,12 @@ interface GroupRowData {
models: Model[] models: Model[]
} }
interface ModelRowData { interface ModelsRowData {
type: 'model' type: 'models'
model: Model models: Model[]
} }
type RowData = GroupRowData | ModelRowData type RowData = GroupRowData | ModelsRowData
interface ManageModelsListProps { interface ManageModelsListProps {
modelGroups: Record<string, Model[]> modelGroups: Record<string, Model[]>
@ -42,14 +42,16 @@ const ManageModelsList: React.FC<ManageModelsListProps> = ({ modelGroups, provid
const [collapsedGroups, setCollapsedGroups] = useState(new Set<string>()) const [collapsedGroups, setCollapsedGroups] = useState(new Set<string>())
const handleGroupToggle = useCallback((groupName: string) => { const handleGroupToggle = useCallback((groupName: string) => {
setCollapsedGroups((prev) => { startTransition(() => {
const newSet = new Set(prev) setCollapsedGroups((prev) => {
if (newSet.has(groupName)) { const newSet = new Set(prev)
newSet.delete(groupName) // 如果已折叠,则展开 if (newSet.has(groupName)) {
} else { newSet.delete(groupName) // 如果已折叠,则展开
newSet.add(groupName) // 如果已展开,则折叠 } else {
} newSet.add(groupName) // 如果已展开,则折叠
return newSet }
return newSet
})
}) })
}, []) }, [])
@ -62,9 +64,7 @@ const ManageModelsList: React.FC<ManageModelsListProps> = ({ modelGroups, provid
// 只添加非空组 // 只添加非空组
rows.push({ type: 'group', groupName, models }) rows.push({ type: 'group', groupName, models })
if (!collapsedGroups.has(groupName)) { if (!collapsedGroups.has(groupName)) {
models.forEach((model) => { rows.push({ type: 'models', models })
rows.push({ type: 'model', model })
})
} }
} }
}) })
@ -132,37 +132,44 @@ const ManageModelsList: React.FC<ManageModelsListProps> = ({ modelGroups, provid
overscan={5} overscan={5}
scrollerStyle={{ scrollerStyle={{
paddingRight: '10px' paddingRight: '10px'
}}
itemContainerStyle={{
paddingBottom: '8px'
}}> }}>
{(row) => { {(row) => {
if (row.type === 'group') { if (row.type === 'group') {
const isCollapsed = collapsedGroups.has(row.groupName) const isCollapsed = collapsedGroups.has(row.groupName)
return ( return (
<GroupHeader <GroupHeaderContainer isCollapsed={isCollapsed}>
style={{ background: 'var(--color-background)' }} <GroupHeader isCollapsed={isCollapsed} onClick={() => handleGroupToggle(row.groupName)}>
onClick={() => handleGroupToggle(row.groupName)}> <Flex align="center" gap={10} style={{ flex: 1 }}>
<Flex align="center" gap={10} style={{ flex: 1 }}> <ChevronRight
<ChevronRight size={16}
size={16} color="var(--color-text-3)"
color="var(--color-text-3)" strokeWidth={1.5}
strokeWidth={1.5} style={{ transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)' }}
style={{ transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)' }} />
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>{row.groupName}</span>
<CustomTag color="#02B96B" size={10}>
{row.models.length}
</CustomTag>
</Flex>
{renderGroupTools(row.models)}
</GroupHeader>
</GroupHeaderContainer>
)
} else {
return (
<ModelsContainer>
{row.models.map((model) => (
<ModelListItem
key={model.id}
model={model}
provider={provider}
onAddModel={onAddModel}
onRemoveModel={onRemoveModel}
/> />
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>{row.groupName}</span> ))}
<CustomTag color="#02B96B" size={10}> </ModelsContainer>
{row.models.length}
</CustomTag>
</Flex>
{renderGroupTools(row.models)}
</GroupHeader>
) )
} }
return (
<ModelListItem model={row.model} provider={provider} onAddModel={onAddModel} onRemoveModel={onRemoveModel} />
)
}} }}
</DynamicVirtualList> </DynamicVirtualList>
) )
@ -180,35 +187,65 @@ const ModelListItem: React.FC<ModelListItemProps> = memo(({ model, provider, onA
const isAdded = useMemo(() => isModelInProvider(provider, model.id), [provider, model.id]) const isAdded = useMemo(() => isModelInProvider(provider, model.id), [provider, model.id])
return ( return (
<FileItem <ModelItem>
style={{ <FileItem
backgroundColor: isAdded ? 'rgba(0, 126, 0, 0.06)' : '', style={{
border: 'none', backgroundColor: isAdded ? 'rgba(0, 126, 0, 0.06)' : '',
boxShadow: 'none' border: 'none',
}} boxShadow: 'none'
fileInfo={{ }}
icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>, fileInfo={{
name: <ModelIdWithTags model={model} />, icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>,
extra: model.description && <ExpandableText text={model.description} />, name: <ModelIdWithTags model={model} />,
ext: '.model', extra: model.description && <ExpandableText text={model.description} />,
actions: isAdded ? ( ext: '.model',
<Button type="text" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} /> actions: isAdded ? (
) : ( <Button type="text" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} />
<Button type="text" onClick={() => onAddModel(model)} icon={<PlusOutlined />} /> ) : (
) <Button type="text" onClick={() => onAddModel(model)} icon={<PlusOutlined />} />
}} )
/> }}
/>
</ModelItem>
) )
}) })
const GroupHeader = styled.div` const GroupHeaderContainer = styled.div<{ isCollapsed: boolean }>`
background-color: ${(props) => (props.isCollapsed ? 'transparent' : 'var(--color-background)')};
padding-bottom: ${(props) => (props.isCollapsed ? '8px' : '0')};
`
const GroupHeader = styled.div<{ isCollapsed: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
background: var(--color-background-mute);
border-radius: ${(props) => (props.isCollapsed ? '1em' : '1em 1em 0 0')};
justify-content: space-between; justify-content: space-between;
padding: 0 8px; padding: 0 8px;
min-height: 50px; min-height: 50px;
color: var(--color-text); color: var(--color-text);
cursor: pointer; cursor: pointer;
border: 1px solid var(--color-border);
border-bottom: none;
`
const ModelsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 1em 1em;
margin-bottom: 8px;
`
const ModelItem = styled.div`
flex-direction: row;
position: relative;
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
cursor: pointer;
` `
export default memo(ManageModelsList) export default memo(ManageModelsList)