SettingsAPIInfo.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. Button,
  4. Space,
  5. Table,
  6. Form,
  7. Typography,
  8. Empty,
  9. Divider,
  10. Avatar,
  11. Modal,
  12. Tag,
  13. Switch
  14. } from '@douyinfe/semi-ui';
  15. import {
  16. IllustrationNoResult,
  17. IllustrationNoResultDark
  18. } from '@douyinfe/semi-illustrations';
  19. import {
  20. Plus,
  21. Edit,
  22. Trash2,
  23. Save,
  24. Settings
  25. } from 'lucide-react';
  26. import { API, showError, showSuccess } from '../../../helpers';
  27. import { useTranslation } from 'react-i18next';
  28. const { Text } = Typography;
  29. const SettingsAPIInfo = ({ options, refresh }) => {
  30. const { t } = useTranslation();
  31. const [apiInfoList, setApiInfoList] = useState([]);
  32. const [showApiModal, setShowApiModal] = useState(false);
  33. const [showDeleteModal, setShowDeleteModal] = useState(false);
  34. const [deletingApi, setDeletingApi] = useState(null);
  35. const [editingApi, setEditingApi] = useState(null);
  36. const [modalLoading, setModalLoading] = useState(false);
  37. const [loading, setLoading] = useState(false);
  38. const [hasChanges, setHasChanges] = useState(false);
  39. const [apiForm, setApiForm] = useState({
  40. url: '',
  41. description: '',
  42. route: '',
  43. color: 'blue'
  44. });
  45. const [currentPage, setCurrentPage] = useState(1);
  46. const [pageSize, setPageSize] = useState(10);
  47. const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  48. // 面板启用状态 state
  49. const [panelEnabled, setPanelEnabled] = useState(true);
  50. const colorOptions = [
  51. { value: 'blue', label: 'blue' },
  52. { value: 'green', label: 'green' },
  53. { value: 'cyan', label: 'cyan' },
  54. { value: 'purple', label: 'purple' },
  55. { value: 'pink', label: 'pink' },
  56. { value: 'red', label: 'red' },
  57. { value: 'orange', label: 'orange' },
  58. { value: 'amber', label: 'amber' },
  59. { value: 'yellow', label: 'yellow' },
  60. { value: 'lime', label: 'lime' },
  61. { value: 'light-green', label: 'light-green' },
  62. { value: 'teal', label: 'teal' },
  63. { value: 'light-blue', label: 'light-blue' },
  64. { value: 'indigo', label: 'indigo' },
  65. { value: 'violet', label: 'violet' },
  66. { value: 'grey', label: 'grey' }
  67. ];
  68. const updateOption = async (key, value) => {
  69. const res = await API.put('/api/option/', {
  70. key,
  71. value,
  72. });
  73. const { success, message } = res.data;
  74. if (success) {
  75. showSuccess('API信息已更新');
  76. if (refresh) refresh();
  77. } else {
  78. showError(message);
  79. }
  80. };
  81. const submitApiInfo = async () => {
  82. try {
  83. setLoading(true);
  84. const apiInfoJson = JSON.stringify(apiInfoList);
  85. await updateOption('console_setting.api_info', apiInfoJson);
  86. setHasChanges(false);
  87. } catch (error) {
  88. console.error('API信息更新失败', error);
  89. showError('API信息更新失败');
  90. } finally {
  91. setLoading(false);
  92. }
  93. };
  94. const handleAddApi = () => {
  95. setEditingApi(null);
  96. setApiForm({
  97. url: '',
  98. description: '',
  99. route: '',
  100. color: 'blue'
  101. });
  102. setShowApiModal(true);
  103. };
  104. const handleEditApi = (api) => {
  105. setEditingApi(api);
  106. setApiForm({
  107. url: api.url,
  108. description: api.description,
  109. route: api.route,
  110. color: api.color
  111. });
  112. setShowApiModal(true);
  113. };
  114. const handleDeleteApi = (api) => {
  115. setDeletingApi(api);
  116. setShowDeleteModal(true);
  117. };
  118. const confirmDeleteApi = () => {
  119. if (deletingApi) {
  120. const newList = apiInfoList.filter(api => api.id !== deletingApi.id);
  121. setApiInfoList(newList);
  122. setHasChanges(true);
  123. showSuccess('API信息已删除,请及时点击“保存设置”进行保存');
  124. }
  125. setShowDeleteModal(false);
  126. setDeletingApi(null);
  127. };
  128. const handleSaveApi = async () => {
  129. if (!apiForm.url || !apiForm.route || !apiForm.description) {
  130. showError('请填写完整的API信息');
  131. return;
  132. }
  133. try {
  134. setModalLoading(true);
  135. let newList;
  136. if (editingApi) {
  137. newList = apiInfoList.map(api =>
  138. api.id === editingApi.id
  139. ? { ...api, ...apiForm }
  140. : api
  141. );
  142. } else {
  143. const newId = Math.max(...apiInfoList.map(api => api.id), 0) + 1;
  144. const newApi = {
  145. id: newId,
  146. ...apiForm
  147. };
  148. newList = [...apiInfoList, newApi];
  149. }
  150. setApiInfoList(newList);
  151. setHasChanges(true);
  152. setShowApiModal(false);
  153. showSuccess(editingApi ? 'API信息已更新,请及时点击“保存设置”进行保存' : 'API信息已添加,请及时点击“保存设置”进行保存');
  154. } catch (error) {
  155. showError('操作失败: ' + error.message);
  156. } finally {
  157. setModalLoading(false);
  158. }
  159. };
  160. const parseApiInfo = (apiInfoStr) => {
  161. if (!apiInfoStr) {
  162. setApiInfoList([]);
  163. return;
  164. }
  165. try {
  166. const parsed = JSON.parse(apiInfoStr);
  167. setApiInfoList(Array.isArray(parsed) ? parsed : []);
  168. } catch (error) {
  169. console.error('解析API信息失败:', error);
  170. setApiInfoList([]);
  171. }
  172. };
  173. useEffect(() => {
  174. const apiInfoStr = options['console_setting.api_info'] ?? options.ApiInfo;
  175. if (apiInfoStr !== undefined) {
  176. parseApiInfo(apiInfoStr);
  177. }
  178. }, [options['console_setting.api_info'], options.ApiInfo]);
  179. useEffect(() => {
  180. const enabledStr = options['console_setting.api_info_enabled'];
  181. setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
  182. }, [options['console_setting.api_info_enabled']]);
  183. const handleToggleEnabled = async (checked) => {
  184. const newValue = checked ? 'true' : 'false';
  185. try {
  186. const res = await API.put('/api/option/', {
  187. key: 'console_setting.api_info_enabled',
  188. value: newValue,
  189. });
  190. if (res.data.success) {
  191. setPanelEnabled(checked);
  192. showSuccess(t('设置已保存'));
  193. refresh?.();
  194. } else {
  195. showError(res.data.message);
  196. }
  197. } catch (err) {
  198. showError(err.message);
  199. }
  200. };
  201. const columns = [
  202. {
  203. title: 'ID',
  204. dataIndex: 'id',
  205. },
  206. {
  207. title: t('API地址'),
  208. dataIndex: 'url',
  209. render: (text, record) => (
  210. <Tag
  211. color={record.color}
  212. shape='circle'
  213. style={{ maxWidth: '280px' }}
  214. >
  215. {text}
  216. </Tag>
  217. ),
  218. },
  219. {
  220. title: t('线路描述'),
  221. dataIndex: 'route',
  222. render: (text, record) => (
  223. <Tag shape='circle'>
  224. {text}
  225. </Tag>
  226. ),
  227. },
  228. {
  229. title: t('说明'),
  230. dataIndex: 'description',
  231. ellipsis: true,
  232. render: (text, record) => (
  233. <Tag shape='circle'>
  234. {text || '-'}
  235. </Tag>
  236. ),
  237. },
  238. {
  239. title: t('颜色'),
  240. dataIndex: 'color',
  241. render: (color) => (
  242. <Avatar
  243. size="extra-extra-small"
  244. color={color}
  245. />
  246. ),
  247. },
  248. {
  249. title: t('操作'),
  250. fixed: 'right',
  251. width: 150,
  252. render: (_, record) => (
  253. <Space>
  254. <Button
  255. icon={<Edit size={14} />}
  256. theme='light'
  257. type='tertiary'
  258. size='small'
  259. onClick={() => handleEditApi(record)}
  260. >
  261. {t('编辑')}
  262. </Button>
  263. <Button
  264. icon={<Trash2 size={14} />}
  265. type='danger'
  266. theme='light'
  267. size='small'
  268. onClick={() => handleDeleteApi(record)}
  269. >
  270. {t('删除')}
  271. </Button>
  272. </Space>
  273. ),
  274. },
  275. ];
  276. const handleBatchDelete = () => {
  277. if (selectedRowKeys.length === 0) {
  278. showError('请先选择要删除的API信息');
  279. return;
  280. }
  281. const newList = apiInfoList.filter(api => !selectedRowKeys.includes(api.id));
  282. setApiInfoList(newList);
  283. setSelectedRowKeys([]);
  284. setHasChanges(true);
  285. showSuccess(`已删除 ${selectedRowKeys.length} 个API信息,请及时点击“保存设置”进行保存`);
  286. };
  287. const renderHeader = () => (
  288. <div className="flex flex-col w-full">
  289. <div className="mb-2">
  290. <div className="flex items-center text-blue-500">
  291. <Settings size={16} className="mr-2" />
  292. <Text>{t('API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)')}</Text>
  293. </div>
  294. </div>
  295. <Divider margin="12px" />
  296. <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
  297. <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
  298. <Button
  299. theme='light'
  300. type='primary'
  301. icon={<Plus size={14} />}
  302. className="w-full md:w-auto"
  303. onClick={handleAddApi}
  304. >
  305. {t('添加API')}
  306. </Button>
  307. <Button
  308. icon={<Trash2 size={14} />}
  309. type='danger'
  310. theme='light'
  311. onClick={handleBatchDelete}
  312. disabled={selectedRowKeys.length === 0}
  313. className="w-full md:w-auto"
  314. >
  315. {t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
  316. </Button>
  317. <Button
  318. icon={<Save size={14} />}
  319. onClick={submitApiInfo}
  320. loading={loading}
  321. disabled={!hasChanges}
  322. type='secondary'
  323. className="w-full md:w-auto"
  324. >
  325. {t('保存设置')}
  326. </Button>
  327. </div>
  328. {/* 启用开关 */}
  329. <div className="order-1 md:order-2 flex items-center gap-2">
  330. <Switch
  331. checked={panelEnabled}
  332. onChange={handleToggleEnabled}
  333. />
  334. <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
  335. </div>
  336. </div>
  337. </div>
  338. );
  339. // 计算当前页显示的数据
  340. const getCurrentPageData = () => {
  341. const startIndex = (currentPage - 1) * pageSize;
  342. const endIndex = startIndex + pageSize;
  343. return apiInfoList.slice(startIndex, endIndex);
  344. };
  345. const rowSelection = {
  346. selectedRowKeys,
  347. onChange: (selectedRowKeys, selectedRows) => {
  348. setSelectedRowKeys(selectedRowKeys);
  349. },
  350. onSelect: (record, selected, selectedRows) => {
  351. console.log(`选择行: ${selected}`, record);
  352. },
  353. onSelectAll: (selected, selectedRows) => {
  354. console.log(`全选: ${selected}`, selectedRows);
  355. },
  356. getCheckboxProps: (record) => ({
  357. disabled: false,
  358. name: record.id,
  359. }),
  360. };
  361. return (
  362. <>
  363. <Form.Section text={renderHeader()}>
  364. <Table
  365. columns={columns}
  366. dataSource={getCurrentPageData()}
  367. rowSelection={rowSelection}
  368. rowKey="id"
  369. scroll={{ x: 'max-content' }}
  370. pagination={{
  371. currentPage: currentPage,
  372. pageSize: pageSize,
  373. total: apiInfoList.length,
  374. showSizeChanger: true,
  375. showQuickJumper: true,
  376. formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  377. start: page.currentStart,
  378. end: page.currentEnd,
  379. total: apiInfoList.length,
  380. }),
  381. pageSizeOptions: ['5', '10', '20', '50'],
  382. onChange: (page, size) => {
  383. setCurrentPage(page);
  384. setPageSize(size);
  385. },
  386. onShowSizeChange: (current, size) => {
  387. setCurrentPage(1);
  388. setPageSize(size);
  389. }
  390. }}
  391. size='middle'
  392. loading={loading}
  393. empty={
  394. <Empty
  395. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  396. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  397. description={t('暂无API信息')}
  398. style={{ padding: 30 }}
  399. />
  400. }
  401. className="overflow-hidden"
  402. />
  403. </Form.Section>
  404. <Modal
  405. title={editingApi ? t('编辑API') : t('添加API')}
  406. visible={showApiModal}
  407. onOk={handleSaveApi}
  408. onCancel={() => setShowApiModal(false)}
  409. okText={t('保存')}
  410. cancelText={t('取消')}
  411. confirmLoading={modalLoading}
  412. >
  413. <Form layout='vertical' initValues={apiForm} key={editingApi ? editingApi.id : 'new'}>
  414. <Form.Input
  415. field='url'
  416. label={t('API地址')}
  417. placeholder='https://api.example.com'
  418. rules={[{ required: true, message: t('请输入API地址') }]}
  419. onChange={(value) => setApiForm({ ...apiForm, url: value })}
  420. />
  421. <Form.Input
  422. field='route'
  423. label={t('线路描述')}
  424. placeholder={t('如:香港线路')}
  425. rules={[{ required: true, message: t('请输入线路描述') }]}
  426. onChange={(value) => setApiForm({ ...apiForm, route: value })}
  427. />
  428. <Form.Input
  429. field='description'
  430. label={t('说明')}
  431. placeholder={t('如:大带宽批量分析图片推荐')}
  432. rules={[{ required: true, message: t('请输入说明') }]}
  433. onChange={(value) => setApiForm({ ...apiForm, description: value })}
  434. />
  435. <Form.Select
  436. field='color'
  437. label={t('标识颜色')}
  438. optionList={colorOptions}
  439. onChange={(value) => setApiForm({ ...apiForm, color: value })}
  440. render={(option) => (
  441. <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
  442. <Avatar
  443. size="extra-extra-small"
  444. color={option.value}
  445. />
  446. {option.label}
  447. </div>
  448. )}
  449. />
  450. </Form>
  451. </Modal>
  452. <Modal
  453. title={t('确认删除')}
  454. visible={showDeleteModal}
  455. onOk={confirmDeleteApi}
  456. onCancel={() => {
  457. setShowDeleteModal(false);
  458. setDeletingApi(null);
  459. }}
  460. okText={t('确认删除')}
  461. cancelText={t('取消')}
  462. type="warning"
  463. okButtonProps={{
  464. type: 'danger',
  465. theme: 'solid'
  466. }}
  467. >
  468. <Text>{t('确定要删除此API信息吗?')}</Text>
  469. </Modal>
  470. </>
  471. );
  472. };
  473. export default SettingsAPIInfo;