import React, { useEffect, useState } from 'react'; import { API, showError, showInfo, showSuccess, timestamp2string, renderGroup, renderNumberWithPoint, renderQuota, getChannelIcon } from '../../helpers/index.js'; import { CheckCircle, XCircle, AlertCircle, HelpCircle, TestTube, Zap, Timer, Clock, AlertTriangle, Coins, Tags } from 'lucide-react'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js'; import { Button, Divider, Dropdown, Empty, Input, InputNumber, Modal, Space, SplitButtonGroup, Switch, Table, Tag, Tooltip, Typography, Card, Form } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import EditChannel from '../../pages/Channel/EditChannel.js'; import { IconTreeTriangleDown, IconFilter, IconPlus, IconRefresh, IconSetting, IconSearch, IconEdit, IconDelete, IconStop, IconPlay, IconMore, IconCopy, IconSmallTriangleRight } from '@douyinfe/semi-icons'; import { loadChannelModels } from '../../helpers/index.js'; import EditTagModal from '../../pages/Channel/EditTagModal.js'; import { useTranslation } from 'react-i18next'; const ChannelsTable = () => { const { t } = useTranslation(); let type2label = undefined; const renderType = (type) => { 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' }; } return ( {type2label[type]?.label} ); }; const renderTagType = () => { return ( } size='large' shape='circle' type='light' > {t('标签聚合')} ); }; const renderStatus = (status) => { switch (status) { case 1: return ( }> {t('已启用')} ); case 2: return ( }> {t('已禁用')} ); case 3: return ( }> {t('自动禁用')} ); default: return ( }> {t('未知状态')} ); } }; 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 all columns const columns = [ { title: t('ID'), dataIndex: 'id', }, { title: t('名称'), dataIndex: 'name', }, { 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))}
), }, { title: t('类型'), dataIndex: 'type', render: (text, record, index) => { if (record.children === undefined) { return <>{renderType(text)}; } else { return <>{renderTagType()}; } }, }, { 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)}
); } else { return renderStatus(text); } }, }, { title: t('响应时间'), dataIndex: 'response_time', render: (text, record, index) => (
{renderResponseTime(text)}
), }, { title: t('已用/剩余'), dataIndex: 'expired_time', render: (text, record, index) => { if (record.children === undefined) { return (
}> {renderQuota(record.used_quota)} } onClick={() => updateChannelBalance(record)} > ${renderNumberWithPoint(record.balance)}
); } else { return ( }> {renderQuota(record.used_quota)} ); } }, }, { 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" /> ); } }, }, { 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" /> ); } }, }, { title: '', dataIndex: 'operate', fixed: 'right', render: (text, record, index) => { if (record.children === undefined) { // 创建更多操作的下拉菜单项 const moreMenuItems = [ { node: 'item', name: t('删除'), icon: , type: 'danger', onClick: () => { Modal.confirm({ title: t('确定是否要删除此渠道?'), content: t('此修改将不可逆'), onOk: () => { manageChannel(record.id, 'delete', record).then(() => { removeRecord(record); }); }, }); }, }, { node: 'item', name: t('复制'), icon: , type: 'primary', onClick: () => { Modal.confirm({ title: t('确定是否要复制此渠道?'), content: t('复制渠道的所有信息'), onOk: () => copySelectedChannel(record), }); }, }, ]; return ( ) : ( )} } >
{t('使用ID排序')} { localStorage.setItem('id-sort', v + ''); setIdSort(v); loadChannels(0, pageSize, v, enableTagMode); }} />
{t('开启批量操作')} { localStorage.setItem('enable-batch-delete', v + ''); setEnableBatchDelete(v); }} />
{t('标签聚合模式')} { localStorage.setItem('enable-tag-mode', v + ''); setEnableTagMode(v); loadChannels(0, pageSize, idSort, v); }} />
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地址 ...')} className="!rounded-full" showClear pure />
} placeholder={t('模型关键字')} className="!rounded-full" showClear pure />
{ // 延迟执行搜索,让表单值先更新 setTimeout(() => { searchChannels(enableTagMode); }, 0); }} />
); return ( <> setShowEditTag(false)} refresh={refresh} /> t('第 {{start}} - {{end}} 条,共 {{total}} 条', { start: page.currentStart, end: page.currentEnd, total: channels.length, }), 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} /> {/* 批量设置标签模态框 */} setShowBatchSetTag(false)} maskClosable={false} centered={true} size="small" className="!rounded-lg" >
{t('请输入要设置的标签名称')}
setBatchSetTagValue(v)} size='large' className="!rounded-full" />
{t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
{/* 模型测试弹窗 */} {currentTestChannel.name} {t('渠道的模型测试')} {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')} ) } visible={showModelTestModal && currentTestChannel !== null} onCancel={handleCloseModal} footer={
{isBatchTesting ? ( ) : ( )}
} maskClosable={!isBatchTesting} centered={true} className="!rounded-lg" size="large" >
{currentTestChannel && (
setModelSearchKeyword(v)} className="w-64 !rounded-full" prefix={} showClear />
(
{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={currentTestChannel.models .split(',') .filter((model) => model.toLowerCase().includes(modelSearchKeyword.toLowerCase()) ) .map((model) => ({ model, key: model }))} pagination={false} size="middle" /> )} ); }; export default ChannelsTable;