// ============================================================ // ModelSelector — 模型选择器(Tabs 标签页分组 + 平铺列表) // 使用 useModelList() Hook 获取数据,组件仅负责渲染 // ============================================================ import React, { useState, useMemo, useCallback } from 'react'; import { Tabs, Spin, Empty, Tag, Typography, Button, Result, message, theme as antTheme } from 'antd'; import { AppstoreOutlined } from '@ant-design/icons'; import { useModelList } from '@/hooks/use-api'; import { useHomeContext } from '@/pages/home/HomeContent'; import { resolveCategoryLabel, MODEL_CATEGORIES } from '@/services/modules'; import type { ModelsListResponseBody, ModelCategoryStringEnum } from '@/services/modules'; const { Text } = Typography; /** "全部模型" 的虚拟 tab key */ const ALL_TAB_KEY = '__all__'; // ---------- 单模型列表项(平铺) ---------- /** 单个模型行:hover 高亮 + 选中态 + 状态标签 + 禁用态处理 */ interface ModelItemProps { model: ModelsListResponseBody; isSelected: boolean; onSelect: () => void; } function ModelItem({ model, isSelected, onSelect }: ModelItemProps) { const { token } = antTheme.useToken(); const [isHovered, setIsHovered] = useState(false); // 维护中的模型不可选 const isDisabled = model.status === 'maintenance'; const handleClick = useCallback(() => { if (isDisabled) { message.warning(`${model.name} 正在维护中,暂不可用`); return; } onSelect(); }, [isDisabled, model.name, onSelect]); // 背景色 / 边框色 优先级:选中 > hover > 默认 let bgColor = 'transparent'; let borderColor = 'transparent'; if (isSelected) { bgColor = token.colorPrimaryBg; borderColor = token.colorPrimaryBorder; } else if (isHovered && !isDisabled) { bgColor = token.colorFillSecondary; borderColor = token.colorBorderSecondary; } return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ padding: '6px 12px', cursor: isDisabled ? 'not-allowed' : 'pointer', borderRadius: 6, background: bgColor, border: `1px solid ${borderColor}`, opacity: isDisabled ? 0.5 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, transition: 'background 0.15s linear, border-color 0.15s linear, opacity 0.15s linear', }} > {model.name} {model.status !== 'active' && ( {model.status === 'maintenance' ? '维护' : '停用'} )}
); } // ---------- 主组件 ---------- export function ModelSelector() { const { token } = antTheme.useToken(); const { selectedModel, setSelectedModel } = useHomeContext(); const { data: models, loading, error, groupedModels, refetch } = useModelList(); const [activeTab, setActiveTab] = useState(ALL_TAB_KEY); // 当前 Tab 对应的模型列表 const filteredModels:ModelsListResponseBody[] = useMemo(() => { if (activeTab === ALL_TAB_KEY) return models ?? []; return groupedModels.get(activeTab) ?? []; }, [activeTab, models, groupedModels]); // 构建 Tab 项(全部 + 各分类,顺序由 MODEL_CATEGORIES 控制) const tabItems: { key: string; label: React.ReactNode }[] = useMemo(() => { const items: { key: string; label: React.ReactNode }[] = [ { key: ALL_TAB_KEY, label: `全部 (${models?.length ?? 0})`, }, ]; // 按 MODEL_CATEGORIES 定义的顺序遍历,确保 Tab 顺序可控 const seenCategories = new Set(); MODEL_CATEGORIES.forEach((cat: ModelCategoryStringEnum) => { const groupModels = groupedModels.get(cat); if (groupModels && groupModels.length > 0) { seenCategories.add(cat); items.push({ key: cat, label: `${resolveCategoryLabel(cat)} (${groupModels.length})`, }); } }); // 兜底:API 返回了 MODEL_CATEGORIES 中未注册的新分类,追加到末尾 groupedModels.forEach((groupModels, category) => { if (!seenCategories.has(category) && groupModels.length > 0) { items.push({ key: category, label: `${resolveCategoryLabel(category)} (${groupModels.length})`, }); } }); return items; }, [models, groupedModels]); // ---------- 加载态 ---------- if (loading) { return (
); } // ---------- 错误态 ---------- if (error) { return ( 重试 } /> ); } // ---------- 空态 ---------- if (!models || models.length === 0) { return ; } // ---------- 正常渲染 ---------- return (
{/* 标题栏 */}
选择模型
{/* 分类 Tabs */} {/* 模型列表 */}
{filteredModels.length === 0 ? ( ) : (
{filteredModels.map((model) => ( setSelectedModel(model)} /> ))}
)}
); }