| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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; |
|
|