Explorar o código

🎛️ feat(dashboard): add per-panel enable switches & conditional backend payload

Backend:
• ConsoleSetting
  - Introduce `ApiInfoEnabled`, `UptimeKumaEnabled`, `AnnouncementsEnabled`, `FAQEnabled` (default true).
• misc.GetStatus
  - Refactor to build response map dynamically.
  - Return the four *_enabled flags.
  - Only append `api_info`, `announcements`, `faq` when their respective flags are true.

Frontend:
• Detail page
  - Remove all `self_use_mode_enabled` checks.
  - Render API, Announcement, FAQ and Uptime panels based on the new *_enabled flags.
• Dashboard → Settings
  - Added `Switch` controls in:
    · SettingsAPIInfo.js
    · SettingsAnnouncements.js
    · SettingsFAQ.js
    · SettingsUptimeKuma.js
  - Each switch persists its state via `/api/option` to the corresponding
    `console_setting.<panel>_enabled` key and reflects current status on load.
  - DashboardSetting.js now initialises and refreshes the four *_enabled keys so
    child components receive accurate panel states.

Fixes:
• Switches previously defaulted to “on” because *_enabled keys were missing.
  They are now included, ensuring correct visual state when panels are disabled.

No breaking changes; existing functionality remains untouched aside from the
new per-panel visibility control.
Apple\Apple hai 8 meses
pai
achega
4c05377c87

+ 62 - 43
controller/misc.go

@@ -38,52 +38,71 @@ func TestStatus(c *gin.Context) {
 
 func GetStatus(c *gin.Context) {
 
+	cs := console_setting.GetConsoleSetting()
+
+	data := gin.H{
+		"version":                     common.Version,
+		"start_time":                  common.StartTime,
+		"email_verification":          common.EmailVerificationEnabled,
+		"github_oauth":                common.GitHubOAuthEnabled,
+		"github_client_id":            common.GitHubClientId,
+		"linuxdo_oauth":               common.LinuxDOOAuthEnabled,
+		"linuxdo_client_id":           common.LinuxDOClientId,
+		"telegram_oauth":              common.TelegramOAuthEnabled,
+		"telegram_bot_name":           common.TelegramBotName,
+		"system_name":                 common.SystemName,
+		"logo":                        common.Logo,
+		"footer_html":                 common.Footer,
+		"wechat_qrcode":               common.WeChatAccountQRCodeImageURL,
+		"wechat_login":                common.WeChatAuthEnabled,
+		"server_address":              setting.ServerAddress,
+		"price":                       setting.Price,
+		"min_topup":                   setting.MinTopUp,
+		"turnstile_check":             common.TurnstileCheckEnabled,
+		"turnstile_site_key":          common.TurnstileSiteKey,
+		"top_up_link":                 common.TopUpLink,
+		"docs_link":                   operation_setting.GetGeneralSetting().DocsLink,
+		"quota_per_unit":              common.QuotaPerUnit,
+		"display_in_currency":         common.DisplayInCurrencyEnabled,
+		"enable_batch_update":         common.BatchUpdateEnabled,
+		"enable_drawing":              common.DrawingEnabled,
+		"enable_task":                 common.TaskEnabled,
+		"enable_data_export":          common.DataExportEnabled,
+		"data_export_default_time":    common.DataExportDefaultTime,
+		"default_collapse_sidebar":    common.DefaultCollapseSidebar,
+		"enable_online_topup":         setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
+		"mj_notify_enabled":           setting.MjNotifyEnabled,
+		"chats":                       setting.Chats,
+		"demo_site_enabled":           operation_setting.DemoSiteEnabled,
+		"self_use_mode_enabled":       operation_setting.SelfUseModeEnabled,
+
+		// 面板启用开关
+		"api_info_enabled":            cs.ApiInfoEnabled,
+		"uptime_kuma_enabled":         cs.UptimeKumaEnabled,
+		"announcements_enabled":       cs.AnnouncementsEnabled,
+		"faq_enabled":                 cs.FAQEnabled,
+
+		"oidc_enabled":                system_setting.GetOIDCSettings().Enabled,
+		"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,
+		"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
+		"setup":                       constant.Setup,
+	}
+
+	// 根据启用状态注入可选内容
+	if cs.ApiInfoEnabled {
+		data["api_info"] = console_setting.GetApiInfo()
+	}
+	if cs.AnnouncementsEnabled {
+		data["announcements"] = console_setting.GetAnnouncements()
+	}
+	if cs.FAQEnabled {
+		data["faq"] = console_setting.GetFAQ()
+	}
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data": gin.H{
-			"version":                     common.Version,
-			"start_time":                  common.StartTime,
-			"email_verification":          common.EmailVerificationEnabled,
-			"github_oauth":                common.GitHubOAuthEnabled,
-			"github_client_id":            common.GitHubClientId,
-			"linuxdo_oauth":               common.LinuxDOOAuthEnabled,
-			"linuxdo_client_id":           common.LinuxDOClientId,
-			"telegram_oauth":              common.TelegramOAuthEnabled,
-			"telegram_bot_name":           common.TelegramBotName,
-			"system_name":                 common.SystemName,
-			"logo":                        common.Logo,
-			"footer_html":                 common.Footer,
-			"wechat_qrcode":               common.WeChatAccountQRCodeImageURL,
-			"wechat_login":                common.WeChatAuthEnabled,
-			"server_address":              setting.ServerAddress,
-			"price":                       setting.Price,
-			"min_topup":                   setting.MinTopUp,
-			"turnstile_check":             common.TurnstileCheckEnabled,
-			"turnstile_site_key":          common.TurnstileSiteKey,
-			"top_up_link":                 common.TopUpLink,
-			"docs_link":                   operation_setting.GetGeneralSetting().DocsLink,
-			"quota_per_unit":              common.QuotaPerUnit,
-			"display_in_currency":         common.DisplayInCurrencyEnabled,
-			"enable_batch_update":         common.BatchUpdateEnabled,
-			"enable_drawing":              common.DrawingEnabled,
-			"enable_task":                 common.TaskEnabled,
-			"enable_data_export":          common.DataExportEnabled,
-			"data_export_default_time":    common.DataExportDefaultTime,
-			"default_collapse_sidebar":    common.DefaultCollapseSidebar,
-			"enable_online_topup":         setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
-			"mj_notify_enabled":           setting.MjNotifyEnabled,
-			"chats":                       setting.Chats,
-			"demo_site_enabled":           operation_setting.DemoSiteEnabled,
-			"self_use_mode_enabled":       operation_setting.SelfUseModeEnabled,
-			"oidc_enabled":                system_setting.GetOIDCSettings().Enabled,
-			"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,
-			"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
-			"setup":                       constant.Setup,
-			"api_info":                    console_setting.GetApiInfo(),
-			"announcements":               console_setting.GetAnnouncements(),
-			"faq":                         console_setting.GetFAQ(),
-		},
+		"data":    data,
 	})
 	return
 }

