ChannelSelectorModal.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
  16. import { useIsMobile } from '../../hooks/common/useIsMobile.js';
  17. import {
  18. Modal,
  19. Table,
  20. Input,
  21. Space,
  22. Highlight,
  23. Select,
  24. Tag,
  25. } from '@douyinfe/semi-ui';
  26. import { IconSearch } from '@douyinfe/semi-icons';
  27. import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
  28. const ChannelSelectorModal = forwardRef(({
  29. visible,
  30. onCancel,
  31. onOk,
  32. allChannels,
  33. selectedChannelIds,
  34. setSelectedChannelIds,
  35. channelEndpoints,
  36. updateChannelEndpoint,
  37. t,
  38. }, ref) => {
  39. const [searchText, setSearchText] = useState('');
  40. const [currentPage, setCurrentPage] = useState(1);
  41. const [pageSize, setPageSize] = useState(10);
  42. const isMobile = useIsMobile();
  43. const [filteredData, setFilteredData] = useState([]);
  44. useImperativeHandle(ref, () => ({
  45. resetPagination: () => {
  46. setCurrentPage(1);
  47. setSearchText('');
  48. },
  49. }));
  50. useEffect(() => {
  51. if (!allChannels) return;
  52. const searchLower = searchText.trim().toLowerCase();
  53. const matched = searchLower
  54. ? allChannels.filter((item) => {
  55. const name = (item.label || '').toLowerCase();
  56. const baseUrl = (item._originalData?.base_url || '').toLowerCase();
  57. return name.includes(searchLower) || baseUrl.includes(searchLower);
  58. })
  59. : allChannels;
  60. setFilteredData(matched);
  61. }, [allChannels, searchText]);
  62. const total = filteredData.length;
  63. const paginatedData = filteredData.slice(
  64. (currentPage - 1) * pageSize,
  65. currentPage * pageSize,
  66. );
  67. const updateEndpoint = (channelId, endpoint) => {
  68. if (typeof updateChannelEndpoint === 'function') {
  69. updateChannelEndpoint(channelId, endpoint);
  70. }
  71. };
  72. const renderEndpointCell = (text, record) => {
  73. const channelId = record.key || record.value;
  74. const currentEndpoint = channelEndpoints[channelId] || '';
  75. const getEndpointType = (ep) => {
  76. if (ep === '/api/ratio_config') return 'ratio_config';
  77. if (ep === '/api/pricing') return 'pricing';
  78. return 'custom';
  79. };
  80. const currentType = getEndpointType(currentEndpoint);
  81. const handleTypeChange = (val) => {
  82. if (val === 'ratio_config') {
  83. updateEndpoint(channelId, '/api/ratio_config');
  84. } else if (val === 'pricing') {
  85. updateEndpoint(channelId, '/api/pricing');
  86. } else {
  87. if (currentType !== 'custom') {
  88. updateEndpoint(channelId, '');
  89. }
  90. }
  91. };
  92. return (
  93. <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
  94. <Select
  95. size="small"
  96. value={currentType}
  97. onChange={handleTypeChange}
  98. style={{ width: 120 }}
  99. optionList={[
  100. { label: 'ratio_config', value: 'ratio_config' },
  101. { label: 'pricing', value: 'pricing' },
  102. { label: 'custom', value: 'custom' },
  103. ]}
  104. />
  105. {currentType === 'custom' && (
  106. <Input
  107. size="small"
  108. value={currentEndpoint}
  109. onChange={(val) => updateEndpoint(channelId, val)}
  110. placeholder="/your/endpoint"
  111. style={{ width: 160, fontSize: 12 }}
  112. />
  113. )}
  114. </div>
  115. );
  116. };
  117. const renderStatusCell = (status) => {
  118. switch (status) {
  119. case 1:
  120. return (
  121. <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
  122. {t('已启用')}
  123. </Tag>
  124. );
  125. case 2:
  126. return (
  127. <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
  128. {t('已禁用')}
  129. </Tag>
  130. );
  131. case 3:
  132. return (
  133. <Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
  134. {t('自动禁用')}
  135. </Tag>
  136. );
  137. default:
  138. return (
  139. <Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  140. {t('未知状态')}
  141. </Tag>
  142. );
  143. }
  144. };
  145. const renderNameCell = (text) => (
  146. <Highlight sourceString={text} searchWords={[searchText]} />
  147. );
  148. const renderBaseUrlCell = (text) => (
  149. <Highlight sourceString={text} searchWords={[searchText]} />
  150. );
  151. const columns = [
  152. {
  153. title: t('名称'),
  154. dataIndex: 'label',
  155. render: renderNameCell,
  156. },
  157. {
  158. title: t('源地址'),
  159. dataIndex: '_originalData.base_url',
  160. render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''),
  161. },
  162. {
  163. title: t('状态'),
  164. dataIndex: '_originalData.status',
  165. render: (_, record) => renderStatusCell(record._originalData?.status || 0),
  166. },
  167. {
  168. title: t('同步接口'),
  169. dataIndex: 'endpoint',
  170. fixed: 'right',
  171. render: renderEndpointCell,
  172. },
  173. ];
  174. const rowSelection = {
  175. selectedRowKeys: selectedChannelIds,
  176. onChange: (keys) => setSelectedChannelIds(keys),
  177. };
  178. return (
  179. <Modal
  180. visible={visible}
  181. onCancel={onCancel}
  182. onOk={onOk}
  183. title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
  184. size={isMobile ? 'full-width' : 'large'}
  185. keepDOM
  186. lazyRender={false}
  187. >
  188. <Space vertical style={{ width: '100%' }}>
  189. <Input
  190. prefix={<IconSearch size={14} />}
  191. placeholder={t('搜索渠道名称或地址')}
  192. value={searchText}
  193. onChange={setSearchText}
  194. showClear
  195. />
  196. <Table
  197. columns={columns}
  198. dataSource={paginatedData}
  199. rowKey="key"
  200. rowSelection={rowSelection}
  201. pagination={{
  202. currentPage: currentPage,
  203. pageSize: pageSize,
  204. total: total,
  205. showSizeChanger: true,
  206. showQuickJumper: true,
  207. pageSizeOptions: ['10', '20', '50', '100'],
  208. onChange: (page, size) => {
  209. setCurrentPage(page);
  210. setPageSize(size);
  211. },
  212. onShowSizeChange: (curr, size) => {
  213. setCurrentPage(1);
  214. setPageSize(size);
  215. },
  216. }}
  217. size="small"
  218. />
  219. </Space>
  220. </Modal>
  221. );
  222. });
  223. export default ChannelSelectorModal;