Browse Source

✨ feat: Add console announcements and FAQ management system

- Add SettingsAnnouncements component with full CRUD operations for system announcements
  * Support multiple announcement types (default, ongoing, success, warning, error)
  * Include publish date, content, type classification and additional notes
  * Implement batch operations and pagination for better data management
  * Add real-time preview with relative time display and date formatting

- Add SettingsFAQ component for comprehensive FAQ management
  * Support question-answer pairs with rich text content
  * Include full editing, deletion and creation capabilities
  * Implement batch delete operations and paginated display
  * Add validation for complete Q&A information

- Integrate announcement and FAQ modules into DashboardSetting
  * Add unified configuration interface in admin console
  * Implement auto-refresh functionality for real-time updates
  * Add loading states and error handling for better UX

- Enhance backend API support in controller and setting modules
  * Add validation functions for console settings
  * Include time and sorting utilities for announcement management
  * Extend API endpoints for announcement and FAQ data persistence

- Improve frontend infrastructure
  * Add new translation keys for internationalization support
  * Update utility functions for date/time formatting
  * Enhance CSS styles for better component presentation
  * Add icons and visual improvements for announcements and FAQ sections

This implementation provides administrators with comprehensive tools to manage
system-wide announcements and user FAQ content through an intuitive console interface.
Apple\Apple 9 tháng trước cách đây
mục cha
commit
26b70d6a25

+ 2 - 0
controller/misc.go

@@ -75,6 +75,8 @@ func GetStatus(c *gin.Context) {
 			"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
 			"setup":                       constant.Setup,
 			"api_info":                    setting.GetApiInfo(),
+			"announcements":               setting.GetAnnouncements(),
+			"faq":                         setting.GetFAQ(),
 		},
 	})
 	return

+ 18 - 0
controller/option.go

@@ -128,6 +128,24 @@ func UpdateOption(c *gin.Context) {
 			})
 			return
 		}
