SettingsUptimeKuma.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
  2. import {
  3. Form,
  4. Button,
  5. Typography,
  6. Row,
  7. Col,
  8. } from '@douyinfe/semi-ui';
  9. import {
  10. Save,
  11. Activity
  12. } from 'lucide-react';
  13. import { API, showError, showSuccess } from '../../../helpers';
  14. import { useTranslation } from 'react-i18next';
  15. const { Text } = Typography;
  16. const SettingsUptimeKuma = ({ options, refresh }) => {
  17. const { t } = useTranslation();
  18. const [loading, setLoading] = useState(false);
  19. const formApiRef = useRef(null);
  20. const initValues = useMemo(() => ({
  21. uptimeKumaUrl: options?.['console_setting.uptime_kuma_url'] || '',
  22. uptimeKumaSlug: options?.['console_setting.uptime_kuma_slug'] || ''
  23. }), [options?.['console_setting.uptime_kuma_url'], options?.['console_setting.uptime_kuma_slug']]);
  24. useEffect(() => {
  25. if (formApiRef.current) {
  26. formApiRef.current.setValues(initValues, { isOverride: true });
  27. }
  28. }, [initValues]);
  29. const handleSave = async () => {
  30. const api = formApiRef.current;
  31. if (!api) {
  32. showError(t('表单未初始化'));
  33. return;
  34. }
  35. try {
  36. setLoading(true);
  37. const { uptimeKumaUrl, uptimeKumaSlug } = await api.validate();
  38. const trimmedUrl = (uptimeKumaUrl || '').trim();
  39. const trimmedSlug = (uptimeKumaSlug || '').trim();
  40. if (trimmedUrl === options?.['console_setting.uptime_kuma_url'] && trimmedSlug === options?.['console_setting.uptime_kuma_slug']) {
  41. showSuccess(t('无需保存,配置未变动'));
  42. return;
  43. }
  44. const [urlRes, slugRes] = await Promise.all([
  45. trimmedUrl === options?.['console_setting.uptime_kuma_url'] ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
  46. key: 'console_setting.uptime_kuma_url',
  47. value: trimmedUrl
  48. }),
  49. trimmedSlug === options?.['console_setting.uptime_kuma_slug'] ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
  50. key: 'console_setting.uptime_kuma_slug',
  51. value: trimmedSlug
  52. })
  53. ]);
  54. if (!urlRes.data.success) throw new Error(urlRes.data.message || t('URL 保存失败'));
  55. if (!slugRes.data.success) throw new Error(slugRes.data.message || t('Slug 保存失败'));
  56. showSuccess(t('Uptime Kuma 设置保存成功'));
  57. refresh?.();
  58. } catch (err) {
  59. console.error(err);
  60. showError(err.message || t('保存失败,请重试'));
  61. } finally {
  62. setLoading(false);
  63. }
  64. };
  65. const isValidUrl = useCallback((string) => {
  66. try {
  67. new URL(string);
  68. return true;
  69. } catch (_) {
  70. return false;
  71. }
  72. }, []);
  73. const renderHeader = () => (
  74. <div className="flex flex-col w-full">
  75. <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-2">
  76. <div className="flex items-center text-blue-500">
  77. <Activity size={16} className="mr-2" />
  78. <Text>
  79. {t('配置')}&nbsp;
  80. <a
  81. href="https://github.com/louislam/uptime-kuma"
  82. target="_blank"
  83. rel="noopener noreferrer"
  84. className="text-blue-600 hover:underline"
  85. >
  86. Uptime&nbsp;Kuma
  87. </a>
  88. &nbsp;{t('服务监控地址,用于展示服务状态信息')}
  89. </Text>
  90. </div>
  91. <div className="flex gap-2">
  92. <Button
  93. icon={<Save size={14} />}
  94. theme='solid'
  95. type='primary'
  96. onClick={handleSave}
  97. loading={loading}
  98. className="!rounded-full"
  99. >
  100. {t('保存设置')}
  101. </Button>
  102. </div>
  103. </div>
  104. </div>
  105. );
  106. return (
  107. <Form.Section text={renderHeader()}>
  108. <Form
  109. layout="vertical"
  110. autoScrollToError
  111. initValues={initValues}
  112. getFormApi={(api) => {
  113. formApiRef.current = api;
  114. }}
  115. >
  116. <Row gutter={[24, 24]}>
  117. <Col xs={24} md={12}>
  118. <Form.Input
  119. showClear
  120. field="uptimeKumaUrl"
  121. label={{ text: t("Uptime Kuma 服务地址") }}
  122. placeholder={t("请输入 Uptime Kuma 服务地址")}
  123. style={{ fontFamily: 'monospace' }}
  124. helpText={t("请输入 Uptime Kuma 服务的完整地址,例如:https://uptime.example.com")}
  125. rules={[
  126. {
  127. validator: (_, value) => {
  128. const url = (value || '').trim();
  129. if (url && !isValidUrl(url)) {
  130. return Promise.reject(t('请输入有效的 URL 地址'));
  131. }
  132. return Promise.resolve();
  133. }
  134. }
  135. ]}
  136. />
  137. </Col>
  138. <Col xs={24} md={12}>
  139. <Form.Input
  140. showClear
  141. field="uptimeKumaSlug"
  142. label={{ text: t("状态页面 Slug") }}
  143. placeholder={t("请输入状态页面 Slug")}
  144. style={{ fontFamily: 'monospace' }}
  145. helpText={t("请输入状态页面的 slug 标识符,例如:my-status")}
  146. rules={[
  147. {
  148. validator: (_, value) => {
  149. const slug = (value || '').trim();
  150. if (slug && !/^[a-zA-Z0-9_-]+$/.test(slug)) {
  151. return Promise.reject(t('Slug 只能包含字母、数字、下划线和连字符'));
  152. }
  153. return Promise.resolve();
  154. }
  155. }
  156. ]}
  157. />
  158. </Col>
  159. </Row>
  160. </Form>
  161. </Form.Section>
  162. );
  163. };
  164. export default SettingsUptimeKuma;