SettingsUptimeKuma.js 14 KB

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