import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { PageId } from '../components/Sidebar'; import Pagination from '../components/Pagination'; import { Model } from '../types/models'; import { modelApi } from '../services/modelApi'; import { Search, Zap, Flame, Loader2 } from '../icons/commonIcons'; import ModelCard from '../components/ModelCard'; import LocalModelList from '../components/LocalModelList'; interface ModelSquareProps {} const ModelSquare: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); const modelSource = useMemo(() => searchParams.get('source') || 'cloud', [searchParams]); const [activeCategory, setActiveCategory] = useState('全部'); const activeGroupName = useMemo(() => searchParams.get('group') || '全部', [searchParams]); const setActiveGroupName = (name: string) => { setSearchParams(prev => { const newParams = new URLSearchParams(prev); if (name === '全部') { newParams.delete('group'); } else { newParams.set('group', name); } newParams.delete('page'); return newParams; }); }; const [groupNames, setGroupNames] = useState([]); const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchKeyword, setSearchKeyword] = useState(''); // 从 URL 读取页码,如果没有则默认为 1 const currentPage = useMemo(() => { const pageParam = searchParams.get('page'); const page = pageParam ? parseInt(pageParam, 10) : 1; return isNaN(page) || page < 1 ? 1 : page; }, [searchParams]); const [pagination, setPagination] = useState({ page: currentPage, pageSize: 21, total: 0, totalPages: 0 }); const [showFilterPanel, setShowFilterPanel] = useState(false); const [selectedCompanies, setSelectedCompanies] = useState([]); const [selectedModalTypes, setSelectedModalTypes] = useState([]); const [isNewFilter, setIsNewFilter] = useState(undefined); const [isFeaturedFilter, setIsFeaturedFilter] = useState(undefined); const [isApiEnabledFilter, setIsApiEnabledFilter] = useState(undefined); const [sortBy, setSortBy] = useState('createdAt'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const filterPanelRef = useRef(null); const filterButtonRef = useRef(null); // 简单内存缓存,用于切换时快速展示已加载的数据(key 为 source|category|keyword) const modelsCacheRef = useRef>({}); const fetchTimerRef = useRef(null); // 供应商列表(通义合并为一个,不做细分) const allSuppliers = ['通义', 'DeepSeek', 'Moonshot', '智谱AI', 'Black Forest Labs', 'MiniMax', 'Stability AI']; // 标签分组定义(与数据库实际标签值对应) const modalTypeGroups = { '版本': ['最新版本'], '翻译': ['翻译'], '视觉': ['视频生成'] }; // 模型分类列表(与后端 ModelCategory 枚举一致) const modelCategories = ['全部', '语言模型', '多模态模型', 'TTS模型', 'STT模型', '生图模型', '生视频模型', '图像编辑', 'Embedding', 'Rerank']; // 不再需要加载百炼模型数据,直接使用API const getCategoryTagStyle = (category: string) => { switch (category) { case '语言模型': return 'bg-blue-50 text-blue-600 border-blue-200'; case '多模态模型': return 'bg-indigo-50 text-indigo-600 border-indigo-200'; case 'TTS模型': return 'bg-emerald-50 text-emerald-600 border-emerald-200'; case 'STT模型': return 'bg-teal-50 text-teal-600 border-teal-200'; case '生图模型': return 'bg-purple-50 text-purple-600 border-purple-200'; case '生视频模型': return 'bg-orange-50 text-orange-600 border-orange-200'; case '图像编辑': return 'bg-pink-50 text-pink-600 border-pink-200'; case 'Embedding': return 'bg-cyan-50 text-cyan-600 border-cyan-200'; case 'Rerank': return 'bg-amber-50 text-amber-600 border-amber-200'; default: return 'bg-gray-50 text-gray-500 border-gray-100/50'; } }; // 获取模型列表(从API获取) const fetchModels = useCallback(async () => { try { setError(null); // 防抖:清理已有定时器,延迟实际请求以合并快速的切换/输入 if (fetchTimerRef.current) { window.clearTimeout(fetchTimerRef.current); } // 计算缓存键 const categoryParam = modelSource === 'local' ? (activeCategory !== '全部' ? activeCategory : '多模态模型') : (activeCategory !== '全部' ? activeCategory : undefined); const groupNameParam = modelSource === 'cloud' && activeGroupName !== '全部' ? activeGroupName : undefined; const cacheKey = `${modelSource}|${categoryParam || 'all'}|${groupNameParam || 'all'}|${searchKeyword || ''}`; // 如果有缓存,先使用缓存快速展示(不覆盖后续刷新结果) const cached = modelsCacheRef.current[cacheKey]; if (cached && cached.length > 0) { setModels(cached); setPagination(prev => ({ ...prev, total: cached.length, totalPages: 1, page: 1 })); setLoading(false); } else { // 如果没有缓存且是本地且期望显示开源模型,立即展示占位(快速反馈) if (modelSource === 'local') { setModels([]); setLoading(true); } else { setLoading(true); } } // 延迟实际网络请求,合并短时间内的多次变更 fetchTimerRef.current = window.setTimeout(async () => { // 处理供应商筛选:将显示名称映射到实际的供应商名称 let supplierFilter: string | undefined; if (selectedCompanies.length > 0) { const supplierMap: Record = { '通义': 'Qwen', 'DeepSeek': 'DeepSeek', 'Moonshot': 'Moonshot', '智谱AI': '智谱AI', 'Black Forest Labs': 'Black Forest Labs', 'MiniMax': 'MiniMax', 'Stability AI': 'Stability AI' }; supplierFilter = supplierMap[selectedCompanies[0]] || selectedCompanies[0]; } // 处理标签筛选:后端 filter_tag 参数支持在 tag1 或 tag2 中精确匹配 let tagFilter: string | undefined; if (selectedModalTypes.length > 0) { tagFilter = selectedModalTypes[0]; } const response = await modelApi.getModels({ keyword: modelSource === 'local' ? '通义千问3开源模型' : (searchKeyword || undefined), page: currentPage, pageSize: 21, category: categoryParam, supplier: modelSource === 'local' ? undefined : supplierFilter, group_name: groupNameParam, filter_tag: modelSource === 'local' ? undefined : tagFilter, is_api_enabled: modelSource === 'local' ? undefined : isApiEnabledFilter, isNew: modelSource === 'local' ? undefined : isNewFilter, isHot: modelSource === 'local' ? undefined : isFeaturedFilter, sortBy, order: sortOrder }, false); if (response.code === 200 && response.data) { let list = response.data.list; let pagination = response.data.pagination; // 如果是本地模型且列表为空,但当前分类是多模态,则注入mock if (modelSource === 'local' && list.length === 0 && (activeCategory === '多模态模型' || activeCategory === '全部' && categoryParam === '多模态模型')) { const mockModel: Model = { id: -1, title: 'qwen3-vl-32b-thinking', name: '通义千问3开源模型', img: '/icons/Tongyi.svg', description: '通义千问3系列开源模型,支持多模态输入,具备强大的视觉理解与逻辑推理能力。', tag1: '开源', tag2: '32B', category: 1, supplier: 'Qwen', is_featured: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), pricing_mode: 'free', input_price: '0', output_price: '0', price_unit: '1k tokens', price_currency: 'CNY', price_tiers: [], keyword: '开源,多模态,Qwen3' }; list = [mockModel]; pagination = { page: 1, pageSize: 21, total: 1, totalPages: 1 }; } // 云端模型:按正常顺序显示,不硬编码任何模型 // 更新缓存并设置展示 modelsCacheRef.current[cacheKey] = list; setModels(list); setPagination(pagination); setLoading(false); } else { throw new Error(response.message || '获取模型列表失败'); } }, 150); // 150ms 防抖 return; } catch (err) { setError(err instanceof Error ? err.message : '网络错误,请稍后重试'); console.error('Failed to fetch models:', err); } finally { // 仅在没有缓存展示时才关闭 loading(缓存路径上已经设置过) if (!models || models.length === 0) setLoading(false); } }, [activeCategory, activeGroupName, searchKeyword, selectedCompanies, selectedModalTypes, isNewFilter, isFeaturedFilter, isApiEnabledFilter, sortBy, sortOrder, currentPage, modelSource]); // 获取分组列表 const fetchGroupNames = useCallback(async () => { if (modelSource !== 'cloud') return; try { const response = await modelApi.getGroupNames(); if (response.code === 200 && response.data) { setGroupNames(response.data); } } catch (err) { console.error('Failed to fetch group names:', err); } }, [modelSource]); // 初始加载分组列表 useEffect(() => { fetchGroupNames(); }, [fetchGroupNames]); // 初始加载和URL页码变化时,重新获取数据并立即跳转到顶部 useEffect(() => { fetchModels(); // 立即跳转到顶部 const mainElement = document.querySelector('main'); if (mainElement) { mainElement.scrollTop = 0; } }, [currentPage, fetchModels]); // 初始加载和分类切换时,重置页码为 1 useEffect(() => { setSearchParams(prev => { const newParams = new URLSearchParams(prev); newParams.delete('page'); newParams.set('source', modelSource); return newParams; }); }, [activeCategory, activeGroupName, modelSource]); // eslint-disable-line react-hooks/exhaustive-deps // 筛选条件改变时自动应用筛选并重置页码为 1 useEffect(() => { // 使用一个标志来避免初始加载时触发 const timer = setTimeout(() => { if (pagination.total > 0 || models.length > 0) { setSearchParams(prev => { const newParams = new URLSearchParams(prev); newParams.delete('page'); newParams.set('source', modelSource); return newParams; }); } }, 100); return () => clearTimeout(timer); }, [selectedCompanies, selectedModalTypes, isNewFilter, isFeaturedFilter, isApiEnabledFilter, sortBy, sortOrder, modelSource]); // eslint-disable-line react-hooks/exhaustive-deps // 点击外部关闭筛选面板 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( filterPanelRef.current && !filterPanelRef.current.contains(event.target as Node) && filterButtonRef.current && !filterButtonRef.current.contains(event.target as Node) ) { setShowFilterPanel(false); } }; if (showFilterPanel) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [showFilterPanel]); // 切换厂商选择 const toggleCompany = (company: string) => { setSelectedCompanies(prev => prev.includes(company) ? prev.filter(c => c !== company) : [...prev, company] ); }; // 切换模态类型选择 const toggleModalType = (modalType: string) => { setSelectedModalTypes(prev => prev.includes(modalType) ? prev.filter(m => m !== modalType) : [...prev, modalType] ); }; // 清除所有筛选条件 const clearFilters = () => { setSelectedCompanies([]); setSelectedModalTypes([]); setIsNewFilter(undefined); setIsFeaturedFilter(undefined); setIsApiEnabledFilter(undefined); setSortBy('createdAt'); setSortOrder('desc'); }; // 计算选中数量 const selectedCount = selectedCompanies.length + selectedModalTypes.length + (isNewFilter !== undefined ? 1 : 0) + (isFeaturedFilter !== undefined ? 1 : 0) + (isApiEnabledFilter !== undefined ? 1 : 0); // 检查是否有活动的筛选条件 const hasActiveFilters = selectedCompanies.length > 0 || selectedModalTypes.length > 0 || isNewFilter !== undefined || isFeaturedFilter !== undefined || isApiEnabledFilter !== undefined || sortBy !== 'createdAt' || sortOrder !== 'desc'; // 搜索处理 const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setSearchParams(prev => { const newParams = new URLSearchParams(prev); newParams.delete('page'); newParams.set('source', modelSource); return newParams; }); }; return (

