import React, { useEffect, useState, useMemo, useRef } from 'react'; import { API, showError, showInfo, showSuccess, timestamp2string, renderGroup, renderQuota, getChannelIcon, renderQuotaWithAmount } from '../../helpers/index.js'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants/index.js'; import { Button, Divider, Dropdown, Empty, Input, InputNumber, Modal, Space, SplitButtonGroup, Switch, Table, Tag, Tooltip, Typography, Checkbox, Card, Form, Tabs, TabPane, Select } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import EditChannel from '../../pages/Channel/EditChannel.js'; import { IconTreeTriangleDown, IconSearch, IconMore, IconList, IconDescend2 } from '@douyinfe/semi-icons'; import { loadChannelModels, isMobile, copy } from '../../helpers'; import EditTagModal from '../../pages/Channel/EditTagModal.js'; import { useTranslation } from 'react-i18next'; import { useTableCompactMode } from '../../hooks/useTableCompactMode'; import { FaRandom } from 'react-icons/fa'; const ChannelsTable = () => { const { t } = useTranslation(); let type2label = undefined; const renderType = (type, channelInfo = undefined) => { if (!type2label) { type2label = new Map(); for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; } type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' }; } let icon = getChannelIcon(type); if (channelInfo?.is_multi_key) { icon = ( channelInfo?.multi_key_mode === 'random' ? (
{icon}
) : (
{icon}
) ) } return ( {type2label[type]?.label} ); }; const renderTagType = () => { return ( {t('标签聚合')} ); }; const renderStatus = (status, channelInfo = undefined) => { if (channelInfo) { if (channelInfo.is_multi_key) { let keySize = channelInfo.multi_key_size; let enabledKeySize = keySize; if (channelInfo.multi_key_status_list) { // multi_key_status_list is a map, key is key, value is status // get multi_key_status_list length enabledKeySize = keySize - Object.keys(channelInfo.multi_key_status_list).length; } return renderMultiKeyStatus(status, keySize, enabledKeySize); } } switch (status) { case 1: return ( {t('已启用')} ); case 2: return ( {t('已禁用')} ); case 3: return ( {t('自动禁用')} ); default: return ( {t('未知状态')} ); } }; const renderMultiKeyStatus = (status, keySize, enabledKeySize) => { switch (status) { case 1: return ( {t('已启用')} {enabledKeySize}/{keySize} ); case 2: return ( {t('已禁用')} {enabledKeySize}/{keySize} ); case 3: return ( {t('自动禁用')} {enabledKeySize}/{keySize} ); default: return ( {t('未知状态')} {enabledKeySize}/{keySize} ); } } const renderResponseTime = (responseTime) => { let time = responseTime / 1000; time = time.toFixed(2) + t(' 秒'); if (responseTime === 0) { return ( {t('未测试')} ); } else if (responseTime <= 1000) { return ( {time} ); } else if (responseTime <= 3000) { return ( {time} ); } else if (responseTime <= 5000) { return ( {time} ); } else { return ( {time} ); } }; // Define column keys for selection const COLUMN_KEYS = { ID: 'id', NAME: 'name', GROUP: 'group', TYPE: 'type', STATUS: 'status', RESPONSE_TIME: 'response_time', BALANCE: 'balance', PRIORITY: 'priority', WEIGHT: 'weight', OPERATE: 'operate', }; // State for column visibility const [visibleColumns, setVisibleColumns] = useState({}); const [showColumnSelector, setShowColumnSelector] = useState(false); // 状态筛选 all / enabled / disabled const [statusFilter, setStatusFilter] = useState( localStorage.getItem('channel-status-filter') || 'all' ); // Load saved column preferences from localStorage useEffect(() => { const savedColumns = localStorage.getItem('channels-table-columns'); if (savedColumns) { try { const parsed = JSON.parse(savedColumns); // Make sure all columns are accounted for const defaults = getDefaultColumnVisibility(); const merged = { ...defaults, ...parsed }; setVisibleColumns(merged); } catch (e) { console.error('Failed to parse saved column preferences', e); initDefaultColumns(); } } else { initDefaultColumns(); } }, []); // Update table when column visibility changes useEffect(() => { if (Object.keys(visibleColumns).length > 0) { // Save to localStorage localStorage.setItem( 'channels-table-columns', JSON.stringify(visibleColumns), ); } }, [visibleColumns]); // Get default column visibility const getDefaultColumnVisibility = () => { return { [COLUMN_KEYS.ID]: true, [COLUMN_KEYS.NAME]: true, [COLUMN_KEYS.GROUP]: true, [COLUMN_KEYS.TYPE]: true, [COLUMN_KEYS.STATUS]: true, [COLUMN_KEYS.RESPONSE_TIME]: true, [COLUMN_KEYS.BALANCE]: true, [COLUMN_KEYS.PRIORITY]: true, [COLUMN_KEYS.WEIGHT]: true, [COLUMN_KEYS.OPERATE]: true, }; }; // Initialize default column visibility const initDefaultColumns = () => { const defaults = getDefaultColumnVisibility(); setVisibleColumns(defaults); }; // Handle column visibility change const handleColumnVisibilityChange = (columnKey, checked) => { const updatedColumns = { ...visibleColumns, [columnKey]: checked }; setVisibleColumns(updatedColumns); }; // Handle "Select All" checkbox const handleSelectAll = (checked) => { const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]); const updatedColumns = {}; allKeys.forEach((key) => { updatedColumns[key] = checked; }); setVisibleColumns(updatedColumns); }; // Define all columns with keys const allColumns = [ { key: COLUMN_KEYS.ID, title: t('ID'), dataIndex: 'id', }, { key: COLUMN_KEYS.NAME, title: t('名称'), dataIndex: 'name', }, { key: COLUMN_KEYS.GROUP, title: t('分组'), dataIndex: 'group', render: (text, record, index) => (
{text ?.split(',') .sort((a, b) => { if (a === 'default') return -1; if (b === 'default') return 1; return a.localeCompare(b); }) .map((item, index) => renderGroup(item))}
), }, { key: COLUMN_KEYS.TYPE, title: t('类型'), dataIndex: 'type', render: (text, record, index) => { if (record.children === undefined) { if (record.channel_info) { if (record.channel_info.is_multi_key) { return <>{renderType(text, record.channel_info)}; } } return <>{renderType(text)}; } else { return <>{renderTagType()}; } }, }, { key: COLUMN_KEYS.STATUS, title: t('状态'), dataIndex: 'status', render: (text, record, index) => { if (text === 3) { if (record.other_info === '') { record.other_info = '{}'; } let otherInfo = JSON.parse(record.other_info); let reason = otherInfo['status_reason']; let time = otherInfo['status_time']; return (
{renderStatus(text, record.channel_info)}
); } else { return renderStatus(text, record.channel_info); } }, }, { key: COLUMN_KEYS.RESPONSE_TIME, title: t('响应时间'), dataIndex: 'response_time', render: (text, record, index) => (
{renderResponseTime(text)}
), }, { key: COLUMN_KEYS.BALANCE, title: t('已用/剩余'), dataIndex: 'expired_time', render: (text, record, index) => { if (record.children === undefined) { return (
{renderQuota(record.used_quota)} updateChannelBalance(record)} > {renderQuotaWithAmount(record.balance)}
); } else { return ( {renderQuota(record.used_quota)} ); } }, }, { key: COLUMN_KEYS.PRIORITY, title: t('优先级'), dataIndex: 'priority', render: (text, record, index) => { if (record.children === undefined) { return (
{ manageChannel(record.id, 'priority', record, e.target.value); }} keepFocus={true} innerButtons defaultValue={record.priority} min={-999} size="small" />
); } else { return ( { Modal.warning({ title: t('修改子渠道优先级'), content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'), onOk: () => { if (e.target.value === '') { return; } submitTagEdit('priority', { tag: record.key, priority: e.target.value, }); }, }); }} innerButtons defaultValue={record.priority} min={-999} size="small" /> ); } }, }, { key: COLUMN_KEYS.WEIGHT, title: t('权重'), dataIndex: 'weight', render: (text, record, index) => { if (record.children === undefined) { return (
{ manageChannel(record.id, 'weight', record, e.target.value); }} keepFocus={true} innerButtons defaultValue={record.weight} min={0} size="small" />
); } else { return ( { Modal.warning({ title: t('修改子渠道权重'), content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'), onOk: () => { if (e.target.value === '') { return; } submitTagEdit('weight', { tag: record.key, weight: e.target.value, }); }, }); }} innerButtons defaultValue={record.weight} min={-999} size="small" /> ); } }, }, { key: COLUMN_KEYS.OPERATE, title: '', dataIndex: 'operate', fixed: 'right', render: (text, record, index) => { if (record.children === undefined) { // 创建更多操作的下拉菜单项 const moreMenuItems = [ { node: 'item', name: t('删除'), type: 'danger', onClick: () => { Modal.confirm({ title: t('确定是否要删除此渠道?'), content: t('此修改将不可逆'), onOk: () => { manageChannel(record.id, 'delete', record).then(() => { removeRecord(record); }); }, }); }, }, { node: 'item', name: t('复制'), type: 'primary', onClick: () => { Modal.confirm({ title: t('确定是否要复制此渠道?'), content: t('复制渠道的所有信息'), onOk: () => copySelectedChannel(record), }); }, }, ]; return ( ) : ( ) } manageChannel(record.id, 'enable_all', record), } ]} > ) : ( ) )} ); } }, }, ]; const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [idSort, setIdSort] = useState(false); const [searching, setSearching] = useState(false); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [channelCount, setChannelCount] = useState(pageSize); const [groupOptions, setGroupOptions] = useState([]); const [showEdit, setShowEdit] = useState(false); const [enableBatchDelete, setEnableBatchDelete] = useState(false); const [editingChannel, setEditingChannel] = useState({ id: undefined, }); const [showEditTag, setShowEditTag] = useState(false); const [editingTag, setEditingTag] = useState(''); const [selectedChannels, setSelectedChannels] = useState([]); const [enableTagMode, setEnableTagMode] = useState(false); const [showBatchSetTag, setShowBatchSetTag] = useState(false); const [batchSetTagValue, setBatchSetTagValue] = useState(''); const [showModelTestModal, setShowModelTestModal] = useState(false); const [currentTestChannel, setCurrentTestChannel] = useState(null); 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); const [formApi, setFormApi] = useState(null); const [compactMode, setCompactMode] = useTableCompactMode('channels'); const formInitValues = { searchKeyword: '', searchGroup: '', searchModel: '', }; const allSelectingRef = useRef(false); // Filter columns based on visibility settings const getVisibleColumns = () => { return allColumns.filter((column) => visibleColumns[column.key]); }; // Column selector modal const renderColumnSelector = () => { return ( setShowColumnSelector(false)} footer={
} >
v === true)} indeterminate={ Object.values(visibleColumns).some((v) => v === true) && !Object.values(visibleColumns).every((v) => v === true) } onChange={(e) => handleSelectAll(e.target.checked)} > {t('全选')}
{allColumns.map((column) => { // Skip columns without title if (!column.title) { return null; } return (
handleColumnVisibilityChange(column.key, e.target.checked) } > {column.title}
); })}
); }; const removeRecord = (record) => { let newDataSource = [...channels]; if (record.id != null) { let idx = newDataSource.findIndex((data) => { if (data.children !== undefined) { for (let i = 0; i < data.children.length; i++) { if (data.children[i].id === record.id) { data.children.splice(i, 1); return false; } } } else { return data.id === record.id; } }); if (idx > -1) { newDataSource.splice(idx, 1); setChannels(newDataSource); } } }; const setChannelFormat = (channels, enableTagMode) => { let channelDates = []; let channelTags = {}; for (let i = 0; i < channels.length; i++) { channels[i].key = '' + channels[i].id; if (!enableTagMode) { channelDates.push(channels[i]); } else { let tag = channels[i].tag ? channels[i].tag : ''; // find from channelTags let tagIndex = channelTags[tag]; let tagChannelDates = undefined; if (tagIndex === undefined) { // not found, create a new tag channelTags[tag] = 1; tagChannelDates = { key: tag, id: tag, tag: tag, name: '标签:' + tag, group: '', used_quota: 0, response_time: 0, priority: -1, weight: -1, }; tagChannelDates.children = []; channelDates.push(tagChannelDates); } else { // found, add to the tag tagChannelDates = channelDates.find((item) => item.key === tag); } if (tagChannelDates.priority === -1) { tagChannelDates.priority = channels[i].priority; } else { if (tagChannelDates.priority !== channels[i].priority) { tagChannelDates.priority = ''; } } if (tagChannelDates.weight === -1) { tagChannelDates.weight = channels[i].weight; } else { if (tagChannelDates.weight !== channels[i].weight) { tagChannelDates.weight = ''; } } if (tagChannelDates.group === '') { tagChannelDates.group = channels[i].group; } else { let channelGroupsStr = channels[i].group; channelGroupsStr.split(',').forEach((item, index) => { if (tagChannelDates.group.indexOf(item) === -1) { // join tagChannelDates.group += ',' + item; } }); } tagChannelDates.children.push(channels[i]); if (channels[i].status === 1) { tagChannelDates.status = 1; } tagChannelDates.used_quota += channels[i].used_quota; tagChannelDates.response_time += channels[i].response_time; tagChannelDates.response_time = tagChannelDates.response_time / 2; } } setChannels(channelDates); }; const loadChannels = async ( page, pageSize, idSort, enableTagMode, typeKey = activeTypeKey, statusF, ) => { if (statusF === undefined) statusF = statusFilter; const { searchKeyword, searchGroup, searchModel } = getFormValues(); if (searchKeyword !== '' || searchGroup !== '' || searchModel !== '') { setLoading(true); await searchChannels(enableTagMode, typeKey, statusF, page, pageSize, idSort); setLoading(false); return; } const reqId = ++requestCounter.current; // 记录当前请求序号 setLoading(true); const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : ''; const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; const res = await API.get( `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`, ); if (res === undefined || reqId !== requestCounter.current) { return; } const { success, message, data } = res.data; if (success) { const { items, total, type_counts } = data; if (type_counts) { const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); setTypeCounts({ ...type_counts, all: sumAll }); } setChannelFormat(items, enableTagMode); setChannelCount(total); } else { showError(message); } setLoading(false); }; const copySelectedChannel = async (record) => { const channelToCopy = { ...record }; channelToCopy.name += t('_复制'); channelToCopy.created_time = null; channelToCopy.balance = 0; channelToCopy.used_quota = 0; delete channelToCopy.test_time; delete channelToCopy.response_time; if (!channelToCopy) { showError(t('渠道未找到,请刷新页面后重试。')); return; } try { const newChannel = { ...channelToCopy, id: undefined }; const response = await API.post('/api/channel/', newChannel); if (response.data.success) { showSuccess(t('渠道复制成功')); await refresh(); } else { showError(response.data.message); } } catch (error) { showError(t('渠道复制失败: ') + error.message); } }; const refresh = async () => { const { searchKeyword, searchGroup, searchModel } = getFormValues(); if (searchKeyword === '' && searchGroup === '' && searchModel === '') { await loadChannels(activePage, pageSize, idSort, enableTagMode); } else { await searchChannels(enableTagMode, activeTypeKey, statusFilter, activePage, pageSize, idSort); } }; useEffect(() => { // console.log('default effect') const localIdSort = localStorage.getItem('id-sort') === 'true'; const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true'; const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true'; setIdSort(localIdSort); setPageSize(localPageSize); setEnableTagMode(localEnableTagMode); setEnableBatchDelete(localEnableBatchDelete); loadChannels(1, localPageSize, localIdSort, localEnableTagMode) .then() .catch((reason) => { showError(reason); }); fetchGroups().then(); loadChannelModels().then(); }, []); const manageChannel = async (id, action, record, value) => { let data = { id }; let res; switch (action) { case 'delete': res = await API.delete(`/api/channel/${id}/`); break; case 'enable': data.status = 1; res = await API.put('/api/channel/', data); break; case 'disable': data.status = 2; res = await API.put('/api/channel/', data); break; case 'priority': if (value === '') { return; } data.priority = parseInt(value); res = await API.put('/api/channel/', data); break; case 'weight': if (value === '') { return; } data.weight = parseInt(value); if (data.weight < 0) { data.weight = 0; } res = await API.put('/api/channel/', data); break; case 'enable_all': data.channel_info = record.channel_info; data.channel_info.multi_key_status_list = {}; res = await API.put('/api/channel/', data); break; } const { success, message } = res.data; if (success) { showSuccess(t('操作成功完成!')); let channel = res.data.data; let newChannels = [...channels]; if (action === 'delete') { } else { record.status = channel.status; } setChannels(newChannels); } else { showError(message); } }; const manageTag = async (tag, action) => { console.log(tag, action); let res; switch (action) { case 'enable': res = await API.post('/api/channel/tag/enabled', { tag: tag, }); break; case 'disable': res = await API.post('/api/channel/tag/disabled', { tag: tag, }); break; } const { success, message } = res.data; if (success) { showSuccess('操作成功完成!'); let newChannels = [...channels]; for (let i = 0; i < newChannels.length; i++) { if (newChannels[i].tag === tag) { let status = action === 'enable' ? 1 : 2; newChannels[i]?.children?.forEach((channel) => { channel.status = status; }); newChannels[i].status = status; } } setChannels(newChannels); } else { showError(message); } }; // 获取表单值的辅助函数 const getFormValues = () => { const formValues = formApi ? formApi.getValues() : {}; return { searchKeyword: formValues.searchKeyword || '', searchGroup: formValues.searchGroup || '', searchModel: formValues.searchModel || '', }; }; const searchChannels = async ( enableTagMode, typeKey = activeTypeKey, statusF = statusFilter, page = 1, pageSz = pageSize, sortFlag = idSort, ) => { const { searchKeyword, searchGroup, searchModel } = getFormValues(); setSearching(true); try { if (searchKeyword === '' && searchGroup === '' && searchModel === '') { await loadChannels(page, pageSz, sortFlag, enableTagMode, typeKey, statusF); return; } const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : ''; const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; const res = await API.get( `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`, ); const { success, message, data } = res.data; if (success) { const { items = [], total = 0, type_counts = {} } = data; const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); setTypeCounts({ ...type_counts, all: sumAll }); setChannelFormat(items, enableTagMode); setChannelCount(total); setActivePage(page); } else { showError(message); } } finally { setSearching(false); } }; const updateChannelProperty = (channelId, updateFn) => { // Create a new copy of channels array const newChannels = [...channels]; let updated = false; // Find and update the correct channel newChannels.forEach((channel) => { if (channel.children !== undefined) { // If this is a tag group, search in its children channel.children.forEach((child) => { if (child.id === channelId) { updateFn(child); updated = true; } }); } else if (channel.id === channelId) { // Direct channel match updateFn(channel); updated = true; } }); // Only update state if we actually modified a channel if (updated) { setChannels(newChannels); } }; const processTestQueue = async () => { if (!isProcessingQueue || testQueue.length === 0) return; 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])); const res = await API.get(`/api/channel/test/${channel.id}?model=${model}`); const { success, message, time } = res.data; setModelTestResults(prev => ({ ...prev, [`${channel.id}-${model}`]: { success, time } })); if (success) { updateChannelProperty(channel.id, (ch) => { ch.response_time = time * 1000; ch.test_time = Date.now() / 1000; }); if (!model) { showInfo( t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。') .replace('${name}', channel.name) .replace('${time.toFixed(2)}', time.toFixed(2)), ); } } else { showError(message); } } catch (error) { showError(error.message); } finally { setTestingModels(prev => { const newSet = new Set(prev); newSet.delete(model); return newSet; }); } // 移除已处理的测试 setTestQueue(prev => prev.slice(1)); }; // 监听队列变化 useEffect(() => { if (testQueue.length > 0 && isProcessingQueue) { processTestQueue(); } else if (testQueue.length === 0 && isProcessingQueue) { setIsProcessingQueue(false); setIsBatchTesting(false); } }, [testQueue, isProcessingQueue]); const testChannel = async (record, model) => { setTestQueue(prev => [...prev, { channel: record, model }]); if (!isProcessingQueue) { setIsProcessingQueue(true); } }; const batchTestModels = async () => { if (!currentTestChannel) return; setIsBatchTesting(true); // 重置分页到第一页 setModelTablePage(1); const filteredModels = currentTestChannel.models .split(',') .filter((model) => model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), ); setTestQueue( filteredModels.map((model, idx) => ({ channel: currentTestChannel, model, indexInFiltered: idx, // 记录在过滤列表中的顺序 })), ); setIsProcessingQueue(true); }; const handleCloseModal = () => { if (isBatchTesting) { // 清空测试队列来停止测试 setTestQueue([]); setIsProcessingQueue(false); setIsBatchTesting(false); showSuccess(t('已停止测试')); } else { setShowModelTestModal(false); setModelSearchKeyword(''); setSelectedModelKeys([]); setModelTablePage(1); } }; const channelTypeCounts = useMemo(() => { if (Object.keys(typeCounts).length > 0) return typeCounts; // fallback 本地计算 const counts = { all: channels.length }; channels.forEach((channel) => { const collect = (ch) => { const type = ch.type; counts[type] = (counts[type] || 0) + 1; }; if (channel.children !== undefined) { channel.children.forEach(collect); } else { collect(channel); } }); return counts; }, [typeCounts, channels]); const availableTypeKeys = useMemo(() => { const keys = ['all']; Object.entries(channelTypeCounts).forEach(([k, v]) => { if (k !== 'all' && v > 0) keys.push(String(k)); }); return keys; }, [channelTypeCounts]); const renderTypeTabs = () => { if (enableTagMode) return null; return ( { setActiveTypeKey(key); setActivePage(1); loadChannels(1, pageSize, idSort, enableTagMode, key); }} className="mb-4" > {t('全部')} {channelTypeCounts['all'] || 0} } /> {CHANNEL_OPTIONS.filter((opt) => availableTypeKeys.includes(String(opt.value))).map((option) => { const key = String(option.value); const count = channelTypeCounts[option.value] || 0; return ( {getChannelIcon(option.value)} {option.label} {count} } /> ); })} ); }; let pageData = channels; const handlePageChange = (page) => { const { searchKeyword, searchGroup, searchModel } = getFormValues(); setActivePage(page); if (searchKeyword === '' && searchGroup === '' && searchModel === '') { loadChannels(page, pageSize, idSort, enableTagMode).then(() => { }); } else { searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort); } }; const handlePageSizeChange = async (size) => { localStorage.setItem('page-size', size + ''); setPageSize(size); setActivePage(1); const { searchKeyword, searchGroup, searchModel } = getFormValues(); if (searchKeyword === '' && searchGroup === '' && searchModel === '') { loadChannels(1, size, idSort, enableTagMode) .then() .catch((reason) => { showError(reason); }); } else { searchChannels(enableTagMode, activeTypeKey, statusFilter, 1, size, idSort); } }; const fetchGroups = async () => { try { let res = await API.get(`/api/group/`); if (res === undefined) { return; } setGroupOptions( res.data.data.map((group) => ({ label: group, value: group, })), ); } catch (error) { showError(error.message); } }; const submitTagEdit = async (type, data) => { switch (type) { case 'priority': if (data.priority === undefined || data.priority === '') { showInfo('优先级必须是整数!'); return; } data.priority = parseInt(data.priority); break; case 'weight': if ( data.weight === undefined || data.weight < 0 || data.weight === '' ) { showInfo('权重必须是非负整数!'); return; } data.weight = parseInt(data.weight); break; } try { const res = await API.put('/api/channel/tag', data); if (res?.data?.success) { showSuccess('更新成功!'); await refresh(); } } catch (error) { showError(error); } }; const closeEdit = () => { setShowEdit(false); }; const handleRow = (record, index) => { if (record.status !== 1) { return { style: { background: 'var(--semi-color-disabled-border)', }, }; } else { return {}; } }; const batchSetChannelTag = async () => { if (selectedChannels.length === 0) { showError(t('请先选择要设置标签的渠道!')); return; } if (batchSetTagValue === '') { showError(t('标签不能为空!')); return; } let ids = selectedChannels.map((channel) => channel.id); const res = await API.post('/api/channel/batch/tag', { ids: ids, tag: batchSetTagValue === '' ? null : batchSetTagValue, }); if (res.data.success) { showSuccess( t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data), ); await refresh(); setShowBatchSetTag(false); } else { showError(res.data.message); } }; const testAllChannels = async () => { const res = await API.get(`/api/channel/test`); const { success, message } = res.data; if (success) { showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。')); } else { showError(message); } }; const deleteAllDisabledChannels = async () => { const res = await API.delete(`/api/channel/disabled`); const { success, message, data } = res.data; if (success) { showSuccess( t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data), ); await refresh(); } else { showError(message); } }; const updateAllChannelsBalance = async () => { const res = await API.get(`/api/channel/update_balance`); const { success, message } = res.data; if (success) { showInfo(t('已更新完毕所有已启用通道余额!')); } else { showError(message); } }; const updateChannelBalance = async (record) => { const res = await API.get(`/api/channel/update_balance/${record.id}/`); const { success, message, balance } = res.data; if (success) { updateChannelProperty(record.id, (channel) => { channel.balance = balance; channel.balance_updated_time = Date.now() / 1000; }); showInfo( t('通道 ${name} 余额更新成功!').replace('${name}', record.name), ); } else { showError(message); } }; const batchDeleteChannels = async () => { if (selectedChannels.length === 0) { showError(t('请先选择要删除的通道!')); return; } setLoading(true); let ids = []; selectedChannels.forEach((channel) => { ids.push(channel.id); }); const res = await API.post(`/api/channel/batch`, { ids: ids }); const { success, message, data } = res.data; if (success) { showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data)); await refresh(); } else { showError(message); } setLoading(false); }; const fixChannelsAbilities = async () => { const res = await API.post(`/api/channel/fix`); const { success, message, data } = res.data; if (success) { showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails)); await refresh(); } else { showError(message); } }; const renderHeader = () => (
{renderTypeTabs()}
} >
{t('使用ID排序')} { localStorage.setItem('id-sort', v + ''); setIdSort(v); const { searchKeyword, searchGroup, searchModel } = getFormValues(); if (searchKeyword === '' && searchGroup === '' && searchModel === '') { loadChannels(activePage, pageSize, v, enableTagMode); } else { searchChannels(enableTagMode, activeTypeKey, statusFilter, activePage, pageSize, v); } }} />
{t('开启批量操作')} { localStorage.setItem('enable-batch-delete', v + ''); setEnableBatchDelete(v); }} />
{t('标签聚合模式')} { localStorage.setItem('enable-tag-mode', v + ''); setEnableTagMode(v); setActivePage(1); loadChannels(1, pageSize, idSort, v); }} />
{/* 状态筛选器 */}
{t('状态筛选')}
setFormApi(api)} onSubmit={() => searchChannels(enableTagMode)} allowEmpty={true} autoComplete="off" layout="horizontal" trigger="change" stopValidateWithError={false} className="flex flex-col md:flex-row items-center gap-4 w-full" >
} placeholder={t('渠道ID,名称,密钥,API地址')} showClear pure />
} placeholder={t('模型关键字')} showClear pure />
{ // 延迟执行搜索,让表单值先更新 setTimeout(() => { searchChannels(enableTagMode); }, 0); }} />
); return ( <> {renderColumnSelector()} setShowEditTag(false)} refresh={refresh} /> rest) : getVisibleColumns()} dataSource={pageData} scroll={compactMode ? undefined : { x: 'max-content' }} pagination={{ currentPage: activePage, pageSize: pageSize, total: channelCount, pageSizeOpts: [10, 20, 50, 100], showSizeChanger: true, formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', { start: page.currentStart, end: page.currentEnd, total: channelCount, }), onPageSizeChange: (size) => { handlePageSizeChange(size); }, onPageChange: handlePageChange, }} expandAllRows={false} onRow={handleRow} rowSelection={ enableBatchDelete ? { onChange: (selectedRowKeys, selectedRows) => { setSelectedChannels(selectedRows); }, } : null } empty={ } darkModeImage={} description={t('搜索无结果')} style={{ padding: 30 }} /> } className="rounded-xl overflow-hidden" size="middle" loading={loading || searching} /> {/* 批量设置标签模态框 */} setShowBatchSetTag(false)} maskClosable={false} centered={true} size="small" className="!rounded-lg" >
{t('请输入要设置的标签名称')}
setBatchSetTagValue(v)} />
{t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
{/* 模型测试弹窗 */}
{currentTestChannel.name} {t('渠道的模型测试')} {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
{/* 搜索与操作按钮 */}
{ setModelSearchKeyword(v); setModelTablePage(1); }} className="!w-full" prefix={} showClear />
) } visible={showModelTestModal && currentTestChannel !== null} onCancel={handleCloseModal} footer={
{isBatchTesting ? ( ) : ( )}
} maskClosable={!isBatchTesting} className="!rounded-lg" size={isMobile() ? 'full-width' : 'large'} >
{currentTestChannel && (
(
{text}
) }, { title: t('状态'), dataIndex: 'status', render: (text, record) => { const testResult = modelTestResults[`${currentTestChannel.id}-${record.model}`]; const isTesting = testingModels.has(record.model); if (isTesting) { return ( {t('测试中')} ); } if (!testResult) { return ( {t('未开始')} ); } return (
{testResult.success ? t('成功') : t('失败')} {testResult.success && ( {t('请求时长: ${time}s').replace('${time}', testResult.time.toFixed(2))} )}
); } }, { title: '', dataIndex: 'operate', render: (text, record) => { const isTesting = testingModels.has(record.model); return ( ); } } ]} 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, })); })()} 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), }} /> )} ); }; export default ChannelsTable;