✨ feat: Update server configuration translations for multiple languages
- Added new keys for server configuration actions and descriptions in Russian, Thai, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional). - Removed outdated configuration structure and replaced it with a more organized server_config object. - Enhanced the dynamic Inputs component to support textarea input type for better user experience.
This commit is contained in:
@@ -41,14 +41,10 @@ export default function DynamicMultiplier() {
|
||||
}, [periodsResp]);
|
||||
|
||||
async function savePeriods() {
|
||||
try {
|
||||
await setNodeMultiplier({ periods: timeSlots });
|
||||
await refetchPeriods();
|
||||
toast.success(t('config.saveSuccess'));
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(t('config.saveError'));
|
||||
}
|
||||
await setNodeMultiplier({ periods: timeSlots });
|
||||
await refetchPeriods();
|
||||
toast.success(t('server_config.saveSuccess'));
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -62,9 +58,9 @@ export default function DynamicMultiplier() {
|
||||
<Icon icon='mdi:clock-time-eight' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('config.dynamicMultiplier')}</p>
|
||||
<p className='font-medium'>{t('server_config.dynamic_multiplier')}</p>
|
||||
<p className='text-muted-foreground truncate text-sm'>
|
||||
{t('config.dynamicMultiplierDescription')}
|
||||
{t('server_config.dynamic_multiplier_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,18 +72,18 @@ export default function DynamicMultiplier() {
|
||||
|
||||
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('config.dynamicMultiplier')}</SheetTitle>
|
||||
<SheetDescription>{t('config.dynamicMultiplierDescription')}</SheetDescription>
|
||||
<SheetTitle>{t('server_config.dynamic_multiplier')}</SheetTitle>
|
||||
<SheetDescription>{t('server_config.dynamic_multiplier_desc')}</SheetDescription>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-60px-env(safe-area-inset-top))] px-6'>
|
||||
<div className='space-y-4 pt-4'>
|
||||
<ArrayInput<API.TimePeriod>
|
||||
fields={[
|
||||
{ name: 'start_time', prefix: t('config.startTime'), type: 'time' },
|
||||
{ name: 'end_time', prefix: t('config.endTime'), type: 'time' },
|
||||
{ name: 'start_time', prefix: t('server_config.fields.start_time'), type: 'time' },
|
||||
{ name: 'end_time', prefix: t('server_config.fields.end_time'), type: 'time' },
|
||||
{
|
||||
name: 'multiplier',
|
||||
prefix: t('config.multiplier'),
|
||||
prefix: t('server_config.fields.multiplier'),
|
||||
type: 'number',
|
||||
placeholder: '0',
|
||||
},
|
||||
@@ -100,13 +96,13 @@ export default function DynamicMultiplier() {
|
||||
|
||||
<SheetFooter className='flex-row justify-between pt-3'>
|
||||
<Button variant='outline' onClick={() => setTimeSlots(periodsResp || [])}>
|
||||
{t('config.reset')}
|
||||
{t('server_config.fields.reset')}
|
||||
</Button>
|
||||
<div className='flex gap-2'>
|
||||
<Button variant='outline' onClick={() => setOpen(false)}>
|
||||
{t('config.actions.cancel')}
|
||||
{t('actions.cancel')}
|
||||
</Button>
|
||||
<Button onClick={savePeriods}>{t('config.actions.save')}</Button>
|
||||
<Button onClick={savePeriods}>{t('actions.save')}</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
|
||||
@@ -60,6 +60,7 @@ export const TRANSPORTS = {
|
||||
} as const;
|
||||
|
||||
export const SECURITY = {
|
||||
shadowsocks: ['none', 'http', 'tls'] as const,
|
||||
vmess: ['none', 'tls'] as const,
|
||||
vless: ['none', 'tls', 'reality'] as const,
|
||||
trojan: ['tls'] as const,
|
||||
@@ -91,6 +92,8 @@ export const FINGERPRINTS = [
|
||||
'qq',
|
||||
] as const;
|
||||
|
||||
export const CERT_MODES = ['none', 'http', 'dns', 'self'] as const;
|
||||
|
||||
export const multiplexLevels = ['none', 'low', 'middle', 'high'] as const;
|
||||
|
||||
export function getLabel(value: string): string {
|
||||
|
||||
@@ -10,36 +10,85 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
port: null,
|
||||
cipher: 'chacha20-ietf-poly1305',
|
||||
server_key: null,
|
||||
obfs: 'none',
|
||||
obfs_host: null,
|
||||
obfs_path: null,
|
||||
security: 'none',
|
||||
host: null,
|
||||
path: null,
|
||||
sni: null,
|
||||
allow_insecure: null,
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'vmess':
|
||||
return {
|
||||
type: 'vmess',
|
||||
enable: false,
|
||||
host: null,
|
||||
port: null,
|
||||
transport: 'tcp',
|
||||
security: 'none',
|
||||
path: null,
|
||||
service_name: null,
|
||||
sni: null,
|
||||
allow_insecure: null,
|
||||
fingerprint: 'chrome',
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'vless':
|
||||
return {
|
||||
type: 'vless',
|
||||
enable: false,
|
||||
host: null,
|
||||
port: null,
|
||||
transport: 'tcp',
|
||||
security: 'none',
|
||||
flow: 'none',
|
||||
path: null,
|
||||
service_name: null,
|
||||
sni: null,
|
||||
allow_insecure: null,
|
||||
fingerprint: 'chrome',
|
||||
reality_server_addr: null,
|
||||
reality_server_port: null,
|
||||
reality_private_key: null,
|
||||
reality_public_key: null,
|
||||
reality_short_id: null,
|
||||
xhttp_mode: XHTTP_MODES[0], // 'auto'
|
||||
xhttp_extra: null,
|
||||
encryption: 'none',
|
||||
encryption_mode: null,
|
||||
encryption_rtt: null,
|
||||
encryption_ticket: null,
|
||||
encryption_server_padding: null,
|
||||
encryption_private_key: null,
|
||||
encryption_client_padding: null,
|
||||
encryption_password: null,
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'trojan':
|
||||
return {
|
||||
type: 'trojan',
|
||||
enable: false,
|
||||
host: null,
|
||||
port: null,
|
||||
transport: 'tcp',
|
||||
security: 'tls',
|
||||
path: null,
|
||||
service_name: null,
|
||||
sni: null,
|
||||
allow_insecure: null,
|
||||
fingerprint: 'chrome',
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'hysteria2':
|
||||
return {
|
||||
@@ -53,6 +102,13 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
security: 'tls',
|
||||
up_mbps: null,
|
||||
down_mbps: null,
|
||||
sni: null,
|
||||
allow_insecure: null,
|
||||
fingerprint: 'chrome',
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'tuic':
|
||||
return {
|
||||
@@ -67,12 +123,17 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
sni: null,
|
||||
allow_insecure: false,
|
||||
fingerprint: 'chrome',
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'socks':
|
||||
return {
|
||||
type: 'socks',
|
||||
enable: false,
|
||||
port: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'naive':
|
||||
return {
|
||||
@@ -80,6 +141,13 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
enable: false,
|
||||
port: null,
|
||||
security: 'none',
|
||||
sni: null,
|
||||
allow_insecure: null,
|
||||
fingerprint: 'chrome',
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'http':
|
||||
return {
|
||||
@@ -87,6 +155,13 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
enable: false,
|
||||
port: null,
|
||||
security: 'none',
|
||||
sni: null,
|
||||
allow_insecure: null,
|
||||
fingerprint: 'chrome',
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
case 'mieru':
|
||||
return {
|
||||
@@ -106,6 +181,10 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
sni: null,
|
||||
allow_insecure: false,
|
||||
fingerprint: 'chrome',
|
||||
cert_mode: 'none',
|
||||
cert_dns_provider: null,
|
||||
cert_dns_env: null,
|
||||
ratio: 1,
|
||||
} as any;
|
||||
default:
|
||||
return {} as any;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
generateRealityShortId,
|
||||
} from '../generate';
|
||||
import {
|
||||
CERT_MODES,
|
||||
ENCRYPTION_MODES,
|
||||
ENCRYPTION_RTT,
|
||||
ENCRYPTION_TYPES,
|
||||
@@ -22,11 +23,20 @@ import type { FieldConfig } from './types';
|
||||
|
||||
export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
shadowsocks: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -55,36 +65,85 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
].includes(p.cipher),
|
||||
},
|
||||
{
|
||||
name: 'obfs',
|
||||
name: 'security',
|
||||
type: 'select',
|
||||
label: 'obfs',
|
||||
options: ['none', 'http', 'tls'],
|
||||
label: 'security',
|
||||
options: SECURITY.shadowsocks,
|
||||
defaultValue: 'none',
|
||||
group: 'obfs',
|
||||
group: 'security',
|
||||
},
|
||||
{
|
||||
name: 'obfs_host',
|
||||
name: 'host',
|
||||
type: 'input',
|
||||
label: 'obfs_host',
|
||||
label: 'host',
|
||||
placeholder: 'e.g. www.bing.com',
|
||||
group: 'obfs',
|
||||
condition: (p) => p.obfs && p.obfs !== 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security && p.security !== 'none',
|
||||
},
|
||||
{
|
||||
name: 'obfs_path',
|
||||
name: 'path',
|
||||
type: 'input',
|
||||
label: 'obfs_path',
|
||||
label: 'path',
|
||||
placeholder: 'e.g. /path/to/obfs',
|
||||
group: 'obfs',
|
||||
condition: (p) => p.obfs && p.obfs !== 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security && p.security !== 'none',
|
||||
},
|
||||
{
|
||||
name: 'sni',
|
||||
type: 'input',
|
||||
label: 'security_sni',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'allow_insecure',
|
||||
type: 'switch',
|
||||
label: 'security_allow_insecure',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
vmess: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -151,13 +210,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
group: 'security',
|
||||
condition: (p) => p.security !== 'none',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
vless: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -374,13 +468,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
group: 'encryption',
|
||||
condition: (p) => p.encryption === 'mlkem768x25519plus',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
trojan: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -447,13 +576,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
group: 'security',
|
||||
condition: (p) => p.security !== 'none',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
hysteria2: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -470,7 +634,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
type: 'number',
|
||||
label: 'hop_interval',
|
||||
placeholder: 'e.g. 300',
|
||||
min: 0,
|
||||
min: 1,
|
||||
suffix: 'S',
|
||||
group: 'basic',
|
||||
},
|
||||
@@ -497,7 +661,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
name: 'up_mbps',
|
||||
type: 'number',
|
||||
label: 'up_mbps',
|
||||
min: 0,
|
||||
min: 1,
|
||||
placeholder: (t) => t('bandwidth_placeholder'),
|
||||
suffix: 'Mbps',
|
||||
group: 'basic',
|
||||
@@ -506,7 +670,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
name: 'down_mbps',
|
||||
type: 'number',
|
||||
label: 'down_mbps',
|
||||
min: 0,
|
||||
min: 1,
|
||||
placeholder: (t) => t('bandwidth_placeholder'),
|
||||
suffix: 'Mbps',
|
||||
group: 'basic',
|
||||
@@ -521,13 +685,47 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
defaultValue: 'chrome',
|
||||
group: 'security',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
tuic: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -560,24 +758,67 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
defaultValue: 'chrome',
|
||||
group: 'security',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
socks: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
},
|
||||
],
|
||||
naive: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -613,13 +854,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
group: 'security',
|
||||
condition: (p) => p.security !== 'none',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
http: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -655,13 +931,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
group: 'security',
|
||||
condition: (p) => p.security !== 'none',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
mieru: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -684,11 +995,20 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
},
|
||||
],
|
||||
anytls: [
|
||||
{
|
||||
name: 'ratio',
|
||||
type: 'number',
|
||||
label: 'traffic_ratio',
|
||||
min: 1,
|
||||
step: 0.01,
|
||||
defaultValue: 1,
|
||||
group: 'basic',
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
label: 'port',
|
||||
min: 0,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '1-65535',
|
||||
group: 'basic',
|
||||
@@ -710,5 +1030,30 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
|
||||
defaultValue: 'chrome',
|
||||
group: 'security',
|
||||
},
|
||||
{
|
||||
name: 'cert_mode',
|
||||
type: 'select',
|
||||
label: 'cert_mode',
|
||||
options: CERT_MODES,
|
||||
defaultValue: 'none',
|
||||
group: 'security',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_provider',
|
||||
type: 'input',
|
||||
label: 'cert_dns_provider',
|
||||
placeholder: 'e.g. cloudflare, aliyun',
|
||||
group: 'security',
|
||||
condition: (p) => p.cert_mode === 'dns',
|
||||
},
|
||||
{
|
||||
name: 'cert_dns_env',
|
||||
type: 'textarea',
|
||||
label: 'cert_dns_env',
|
||||
placeholder:
|
||||
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
|
||||
group: 'security',
|
||||
condition: (p) => p.cert_mode === 'dns',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
CERT_MODES,
|
||||
ENCRYPTION_MODES,
|
||||
ENCRYPTION_RTT,
|
||||
ENCRYPTION_TYPES,
|
||||
@@ -16,20 +17,25 @@ import {
|
||||
const nullableString = z.string().nullish();
|
||||
const nullableBool = z.boolean().nullish();
|
||||
const nullablePort = z.number().int().min(0).max(65535).nullish();
|
||||
const nullableRatio = z.number().min(1).nullish();
|
||||
|
||||
const ss = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('shadowsocks'),
|
||||
enable: nullableBool,
|
||||
host: nullableString,
|
||||
port: nullablePort,
|
||||
cipher: z.enum(SS_CIPHERS).nullish(),
|
||||
server_key: nullableString,
|
||||
obfs: z.enum(['none', 'http', 'tls'] as const).nullish(),
|
||||
obfs_host: nullableString,
|
||||
obfs_path: nullableString,
|
||||
security: z.enum(SECURITY.shadowsocks).nullish(),
|
||||
host: nullableString,
|
||||
path: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const vmess = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('vmess'),
|
||||
enable: nullableBool,
|
||||
host: nullableString,
|
||||
@@ -41,9 +47,13 @@ const vmess = z.object({
|
||||
sni: nullableString,
|
||||
allow_insecure: nullableBool,
|
||||
fingerprint: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const vless = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('vless'),
|
||||
enable: nullableBool,
|
||||
host: nullableString,
|
||||
@@ -71,9 +81,13 @@ const vless = z.object({
|
||||
encryption_private_key: nullableString,
|
||||
encryption_client_padding: nullableString,
|
||||
encryption_password: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const trojan = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('trojan'),
|
||||
enable: nullableBool,
|
||||
host: nullableString,
|
||||
@@ -85,9 +99,13 @@ const trojan = z.object({
|
||||
sni: nullableString,
|
||||
allow_insecure: nullableBool,
|
||||
fingerprint: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const hysteria2 = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('hysteria2'),
|
||||
enable: nullableBool,
|
||||
hop_ports: nullableString,
|
||||
@@ -101,9 +119,13 @@ const hysteria2 = z.object({
|
||||
fingerprint: nullableString,
|
||||
up_mbps: z.number().nullish(),
|
||||
down_mbps: z.number().nullish(),
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const tuic = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('tuic'),
|
||||
enable: nullableBool,
|
||||
host: nullableString,
|
||||
@@ -116,9 +138,13 @@ const tuic = z.object({
|
||||
sni: nullableString,
|
||||
allow_insecure: nullableBool,
|
||||
fingerprint: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const anytls = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('anytls'),
|
||||
enable: nullableBool,
|
||||
port: nullablePort,
|
||||
@@ -127,15 +153,20 @@ const anytls = z.object({
|
||||
allow_insecure: nullableBool,
|
||||
fingerprint: nullableString,
|
||||
padding_scheme: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const socks = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('socks'),
|
||||
enable: nullableBool,
|
||||
port: nullablePort,
|
||||
});
|
||||
|
||||
const naive = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('naive'),
|
||||
enable: nullableBool,
|
||||
port: nullablePort,
|
||||
@@ -143,9 +174,13 @@ const naive = z.object({
|
||||
sni: nullableString,
|
||||
allow_insecure: nullableBool,
|
||||
fingerprint: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const http = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('http'),
|
||||
enable: nullableBool,
|
||||
port: nullablePort,
|
||||
@@ -153,9 +188,13 @@ const http = z.object({
|
||||
sni: nullableString,
|
||||
allow_insecure: nullableBool,
|
||||
fingerprint: nullableString,
|
||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
||||
cert_dns_provider: nullableString,
|
||||
cert_dns_env: nullableString,
|
||||
});
|
||||
|
||||
const mieru = z.object({
|
||||
ratio: nullableRatio,
|
||||
type: z.literal('mieru'),
|
||||
enable: nullableBool,
|
||||
port: nullablePort,
|
||||
@@ -182,6 +221,5 @@ export const formSchema = z.object({
|
||||
address: z.string().min(1),
|
||||
country: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
ratio: z.number().default(1),
|
||||
protocols: z.array(protocolApiScheme),
|
||||
});
|
||||
|
||||
@@ -15,6 +15,13 @@ import {
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/ui/components/select';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -23,6 +30,9 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { Textarea } from '@workspace/ui/components/textarea';
|
||||
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { DicesIcon } from 'lucide-react';
|
||||
@@ -33,10 +43,46 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
const dnsConfigSchema = z.object({
|
||||
proto: z.enum(['tcp', 'udp', 'tls', 'https', 'quic']),
|
||||
address: z.string(),
|
||||
domains: z.array(z.string()),
|
||||
});
|
||||
|
||||
const outboundConfigSchema = z.object({
|
||||
name: z.string(),
|
||||
protocol: z.enum([
|
||||
'http',
|
||||
'https',
|
||||
'socks5',
|
||||
'shadowsocks',
|
||||
'vmess',
|
||||
'vless',
|
||||
'trojan',
|
||||
'hysteria2',
|
||||
'tuic',
|
||||
'naive',
|
||||
'brook',
|
||||
'snell',
|
||||
'wireguard',
|
||||
'direct',
|
||||
'reject',
|
||||
]),
|
||||
address: z.string(),
|
||||
port: z.number(),
|
||||
password: z.string().optional(),
|
||||
rules: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const nodeConfigSchema = z.object({
|
||||
node_secret: z.string().optional(),
|
||||
node_pull_interval: z.number().optional(),
|
||||
node_push_interval: z.number().optional(),
|
||||
traffic_report_threshold: z.number().optional(),
|
||||
ip_strategy: z.enum(['prefer_ipv4', 'prefer_ipv6']).optional(),
|
||||
dns: z.array(dnsConfigSchema).optional(),
|
||||
block: z.array(z.string()).optional(),
|
||||
outbound: z.array(outboundConfigSchema).optional(),
|
||||
});
|
||||
type NodeConfigFormData = z.infer<typeof nodeConfigSchema>;
|
||||
|
||||
@@ -60,6 +106,11 @@ export default function ServerConfig() {
|
||||
node_secret: '',
|
||||
node_pull_interval: undefined,
|
||||
node_push_interval: undefined,
|
||||
traffic_report_threshold: undefined,
|
||||
ip_strategy: undefined,
|
||||
dns: [],
|
||||
block: [],
|
||||
outbound: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,6 +120,11 @@ export default function ServerConfig() {
|
||||
node_secret: cfgResp.node_secret ?? '',
|
||||
node_pull_interval: cfgResp.node_pull_interval as number | undefined,
|
||||
node_push_interval: cfgResp.node_push_interval as number | undefined,
|
||||
traffic_report_threshold: (cfgResp as any).traffic_report_threshold as number | undefined,
|
||||
ip_strategy: (cfgResp as any).ip_strategy as 'prefer_ipv4' | 'prefer_ipv6' | undefined,
|
||||
dns: (cfgResp as any).dns || [],
|
||||
block: (cfgResp as any).block || [],
|
||||
outbound: (cfgResp as any).outbound || [],
|
||||
});
|
||||
}
|
||||
}, [cfgResp, form]);
|
||||
@@ -77,7 +133,7 @@ export default function ServerConfig() {
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateNodeConfig(values as API.NodeConfig);
|
||||
toast.success(t('config.saveSuccess'));
|
||||
toast.success(t('server_config.saveSuccess'));
|
||||
await refetchCfg();
|
||||
setOpen(false);
|
||||
} finally {
|
||||
@@ -96,9 +152,9 @@ export default function ServerConfig() {
|
||||
<Icon icon='mdi:resistor-nodes' className='text-primary h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium'>{t('config.title')}</p>
|
||||
<p className='font-medium'>{t('server_config.title')}</p>
|
||||
<p className='text-muted-foreground truncate text-sm'>
|
||||
{t('config.description')}
|
||||
{t('server_config.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,102 +166,348 @@ export default function ServerConfig() {
|
||||
|
||||
<SheetContent className='w-[720px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>节点配置</SheetTitle>
|
||||
<SheetTitle>{t('server_config.title')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='server-config-form'
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-4 pt-4'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='node_secret'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('config.communicationKey')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('config.inputPlaceholder')}
|
||||
value={field.value || ''}
|
||||
onValueChange={field.onChange}
|
||||
suffix={
|
||||
<div className='bg-muted flex h-9 items-center px-3'>
|
||||
<DicesIcon
|
||||
onClick={() => {
|
||||
const id = uid(32).toLowerCase();
|
||||
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
||||
form.setValue('node_secret', formatted);
|
||||
}}
|
||||
className='cursor-pointer'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('config.communicationKeyDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Tabs defaultValue='basic' className='pt-4'>
|
||||
<TabsList className='grid w-full grid-cols-4'>
|
||||
<TabsTrigger value='basic'>{t('server_config.tabs.basic')}</TabsTrigger>
|
||||
<TabsTrigger value='dns'>{t('server_config.tabs.dns')}</TabsTrigger>
|
||||
<TabsTrigger value='outbound'>{t('server_config.tabs.outbound')}</TabsTrigger>
|
||||
<TabsTrigger value='block'>{t('server_config.tabs.block')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='node_pull_interval'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('config.nodePullInterval')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
suffix='S'
|
||||
value={field.value as any}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('config.inputPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('config.nodePullIntervalDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Form {...form}>
|
||||
<form id='server-config-form' onSubmit={form.handleSubmit(onSubmit)} className='mt-4'>
|
||||
<TabsContent value='basic' className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='node_secret'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('server_config.fields.communication_key')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('server_config.fields.communication_key_placeholder')}
|
||||
value={field.value || ''}
|
||||
onValueChange={field.onChange}
|
||||
suffix={
|
||||
<div className='bg-muted flex h-9 items-center px-3'>
|
||||
<DicesIcon
|
||||
onClick={() => {
|
||||
const id = uid(32).toLowerCase();
|
||||
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
||||
form.setValue('node_secret', formatted);
|
||||
}}
|
||||
className='cursor-pointer'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('server_config.fields.communication_key_desc')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='node_push_interval'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('config.nodePushInterval')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
suffix='S'
|
||||
step={0.1}
|
||||
value={field.value as any}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('config.inputPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('config.nodePushIntervalDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='node_pull_interval'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('server_config.fields.node_pull_interval')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
suffix='S'
|
||||
value={field.value as any}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('server_config.fields.communication_key_placeholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('server_config.fields.node_pull_interval_desc')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='node_push_interval'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('server_config.fields.node_push_interval')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
suffix='S'
|
||||
step={0.1}
|
||||
value={field.value as any}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('server_config.fields.communication_key_placeholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('server_config.fields.node_push_interval_desc')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='traffic_report_threshold'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('server_config.fields.traffic_report_threshold')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
suffix='MB'
|
||||
value={field.value as any}
|
||||
onValueChange={field.onChange}
|
||||
placeholder='1'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('server_config.fields.traffic_report_threshold_desc')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='dns' className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='ip_strategy'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('server_config.fields.ip_strategy')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t('server_config.fields.ip_strategy_placeholder')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value='prefer_ipv4'>
|
||||
{t('server_config.fields.ip_strategy_ipv4')}
|
||||
</SelectItem>
|
||||
<SelectItem value='prefer_ipv6'>
|
||||
{t('server_config.fields.ip_strategy_ipv6')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t('server_config.fields.ip_strategy_desc')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='dns'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('server_config.fields.dns_config')}</FormLabel>
|
||||
<FormControl>
|
||||
<ArrayInput
|
||||
className='grid grid-cols-2 gap-2'
|
||||
fields={[
|
||||
{
|
||||
name: 'proto',
|
||||
type: 'select',
|
||||
placeholder: t('server_config.fields.dns_proto_placeholder'),
|
||||
options: [
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
{ label: 'TLS', value: 'tls' },
|
||||
{ label: 'HTTPS', value: 'https' },
|
||||
{ label: 'QUIC', value: 'quic' },
|
||||
],
|
||||
},
|
||||
{ name: 'address', type: 'text', placeholder: '8.8.8.8:53' },
|
||||
{
|
||||
name: 'domains',
|
||||
type: 'textarea',
|
||||
className: 'col-span-2',
|
||||
placeholder: t('server_config.fields.dns_domains_placeholder'),
|
||||
},
|
||||
]}
|
||||
value={(field.value || []).map((item) => ({
|
||||
...item,
|
||||
domains: Array.isArray(item.domains) ? item.domains.join('\n') : '',
|
||||
}))}
|
||||
onChange={(values) => {
|
||||
// 转换 domains 字符串为数组
|
||||
const converted = values.map((item: any) => ({
|
||||
proto: item.proto,
|
||||
address: item.address,
|
||||
domains:
|
||||
typeof item.domains === 'string'
|
||||
? item.domains
|
||||
.split('\n')
|
||||
.map((d: string) => d.trim())
|
||||
.filter(Boolean)
|
||||
: item.domains || [],
|
||||
}));
|
||||
field.onChange(converted);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='outbound' className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='outbound'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<ArrayInput
|
||||
className='grid grid-cols-2 gap-2'
|
||||
fields={[
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
className: 'col-span-2',
|
||||
placeholder: t('server_config.fields.outbound_name_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
type: 'select',
|
||||
placeholder: t(
|
||||
'server_config.fields.outbound_protocol_placeholder',
|
||||
),
|
||||
options: [
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'HTTPS', value: 'https' },
|
||||
{ label: 'SOCKS5', value: 'socks5' },
|
||||
{ label: 'Shadowsocks', value: 'shadowsocks' },
|
||||
{ label: 'VMess', value: 'vmess' },
|
||||
{ label: 'VLESS', value: 'vless' },
|
||||
{ label: 'Trojan', value: 'trojan' },
|
||||
{ label: 'Hysteria2', value: 'hysteria2' },
|
||||
{ label: 'TUIC', value: 'tuic' },
|
||||
{ label: 'NaiveProxy', value: 'naive' },
|
||||
{ label: 'Brook', value: 'brook' },
|
||||
{ label: 'Snell', value: 'snell' },
|
||||
{ label: 'WireGuard', value: 'wireguard' },
|
||||
{ label: 'Direct', value: 'direct' },
|
||||
{ label: 'Reject', value: 'reject' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
type: 'text',
|
||||
placeholder: t('server_config.fields.outbound_address_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
placeholder: t('server_config.fields.outbound_port_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
placeholder: t(
|
||||
'server_config.fields.outbound_password_placeholder',
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'rules',
|
||||
type: 'textarea',
|
||||
className: 'col-span-2',
|
||||
placeholder: t('server_config.fields.outbound_rules_placeholder'),
|
||||
},
|
||||
]}
|
||||
value={(field.value || []).map((item) => ({
|
||||
...item,
|
||||
rules: Array.isArray(item.rules) ? item.rules.join('\n') : '',
|
||||
}))}
|
||||
onChange={(values) => {
|
||||
// 转换 rules 字符串为数组
|
||||
const converted = values.map((item: any) => ({
|
||||
name: item.name,
|
||||
protocol: item.protocol,
|
||||
address: item.address,
|
||||
port: item.port,
|
||||
password: item.password,
|
||||
rules:
|
||||
typeof item.rules === 'string'
|
||||
? item.rules
|
||||
.split('\n')
|
||||
.map((r: string) => r.trim())
|
||||
.filter(Boolean)
|
||||
: item.rules || [],
|
||||
}));
|
||||
field.onChange(converted);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='block' className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='block'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t('server_config.fields.block_rules_placeholder')}
|
||||
value={(field.value || []).join('\n')}
|
||||
onChange={(e) => {
|
||||
const lines = e.target.value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
field.onChange(lines);
|
||||
}}
|
||||
rows={10}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
</form>
|
||||
</Form>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button variant='outline' disabled={saving} onClick={() => setOpen(false)}>
|
||||
{t('config.actions.cancel')}
|
||||
{t('actions.cancel')}
|
||||
</Button>
|
||||
<Button disabled={saving} type='submit' form='server-config-form'>
|
||||
<Icon icon='mdi:loading' className={saving ? 'mr-2 animate-spin' : 'hidden'} />
|
||||
{t('config.actions.save')}
|
||||
{t('actions.save')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
|
||||
@@ -338,7 +338,6 @@ export default function ServerForm(props: {
|
||||
address: '',
|
||||
country: '',
|
||||
city: '',
|
||||
ratio: 1,
|
||||
protocols: [] as any[],
|
||||
...initialValues,
|
||||
},
|
||||
@@ -375,7 +374,6 @@ export default function ServerForm(props: {
|
||||
name: values.name,
|
||||
country: values.country,
|
||||
city: values.city,
|
||||
ratio: values.ratio || 1,
|
||||
address: values.address,
|
||||
protocols: filteredProtocols,
|
||||
};
|
||||
@@ -399,7 +397,6 @@ export default function ServerForm(props: {
|
||||
address: '',
|
||||
country: '',
|
||||
city: '',
|
||||
ratio: 1,
|
||||
protocols: full,
|
||||
});
|
||||
}
|
||||
@@ -416,7 +413,7 @@ export default function ServerForm(props: {
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||
<Form {...form}>
|
||||
<form className='grid grid-cols-1 gap-2 px-6 pt-4'>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<div className='grid grid-cols-2 gap-2 md:grid-cols-4'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='name'
|
||||
@@ -430,6 +427,23 @@ export default function ServerForm(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name='address'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('address')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
{...field}
|
||||
placeholder={t('address_placeholder')}
|
||||
onValueChange={(v) => field.onChange(v)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name='country'
|
||||
@@ -457,44 +471,6 @@ export default function ServerForm(props: {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='address'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('address')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
{...field}
|
||||
placeholder={t('address_placeholder')}
|
||||
onValueChange={(v) => field.onChange(v)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name='ratio'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('traffic_ratio')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
{...field}
|
||||
type='number'
|
||||
step={0.1}
|
||||
min={0}
|
||||
onValueChange={(v) => field.onChange(v)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='my-3'>
|
||||
<h3 className='text-foreground text-sm font-semibold'>
|
||||
{t('protocol_configurations')}
|
||||
|
||||
Reference in New Issue
Block a user