ModelSquare.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
  2. import { useSearchParams } from 'react-router-dom';
  3. import { PageId } from '../components/Sidebar';
  4. import Pagination from '../components/Pagination';
  5. import { Model } from '../types/models';
  6. import { modelApi } from '../services/modelApi';
  7. import { Search, Zap, Flame, Loader2 } from '../icons/commonIcons';
  8. import ModelCard from '../components/ModelCard';
  9. import LocalModelList from '../components/LocalModelList';
  10. interface ModelSquareProps {}
  11. const ModelSquare: React.FC<ModelSquareProps> = () => {
  12. const [searchParams, setSearchParams] = useSearchParams();
  13. const modelSource = useMemo(() => searchParams.get('source') || 'cloud', [searchParams]);
  14. const [activeCategory, setActiveCategory] = useState('全部');
  15. const activeGroupName = useMemo(() => searchParams.get('group') || '全部', [searchParams]);
  16. const setActiveGroupName = (name: string) => {
  17. setSearchParams(prev => {
  18. const newParams = new URLSearchParams(prev);
  19. if (name === '全部') {
  20. newParams.delete('group');
  21. } else {
  22. newParams.set('group', name);
  23. }
  24. newParams.delete('page');
  25. return newParams;
  26. });
  27. };
  28. const [groupNames, setGroupNames] = useState<string[]>([]);
  29. const [models, setModels] = useState<Model[]>([]);
  30. const [loading, setLoading] = useState(true);
  31. const [error, setError] = useState<string | null>(null);
  32. const [searchKeyword, setSearchKeyword] = useState('');
  33. // 从 URL 读取页码,如果没有则默认为 1
  34. const currentPage = useMemo(() => {
  35. const pageParam = searchParams.get('page');
  36. const page = pageParam ? parseInt(pageParam, 10) : 1;
  37. return isNaN(page) || page < 1 ? 1 : page;
  38. }, [searchParams]);
  39. const [pagination, setPagination] = useState({
  40. page: currentPage,
  41. pageSize: 21,
  42. total: 0,
  43. totalPages: 0
  44. });
  45. const [showFilterPanel, setShowFilterPanel] = useState(false);
  46. const [selectedCompanies, setSelectedCompanies] = useState<string[]>([]);
  47. const [selectedModalTypes, setSelectedModalTypes] = useState<string[]>([]);
  48. const [isNewFilter, setIsNewFilter] = useState<boolean | undefined>(undefined);
  49. const [isFeaturedFilter, setIsFeaturedFilter] = useState<boolean | undefined>(undefined);
  50. const [isApiEnabledFilter, setIsApiEnabledFilter] = useState<boolean | undefined>(undefined);
  51. const [sortBy, setSortBy] = useState<string>('createdAt');
  52. const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
  53. const filterPanelRef = useRef<HTMLDivElement>(null);
  54. const filterButtonRef = useRef<HTMLButtonElement>(null);
  55. // 简单内存缓存,用于切换时快速展示已加载的数据(key 为 source|category|keyword)
  56. const modelsCacheRef = useRef<Record<string, Model[]>>({});
  57. const fetchTimerRef = useRef<number | null>(null);
  58. // 供应商列表(通义合并为一个,不做细分)
  59. const allSuppliers = ['通义', 'DeepSeek', 'Moonshot', '智谱AI', 'Black Forest Labs', 'MiniMax', 'Stability AI'];
  60. // 标签分组定义(与数据库实际标签值对应)
  61. const modalTypeGroups = {
  62. '版本': ['最新版本'],
  63. '翻译': ['翻译'],
  64. '视觉': ['视频生成']
  65. };
  66. // 模型分类列表(与后端 ModelCategory 枚举一致)
  67. const modelCategories = ['全部', '语言模型', '多模态模型', 'TTS模型', 'STT模型', '生图模型', '生视频模型', '图像编辑', 'Embedding', 'Rerank'];
  68. // 不再需要加载百炼模型数据,直接使用API
  69. const getCategoryTagStyle = (category: string) => {
  70. switch (category) {
  71. case '语言模型': return 'bg-blue-50 text-blue-600 border-blue-200';
  72. case '多模态模型': return 'bg-indigo-50 text-indigo-600 border-indigo-200';
  73. case 'TTS模型': return 'bg-emerald-50 text-emerald-600 border-emerald-200';
  74. case 'STT模型': return 'bg-teal-50 text-teal-600 border-teal-200';
  75. case '生图模型': return 'bg-purple-50 text-purple-600 border-purple-200';
  76. case '生视频模型': return 'bg-orange-50 text-orange-600 border-orange-200';
  77. case '图像编辑': return 'bg-pink-50 text-pink-600 border-pink-200';
  78. case 'Embedding': return 'bg-cyan-50 text-cyan-600 border-cyan-200';
  79. case 'Rerank': return 'bg-amber-50 text-amber-600 border-amber-200';
  80. default: return 'bg-gray-50 text-gray-500 border-gray-100/50';
  81. }
  82. };
  83. // 获取模型列表(从API获取)
  84. const fetchModels = useCallback(async () => {
  85. try {
  86. setError(null);
  87. // 防抖:清理已有定时器,延迟实际请求以合并快速的切换/输入
  88. if (fetchTimerRef.current) {
  89. window.clearTimeout(fetchTimerRef.current);
  90. }
  91. // 计算缓存键
  92. const categoryParam = modelSource === 'local'
  93. ? (activeCategory !== '全部' ? activeCategory : '多模态模型')
  94. : (activeCategory !== '全部' ? activeCategory : undefined);
  95. const groupNameParam = modelSource === 'cloud' && activeGroupName !== '全部' ? activeGroupName : undefined;
  96. const cacheKey = `${modelSource}|${categoryParam || 'all'}|${groupNameParam || 'all'}|${searchKeyword || ''}`;
  97. // 如果有缓存,先使用缓存快速展示(不覆盖后续刷新结果)
  98. const cached = modelsCacheRef.current[cacheKey];
  99. if (cached && cached.length > 0) {
  100. setModels(cached);
  101. setPagination(prev => ({ ...prev, total: cached.length, totalPages: 1, page: 1 }));
  102. setLoading(false);
  103. } else {
  104. // 如果没有缓存且是本地且期望显示开源模型,立即展示占位(快速反馈)
  105. if (modelSource === 'local') {
  106. setModels([]);
  107. setLoading(true);
  108. } else {
  109. setLoading(true);
  110. }
  111. }
  112. // 延迟实际网络请求,合并短时间内的多次变更
  113. fetchTimerRef.current = window.setTimeout(async () => {
  114. // 处理供应商筛选:将显示名称映射到实际的供应商名称
  115. let supplierFilter: string | undefined;
  116. if (selectedCompanies.length > 0) {
  117. const supplierMap: Record<string, string> = {
  118. '通义': 'Qwen',
  119. 'DeepSeek': 'DeepSeek',
  120. 'Moonshot': 'Moonshot',
  121. '智谱AI': '智谱AI',
  122. 'Black Forest Labs': 'Black Forest Labs',
  123. 'MiniMax': 'MiniMax',
  124. 'Stability AI': 'Stability AI'
  125. };
  126. supplierFilter = supplierMap[selectedCompanies[0]] || selectedCompanies[0];
  127. }
  128. // 处理标签筛选:后端 filter_tag 参数支持在 tag1 或 tag2 中精确匹配
  129. let tagFilter: string | undefined;
  130. if (selectedModalTypes.length > 0) {
  131. tagFilter = selectedModalTypes[0];
  132. }
  133. const response = await modelApi.getModels({
  134. keyword: modelSource === 'local' ? '通义千问3开源模型' : (searchKeyword || undefined),
  135. page: currentPage,
  136. pageSize: 21,
  137. category: categoryParam,
  138. supplier: modelSource === 'local' ? undefined : supplierFilter,
  139. group_name: groupNameParam,
  140. filter_tag: modelSource === 'local' ? undefined : tagFilter,
  141. is_api_enabled: modelSource === 'local' ? undefined : isApiEnabledFilter,
  142. isNew: modelSource === 'local' ? undefined : isNewFilter,
  143. isHot: modelSource === 'local' ? undefined : isFeaturedFilter,
  144. sortBy,
  145. order: sortOrder
  146. }, false);
  147. if (response.code === 200 && response.data) {
  148. let list = response.data.list;
  149. let pagination = response.data.pagination;
  150. // 如果是本地模型且列表为空,但当前分类是多模态,则注入mock
  151. if (modelSource === 'local' && list.length === 0 && (activeCategory === '多模态模型' || activeCategory === '全部' && categoryParam === '多模态模型')) {
  152. const mockModel: Model = {
  153. id: -1,
  154. title: 'qwen3-vl-32b-thinking',
  155. name: '通义千问3开源模型',
  156. img: '/icons/Tongyi.svg',
  157. description: '通义千问3系列开源模型,支持多模态输入,具备强大的视觉理解与逻辑推理能力。',
  158. tag1: '开源',
  159. tag2: '32B',
  160. category: 1,
  161. supplier: 'Qwen',
  162. is_featured: true,
  163. created_at: new Date().toISOString(),
  164. updated_at: new Date().toISOString(),
  165. pricing_mode: 'free',
  166. input_price: '0',
  167. output_price: '0',
  168. price_unit: '1k tokens',
  169. price_currency: 'CNY',
  170. price_tiers: [],
  171. keyword: '开源,多模态,Qwen3'
  172. };
  173. list = [mockModel];
  174. pagination = {
  175. page: 1,
  176. pageSize: 21,
  177. total: 1,
  178. totalPages: 1
  179. };
  180. }
  181. // 云端模型:按正常顺序显示,不硬编码任何模型
  182. // 更新缓存并设置展示
  183. modelsCacheRef.current[cacheKey] = list;
  184. setModels(list);
  185. setPagination(pagination);
  186. setLoading(false);
  187. } else {
  188. throw new Error(response.message || '获取模型列表失败');
  189. }
  190. }, 150); // 150ms 防抖
  191. return;
  192. } catch (err) {
  193. setError(err instanceof Error ? err.message : '网络错误,请稍后重试');
  194. console.error('Failed to fetch models:', err);
  195. } finally {
  196. // 仅在没有缓存展示时才关闭 loading(缓存路径上已经设置过)
  197. if (!models || models.length === 0) setLoading(false);
  198. }
  199. }, [activeCategory, activeGroupName, searchKeyword, selectedCompanies, selectedModalTypes, isNewFilter, isFeaturedFilter, isApiEnabledFilter, sortBy, sortOrder, currentPage, modelSource]);
  200. // 获取分组列表
  201. const fetchGroupNames = useCallback(async () => {
  202. if (modelSource !== 'cloud') return;
  203. try {
  204. const response = await modelApi.getGroupNames();
  205. if (response.code === 200 && response.data) {
  206. setGroupNames(response.data);
  207. }
  208. } catch (err) {
  209. console.error('Failed to fetch group names:', err);
  210. }
  211. }, [modelSource]);
  212. // 初始加载分组列表
  213. useEffect(() => {
  214. fetchGroupNames();
  215. }, [fetchGroupNames]);
  216. // 初始加载和URL页码变化时,重新获取数据并立即跳转到顶部
  217. useEffect(() => {
  218. fetchModels();
  219. // 立即跳转到顶部
  220. const mainElement = document.querySelector('main');
  221. if (mainElement) {
  222. mainElement.scrollTop = 0;
  223. }
  224. }, [currentPage, fetchModels]);
  225. // 初始加载和分类切换时,重置页码为 1
  226. useEffect(() => {
  227. setSearchParams(prev => {
  228. const newParams = new URLSearchParams(prev);
  229. newParams.delete('page');
  230. newParams.set('source', modelSource);
  231. return newParams;
  232. });
  233. }, [activeCategory, activeGroupName, modelSource]); // eslint-disable-line react-hooks/exhaustive-deps
  234. // 筛选条件改变时自动应用筛选并重置页码为 1
  235. useEffect(() => {
  236. // 使用一个标志来避免初始加载时触发
  237. const timer = setTimeout(() => {
  238. if (pagination.total > 0 || models.length > 0) {
  239. setSearchParams(prev => {
  240. const newParams = new URLSearchParams(prev);
  241. newParams.delete('page');
  242. newParams.set('source', modelSource);
  243. return newParams;
  244. });
  245. }
  246. }, 100);
  247. return () => clearTimeout(timer);
  248. }, [selectedCompanies, selectedModalTypes, isNewFilter, isFeaturedFilter, isApiEnabledFilter, sortBy, sortOrder, modelSource]); // eslint-disable-line react-hooks/exhaustive-deps
  249. // 点击外部关闭筛选面板
  250. useEffect(() => {
  251. const handleClickOutside = (event: MouseEvent) => {
  252. if (
  253. filterPanelRef.current &&
  254. !filterPanelRef.current.contains(event.target as Node) &&
  255. filterButtonRef.current &&
  256. !filterButtonRef.current.contains(event.target as Node)
  257. ) {
  258. setShowFilterPanel(false);
  259. }
  260. };
  261. if (showFilterPanel) {
  262. document.addEventListener('mousedown', handleClickOutside);
  263. }
  264. return () => {
  265. document.removeEventListener('mousedown', handleClickOutside);
  266. };
  267. }, [showFilterPanel]);
  268. // 切换厂商选择
  269. const toggleCompany = (company: string) => {
  270. setSelectedCompanies(prev =>
  271. prev.includes(company)
  272. ? prev.filter(c => c !== company)
  273. : [...prev, company]
  274. );
  275. };
  276. // 切换模态类型选择
  277. const toggleModalType = (modalType: string) => {
  278. setSelectedModalTypes(prev =>
  279. prev.includes(modalType)
  280. ? prev.filter(m => m !== modalType)
  281. : [...prev, modalType]
  282. );
  283. };
  284. // 清除所有筛选条件
  285. const clearFilters = () => {
  286. setSelectedCompanies([]);
  287. setSelectedModalTypes([]);
  288. setIsNewFilter(undefined);
  289. setIsFeaturedFilter(undefined);
  290. setIsApiEnabledFilter(undefined);
  291. setSortBy('createdAt');
  292. setSortOrder('desc');
  293. };
  294. // 计算选中数量
  295. const selectedCount = selectedCompanies.length + selectedModalTypes.length +
  296. (isNewFilter !== undefined ? 1 : 0) + (isFeaturedFilter !== undefined ? 1 : 0) + (isApiEnabledFilter !== undefined ? 1 : 0);
  297. // 检查是否有活动的筛选条件
  298. const hasActiveFilters = selectedCompanies.length > 0 || selectedModalTypes.length > 0 ||
  299. isNewFilter !== undefined || isFeaturedFilter !== undefined || isApiEnabledFilter !== undefined || sortBy !== 'createdAt' || sortOrder !== 'desc';
  300. // 搜索处理
  301. const handleSearch = (e: React.FormEvent) => {
  302. e.preventDefault();
  303. setSearchParams(prev => {
  304. const newParams = new URLSearchParams(prev);
  305. newParams.delete('page');
  306. newParams.set('source', modelSource);
  307. return newParams;
  308. });
  309. };
  310. return (
  311. <div className="bg-white p-4 sm:p-6 md:p-8 rounded-2xl border border-gray-100 shadow-sm">
  312. <div className="space-y-4 sm:space-y-6 md:space-y-8 animate-in fade-in duration-500">
  313. <div className="flex flex-col md:flex-row md:items-center justify-between gap-3 sm:gap-4 md:gap-6">
  314. <div className="flex-1">
  315. <h2 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight">模型广场</h2>
  316. <p className="text-sm text-gray-500 mt-1">
  317. {modelSource === 'local' ? '运行于本地节点的私域模型,确保数据隐私与高效响应' : '探索前沿 AI 基础模型,一键集成到您的创作流'}
  318. </p>
  319. </div>
  320. {/* 云端/本地模型切换Tab - 居中 */}
  321. <div className="flex items-center justify-center">
  322. <div className="flex items-center bg-gray-100 rounded-xl p-1">
  323. <button
  324. onClick={() => {
  325. if (modelSource !== 'cloud') {
  326. setActiveCategory('全部');
  327. clearFilters();
  328. setSearchParams(prev => {
  329. const newParams = new URLSearchParams(prev);
  330. newParams.set('source', 'cloud');
  331. newParams.delete('page');
  332. return newParams;
  333. });
  334. }
  335. }}
  336. className={`px-5 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
  337. modelSource === 'cloud'
  338. ? 'bg-white text-blue-600 shadow-sm'
  339. : 'text-gray-500 hover:text-gray-700'
  340. }`}
  341. >
  342. 云端模型
  343. </button>
  344. <button
  345. onClick={() => {
  346. if (modelSource !== 'local') {
  347. setActiveCategory('全部');
  348. clearFilters();
  349. setSearchParams(prev => {
  350. const newParams = new URLSearchParams(prev);
  351. newParams.set('source', 'local');
  352. newParams.delete('page');
  353. return newParams;
  354. });
  355. }
  356. }}
  357. className={`px-5 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
  358. modelSource === 'local'
  359. ? 'bg-white text-blue-600 shadow-sm'
  360. : 'text-gray-500 hover:text-gray-700'
  361. }`}
  362. >
  363. 私域模型
  364. </button>
  365. </div>
  366. </div>
  367. {/* 右侧占位,保持Tab居中 */}
  368. <div className="flex-1"></div>
  369. </div>
  370. {/* 分类选择器 + 搜索筛选 - 仅云端模型显示 */}
  371. {modelSource === 'cloud' && (
  372. <div className="flex flex-col gap-3 pb-2">
  373. {/* 分组筛选按钮行 - 在分类按钮上方 */}
  374. {groupNames.length > 0 && (
  375. <div className="flex items-center space-x-2 overflow-x-auto scrollbar-hide">
  376. <button
  377. onClick={() => setActiveGroupName('全部')}
  378. className={`px-4 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all duration-200 ${
  379. activeGroupName === '全部'
  380. ? 'bg-violet-600 text-white shadow-md shadow-violet-600/20'
  381. : 'bg-white text-gray-500 border border-gray-100 hover:border-violet-200 hover:text-violet-600'
  382. }`}
  383. >
  384. 全部分组
  385. </button>
  386. {groupNames.map(name => (
  387. <button
  388. key={name}
  389. onClick={() => setActiveGroupName(name)}
  390. className={`px-4 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all duration-200 ${
  391. activeGroupName === name
  392. ? 'bg-violet-600 text-white shadow-md shadow-violet-600/20'
  393. : 'bg-white text-gray-500 border border-gray-100 hover:border-violet-200 hover:text-violet-600'
  394. }`}
  395. >
  396. {name}
  397. </button>
  398. ))}
  399. </div>
  400. )}
  401. {/* 模型分类按钮 + 搜索框 */}
  402. <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
  403. <div className="flex items-center space-x-2 overflow-x-auto scrollbar-hide w-full sm:w-auto pb-1 sm:pb-0">
  404. {modelCategories.map(cat => (
  405. <button
  406. key={cat}
  407. onClick={() => setActiveCategory(cat)}
  408. className={`px-6 py-2 rounded-full text-xs font-bold whitespace-nowrap transition-all duration-200 ${
  409. cat === activeCategory
  410. ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/20'
  411. : 'bg-white text-gray-500 border border-gray-100 hover:border-blue-200 hover:text-blue-600'
  412. }`}
  413. >
  414. {cat}
  415. </button>
  416. ))}
  417. </div>
  418. {/* 搜索框 */}
  419. <div className="flex items-center gap-2 flex-shrink-0 w-full sm:w-auto">
  420. <form onSubmit={handleSearch} className="relative group w-full sm:w-auto">
  421. <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-4 h-4 group-focus-within:text-blue-500 transition-colors" />
  422. <input
  423. type="text"
  424. value={searchKeyword}
  425. onChange={(e) => setSearchKeyword(e.target.value)}
  426. placeholder="搜索模型名称"
  427. 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"
  428. />
  429. </form>
  430. </div>
  431. </div>
  432. </div>
  433. )}
  434. {/* 主体区域:模型列表(移除右侧分组面板,分组已移至顶部) */}
  435. <div className="flex gap-4">
  436. {/* 模型内容区 */}
  437. <div className="flex-1 min-w-0 space-y-4">
  438. {/* 本地模型列表 */}
  439. {modelSource === 'local' && (
  440. <LocalModelList />
  441. )}
  442. {/* 云端模型加载状态 */}
  443. {modelSource === 'cloud' && loading && (
  444. <div className="flex items-center justify-center py-20">
  445. <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
  446. <span className="ml-3 text-gray-600">加载中...</span>
  447. </div>
  448. )}
  449. {/* 云端模型错误状态 */}
  450. {modelSource === 'cloud' && error && !loading && (
  451. <div className="bg-red-50 border border-red-200 rounded-xl p-4 text-center">
  452. <p className="text-red-600">{error}</p>
  453. <button
  454. onClick={() => fetchModels()}
  455. className="mt-2 text-sm text-red-600 font-bold hover:underline"
  456. >
  457. 重试
  458. </button>
  459. </div>
  460. )}
  461. {/* 云端模型列表或空状态 */}
  462. {modelSource === 'cloud' && !loading && !error && (
  463. <>
  464. {models.length > 0 ? (
  465. <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
  466. {models.map((m) => (
  467. <ModelCard
  468. key={m.id}
  469. model={m}
  470. versionDisplayMode="standard"
  471. />
  472. ))}
  473. </div>
  474. ) : (
  475. <div className="py-20 flex flex-col items-center justify-center text-center bg-gray-50 rounded-3xl border-2 border-dashed border-gray-200">
  476. <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-sm mb-4">
  477. <Search className="w-8 h-8 text-gray-300" />
  478. </div>
  479. <h3 className="text-lg font-bold text-gray-900">未找到相关模型</h3>
  480. <p className="text-sm text-gray-500 mt-2">试试更换分类或搜索关键词</p>
  481. <button
  482. onClick={() => {
  483. setActiveCategory('全部');
  484. setSearchKeyword('');
  485. }}
  486. className="mt-6 text-sm text-blue-600 font-bold hover:underline"
  487. >
  488. 查看全部模型
  489. </button>
  490. </div>
  491. )}
  492. </>
  493. )}
  494. {/* 云端模型分页信息 */}
  495. {modelSource === 'cloud' && !loading && !error && (
  496. <Pagination
  497. total={pagination.total}
  498. totalPages={pagination.totalPages}
  499. currentPage={currentPage}
  500. />
  501. )}
  502. </div>
  503. {/* 右侧分组面板已移至顶部,此处不再显示 */}
  504. </div>
  505. </div>
  506. </div>
  507. );
  508. };
  509. export default ModelSquare;