diff --git a/controller/uptime_kuma.go b/controller/uptime_kuma.go
new file mode 100644
index 00000000..245cac3f
--- /dev/null
+++ b/controller/uptime_kuma.go
@@ -0,0 +1,176 @@
+package controller
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "one-api/common"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "golang.org/x/sync/errgroup"
+)
+
+type UptimeKumaMonitor struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+type UptimeKumaGroup struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Weight int `json:"weight"`
+ MonitorList []UptimeKumaMonitor `json:"monitorList"`
+}
+
+type UptimeKumaHeartbeat struct {
+ Status int `json:"status"`
+ Time string `json:"time"`
+ Msg string `json:"msg"`
+ Ping *float64 `json:"ping"`
+}
+
+type UptimeKumaStatusResponse struct {
+ PublicGroupList []UptimeKumaGroup `json:"publicGroupList"`
+}
+
+type UptimeKumaHeartbeatResponse struct {
+ HeartbeatList map[string][]UptimeKumaHeartbeat `json:"heartbeatList"`
+ UptimeList map[string]float64 `json:"uptimeList"`
+}
+
+type MonitorStatus struct {
+ Name string `json:"name"`
+ Uptime float64 `json:"uptime"`
+ Status int `json:"status"`
+}
+
+var (
+ ErrUpstreamNon200 = errors.New("upstream non-200")
+ ErrTimeout = errors.New("context deadline exceeded")
+)
+
+func getAndDecode(ctx context.Context, client *http.Client, url string, dest interface{}) error {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
+ return ErrTimeout
+ }
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return ErrUpstreamNon200
+ }
+
+ return json.NewDecoder(resp.Body).Decode(dest)
+}
+
+func GetUptimeKumaStatus(c *gin.Context) {
+ common.OptionMapRWMutex.RLock()
+ uptimeKumaUrl := common.OptionMap["UptimeKumaUrl"]
+ slug := common.OptionMap["UptimeKumaSlug"]
+ common.OptionMapRWMutex.RUnlock()
+
+ if uptimeKumaUrl == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "未配置 Uptime Kuma URL",
+ })
+ return
+ }
+
+ if slug == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "未配置 Uptime Kuma Slug",
+ })
+ return
+ }
+
+ uptimeKumaUrl = strings.TrimSuffix(uptimeKumaUrl, "/")
+
+ ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
+ defer cancel()
+
+ client := &http.Client{}
+
+ statusPageUrl := fmt.Sprintf("%s/api/status-page/%s", uptimeKumaUrl, slug)
+ heartbeatUrl := fmt.Sprintf("%s/api/status-page/heartbeat/%s", uptimeKumaUrl, slug)
+
+ var (
+ statusData UptimeKumaStatusResponse
+ heartbeatData UptimeKumaHeartbeatResponse
+ )
+
+ g, gCtx := errgroup.WithContext(ctx)
+
+ g.Go(func() error {
+ return getAndDecode(gCtx, client, statusPageUrl, &statusData)
+ })
+
+ g.Go(func() error {
+ return getAndDecode(gCtx, client, heartbeatUrl, &heartbeatData)
+ })
+
+ if err := g.Wait(); err != nil {
+ switch err {
+ case ErrUpstreamNon200:
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "上游接口出现问题",
+ })
+ case ErrTimeout:
+ c.JSON(http.StatusRequestTimeout, gin.H{
+ "success": false,
+ "message": "请求上游接口超时",
+ })
+ default:
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ }
+ return
+ }
+
+ var monitors []MonitorStatus
+ for _, group := range statusData.PublicGroupList {
+ for _, monitor := range group.MonitorList {
+ monitorStatus := MonitorStatus{
+ Name: monitor.Name,
+ Uptime: 0.0,
+ Status: 0,
+ }
+
+ uptimeKey := fmt.Sprintf("%d_24", monitor.ID)
+ if uptime, exists := heartbeatData.UptimeList[uptimeKey]; exists {
+ monitorStatus.Uptime = uptime
+ }
+
+ heartbeatKey := fmt.Sprintf("%d", monitor.ID)
+ if heartbeats, exists := heartbeatData.HeartbeatList[heartbeatKey]; exists && len(heartbeats) > 0 {
+ latestHeartbeat := heartbeats[0]
+ monitorStatus.Status = latestHeartbeat.Status
+ }
+
+ monitors = append(monitors, monitorStatus)
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": monitors,
+ })
+}
\ No newline at end of file
diff --git a/model/option.go b/model/option.go
index 7bab819b..42949e8b 100644
--- a/model/option.go
+++ b/model/option.go
@@ -123,6 +123,8 @@ func InitOptionMap() {
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
common.OptionMap["ApiInfo"] = ""
+ common.OptionMap["UptimeKumaUrl"] = ""
+ common.OptionMap["UptimeKumaSlug"] = ""
// 自动添加所有注册的模型配置
modelConfigs := config.GlobalConfig.ExportAllConfigs()
diff --git a/router/api-router.go b/router/api-router.go
index 6251c8a2..0ab8be7f 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -16,6 +16,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/setup", controller.GetSetup)
apiRouter.POST("/setup", controller.PostSetup)
apiRouter.GET("/status", controller.GetStatus)
+ apiRouter.GET("/uptime/status", controller.GetUptimeKumaStatus)
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
apiRouter.GET("/notice", controller.GetNotice)
diff --git a/web/src/components/settings/DashboardSetting.js b/web/src/components/settings/DashboardSetting.js
index 25d2ff8c..649e0ffa 100644
--- a/web/src/components/settings/DashboardSetting.js
+++ b/web/src/components/settings/DashboardSetting.js
@@ -4,12 +4,15 @@ import { API, showError } from '../../helpers';
import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
import SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js';
import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
+import SettingsUptimeKuma from '../../pages/Setting/Dashboard/SettingsUptimeKuma.js';
const DashboardSetting = () => {
let [inputs, setInputs] = useState({
ApiInfo: '',
Announcements: '',
FAQ: '',
+ UptimeKumaUrl: '',
+ UptimeKumaSlug: '',
});
let [loading, setLoading] = useState(false);
@@ -63,6 +66,11 @@ const DashboardSetting = () => {
+
+ {/* Uptime Kuma 监控设置 */}
+
+
+
>
);
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index ee90e65c..d0077a73 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -941,7 +941,7 @@
"支付中..": "Paying",
"查看图片": "View pictures",
"并发限制": "Concurrency limit",
- "正常": "normal",
+ "正常": "Normal",
"周期": "cycle",
"同步频率10-20分钟": "Synchronization frequency 10-20 minutes",
"模型调用占比": "Model call proportion",
@@ -1589,7 +1589,6 @@
"颜色": "Color",
"标识颜色": "Identifier color",
"添加API": "Add API",
- "保存配置": "Save Configuration",
"API信息": "API Information",
"暂无API信息": "No API information",
"请输入API地址": "Please enter the API address",
@@ -1629,5 +1628,22 @@
"系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "System notice management, you can publish system notices and important messages (maximum 100, display latest 20 on the front end)",
"常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)",
"暂无常见问答": "No FAQ",
- "显示最新20条": "Display latest 20"
+ "显示最新20条": "Display latest 20",
+ "Uptime Kuma 服务地址": "Uptime Kuma service address",
+ "状态页面 Slug": "Status page slug",
+ "请输入 Uptime Kuma 服务的完整地址,例如:https://uptime.example.com": "Please enter the complete address of Uptime Kuma, for example: https://uptime.example.com",
+ "请输入状态页面的 slug 标识符,例如:my-status": "Please enter the slug identifier for the status page, for example: my-status",
+ "Uptime Kuma 服务地址不能为空": "Uptime Kuma service address cannot be empty",
+ "请输入有效的 URL 地址": "Please enter a valid URL address",
+ "状态页面 Slug 不能为空": "Status page slug cannot be empty",
+ "Slug 只能包含字母、数字、下划线和连字符": "Slug can only contain letters, numbers, underscores, and hyphens",
+ "请输入 Uptime Kuma 服务地址": "Please enter the Uptime Kuma service address",
+ "请输入状态页面 Slug": "Please enter the status page slug",
+ "配置": "Configure",
+ "服务监控地址,用于展示服务状态信息": "service monitoring address for displaying status information",
+ "服务可用性": "Service Status",
+ "可用率": "Availability",
+ "有异常": "Abnormal",
+ "暂无监控数据": "No monitoring data",
+ "请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings."
}
\ No newline at end of file
diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js
index 592f8d37..1292d337 100644
--- a/web/src/pages/Detail/index.js
+++ b/web/src/pages/Detail/index.js
@@ -15,7 +15,8 @@ import {
Empty,
Tag,
Timeline,
- Collapse
+ Collapse,
+ Progress
} from '@douyinfe/semi-ui';
import {
IconRefresh,
@@ -201,6 +202,20 @@ const Detail = (props) => {
tpm: []
});
+ // ========== Additional Refs for new cards ==========
+ const announcementScrollRef = useRef(null);
+ const faqScrollRef = useRef(null);
+ const uptimeScrollRef = useRef(null);
+
+ // ========== Additional State for scroll hints ==========
+ const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
+ const [showFaqScrollHint, setShowFaqScrollHint] = useState(false);
+ const [showUptimeScrollHint, setShowUptimeScrollHint] = useState(false);
+
+ // ========== Uptime data ==========
+ const [uptimeData, setUptimeData] = useState([]);
+ const [uptimeLoading, setUptimeLoading] = useState(false);
+
// ========== Props Destructuring ==========
const { username, model_name, start_timestamp, end_timestamp, channel } = inputs;
@@ -548,9 +563,26 @@ const Detail = (props) => {
}
}, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
+ const loadUptimeData = useCallback(async () => {
+ setUptimeLoading(true);
+ try {
+ const res = await API.get('/api/uptime/status');
+ const { success, message, data } = res.data;
+ if (success) {
+ setUptimeData(data || []);
+ } else {
+ showError(message);
+ }
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setUptimeLoading(false);
+ }
+ }, []);
+
const refresh = useCallback(async () => {
- await loadQuotaData();
- }, [loadQuotaData]);
+ await Promise.all([loadQuotaData(), loadUptimeData()]);
+ }, [loadQuotaData, loadUptimeData]);
const handleSearchConfirm = useCallback(() => {
refresh();
@@ -559,7 +591,8 @@ const Detail = (props) => {
const initChart = useCallback(async () => {
await loadQuotaData();
- }, [loadQuotaData]);
+ await loadUptimeData();
+ }, [loadQuotaData, loadUptimeData]);
const showSearchModal = useCallback(() => {
setSearchModalVisible(true);
@@ -596,23 +629,16 @@ const Detail = (props) => {
checkCardScrollable(ref, setHintFunction);
};
- // ========== Additional Refs for new cards ==========
- const announcementScrollRef = useRef(null);
- const faqScrollRef = useRef(null);
-
- // ========== Additional State for scroll hints ==========
- const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
- const [showFaqScrollHint, setShowFaqScrollHint] = useState(false);
-
// ========== Effects for scroll detection ==========
useEffect(() => {
const timer = setTimeout(() => {
checkApiScrollable();
checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint);
checkCardScrollable(faqScrollRef, setShowFaqScrollHint);
+ checkCardScrollable(uptimeScrollRef, setShowUptimeScrollHint);
}, 100);
return () => clearTimeout(timer);
- }, []);
+ }, [uptimeData]);
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
@@ -820,6 +846,29 @@ const Detail = (props) => {
{ color: 'red', label: t('异常'), type: 'error' }
], [t]);
+ const uptimeLegendData = useMemo(() => [
+ { color: 'green', label: t('正常'), status: 1 },
+ { color: 'red', label: t('异常'), status: 0 }
+ ], [t]);
+
+ const getUptimeStatusColor = useCallback((status) => {
+ switch (status) {
+ case 1:
+ return '#10b981'; // 绿色 - 正常
+ default:
+ return '#ef4444'; // 红色 - 异常
+ }
+ }, []);
+
+ const getUptimeStatusText = useCallback((status) => {
+ switch (status) {
+ case 1:
+ return t('可用率');
+ default:
+ return t('有异常');
+ }
+ }, [t]);
+
const apiInfoData = useMemo(() => {
return statusState?.status?.api_info || [];
}, [statusState?.status?.api_info]);
@@ -1160,7 +1209,7 @@ const Detail = (props) => {
{/* 常见问答卡片 */}
@@ -1208,6 +1257,99 @@ const Detail = (props) => {
/>
+
+ {/* 服务可用性卡片 */}
+
+
+
+ {t('服务可用性')}
+
+
+ {/* 图例 */}
+
+ {uptimeLegendData.map((legend, index) => (
+
+ ))}
+
+
}
+ onClick={loadUptimeData}
+ loading={uptimeLoading}
+ size="small"
+ theme="borderless"
+ className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
+ />
+
+
+ }
+ >
+
+
+ handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
+ >
+ {uptimeData.length > 0 ? (
+ uptimeData.map((monitor, idx) => (
+
+
+
+
{((monitor.uptime || 0) * 100).toFixed(2)}%
+
+
+
{getUptimeStatusText(monitor.status)}
+
+
+
+ ))
+ ) : (
+
+ }
+ darkModeImage={}
+ title={t('暂无监控数据')}
+ description={t('请联系管理员在系统设置中配置Uptime')}
+ style={{ padding: '12px' }}
+ />
+
+ )}
+
+
+
+
+
)}
diff --git a/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js b/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
index f4340e6e..4e87c697 100644
--- a/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
+++ b/web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
@@ -127,7 +127,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
const newList = apiInfoList.filter(api => api.id !== deletingApi.id);
setApiInfoList(newList);
setHasChanges(true);
- showSuccess('API信息已删除,请及时点击“保存配置”进行保存');
+ showSuccess('API信息已删除,请及时点击“保存设置”进行保存');
}
setShowDeleteModal(false);
setDeletingApi(null);
@@ -161,7 +161,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
setApiInfoList(newList);
setHasChanges(true);
setShowApiModal(false);
- showSuccess(editingApi ? 'API信息已更新,请及时点击“保存配置”进行保存' : 'API信息已添加,请及时点击“保存配置”进行保存');
+ showSuccess(editingApi ? 'API信息已更新,请及时点击“保存设置”进行保存' : 'API信息已添加,请及时点击“保存设置”进行保存');
} catch (error) {
showError('操作失败: ' + error.message);
} finally {
@@ -278,7 +278,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
setApiInfoList(newList);
setSelectedRowKeys([]);
setHasChanges(true);
- showSuccess(`已删除 ${selectedRowKeys.length} 个API信息,请及时点击“保存配置”进行保存`);
+ showSuccess(`已删除 ${selectedRowKeys.length} 个API信息,请及时点击“保存设置”进行保存`);
};
const renderHeader = () => (
@@ -321,7 +321,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
type='secondary'
className="!rounded-full w-full md:w-auto"
>
- {t('保存配置')}
+ {t('保存设置')}
diff --git a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
index 7e890ed7..3a9f71ca 100644
--- a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
+++ b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.js
@@ -218,7 +218,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
const newList = announcementsList.filter(item => item.id !== deletingAnnouncement.id);
setAnnouncementsList(newList);
setHasChanges(true);
- showSuccess('公告已删除,请及时点击“保存配置”进行保存');
+ showSuccess('公告已删除,请及时点击“保存设置”进行保存');
}
setShowDeleteModal(false);
setDeletingAnnouncement(null);
@@ -258,7 +258,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
setAnnouncementsList(newList);
setHasChanges(true);
setShowAnnouncementModal(false);
- showSuccess(editingAnnouncement ? '公告已更新,请及时点击“保存配置”进行保存' : '公告已添加,请及时点击“保存配置”进行保存');
+ showSuccess(editingAnnouncement ? '公告已更新,请及时点击“保存设置”进行保存' : '公告已添加,请及时点击“保存设置”进行保存');
} catch (error) {
showError('操作失败: ' + error.message);
} finally {
@@ -303,7 +303,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
setAnnouncementsList(newList);
setSelectedRowKeys([]);
setHasChanges(true);
- showSuccess(`已删除 ${selectedRowKeys.length} 个系统公告,请及时点击“保存配置”进行保存`);
+ showSuccess(`已删除 ${selectedRowKeys.length} 个系统公告,请及时点击“保存设置”进行保存`);
};
const renderHeader = () => (
@@ -346,7 +346,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
type='secondary'
className="!rounded-full w-full md:w-auto"
>
- {t('保存配置')}
+ {t('保存设置')}
diff --git a/web/src/pages/Setting/Dashboard/SettingsFAQ.js b/web/src/pages/Setting/Dashboard/SettingsFAQ.js
index f6cc8ac6..3e1ff805 100644
--- a/web/src/pages/Setting/Dashboard/SettingsFAQ.js
+++ b/web/src/pages/Setting/Dashboard/SettingsFAQ.js
@@ -162,7 +162,7 @@ const SettingsFAQ = ({ options, refresh }) => {
const newList = faqList.filter(item => item.id !== deletingFaq.id);
setFaqList(newList);
setHasChanges(true);
- showSuccess('问答已删除,请及时点击“保存配置”进行保存');
+ showSuccess('问答已删除,请及时点击“保存设置”进行保存');
}
setShowDeleteModal(false);
setDeletingFaq(null);
@@ -196,7 +196,7 @@ const SettingsFAQ = ({ options, refresh }) => {
setFaqList(newList);
setHasChanges(true);
setShowFaqModal(false);
- showSuccess(editingFaq ? '问答已更新,请及时点击“保存配置”进行保存' : '问答已添加,请及时点击“保存配置”进行保存');
+ showSuccess(editingFaq ? '问答已更新,请及时点击“保存设置”进行保存' : '问答已添加,请及时点击“保存设置”进行保存');
} catch (error) {
showError('操作失败: ' + error.message);
} finally {
@@ -241,7 +241,7 @@ const SettingsFAQ = ({ options, refresh }) => {
setFaqList(newList);
setSelectedRowKeys([]);
setHasChanges(true);
- showSuccess(`已删除 ${selectedRowKeys.length} 个常见问答,请及时点击“保存配置”进行保存`);
+ showSuccess(`已删除 ${selectedRowKeys.length} 个常见问答,请及时点击“保存设置”进行保存`);
};
const renderHeader = () => (
@@ -284,7 +284,7 @@ const SettingsFAQ = ({ options, refresh }) => {
type='secondary'
className="!rounded-full w-full md:w-auto"
>
- {t('保存配置')}
+ {t('保存设置')}
diff --git a/web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js b/web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js
new file mode 100644
index 00000000..3a7b4896
--- /dev/null
+++ b/web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js
@@ -0,0 +1,186 @@
+import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
+import {
+ Form,
+ Button,
+ Typography,
+ Row,
+ Col,
+} from '@douyinfe/semi-ui';
+import {
+ Save,
+ Activity
+} from 'lucide-react';
+import { API, showError, showSuccess } from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+const { Text } = Typography;
+
+const SettingsUptimeKuma = ({ options, refresh }) => {
+ const { t } = useTranslation();
+
+ const [loading, setLoading] = useState(false);
+ const formApiRef = useRef(null);
+
+ const initValues = useMemo(() => ({
+ uptimeKumaUrl: options?.UptimeKumaUrl || '',
+ uptimeKumaSlug: options?.UptimeKumaSlug || ''
+ }), [options?.UptimeKumaUrl, options?.UptimeKumaSlug]);
+
+ useEffect(() => {
+ if (formApiRef.current) {
+ formApiRef.current.setValues(initValues, { isOverride: true });
+ }
+ }, [initValues]);
+
+ const handleSave = async () => {
+ const api = formApiRef.current;
+ if (!api) {
+ showError(t('表单未初始化'));
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const { uptimeKumaUrl, uptimeKumaSlug } = await api.validate();
+
+ const trimmedUrl = (uptimeKumaUrl || '').trim();
+ const trimmedSlug = (uptimeKumaSlug || '').trim();
+
+ if (trimmedUrl === options?.UptimeKumaUrl && trimmedSlug === options?.UptimeKumaSlug) {
+ showSuccess(t('无需保存,配置未变动'));
+ return;
+ }
+
+ const [urlRes, slugRes] = await Promise.all([
+ trimmedUrl === options?.UptimeKumaUrl ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
+ key: 'UptimeKumaUrl',
+ value: trimmedUrl
+ }),
+ trimmedSlug === options?.UptimeKumaSlug ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
+ key: 'UptimeKumaSlug',
+ value: trimmedSlug
+ })
+ ]);
+
+ if (!urlRes.data.success) throw new Error(urlRes.data.message || t('URL 保存失败'));
+ if (!slugRes.data.success) throw new Error(slugRes.data.message || t('Slug 保存失败'));
+
+ showSuccess(t('Uptime Kuma 设置保存成功'));
+ refresh?.();
+ } catch (err) {
+ console.error(err);
+ showError(err.message || t('保存失败,请重试'));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const isValidUrl = useCallback((string) => {
+ try {
+ new URL(string);
+ return true;
+ } catch (_) {
+ return false;
+ }
+ }, []);
+
+ const renderHeader = () => (
+
+
+
+
+
+ }
+ theme='solid'
+ type='primary'
+ onClick={handleSave}
+ loading={loading}
+ className="!rounded-full"
+ >
+ {t('保存设置')}
+
+
+
+
+ );
+
+ return (
+
+
+
+ );
+};
+
+export default SettingsUptimeKuma;
\ No newline at end of file