✨ refactor(console_setting): migrate console settings to model_setting auto-injection
Backend - Introduce `setting/console_setting` package that defines `ConsoleSetting` struct with JSON tags and validation rules. - Register the new module with `config.GlobalConfig` to enable automatic injection/export of configuration values. - Remove legacy `setting/console.go` and the manual `OptionMap` hooks; clean up `model/option.go`. - Add `controller/console_migrate.go` providing `/api/option/migrate_console_setting` endpoint for one-off data migration. - Update controllers (`misc`, `option`, `uptime_kuma`) and router to consume namespaced keys `console_setting.*`. Frontend - Refactor dashboard pages (`SettingsAPIInfo`, `SettingsAnnouncements`, `SettingsFAQ`, `SettingsUptimeKuma`) and detail page to read/write the new keys. - Simplify `DashboardSetting.js` state to only include namespaced options. BREAKING CHANGE: All console-related option keys are now stored under `console_setting.*`. Run the migration endpoint once after deployment to preserve existing data.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
"one-api/setting/operation_setting"
|
"one-api/setting/operation_setting"
|
||||||
"one-api/setting/system_setting"
|
"one-api/setting/system_setting"
|
||||||
|
"one-api/setting/console_setting"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -79,9 +80,9 @@ func GetStatus(c *gin.Context) {
|
|||||||
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
|
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
|
||||||
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
|
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
|
||||||
"setup": constant.Setup,
|
"setup": constant.Setup,
|
||||||
"api_info": setting.GetApiInfo(),
|
"api_info": console_setting.GetApiInfo(),
|
||||||
"announcements": setting.GetAnnouncements(),
|
"announcements": console_setting.GetAnnouncements(),
|
||||||
"faq": setting.GetFAQ(),
|
"faq": console_setting.GetFAQ(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/setting"
|
"one-api/setting"
|
||||||
|
"one-api/setting/console_setting"
|
||||||
"one-api/setting/system_setting"
|
"one-api/setting/system_setting"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -119,8 +120,8 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "ApiInfo":
|
case "console_setting.api_info":
|
||||||
err = setting.ValidateApiInfo(option.Value)
|
err = console_setting.ValidateApiInfo(option.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -128,8 +129,8 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "Announcements":
|
case "console_setting.announcements":
|
||||||
err = setting.ValidateConsoleSettings(option.Value, "Announcements")
|
err = console_setting.ValidateConsoleSettings(option.Value, "Announcements")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -137,8 +138,8 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "FAQ":
|
case "console_setting.faq":
|
||||||
err = setting.ValidateConsoleSettings(option.Value, "FAQ")
|
err = console_setting.ValidateConsoleSettings(option.Value, "FAQ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/setting/console_setting"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -77,10 +77,9 @@ func getAndDecode(ctx context.Context, client *http.Client, url string, dest int
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetUptimeKumaStatus(c *gin.Context) {
|
func GetUptimeKumaStatus(c *gin.Context) {
|
||||||
common.OptionMapRWMutex.RLock()
|
cs := console_setting.GetConsoleSetting()
|
||||||
uptimeKumaUrl := common.OptionMap["UptimeKumaUrl"]
|
uptimeKumaUrl := cs.UptimeKumaUrl
|
||||||
slug := common.OptionMap["UptimeKumaSlug"]
|
slug := cs.UptimeKumaSlug
|
||||||
common.OptionMapRWMutex.RUnlock()
|
|
||||||
|
|
||||||
if uptimeKumaUrl == "" || slug == "" {
|
if uptimeKumaUrl == "" || slug == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -123,9 +123,6 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
||||||
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
||||||
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
|
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
|
||||||
common.OptionMap["ApiInfo"] = ""
|
|
||||||
common.OptionMap["UptimeKumaUrl"] = ""
|
|
||||||
common.OptionMap["UptimeKumaSlug"] = ""
|
|
||||||
|
|
||||||
// 自动添加所有注册的模型配置
|
// 自动添加所有注册的模型配置
|
||||||
modelConfigs := config.GlobalConfig.ExportAllConfigs()
|
modelConfigs := config.GlobalConfig.ExportAllConfigs()
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
package setting
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"one-api/common"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateConsoleSettings 验证控制台设置信息格式
|
|
||||||
func ValidateConsoleSettings(settingsStr string, settingType string) error {
|
|
||||||
if settingsStr == "" {
|
|
||||||
return nil // 空字符串是合法的
|
|
||||||
}
|
|
||||||
|
|
||||||
switch settingType {
|
|
||||||
case "ApiInfo":
|
|
||||||
return validateApiInfo(settingsStr)
|
|
||||||
case "Announcements":
|
|
||||||
return validateAnnouncements(settingsStr)
|
|
||||||
case "FAQ":
|
|
||||||
return validateFAQ(settingsStr)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("未知的设置类型:%s", settingType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateApiInfo 验证API信息格式
|
|
||||||
func validateApiInfo(apiInfoStr string) error {
|
|
||||||
var apiInfoList []map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil {
|
|
||||||
return fmt.Errorf("API信息格式错误:%s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证数组长度
|
|
||||||
if len(apiInfoList) > 50 {
|
|
||||||
return fmt.Errorf("API信息数量不能超过50个")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 允许的颜色值
|
|
||||||
validColors := map[string]bool{
|
|
||||||
"blue": true, "green": true, "cyan": true, "purple": true, "pink": true,
|
|
||||||
"red": true, "orange": true, "amber": true, "yellow": true, "lime": true,
|
|
||||||
"light-green": true, "teal": true, "light-blue": true, "indigo": true,
|
|
||||||
"violet": true, "grey": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL正则表达式,支持域名和IP地址格式
|
|
||||||
// 域名格式:https://example.com 或 https://sub.example.com:8080
|
|
||||||
// IP地址格式:https://192.168.1.1 或 https://192.168.1.1:8080
|
|
||||||
urlRegex := regexp.MustCompile(`^https?://(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?::[0-9]{1,5})?(?:/.*)?$`)
|
|
||||||
|
|
||||||
for i, apiInfo := range apiInfoList {
|
|
||||||
// 检查必填字段
|
|
||||||
urlStr, ok := apiInfo["url"].(string)
|
|
||||||
if !ok || urlStr == "" {
|
|
||||||
return fmt.Errorf("第%d个API信息缺少URL字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
route, ok := apiInfo["route"].(string)
|
|
||||||
if !ok || route == "" {
|
|
||||||
return fmt.Errorf("第%d个API信息缺少线路描述字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
description, ok := apiInfo["description"].(string)
|
|
||||||
if !ok || description == "" {
|
|
||||||
return fmt.Errorf("第%d个API信息缺少说明字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
color, ok := apiInfo["color"].(string)
|
|
||||||
if !ok || color == "" {
|
|
||||||
return fmt.Errorf("第%d个API信息缺少颜色字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证URL格式
|
|
||||||
if !urlRegex.MatchString(urlStr) {
|
|
||||||
return fmt.Errorf("第%d个API信息的URL格式不正确", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证URL可解析性
|
|
||||||
if _, err := url.Parse(urlStr); err != nil {
|
|
||||||
return fmt.Errorf("第%d个API信息的URL无法解析:%s", i+1, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证字段长度
|
|
||||||
if len(urlStr) > 500 {
|
|
||||||
return fmt.Errorf("第%d个API信息的URL长度不能超过500字符", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(route) > 100 {
|
|
||||||
return fmt.Errorf("第%d个API信息的线路描述长度不能超过100字符", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(description) > 200 {
|
|
||||||
return fmt.Errorf("第%d个API信息的说明长度不能超过200字符", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证颜色值
|
|
||||||
if !validColors[color] {
|
|
||||||
return fmt.Errorf("第%d个API信息的颜色值不合法", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查并过滤危险字符(防止XSS)
|
|
||||||
dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
|
|
||||||
for _, dangerous := range dangerousChars {
|
|
||||||
if strings.Contains(strings.ToLower(description), dangerous) {
|
|
||||||
return fmt.Errorf("第%d个API信息的说明包含不允许的内容", i+1)
|
|
||||||
}
|
|
||||||
if strings.Contains(strings.ToLower(route), dangerous) {
|
|
||||||
return fmt.Errorf("第%d个API信息的线路描述包含不允许的内容", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateApiInfo 保持向后兼容的函数
|
|
||||||
func ValidateApiInfo(apiInfoStr string) error {
|
|
||||||
return validateApiInfo(apiInfoStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetApiInfo 获取API信息列表
|
|
||||||
func GetApiInfo() []map[string]interface{} {
|
|
||||||
// 从OptionMap中获取API信息,如果不存在则返回空数组
|
|
||||||
common.OptionMapRWMutex.RLock()
|
|
||||||
apiInfoStr, exists := common.OptionMap["ApiInfo"]
|
|
||||||
common.OptionMapRWMutex.RUnlock()
|
|
||||||
|
|
||||||
if !exists || apiInfoStr == "" {
|
|
||||||
// 如果没有配置,返回空数组
|
|
||||||
return []map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析存储的API信息
|
|
||||||
var apiInfo []map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(apiInfoStr), &apiInfo); err != nil {
|
|
||||||
// 如果解析失败,返回空数组
|
|
||||||
return []map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateAnnouncements 验证系统公告格式
|
|
||||||
func validateAnnouncements(announcementsStr string) error {
|
|
||||||
var announcementsList []map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(announcementsStr), &announcementsList); err != nil {
|
|
||||||
return fmt.Errorf("系统公告格式错误:%s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证数组长度
|
|
||||||
if len(announcementsList) > 100 {
|
|
||||||
return fmt.Errorf("系统公告数量不能超过100个")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 允许的类型值
|
|
||||||
validTypes := map[string]bool{
|
|
||||||
"default": true, "ongoing": true, "success": true, "warning": true, "error": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, announcement := range announcementsList {
|
|
||||||
// 检查必填字段
|
|
||||||
content, ok := announcement["content"].(string)
|
|
||||||
if !ok || content == "" {
|
|
||||||
return fmt.Errorf("第%d个公告缺少内容字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查发布日期字段
|
|
||||||
publishDate, exists := announcement["publishDate"]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("第%d个公告缺少发布日期字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
publishDateStr, ok := publishDate.(string)
|
|
||||||
if !ok || publishDateStr == "" {
|
|
||||||
return fmt.Errorf("第%d个公告的发布日期不能为空", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证ISO日期格式
|
|
||||||
if _, err := time.Parse(time.RFC3339, publishDateStr); err != nil {
|
|
||||||
return fmt.Errorf("第%d个公告的发布日期格式错误", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证可选字段
|
|
||||||
if announcementType, exists := announcement["type"]; exists {
|
|
||||||
if typeStr, ok := announcementType.(string); ok {
|
|
||||||
if !validTypes[typeStr] {
|
|
||||||
return fmt.Errorf("第%d个公告的类型值不合法", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证字段长度
|
|
||||||
if len(content) > 500 {
|
|
||||||
return fmt.Errorf("第%d个公告的内容长度不能超过500字符", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if extra, exists := announcement["extra"]; exists {
|
|
||||||
if extraStr, ok := extra.(string); ok && len(extraStr) > 200 {
|
|
||||||
return fmt.Errorf("第%d个公告的说明长度不能超过200字符", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查并过滤危险字符(防止XSS)
|
|
||||||
dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
|
|
||||||
for _, dangerous := range dangerousChars {
|
|
||||||
if strings.Contains(strings.ToLower(content), dangerous) {
|
|
||||||
return fmt.Errorf("第%d个公告的内容包含不允许的内容", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateFAQ 验证常见问答格式
|
|
||||||
func validateFAQ(faqStr string) error {
|
|
||||||
var faqList []map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(faqStr), &faqList); err != nil {
|
|
||||||
return fmt.Errorf("常见问答格式错误:%s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证数组长度
|
|
||||||
if len(faqList) > 100 {
|
|
||||||
return fmt.Errorf("常见问答数量不能超过100个")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, faq := range faqList {
|
|
||||||
// 检查必填字段
|
|
||||||
title, ok := faq["title"].(string)
|
|
||||||
if !ok || title == "" {
|
|
||||||
return fmt.Errorf("第%d个问答缺少标题字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, ok := faq["content"].(string)
|
|
||||||
if !ok || content == "" {
|
|
||||||
return fmt.Errorf("第%d个问答缺少内容字段", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证字段长度
|
|
||||||
if len(title) > 200 {
|
|
||||||
return fmt.Errorf("第%d个问答的标题长度不能超过200字符", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(content) > 1000 {
|
|
||||||
return fmt.Errorf("第%d个问答的内容长度不能超过1000字符", i+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查并过滤危险字符(防止XSS)
|
|
||||||
dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
|
|
||||||
for _, dangerous := range dangerousChars {
|
|
||||||
if strings.Contains(strings.ToLower(title), dangerous) {
|
|
||||||
return fmt.Errorf("第%d个问答的标题包含不允许的内容", i+1)
|
|
||||||
}
|
|
||||||
if strings.Contains(strings.ToLower(content), dangerous) {
|
|
||||||
return fmt.Errorf("第%d个问答的内容包含不允许的内容", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAnnouncements 获取系统公告列表(返回最新的前20条)
|
|
||||||
func GetAnnouncements() []map[string]interface{} {
|
|
||||||
common.OptionMapRWMutex.RLock()
|
|
||||||
announcementsStr, exists := common.OptionMap["Announcements"]
|
|
||||||
common.OptionMapRWMutex.RUnlock()
|
|
||||||
|
|
||||||
if !exists || announcementsStr == "" {
|
|
||||||
return []map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var announcements []map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(announcementsStr), &announcements); err != nil {
|
|
||||||
return []map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按发布日期降序排序(最新的在前)
|
|
||||||
sort.Slice(announcements, func(i, j int) bool {
|
|
||||||
dateI, okI := announcements[i]["publishDate"].(string)
|
|
||||||
dateJ, okJ := announcements[j]["publishDate"].(string)
|
|
||||||
|
|
||||||
if !okI || !okJ {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
timeI, errI := time.Parse(time.RFC3339, dateI)
|
|
||||||
timeJ, errJ := time.Parse(time.RFC3339, dateJ)
|
|
||||||
|
|
||||||
if errI != nil || errJ != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeI.After(timeJ)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 限制返回前20条
|
|
||||||
if len(announcements) > 20 {
|
|
||||||
announcements = announcements[:20]
|
|
||||||
}
|
|
||||||
|
|
||||||
return announcements
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFAQ 获取常见问答列表
|
|
||||||
func GetFAQ() []map[string]interface{} {
|
|
||||||
common.OptionMapRWMutex.RLock()
|
|
||||||
faqStr, exists := common.OptionMap["FAQ"]
|
|
||||||
common.OptionMapRWMutex.RUnlock()
|
|
||||||
|
|
||||||
if !exists || faqStr == "" {
|
|
||||||
return []map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var faq []map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(faqStr), &faq); err != nil {
|
|
||||||
return []map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return faq
|
|
||||||
}
|
|
||||||
33
setting/console_setting/config.go
Normal file
33
setting/console_setting/config.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package console_setting
|
||||||
|
|
||||||
|
import "one-api/setting/config"
|
||||||
|
|
||||||
|
type ConsoleSetting struct {
|
||||||
|
ApiInfo string `json:"api_info"` // 控制台 API 信息 (JSON 数组字符串)
|
||||||
|
UptimeKumaUrl string `json:"uptime_kuma_url"` // Uptime Kuma 服务地址(如 https://status.example.com )
|
||||||
|
UptimeKumaSlug string `json:"uptime_kuma_slug"` // Uptime Kuma Status Page Slug
|
||||||
|
Announcements string `json:"announcements"` // 系统公告 (JSON 数组字符串)
|
||||||
|
FAQ string `json:"faq"` // 常见问题 (JSON 数组字符串)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
var defaultConsoleSetting = ConsoleSetting{
|
||||||
|
ApiInfo: "",
|
||||||
|
UptimeKumaUrl: "",
|
||||||
|
UptimeKumaSlug: "",
|
||||||
|
Announcements: "",
|
||||||
|
FAQ: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局实例
|
||||||
|
var consoleSetting = defaultConsoleSetting
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册到全局配置管理器,键名为 console_setting
|
||||||
|
config.GlobalConfig.Register("console_setting", &consoleSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConsoleSetting 获取 ConsoleSetting 配置实例
|
||||||
|
func GetConsoleSetting() *ConsoleSetting {
|
||||||
|
return &consoleSetting
|
||||||
|
}
|
||||||
212
setting/console_setting/validation.go
Normal file
212
setting/console_setting/validation.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package console_setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateConsoleSettings 验证控制台设置信息格式
|
||||||
|
func ValidateConsoleSettings(settingsStr string, settingType string) error {
|
||||||
|
if settingsStr == "" {
|
||||||
|
return nil // 空字符串是合法的
|
||||||
|
}
|
||||||
|
|
||||||
|
switch settingType {
|
||||||
|
case "ApiInfo":
|
||||||
|
return validateApiInfo(settingsStr)
|
||||||
|
case "Announcements":
|
||||||
|
return validateAnnouncements(settingsStr)
|
||||||
|
case "FAQ":
|
||||||
|
return validateFAQ(settingsStr)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未知的设置类型:%s", settingType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateApiInfo 验证API信息格式
|
||||||
|
func validateApiInfo(apiInfoStr string) error {
|
||||||
|
var apiInfoList []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil {
|
||||||
|
return fmt.Errorf("API信息格式错误:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数组长度
|
||||||
|
if len(apiInfoList) > 50 {
|
||||||
|
return fmt.Errorf("API信息数量不能超过50个")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许的颜色值
|
||||||
|
validColors := map[string]bool{
|
||||||
|
"blue": true, "green": true, "cyan": true, "purple": true, "pink": true,
|
||||||
|
"red": true, "orange": true, "amber": true, "yellow": true, "lime": true,
|
||||||
|
"light-green": true, "teal": true, "light-blue": true, "indigo": true,
|
||||||
|
"violet": true, "grey": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL 正则,支持域名 / IP
|
||||||
|
urlRegex := regexp.MustCompile(`^https?://(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?:\:[0-9]{1,5})?(?:/.*)?$`)
|
||||||
|
|
||||||
|
for i, apiInfo := range apiInfoList {
|
||||||
|
urlStr, ok := apiInfo["url"].(string)
|
||||||
|
if !ok || urlStr == "" {
|
||||||
|
return fmt.Errorf("第%d个API信息缺少URL字段", i+1)
|
||||||
|
}
|
||||||
|
route, ok := apiInfo["route"].(string)
|
||||||
|
if !ok || route == "" {
|
||||||
|
return fmt.Errorf("第%d个API信息缺少线路描述字段", i+1)
|
||||||
|
}
|
||||||
|
description, ok := apiInfo["description"].(string)
|
||||||
|
if !ok || description == "" {
|
||||||
|
return fmt.Errorf("第%d个API信息缺少说明字段", i+1)
|
||||||
|
}
|
||||||
|
color, ok := apiInfo["color"].(string)
|
||||||
|
if !ok || color == "" {
|
||||||
|
return fmt.Errorf("第%d个API信息缺少颜色字段", i+1)
|
||||||
|
}
|
||||||
|
if !urlRegex.MatchString(urlStr) {
|
||||||
|
return fmt.Errorf("第%d个API信息的URL格式不正确", i+1)
|
||||||
|
}
|
||||||
|
if _, err := url.Parse(urlStr); err != nil {
|
||||||
|
return fmt.Errorf("第%d个API信息的URL无法解析:%s", i+1, err.Error())
|
||||||
|
}
|
||||||
|
if len(urlStr) > 500 {
|
||||||
|
return fmt.Errorf("第%d个API信息的URL长度不能超过500字符", i+1)
|
||||||
|
}
|
||||||
|
if len(route) > 100 {
|
||||||
|
return fmt.Errorf("第%d个API信息的线路描述长度不能超过100字符", i+1)
|
||||||
|
}
|
||||||
|
if len(description) > 200 {
|
||||||
|
return fmt.Errorf("第%d个API信息的说明长度不能超过200字符", i+1)
|
||||||
|
}
|
||||||
|
if !validColors[color] {
|
||||||
|
return fmt.Errorf("第%d个API信息的颜色值不合法", i+1)
|
||||||
|
}
|
||||||
|
dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
|
||||||
|
for _, d := range dangerousChars {
|
||||||
|
lower := strings.ToLower(description)
|
||||||
|
if strings.Contains(lower, d) || strings.Contains(strings.ToLower(route), d) {
|
||||||
|
return fmt.Errorf("第%d个API信息包含不允许的内容", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateApiInfo 保持向后兼容
|
||||||
|
func ValidateApiInfo(apiInfoStr string) error {
|
||||||
|
return validateApiInfo(apiInfoStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApiInfo 获取 API 信息列表
|
||||||
|
func GetApiInfo() []map[string]interface{} {
|
||||||
|
apiInfoStr := GetConsoleSetting().ApiInfo
|
||||||
|
if apiInfoStr == "" {
|
||||||
|
return []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
var apiInfo []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(apiInfoStr), &apiInfo); err != nil {
|
||||||
|
return []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
return apiInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------- 公告 / FAQ -----------------
|
||||||
|
|
||||||
|
func validateAnnouncements(announcementsStr string) error {
|
||||||
|
var list []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(announcementsStr), &list); err != nil {
|
||||||
|
return fmt.Errorf("系统公告格式错误:%s", err.Error())
|
||||||
|
}
|
||||||
|
if len(list) > 100 {
|
||||||
|
return fmt.Errorf("系统公告数量不能超过100个")
|
||||||
|
}
|
||||||
|
validTypes := map[string]bool{
|
||||||
|
"default": true, "ongoing": true, "success": true, "warning": true, "error": true,
|
||||||
|
}
|
||||||
|
for i, ann := range list {
|
||||||
|
content, ok := ann["content"].(string)
|
||||||
|
if !ok || content == "" {
|
||||||
|
return fmt.Errorf("第%d个公告缺少内容字段", i+1)
|
||||||
|
}
|
||||||
|
publishDateAny, exists := ann["publishDate"]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("第%d个公告缺少发布日期字段", i+1)
|
||||||
|
}
|
||||||
|
publishDateStr, ok := publishDateAny.(string)
|
||||||
|
if !ok || publishDateStr == "" {
|
||||||
|
return fmt.Errorf("第%d个公告的发布日期不能为空", i+1)
|
||||||
|
}
|
||||||
|
if _, err := time.Parse(time.RFC3339, publishDateStr); err != nil {
|
||||||
|
return fmt.Errorf("第%d个公告的发布日期格式错误", i+1)
|
||||||
|
}
|
||||||
|
if t, exists := ann["type"]; exists {
|
||||||
|
if typeStr, ok := t.(string); ok {
|
||||||
|
if !validTypes[typeStr] {
|
||||||
|
return fmt.Errorf("第%d个公告的类型值不合法", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(content) > 500 {
|
||||||
|
return fmt.Errorf("第%d个公告的内容长度不能超过500字符", i+1)
|
||||||
|
}
|
||||||
|
if extra, exists := ann["extra"]; exists {
|
||||||
|
if extraStr, ok := extra.(string); ok && len(extraStr) > 200 {
|
||||||
|
return fmt.Errorf("第%d个公告的说明长度不能超过200字符", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFAQ(faqStr string) error {
|
||||||
|
var list []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(faqStr), &list); err != nil {
|
||||||
|
return fmt.Errorf("FAQ信息格式错误:%s", err.Error())
|
||||||
|
}
|
||||||
|
if len(list) > 100 {
|
||||||
|
return fmt.Errorf("FAQ数量不能超过100个")
|
||||||
|
}
|
||||||
|
for i, faq := range list {
|
||||||
|
question, ok := faq["question"].(string)
|
||||||
|
if !ok || question == "" {
|
||||||
|
return fmt.Errorf("第%d个FAQ缺少问题字段", i+1)
|
||||||
|
}
|
||||||
|
answer, ok := faq["answer"].(string)
|
||||||
|
if !ok || answer == "" {
|
||||||
|
return fmt.Errorf("第%d个FAQ缺少答案字段", i+1)
|
||||||
|
}
|
||||||
|
if len(question) > 200 {
|
||||||
|
return fmt.Errorf("第%d个FAQ的问题长度不能超过200字符", i+1)
|
||||||
|
}
|
||||||
|
if len(answer) > 1000 {
|
||||||
|
return fmt.Errorf("第%d个FAQ的答案长度不能超过1000字符", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAnnouncements 获取系统公告
|
||||||
|
func GetAnnouncements() []map[string]interface{} {
|
||||||
|
annStr := GetConsoleSetting().Announcements
|
||||||
|
if annStr == "" {
|
||||||
|
return []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
var ann []map[string]interface{}
|
||||||
|
_ = json.Unmarshal([]byte(annStr), &ann)
|
||||||
|
return ann
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFAQ 获取常见问题
|
||||||
|
func GetFAQ() []map[string]interface{} {
|
||||||
|
faqStr := GetConsoleSetting().FAQ
|
||||||
|
if faqStr == "" {
|
||||||
|
return []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
var faq []map[string]interface{}
|
||||||
|
_ = json.Unmarshal([]byte(faqStr), &faq)
|
||||||
|
return faq
|
||||||
|
}
|
||||||
@@ -8,11 +8,11 @@ import SettingsUptimeKuma from '../../pages/Setting/Dashboard/SettingsUptimeKuma
|
|||||||
|
|
||||||
const DashboardSetting = () => {
|
const DashboardSetting = () => {
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
ApiInfo: '',
|
'console_setting.api_info': '',
|
||||||
Announcements: '',
|
'console_setting.announcements': '',
|
||||||
FAQ: '',
|
'console_setting.faq': '',
|
||||||
UptimeKumaUrl: '',
|
'console_setting.uptime_kuma_url': '',
|
||||||
UptimeKumaSlug: '',
|
'console_setting.uptime_kuma_slug': '',
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
|
|||||||
@@ -1231,10 +1231,10 @@ const Detail = (props) => {
|
|||||||
{faqData.map((item, index) => (
|
{faqData.map((item, index) => (
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key={index}
|
key={index}
|
||||||
header={item.title}
|
header={item.question}
|
||||||
itemKey={index.toString()}
|
itemKey={index.toString()}
|
||||||
>
|
>
|
||||||
<p>{item.content}</p>
|
<p>{item.answer}</p>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
))}
|
))}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const apiInfoJson = JSON.stringify(apiInfoList);
|
const apiInfoJson = JSON.stringify(apiInfoList);
|
||||||
await updateOption('ApiInfo', apiInfoJson);
|
await updateOption('console_setting.api_info', apiInfoJson);
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API信息更新失败', error);
|
console.error('API信息更新失败', error);
|
||||||
@@ -185,10 +185,11 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (options.ApiInfo !== undefined) {
|
const apiInfoStr = options['console_setting.api_info'] ?? options.ApiInfo;
|
||||||
parseApiInfo(options.ApiInfo);
|
if (apiInfoStr !== undefined) {
|
||||||
|
parseApiInfo(apiInfoStr);
|
||||||
}
|
}
|
||||||
}, [options.ApiInfo]);
|
}, [options['console_setting.api_info'], options.ApiInfo]);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const announcementsJson = JSON.stringify(announcementsList);
|
const announcementsJson = JSON.stringify(announcementsList);
|
||||||
await updateOption('Announcements', announcementsJson);
|
await updateOption('console_setting.announcements', announcementsJson);
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('系统公告更新失败', error);
|
console.error('系统公告更新失败', error);
|
||||||
@@ -288,10 +288,11 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (options.Announcements !== undefined) {
|
const annStr = options['console_setting.announcements'] ?? options.Announcements;
|
||||||
parseAnnouncements(options.Announcements);
|
if (annStr !== undefined) {
|
||||||
|
parseAnnouncements(annStr);
|
||||||
}
|
}
|
||||||
}, [options.Announcements]);
|
}, [options['console_setting.announcements'], options.Announcements]);
|
||||||
|
|
||||||
const handleBatchDelete = () => {
|
const handleBatchDelete = () => {
|
||||||
if (selectedRowKeys.length === 0) {
|
if (selectedRowKeys.length === 0) {
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [faqForm, setFaqForm] = useState({
|
const [faqForm, setFaqForm] = useState({
|
||||||
title: '',
|
question: '',
|
||||||
content: ''
|
answer: ''
|
||||||
});
|
});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
@@ -47,8 +47,8 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t('问题标题'),
|
title: t('问题标题'),
|
||||||
dataIndex: 'title',
|
dataIndex: 'question',
|
||||||
key: 'title',
|
key: 'question',
|
||||||
render: (text) => (
|
render: (text) => (
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: '300px',
|
maxWidth: '300px',
|
||||||
@@ -61,8 +61,8 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('回答内容'),
|
title: t('回答内容'),
|
||||||
dataIndex: 'content',
|
dataIndex: 'answer',
|
||||||
key: 'content',
|
key: 'answer',
|
||||||
render: (text) => (
|
render: (text) => (
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: '400px',
|
maxWidth: '400px',
|
||||||
@@ -124,7 +124,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const faqJson = JSON.stringify(faqList);
|
const faqJson = JSON.stringify(faqList);
|
||||||
await updateOption('FAQ', faqJson);
|
await updateOption('console_setting.faq', faqJson);
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('常见问答更新失败', error);
|
console.error('常见问答更新失败', error);
|
||||||
@@ -137,8 +137,8 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
const handleAddFaq = () => {
|
const handleAddFaq = () => {
|
||||||
setEditingFaq(null);
|
setEditingFaq(null);
|
||||||
setFaqForm({
|
setFaqForm({
|
||||||
title: '',
|
question: '',
|
||||||
content: ''
|
answer: ''
|
||||||
});
|
});
|
||||||
setShowFaqModal(true);
|
setShowFaqModal(true);
|
||||||
};
|
};
|
||||||
@@ -146,8 +146,8 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
const handleEditFaq = (faq) => {
|
const handleEditFaq = (faq) => {
|
||||||
setEditingFaq(faq);
|
setEditingFaq(faq);
|
||||||
setFaqForm({
|
setFaqForm({
|
||||||
title: faq.title,
|
question: faq.question,
|
||||||
content: faq.content
|
answer: faq.answer
|
||||||
});
|
});
|
||||||
setShowFaqModal(true);
|
setShowFaqModal(true);
|
||||||
};
|
};
|
||||||
@@ -169,7 +169,7 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveFaq = async () => {
|
const handleSaveFaq = async () => {
|
||||||
if (!faqForm.title || !faqForm.content) {
|
if (!faqForm.question || !faqForm.answer) {
|
||||||
showError('请填写完整的问答信息');
|
showError('请填写完整的问答信息');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -226,10 +226,10 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (options.FAQ !== undefined) {
|
if (options['console_setting.faq'] !== undefined) {
|
||||||
parseFAQ(options.FAQ);
|
parseFAQ(options['console_setting.faq']);
|
||||||
}
|
}
|
||||||
}, [options.FAQ]);
|
}, [options['console_setting.faq']]);
|
||||||
|
|
||||||
const handleBatchDelete = () => {
|
const handleBatchDelete = () => {
|
||||||
if (selectedRowKeys.length === 0) {
|
if (selectedRowKeys.length === 0) {
|
||||||
@@ -372,21 +372,21 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
>
|
>
|
||||||
<Form layout='vertical' initValues={faqForm} key={editingFaq ? editingFaq.id : 'new'}>
|
<Form layout='vertical' initValues={faqForm} key={editingFaq ? editingFaq.id : 'new'}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field='title'
|
field='question'
|
||||||
label={t('问题标题')}
|
label={t('问题标题')}
|
||||||
placeholder={t('请输入问题标题')}
|
placeholder={t('请输入问题标题')}
|
||||||
maxLength={200}
|
maxLength={200}
|
||||||
rules={[{ required: true, message: t('请输入问题标题') }]}
|
rules={[{ required: true, message: t('请输入问题标题') }]}
|
||||||
onChange={(value) => setFaqForm({ ...faqForm, title: value })}
|
onChange={(value) => setFaqForm({ ...faqForm, question: value })}
|
||||||
/>
|
/>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
field='content'
|
field='answer'
|
||||||
label={t('回答内容')}
|
label={t('回答内容')}
|
||||||
placeholder={t('请输入回答内容')}
|
placeholder={t('请输入回答内容')}
|
||||||
maxCount={1000}
|
maxCount={1000}
|
||||||
rows={6}
|
rows={6}
|
||||||
rules={[{ required: true, message: t('请输入回答内容') }]}
|
rules={[{ required: true, message: t('请输入回答内容') }]}
|
||||||
onChange={(value) => setFaqForm({ ...faqForm, content: value })}
|
onChange={(value) => setFaqForm({ ...faqForm, answer: value })}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
|||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
|
|
||||||
const initValues = useMemo(() => ({
|
const initValues = useMemo(() => ({
|
||||||
uptimeKumaUrl: options?.UptimeKumaUrl || '',
|
uptimeKumaUrl: options?.['console_setting.uptime_kuma_url'] || '',
|
||||||
uptimeKumaSlug: options?.UptimeKumaSlug || ''
|
uptimeKumaSlug: options?.['console_setting.uptime_kuma_slug'] || ''
|
||||||
}), [options?.UptimeKumaUrl, options?.UptimeKumaSlug]);
|
}), [options?.['console_setting.uptime_kuma_url'], options?.['console_setting.uptime_kuma_slug']]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formApiRef.current) {
|
if (formApiRef.current) {
|
||||||
@@ -46,18 +46,18 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
|||||||
const trimmedUrl = (uptimeKumaUrl || '').trim();
|
const trimmedUrl = (uptimeKumaUrl || '').trim();
|
||||||
const trimmedSlug = (uptimeKumaSlug || '').trim();
|
const trimmedSlug = (uptimeKumaSlug || '').trim();
|
||||||
|
|
||||||
if (trimmedUrl === options?.UptimeKumaUrl && trimmedSlug === options?.UptimeKumaSlug) {
|
if (trimmedUrl === options?.['console_setting.uptime_kuma_url'] && trimmedSlug === options?.['console_setting.uptime_kuma_slug']) {
|
||||||
showSuccess(t('无需保存,配置未变动'));
|
showSuccess(t('无需保存,配置未变动'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [urlRes, slugRes] = await Promise.all([
|
const [urlRes, slugRes] = await Promise.all([
|
||||||
trimmedUrl === options?.UptimeKumaUrl ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
|
trimmedUrl === options?.['console_setting.uptime_kuma_url'] ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
|
||||||
key: 'UptimeKumaUrl',
|
key: 'console_setting.uptime_kuma_url',
|
||||||
value: trimmedUrl
|
value: trimmedUrl
|
||||||
}),
|
}),
|
||||||
trimmedSlug === options?.UptimeKumaSlug ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
|
trimmedSlug === options?.['console_setting.uptime_kuma_slug'] ? Promise.resolve({ data: { success: true } }) : API.put('/api/option/', {
|
||||||
key: 'UptimeKumaSlug',
|
key: 'console_setting.uptime_kuma_slug',
|
||||||
value: trimmedSlug
|
value: trimmedSlug
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user