mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-26 21:17:14 +00:00
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:
@@ -2,15 +2,26 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Card,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
PartitionOutlined,
|
||||
PlusOutlined,
|
||||
ScissorOutlined,
|
||||
SendOutlined,
|
||||
SettingOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
import { sanitizePath, normalizePath } from './uriPath';
|
||||
import './SubscriptionFormatsTab.css';
|
||||
|
||||
@@ -72,6 +83,7 @@ function readJson<T>(raw: string, fallback: T): T {
|
||||
|
||||
export default function SubscriptionFormatsTab({ allSetting, updateSetting }: SubscriptionFormatsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
const fragment = allSetting.subJsonFragment !== '';
|
||||
const noisesEnabled = allSetting.subJsonNoises !== '';
|
||||
@@ -190,10 +202,10 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse defaultActiveKey="1" items={[
|
||||
<Tabs defaultActiveKey="1" items={[
|
||||
{
|
||||
key: '1',
|
||||
label: t('pages.settings.panelSettings'),
|
||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
{allSetting.subJsonEnable && (
|
||||
@@ -239,40 +251,30 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('pages.settings.fragment'),
|
||||
label: catTabLabel(<ScissorOutlined />, t('pages.settings.fragment'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.fragment')} description={t('pages.settings.fragmentDesc')}>
|
||||
<Switch checked={fragment} onChange={setFragmentEnabled} />
|
||||
</SettingListItem>
|
||||
{fragment && (
|
||||
<div className="nested-block">
|
||||
<Collapse items={[
|
||||
{
|
||||
key: 'sett',
|
||||
label: t('pages.settings.fragmentSett'),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packets')}>
|
||||
<Input value={fragmentObj.packets} placeholder="1-1 | 1-3 | tlshello | …"
|
||||
onChange={(e) => setFragmentField('packets', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.length')}>
|
||||
<Input value={fragmentObj.length} placeholder="100-200"
|
||||
onChange={(e) => setFragmentField('length', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.interval')}>
|
||||
<Input value={fragmentObj.interval} placeholder="10-20"
|
||||
onChange={(e) => setFragmentField('interval', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.maxSplit')}>
|
||||
<Input value={fragmentObj.maxSplit} placeholder="300-400"
|
||||
onChange={(e) => setFragmentField('maxSplit', e.target.value)} />
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]} />
|
||||
<div className="format-settings">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packets')}>
|
||||
<Input value={fragmentObj.packets} placeholder="1-1 | 1-3 | tlshello | …"
|
||||
onChange={(e) => setFragmentField('packets', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.length')}>
|
||||
<Input value={fragmentObj.length} placeholder="100-200"
|
||||
onChange={(e) => setFragmentField('length', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.interval')}>
|
||||
<Input value={fragmentObj.interval} placeholder="10-20"
|
||||
onChange={(e) => setFragmentField('interval', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.maxSplit')}>
|
||||
<Input value={fragmentObj.maxSplit} placeholder="300-400"
|
||||
onChange={(e) => setFragmentField('maxSplit', e.target.value)} />
|
||||
</SettingListItem>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -280,54 +282,60 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: t('pages.settings.subFormats.noises'),
|
||||
label: catTabLabel(<ThunderboltOutlined />, t('pages.settings.subFormats.noises'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.noises')} description={t('pages.settings.noisesDesc')}>
|
||||
<Switch checked={noisesEnabled} onChange={setNoisesEnabled} />
|
||||
</SettingListItem>
|
||||
{noisesEnabled && (
|
||||
<div className="nested-block">
|
||||
<Collapse items={noisesArray.map((noise, index) => ({
|
||||
key: String(index),
|
||||
label: t('pages.settings.subFormats.noiseItem', { n: index + 1 }),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.type')}>
|
||||
<Select
|
||||
value={noise.type}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => updateNoiseField(index, 'type', v)}
|
||||
options={['rand', 'base64', 'str', 'hex'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packet')}>
|
||||
<Input value={noise.packet} placeholder="5-10"
|
||||
onChange={(e) => updateNoiseField(index, 'packet', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.delayMs')}>
|
||||
<Input value={noise.delay} placeholder="10-20"
|
||||
onChange={(e) => updateNoiseField(index, 'delay', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.applyTo')}>
|
||||
<Select
|
||||
value={noise.applyTo}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => updateNoiseField(index, 'applyTo', v)}
|
||||
options={['ip', 'ipv4', 'ipv6'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<Space style={{ padding: '10px 20px' }}>
|
||||
{noisesArray.length > 1 && (
|
||||
<Button type="primary" danger onClick={() => removeNoise(index)}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</>
|
||||
),
|
||||
}))} />
|
||||
<Button type="primary" style={{ marginTop: 10 }} onClick={addNoise}>{t('pages.settings.subFormats.addNoise')}</Button>
|
||||
<div className="format-settings-list">
|
||||
{noisesArray.map((noise, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
size="small"
|
||||
className="noise-card"
|
||||
title={t('pages.settings.subFormats.noiseItem', { n: index + 1 })}
|
||||
extra={noisesArray.length > 1 ? (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
aria-label={t('delete')}
|
||||
onClick={() => removeNoise(index)}
|
||||
/>
|
||||
) : null}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.type')}>
|
||||
<Select
|
||||
value={noise.type}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => updateNoiseField(index, 'type', v)}
|
||||
options={['rand', 'base64', 'str', 'hex'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packet')}>
|
||||
<Input value={noise.packet} placeholder="5-10"
|
||||
onChange={(e) => updateNoiseField(index, 'packet', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.delayMs')}>
|
||||
<Input value={noise.delay} placeholder="10-20"
|
||||
onChange={(e) => updateNoiseField(index, 'delay', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.applyTo')}>
|
||||
<Select
|
||||
value={noise.applyTo}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => updateNoiseField(index, 'applyTo', v)}
|
||||
options={['ip', 'ipv4', 'ipv6'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</Card>
|
||||
))}
|
||||
<Button type="dashed" block icon={<PlusOutlined />} onClick={addNoise}>
|
||||
{t('pages.settings.subFormats.addNoise')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -335,40 +343,30 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
label: t('pages.settings.mux'),
|
||||
label: catTabLabel(<PartitionOutlined />, t('pages.settings.mux'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.mux')} description={t('pages.settings.muxDesc')}>
|
||||
<Switch checked={muxEnabled} onChange={setMuxEnabled} />
|
||||
</SettingListItem>
|
||||
{muxEnabled && (
|
||||
<div className="nested-block">
|
||||
<Collapse items={[
|
||||
{
|
||||
key: 'sett',
|
||||
label: t('pages.settings.muxSett'),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.concurrency')}>
|
||||
<InputNumber value={muxObj.concurrency} min={-1} max={1024} style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('concurrency', Number(v) || 0)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpConcurrency')}>
|
||||
<InputNumber value={muxObj.xudpConcurrency} min={-1} max={1024} style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('xudpConcurrency', Number(v) || 0)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpUdp443')}>
|
||||
<Select
|
||||
value={muxObj.xudpProxyUDP443}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('xudpProxyUDP443', v)}
|
||||
options={['reject', 'allow', 'skip'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]} />
|
||||
<div className="format-settings">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.concurrency')}>
|
||||
<InputNumber value={muxObj.concurrency} min={-1} max={1024} style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('concurrency', Number(v) || 0)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpConcurrency')}>
|
||||
<InputNumber value={muxObj.xudpConcurrency} min={-1} max={1024} style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('xudpConcurrency', Number(v) || 0)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpUdp443')}>
|
||||
<Select
|
||||
value={muxObj.xudpProxyUDP443}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('xudpProxyUDP443', v)}
|
||||
options={['reject', 'allow', 'skip'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -376,42 +374,32 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
label: t('pages.settings.direct'),
|
||||
label: catTabLabel(<SendOutlined />, t('pages.settings.direct'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.direct')} description={t('pages.settings.directDesc')}>
|
||||
<Switch checked={directEnabled} onChange={setDirectEnabled} />
|
||||
</SettingListItem>
|
||||
{directEnabled && (
|
||||
<div className="nested-block">
|
||||
<Collapse items={[
|
||||
{
|
||||
key: 'rules',
|
||||
label: t('pages.settings.direct'),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={<>{t('pages.settings.direct')} IPs</>}>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={directIPs}
|
||||
style={{ width: '100%' }}
|
||||
onChange={setDirectIPs}
|
||||
options={directIPsOptions}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={<>{t('pages.settings.direct')} {t('domainName')}</>}>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={directDomains}
|
||||
style={{ width: '100%' }}
|
||||
onChange={setDirectDomains}
|
||||
options={directDomainsOptions}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]} />
|
||||
<div className="format-settings">
|
||||
<SettingListItem paddings="small" title={<>{t('pages.settings.direct')} IPs</>}>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={directIPs}
|
||||
style={{ width: '100%' }}
|
||||
onChange={setDirectIPs}
|
||||
options={directIPsOptions}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={<>{t('pages.settings.direct')} {t('domainName')}</>}>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={directDomains}
|
||||
style={{ width: '100%' }}
|
||||
onChange={setDirectDomains}
|
||||
options={directDomainsOptions}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user