| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import React, { |
| | useState, |
| | useEffect, |
| | forwardRef, |
| | useImperativeHandle, |
| | } from 'react'; |
| | import { useIsMobile } from '../../hooks/common/useIsMobile'; |
| | import { |
| | Modal, |
| | Table, |
| | Input, |
| | Space, |
| | Highlight, |
| | Select, |
| | Tag, |
| | } from '@douyinfe/semi-ui'; |
| | import { IconSearch } from '@douyinfe/semi-icons'; |
| |
|
| | const ChannelSelectorModal = forwardRef( |
| | ( |
| | { |
| | visible, |
| | onCancel, |
| | onOk, |
| | allChannels, |
| | selectedChannelIds, |
| | setSelectedChannelIds, |
| | channelEndpoints, |
| | updateChannelEndpoint, |
| | t, |
| | }, |
| | ref, |
| | ) => { |
| | const [searchText, setSearchText] = useState(''); |
| | const [currentPage, setCurrentPage] = useState(1); |
| | const [pageSize, setPageSize] = useState(10); |
| | const isMobile = useIsMobile(); |
| |
|
| | const [filteredData, setFilteredData] = useState([]); |
| |
|
| | useImperativeHandle(ref, () => ({ |
| | resetPagination: () => { |
| | setCurrentPage(1); |
| | setSearchText(''); |
| | }, |
| | })); |
| |
|
| | |
| | const isOfficialChannel = (record) => { |
| | const id = record?.key ?? record?.value ?? record?._originalData?.id; |
| | const base = record?._originalData?.base_url || ''; |
| | const name = record?.label || ''; |
| | return ( |
| | id === -100 || |
| | base === 'https://basellm.github.io' || |
| | name === '官方倍率预设' |
| | ); |
| | }; |
| |
|
| | useEffect(() => { |
| | if (!allChannels) return; |
| |
|
| | const searchLower = searchText.trim().toLowerCase(); |
| | const matched = searchLower |
| | ? allChannels.filter((item) => { |
| | const name = (item.label || '').toLowerCase(); |
| | const baseUrl = (item._originalData?.base_url || '').toLowerCase(); |
| | return name.includes(searchLower) || baseUrl.includes(searchLower); |
| | }) |
| | : allChannels; |
| |
|
| | const sorted = [...matched].sort((a, b) => { |
| | const wa = isOfficialChannel(a) ? 0 : 1; |
| | const wb = isOfficialChannel(b) ? 0 : 1; |
| | return wa - wb; |
| | }); |
| |
|
| | setFilteredData(sorted); |
| | }, [allChannels, searchText]); |
| |
|
| | const total = filteredData.length; |
| |
|
| | const paginatedData = filteredData.slice( |
| | (currentPage - 1) * pageSize, |
| | currentPage * pageSize, |
| | ); |
| |
|
| | const updateEndpoint = (channelId, endpoint) => { |
| | if (typeof updateChannelEndpoint === 'function') { |
| | updateChannelEndpoint(channelId, endpoint); |
| | } |
| | }; |
| |
|
| | const renderEndpointCell = (text, record) => { |
| | const channelId = record.key || record.value; |
| | const currentEndpoint = channelEndpoints[channelId] || ''; |
| |
|
| | const getEndpointType = (ep) => { |
| | if (ep === '/api/ratio_config') return 'ratio_config'; |
| | if (ep === '/api/pricing') return 'pricing'; |
| | return 'custom'; |
| | }; |
| |
|
| | const currentType = getEndpointType(currentEndpoint); |
| |
|
| | const handleTypeChange = (val) => { |
| | if (val === 'ratio_config') { |
| | updateEndpoint(channelId, '/api/ratio_config'); |
| | } else if (val === 'pricing') { |
| | updateEndpoint(channelId, '/api/pricing'); |
| | } else { |
| | if (currentType !== 'custom') { |
| | updateEndpoint(channelId, ''); |
| | } |
| | } |
| | }; |
| |
|
| | return ( |
| | <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> |
| | <Select |
| | size='small' |
| | value={currentType} |
| | onChange={handleTypeChange} |
| | style={{ width: 120 }} |
| | optionList={[ |
| | { label: 'ratio_config', value: 'ratio_config' }, |
| | { label: 'pricing', value: 'pricing' }, |
| | { label: 'custom', value: 'custom' }, |
| | ]} |
| | /> |
| | {currentType === 'custom' && ( |
| | <Input |
| | size='small' |
| | value={currentEndpoint} |
| | onChange={(val) => updateEndpoint(channelId, val)} |
| | placeholder='/your/endpoint' |
| | style={{ width: 160, fontSize: 12 }} |
| | /> |
| | )} |
| | </div> |
| | ); |
| | }; |
| |
|
| | const renderStatusCell = (record) => { |
| | const status = record?._originalData?.status || 0; |
| | const official = isOfficialChannel(record); |
| | let statusTag = null; |
| | switch (status) { |
| | case 1: |
| | statusTag = ( |
| | <Tag color='green' shape='circle'> |
| | {t('已启用')} |
| | </Tag> |
| | ); |
| | break; |
| | case 2: |
| | statusTag = ( |
| | <Tag color='red' shape='circle'> |
| | {t('已禁用')} |
| | </Tag> |
| | ); |
| | break; |
| | case 3: |
| | statusTag = ( |
| | <Tag color='yellow' shape='circle'> |
| | {t('自动禁用')} |
| | </Tag> |
| | ); |
| | break; |
| | default: |
| | statusTag = ( |
| | <Tag color='grey' shape='circle'> |
| | {t('未知状态')} |
| | </Tag> |
| | ); |
| | } |
| | return ( |
| | <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> |
| | {statusTag} |
| | {official && ( |
| | <Tag color='green' shape='circle' type='light'> |
| | {t('官方')} |
| | </Tag> |
| | )} |
| | </div> |
| | ); |
| | }; |
| |
|
| | const renderNameCell = (text) => ( |
| | <Highlight sourceString={text} searchWords={[searchText]} /> |
| | ); |
| |
|
| | const renderBaseUrlCell = (text) => ( |
| | <Highlight sourceString={text} searchWords={[searchText]} /> |
| | ); |
| |
|
| | const columns = [ |
| | { |
| | title: t('名称'), |
| | dataIndex: 'label', |
| | render: renderNameCell, |
| | }, |
| | { |
| | title: t('源地址'), |
| | dataIndex: '_originalData.base_url', |
| | render: (_, record) => |
| | renderBaseUrlCell(record._originalData?.base_url || ''), |
| | }, |
| | { |
| | title: t('状态'), |
| | dataIndex: '_originalData.status', |
| | render: (_, record) => renderStatusCell(record), |
| | }, |
| | { |
| | title: t('同步接口'), |
| | dataIndex: 'endpoint', |
| | fixed: 'right', |
| | render: renderEndpointCell, |
| | }, |
| | ]; |
| |
|
| | const rowSelection = { |
| | selectedRowKeys: selectedChannelIds, |
| | onChange: (keys) => setSelectedChannelIds(keys), |
| | }; |
| |
|
| | return ( |
| | <Modal |
| | visible={visible} |
| | onCancel={onCancel} |
| | onOk={onOk} |
| | title={ |
| | <span className='text-lg font-semibold'>{t('选择同步渠道')}</span> |
| | } |
| | size={isMobile ? 'full-width' : 'large'} |
| | keepDOM |
| | lazyRender={false} |
| | > |
| | <Space vertical style={{ width: '100%' }}> |
| | <Input |
| | prefix={<IconSearch size={14} />} |
| | placeholder={t('搜索渠道名称或地址')} |
| | value={searchText} |
| | onChange={setSearchText} |
| | showClear |
| | /> |
| | |
| | <Table |
| | columns={columns} |
| | dataSource={paginatedData} |
| | rowKey='key' |
| | rowSelection={rowSelection} |
| | pagination={{ |
| | currentPage: currentPage, |
| | pageSize: pageSize, |
| | total: total, |
| | showSizeChanger: true, |
| | showQuickJumper: true, |
| | pageSizeOptions: ['10', '20', '50', '100'], |
| | onChange: (page, size) => { |
| | setCurrentPage(page); |
| | setPageSize(size); |
| | }, |
| | onShowSizeChange: (curr, size) => { |
| | setCurrentPage(1); |
| | setPageSize(size); |
| | }, |
| | }} |
| | size='small' |
| | /> |
| | </Space> |
| | </Modal> |
| | ); |
| | }, |
| | ); |
| |
|
| | export default ChannelSelectorModal; |
| |
|