🎛️ 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,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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user