🎛️ 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,10 +38,9 @@ func TestStatus(c *gin.Context) {
func GetStatus(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": gin.H{
cs := console_setting.GetConsoleSetting()
data := gin.H{
"version": common.Version,
"start_time": common.StartTime,
"email_verification": common.EmailVerificationEnabled,
@@ -76,14 +75,34 @@ func GetStatus(c *gin.Context) {
"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,
"api_info": console_setting.GetApiInfo(),
"announcements": console_setting.GetAnnouncements(),
"faq": console_setting.GetFAQ(),
},
}
// 根据启用状态注入可选内容
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": 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,10 +1147,11 @@ 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">
{/* 公告卡片 */}
{announcementsEnabled && (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-2"
@@ -1204,8 +1214,10 @@ const Detail = (props) => {
/>
</div>
</Card>
)}
{/* 常见问答卡片 */}
{faqEnabled && (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-1"
@@ -1256,8 +1268,10 @@ const Detail = (props) => {
/>
</div>
</Card>
)}
{/* 服务可用性卡片 */}
{uptimeEnabled && (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-1"
@@ -1351,6 +1365,7 @@ const Detail = (props) => {
/>
</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>