'use client'; import { getNodeGroupList } from '@/services/admin/server'; import { Icon } from '@iconify/react'; import { Combobox } from '@repo/ui/combobox'; import { JSONEditor } from '@repo/ui/editor'; import { EnhancedInput } from '@repo/ui/enhanced-input'; import { unitConversion } from '@repo/ui/utils'; import { Button } from '@shadcn/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form'; import { useForm } from '@shadcn/ui/lib/react-hook-form'; import { z, zodResolver } from '@shadcn/ui/lib/zod'; import { ScrollArea } from '@shadcn/ui/scroll-area'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@shadcn/ui/select'; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from '@shadcn/ui/sheet'; import { Switch } from '@shadcn/ui/switch'; import { Tabs, TabsList, TabsTrigger } from '@shadcn/ui/tabs'; import { useQuery } from '@tanstack/react-query'; import { useTranslations } from 'next-intl'; import { useEffect, useState } from 'react'; const shadowsocksSchema = z.object({ method: z.string(), port: z.number(), enable_relay: z.boolean().nullish(), relay_host: z.string().nullish(), relay_port: z.number().nullish(), }); const vmessSchema = z.object({ host: z.string(), port: z.number(), enable_tls: z.boolean().nullable(), tls_config: z.any().nullable(), network: z.string(), transport: z.any().nullable(), enable_relay: z.boolean().nullish(), relay_host: z.string().nullish(), relay_port: z.number().nullish(), }); const vlessSchema = z.object({ host: z.string(), port: z.number(), network: z.string(), transport: z.any().nullable(), security: z.string(), security_config: z.any().nullable(), xtls: z.string().nullish(), enable_relay: z.boolean().nullish(), relay_host: z.string().nullish(), relay_port: z.number().nullish(), }); const trojanSchema = z.object({ host: z.string(), port: z.number(), network: z.string(), sni: z.string().nullish(), allow_insecure: z.boolean().nullable(), transport: z.any().nullable(), enable_relay: z.boolean().nullish(), relay_host: z.string().nullish(), relay_port: z.number().nullish(), }); const formSchema = z.object({ name: z.string(), server_addr: z.string(), speed_limit: z.number().nullish(), traffic_ratio: z.number(), groupId: z.number().nullish(), protocol: z.enum(['shadowsocks', 'vmess', 'vless', 'trojan']), vmess: vmessSchema.nullish(), vless: vlessSchema.nullish(), trojan: trojanSchema.nullish(), shadowsocks: shadowsocksSchema.nullish(), }); interface NodeFormProps { onSubmit: (data: T) => Promise | boolean; initialValues?: T; loading?: boolean; trigger: string; title: string; } export default function NodeForm({ onSubmit, initialValues, loading, trigger, title, }: NodeFormProps) { const t = useTranslations('server.node'); const [open, setOpen] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { traffic_ratio: 1, protocol: 'shadowsocks', ...initialValues, } as any, }); const protocol = form.watch('protocol'); const vmessNetwork = form.watch('vmess.network'); const vlessNetwork = form.watch('vless.network'); const trojanNetwork = form.watch('trojan.network'); useEffect(() => { form?.reset(initialValues); }, [form, initialValues]); async function handleSubmit(data: { [x: string]: any }) { let newData = {}; switch (data.protocol) { case 'vmess': newData = { tls_config: {}, transport: {}, enable_tls: false, enable_relay: false, }; break; case 'vless': newData = { security_config: {}, transport: {}, enable_tls: false, enable_relay: false, }; break; case 'trojan': newData = { transport: {}, allow_insecure: false, enable_tls: false, enable_relay: false, }; break; } const bool = await onSubmit({ ...newData, ...data, } as unknown as T); if (bool) setOpen(false); } const { data: groups } = useQuery({ queryKey: ['getNodeGroupList'], queryFn: async () => { const { data } = await getNodeGroupList(); return (data.data?.list || []) as API.ServerGroup[]; }, }); return ( {title}
( {t('form.name')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.nodeGroupId')} placeholder={t('form.selectNodeGroup')} {...field} options={groups?.map((item) => ({ value: item.id, label: item.name, }))} /> )} />
( {t('form.serverAddr')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.speedLimit')} unitConversion('bitsToMb', value)} formatOutput={(value) => unitConversion('mbToBits', value)} onValueChange={(value) => { form.setValue(field.name, value); }} suffix='MB' /> )} /> ( {t('form.trafficRatio')} { form.setValue(field.name, value); }} suffix='X' /> )} />
( {t('form.protocol')} { form.setValue(field.name, value); const protocols = ['shadowsocks', 'vmess', 'vless', 'trojan']; protocols.forEach((proto) => { if (proto !== value) { form.setValue(proto, undefined); } }); const host = form.getValues().server_addr; if (value !== 'shadowsocks') form.setValue(`${value}.host`, host); if (value === 'vless') { form.setValue('vless.security', 'none'); form.setValue('vless.xtls', 'none'); } }} > Shadowsocks Vmess Vless Trojan )} /> {protocol === 'shadowsocks' && (
( {t('form.encryptionMethod')} )} /> ( {t('form.port')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.enableRelay')}
{ form.setValue(field.name, value); }} />
)} /> ( {t('form.relayHost')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.relayPort')} { form.setValue(field.name, value); }} /> )} />
)} {protocol === 'vmess' && (
( {t('form.Host Name')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.port')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.serverName')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.enableTls')}
{ form.setValue(field.name, value); }} />
)} /> ( {t('form.allowInsecure')}
{ form.setValue(field.name, value); }} />
)} /> ( {t('form.networkType')} )} /> { const placeholders: any = { tcp: { header: { type: 'http', request: { path: ['/'], headers: { Host: ['www.baidu.com', 'www.bing.com'], }, }, response: {}, }, }, websocket: { path: '/', headers: { Host: 'ppanel.dev', }, }, grpc: { serviceName: 'GunService', }, quic: { security: 'none', key: '', header: { type: 'none', }, }, mkcp: { header: { type: 'none', }, seed: '', }, httpupgrade: { path: '/', host: 'xtls.github.io', }, splithttp: { path: '/', host: 'xtls.github.io', }, }; return ( {/* {t('form.transport')} */} { form.setValue(field.name, value); }} /> ); }} /> ( {t('form.enableRelay')}
{ form.setValue(field.name, value); }} />
)} /> ( {t('form.relayHost')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.relayPort')} { form.setValue(field.name, value); }} /> )} />
)} {protocol === 'vless' && (
( {t('form.Host Name')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.port')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security')} {field.value && field.value !== 'none' && ( {t('form.edit')} {t('form.editSecurity')}
{field.value === 'tls' && ( <> ( {t('form.security_config.serverName')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security_config.fingerprint')} )} /> ( {t('form.allowInsecure')}
{ form.setValue(field.name, value); }} />
)} /> )} {field.value === 'reality' && ( <> ( {t('form.security_config.serverName')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security_config.serverAddress')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security_config.serverPort')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security_config.proxyProtocol')} )} /> ( {t('form.security_config.privateKey')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security_config.publicKey')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security_config.shortId')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.security_config.fingerprint')} )} /> ( {t('form.allowInsecure')}
{ form.setValue(field.name, value); }} />
)} /> )}
)}
)} /> ( {t('form.networkType')} )} /> ( {t('form.xtls')} )} /> { const placeholders: any = { tcp: { header: { type: 'http', request: { path: ['/'], headers: { Host: ['www.baidu.com', 'www.bing.com'], }, }, response: {}, }, }, websocket: { path: '/', headers: { Host: 'ppanel.dev', }, }, grpc: { serviceName: 'GunService', }, http2: { path: '/', host: 'xtls.github.io', }, quic: { security: 'none', key: '', header: { type: 'none', }, }, mkcp: { header: { type: 'none', }, seed: '', }, httpupgrade: { path: '/', host: 'xtls.github.io', }, splithttp: { path: '/', host: 'xtls.github.io', }, }; return ( { form.setValue(field.name, value); }} /> ); }} /> ( {t('form.enableRelay')}
{ form.setValue(field.name, value); }} />
)} /> ( {t('form.relayHost')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.relayPort')} { form.setValue(field.name, value); }} /> )} />
)} {protocol === 'trojan' && (
( {t('form.Host Name')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.port')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.allowInsecure')}
{ form.setValue(field.name, value); }} />
)} /> ( {t('form.sni')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.networkType')} )} /> { const placeholders: any = { tcp: {}, websocket: { path: '/', headers: { Host: 'ppanel.dev', }, }, grpc: { serviceName: 'GunService', }, }; return ( { form.setValue(field.name, value); }} /> ); }} /> ( {t('form.enableRelay')}
{ form.setValue(field.name, value); }} />
)} /> ( {t('form.relayHost')} { form.setValue(field.name, value); }} /> )} /> ( {t('form.relayPort')} { form.setValue(field.name, value); }} /> )} />
)}
); }