From c730873be38f4a2e0385b7b6a27562a6c5fbebdd Mon Sep 17 00:00:00 2001 From: dushixiang Date: Sun, 23 Aug 2026 16:35:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E9=9D=A2=E5=B8=83=E5=B1=80=E4=B8=8E=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 设置页全宽对齐其他页面,左侧分类导航去卡片化并支持桌面端吸顶 - 通知渠道改为「渠道列表 + 配置区」布局,支持未保存提醒与校验失败自动跳转 - 系统配置/公网IP采集/告警规则页去嵌套卡片,改为平铺分区(SettingsSection) - 主题管理页上传按钮与警告提示合并为一行 - 调整全部页面的小布局问题 --- web/src/layouts/AdminLayout/index.tsx | 1 + web/src/pages/Agents/AgentDetail.tsx | 2 +- web/src/pages/Agents/AgentList.tsx | 2 +- web/src/pages/AlertRecords/index.tsx | 2 +- web/src/pages/ApiKeys/ApiKeyList.tsx | 2 +- web/src/pages/DDNS/index.tsx | 2 +- .../pages/ManageApiKeys/ManageApiKeyList.tsx | 2 +- web/src/pages/Monitors/MonitorList.tsx | 2 +- web/src/pages/Settings/AlertSettings.tsx | 290 ++--- .../pages/Settings/NotificationChannels.tsx | 1135 ++++++++--------- web/src/pages/Settings/PublicIPConfig.tsx | 217 ++-- web/src/pages/Settings/SettingsSection.tsx | 49 + web/src/pages/Settings/SystemConfig.tsx | 289 +++-- web/src/pages/Settings/ThemeSettings.tsx | 15 +- web/src/pages/Settings/index.tsx | 166 +-- 15 files changed, 1059 insertions(+), 1117 deletions(-) create mode 100644 web/src/pages/Settings/SettingsSection.tsx diff --git a/web/src/layouts/AdminLayout/index.tsx b/web/src/layouts/AdminLayout/index.tsx index 51d2d0e..c257541 100644 --- a/web/src/layouts/AdminLayout/index.tsx +++ b/web/src/layouts/AdminLayout/index.tsx @@ -20,6 +20,7 @@ const AdminLayout = () => { '/admin/alert-records', '/admin/agents-install/one-click', '/admin/agents-install/manual', + '/admin/settings', ].includes(location.pathname); const [userInfo, setUserInfo] = useState(null); const themeButtonRef = useRef(null); diff --git a/web/src/pages/Agents/AgentDetail.tsx b/web/src/pages/Agents/AgentDetail.tsx index 80992dd..033994d 100644 --- a/web/src/pages/Agents/AgentDetail.tsx +++ b/web/src/pages/Agents/AgentDetail.tsx @@ -134,7 +134,7 @@ const AgentDetail = () => { ]; return ( -
+
{ ]; return ( -
+
{ ]; return ( -
+
{ ]; return ( -
+
{ ]; return ( -
+
{ ]; return ( -
+
{ const total = monitorPaging?.total || 0; return ( -
+
{ const [form] = Form.useForm(); @@ -42,202 +71,103 @@ const AlertSettings = () => { return (
-
-
- - - + + + + + - - + + + + - + + - - - + + + + - - + + + + - - + + + + - + + - {/*告警规则*/} - - {[ - { key: 'cpu', title: 'CPU 告警规则', thresholdLabel: 'CPU 使用率阈值 (%)', max: 100 }, - { key: 'memory', title: '内存告警规则', thresholdLabel: '内存使用率阈值 (%)', max: 100 }, - { key: 'disk', title: '磁盘告警规则', thresholdLabel: '磁盘使用率阈值 (%)', max: 100 }, - { key: 'network', title: '网速告警规则', thresholdLabel: '网速阈值 (MB/s)', max: 10000 }, - ].map((rule) => ( - - + +
+ {RULES.map((rule) => ( + {({ getFieldValue }) => { const enabled = getFieldValue(['rules', `${rule.key}Enabled`]); return ( -
- - - - - - - - - +
+
+ + + + + {rule.name} + +
+
+ {rule.threshold && ( + + + + )} + {rule.duration && ( + + + + )} +
); }} - - ))} + ))} +
+ - - - {({ getFieldValue }) => { - const enabled = getFieldValue(['rules', 'certEnabled']); - return ( -
- - - - - - -
- ); - }} -
-
- - - - {({ getFieldValue }) => { - const enabled = getFieldValue(['rules', 'serviceEnabled']); - return ( -
- - - - - - -
- ); - }} -
-
- - - - {({ getFieldValue }) => { - const enabled = getFieldValue(['rules', 'agentOfflineEnabled']); - return ( -
- - - - - - -
- ); - }} -
-
- - -
+
); diff --git a/web/src/pages/Settings/NotificationChannels.tsx b/web/src/pages/Settings/NotificationChannels.tsx index 0749319..e2dcca9 100644 --- a/web/src/pages/Settings/NotificationChannels.tsx +++ b/web/src/pages/Settings/NotificationChannels.tsx @@ -1,6 +1,6 @@ import {useEffect, useState} from 'react'; -import {Alert, App, Button, Card, Form, Input, InputNumber, Select, Space, Spin, Switch} from 'antd'; -import {TestTube} from 'lucide-react'; +import {App, Button, Form, Input, InputNumber, Select, Space, Spin, Switch, Tag} from 'antd'; +import {Bell, Bot, Building2, ExternalLink, Mail, MessageSquare, Smartphone, TestTube, Webhook, type LucideIcon} from 'lucide-react'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import { getNotificationChannels, @@ -8,14 +8,91 @@ import { saveNotificationChannels, testNotificationChannel, } from '@/api/property.ts'; -import {getErrorMessage} from '@/lib/utils'; +import {cn, getErrorMessage} from '@/lib/utils'; import NotificationCustomHelp from "@/pages/Settings/NotificationCustomHelp.tsx"; +type ChannelType = 'dingtalk' | 'wecom' | 'wecomApp' | 'feishu' | 'telegram' | 'email' | 'webhook'; + +interface ChannelDefinition { + type: ChannelType; + name: string; + description: string; + enabledField: string; + fieldPrefix: string; + icon: LucideIcon; + docsUrl?: string; +} + +const CHANNELS: ChannelDefinition[] = [ + { + type: 'dingtalk', + name: '钉钉', + description: '自定义机器人通知', + enabledField: 'dingtalkEnabled', + fieldPrefix: 'dingtalk', + icon: Bell, + docsUrl: 'https://open.dingtalk.com/document/robots/custom-robot-access', + }, + { + type: 'wecom', + name: '企业微信', + description: '群机器人通知', + enabledField: 'wecomEnabled', + fieldPrefix: 'wecomSecretKey', + icon: Building2, + docsUrl: 'https://work.weixin.qq.com/api/doc/90000/90136/91770', + }, + { + type: 'wecomApp', + name: '企业微信应用', + description: '应用消息通知', + enabledField: 'wecomAppEnabled', + fieldPrefix: 'wecomApp', + icon: Smartphone, + docsUrl: 'https://developer.work.weixin.qq.com/document/path/90236', + }, + { + type: 'feishu', + name: '飞书', + description: '自定义机器人通知', + enabledField: 'feishuEnabled', + fieldPrefix: 'feishu', + icon: MessageSquare, + docsUrl: 'https://www.feishu.cn/hc/zh-CN/articles/360024984973', + }, + { + type: 'telegram', + name: 'Telegram', + description: 'Bot 消息通知', + enabledField: 'telegramEnabled', + fieldPrefix: 'telegram', + icon: Bot, + docsUrl: 'https://core.telegram.org/bots/api', + }, + { + type: 'email', + name: '邮件', + description: 'SMTP 邮件通知', + enabledField: 'emailEnabled', + fieldPrefix: 'email', + icon: Mail, + }, + { + type: 'webhook', + name: '自定义 Webhook', + description: '自定义 HTTP 请求', + enabledField: 'webhookEnabled', + fieldPrefix: 'webhook', + icon: Webhook, + }, +]; + const NotificationChannels = () => { const [form] = Form.useForm(); const {message: messageApi} = App.useApp(); const queryClient = useQueryClient(); - const [savedValues, setSavedValues] = useState>({}); + const [selectedType, setSelectedType] = useState('dingtalk'); + const [dirty, setDirty] = useState(false); // 验证 token 字段,检查是否误输入了完整的 URL const validateToken = (_: any, value: string) => { @@ -34,6 +111,16 @@ const NotificationChannels = () => { const emailEnabled = Form.useWatch('emailEnabled', form); const webhookEnabled = Form.useWatch('webhookEnabled', form); + const enabledMap: Record = { + dingtalk: dingtalkEnabled, + wecom: wecomEnabled, + wecomApp: wecomAppEnabled, + feishu: feishuEnabled, + telegram: telegramEnabled, + email: emailEnabled, + webhook: webhookEnabled, + }; + // 获取通知渠道列表 const {data: channels = [], isLoading} = useQuery({ queryKey: ['notificationChannels'], @@ -45,6 +132,7 @@ const NotificationChannels = () => { mutationFn: saveNotificationChannels, onSuccess: () => { messageApi.success('保存成功'); + setDirty(false); queryClient.invalidateQueries({queryKey: ['notificationChannels']}); }, onError: (error: unknown) => { @@ -115,16 +203,12 @@ const NotificationChannels = () => { }); form.setFieldsValue(formValues); - // 保存当前值作为"已保存"的值,用于检测是否有未保存的修改 - setSavedValues(formValues); } }, [channels, form]); // 将表单值转换回渠道数组 - const handleSave = async () => { - try { - const values = await form.validateFields(); - const newChannels: NotificationChannel[] = []; + const handleSave = (values: any) => { + const newChannels: NotificationChannel[] = []; // 钉钉 if (values.dingtalkEnabled || values.dingtalkSecretKey) { @@ -229,15 +313,26 @@ const NotificationChannels = () => { } saveMutation.mutate(newChannels); - } catch (error) { - // 表单验证失败 - } }; - const handleTest = (type: string) => { - testMutation.mutate(type); + // 校验失败时跳转到第一个出错的渠道(错误可能位于未选中的渠道分组中) + const handleValidateFailed = (errorFields: { name: (string | number)[] }[]) => { + const firstField = errorFields?.[0]?.name?.[0]; + if (typeof firstField === 'string') { + const matched = [...CHANNELS] + .sort((a, b) => b.fieldPrefix.length - a.fieldPrefix.length) + .find((channel) => firstField.startsWith(channel.fieldPrefix)); + if (matched) { + setSelectedType(matched.type); + } + } + messageApi.warning('存在未填写的必填项,请检查对应渠道的配置'); }; + const selectedChannel = CHANNELS.find((channel) => channel.type === selectedType) ?? CHANNELS[0]; + const selectedEnabled = enabledMap[selectedChannel.type]; + const testingSelected = testMutation.isPending && testMutation.variables === selectedChannel.type; + if (isLoading) { return (
@@ -246,599 +341,463 @@ const NotificationChannels = () => { ); } + const fieldGridClass = 'grid gap-x-4 sm:grid-cols-2'; + + const renderChannelFields = (channel: ChannelDefinition) => { + switch (channel.type) { + case 'dingtalk': + return ( +
+ + + + + + +
+ ); + case 'wecom': + return ( + + + + ); + case 'wecomApp': + return ( +
+ + + +
+ + + + + + + + + + + + +
+
+ ); + case 'feishu': + return ( +
+ + + + + + +
+ ); + case 'telegram': + return ( +
+ + + + + + +
+ ); + case 'email': + return ( +
+
+ + + + + + + + + + + + + + + + + + +
+
+ ); + case 'webhook': + return ( +
+ + + + {/* HTTP 方法 */} + + + + + + + + + ))} + + + + + )} + + +
+ ); + } + }; + + const SelectedIcon = selectedChannel.icon; + return ( -
-
- -
- -
-
- {/* 钉钉通知 */} - -
钉钉通知
- -
- } - type="inner" - extra={ - - } - > - - - - - - prevValues.dingtalkEnabled !== currentValues.dingtalkEnabled - } - > - {({getFieldValue}) => - getFieldValue('dingtalkEnabled') ? ( - <> - - - - - - - - ) : null - } - - - - {/* 企业微信通知 */} - -
企业微信通知
- -
- } - type="inner" - extra={ - - } - > - - - - - prevValues.wecomEnabled !== currentValues.wecomEnabled} - > - {({getFieldValue}) => - getFieldValue('wecomEnabled') ? ( - handleValidateFailed(errorFields)} + onValuesChange={() => setDirty(true)} + > +
+ {/* 渠道列表 */} +
+
+ 点击渠道查看配置,开关控制启用状态 +
+
+ {CHANNELS.map((channel) => { + const Icon = channel.icon; + const enabled = enabledMap[channel.type]; + const selected = channel.type === selectedType; + return ( +
+ + + - ) : null - } - - +
+ ); + })} +
+
- {/* 企业微信应用通知 */} - -
企业微信应用通知
-
- 了解更多:https://developer.work.weixin.qq.com/document/path/90236 + {/* 选中渠道的配置 */} +
+
+
+ + + +
+
+ + {selectedChannel.name} + + + {selectedEnabled ? '已启用' : '未启用'} + +
+
+ {selectedChannel.description}
- } - type="inner" - extra={ - - } - > - - - - - prevValues.wecomAppEnabled !== currentValues.wecomAppEnabled} - > - {({getFieldValue}) => - getFieldValue('wecomAppEnabled') ? ( - <> - - - - - - - - - - - - - - - - - ) : null - } - - - - {/* 飞书通知 */} - -
飞书通知
- - } - type="inner" - extra={ + rel="noopener noreferrer" + > + 接入文档 + + )} - } - > - - - +
+
- - prevValues.feishuEnabled !== currentValues.feishuEnabled - } - > - {({getFieldValue}) => - getFieldValue('feishuEnabled') ? ( - <> - - - - - - - - ) : null - } - - +
- {/* Telegram 通知 */} - -
Telegram 通知
- -
- } - type="inner" - extra={ - - } - > - - - + {!selectedEnabled && ( +
+ + 当前渠道未启用,打开左侧的开关后即可编辑配置。 +
+ )} - - prevValues.telegramEnabled !== currentValues.telegramEnabled - } - > - {({getFieldValue}) => - getFieldValue('telegramEnabled') ? ( - <> - - - - - - - - ) : null - } - - + {/* 已启用渠道的表单项保持挂载(仅隐藏未选中的),保证校验行为一致 */} + {CHANNELS.filter((channel) => enabledMap[channel.type]).map((channel) => ( +
+ {renderChannelFields(channel)} +
+ ))} - {/* 邮件通知 */} - -
邮件通知
-
- 使用 SMTP 协议发送邮件告警 -
-
- } - type="inner" - extra={ - - } - > - - - + {selectedType === 'webhook' && } - - prevValues.emailEnabled !== currentValues.emailEnabled - } - > - {({getFieldValue}) => - getFieldValue('emailEnabled') ? ( - <> - - - - - - - - - - - - - - - - - - - - ) : null - } - - - - {/* 自定义 Webhook */} - } - onClick={() => handleTest('webhook')} - loading={testMutation.isPending} - disabled={!webhookEnabled} - > - 测试 - - } - > - - - - - - prevValues.webhookEnabled !== currentValues.webhookEnabled - } - > - {({getFieldValue}) => - getFieldValue('webhookEnabled') ? ( - <> - - - - {/* HTTP 方法 */} - - - - - - - - - ))} - - - - - )} - - - - ) : null - } - - - - - - +
- + {dirty && ( + + 配置尚未保存,保存后才能发送测试通知。 + + )} +
- -
+
+ ); }; diff --git a/web/src/pages/Settings/PublicIPConfig.tsx b/web/src/pages/Settings/PublicIPConfig.tsx index fcdcbd9..a686225 100644 --- a/web/src/pages/Settings/PublicIPConfig.tsx +++ b/web/src/pages/Settings/PublicIPConfig.tsx @@ -1,11 +1,12 @@ import {useEffect} from 'react'; -import {App, Button, Card, Form, Input, InputNumber, Radio, Select, Space, Spin, Switch} from 'antd'; +import {App, Button, Form, Input, InputNumber, Radio, Select, Spin, Switch} from 'antd'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import type {PublicIPConfig} from '@/api/property'; import {getPublicIPConfig, savePublicIPConfig} from '@/api/property'; import {listAgentsByAdmin} from '@/api/agent.ts'; import {getErrorMessage} from '@/lib/utils'; import type {Agent} from '@/types'; +import {SettingsActions, SettingsSection} from './SettingsSection'; interface PublicIPConfigProps { defaultIPv4APIs: string[]; @@ -136,122 +137,118 @@ const PublicIPConfigComponent = ({defaultIPv4APIs, defaultIPv6APIs}: PublicIPCon return (
-
- -
- - - - - - -
-
- - - + +
+ - - - 全部探针 - 自定义探针 - - - - {({getFieldValue}) => { - const enabled = getFieldValue('ipv4Enabled'); - const scope = getFieldValue('ipv4Scope'); - if (!enabled || scope !== 'custom') { - return null; - } - return ( - - - - ); - }} - - - - - - - - - - - - + + + -
+ + + 全部探针 + 自定义探针 + + + + {({getFieldValue}) => { + const enabled = getFieldValue('ipv4Enabled'); + const scope = getFieldValue('ipv4Scope'); + if (!enabled || scope !== 'custom') { + return null; + } + return ( + + + + ); + }} + + + + +
+ + + + + +
); diff --git a/web/src/pages/Settings/SettingsSection.tsx b/web/src/pages/Settings/SettingsSection.tsx new file mode 100644 index 0000000..7916879 --- /dev/null +++ b/web/src/pages/Settings/SettingsSection.tsx @@ -0,0 +1,49 @@ +import type {PropsWithChildren, ReactNode} from 'react'; +import {cn} from '@/lib/utils'; + +interface SettingsSectionProps { + title: string; + description?: ReactNode; + /** 渲染与上一区块的分隔线,首个区块传 false */ + divided?: boolean; + className?: string; + children: ReactNode; +} + +/** 设置页内部的平铺分区:标题 + 描述 + 内容,用分隔线取代嵌套卡片。 */ +export const SettingsSection = ({title, description, divided = true, className, children}: SettingsSectionProps) => ( +
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
{children}
+
+); + +/** 设置页底部的操作按钮行。 */ +export const SettingsActions = ({children}: PropsWithChildren) => ( +
+ {children} +
+); + +interface SettingsSwitchRowProps { + title: string; + description?: ReactNode; + /** 右侧的开关(通常是 noStyle 的 Form.Item + Switch) */ + children: ReactNode; +} + +/** 带说明文字的开关行:左侧标题与描述,右侧开关。 */ +export const SettingsSwitchRow = ({title, description, children}: SettingsSwitchRowProps) => ( +
+
+
{title}
+ {description ? ( +
{description}
+ ) : null} +
+
{children}
+
+); diff --git a/web/src/pages/Settings/SystemConfig.tsx b/web/src/pages/Settings/SystemConfig.tsx index 1271aa0..4bf8c76 100644 --- a/web/src/pages/Settings/SystemConfig.tsx +++ b/web/src/pages/Settings/SystemConfig.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from 'react'; -import { App, Button, Card, Form, Input, Radio, Space, Spin, Upload } from 'antd'; +import { App, Button, Form, Input, Radio, Space, Spin, Upload } from 'antd'; import { Upload as UploadIcon, Grid3x3, List } from 'lucide-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import type { SystemConfig } from '@/api/property.ts'; import { getSystemConfig, saveSystemConfig } from '@/api/property.ts'; import { getErrorMessage } from '@/lib/utils.ts'; import type { RcFile } from 'antd/es/upload/interface'; +import {SettingsActions, SettingsSection} from './SettingsSection'; const SystemConfigComponent = () => { const [form] = Form.useForm(); @@ -142,106 +143,105 @@ const SystemConfigComponent = () => { return (
-
- -
- ({ - validator(_, value) { - if (!value && !getFieldValue('systemNameZh')) { - return Promise.reject(new Error('系统英文名称和中文名称不能同时为空')); - } - return Promise.resolve(); - }, - }), - { max: 50, message: '系统名称不能超过 50 个字符' }, - ]} - > - - - - ({ - validator(_, value) { - if (!value && !getFieldValue('systemNameEn')) { - return Promise.reject(new Error('系统英文名称和中文名称不能同时为空')); - } - return Promise.resolve(); - }, - }), - { max: 50, message: '系统名称不能超过 50 个字符' }, - ]} - > - - -
- + +
({ + validator(_, value) { + if (!value && !getFieldValue('systemNameZh')) { + return Promise.reject(new Error('系统英文名称和中文名称不能同时为空')); + } + return Promise.resolve(); + }, + }), + { max: 50, message: '系统名称不能超过 50 个字符' }, ]} - tooltip="ICP 备案号将显示在公共页面底部,例如:京ICP备12345678号" > - + ({ + validator(_, value) { + if (!value && !getFieldValue('systemNameEn')) { + return Promise.reject(new Error('系统英文名称和中文名称不能同时为空')); + } + return Promise.resolve(); + }, + }), + { max: 50, message: '系统名称不能超过 50 个字符' }, + ]} > - - - - - 网格视图 - - - - - - 列表视图 - - - + +
- - - - - - - -
- - + + + + + + + + + 网格视图 + + + + + + 列表视图 + + + + + + + + + + + + + +
{ rows={6} /> - +
+
- - - {({ getFieldValue }) => { - const systemNameEn = getFieldValue('systemNameEn') || ''; - const systemNameZh = getFieldValue('systemNameZh') || ''; + + + {({ getFieldValue }) => { + const systemNameEn = getFieldValue('systemNameEn') || ''; + const systemNameZh = getFieldValue('systemNameZh') || ''; - // 与 PublicHeader 相同的分割逻辑 - let leftName = ''; - let rightName = ''; + // 与 PublicHeader 相同的分割逻辑 + let leftName = ''; + let rightName = ''; - if (systemNameEn) { - // 优先在空格处分割 - const spaceIndex = systemNameEn.indexOf(' '); - if (spaceIndex > 0) { - leftName = systemNameEn.substring(0, spaceIndex); - rightName = systemNameEn.substring(spaceIndex); // 保留空格 - } else { - // 如果没有空格,从中间分割 - const mid = Math.floor(systemNameEn.length / 2); - leftName = systemNameEn.substring(0, mid); - rightName = systemNameEn.substring(mid); - } + if (systemNameEn) { + // 优先在空格处分割 + const spaceIndex = systemNameEn.indexOf(' '); + if (spaceIndex > 0) { + leftName = systemNameEn.substring(0, spaceIndex); + rightName = systemNameEn.substring(spaceIndex); // 保留空格 + } else { + // 如果没有空格,从中间分割 + const mid = Math.floor(systemNameEn.length / 2); + leftName = systemNameEn.substring(0, mid); + rightName = systemNameEn.substring(mid); } + } - return ( -
- Logo 预览 { - e.currentTarget.src = '/logo.png'; - }} - /> -
-

- {leftName}{rightName} -

-

- {systemNameZh} -

-
+ return ( +
+ Logo 预览 { + e.currentTarget.src = '/logo.png'; + }} + /> +
+

+ {leftName}{rightName} +

+

+ {systemNameZh} +

- ); - }} - - - - - - - - +
+ ); + }} -
+
+ + + + +
); diff --git a/web/src/pages/Settings/ThemeSettings.tsx b/web/src/pages/Settings/ThemeSettings.tsx index 575e5c7..6a7dc18 100644 --- a/web/src/pages/Settings/ThemeSettings.tsx +++ b/web/src/pages/Settings/ThemeSettings.tsx @@ -123,7 +123,14 @@ const ThemeSettings = () => { return (
-
+
+
+ +
{
- - {themesQuery.isLoading ? (
diff --git a/web/src/pages/Settings/index.tsx b/web/src/pages/Settings/index.tsx index bb49eef..10a6ffa 100644 --- a/web/src/pages/Settings/index.tsx +++ b/web/src/pages/Settings/index.tsx @@ -1,10 +1,14 @@ +import type {ReactNode} from 'react'; import {Bell, MessageSquare, Palette, Settings2, Wifi} from 'lucide-react'; +import type {LucideIcon} from 'lucide-react'; import AlertSettings from './AlertSettings'; import NotificationChannels from './NotificationChannels'; import SystemConfig from './SystemConfig'; import PublicIPConfig from './PublicIPConfig'; import ThemeSettings from './ThemeSettings'; -import {useSearchParams} from "react-router-dom"; +import {PageHeader} from '@/components/PageHeader'; +import {PagePanel} from '@/components/PagePanel'; +import {useSearchParams} from 'react-router-dom'; // 默认 IPv4 API 列表 export const defaultIPv4APIs = [ @@ -23,88 +27,94 @@ export const defaultIPv6APIs = [ 'https://v6.yinghualuo.cn/bejson', ]; +interface SettingsTab { + key: string; + label: string; + icon: LucideIcon; + content: ReactNode; +} + +const settingsTabs: SettingsTab[] = [ + { + key: 'system', + label: '系统配置', + icon: Settings2, + content: , + }, + { + key: 'themes', + label: '主题管理', + icon: Palette, + content: , + }, + { + key: 'channels', + label: '通知渠道', + icon: MessageSquare, + content: , + }, + { + key: 'public-ip', + label: '公网 IP 采集', + icon: Wifi, + content: ( + + ), + }, + { + key: 'alert', + label: '告警规则', + icon: Bell, + content: , + }, +]; + const Settings = () => { - const [searchParams, setSearchParams] = useSearchParams({tab: 'system'}); - const items = [ - { - key: 'system', - label: ( - - - 系统配置 - - ), - children: , - }, - { - key: 'themes', - label: ( - - - 主题管理 - - ), - children: , - }, - { - key: 'channels', - label: ( - - - 通知渠道 - - ), - children: , - }, - { - key: 'public-ip', - label: ( - - - 公网 IP 采集 - - ), - children: ( - - ), - }, - { - key: 'alert', - label: ( - - - 告警规则 - - ), - children: , - }, - ]; - const activeKey = searchParams.get('tab') || 'system'; - const activeItem = items.find((item) => item.key === activeKey) || items[0]; + const activeKey = settingsTabs.some((tab) => tab.key === searchParams.get('tab')) + ? searchParams.get('tab')! + : 'system'; + const activeTab = settingsTabs.find((tab) => tab.key === activeKey) ?? settingsTabs[0]; return ( -
- -
{activeItem.children}
+
+ + +
+ + + + {activeTab.content} + +
); };