feat(settings): sidebar submenu nav for settings and xray with icon tabs

Settings and Xray Configs are now expandable sidebar submenus that list their sections; clicking a section opens it via the URL hash (e.g. #general, #basic) and the in-page top tab bar is removed on both pages.

Within each section the collapse groups become horizontal tabs, each with an icon; on mobile only the icon shows with the label in a tooltip, via a shared catTabLabel helper used by both settings and xray.

Subscription Formats: the nested collapses in Fragment/Noises/Mux/Direct are replaced with a cleaner layout - framed field groups, and each noise is a card with a delete button plus a dashed add button.

Xray: the Reset to Default button is now a solid danger button so its hover state is visible.
This commit is contained in:
MHSanaei
2026-06-03 09:26:25 +02:00
parent e63cde8fcb
commit ac89ec724f
12 changed files with 400 additions and 458 deletions
+13 -90
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import {
Alert,
Button,
@@ -12,17 +13,8 @@ import {
Row,
Space,
Spin,
Tabs,
Tooltip,
message,
} from 'antd';
import {
CloudServerOutlined,
CodeOutlined,
MessageOutlined,
SafetyOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { HttpUtil, PromiseUtil } from '@/utils';
import { setMessageInstance } from '@/utils/messageBus';
@@ -44,15 +36,6 @@ interface ApiMsg {
const tabSlugs = ['general', 'security', 'telegram', 'subscription', 'subscription-formats'];
function slugToKey(slug: string): string {
const i = tabSlugs.indexOf(slug);
return i >= 0 ? String(i + 1) : '1';
}
function keyToSlug(key: string): string {
return tabSlugs[Number(key) - 1] || tabSlugs[0];
}
function isIp(h: string): boolean {
if (typeof h !== 'string') return false;
const v4 = h.split('.');
@@ -108,21 +91,9 @@ export default function SettingsPage() {
}, []);
const [alertVisible, setAlertVisible] = useState(true);
const [activeTabKey, setActiveTabKey] = useState<string>(() => slugToKey(window.location.hash.slice(1)));
useEffect(() => {
const onHashChange = () => setActiveTabKey(slugToKey(window.location.hash.slice(1)));
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
}, []);
function onTabChange(key: string) {
setActiveTabKey(key);
const slug = keyToSlug(key);
if (window.location.hash !== `#${slug}`) {
history.replaceState(null, '', `#${slug}`);
}
}
const location = useLocation();
const slug = location.hash.replace(/^#/, '');
const activeSlug = tabSlugs.includes(slug) ? slug : 'general';
function rebuildUrlAfterRestart(): string {
const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = allSetting;
@@ -222,58 +193,15 @@ export default function SettingsPage() {
return classes.join(' ');
}, [isDark, isUltra]);
const tabItems = useMemo(() => {
const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = [
{
key: '1',
label: (
<Tooltip title={isMobile ? t('pages.settings.panelSettings') : null}>
<span><SettingOutlined />{!isMobile && <> {t('pages.settings.panelSettings')}</>}</span>
</Tooltip>
),
children: <GeneralTab allSetting={allSetting} updateSetting={updateSetting} />,
},
{
key: '2',
label: (
<Tooltip title={isMobile ? t('pages.settings.securitySettings') : null}>
<span><SafetyOutlined />{!isMobile && <> {t('pages.settings.securitySettings')}</>}</span>
</Tooltip>
),
children: <SecurityTab allSetting={allSetting} updateSetting={updateSetting} />,
},
{
key: '3',
label: (
<Tooltip title={isMobile ? t('pages.settings.TGBotSettings') : null}>
<span><MessageOutlined />{!isMobile && <> {t('pages.settings.TGBotSettings')}</>}</span>
</Tooltip>
),
children: <TelegramTab allSetting={allSetting} updateSetting={updateSetting} />,
},
{
key: '4',
label: (
<Tooltip title={isMobile ? t('pages.settings.subSettings') : null}>
<span><CloudServerOutlined />{!isMobile && <> {t('pages.settings.subSettings')}</>}</span>
</Tooltip>
),
children: <SubscriptionGeneralTab allSetting={allSetting} updateSetting={updateSetting} />,
},
];
if (allSetting.subJsonEnable || allSetting.subClashEnable) {
items.push({
key: '5',
label: (
<Tooltip title={isMobile ? `${t('pages.settings.subSettings')} (Formats)` : null}>
<span><CodeOutlined />{!isMobile && <> {t('pages.settings.subSettings')} (Formats)</>}</span>
</Tooltip>
),
children: <SubscriptionFormatsTab allSetting={allSetting} updateSetting={updateSetting} />,
});
const categoryBody = useMemo(() => {
switch (activeSlug) {
case 'security': return <SecurityTab allSetting={allSetting} updateSetting={updateSetting} />;
case 'telegram': return <TelegramTab allSetting={allSetting} updateSetting={updateSetting} />;
case 'subscription': return <SubscriptionGeneralTab allSetting={allSetting} updateSetting={updateSetting} />;
case 'subscription-formats': return <SubscriptionFormatsTab allSetting={allSetting} updateSetting={updateSetting} />;
default: return <GeneralTab allSetting={allSetting} updateSetting={updateSetting} />;
}
return items;
}, [allSetting, updateSetting, isMobile, t]);
}, [activeSlug, allSetting, updateSetting]);
return (
<ConfigProvider theme={antdThemeConfig}>
@@ -331,12 +259,7 @@ export default function SettingsPage() {
<Col span={24}>
<Card hoverable>
<Tabs
activeKey={activeTabKey}
onChange={onTabChange}
className={isMobile ? 'icons-only' : ''}
items={tabItems}
/>
{categoryBody}
</Card>
</Col>
</Row>