123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
// ============================================================
|
||
// ModelSelector — 模型选择器(垂直单选列表,按类别分组)
|
||
// 使用 useModelList() Hook 获取数据,组件仅负责渲染
|
||
// ============================================================
|
||
|
||
import { useMemo } from 'react';
|
||
import { Menu, Spin, Empty, Tag, Typography, theme as antTheme } from 'antd';
|
||
import { AppstoreOutlined } from '@ant-design/icons';
|
||
|
||
import { useModelList, MODEL_CATEGORY_LABELS } from '@/hooks/use-api';
|
||
import { useHomeContext } from '@/contexts/home-context';
|
||
import type { ModelCategory } from '@/services/modules';
|
||
|
||
const { Text } = Typography;
|
||
|
||
// ---------- 组件 ----------
|
||
|
||
export function ModelSelector() {
|
||
const { token } = antTheme.useToken();
|
||
const { selectedModel, setSelectedModel } = useHomeContext();
|
||
const { data: models, loading, groupedModels } = useModelList();
|
||
|
||
// 构建 Menu items:分组标题 + 模型项
|
||
const menuItems = useMemo(() => {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const items: any[] = [];
|
||
|
||
groupedModels.forEach((groupModels, category) => {
|
||
const categoryLabel = MODEL_CATEGORY_LABELS[category as ModelCategory] || category;
|
||
items.push({
|
||
key: `group-${category}`,
|
||
label: (
|
||
<Text strong style={{ fontSize: 12, color: token.colorTextSecondary }}>
|
||
{categoryLabel} ({groupModels.length})
|
||
</Text>
|
||
),
|
||
type: 'group' as const,
|
||
children: groupModels.map((model) => ({
|
||
key: model.id,
|
||
label: (
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||
<Text
|
||
style={{
|
||
flex: 1,
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
fontSize: 13,
|
||
}}
|
||
title={model.name}
|
||
>
|
||
{model.name}
|
||
</Text>
|
||
{model.status !== 'active' && (
|
||
<Tag
|
||
color={model.status === 'inactive' ? 'default' : 'orange'}
|
||
style={{ fontSize: 10, lineHeight: '16px', padding: '0 4px' }}
|
||
>
|
||
{model.status === 'maintenance' ? '维护' : '停用'}
|
||
</Tag>
|
||
)}
|
||
</div>
|
||
),
|
||
})),
|
||
});
|
||
});
|
||
|
||
return items;
|
||
}, [groupedModels, token.colorTextSecondary]);
|
||
|
||
// 选中菜单项
|
||
const handleSelect = ({ key }: { key: string }) => {
|
||
const model = models?.find((m) => m.id === key);
|
||
if (model) {
|
||
setSelectedModel(model);
|
||
}
|
||
};
|
||
|
||
// 加载态
|
||
if (loading) {
|
||
return (
|
||
<div style={{ display: 'flex', justifyContent: 'center', padding: 24 }}>
|
||
<Spin size="small" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 空态
|
||
if (!models || models.length === 0) {
|
||
return <Empty description="暂无可用模型" image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||
}
|
||
|
||
// 选中 key
|
||
const selectedKeys = selectedModel ? [selectedModel.id] : [];
|
||
|
||
return (
|
||
<div style={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
|
||
<div
|
||
style={{
|
||
padding: '8px 12px',
|
||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
}}
|
||
>
|
||
<AppstoreOutlined style={{ color: token.colorPrimary, fontSize: 14 }} />
|
||
<Text strong style={{ fontSize: 13 }}>选择模型</Text>
|
||
</div>
|
||
<Menu
|
||
mode="inline"
|
||
selectedKeys={selectedKeys}
|
||
onSelect={handleSelect}
|
||
items={menuItems}
|
||
style={{
|
||
borderInlineEnd: 'none',
|
||
background: 'transparent',
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|