refactor(frontend): split outbound protocol forms into per-protocol files

Replace the grouped outbound-core-fields / outbound-wireguard-fields with one file per protocol under outbounds/protocols/: vmess, vless, trojan, shadowsocks, http, socks, wireguard, freedom, blackhole, dns, loopback (+ shared server-target). Matches the prompt's 1-file-per-protocol structure (per-modal). Outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
This commit is contained in:
MHSanaei 2026-05-30 17:46:42 +02:00
parent 2be473aea3
commit 47a2eb7efe
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
11 changed files with 208 additions and 131 deletions

View file

@ -53,6 +53,7 @@ import {
MODE_OPTIONS,
NETWORK_OPTIONS,
PROTOCOL_OPTIONS,
SERVER_PROTOCOLS,
UTLS_OPTIONS,
} from './outbound-form-constants';
import {
@ -61,9 +62,20 @@ import {
isMuxAllowed,
newStreamSlice,
} from './outbound-form-helpers';
import { OutboundCoreProtocolFields } from './outbound-core-fields';
import { WireguardOutboundFields } from './outbound-wireguard-fields';
import { BlackholeFields, DnsFields, FreedomFields, LoopbackFields } from './protocols';
import {
BlackholeFields,
DnsFields,
FreedomFields,
HttpFields,
LoopbackFields,
ServerTarget,
ShadowsocksFields,
SocksFields,
TrojanFields,
VlessFields,
VmessFields,
WireguardFields,
} from './protocols';
import './OutboundFormModal.css';
// Pattern A rewrite of OutboundFormModal. Built as a sibling `.new.tsx`
@ -388,7 +400,13 @@ export default function OutboundFormModal({
<Input placeholder={t('pages.xray.outboundForm.localIpPlaceholder')} />
</Form.Item>
<OutboundCoreProtocolFields protocol={protocol} />
{SERVER_PROTOCOLS.has(protocol) && <ServerTarget />}
{protocol === 'vmess' && <VmessFields />}
{protocol === 'vless' && <VlessFields />}
{protocol === 'trojan' && <TrojanFields />}
{protocol === 'shadowsocks' && <ShadowsocksFields />}
{protocol === 'http' && <HttpFields />}
{protocol === 'socks' && <SocksFields />}
{protocol === 'loopback' && <LoopbackFields />}
{protocol === 'blackhole' && <BlackholeFields />}
@ -470,7 +488,7 @@ export default function OutboundFormModal({
</Form.Item>
)}
{protocol === 'wireguard' && <WireguardOutboundFields form={form} />}
{protocol === 'wireguard' && <WireguardFields form={form} />}
{streamAllowed && network && (
<>

View file

@ -1,125 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Form, Input, InputNumber, Select, Switch } from 'antd';
import {
ShadowsocksOutboundFormSettingsSchema,
TrojanOutboundFormSettingsSchema,
VlessOutboundFormSettingsSchema,
VmessOutboundFormSettingsSchema,
} from '@/schemas/forms/outbound-form';
import { SSMethodSchema } from '@/schemas/protocols/shared/shadowsocks';
import { antdRule } from '@/utils/zodForm';
import { SECURITY_OPTIONS, SERVER_PROTOCOLS, SS_METHOD_OPTIONS } from './outbound-form-constants';
export function OutboundCoreProtocolFields({ protocol }: { protocol: string }) {
const { t } = useTranslation();
return (
<>
{/* Shared connect target (address + port) for protocols
whose form schema carries them flat at settings root.
Hidden for freedom/blackhole/dns/loopback/wireguard. */}
{SERVER_PROTOCOLS.has(protocol) && (
<>
<Form.Item
label={t('pages.inbounds.address')}
name={['settings', 'address']}
rules={[{ required: true, message: t('pages.xray.outboundForm.addressRequired') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.port')}
name={['settings', 'port']}
rules={[{ required: true, message: t('pages.xray.outboundForm.portRequired') }]}
>
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
</Form.Item>
</>
)}
{(protocol === 'vmess' || protocol === 'vless') && (
<Form.Item
label="ID"
name={['settings', 'id']}
rules={[antdRule(VmessOutboundFormSettingsSchema.shape.id, t)]}
>
<Input placeholder="UUID" />
</Form.Item>
)}
{protocol === 'vmess' && (
<Form.Item
label={t('security')}
name={['settings', 'security']}
rules={[antdRule(VmessOutboundFormSettingsSchema.shape.security, t)]}
>
<Select options={SECURITY_OPTIONS} />
</Form.Item>
)}
{protocol === 'vless' && (
<>
<Form.Item
label={t('encryption')}
name={['settings', 'encryption']}
rules={[antdRule(VlessOutboundFormSettingsSchema.shape.encryption, t)]}
>
<Input />
</Form.Item>
<Form.Item label={t('pages.clients.reverseTag')} name={['settings', 'reverseTag']}>
<Input placeholder={t('pages.xray.outboundForm.optional')} />
</Form.Item>
</>
)}
{(protocol === 'trojan' || protocol === 'shadowsocks') && (
<Form.Item
label={t('password')}
name={['settings', 'password']}
rules={[
antdRule(
protocol === 'trojan'
? TrojanOutboundFormSettingsSchema.shape.password
: ShadowsocksOutboundFormSettingsSchema.shape.password,
t,
),
]}
>
<Input />
</Form.Item>
)}
{protocol === 'shadowsocks' && (
<>
<Form.Item
label={t('encryption')}
name={['settings', 'method']}
rules={[antdRule(SSMethodSchema, t)]}
>
<Select options={SS_METHOD_OPTIONS} />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.udpOverTcp')}
name={['settings', 'uot']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item label={t('pages.xray.outboundForm.uotVersion')} name={['settings', 'UoTVersion']}>
<InputNumber min={1} max={2} />
</Form.Item>
</>
)}
{(protocol === 'socks' || protocol === 'http') && (
<>
<Form.Item label={t('username')} name={['settings', 'user']}>
<Input />
</Form.Item>
<Form.Item label={t('password')} name={['settings', 'pass']}>
<Input />
</Form.Item>
</>
)}
</>
);
}

View file

@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
export default function HttpFields() {
const { t } = useTranslation();
return (
<>
<Form.Item label={t('username')} name={['settings', 'user']}>
<Input />
</Form.Item>
<Form.Item label={t('password')} name={['settings', 'pass']}>
<Input />
</Form.Item>
</>
);
}

View file

@ -1,3 +1,11 @@
export { default as ServerTarget } from './server-target';
export { default as VmessFields } from './vmess';
export { default as VlessFields } from './vless';
export { default as TrojanFields } from './trojan';
export { default as ShadowsocksFields } from './shadowsocks';
export { default as HttpFields } from './http';
export { default as SocksFields } from './socks';
export { default as WireguardFields } from './wireguard';
export { default as FreedomFields } from './freedom';
export { default as LoopbackFields } from './loopback';
export { default as BlackholeFields } from './blackhole';

View file

@ -0,0 +1,24 @@
import { useTranslation } from 'react-i18next';
import { Form, Input, InputNumber } from 'antd';
export default function ServerTarget() {
const { t } = useTranslation();
return (
<>
<Form.Item
label={t('pages.inbounds.address')}
name={['settings', 'address']}
rules={[{ required: true, message: t('pages.xray.outboundForm.addressRequired') }]}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.port')}
name={['settings', 'port']}
rules={[{ required: true, message: t('pages.xray.outboundForm.portRequired') }]}
>
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
</Form.Item>
</>
);
}

View file

@ -0,0 +1,40 @@
import { useTranslation } from 'react-i18next';
import { Form, Input, InputNumber, Select, Switch } from 'antd';
import { ShadowsocksOutboundFormSettingsSchema } from '@/schemas/forms/outbound-form';
import { SSMethodSchema } from '@/schemas/protocols/shared/shadowsocks';
import { antdRule } from '@/utils/zodForm';
import { SS_METHOD_OPTIONS } from '../outbound-form-constants';
export default function ShadowsocksFields() {
const { t } = useTranslation();
return (
<>
<Form.Item
label={t('password')}
name={['settings', 'password']}
rules={[antdRule(ShadowsocksOutboundFormSettingsSchema.shape.password, t)]}
>
<Input />
</Form.Item>
<Form.Item
label={t('encryption')}
name={['settings', 'method']}
rules={[antdRule(SSMethodSchema, t)]}
>
<Select options={SS_METHOD_OPTIONS} />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.udpOverTcp')}
name={['settings', 'uot']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item label={t('pages.xray.outboundForm.uotVersion')} name={['settings', 'UoTVersion']}>
<InputNumber min={1} max={2} />
</Form.Item>
</>
);
}

View file

@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
export default function SocksFields() {
const { t } = useTranslation();
return (
<>
<Form.Item label={t('username')} name={['settings', 'user']}>
<Input />
</Form.Item>
<Form.Item label={t('password')} name={['settings', 'pass']}>
<Input />
</Form.Item>
</>
);
}

View file

@ -0,0 +1,18 @@
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
import { TrojanOutboundFormSettingsSchema } from '@/schemas/forms/outbound-form';
import { antdRule } from '@/utils/zodForm';
export default function TrojanFields() {
const { t } = useTranslation();
return (
<Form.Item
label={t('password')}
name={['settings', 'password']}
rules={[antdRule(TrojanOutboundFormSettingsSchema.shape.password, t)]}
>
<Input />
</Form.Item>
);
}

View file

@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next';
import { Form, Input } from 'antd';
import {
VlessOutboundFormSettingsSchema,
VmessOutboundFormSettingsSchema,
} from '@/schemas/forms/outbound-form';
import { antdRule } from '@/utils/zodForm';
export default function VlessFields() {
const { t } = useTranslation();
return (
<>
<Form.Item
label="ID"
name={['settings', 'id']}
rules={[antdRule(VmessOutboundFormSettingsSchema.shape.id, t)]}
>
<Input placeholder="UUID" />
</Form.Item>
<Form.Item
label={t('encryption')}
name={['settings', 'encryption']}
rules={[antdRule(VlessOutboundFormSettingsSchema.shape.encryption, t)]}
>
<Input />
</Form.Item>
<Form.Item label={t('pages.clients.reverseTag')} name={['settings', 'reverseTag']}>
<Input placeholder={t('pages.xray.outboundForm.optional')} />
</Form.Item>
</>
);
}

View file

@ -0,0 +1,29 @@
import { useTranslation } from 'react-i18next';
import { Form, Input, Select } from 'antd';
import { VmessOutboundFormSettingsSchema } from '@/schemas/forms/outbound-form';
import { antdRule } from '@/utils/zodForm';
import { SECURITY_OPTIONS } from '../outbound-form-constants';
export default function VmessFields() {
const { t } = useTranslation();
return (
<>
<Form.Item
label="ID"
name={['settings', 'id']}
rules={[antdRule(VmessOutboundFormSettingsSchema.shape.id, t)]}
>
<Input placeholder="UUID" />
</Form.Item>
<Form.Item
label={t('security')}
name={['settings', 'security']}
rules={[antdRule(VmessOutboundFormSettingsSchema.shape.security, t)]}
>
<Select options={SECURITY_OPTIONS} />
</Form.Item>
</>
);
}

View file

@ -7,7 +7,7 @@ import { InputAddon } from '@/components/ui';
import { WireguardDomainStrategy } from '@/schemas/primitives';
import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
export function WireguardOutboundFields({ form }: { form: FormInstance<OutboundFormValues> }) {
export default function WireguardFields({ form }: { form: FormInstance<OutboundFormValues> }) {
const { t } = useTranslation();
return (
<>