🎛️ 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:
Apple\Apple
2025-06-14 01:39:23 +08:00
parent a9cdbce9de
commit 4c05377c87
8 changed files with 428 additions and 248 deletions

View File

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

View File

@@ -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,
}
// 全局实例

View File

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

View File

@@ -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>
</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' }}
/>
{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>
<div
className="card-content-fade-indicator"
style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
/>
</div>
</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('服务可用性')}
</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 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('请联系管理员在系统设置中配置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: showAnnouncementScrollHint ? 1 : 0 }}
/>
</div>
</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>
)}

View File

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

View File

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

View File

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

View File

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