diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 5045307a..65076926 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -20,7 +20,7 @@ import { Tags, } from 'lucide-react'; -import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js'; +import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants/index.js'; import { Button, Divider, @@ -63,7 +63,7 @@ import { IconCopy, IconSmallTriangleRight } from '@douyinfe/semi-icons'; -import { loadChannelModels } from '../../helpers/index.js'; +import { loadChannelModels, isMobile, copy } from '../../helpers'; import EditTagModal from '../../pages/Channel/EditTagModal.js'; import { useTranslation } from 'react-i18next'; import { useTableCompactMode } from '../../hooks/useTableCompactMode'; @@ -684,9 +684,11 @@ const ChannelsTable = () => { const [modelSearchKeyword, setModelSearchKeyword] = useState(''); const [modelTestResults, setModelTestResults] = useState({}); const [testingModels, setTestingModels] = useState(new Set()); + const [selectedModelKeys, setSelectedModelKeys] = useState([]); const [isBatchTesting, setIsBatchTesting] = useState(false); const [testQueue, setTestQueue] = useState([]); const [isProcessingQueue, setIsProcessingQueue] = useState(false); + const [modelTablePage, setModelTablePage] = useState(1); const [activeTypeKey, setActiveTypeKey] = useState('all'); const [typeCounts, setTypeCounts] = useState({}); const requestCounter = useRef(0); @@ -697,6 +699,7 @@ const ChannelsTable = () => { searchGroup: '', searchModel: '', }; + const allSelectingRef = useRef(false); // Filter columns based on visibility settings const getVisibleColumns = () => { @@ -1131,7 +1134,22 @@ const ChannelsTable = () => { const processTestQueue = async () => { if (!isProcessingQueue || testQueue.length === 0) return; - const { channel, model } = testQueue[0]; + const { channel, model, indexInFiltered } = testQueue[0]; + + // 自动翻页到正在测试的模型所在页 + if (currentTestChannel && currentTestChannel.id === channel.id) { + let pageNo; + if (indexInFiltered !== undefined) { + pageNo = Math.floor(indexInFiltered / MODEL_TABLE_PAGE_SIZE) + 1; + } else { + const filteredModelsList = currentTestChannel.models + .split(',') + .filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase())); + const modelIdx = filteredModelsList.indexOf(model); + pageNo = modelIdx !== -1 ? Math.floor(modelIdx / MODEL_TABLE_PAGE_SIZE) + 1 : 1; + } + setModelTablePage(pageNo); + } try { setTestingModels(prev => new Set([...prev, model])); @@ -1194,16 +1212,22 @@ const ChannelsTable = () => { setIsBatchTesting(true); - const models = currentTestChannel.models + // 重置分页到第一页 + setModelTablePage(1); + + const filteredModels = currentTestChannel.models .split(',') .filter((model) => - model.toLowerCase().includes(modelSearchKeyword.toLowerCase()) + model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), ); - setTestQueue(models.map(model => ({ - channel: currentTestChannel, - model - }))); + setTestQueue( + filteredModels.map((model, idx) => ({ + channel: currentTestChannel, + model, + indexInFiltered: idx, // 记录在过滤列表中的顺序 + })), + ); setIsProcessingQueue(true); }; @@ -1217,6 +1241,8 @@ const ChannelsTable = () => { } else { setShowModelTestModal(false); setModelSearchKeyword(''); + setSelectedModelKeys([]); + setModelTablePage(1); } }; @@ -1912,13 +1938,73 @@ const ChannelsTable = () => { - - {currentTestChannel.name} {t('渠道的模型测试')} - - - {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')} - +
+
+ + {currentTestChannel.name} {t('渠道的模型测试')} + + + {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')} + +
+ + {/* 搜索与操作按钮 */} +
+ { + setModelSearchKeyword(v); + setModelTablePage(1); + }} + className="!w-full !rounded-full" + prefix={} + showClear + /> + + + + +
) } @@ -1968,22 +2054,11 @@ const ChannelsTable = () => { } maskClosable={!isBatchTesting} className="!rounded-lg" - size="large" + size={isMobile() ? 'full-width' : 'large'} > -
+
{currentTestChannel && (
-
- setModelSearchKeyword(v)} - className="w-64 !rounded-full" - prefix={} - showClear - /> -
- { } } ]} - dataSource={currentTestChannel.models - .split(',') - .filter((model) => - model.toLowerCase().includes(modelSearchKeyword.toLowerCase()) - ) - .map((model) => ({ + dataSource={(() => { + const filtered = currentTestChannel.models + .split(',') + .filter((model) => + model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), + ); + const start = (modelTablePage - 1) * MODEL_TABLE_PAGE_SIZE; + const end = start + MODEL_TABLE_PAGE_SIZE; + return filtered.slice(start, end).map((model) => ({ model, - key: model - }))} - pagination={false} + key: model, + })); + })()} + rowSelection={{ + selectedRowKeys: selectedModelKeys, + onChange: (keys) => { + if (allSelectingRef.current) { + allSelectingRef.current = false; + return; + } + setSelectedModelKeys(keys); + }, + onSelectAll: (checked) => { + const filtered = currentTestChannel.models + .split(',') + .filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase())); + allSelectingRef.current = true; + setSelectedModelKeys(checked ? filtered : []); + }, + }} + pagination={{ + currentPage: modelTablePage, + pageSize: MODEL_TABLE_PAGE_SIZE, + total: currentTestChannel.models + .split(',') + .filter((model) => + model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), + ).length, + showSizeChanger: false, + onPageChange: (page) => setModelTablePage(page), + }} /> )} diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index c4220bd4..4018aa4f 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -131,3 +131,5 @@ export const CHANNEL_OPTIONS = [ label: '可灵', }, ]; + +export const MODEL_TABLE_PAGE_SIZE = 10; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index a618ee97..408371b4 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1737,5 +1737,11 @@ "状态筛选": "Status filter", "没有模型可以复制": "No models to copy", "模型列表已复制到剪贴板": "Model list copied to clipboard", - "复制失败": "Copy failed" + "复制失败": "Copy failed", + "复制已选": "Copy selected", + "选择成功": "Selection successful", + "暂无成功模型": "No successful models", + "请先选择模型!": "Please select a model first!", + "已复制 ${count} 个模型": "Copied ${count} models", + "复制失败,请手动复制": "Copy failed, please copy manually" } \ No newline at end of file diff --git a/web/src/index.css b/web/src/index.css index c95e6db4..45d2d378 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -375,6 +375,7 @@ code { } /* 隐藏卡片内容区域的滚动条 */ +.model-test-scroll, .card-content-scroll, .model-settings-scroll, .thinking-content-scroll, @@ -385,6 +386,7 @@ code { scrollbar-width: none; } +.model-test-scroll::-webkit-scrollbar, .card-content-scroll::-webkit-scrollbar, .model-settings-scroll::-webkit-scrollbar, .thinking-content-scroll::-webkit-scrollbar,