import React, {useEffect, useState} from 'react'; import { API, isMobile, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; import {CHANNEL_OPTIONS, ITEMS_PER_PAGE} from '../constants'; import {renderGroup, renderNumber, renderNumberWithPoint, renderQuota, renderQuotaWithPrompt} from '../helpers/render'; import { Avatar, Tag, Table, Button, Popover, Form, Modal, Popconfirm, Space, Tooltip, Switch, Typography, InputNumber, Dropdown, SplitButtonGroup } from "@douyinfe/semi-ui"; import EditChannel from "../pages/Channel/EditChannel"; import {IconTreeTriangleDown} from "@douyinfe/semi-icons"; function renderTimestamp(timestamp) { return ( <> {timestamp2string(timestamp)} ); } let type2label = undefined; function 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, text: '未知类型', color: 'grey'}; } return {type2label[type]?.text}; } function renderBalance(type, balance) { switch (type) { case 1: // OpenAI return ${balance.toFixed(2)}; case 4: // CloseAI return ¥{balance.toFixed(2)}; case 8: // 自定义 return ${balance.toFixed(2)}; case 5: // OpenAI-SB return ¥{(balance / 10000).toFixed(2)}; case 10: // AI Proxy return {renderNumber(balance)}; case 12: // API2GPT return ¥{balance.toFixed(2)}; case 13: // AIGC2D return {renderNumber(balance)}; default: return 不支持; } } const ChannelsTable = () => { const columns = [ // { // title: '', // dataIndex: 'checkbox', // className: 'checkbox', // }, { title: 'ID', dataIndex: 'id', }, { title: '名称', dataIndex: 'name', }, { title: '分组', dataIndex: 'group', render: (text, record, index) => { return (
{ text.split(',').map((item, index) => { return (renderGroup(item)) }) }
); }, }, { title: '类型', dataIndex: 'type', render: (text, record, index) => { return (
{renderType(text)}
); }, }, { title: '状态', dataIndex: 'status', render: (text, record, index) => { return (
{renderStatus(text)}
); }, }, { title: '响应时间', dataIndex: 'response_time', render: (text, record, index) => { return (
{renderResponseTime(text)}
); }, }, { title: '已用/剩余', dataIndex: 'expired_time', render: (text, record, index) => { return (
{renderQuota(record.used_quota)} {updateChannelBalance(record)}}>${renderNumberWithPoint(record.balance)}
); }, }, { title: '优先级', dataIndex: 'priority', render: (text, record, index) => { return (
{ manageChannel(record.id, 'priority', record, value); }} defaultValue={record.priority} min={-999} />
); }, }, { title: '权重', dataIndex: 'weight', render: (text, record, index) => { return (
{ manageChannel(record.id, 'weight', record, value); }} defaultValue={record.weight} min={0} />
); }, }, { title: '', dataIndex: 'operate', render: (text, record, index) => (
{/**/} { manageChannel(record.id, 'delete', record).then( () => { removeRecord(record.id); } ) }} > { record.status === 1 ? : }
), }, ]; const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [idSort, setIdSort] = useState(false); const [searchKeyword, setSearchKeyword] = useState(''); const [searchGroup, setSearchGroup] = useState(''); const [searching, setSearching] = useState(false); const [updatingBalance, setUpdatingBalance] = useState(false); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [showPrompt, setShowPrompt] = useState(shouldShowPrompt("channel-test")); 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 [selectedChannels, setSelectedChannels] = useState([]); const removeRecord = id => { let newDataSource = [...channels]; if (id != null) { let idx = newDataSource.findIndex(data => data.id === id); if (idx > -1) { newDataSource.splice(idx, 1); setChannels(newDataSource); } } }; const setChannelFormat = (channels) => { for (let i = 0; i < channels.length; i++) { channels[i].key = '' + channels[i].id; let test_models = [] channels[i].models.split(',').forEach((item, index) => { test_models.push({ node: 'item', name: item, onClick: () => { testChannel(channels[i], item) } }) }) channels[i].test_models = test_models } // data.key = '' + data.id setChannels(channels); if (channels.length >= pageSize) { setChannelCount(channels.length + pageSize); } else { setChannelCount(channels.length); } } const loadChannels = async (startIdx, pageSize, idSort) => { setLoading(true); const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`); const {success, message, data} = res.data; if (success) { if (startIdx === 0) { setChannelFormat(data); } else { let newChannels = [...channels]; newChannels.splice(startIdx * pageSize, data.length, ...data); setChannelFormat(newChannels); } } else { showError(message); } setLoading(false); }; const refresh = async () => { await loadChannels(activePage - 1, pageSize, idSort); }; useEffect(() => { // console.log('default effect') const localIdSort = localStorage.getItem('id-sort') === 'true'; setIdSort(localIdSort) loadChannels(0, pageSize, localIdSort) .then() .catch((reason) => { showError(reason); }); fetchGroups().then(); }, []); // useEffect(() => { // console.log('search effect') // searchChannels() // }, [searchGroup]); // useEffect(() => { // localStorage.setItem('id-sort', idSort + ''); // refresh() // }, [idSort]); 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; } const {success, message} = res.data; if (success) { showSuccess('操作成功完成!'); let channel = res.data.data; let newChannels = [...channels]; if (action === 'delete') { } else { record.status = channel.status; } setChannels(newChannels); } else { showError(message); } }; const renderStatus = (status) => { switch (status) { case 1: return 已启用; case 2: return ( 已禁用 ); case 3: return ( 自动禁用 ); default: return ( 未知状态 ); } }; const renderResponseTime = (responseTime) => { let time = responseTime / 1000; time = time.toFixed(2) + ' 秒'; if (responseTime === 0) { return 未测试; } else if (responseTime <= 1000) { return {time}; } else if (responseTime <= 3000) { return {time}; } else if (responseTime <= 5000) { return {time}; } else { return {time}; } }; const searchChannels = async (searchKeyword, searchGroup) => { if (searchKeyword === '' && searchGroup === '') { // if keyword is blank, load files instead. await loadChannels(0, pageSize, idSort); setActivePage(1); return; } setSearching(true); const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}`); const {success, message, data} = res.data; if (success) { setChannels(data); setActivePage(1); } else { showError(message); } setSearching(false); }; const testChannel = async (record, model) => { const res = await API.get(`/api/channel/test/${record.id}?model=${model}`); const {success, message, time} = res.data; if (success) { record.response_time = time * 1000; record.test_time = Date.now() / 1000; showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); } else { showError(message); } }; const testAllChannels = async () => { const res = await API.get(`/api/channel/test`); const {success, message} = res.data; if (success) { showInfo('已成功开始测试所有通道,请刷新页面查看结果。'); } else { showError(message); } }; const deleteAllDisabledChannels = async () => { const res = await API.delete(`/api/channel/disabled`); const {success, message, data} = res.data; if (success) { showSuccess(`已删除所有禁用渠道,共计 ${data} 个`); await refresh(); } 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) { record.balance = balance; record.balance_updated_time = Date.now() / 1000; showInfo(`通道 ${record.name} 余额更新成功!`); } else { showError(message); } }; const updateAllChannelsBalance = async () => { setUpdatingBalance(true); const res = await API.get(`/api/channel/update_balance`); const {success, message} = res.data; if (success) { showInfo('已更新完毕所有已启用通道余额!'); } else { showError(message); } setUpdatingBalance(false); }; const batchDeleteChannels = async () => { if (selectedChannels.length === 0) { showError('请先选择要删除的通道!'); 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(`已删除 ${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(`已修复 ${data} 个通道!`); await refresh(); } else { showError(message); } } const sortChannel = (key) => { if (channels.length === 0) return; setLoading(true); let sortedChannels = [...channels]; if (typeof sortedChannels[0][key] === 'string') { sortedChannels.sort((a, b) => { return ('' + a[key]).localeCompare(b[key]); }); } else { sortedChannels.sort((a, b) => { if (a[key] === b[key]) return 0; if (a[key] > b[key]) return -1; if (a[key] < b[key]) return 1; }); } if (sortedChannels[0].id === channels[0].id) { sortedChannels.reverse(); } setChannels(sortedChannels); setLoading(false); }; let pageData = channels.slice((activePage - 1) * pageSize, activePage * pageSize); const handlePageChange = page => { setActivePage(page); if (page === Math.ceil(channels.length / pageSize) + 1) { // In this case we have to load more data and then append them. loadChannels(page - 1, pageSize, idSort).then(r => { }); } }; const handlePageSizeChange = async(size) => { setPageSize(size) setActivePage(1) loadChannels(0, size, idSort) .then() .catch((reason) => { showError(reason); }) }; const fetchGroups = async () => { try { let res = await API.get(`/api/group/`); // add 'all' option // res.data.data.unshift('all'); setGroupOptions(res.data.data.map((group) => ({ label: group, value: group, }))); } catch (error) { showError(error.message); } }; const closeEdit = () => { setShowEdit(false); } const handleRow = (record, index) => { if (record.status !== 1) { return { style: { background: 'var(--semi-color-disabled-border)', }, }; } else { return {}; } }; return ( <>
{searchChannels(searchKeyword, searchGroup)}} labelPosition='left'>
{ setSearchKeyword(v.trim()) }} /> { setSearchGroup(v) searchChannels(searchKeyword, v) }}/>
使用ID排序 { localStorage.setItem('id-sort', v + '') setIdSort(v) loadChannels(0, pageSize, v) .then() .catch((reason) => { showError(reason); }) }}>
'', onPageSizeChange: (size) => { handlePageSizeChange(size).then() }, onPageChange: handlePageChange, }} loading={loading} onRow={handleRow} rowSelection={ enableBatchDelete ? { onChange: (selectedRowKeys, selectedRows) => { // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); setSelectedChannels(selectedRows); }, } : null }/>
{/*
*/} {/*
*/}
开启批量删除 { setEnableBatchDelete(v) }}>
); }; export default ChannelsTable;