'use client'; import { getNodeGroupList } from '@/services/admin/server'; import { zodResolver } from '@hookform/resolvers/zod'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; import { Form, FormControl, FormField, FormItem, FormLabel, 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, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from '@workspace/ui/components/sheet'; import { Switch } from '@workspace/ui/components/switch'; import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { Combobox } from '@workspace/ui/custom-components/combobox'; 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 TagInput from '@workspace/ui/custom-components/tag-input'; import { cn } from '@workspace/ui/lib/utils'; import { unitConversion } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { formSchema, protocols } from './form-schema'; interface NodeFormProps { onSubmit: (data: T) => Promise | boolean; initialValues?: T; loading?: boolean; trigger: string; title: string; } export default function NodeForm({ onSubmit, initialValues, loading, trigger, title, }: Readonly>) { const t = useTranslations('server'); const tf = useTranslations('server.nodeForm'); const trs = useTranslations('server.relayModeOptions'); const tsc = useTranslations('server.securityConfig'); const [open, setOpen] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { tags: [], traffic_ratio: 1, protocol: 'shadowsocks', ...initialValues, config: { security: 'none', transport: 'tcp', ...initialValues?.config, }, } as any, }); const protocol = form.watch('protocol'); const transport = form.watch('config.transport'); const security = form.watch('config.security'); const relayMode = form.watch('relay_mode'); const method = form.watch('config.method'); useEffect(() => { form?.reset(initialValues); }, [form, initialValues]); async function handleSubmit(data: { [x: string]: any }) { const bool = await onSubmit(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('nodeForm.name')} { form.setValue(field.name, value); }} /> )} /> ( {t('nodeForm.groupId')} placeholder={t('nodeForm.selectNodeGroup')} {...field} options={groups?.map((item) => ({ value: item.id, label: item.name, }))} onChange={(value) => { form.setValue(field.name, value || 0); }} /> )} />
( {t('nodeForm.tags')} form.setValue(field.name, value)} /> )} /> ( {t('nodeForm.country')} { form.setValue(field.name, value); }} /> )} /> ( {t('nodeForm.city')} { form.setValue(field.name, value); }} /> )} />
( {t('nodeForm.serverAddr')} { form.setValue(field.name, value); }} /> )} /> ( {t('nodeForm.speedLimit')} unitConversion('bitsToMb', value)} formatOutput={(value) => unitConversion('mbToBits', value)} onValueChange={(value) => { form.setValue(field.name, value); }} suffix='Mbps' /> )} /> ( {t('nodeForm.trafficRatio')} { form.setValue(field.name, value); }} suffix='X' /> )} />
( {t('nodeForm.protocol')} { form.setValue(field.name, value); if (['trojan', 'hysteria2', 'tuic', 'anytls'].includes(value)) { form.setValue('config.security', 'tls'); } }} > {protocols.map((proto) => ( {proto.charAt(0).toUpperCase() + proto.slice(1)} ))} )} /> {protocol === 'shadowsocks' && (
( {t('nodeForm.encryptionMethod')} )} /> ( {t('nodeForm.port')} { form.setValue(field.name, value); }} /> )} /> {[ '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305', ].includes(method) && ( ( {t('nodeForm.serverKey')} { form.setValue(field.name, value); }} /> )} /> )}
)} {['vmess', 'vless', 'trojan', 'hysteria2', 'tuic', 'anytls'].includes(protocol) && (
( {t('nodeForm.port')} { form.setValue(field.name, value); }} /> )} /> {protocol === 'vless' && ( ( {t('nodeForm.flow')} )} /> )} {protocol === 'hysteria2' && ( <> ( {t('nodeForm.obfsPassword')} { form.setValue(field.name, value); }} /> )} /> ( {t('nodeForm.hopPorts')} { form.setValue(field.name, value); }} /> )} /> ( {t('nodeForm.hopInterval')} { form.setValue(field.name, value); }} suffix='S' /> )} /> )} {protocol === 'tuic' && ( <> ( {t('nodeForm.udpRelayMode')} )} /> ( {t('nodeForm.congestionController')} )} />
( {t('nodeForm.disableSni')}
{ form.setValue(field.name, checked); }} />
)} /> ( {t('nodeForm.reduceRtt')}
{ form.setValue(field.name, checked); }} />
)} />
)}
{['vmess', 'vless', 'trojan'].includes(protocol) && ( {t('nodeForm.transportConfig')} ( )} /> {transport !== 'tcp' && ( {['websocket', 'http2', 'httpupgrade'].includes(transport) && ( <> ( PATH { form.setValue(field.name, value); }} /> )} /> ( HOST { form.setValue(field.name, value); }} /> )} /> )} {['grpc'].includes(transport) && ( ( Service Name { form.setValue(field.name, value); }} /> )} /> )} )} )} {(['vmess', 'vless', 'trojan'].includes(protocol) || ['anytls', 'tuic', 'hysteria2'].includes(protocol)) && ( {t('nodeForm.securityConfig')} {['vmess', 'vless', 'trojan'].includes(protocol) && ( ( )} /> )} {(['anytls', 'tuic', 'hysteria2'].includes(protocol) || (['vmess', 'vless', 'trojan'].includes(protocol) && security !== 'none')) && ( ( Server Name(SNI) { form.setValue(field.name, value); }} /> )} /> {/* Reality 特殊配置只在 vless + reality 时显示 */} {protocol === 'vless' && security === 'reality' && ( <> ( {t('securityConfig.serverAddress')} { form.setValue(field.name, value); }} /> )} /> ( {t('securityConfig.serverPort')} { form.setValue(field.name, value); }} /> )} /> ( {t('securityConfig.privateKey')} { form.setValue(field.name, value); }} /> )} /> ( {t('securityConfig.publicKey')} { form.setValue(field.name, value); }} /> )} /> ( {t('securityConfig.shortId')} { form.setValue(field.name, value); }} /> )} /> )} {protocol === 'vless' && ( ( {t('securityConfig.fingerprint')} )} /> )} ( Allow Insecure
{ form.setValue(field.name, checked); }} />
)} />
)}
)}
)} {t('nodeForm.relayMode')} ( )} /> {relayMode !== 'none' && ( ( { form.setValue(field.name, value); }} /> )} /> )}
); }