Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

523 строки
14 KiB

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