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:
Apple\Apple
2025-06-14 00:40:29 +08:00
parent 35313ae0d6
commit c554015526
13 changed files with 304 additions and 386 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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{

View File

@@ -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()

View File

@@ -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
}

View 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
}

View 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
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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 = [
{ {

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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
}) })
]); ]);