+	case "Announcements":
+		err = setting.ValidateConsoleSettings(option.Value, "Announcements")
+		if err != nil {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": err.Error(),
+			})
+			return
+		}
+	case "FAQ":
+		err = setting.ValidateConsoleSettings(option.Value, "FAQ")
+		if err != nil {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": err.Error(),
+			})
+			return
+		}
 	}
 	err = model.UpdateOption(option.Key, option.Value)
 	if err != nil {

+ 186 - 0
setting/console.go

@@ -6,7 +6,9 @@ import (
 	"net/url"
 	"one-api/common"
 	"regexp"
+	"sort"
 	"strings"
+	"time"
 )
 
 // ValidateConsoleSettings 验证控制台设置信息格式
@@ -18,6 +20,10 @@ func ValidateConsoleSettings(settingsStr string, settingType string) error {
 	switch settingType {
 	case "ApiInfo":
 		return validateApiInfo(settingsStr)
+	case "Announcements":
+		return validateAnnouncements(settingsStr)
+	case "FAQ":
+		return validateFAQ(settingsStr)
 	default:
 		return fmt.Errorf("未知的设置类型:%s", settingType)
 	}
@@ -138,4 +144,184 @@ func GetApiInfo() []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
 } 

+ 14 - 0
web/src/components/settings/DashboardSetting.js

@@ -2,10 +2,14 @@ import React, { useEffect, useState } from 'react';
 import { Card, Spin } from '@douyinfe/semi-ui';
 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';
 
 const DashboardSetting = () => {
   let [inputs, setInputs] = useState({
     ApiInfo: '',
+    Announcements: '',
+    FAQ: '',
   });
 
   let [loading, setLoading] = useState(false);
@@ -49,6 +53,16 @@ const DashboardSetting = () => {
         <Card style={{ marginTop: '10px' }}>
           <SettingsAPIInfo options={inputs} refresh={onRefresh} />
         </Card>
+
+        {/* 系统公告管理 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsAnnouncements options={inputs} refresh={onRefresh} />
+        </Card>
+
+        {/* 常见问答管理 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsFAQ options={inputs} refresh={onRefresh} />
+        </Card>
       </Spin>
     </>
   );

+ 63 - 0
web/src/helpers/utils.js

@@ -446,3 +446,66 @@ export const getLastAssistantMessage = (messages) => {
   }
   return null;
 };
+
+// 计算相对时间(几天前、几小时前等)
+export const getRelativeTime = (publishDate) => {
+  if (!publishDate) return '';
+
+  const now = new Date();
+  const pubDate = new Date(publishDate);
+
+  // 如果日期无效,返回原始字符串
+  if (isNaN(pubDate.getTime())) return publishDate;
+
+  const diffMs = now.getTime() - pubDate.getTime();
+  const diffSeconds = Math.floor(diffMs / 1000);
+  const diffMinutes = Math.floor(diffSeconds / 60);
+  const diffHours = Math.floor(diffMinutes / 60);
+  const diffDays = Math.floor(diffHours / 24);
+  const diffWeeks = Math.floor(diffDays / 7);
+  const diffMonths = Math.floor(diffDays / 30);
+  const diffYears = Math.floor(diffDays / 365);
+
+  // 如果是未来时间,显示具体日期
+  if (diffMs < 0) {
+    return formatDateString(pubDate);
+  }
+
+  // 根据时间差返回相应的描述
+  if (diffSeconds < 60) {
+    return '刚刚';
+  } else if (diffMinutes < 60) {
+    return `${diffMinutes} 分钟前`;
+  } else if (diffHours < 24) {
+    return `${diffHours} 小时前`;
+  } else if (diffDays < 7) {
+    return `${diffDays} 天前`;
+  } else if (diffWeeks < 4) {
+    return `${diffWeeks} 周前`;
+  } else if (diffMonths < 12) {
+    return `${diffMonths} 个月前`;
+  } else if (diffYears < 2) {
+    return '1 年前';
+  } else {
+    // 超过2年显示具体日期
+    return formatDateString(pubDate);
+  }
+};
+
+// 格式化日期字符串
+export const formatDateString = (date) => {
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0');
+  const day = String(date.getDate()).padStart(2, '0');
+  return `${year}-${month}-${day}`;
+};
+
+// 格式化日期时间字符串(包含时间)
+export const formatDateTimeString = (date) => {
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0');
+  const day = String(date.getDate()).padStart(2, '0');
+  const hours = String(date.getHours()).padStart(2, '0');
+  const minutes = String(date.getMinutes()).padStart(2, '0');
+  return `${year}-${month}-${day} ${hours}:${minutes}`;
+};

+ 34 - 5
web/src/i18n/locales/en.json

@@ -510,7 +510,7 @@
   "此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
   "模型映射": "Model mapping",
   "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
-  "默认": "default",
+  "默认": "Default",
   "图片演示": "Image demo",
   "注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41",
   "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment",
@@ -882,7 +882,7 @@
   "线路监控": "line monitoring",
   "查看全部": "View all",
   "高延迟": "high latency",
-  "异常": "abnormal",
+  "异常": "Abnormal",
   "的未命名令牌": "unnamed token",
   "令牌更新成功!": "Token updated successfully!",
   "(origin) Discord原链接": "(origin) Discord original link",
@@ -1584,14 +1584,13 @@
   "模型数据分析": "Model Data Analysis",
   "搜索无结果": "No results found",
   "仪表盘配置": "Dashboard Configuration",
-  "API信息管理,可以配置多个API地址用于状态展示和负载均衡": "API information management, you can configure multiple API addresses for status display and load balancing",
+  "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "API information management, you can configure multiple API addresses for status display and load balancing (maximum 50)",
   "线路描述": "Route description",
   "颜色": "Color",
   "标识颜色": "Identifier color",
   "添加API": "Add API",
   "保存配置": "Save Configuration",
   "API信息": "API Information",
-  "暂无API信息配置": "No API information configured",
   "暂无API信息": "No API information",
   "请输入API地址": "Please enter the API address",
   "请输入线路描述": "Please enter the route description",
@@ -1599,6 +1598,36 @@
   "请输入说明": "Please enter the description",
   "如:香港线路": "e.g. Hong Kong line",
   "请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.",
+  "请联系管理员在系统设置中配置公告信息": "Please contact the administrator to configure notice information in the system settings.",
+  "请联系管理员在系统设置中配置常见问答": "Please contact the administrator to configure FAQ information in the system settings.",
   "确定要删除此API信息吗?": "Are you sure you want to delete this API information?",
-  "测速": "Speed Test"
+  "测速": "Speed Test",
+  "批量删除": "Batch Delete",
+  "常见问答": "FAQ",
+  "进行中": "Ongoing",
+  "警告": "Warning",
+  "添加公告": "Add Notice",
+  "编辑公告": "Edit Notice",
+  "公告内容": "Notice Content",
+  "请输入公告内容": "Please enter the notice content",
+  "发布日期": "Publish Date",
+  "请选择发布日期": "Please select the publish date",
+  "发布时间": "Publish Time",
+  "公告类型": "Notice Type",
+  "说明信息": "Description",
+  "可选,公告的补充说明": "Optional, additional information for the notice",
+  "确定要删除此公告吗?": "Are you sure you want to delete this notice?",
+  "系统公告管理,可以发布系统通知和重要消息": "System notice management, you can publish system notices and important messages",
+  "暂无系统公告": "No system notice",
+  "添加问答": "Add FAQ",
+  "编辑问答": "Edit FAQ",
+  "问题标题": "Question Title",
+  "请输入问题标题": "Please enter the question title",
+  "回答内容": "Answer Content",
+  "请输入回答内容": "Please enter the answer content",
+  "确定要删除此问答吗?": "Are you sure you want to delete this FAQ?",
+  "系统公告管理,可以发布系统通知和重要消息(最多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"
 }

+ 6 - 6
web/src/index.css

@@ -301,12 +301,12 @@ code {
   font-size: 1.1em;
 }
 
-/* API信息卡片样式 */
-.api-info-container {
+/* 卡片内容容器通用样式 */
+.card-content-container {
   position: relative;
 }
 
-.api-info-fade-indicator {
+.card-content-fade-indicator {
   position: absolute;
   bottom: 0;
   left: 0;
@@ -374,8 +374,8 @@ code {
   background: transparent;
 }
 
-/* 隐藏模型设置区域的滚动条 */
-.api-info-scroll::-webkit-scrollbar,
+/* 隐藏卡片内容区域的滚动条 */
+.card-content-scroll::-webkit-scrollbar,
 .model-settings-scroll::-webkit-scrollbar,
 .thinking-content-scroll::-webkit-scrollbar,
 .custom-request-textarea .semi-input::-webkit-scrollbar,
@@ -383,7 +383,7 @@ code {
   display: none;
 }
 
-.api-info-scroll,
+.card-content-scroll,
 .model-settings-scroll,
 .thinking-content-scroll,
 .custom-request-textarea .semi-input,

+ 194 - 22
web/src/pages/Detail/index.js

@@ -1,7 +1,7 @@
 import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
 import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
 import { useNavigate } from 'react-router-dom';
-import { Wallet, Activity, Zap, Gauge, PieChart, Server } from 'lucide-react';
+import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react';
 
 import {
   Card,
@@ -13,7 +13,9 @@ import {
   Tabs,
   TabPane,
   Empty,
-  Tag
+  Tag,
+  Timeline,
+  Collapse
 } from '@douyinfe/semi-ui';
 import {
   IconRefresh,
@@ -26,7 +28,9 @@ import {
   IconPulse,
   IconStopwatchStroked,
   IconTypograph,
-  IconPieChart2Stroked
+  IconPieChart2Stroked,
+  IconPlus,
+  IconMinus
 } from '@douyinfe/semi-icons';
 import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
 import { VChart } from '@visactor/react-vchart';
@@ -43,7 +47,8 @@ import {
   renderQuota,
   modelToColor,
   copy,
-  showSuccess
+  showSuccess,
+  getRelativeTime
 } from '../../helpers';
 import { UserContext } from '../../context/User/index.js';
 import { StatusContext } from '../../context/Status/index.js';
@@ -179,7 +184,7 @@ const Detail = (props) => {
   const [times, setTimes] = useState(0);
   const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
   const [lineData, setLineData] = useState([]);
-  const [apiInfoData, setApiInfoData] = useState([]);
+
   const [modelColors, setModelColors] = useState({});
   const [activeChartTab, setActiveChartTab] = useState('1');
   const [showApiScrollHint, setShowApiScrollHint] = useState(false);
@@ -578,6 +583,37 @@ const Detail = (props) => {
     checkApiScrollable();
   };
 
+  const checkCardScrollable = (ref, setHintFunction) => {
+    if (ref.current) {
+      const element = ref.current;
+      const isScrollable = element.scrollHeight > element.clientHeight;
+      const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 5;
+      setHintFunction(isScrollable && !isAtBottom);
+    }
+  };
+
+  const handleCardScroll = (ref, setHintFunction) => {
+    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);
+    }, 100);
+    return () => clearTimeout(timer);
+  }, []);
+
   const getUserData = async () => {
     let res = await API.get(`/api/user/self`);
     const { success, message, data } = res.data;
@@ -775,6 +811,32 @@ const Detail = (props) => {
     generateChartTimePoints, updateChartSpec, updateMapValue, t
   ]);
 
+  // ========== Status Data Management ==========
+  const announcementLegendData = useMemo(() => [
+    { color: 'grey', label: t('默认'), type: 'default' },
+    { color: 'blue', label: t('进行中'), type: 'ongoing' },
+    { color: 'green', label: t('成功'), type: 'success' },
+    { color: 'orange', label: t('警告'), type: 'warning' },
+    { color: 'red', label: t('异常'), type: 'error' }
+  ], [t]);
+
+  const apiInfoData = useMemo(() => {
+    return statusState?.status?.api_info || [];
+  }, [statusState?.status?.api_info]);
+
+  const announcementData = useMemo(() => {
+    const announcements = statusState?.status?.announcements || [];
+    // 处理后台配置的公告数据,自动生成相对时间
+    return announcements.map(item => ({
+      ...item,
+      time: getRelativeTime(item.publishDate)
+    }));
+  }, [statusState?.status?.announcements]);
+
+  const faqData = useMemo(() => {
+    return statusState?.status?.faq || [];
+  }, [statusState?.status?.faq]);
+
   // ========== Hooks - Effects ==========
   useEffect(() => {
     getUserData();
@@ -787,19 +849,6 @@ const Detail = (props) => {
     }
   }, []);
 
-  useEffect(() => {
-    if (statusState?.status?.api_info) {
-      setApiInfoData(statusState.status.api_info);
-    }
-  }, [statusState?.status?.api_info]);
-
-  useEffect(() => {
-    const timer = setTimeout(() => {
-      checkApiScrollable();
-    }, 100);
-    return () => clearTimeout(timer);
-  }, []);
-
   return (
     <div className="bg-gray-50 h-full">
       <div className="flex items-center justify-between mb-4">
@@ -975,10 +1024,10 @@ const Detail = (props) => {
                   </div>
                 }
               >
-                <div className="api-info-container">
+                <div className="card-content-container">
                   <div
                     ref={apiScrollRef}
-                    className="space-y-3 max-h-96 overflow-y-auto api-info-scroll"
+                    className="space-y-3 max-h-96 overflow-y-auto card-content-scroll"
                     onScroll={handleApiScroll}
                   >
                     {apiInfoData.length > 0 ? (
@@ -1023,7 +1072,7 @@ const Detail = (props) => {
                         <Empty
                           image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
                           darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
-                          title={t('暂无API信息配置')}
+                          title={t('暂无API信息')}
                           description={t('请联系管理员在系统设置中配置API信息')}
                           style={{ padding: '12px' }}
                         />
@@ -1031,7 +1080,7 @@ const Detail = (props) => {
                     )}
                   </div>
                   <div
-                    className="api-info-fade-indicator"
+                    className="card-content-fade-indicator"
                     style={{ opacity: showApiScrollHint ? 1 : 0 }}
                   />
                 </div>
@@ -1039,6 +1088,129 @@ const Detail = (props) => {
             )}
           </div>
         </div>
+
+        {/* 系统公告和常见问答卡片 */}
+        {!statusState?.status?.self_use_mode_enabled && (
+          <div className="mb-4">
+            <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
+              {/* 公告卡片 */}
+              <Card
+                {...CARD_PROPS}
+                className="shadow-sm !rounded-2xl lg:col-span-2"
+                title={
+                  <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
+                    <div className="flex items-center gap-2">
+                      <Bell size={16} />
+                      {t('系统公告')}
+                      <Tag size="small" color="grey" shape="circle">
+                        {t('显示最新20条')}
+                      </Tag>
+                    </div>
+                    {/* 图例 */}
+                    <div className="flex flex-wrap gap-3 text-xs">
+                      {announcementLegendData.map((legend, index) => (
+                        <div key={index} className="flex items-center gap-1">
+                          <div
+                            className="w-2 h-2 rounded-full"
+                            style={{
+                              backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
+                                legend.color === 'blue' ? '#3b82f6' :
+                                  legend.color === 'green' ? '#10b981' :
+                                    legend.color === 'orange' ? '#f59e0b' :
+                                      legend.color === 'red' ? '#ef4444' : '#8b9aa7'
+                            }}
+                          />
+                          <span className="text-gray-600">{legend.label}</span>
+                        </div>
+                      ))}
+                    </div>
+                  </div>
+                }
+              >
+                <div className="card-content-container">
+                  <div
+                    ref={announcementScrollRef}
+                    className="p-2 max-h-96 overflow-y-auto card-content-scroll"
+                    onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
+                  >
+                    {announcementData.length > 0 ? (
+                      <Timeline
+                        mode="alternate"
+                        dataSource={announcementData}
+                      />
+                    ) : (
+                      <div className="flex justify-center items-center py-8">
+                        <Empty
+                          image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
+                          darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
+                          title={t('暂无系统公告')}
+                          description={t('请联系管理员在系统设置中配置公告信息')}
+                          style={{ padding: '12px' }}
+                        />
+                      </div>
+                    )}
+                  </div>
+                  <div
+                    className="card-content-fade-indicator"
+                    style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
+                  />
+                </div>
+              </Card>
+
+              {/* 常见问答卡片 */}
+              <Card
+                {...CARD_PROPS}
+                className="shadow-sm !rounded-2xl lg:col-span-2"
+                title={
+                  <div className={FLEX_CENTER_GAP2}>
+                    <HelpCircle size={16} />
+                    {t('常见问答')}
+                  </div>
+                }
+              >
+                <div className="card-content-container">
+                  <div
+                    ref={faqScrollRef}
+                    className="p-2 max-h-96 overflow-y-auto card-content-scroll"
+                    onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
+                  >
+                    {faqData.length > 0 ? (
+                      <Collapse
+                        accordion
+                        expandIcon={<IconPlus />}
+                        collapseIcon={<IconMinus />}
+                      >
+                        {faqData.map((item, index) => (
+                          <Collapse.Panel
+                            key={index}
+                            header={item.title}
+                            itemKey={index.toString()}
+                          >
+                            <p>{item.content}</p>
+                          </Collapse.Panel>
+                        ))}
+                      </Collapse>
+                    ) : (
+                      <div className="flex justify-center items-center py-8">
+                        <Empty
+                          image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
+                          darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
+                          title={t('暂无常见问答')}
+                          description={t('请联系管理员在系统设置中配置常见问答')}
+                          style={{ padding: '12px' }}
+                        />
+                      </div>
+                    )}
+                  </div>
+                  <div
+                    className="card-content-fade-indicator"
+                    style={{ opacity: showFaqScrollHint ? 1 : 0 }}
+                  />
+                </div>
+              </Card>
+            </div>
+          </div>
+        )}
       </Spin>
     </div>
   );

+ 72 - 3
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js

@@ -44,6 +44,9 @@ const SettingsAPIInfo = ({ options, refresh }) => {
     route: '',
     color: 'blue'
   });
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
+  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 
   const colorOptions = [
     { value: 'blue', label: 'blue' },
@@ -237,6 +240,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
     {
       title: t('操作'),
       fixed: 'right',
+      width: 150,
       render: (_, record) => (
         <Space>
           <Button
@@ -264,12 +268,25 @@ const SettingsAPIInfo = ({ options, refresh }) => {
     },
   ];
 
+  const handleBatchDelete = () => {
+    if (selectedRowKeys.length === 0) {
+      showError('请先选择要删除的API信息');
+      return;
+    }
+
+    const newList = apiInfoList.filter(api => !selectedRowKeys.includes(api.id));
+    setApiInfoList(newList);
+    setSelectedRowKeys([]);
+    setHasChanges(true);
+    showSuccess(`已删除 ${selectedRowKeys.length} 个API信息,请及时点击“保存配置”进行保存`);
+  };
+
   const renderHeader = () => (
     <div className="flex flex-col w-full">
       <div className="mb-2">
         <div className="flex items-center text-blue-500">
           <Settings size={16} className="mr-2" />
-          <Text>{t('API信息管理,可以配置多个API地址用于状态展示和负载均衡')}</Text>
+          <Text>{t('API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)')}</Text>
         </div>
       </div>
 
@@ -286,6 +303,16 @@ const SettingsAPIInfo = ({ options, refresh }) => {
           >
             {t('添加API')}
           </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            onClick={handleBatchDelete}
+            disabled={selectedRowKeys.length === 0}
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
+          </Button>
           <Button
             icon={<Save size={14} />}
             onClick={submitApiInfo}
@@ -301,14 +328,56 @@ const SettingsAPIInfo = ({ options, refresh }) => {
     </div>
   );
 
+  // 计算当前页显示的数据
+  const getCurrentPageData = () => {
+    const startIndex = (currentPage - 1) * pageSize;
+    const endIndex = startIndex + pageSize;
+    return apiInfoList.slice(startIndex, endIndex);
+  };
+
+  const rowSelection = {
+    selectedRowKeys,
+    onChange: (selectedRowKeys, selectedRows) => {
+      setSelectedRowKeys(selectedRowKeys);
+    },
+    onSelect: (record, selected, selectedRows) => {
+      console.log(`选择行: ${selected}`, record);
+    },
+    onSelectAll: (selected, selectedRows) => {
+      console.log(`全选: ${selected}`, selectedRows);
+    },
+    getCheckboxProps: (record) => ({
+      disabled: false,
+      name: record.id,
+    }),
+  };
+
   return (
     <>
       <Form.Section text={renderHeader()}>
         <Table
           columns={columns}
-          dataSource={apiInfoList}
+          dataSource={getCurrentPageData()}
+          rowSelection={rowSelection}
+          rowKey="id"
           scroll={{ x: 'max-content' }}
-          pagination={false}
+          pagination={{
+            currentPage: currentPage,
+            pageSize: pageSize,
+            total: apiInfoList.length,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total, range) => t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`),
+            pageSizeOptions: ['5', '10', '20', '50'],
+            onChange: (page, size) => {
+              setCurrentPage(page);
+              setPageSize(size);
+            },
+            onShowSizeChange: (current, size) => {
+              setCurrentPage(1);
+              setPageSize(size);
+            }
+          }}
           size='middle'
           loading={loading}
           empty={

+ 485 - 0
web/src/pages/Setting/Dashboard/SettingsAnnouncements.js

@@ -0,0 +1,485 @@
+import React, { useEffect, useState } from 'react';
+import {
+  Button,
+  Space,
+  Table,
+  Form,
+  Typography,
+  Empty,
+  Divider,
+  Modal,
+  Tag
+} from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
+import {
+  Plus,
+  Edit,
+  Trash2,
+  Save,
+  Bell
+} from 'lucide-react';
+import { API, showError, showSuccess, getRelativeTime, formatDateTimeString } from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+const { Text } = Typography;
+
+const SettingsAnnouncements = ({ options, refresh }) => {
+  const { t } = useTranslation();
+
+  const [announcementsList, setAnnouncementsList] = useState([]);
+  const [showAnnouncementModal, setShowAnnouncementModal] = useState(false);
+  const [showDeleteModal, setShowDeleteModal] = useState(false);
+  const [deletingAnnouncement, setDeletingAnnouncement] = useState(null);
+  const [editingAnnouncement, setEditingAnnouncement] = useState(null);
+  const [modalLoading, setModalLoading] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [hasChanges, setHasChanges] = useState(false);
+  const [announcementForm, setAnnouncementForm] = useState({
+    content: '',
+    publishDate: new Date(),
+    type: 'default',
+    extra: ''
+  });
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
+  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+
+  const typeOptions = [
+    { value: 'default', label: t('默认') },
+    { value: 'ongoing', label: t('进行中') },
+    { value: 'success', label: t('成功') },
+    { value: 'warning', label: t('警告') },
+    { value: 'error', label: t('错误') }
+  ];
+
+  const getTypeColor = (type) => {
+    const colorMap = {
+      default: 'grey',
+      ongoing: 'blue',
+      success: 'green',
+      warning: 'orange',
+      error: 'red'
+    };
+    return colorMap[type] || 'grey';
+  };
+
+  const columns = [
+    {
+      title: t('内容'),
+      dataIndex: 'content',
+      key: 'content',
+      render: (text) => (
+        <div style={{
+          maxWidth: '300px',
+          wordBreak: 'break-word',
+          whiteSpace: 'pre-wrap'
+        }}>
+          {text}
+        </div>
+      )
+    },
+    {
+      title: t('发布时间'),
+      dataIndex: 'publishDate',
+      key: 'publishDate',
+      width: 180,
+      render: (publishDate) => (
+        <div>
+          <div style={{ fontWeight: 'bold' }}>
+            {getRelativeTime(publishDate)}
+          </div>
+          <div style={{
+            fontSize: '12px',
+            color: 'var(--semi-color-text-2)',
+            marginTop: '2px'
+          }}>
+            {publishDate ? formatDateTimeString(new Date(publishDate)) : '-'}
+          </div>
+        </div>
+      )
+    },
+    {
+      title: t('类型'),
+      dataIndex: 'type',
+      key: 'type',
+      width: 100,
+      render: (type) => (
+        <Tag color={getTypeColor(type)} shape='circle'>
+          {typeOptions.find(opt => opt.value === type)?.label || type}
+        </Tag>
+      )
+    },
+    {
+      title: t('说明'),
+      dataIndex: 'extra',
+      key: 'extra',
+      render: (text) => (
+        <div style={{
+          maxWidth: '200px',
+          wordBreak: 'break-word',
+          color: 'var(--semi-color-text-2)'
+        }}>
+          {text || '-'}
+        </div>
+      )
+    },
+    {
+      title: t('操作'),
+      key: 'action',
+      fixed: 'right',
+      width: 150,
+      render: (text, record) => (
+        <Space>
+          <Button
+            icon={<Edit size={14} />}
+            theme='light'
+            type='tertiary'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleEditAnnouncement(record)}
+          >
+            {t('编辑')}
+          </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleDeleteAnnouncement(record)}
+          >
+            {t('删除')}
+          </Button>
+        </Space>
+      )
+    }
+  ];
+
+  const updateOption = async (key, value) => {
+    const res = await API.put('/api/option/', {
+      key,
+      value,
+    });
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('系统公告已更新');
+      if (refresh) refresh();
+    } else {
+      showError(message);
+    }
+  };
+
+  const submitAnnouncements = async () => {
+    try {
+      setLoading(true);
+      const announcementsJson = JSON.stringify(announcementsList);
+      await updateOption('Announcements', announcementsJson);
+      setHasChanges(false);
+    } catch (error) {
+      console.error('系统公告更新失败', error);
+      showError('系统公告更新失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleAddAnnouncement = () => {
+    setEditingAnnouncement(null);
+    setAnnouncementForm({
+      content: '',
+      publishDate: new Date(),
+      type: 'default',
+      extra: ''
+    });
+    setShowAnnouncementModal(true);
+  };
+
+  const handleEditAnnouncement = (announcement) => {
+    setEditingAnnouncement(announcement);
+    setAnnouncementForm({
+      content: announcement.content,
+      publishDate: announcement.publishDate ? new Date(announcement.publishDate) : new Date(),
+      type: announcement.type || 'default',
+      extra: announcement.extra || ''
+    });
+    setShowAnnouncementModal(true);
+  };
+
+  const handleDeleteAnnouncement = (announcement) => {
+    setDeletingAnnouncement(announcement);
+    setShowDeleteModal(true);
+  };
+
+  const confirmDeleteAnnouncement = () => {
+    if (deletingAnnouncement) {
+      const newList = announcementsList.filter(item => item.id !== deletingAnnouncement.id);
+      setAnnouncementsList(newList);
+      setHasChanges(true);
+      showSuccess('公告已删除,请及时点击“保存配置”进行保存');
+    }
+    setShowDeleteModal(false);
+    setDeletingAnnouncement(null);
+  };
+
+  const handleSaveAnnouncement = async () => {
+    if (!announcementForm.content || !announcementForm.publishDate) {
+      showError('请填写完整的公告信息');
+      return;
+    }
+
+    try {
+      setModalLoading(true);
+
+      // 将publishDate转换为ISO字符串保存
+      const formData = {
+        ...announcementForm,
+        publishDate: announcementForm.publishDate.toISOString()
+      };
+
+      let newList;
+      if (editingAnnouncement) {
+        newList = announcementsList.map(item =>
+          item.id === editingAnnouncement.id
+            ? { ...item, ...formData }
+            : item
+        );
+      } else {
+        const newId = Math.max(...announcementsList.map(item => item.id), 0) + 1;
+        const newAnnouncement = {
+          id: newId,
+          ...formData
+        };
+        newList = [...announcementsList, newAnnouncement];
+      }
+
+      setAnnouncementsList(newList);
+      setHasChanges(true);
+      setShowAnnouncementModal(false);
+      showSuccess(editingAnnouncement ? '公告已更新,请及时点击“保存配置”进行保存' : '公告已添加,请及时点击“保存配置”进行保存');
+    } catch (error) {
+      showError('操作失败: ' + error.message);
+    } finally {
+      setModalLoading(false);
+    }
+  };
+
+  const parseAnnouncements = (announcementsStr) => {
+    if (!announcementsStr) {
+      setAnnouncementsList([]);
+      return;
+    }
+
+    try {
+      const parsed = JSON.parse(announcementsStr);
+      const list = Array.isArray(parsed) ? parsed : [];
+      // 确保每个项目都有id
+      const listWithIds = list.map((item, index) => ({
+        ...item,
+        id: item.id || index + 1
+      }));
+      setAnnouncementsList(listWithIds);
+    } catch (error) {
+      console.error('解析系统公告失败:', error);
+      setAnnouncementsList([]);
+    }
+  };
+
+  useEffect(() => {
+    if (options.Announcements !== undefined) {
+      parseAnnouncements(options.Announcements);
+    }
+  }, [options.Announcements]);
+
+  const handleBatchDelete = () => {
+    if (selectedRowKeys.length === 0) {
+      showError('请先选择要删除的系统公告');
+      return;
+    }
+
+    const newList = announcementsList.filter(item => !selectedRowKeys.includes(item.id));
+    setAnnouncementsList(newList);
+    setSelectedRowKeys([]);
+    setHasChanges(true);
+    showSuccess(`已删除 ${selectedRowKeys.length} 个系统公告,请及时点击“保存配置”进行保存`);
+  };
+
+  const renderHeader = () => (
+    <div className="flex flex-col w-full">
+      <div className="mb-2">
+        <div className="flex items-center text-blue-500">
+          <Bell size={16} className="mr-2" />
+          <Text>{t('系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)')}</Text>
+        </div>
+      </div>
+
+      <Divider margin="12px" />
+
+      <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
+        <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
+          <Button
+            theme='light'
+            type='primary'
+            icon={<Plus size={14} />}
+            className="!rounded-full w-full md:w-auto"
+            onClick={handleAddAnnouncement}
+          >
+            {t('添加公告')}
+          </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            onClick={handleBatchDelete}
+            disabled={selectedRowKeys.length === 0}
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
+          </Button>
+          <Button
+            icon={<Save size={14} />}
+            onClick={submitAnnouncements}
+            loading={loading}
+            disabled={!hasChanges}
+            type='secondary'
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('保存配置')}
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+
+  // 计算当前页显示的数据
+  const getCurrentPageData = () => {
+    const startIndex = (currentPage - 1) * pageSize;
+    const endIndex = startIndex + pageSize;
+    return announcementsList.slice(startIndex, endIndex);
+  };
+
+  const rowSelection = {
+    selectedRowKeys,
+    onChange: (selectedRowKeys, selectedRows) => {
+      setSelectedRowKeys(selectedRowKeys);
+    },
+    onSelect: (record, selected, selectedRows) => {
+      console.log(`选择行: ${selected}`, record);
+    },
+    onSelectAll: (selected, selectedRows) => {
+      console.log(`全选: ${selected}`, selectedRows);
+    },
+    getCheckboxProps: (record) => ({
+      disabled: false,
+      name: record.id,
+    }),
+  };
+
+  return (
+    <>
+      <Form.Section text={renderHeader()}>
+        <Table
+          columns={columns}
+          dataSource={getCurrentPageData()}
+          rowSelection={rowSelection}
+          rowKey="id"
+          scroll={{ x: 'max-content' }}
+          pagination={{
+            currentPage: currentPage,
+            pageSize: pageSize,
+            total: announcementsList.length,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total, range) => t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`),
+            pageSizeOptions: ['5', '10', '20', '50'],
+            onChange: (page, size) => {
+              setCurrentPage(page);
+              setPageSize(size);
+            },
+            onShowSizeChange: (current, size) => {
+              setCurrentPage(1);
+              setPageSize(size);
+            }
+          }}
+          size='middle'
+          loading={loading}
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('暂无系统公告')}
+              style={{ padding: 30 }}
+            />
+          }
+          className="rounded-xl overflow-hidden"
+        />
+      </Form.Section>
+
+      <Modal
+        title={editingAnnouncement ? t('编辑公告') : t('添加公告')}
+        visible={showAnnouncementModal}
+        onOk={handleSaveAnnouncement}
+        onCancel={() => setShowAnnouncementModal(false)}
+        okText={t('保存')}
+        cancelText={t('取消')}
+        className="rounded-xl"
+        confirmLoading={modalLoading}
+      >
+        <Form layout='vertical' initValues={announcementForm} key={editingAnnouncement ? editingAnnouncement.id : 'new'}>
+          <Form.TextArea
+            field='content'
+            label={t('公告内容')}
+            placeholder={t('请输入公告内容')}
+            maxCount={500}
+            rows={3}
+            rules={[{ required: true, message: t('请输入公告内容') }]}
+            onChange={(value) => setAnnouncementForm({ ...announcementForm, content: value })}
+          />
+          <Form.DatePicker
+            field='publishDate'
+            label={t('发布日期')}
+            type='dateTime'
+            rules={[{ required: true, message: t('请选择发布日期') }]}
+            onChange={(value) => setAnnouncementForm({ ...announcementForm, publishDate: value })}
+          />
+          <Form.Select
+            field='type'
+            label={t('公告类型')}
+            optionList={typeOptions}
+            onChange={(value) => setAnnouncementForm({ ...announcementForm, type: value })}
+          />
+          <Form.Input
+            field='extra'
+            label={t('说明信息')}
+            placeholder={t('可选,公告的补充说明')}
+            onChange={(value) => setAnnouncementForm({ ...announcementForm, extra: value })}
+          />
+        </Form>
+      </Modal>
+
+      <Modal
+        title={t('确认删除')}
+        visible={showDeleteModal}
+        onOk={confirmDeleteAnnouncement}
+        onCancel={() => {
+          setShowDeleteModal(false);
+          setDeletingAnnouncement(null);
+        }}
+        okText={t('确认删除')}
+        cancelText={t('取消')}
+        type="warning"
+        className="rounded-xl"
+        okButtonProps={{
+          type: 'danger',
+          theme: 'solid'
+        }}
+      >
+        <Text>{t('确定要删除此公告吗?')}</Text>
+      </Modal>
+    </>
+  );
+};
+
+export default SettingsAnnouncements; 

+ 413 - 0
web/src/pages/Setting/Dashboard/SettingsFAQ.js

@@ -0,0 +1,413 @@
+import React, { useEffect, useState } from 'react';
+import {
+  Button,
+  Space,
+  Table,
+  Form,
+  Typography,
+  Empty,
+  Divider,
+  Modal
+} from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
+import {
+  Plus,
+  Edit,
+  Trash2,
+  Save,
+  HelpCircle
+} from 'lucide-react';
+import { API, showError, showSuccess } from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+const { Text } = Typography;
+
+const SettingsFAQ = ({ options, refresh }) => {
+  const { t } = useTranslation();
+
+  const [faqList, setFaqList] = useState([]);
+  const [showFaqModal, setShowFaqModal] = useState(false);
+  const [showDeleteModal, setShowDeleteModal] = useState(false);
+  const [deletingFaq, setDeletingFaq] = useState(null);
+  const [editingFaq, setEditingFaq] = useState(null);
+  const [modalLoading, setModalLoading] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [hasChanges, setHasChanges] = useState(false);
+  const [faqForm, setFaqForm] = useState({
+    title: '',
+    content: ''
+  });
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
+  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+
+  const columns = [
+    {
+      title: t('问题标题'),
+      dataIndex: 'title',
+      key: 'title',
+      render: (text) => (
+        <div style={{
+          maxWidth: '300px',
+          wordBreak: 'break-word',
+          fontWeight: 'bold'
+        }}>
+          {text}
+        </div>
+      )
+    },
+    {
+      title: t('回答内容'),
+      dataIndex: 'content',
+      key: 'content',
+      render: (text) => (
+        <div style={{
+          maxWidth: '400px',
+          wordBreak: 'break-word',
+          whiteSpace: 'pre-wrap',
+          color: 'var(--semi-color-text-1)'
+        }}>
+          {text}
+        </div>
+      )
+    },
+    {
+      title: t('操作'),
+      key: 'action',
+      fixed: 'right',
+      width: 150,
+      render: (text, record) => (
+        <Space>
+          <Button
+            icon={<Edit size={14} />}
+            theme='light'
+            type='tertiary'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleEditFaq(record)}
+          >
+            {t('编辑')}
+          </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleDeleteFaq(record)}
+          >
+            {t('删除')}
+          </Button>
+        </Space>
+      )
+    }
+  ];
+
+  const updateOption = async (key, value) => {
+    const res = await API.put('/api/option/', {
+      key,
+      value,
+    });
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('常见问答已更新');
+      if (refresh) refresh();
+    } else {
+      showError(message);
+    }
+  };
+
+  const submitFAQ = async () => {
+    try {
+      setLoading(true);
+      const faqJson = JSON.stringify(faqList);
+      await updateOption('FAQ', faqJson);
+      setHasChanges(false);
+    } catch (error) {
+      console.error('常见问答更新失败', error);
+      showError('常见问答更新失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleAddFaq = () => {
+    setEditingFaq(null);
+    setFaqForm({
+      title: '',
+      content: ''
+    });
+    setShowFaqModal(true);
+  };
+
+  const handleEditFaq = (faq) => {
+    setEditingFaq(faq);
+    setFaqForm({
+      title: faq.title,
+      content: faq.content
+    });
+    setShowFaqModal(true);
+  };
+
+  const handleDeleteFaq = (faq) => {
+    setDeletingFaq(faq);
+    setShowDeleteModal(true);
+  };
+
+  const confirmDeleteFaq = () => {
+    if (deletingFaq) {
+      const newList = faqList.filter(item => item.id !== deletingFaq.id);
+      setFaqList(newList);
+      setHasChanges(true);
+      showSuccess('问答已删除,请及时点击“保存配置”进行保存');
+    }
+    setShowDeleteModal(false);
+    setDeletingFaq(null);
+  };
+
+  const handleSaveFaq = async () => {
+    if (!faqForm.title || !faqForm.content) {
+      showError('请填写完整的问答信息');
+      return;
+    }
+
+    try {
+      setModalLoading(true);
+
+      let newList;
+      if (editingFaq) {
+        newList = faqList.map(item =>
+          item.id === editingFaq.id
+            ? { ...item, ...faqForm }
+            : item
+        );
+      } else {
+        const newId = Math.max(...faqList.map(item => item.id), 0) + 1;
+        const newFaq = {
+          id: newId,
+          ...faqForm
+        };
+        newList = [...faqList, newFaq];
+      }
+
+      setFaqList(newList);
+      setHasChanges(true);
+      setShowFaqModal(false);
+      showSuccess(editingFaq ? '问答已更新,请及时点击“保存配置”进行保存' : '问答已添加,请及时点击“保存配置”进行保存');
+    } catch (error) {
+      showError('操作失败: ' + error.message);
+    } finally {
+      setModalLoading(false);
+    }
+  };
+
+  const parseFAQ = (faqStr) => {
+    if (!faqStr) {
+      setFaqList([]);
+      return;
+    }
+
+    try {
+      const parsed = JSON.parse(faqStr);
+      const list = Array.isArray(parsed) ? parsed : [];
+      // 确保每个项目都有id
+      const listWithIds = list.map((item, index) => ({
+        ...item,
+        id: item.id || index + 1
+      }));
+      setFaqList(listWithIds);
+    } catch (error) {
+      console.error('解析常见问答失败:', error);
+      setFaqList([]);
+    }
+  };
+
+  useEffect(() => {
+    if (options.FAQ !== undefined) {
+      parseFAQ(options.FAQ);
+    }
+  }, [options.FAQ]);
+
+  const handleBatchDelete = () => {
+    if (selectedRowKeys.length === 0) {
+      showError('请先选择要删除的常见问答');
+      return;
+    }
+
+    const newList = faqList.filter(item => !selectedRowKeys.includes(item.id));
+    setFaqList(newList);
+    setSelectedRowKeys([]);
+    setHasChanges(true);
+    showSuccess(`已删除 ${selectedRowKeys.length} 个常见问答,请及时点击“保存配置”进行保存`);
+  };
+
+  const renderHeader = () => (
+    <div className="flex flex-col w-full">
+      <div className="mb-2">
+        <div className="flex items-center text-blue-500">
+          <HelpCircle size={16} className="mr-2" />
+          <Text>{t('常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)')}</Text>
+        </div>
+      </div>
+
+      <Divider margin="12px" />
+
+      <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
+        <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
+          <Button
+            theme='light'
+            type='primary'
+            icon={<Plus size={14} />}
+            className="!rounded-full w-full md:w-auto"
+            onClick={handleAddFaq}
+          >
+            {t('添加问答')}
+          </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            onClick={handleBatchDelete}
+            disabled={selectedRowKeys.length === 0}
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
+          </Button>
+          <Button
+            icon={<Save size={14} />}
+            onClick={submitFAQ}
+            loading={loading}
+            disabled={!hasChanges}
+            type='secondary'
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('保存配置')}
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+
+  // 计算当前页显示的数据
+  const getCurrentPageData = () => {
+    const startIndex = (currentPage - 1) * pageSize;
+    const endIndex = startIndex + pageSize;
+    return faqList.slice(startIndex, endIndex);
+  };
+
+  const rowSelection = {
+    selectedRowKeys,
+    onChange: (selectedRowKeys, selectedRows) => {
+      setSelectedRowKeys(selectedRowKeys);
+    },
+    onSelect: (record, selected, selectedRows) => {
+      console.log(`选择行: ${selected}`, record);
+    },
+    onSelectAll: (selected, selectedRows) => {
+      console.log(`全选: ${selected}`, selectedRows);
+    },
+    getCheckboxProps: (record) => ({
+      disabled: false,
+      name: record.id,
+    }),
+  };
+
+  return (
+    <>
+      <Form.Section text={renderHeader()}>
+        <Table
+          columns={columns}
+          dataSource={getCurrentPageData()}
+          rowSelection={rowSelection}
+          rowKey="id"
+          scroll={{ x: 'max-content' }}
+          pagination={{
+            currentPage: currentPage,
+            pageSize: pageSize,
+            total: faqList.length,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total, range) => t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`),
+            pageSizeOptions: ['5', '10', '20', '50'],
+            onChange: (page, size) => {
+              setCurrentPage(page);
+              setPageSize(size);
+            },
+            onShowSizeChange: (current, size) => {
+              setCurrentPage(1);
+              setPageSize(size);
+            }
+          }}
+          size='middle'
+          loading={loading}
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('暂无常见问答')}
+              style={{ padding: 30 }}
+            />
+          }
+          className="rounded-xl overflow-hidden"
+        />
+      </Form.Section>
+
+      <Modal
+        title={editingFaq ? t('编辑问答') : t('添加问答')}
+        visible={showFaqModal}
+        onOk={handleSaveFaq}
+        onCancel={() => setShowFaqModal(false)}
+        okText={t('保存')}
+        cancelText={t('取消')}
+        className="rounded-xl"
+        confirmLoading={modalLoading}
+        width={800}
+      >
+        <Form layout='vertical' initValues={faqForm} key={editingFaq ? editingFaq.id : 'new'}>
+          <Form.Input
+            field='title'
+            label={t('问题标题')}
+            placeholder={t('请输入问题标题')}
+            maxLength={200}
+            rules={[{ required: true, message: t('请输入问题标题') }]}
+            onChange={(value) => setFaqForm({ ...faqForm, title: value })}
+          />
+          <Form.TextArea
+            field='content'
+            label={t('回答内容')}
+            placeholder={t('请输入回答内容')}
+            maxCount={1000}
+            rows={6}
+            rules={[{ required: true, message: t('请输入回答内容') }]}
+            onChange={(value) => setFaqForm({ ...faqForm, content: value })}
+          />
+        </Form>
+      </Modal>
+
+      <Modal
+        title={t('确认删除')}
+        visible={showDeleteModal}
+        onOk={confirmDeleteFaq}
+        onCancel={() => {
+          setShowDeleteModal(false);
+          setDeletingFaq(null);
+        }}
+        okText={t('确认删除')}
+        cancelText={t('取消')}
+        type="warning"
+        className="rounded-xl"
+        okButtonProps={{
+          type: 'danger',
+          theme: 'solid'
+        }}
+      >
+        <Text>{t('确定要删除此问答吗?')}</Text>
+      </Modal>
+    </>
+  );
+};
+
+export default SettingsFAQ;