ChannelSelectorModal.js 6.3 KB

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