模型广场

{modelSource === 'local' ? '运行于本地节点的私域模型,确保数据隐私与高效响应' : '探索前沿 AI 基础模型,一键集成到您的创作流'}

{/* 云端/本地模型切换Tab - 居中 */}
{/* 右侧占位,保持Tab居中 */}
{/* 分类选择器 + 搜索筛选 - 仅云端模型显示 */} {modelSource === 'cloud' && (
{/* 分组筛选按钮行 - 在分类按钮上方 */} {groupNames.length > 0 && (
{groupNames.map(name => ( ))}
)} {/* 模型分类按钮 + 搜索框 */}
{modelCategories.map(cat => ( ))}
{/* 搜索框 */}
setSearchKeyword(e.target.value)} placeholder="搜索模型名称" className="pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 w-full sm:w-48 transition-all shadow-sm" />
)} {/* 主体区域:模型列表(移除右侧分组面板,分组已移至顶部) */}
{/* 模型内容区 */}
{/* 本地模型列表 */} {modelSource === 'local' && ( )} {/* 云端模型加载状态 */} {modelSource === 'cloud' && loading && (
加载中...
)} {/* 云端模型错误状态 */} {modelSource === 'cloud' && error && !loading && (

{error}

)} {/* 云端模型列表或空状态 */} {modelSource === 'cloud' && !loading && !error && ( <> {models.length > 0 ? (
{models.map((m) => ( ))}
) : (

未找到相关模型

试试更换分类或搜索关键词

)} )} {/* 云端模型分页信息 */} {modelSource === 'cloud' && !loading && !error && ( )}
{/* 右侧分组面板已移至顶部,此处不再显示 */}
); }; export default ModelSquare;