Spaces:
Build error
Build error
| /* | |
| Copyright (C) 2025 QuantumNous | |
| This program is free software: you can redistribute it and/or modify | |
| it under the terms of the GNU Affero General Public License as | |
| published by the Free Software Foundation, either version 3 of the | |
| License, or (at your option) any later version. | |
| This program is distributed in the hope that it will be useful, | |
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| GNU Affero General Public License for more details. | |
| You should have received a copy of the GNU Affero General Public License | |
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| For commercial licensing, please contact support@quantumnous.com | |
| */ | |
| import React from 'react'; | |
| import { | |
| Button, | |
| Dropdown, | |
| InputNumber, | |
| Modal, | |
| Space, | |
| SplitButtonGroup, | |
| Tag, | |
| Tooltip, | |
| Typography, | |
| } from '@douyinfe/semi-ui'; | |
| import { | |
| timestamp2string, | |
| renderGroup, | |
| renderQuota, | |
| getChannelIcon, | |
| renderQuotaWithAmount, | |
| showSuccess, | |
| showError, | |
| } from '../../../helpers'; | |
| import { CHANNEL_OPTIONS } from '../../../constants'; | |
| import { IconTreeTriangleDown, IconMore } from '@douyinfe/semi-icons'; | |
| import { FaRandom } from 'react-icons/fa'; | |
| // Render functions | |
| const renderType = (type, channelInfo = undefined, t) => { | |
| let 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' ? ( | |
| <div className='flex items-center gap-1'> | |
| <FaRandom className='text-blue-500' /> | |
| {icon} | |
| </div> | |
| ) : ( | |
| <div className='flex items-center gap-1'> | |
| <IconTreeTriangleDown className='text-blue-500' /> | |
| {icon} | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <Tag color={type2label[type]?.color} shape='circle' prefixIcon={icon}> | |
| {type2label[type]?.label} | |
| </Tag> | |
| ); | |
| }; | |
| const renderTagType = (t) => { | |
| return ( | |
| <Tag color='light-blue' shape='circle' type='light'> | |
| {t('标签聚合')} | |
| </Tag> | |
| ); | |
| }; | |
| const renderStatus = (status, channelInfo = undefined, t) => { | |
| if (channelInfo) { | |
| if (channelInfo.is_multi_key) { | |
| let keySize = channelInfo.multi_key_size; | |
| let enabledKeySize = keySize; | |
| if (channelInfo.multi_key_status_list) { | |
| enabledKeySize = | |
| keySize - Object.keys(channelInfo.multi_key_status_list).length; | |
| } | |
| return renderMultiKeyStatus(status, keySize, enabledKeySize, t); | |
| } | |
| } | |
| switch (status) { | |
| case 1: | |
| return ( | |
| <Tag color='green' shape='circle'> | |
| {t('已启用')} | |
| </Tag> | |
| ); | |
| case 2: | |
| return ( | |
| <Tag color='red' shape='circle'> | |
| {t('已禁用')} | |
| </Tag> | |
| ); | |
| case 3: | |
| return ( | |
| <Tag color='yellow' shape='circle'> | |
| {t('自动禁用')} | |
| </Tag> | |
| ); | |
| default: | |
| return ( | |
| <Tag color='grey' shape='circle'> | |
| {t('未知状态')} | |
| </Tag> | |
| ); | |
| } | |
| }; | |
| const renderMultiKeyStatus = (status, keySize, enabledKeySize, t) => { | |
| switch (status) { | |
| case 1: | |
| return ( | |
| <Tag color='green' shape='circle'> | |
| {t('已启用')} {enabledKeySize}/{keySize} | |
| </Tag> | |
| ); | |
| case 2: | |
| return ( | |
| <Tag color='red' shape='circle'> | |
| {t('已禁用')} {enabledKeySize}/{keySize} | |
| </Tag> | |
| ); | |
| case 3: | |
| return ( | |
| <Tag color='yellow' shape='circle'> | |
| {t('自动禁用')} {enabledKeySize}/{keySize} | |
| </Tag> | |
| ); | |
| default: | |
| return ( | |
| <Tag color='grey' shape='circle'> | |
| {t('未知状态')} {enabledKeySize}/{keySize} | |
| </Tag> | |
| ); | |
| } | |
| }; | |
| const renderResponseTime = (responseTime, t) => { | |
| let time = responseTime / 1000; | |
| time = time.toFixed(2) + t(' 秒'); | |
| if (responseTime === 0) { | |
| return ( | |
| <Tag color='grey' shape='circle'> | |
| {t('未测试')} | |
| </Tag> | |
| ); | |
| } else if (responseTime <= 1000) { | |
| return ( | |
| <Tag color='green' shape='circle'> | |
| {time} | |
| </Tag> | |
| ); | |
| } else if (responseTime <= 3000) { | |
| return ( | |
| <Tag color='lime' shape='circle'> | |
| {time} | |
| </Tag> | |
| ); | |
| } else if (responseTime <= 5000) { | |
| return ( | |
| <Tag color='yellow' shape='circle'> | |
| {time} | |
| </Tag> | |
| ); | |
| } else { | |
| return ( | |
| <Tag color='red' shape='circle'> | |
| {time} | |
| </Tag> | |
| ); | |
| } | |
| }; | |
| export const getChannelsColumns = ({ | |
| t, | |
| COLUMN_KEYS, | |
| updateChannelBalance, | |
| manageChannel, | |
| manageTag, | |
| submitTagEdit, | |
| testChannel, | |
| setCurrentTestChannel, | |
| setShowModelTestModal, | |
| setEditingChannel, | |
| setShowEdit, | |
| setShowEditTag, | |
| setEditingTag, | |
| copySelectedChannel, | |
| refresh, | |
| activePage, | |
| channels, | |
| setShowMultiKeyManageModal, | |
| setCurrentMultiKeyChannel, | |
| }) => { | |
| return [ | |
| { | |
| key: COLUMN_KEYS.ID, | |
| title: t('ID'), | |
| dataIndex: 'id', | |
| }, | |
| { | |
| key: COLUMN_KEYS.NAME, | |
| title: t('名称'), | |
| dataIndex: 'name', | |
| render: (text, record, index) => { | |
| if (record.remark && record.remark.trim() !== '') { | |
| return ( | |
| <Tooltip | |
| content={ | |
| <div className='flex flex-col gap-2 max-w-xs'> | |
| <div className='text-sm'>{record.remark}</div> | |
| <Button | |
| size='small' | |
| type='primary' | |
| theme='outline' | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| navigator.clipboard | |
| .writeText(record.remark) | |
| .then(() => { | |
| showSuccess(t('复制成功')); | |
| }) | |
| .catch(() => { | |
| showError(t('复制失败')); | |
| }); | |
| }} | |
| > | |
| {t('复制')} | |
| </Button> | |
| </div> | |
| } | |
| trigger='hover' | |
| position='topLeft' | |
| > | |
| <span>{text}</span> | |
| </Tooltip> | |
| ); | |
| } | |
| return text; | |
| }, | |
| }, | |
| { | |
| key: COLUMN_KEYS.GROUP, | |
| title: t('分组'), | |
| dataIndex: 'group', | |
| render: (text, record, index) => ( | |
| <div> | |
| <Space spacing={2}> | |
| {text | |
| ?.split(',') | |
| .sort((a, b) => { | |
| if (a === 'default') return -1; | |
| if (b === 'default') return 1; | |
| return a.localeCompare(b); | |
| }) | |
| .map((item, index) => renderGroup(item))} | |
| </Space> | |
| </div> | |
| ), | |
| }, | |
| { | |
| 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, t)}</>; | |
| } | |
| } | |
| return <>{renderType(text, undefined, t)}</>; | |
| } else { | |
| return <>{renderTagType(t)}</>; | |
| } | |
| }, | |
| }, | |
| { | |
| 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 ( | |
| <div> | |
| <Tooltip | |
| content={ | |
| t('原因:') + reason + t(',时间:') + timestamp2string(time) | |
| } | |
| > | |
| {renderStatus(text, record.channel_info, t)} | |
| </Tooltip> | |
| </div> | |
| ); | |
| } else { | |
| return renderStatus(text, record.channel_info, t); | |
| } | |
| }, | |
| }, | |
| { | |
| key: COLUMN_KEYS.RESPONSE_TIME, | |
| title: t('响应时间'), | |
| dataIndex: 'response_time', | |
| render: (text, record, index) => <div>{renderResponseTime(text, t)}</div>, | |
| }, | |
| { | |
| key: COLUMN_KEYS.BALANCE, | |
| title: t('已用/剩余'), | |
| dataIndex: 'expired_time', | |
| render: (text, record, index) => { | |
| if (record.children === undefined) { | |
| return ( | |
| <div> | |
| <Space spacing={1}> | |
| <Tooltip content={t('已用额度')}> | |
| <Tag color='white' type='ghost' shape='circle'> | |
| {renderQuota(record.used_quota)} | |
| </Tag> | |
| </Tooltip> | |
| <Tooltip | |
| content={t('剩余额度$') + record.balance + t(',点击更新')} | |
| > | |
| <Tag | |
| color='white' | |
| type='ghost' | |
| shape='circle' | |
| onClick={() => updateChannelBalance(record)} | |
| > | |
| {renderQuotaWithAmount(record.balance)} | |
| </Tag> | |
| </Tooltip> | |
| </Space> | |
| </div> | |
| ); | |
| } else { | |
| return ( | |
| <Tooltip content={t('已用额度')}> | |
| <Tag color='white' type='ghost' shape='circle'> | |
| {renderQuota(record.used_quota)} | |
| </Tag> | |
| </Tooltip> | |
| ); | |
| } | |
| }, | |
| }, | |
| { | |
| key: COLUMN_KEYS.PRIORITY, | |
| title: t('优先级'), | |
| dataIndex: 'priority', | |
| render: (text, record, index) => { | |
| if (record.children === undefined) { | |
| return ( | |
| <div> | |
| <InputNumber | |
| style={{ width: 70 }} | |
| name='priority' | |
| onBlur={(e) => { | |
| manageChannel(record.id, 'priority', record, e.target.value); | |
| }} | |
| keepFocus={true} | |
| innerButtons | |
| defaultValue={record.priority} | |
| min={-999} | |
| size='small' | |
| /> | |
| </div> | |
| ); | |
| } else { | |
| return ( | |
| <InputNumber | |
| style={{ width: 70 }} | |
| name='priority' | |
| keepFocus={true} | |
| onBlur={(e) => { | |
| 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 ( | |
| <div> | |
| <InputNumber | |
| style={{ width: 70 }} | |
| name='weight' | |
| onBlur={(e) => { | |
| manageChannel(record.id, 'weight', record, e.target.value); | |
| }} | |
| keepFocus={true} | |
| innerButtons | |
| defaultValue={record.weight} | |
| min={0} | |
| size='small' | |
| /> | |
| </div> | |
| ); | |
| } else { | |
| return ( | |
| <InputNumber | |
| style={{ width: 70 }} | |
| name='weight' | |
| keepFocus={true} | |
| onBlur={(e) => { | |
| 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: () => { | |
| (async () => { | |
| await manageChannel(record.id, 'delete', record); | |
| await refresh(); | |
| setTimeout(() => { | |
| if (channels.length === 0 && activePage > 1) { | |
| refresh(activePage - 1); | |
| } | |
| }, 100); | |
| })(); | |
| }, | |
| }); | |
| }, | |
| }, | |
| { | |
| node: 'item', | |
| name: t('复制'), | |
| type: 'tertiary', | |
| onClick: () => { | |
| Modal.confirm({ | |
| title: t('确定是否要复制此渠道?'), | |
| content: t('复制渠道的所有信息'), | |
| onOk: () => copySelectedChannel(record), | |
| }); | |
| }, | |
| }, | |
| ]; | |
| return ( | |
| <Space wrap> | |
| <SplitButtonGroup | |
| className='overflow-hidden' | |
| aria-label={t('测试单个渠道操作项目组')} | |
| > | |
| <Button | |
| size='small' | |
| type='tertiary' | |
| onClick={() => testChannel(record, '')} | |
| > | |
| {t('测试')} | |
| </Button> | |
| <Button | |
| size='small' | |
| type='tertiary' | |
| icon={<IconTreeTriangleDown />} | |
| onClick={() => { | |
| setCurrentTestChannel(record); | |
| setShowModelTestModal(true); | |
| }} | |
| /> | |
| </SplitButtonGroup> | |
| {record.status === 1 ? ( | |
| <Button | |
| type='danger' | |
| size='small' | |
| onClick={() => manageChannel(record.id, 'disable', record)} | |
| > | |
| {t('禁用')} | |
| </Button> | |
| ) : ( | |
| <Button | |
| size='small' | |
| onClick={() => manageChannel(record.id, 'enable', record)} | |
| > | |
| {t('启用')} | |
| </Button> | |
| )} | |
| {record.channel_info?.is_multi_key ? ( | |
| <SplitButtonGroup aria-label={t('多密钥渠道操作项目组')}> | |
| <Button | |
| type='tertiary' | |
| size='small' | |
| onClick={() => { | |
| setEditingChannel(record); | |
| setShowEdit(true); | |
| }} | |
| > | |
| {t('编辑')} | |
| </Button> | |
| <Dropdown | |
| trigger='click' | |
| position='bottomRight' | |
| menu={[ | |
| { | |
| node: 'item', | |
| name: t('多密钥管理'), | |
| onClick: () => { | |
| setCurrentMultiKeyChannel(record); | |
| setShowMultiKeyManageModal(true); | |
| }, | |
| }, | |
| ]} | |
| > | |
| <Button | |
| type='tertiary' | |
| size='small' | |
| icon={<IconTreeTriangleDown />} | |
| /> | |
| </Dropdown> | |
| </SplitButtonGroup> | |
| ) : ( | |
| <Button | |
| type='tertiary' | |
| size='small' | |
| onClick={() => { | |
| setEditingChannel(record); | |
| setShowEdit(true); | |
| }} | |
| > | |
| {t('编辑')} | |
| </Button> | |
| )} | |
| <Dropdown | |
| trigger='click' | |
| position='bottomRight' | |
| menu={moreMenuItems} | |
| > | |
| <Button icon={<IconMore />} type='tertiary' size='small' /> | |
| </Dropdown> | |
| </Space> | |
| ); | |
| } else { | |
| // 标签操作按钮 | |
| return ( | |
| <Space wrap> | |
| <Button | |
| type='tertiary' | |
| size='small' | |
| onClick={() => manageTag(record.key, 'enable')} | |
| > | |
| {t('启用全部')} | |
| </Button> | |
| <Button | |
| type='tertiary' | |
| size='small' | |
| onClick={() => manageTag(record.key, 'disable')} | |
| > | |
| {t('禁用全部')} | |
| </Button> | |
| <Button | |
| type='tertiary' | |
| size='small' | |
| onClick={() => { | |
| setShowEditTag(true); | |
| setEditingTag(record.key); | |
| }} | |
| > | |
| {t('编辑')} | |
| </Button> | |
| </Space> | |
| ); | |
| } | |
| }, | |
| }, | |
| ]; | |
| }; | |