ChannelSelectorModal.jsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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, {
  16. useState,
  17. useEffect,
  18. forwardRef,
  19. useImperativeHandle,
  20. } from 'react';
  21. import { useIsMobile } from '../../hooks/common/useIsMobile';
  22. import {
  23. Modal,
  24. Table,
  25. Input,
  26. Space,
  27. Highlight,
  28. Select,
  29. Tag,
  30. } from '@douyinfe/semi-ui';
  31. import { IconSearch } from '@douyinfe/semi-icons';
  32. import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
  33. const ChannelSelectorModal = forwardRef(
  34. (
  35. {
  36. visible,
  37. onCancel,
  38. onOk,
  39. allChannels,
  40. selectedChannelIds,
  41. setSelectedChannelIds,
  42. channelEndpoints,
  43. updateChannelEndpoint,
  44. t,
  45. },
  46. ref,
  47. ) => {
  48. const [searchText, setSearchText] = useState('');
  49. const [currentPage, setCurrentPage] = useState(1);
  50. const [pageSize, setPageSize] = useState(10);
  51. const isMobile = useIsMobile();
  52. const [filteredData, setFilteredData] = useState([]);
  53. useImperativeHandle(ref, () => ({
  54. resetPagination: () => {
  55. setCurrentPage(1);
  56. setSearchText('');
  57. },
  58. }));
  59. useEffect(() => {
  60. if (!allChannels) return;
  61. const searchLower = searchText.trim().toLowerCase();
  62. const matched = searchLower
  63. ? allChannels.filter((item) => {
  64. const name = (item.label || '').toLowerCase();
  65. const baseUrl = (item._originalData?.base_url || '').toLowerCase();
  66. return name.includes(searchLower) || baseUrl.includes(searchLower);
  67. })
  68. : allChannels;
  69. setFilteredData(matched);
  70. }, [allChannels, searchText]);
  71. const total = filteredData.length;
  72. const paginatedData = filteredData.slice(
  73. (currentPage - 1) * pageSize,
  74. currentPage * pageSize,
  75. );
  76. const updateEndpoint = (channelId, endpoint) => {
  77. if (typeof updateChannelEndpoint === 'function') {
  78. updateChannelEndpoint(channelId, endpoint);
  79. }
  80. };
  81. const renderEndpointCell = (text, record) => {
  82. const channelId = record.key || record.value;
  83. const currentEndpoint = channelEndpoints[channelId] || '';
  84. const getEndpointType = (ep) => {
  85. if (ep === '/api/ratio_config') return 'ratio_config';
  86. if (ep === '/api/pricing') return 'pricing';
  87. return 'custom';
  88. };
  89. const currentType = getEndpointType(currentEndpoint);
  90. const handleTypeChange = (val) => {
  91. if (val === 'ratio_config') {
  92. updateEndpoint(channelId, '/api/ratio_config');
  93. } else if (val === 'pricing') {
  94. updateEndpoint(channelId, '/api/pricing');
  95. } else {
  96. if (currentType !== 'custom') {
  97. updateEndpoint(channelId, '');
  98. }
  99. }
  100. };
  101. return (
  102. <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
  103. <Select
  104. size='small'
  105. value={currentType}
  106. onChange={handleTypeChange}
  107. style={{ width: 120 }}
  108. optionList={[
  109. { label: 'ratio_config', value: 'ratio_config' },
  110. { label: 'pricing', value: 'pricing' },
  111. { label: 'custom', value: 'custom' },
  112. ]}
  113. />
  114. {currentType === 'custom' && (
  115. <Input
  116. size='small'
  117. value={currentEndpoint}
  118. onChange={(val) => updateEndpoint(channelId, val)}
  119. placeholder='/your/endpoint'
  120. style={{ width: 160, fontSize: 12 }}
  121. />
  122. )}
  123. </div>
  124. );
  125. };
  126. const renderStatusCell = (status) => {
  127. switch (status) {
  128. case 1:
  129. return (
  130. <Tag
  131. color='green'
  132. shape='circle'
  133. prefixIcon={<CheckCircle size={14} />}
  134. >
  135. {t('已启用')}
  136. </Tag>
  137. );
  138. case 2:
  139. return (
  140. <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
  141. {t('已禁用')}
  142. </Tag>
  143. );
  144. case 3:
  145. return (
  146. <Tag
  147. color='yellow'
  148. shape='circle'
  149. prefixIcon={<AlertCircle size={14} />}
  150. >
  151. {t('自动禁用')}
  152. </Tag>
  153. );
  154. default:
  155. return (
  156. <Tag
  157. color='grey'
  158. shape='circle'
  159. prefixIcon={<HelpCircle size={14} />}
  160. >
  161. {t('未知状态')}
  162. </Tag>
  163. );
  164. }
  165. };
  166. const renderNameCell = (text) => (
  167. <Highlight sourceString={text} searchWords={[searchText]} />
  168. );
  169. const renderBaseUrlCell = (text) => (
  170. <Highlight sourceString={text} searchWords={[searchText]} />
  171. );
  172. const columns = [
  173. {
  174. title: t('名称'),
  175. dataIndex: 'label',
  176. render: renderNameCell,
  177. },
  178. {
  179. title: t('源地址'),
  180. dataIndex: '_originalData.base_url',
  181. render: (_, record) =>
  182. renderBaseUrlCell(record._originalData?.base_url || ''),
  183. },
  184. {
  185. title: t('状态'),
  186. dataIndex: '_originalData.status',
  187. render: (_, record) =>
  188. renderStatusCell(record._originalData?.status || 0),
  189. },
  190. {
  191. title: t('同步接口'),
  192. dataIndex: 'endpoint',
  193. fixed: 'right',
  194. render: renderEndpointCell,
  195. },
  196. ];
  197. const rowSelection = {
  198. selectedRowKeys: selectedChannelIds,
  199. onChange: (keys) => setSelectedChannelIds(keys),
  200. };
  201. return (
  202. <Modal
  203. visible={visible}
  204. onCancel={onCancel}
  205. onOk={onOk}
  206. title={
  207. <span className='text-lg font-semibold'>{t('选择同步渠道')}</span>
  208. }
  209. size={isMobile ? 'full-width' : 'large'}
  210. keepDOM
  211. lazyRender={false}
  212. >
  213. <Space vertical style={{ width: '100%' }}>
  214. <Input
  215. prefix={<IconSearch size={14} />}
  216. placeholder={t('搜索渠道名称或地址')}
  217. value={searchText}
  218. onChange={setSearchText}
  219. showClear
  220. />
  221. <Table
  222. columns={columns}
  223. dataSource={paginatedData}
  224. rowKey='key'
  225. rowSelection={rowSelection}
  226. pagination={{
  227. currentPage: currentPage,
  228. pageSize: pageSize,
  229. total: total,
  230. showSizeChanger: true,
  231. showQuickJumper: true,
  232. pageSizeOptions: ['10', '20', '50', '100'],
  233. onChange: (page, size) => {
  234. setCurrentPage(page);
  235. setPageSize(size);
  236. },
  237. onShowSizeChange: (curr, size) => {
  238. setCurrentPage(1);
  239. setPageSize(size);
  240. },
  241. }}
  242. size='small'
  243. />
  244. </Space>
  245. </Modal>
  246. );
  247. },
  248. );
  249. export default ChannelSelectorModal;