+ 8 - 0
setting/console_setting/config.go

@@ -8,6 +8,10 @@ type ConsoleSetting struct {
     UptimeKumaSlug string `json:"uptime_kuma_slug"` // Uptime Kuma Status Page Slug
     Announcements  string `json:"announcements"`    // 系统公告 (JSON 数组字符串)
     FAQ            string `json:"faq"`              // 常见问题 (JSON 数组字符串)
+    ApiInfoEnabled        bool `json:"api_info_enabled"`        // 是否启用 API 信息面板
+    UptimeKumaEnabled     bool `json:"uptime_kuma_enabled"`     // 是否启用 Uptime Kuma 面板
+    AnnouncementsEnabled  bool `json:"announcements_enabled"`   // 是否启用系统公告面板
+    FAQEnabled            bool `json:"faq_enabled"`             // 是否启用常见问答面板
 }
 
 // 默认配置
@@ -17,6 +21,10 @@ var defaultConsoleSetting = ConsoleSetting{
     UptimeKumaSlug: "",
     Announcements:  "",
     FAQ:            "",
+    ApiInfoEnabled:       true,
+    UptimeKumaEnabled:    true,
+    AnnouncementsEnabled: true,
+    FAQEnabled:           true,
 }
 
 // 全局实例

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

@@ -13,6 +13,10 @@ const DashboardSetting = () => {
     'console_setting.faq': '',
     'console_setting.uptime_kuma_url': '',
     'console_setting.uptime_kuma_slug': '',
+    'console_setting.api_info_enabled': '',
+    'console_setting.announcements_enabled': '',
+    'console_setting.faq_enabled': '',
+    'console_setting.uptime_kuma_enabled': '',
 
     // 用于迁移检测的旧键,下个版本会删除
     ApiInfo: '',

+ 208 - 193
web/src/pages/Detail/index.js

@@ -90,6 +90,15 @@ const Detail = (props) => {
   let now = new Date();
   const isAdminUser = isAdmin();
 
+  // ========== Panel enable flags ==========
+  const apiInfoEnabled = statusState?.status?.api_info_enabled ?? true;
+  const announcementsEnabled = statusState?.status?.announcements_enabled ?? true;
+  const faqEnabled = statusState?.status?.faq_enabled ?? true;
+  const uptimeEnabled = statusState?.status?.uptime_kuma_enabled ?? true;
+
+  const hasApiInfoPanel = apiInfoEnabled;
+  const hasInfoPanels = announcementsEnabled || faqEnabled || uptimeEnabled;
+
   // ========== Helper Functions ==========
   const getDefaultTime = useCallback(() => {
     return localStorage.getItem('data_export_default_time') || 'hour';
@@ -1015,10 +1024,10 @@ const Detail = (props) => {
         </div>
 
         <div className="mb-4">
-          <div className={`grid grid-cols-1 gap-4 ${!statusState?.status?.self_use_mode_enabled ? 'lg:grid-cols-4' : ''}`}>
+          <div className={`grid grid-cols-1 gap-4 ${hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
             <Card
               {...CARD_PROPS}
-              className={`shadow-sm !rounded-2xl ${!statusState?.status?.self_use_mode_enabled ? 'lg:col-span-3' : ''}`}
+              className={`shadow-sm !rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
               title={
                 <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
                   <div className={FLEX_CENTER_GAP2}>
@@ -1061,7 +1070,7 @@ const Detail = (props) => {
               </div>
             </Card>
 
-            {!statusState?.status?.self_use_mode_enabled && (
+            {hasApiInfoPanel && (
               <Card
                 {...CARD_PROPS}
                 className="bg-gray-50 border-0 !rounded-2xl"
@@ -1138,219 +1147,225 @@ const Detail = (props) => {
         </div>
 
         {/* 系统公告和常见问答卡片 */}
-        {!statusState?.status?.self_use_mode_enabled && (
+        {hasInfoPanels && (
           <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>
+              {announcementsEnabled && (
+                <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="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'
-                            }}
+                  }
+                >
+                  <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' }}
                           />
-                          <span className="text-gray-600">{legend.label}</span>
                         </div>
-                      ))}
+                      )}
                     </div>
+                    <div
+                      className="card-content-fade-indicator"
+                      style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
+                    />
                   </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
-                {...CARD_PROPS}
-                className="shadow-sm !rounded-2xl lg:col-span-1"
-                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.question}
-                            itemKey={index.toString()}
-                          >
-                            <p>{item.answer}</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>
-
-              {/* 服务可用性卡片 */}
-              <Card
-                {...CARD_PROPS}
-                className="shadow-sm !rounded-2xl lg:col-span-1"
-                title={
-                  <div className="flex items-center justify-between w-full gap-2">
-                    <div className="flex items-center gap-2">
-                      <Gauge size={16} />
-                      {t('服务可用性')}
+              {faqEnabled && (
+                <Card
+                  {...CARD_PROPS}
+                  className="shadow-sm !rounded-2xl lg:col-span-1"
+                  title={
+                    <div className={FLEX_CENTER_GAP2}>
+                      <HelpCircle size={16} />
+                      {t('常见问答')}
                     </div>
-                    <IconButton
-                      icon={<IconRefresh />}
-                      onClick={loadUptimeData}
-                      loading={uptimeLoading}
-                      size="small"
-                      theme="borderless"
-                      className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
-                    />
-                  </div>
-                }
-                footer={uptimeData.length > 0 ? (
-                  <Card
-                    bordered={false}
-                    className="!rounded-2xl backdrop-blur !shadow-none"
-                  >
-                    <div className="flex flex-wrap gap-3 text-xs justify-center">
-                      {uptimeLegendData.map((legend, index) => (
-                        <div key={index} className="flex items-center gap-1">
-                          <div
-                            className="w-2 h-2 rounded-full"
-                            style={{ backgroundColor: legend.color }}
-                          />
-                          <span className="text-gray-600">{legend.label}</span>
-                        </div>
-                      ))}
-                    </div>
-                  </Card>
-                ) : null}
-                footerStyle={uptimeData.length > 0 ? { padding: '0px' } : undefined}
-              >
-                <div className="card-content-container">
-                  <Spin spinning={uptimeLoading}>
+                  }
+                >
+                  <div className="card-content-container">
                     <div
-                      ref={uptimeScrollRef}
-                      className="p-2 max-h-80 overflow-y-auto card-content-scroll"
-                      onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
+                      ref={faqScrollRef}
+                      className="p-2 max-h-96 overflow-y-auto card-content-scroll"
+                      onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
                     >
-                      {uptimeData.length > 0 ? (
-                        uptimeData.map((monitor, idx) => (
-                          <div key={idx} className="p-2 hover:bg-white rounded-lg transition-colors">
-                            <div className="flex items-center justify-between mb-1">
-                              <div className="flex items-center gap-2">
-                                <div
-                                  className="w-2 h-2 rounded-full flex-shrink-0"
-                                  style={{
-                                    backgroundColor: getUptimeStatusColor(monitor.status)
-                                  }}
-                                />
-                                <span className="text-sm font-medium text-gray-900">{monitor.name}</span>
-                              </div>
-                              <span className="text-xs text-gray-500">{((monitor.uptime || 0) * 100).toFixed(2)}%</span>
-                            </div>
-                            <div className="flex items-center gap-2">
-                              <span className="text-xs text-gray-500">{getUptimeStatusText(monitor.status)}</span>
-                              <div className="flex-1">
-                                <Progress
-                                  percent={(monitor.uptime || 0) * 100}
-                                  showInfo={false}
-                                  aria-label={`${monitor.name} uptime`}
-                                  stroke={getUptimeStatusColor(monitor.status)}
-                                />
-                              </div>
-                            </div>
-                          </div>
-                        ))
+                      {faqData.length > 0 ? (
+                        <Collapse
+                          accordion
+                          expandIcon={<IconPlus />}
+                          collapseIcon={<IconMinus />}
+                        >
+                          {faqData.map((item, index) => (
+                            <Collapse.Panel
+                              key={index}
+                              header={item.question}
+                              itemKey={index.toString()}
+                            >
+                              <p>{item.answer}</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('请联系管理员在系统设置中配置Uptime')}
+                            title={t('暂无常见问答')}
+                            description={t('请联系管理员在系统设置中配置常见问答')}
                             style={{ padding: '12px' }}
                           />
                         </div>
                       )}
                     </div>
-                  </Spin>
-                  <div
-                    className="card-content-fade-indicator"
-                    style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
-                  />
-                </div>
-              </Card>
+                    <div
+                      className="card-content-fade-indicator"
+                      style={{ opacity: showFaqScrollHint ? 1 : 0 }}
+                    />
+                  </div>
+                </Card>
+              )}
+
+              {/* 服务可用性卡片 */}
+              {uptimeEnabled && (
+                <Card
+                  {...CARD_PROPS}
+                  className="shadow-sm !rounded-2xl lg:col-span-1"
+                  title={
+                    <div className="flex items-center justify-between w-full gap-2">
+                      <div className="flex items-center gap-2">
+                        <Gauge size={16} />
+                        {t('服务可用性')}
+                      </div>
+                      <IconButton
+                        icon={<IconRefresh />}
+                        onClick={loadUptimeData}
+                        loading={uptimeLoading}
+                        size="small"
+                        theme="borderless"
+                        className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
+                      />
+                    </div>
+                  }
+                  footer={uptimeData.length > 0 ? (
+                    <Card
+                      bordered={false}
+                      className="!rounded-2xl backdrop-blur !shadow-none"
+                    >
+                      <div className="flex flex-wrap gap-3 text-xs justify-center">
+                        {uptimeLegendData.map((legend, index) => (
+                          <div key={index} className="flex items-center gap-1">
+                            <div
+                              className="w-2 h-2 rounded-full"
+                              style={{ backgroundColor: legend.color }}
+                            />
+                            <span className="text-gray-600">{legend.label}</span>
+                          </div>
+                        ))}
+                      </div>
+                    </Card>
+                  ) : null}
+                  footerStyle={uptimeData.length > 0 ? { padding: '0px' } : undefined}
+                >
+                  <div className="card-content-container">
+                    <Spin spinning={uptimeLoading}>
+                      <div
+                        ref={uptimeScrollRef}
+                        className="p-2 max-h-80 overflow-y-auto card-content-scroll"
+                        onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
+                      >
+                        {uptimeData.length > 0 ? (
+                          uptimeData.map((monitor, idx) => (
+                            <div key={idx} className="p-2 hover:bg-white rounded-lg transition-colors">
+                              <div className="flex items-center justify-between mb-1">
+                                <div className="flex items-center gap-2">
+                                  <div
+                                    className="w-2 h-2 rounded-full flex-shrink-0"
+                                    style={{
+                                      backgroundColor: getUptimeStatusColor(monitor.status)
+                                    }}
+                                  />
+                                  <span className="text-sm font-medium text-gray-900">{monitor.name}</span>
+                                </div>
+                                <span className="text-xs text-gray-500">{((monitor.uptime || 0) * 100).toFixed(2)}%</span>
+                              </div>
+                              <div className="flex items-center gap-2">
+                                <span className="text-xs text-gray-500">{getUptimeStatusText(monitor.status)}</span>
+                                <div className="flex-1">
+                                  <Progress
+                                    percent={(monitor.uptime || 0) * 100}
+                                    showInfo={false}
+                                    aria-label={`${monitor.name} uptime`}
+                                    stroke={getUptimeStatusColor(monitor.status)}
+                                  />
+                                </div>
+                              </div>
+                            </div>
+                          ))
+                        ) : (
+                          <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('请联系管理员在系统设置中配置Uptime')}
+                              style={{ padding: '12px' }}
+                            />
+                          </div>
+                        )}
+                      </div>
+                    </Spin>
+                    <div
+                      className="card-content-fade-indicator"
+                      style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
+                    />
+                  </div>
+                </Card>
+              )}
             </div>
           </div>
         )}

+ 38 - 1
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js

@@ -9,7 +9,8 @@ import {
   Divider,
   Avatar,
   Modal,
-  Tag
+  Tag,
+  Switch
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -48,6 +49,9 @@ const SettingsAPIInfo = ({ options, refresh }) => {
   const [pageSize, setPageSize] = useState(10);
   const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 
+  // 面板启用状态 state
+  const [panelEnabled, setPanelEnabled] = useState(true);
+
   const colorOptions = [
     { value: 'blue', label: 'blue' },
     { value: 'green', label: 'green' },
@@ -191,6 +195,30 @@ const SettingsAPIInfo = ({ options, refresh }) => {
     }
   }, [options['console_setting.api_info'], options.ApiInfo]);
 
+  useEffect(() => {
+    const enabledStr = options['console_setting.api_info_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options['console_setting.api_info_enabled']]);
+
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
+    try {
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.api_info_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
+    }
+  };
+
   const columns = [
     {
       title: 'ID',
@@ -325,6 +353,15 @@ const SettingsAPIInfo = ({ options, refresh }) => {
             {t('保存设置')}
           </Button>
         </div>
+
+        {/* 启用开关 */}
+        <div className="order-1 md:order-2 flex items-center gap-2">
+          <Switch
+            checked={panelEnabled}
+            onChange={handleToggleEnabled}
+          />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
+        </div>
       </div>
     </div>
   );

+ 35 - 1
web/src/pages/Setting/Dashboard/SettingsAnnouncements.js

@@ -8,7 +8,8 @@ import {
   Empty,
   Divider,
   Modal,
-  Tag
+  Tag,
+  Switch
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -47,6 +48,9 @@ const SettingsAnnouncements = ({ options, refresh }) => {
   const [pageSize, setPageSize] = useState(10);
   const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 
+  // 面板启用状态
+  const [panelEnabled, setPanelEnabled] = useState(true);
+
   const typeOptions = [
     { value: 'default', label: t('默认') },
     { value: 'ongoing', label: t('进行中') },
@@ -294,6 +298,30 @@ const SettingsAnnouncements = ({ options, refresh }) => {
     }
   }, [options['console_setting.announcements'], options.Announcements]);
 
+  useEffect(() => {
+    const enabledStr = options['console_setting.announcements_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options['console_setting.announcements_enabled']]);
+
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
+    try {
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.announcements_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
+    }
+  };
+
   const handleBatchDelete = () => {
     if (selectedRowKeys.length === 0) {
       showError('请先选择要删除的系统公告');
@@ -350,6 +378,12 @@ const SettingsAnnouncements = ({ options, refresh }) => {
             {t('保存设置')}
           </Button>
         </div>
+
+        {/* 启用开关 */}
+        <div className="order-1 md:order-2 flex items-center gap-2">
+          <Switch checked={panelEnabled} onChange={handleToggleEnabled} />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
+        </div>
       </div>
     </div>
   );

+ 35 - 1
web/src/pages/Setting/Dashboard/SettingsFAQ.js

@@ -7,7 +7,8 @@ import {
   Typography,
   Empty,
   Divider,
-  Modal
+  Modal,
+  Switch
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -44,6 +45,9 @@ const SettingsFAQ = ({ options, refresh }) => {
   const [pageSize, setPageSize] = useState(10);
   const [selectedRowKeys, setSelectedRowKeys] = useState([]);
 
+  // 面板启用状态
+  const [panelEnabled, setPanelEnabled] = useState(true);
+
   const columns = [
     {
       title: t('问题标题'),
@@ -231,6 +235,30 @@ const SettingsFAQ = ({ options, refresh }) => {
     }
   }, [options['console_setting.faq']]);
 
+  useEffect(() => {
+    const enabledStr = options['console_setting.faq_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options['console_setting.faq_enabled']]);
+
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
+    try {
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.faq_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
+    }
+  };
+
   const handleBatchDelete = () => {
     if (selectedRowKeys.length === 0) {
       showError('请先选择要删除的常见问答');
@@ -287,6 +315,12 @@ const SettingsFAQ = ({ options, refresh }) => {
             {t('保存设置')}
           </Button>
         </div>
+
+        {/* 启用开关 */}
+        <div className="order-1 md:order-2 flex items-center gap-2">
+          <Switch checked={panelEnabled} onChange={handleToggleEnabled} />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
+        </div>
       </div>
     </div>
   );

+ 30 - 1
web/src/pages/Setting/Dashboard/SettingsUptimeKuma.js

@@ -5,6 +5,7 @@ import {
   Typography,
   Row,
   Col,
+  Switch,
 } from '@douyinfe/semi-ui';
 import {
   Save,
@@ -19,6 +20,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
   const { t } = useTranslation();
 
   const [loading, setLoading] = useState(false);
+  const [panelEnabled, setPanelEnabled] = useState(true);
   const formApiRef = useRef(null);
 
   const initValues = useMemo(() => ({
@@ -32,6 +34,11 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
     }
   }, [initValues]);
 
+  useEffect(() => {
+    const enabledStr = options?.['console_setting.uptime_kuma_enabled'];
+    setPanelEnabled(enabledStr === undefined ? true : enabledStr === 'true' || enabledStr === true);
+  }, [options?.['console_setting.uptime_kuma_enabled']]);
+
   const handleSave = async () => {
     const api = formApiRef.current;
     if (!api) {
@@ -75,6 +82,25 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
     }
   };
 
+  const handleToggleEnabled = async (checked) => {
+    const newValue = checked ? 'true' : 'false';
+    try {
+      const res = await API.put('/api/option/', {
+        key: 'console_setting.uptime_kuma_enabled',
+        value: newValue,
+      });
+      if (res.data.success) {
+        setPanelEnabled(checked);
+        showSuccess(t('设置已保存'));
+        refresh?.();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (err) {
+      showError(err.message);
+    }
+  };
+
   const isValidUrl = useCallback((string) => {
     try {
       new URL(string);
@@ -103,7 +129,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
           </Text>
         </div>
 
-        <div className="flex gap-2">
+        <div className="flex gap-2 items-center">
           <Button
             icon={<Save size={14} />}
             theme='solid'
@@ -114,6 +140,9 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
           >
             {t('保存设置')}
           </Button>
+
+          <Switch checked={panelEnabled} onChange={handleToggleEnabled} />
+          <Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
         </div>
       </div>
     </div>