ChannelSelectorModal.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
  2. import { useIsMobile } from '../../hooks/useIsMobile.js';
  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 isMobile = useIsMobile();
  29. const [filteredData, setFilteredData] = useState([]);
  30. useImperativeHandle(ref, () => ({
  31. resetPagination: () => {
  32. setCurrentPage(1);
  33. setSearchText('');
  34. },
  35. }));
  36. useEffect(() => {
  37. if (!allChannels) return;
  38. const searchLower = searchText.trim().toLowerCase();
  39. const matched = searchLower
  40. ? allChannels.filter((item) => {
  41. const name = (item.label || '').toLowerCase();
  42. const baseUrl = (item._originalData?.base_url || '').toLowerCase();
  43. return name.includes(searchLower) || baseUrl.includes(searchLower);
  44. })
  45. : allChannels;
  46. setFilteredData(matched);
  47. }, [allChannels, searchText]);
  48. const total = filteredData.length;
  49. const paginatedData = filteredData.slice(
  50. (currentPage - 1) * pageSize,
  51. currentPage * pageSize,
  52. );
  53. const updateEndpoint = (channelId, endpoint) => {
  54. if (typeof updateChannelEndpoint === 'function') {
  55. updateChannelEndpoint(channelId, endpoint);
  56. }
  57. };
  58. const renderEndpointCell = (text, record) => {
  59. const channelId = record.key || record.value;
  60. const currentEndpoint = channelEndpoints[channelId] || '';
  61. const getEndpointType = (ep) => {
  62. if (ep === '/api/ratio_config') return 'ratio_config';
  63. if (ep === '/api/pricing') return 'pricing';
  64. return 'custom';
  65. };
  66. const currentType = getEndpointType(currentEndpoint);
  67. const handleTypeChange = (val) => {
  68. if (val === 'ratio_config') {
  69. updateEndpoint(channelId, '/api/ratio_config');
  70. } else if (val === 'pricing') {
  71. updateEndpoint(channelId, '/api/pricing');
  72. } else {
  73. if (currentType !== 'custom') {
  74. updateEndpoint(channelId, '');
  75. }
  76. }
  77. };
  78. return (
  79. <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
  80. <Select
  81. size="small"
  82. value={currentType}
  83. onChange={handleTypeChange}
  84. style={{ width: 120 }}
  85. optionList={[
  86. { label: 'ratio_config', value: 'ratio_config' },
  87. { label: 'pricing', value: 'pricing' },
  88. { label: 'custom', value: 'custom' },
  89. ]}
  90. />
  91. {currentType === 'custom' && (
  92. <Input
  93. size="small"
  94. value={currentEndpoint}
  95. onChange={(val) => updateEndpoint(channelId, val)}
  96. placeholder="/your/endpoint"
  97. style={{ width: 160, fontSize: 12 }}
  98. />
  99. )}
  100. </div>
  101. );
  102. };
  103. const renderStatusCell = (status) => {
  104. switch (status) {
  105. case 1:
  106. return (
  107. <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
  108. {t('已启用')}
  109. </Tag>
  110. );
  111. case 2:
  112. return (
  113. <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
  114. {t('已禁用')}
  115. </Tag>
  116. );
  117. case 3:
  118. return (
  119. <Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
  120. {t('自动禁用')}
  121. </Tag>
  122. );
  123. default:
  124. return (
  125. <Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  126. {t('未知状态')}
  127. </Tag>
  128. );
  129. }
  130. };
  131. const renderNameCell = (text) => (
  132. <Highlight sourceString={text} searchWords={[searchText]} />
  133. );
  134. const renderBaseUrlCell = (text) => (
  135. <Highlight sourceString={text} searchWords={[searchText]} />
  136. );
  137. const columns = [
  138. {
  139. title: t('名称'),
  140. dataIndex: 'label',
  141. render: renderNameCell,
  142. },
  143. {
  144. title: t('源地址'),
  145. dataIndex: '_originalData.base_url',
  146. render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''),
  147. },
  148. {
  149. title: t('状态'),
  150. dataIndex: '_originalData.status',
  151. render: (_, record) => renderStatusCell(record._originalData?.status || 0),
  152. },
  153. {
  154. title: t('同步接口'),
  155. dataIndex: 'endpoint',
  156. fixed: 'right',
  157. render: renderEndpointCell,
  158. },
  159. ];
  160. const rowSelection = {
  161. selectedRowKeys: selectedChannelIds,
  162. onChange: (keys) => setSelectedChannelIds(keys),
  163. };
  164. return (
  165. <Modal
  166. visible={visible}
  167. onCancel={onCancel}
  168. onOk={onOk}
  169. title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
  170. size={isMobile ? 'full-width' : 'large'}
  171. keepDOM
  172. lazyRender={false}
  173. >
  174. <Space vertical style={{ width: '100%' }}>
  175. <Input
  176. prefix={<IconSearch size={14} />}
  177. placeholder={t('搜索渠道名称或地址')}
  178. value={searchText}
  179. onChange={setSearchText}
  180. showClear
  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;