🎛️ 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.
This commit is contained in:
@@ -38,52 +38,71 @@ func TestStatus(c *gin.Context) {
|
|||||||
|
|
||||||
func GetStatus(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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": gin.H{
|
"data": data,
|
||||||
"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(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ type ConsoleSetting struct {
|
|||||||
UptimeKumaSlug string `json:"uptime_kuma_slug"` // Uptime Kuma Status Page Slug
|
UptimeKumaSlug string `json:"uptime_kuma_slug"` // Uptime Kuma Status Page Slug
|
||||||
Announcements string `json:"announcements"` // 系统公告 (JSON 数组字符串)
|
Announcements string `json:"announcements"` // 系统公告 (JSON 数组字符串)
|
||||||
FAQ string `json:"faq"` // 常见问题 (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: "",
|
UptimeKumaSlug: "",
|
||||||
Announcements: "",
|
Announcements: "",
|
||||||
FAQ: "",
|
FAQ: "",
|
||||||
|
ApiInfoEnabled: true,
|
||||||
|
UptimeKumaEnabled: true,
|
||||||
|
AnnouncementsEnabled: true,
|
||||||
|
FAQEnabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局实例
|
// 全局实例
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const DashboardSetting = () => {
|
|||||||
'console_setting.faq': '',
|
'console_setting.faq': '',
|
||||||
'console_setting.uptime_kuma_url': '',
|
'console_setting.uptime_kuma_url': '',
|
||||||
'console_setting.uptime_kuma_slug': '',
|
'console_setting.uptime_kuma_slug': '',
|
||||||
|
'console_setting.api_info_enabled': '',
|
||||||
|
'console_setting.announcements_enabled': '',
|
||||||
|
'console_setting.faq_enabled': '',
|
||||||
|
'console_setting.uptime_kuma_enabled': '',
|
||||||
|
|
||||||
// 用于迁移检测的旧键,下个版本会删除
|
// 用于迁移检测的旧键,下个版本会删除
|
||||||
ApiInfo: '',
|
ApiInfo: '',
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ const Detail = (props) => {
|
|||||||
let now = new Date();
|
let now = new Date();
|
||||||
const isAdminUser = isAdmin();
|
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 ==========
|
// ========== Helper Functions ==========
|
||||||
const getDefaultTime = useCallback(() => {
|
const getDefaultTime = useCallback(() => {
|
||||||
return localStorage.getItem('data_export_default_time') || 'hour';
|
return localStorage.getItem('data_export_default_time') || 'hour';
|
||||||
@@ -1015,10 +1024,10 @@ const Detail = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<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
|
||||||
{...CARD_PROPS}
|
{...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={
|
title={
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
|
||||||
<div className={FLEX_CENTER_GAP2}>
|
<div className={FLEX_CENTER_GAP2}>
|
||||||
@@ -1061,7 +1070,7 @@ const Detail = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{!statusState?.status?.self_use_mode_enabled && (
|
{hasApiInfoPanel && (
|
||||||
<Card
|
<Card
|
||||||
{...CARD_PROPS}
|
{...CARD_PROPS}
|
||||||
className="bg-gray-50 border-0 !rounded-2xl"
|
className="bg-gray-50 border-0 !rounded-2xl"
|
||||||
@@ -1138,219 +1147,225 @@ const Detail = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 系统公告和常见问答卡片 */}
|
{/* 系统公告和常见问答卡片 */}
|
||||||
{!statusState?.status?.self_use_mode_enabled && (
|
{hasInfoPanels && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||||
{/* 公告卡片 */}
|
{/* 公告卡片 */}
|
||||||
<Card
|
{announcementsEnabled && (
|
||||||
{...CARD_PROPS}
|
<Card
|
||||||
className="shadow-sm !rounded-2xl lg:col-span-2"
|
{...CARD_PROPS}
|
||||||
title={
|
className="shadow-sm !rounded-2xl lg:col-span-2"
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
|
||||||
<Bell size={16} />
|
<div className="flex items-center gap-2">
|
||||||
{t('系统公告')}
|
<Bell size={16} />
|
||||||
<Tag size="small" color="grey" shape="circle">
|
{t('系统公告')}
|
||||||
{t('显示最新20条')}
|
<Tag size="small" color="grey" shape="circle">
|
||||||
</Tag>
|
{t('显示最新20条')}
|
||||||
</div>
|
</Tag>
|
||||||
{/* 图例 */}
|
|
||||||
<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>
|
<div className="flex flex-wrap gap-3 text-xs">
|
||||||
<div
|
{announcementLegendData.map((legend, index) => (
|
||||||
className="card-content-fade-indicator"
|
<div key={index} className="flex items-center gap-1">
|
||||||
style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
|
<div
|
||||||
/>
|
className="w-2 h-2 rounded-full"
|
||||||
</div>
|
style={{
|
||||||
</Card>
|
backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
|
||||||
|
legend.color === 'blue' ? '#3b82f6' :
|
||||||
{/* 常见问答卡片 */}
|
legend.color === 'green' ? '#10b981' :
|
||||||
<Card
|
legend.color === 'orange' ? '#f59e0b' :
|
||||||
{...CARD_PROPS}
|
legend.color === 'red' ? '#ef4444' : '#8b9aa7'
|
||||||
className="shadow-sm !rounded-2xl lg:col-span-1"
|
}}
|
||||||
title={
|
/>
|
||||||
<div className={FLEX_CENTER_GAP2}>
|
<span className="text-gray-600">{legend.label}</span>
|
||||||
<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('服务可用性')}
|
|
||||||
</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>
|
||||||
))
|
))}
|
||||||
|
</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">
|
<div className="flex justify-center items-center py-8">
|
||||||
<Empty
|
<Empty
|
||||||
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
|
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
|
||||||
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
|
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
|
||||||
title={t('暂无监控数据')}
|
title={t('暂无系统公告')}
|
||||||
description={t('请联系管理员在系统设置中配置Uptime')}
|
description={t('请联系管理员在系统设置中配置公告信息')}
|
||||||
style={{ padding: '12px' }}
|
style={{ padding: '12px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
<div
|
||||||
<div
|
className="card-content-fade-indicator"
|
||||||
className="card-content-fade-indicator"
|
style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
|
||||||
style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
)}
|
||||||
|
|
||||||
|
{/* 常见问答卡片 */}
|
||||||
|
{faqEnabled && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 服务可用性卡片 */}
|
||||||
|
{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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Avatar,
|
Avatar,
|
||||||
Modal,
|
Modal,
|
||||||
Tag
|
Tag,
|
||||||
|
Switch
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
@@ -48,6 +49,9 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
|||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
|
// 面板启用状态 state
|
||||||
|
const [panelEnabled, setPanelEnabled] = useState(true);
|
||||||
|
|
||||||
const colorOptions = [
|
const colorOptions = [
|
||||||
{ value: 'blue', label: 'blue' },
|
{ value: 'blue', label: 'blue' },
|
||||||
{ value: 'green', label: 'green' },
|
{ value: 'green', label: 'green' },
|
||||||
@@ -191,6 +195,30 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
|||||||
}
|
}
|
||||||
}, [options['console_setting.api_info'], options.ApiInfo]);
|
}, [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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
@@ -325,6 +353,15 @@ const SettingsAPIInfo = ({ options, refresh }) => {
|
|||||||
{t('保存设置')}
|
{t('保存设置')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
Empty,
|
Empty,
|
||||||
Divider,
|
Divider,
|
||||||
Modal,
|
Modal,
|
||||||
Tag
|
Tag,
|
||||||
|
Switch
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
@@ -47,6 +48,9 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
|||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
|
// 面板启用状态
|
||||||
|
const [panelEnabled, setPanelEnabled] = useState(true);
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{ value: 'default', label: t('默认') },
|
{ value: 'default', label: t('默认') },
|
||||||
{ value: 'ongoing', label: t('进行中') },
|
{ value: 'ongoing', label: t('进行中') },
|
||||||
@@ -294,6 +298,30 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
|||||||
}
|
}
|
||||||
}, [options['console_setting.announcements'], options.Announcements]);
|
}, [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 = () => {
|
const handleBatchDelete = () => {
|
||||||
if (selectedRowKeys.length === 0) {
|
if (selectedRowKeys.length === 0) {
|
||||||
showError('请先选择要删除的系统公告');
|
showError('请先选择要删除的系统公告');
|
||||||
@@ -350,6 +378,12 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
|||||||
{t('保存设置')}
|
{t('保存设置')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Empty,
|
Empty,
|
||||||
Divider,
|
Divider,
|
||||||
Modal
|
Modal,
|
||||||
|
Switch
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
@@ -44,6 +45,9 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
|
// 面板启用状态
|
||||||
|
const [panelEnabled, setPanelEnabled] = useState(true);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t('问题标题'),
|
title: t('问题标题'),
|
||||||
@@ -231,6 +235,30 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
}
|
}
|
||||||
}, [options['console_setting.faq']]);
|
}, [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 = () => {
|
const handleBatchDelete = () => {
|
||||||
if (selectedRowKeys.length === 0) {
|
if (selectedRowKeys.length === 0) {
|
||||||
showError('请先选择要删除的常见问答');
|
showError('请先选择要删除的常见问答');
|
||||||
@@ -287,6 +315,12 @@ const SettingsFAQ = ({ options, refresh }) => {
|
|||||||
{t('保存设置')}
|
{t('保存设置')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
|
Switch,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
Save,
|
Save,
|
||||||
@@ -19,6 +20,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [panelEnabled, setPanelEnabled] = useState(true);
|
||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
|
|
||||||
const initValues = useMemo(() => ({
|
const initValues = useMemo(() => ({
|
||||||
@@ -32,6 +34,11 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
|||||||
}
|
}
|
||||||
}, [initValues]);
|
}, [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 handleSave = async () => {
|
||||||
const api = formApiRef.current;
|
const api = formApiRef.current;
|
||||||
if (!api) {
|
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) => {
|
const isValidUrl = useCallback((string) => {
|
||||||
try {
|
try {
|
||||||
new URL(string);
|
new URL(string);
|
||||||
@@ -103,7 +129,7 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<Button
|
<Button
|
||||||
icon={<Save size={14} />}
|
icon={<Save size={14} />}
|
||||||
theme='solid'
|
theme='solid'
|
||||||
@@ -114,6 +140,9 @@ const SettingsUptimeKuma = ({ options, refresh }) => {
|
|||||||
>
|
>
|
||||||
{t('保存设置')}
|
{t('保存设置')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Switch checked={panelEnabled} onChange={handleToggleEnabled} />
|
||||||
|
<Text>{panelEnabled ? t('已启用') : t('已禁用')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user