♻️ refactor: Clean up NodeForm and ServerForm components by removing unused functions and optimizing state management

This commit is contained in:
web 2025-09-03 08:58:18 -07:00
parent 6a3bb7016e
commit 10250d9e34
4 changed files with 618 additions and 597 deletions

View File

@ -62,11 +62,6 @@ const buildSchema = (t: ReturnType<typeof useTranslations>) =>
export type NodeFormValues = z.infer<ReturnType<typeof buildSchema>>; export type NodeFormValues = z.infer<ReturnType<typeof buildSchema>>;
async function getServers(): Promise<ServerRow[]> {
const { data } = await filterServerList({ page: 1, size: 1000 });
return (data?.data?.list || []) as ServerRow[];
}
export default function NodeForm(props: { export default function NodeForm(props: {
trigger: string; trigger: string;
title: string; title: string;
@ -147,7 +142,6 @@ export default function NodeForm(props: {
if (!allowed.includes(form.getValues('protocol') as ProtocolName)) { if (!allowed.includes(form.getValues('protocol') as ProtocolName)) {
form.setValue('protocol', '' as any); form.setValue('protocol', '' as any);
} }
// Do not auto-fill port here; handled in handleProtocolChange
} }
function handleProtocolChange(nextProto?: ProtocolName | null) { function handleProtocolChange(nextProto?: ProtocolName | null) {
@ -164,15 +158,6 @@ export default function NodeForm(props: {
} }
} }
async function submit(values: NodeFormValues) {
const ok = await onSubmit(values);
if (ok) {
form.reset();
setOpen(false);
}
return ok;
}
return ( return (
<Sheet open={open} onOpenChange={setOpen}> <Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
@ -310,14 +295,11 @@ export default function NodeForm(props: {
</Button> </Button>
<Button <Button
disabled={loading} disabled={loading}
onClick={form.handleSubmit( onClick={form.handleSubmit(onSubmit, (errors) => {
async (vals) => submit(vals),
(errors) => {
const key = Object.keys(errors)[0] as keyof typeof errors; const key = Object.keys(errors)[0] as keyof typeof errors;
if (key) toast.error(String(errors[key]?.message)); if (key) toast.error(String(errors[key]?.message));
return false; return false;
}, })}
)}
> >
{t('confirm')} {t('confirm')}
</Button> </Button>

View File

@ -190,6 +190,8 @@ export const formSchema = z.object({
protocols: z.array(protocolApiScheme), protocols: z.array(protocolApiScheme),
}); });
export type ServerFormValues = z.infer<typeof formSchema>;
export type ProtocolType = (typeof protocols)[number]; export type ProtocolType = (typeof protocols)[number];
export function getProtocolDefaultConfig(proto: ProtocolType) { export function getProtocolDefaultConfig(proto: ProtocolType) {

View File

@ -32,7 +32,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
@ -44,102 +44,104 @@ import {
LABELS, LABELS,
protocols as PROTOCOLS, protocols as PROTOCOLS,
SECURITY, SECURITY,
ServerFormValues,
SS_CIPHERS, SS_CIPHERS,
TRANSPORTS, TRANSPORTS,
TUIC_CONGESTION, TUIC_CONGESTION,
TUIC_UDP_RELAY_MODES, TUIC_UDP_RELAY_MODES,
} from './form-schema'; } from './form-schema';
interface ServerFormProps<T> {
onSubmit: (data: T) => Promise<boolean> | boolean;
initialValues?: T | any;
loading?: boolean;
trigger: string;
title: string;
}
function titleCase(s: string) { function titleCase(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1); return s.charAt(0).toUpperCase() + s.slice(1);
} }
function normalizeValues(raw: any) { export default function ServerForm(props: {
return { trigger: string;
name: raw?.name ?? '', title: string;
address: raw?.address ?? '', loading?: boolean;
country: raw?.country ?? '', initialValues?: Partial<ServerFormValues>;
city: raw?.city ?? '', onSubmit: (values: ServerFormValues) => Promise<boolean> | boolean;
ratio: Number(raw?.ratio ?? 1), }) {
protocols: Array.isArray(raw?.protocols) ? raw.protocols : [], const { trigger, title, loading, initialValues, onSubmit } = props;
};
}
export default function ServerForm<T extends { [x: string]: any }>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: Readonly<ServerFormProps<T>>) {
const t = useTranslations('servers'); const t = useTranslations('servers');
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [activeType, setActiveType] = useState<(typeof PROTOCOLS)[number]>('shadowsocks'); const [activeType, setActiveType] = useState<(typeof PROTOCOLS)[number]>('shadowsocks');
const [protocolsEnabled, setProtocolsEnabled] = useState<string[]>([]);
const defaultValues = useMemo( const form = useForm({
() => resolver: zodResolver(formSchema),
normalizeValues({ defaultValues: {
name: '', name: '',
address: '', address: '',
country: '', country: '',
city: '', city: '',
ratio: 1, ratio: 1,
protocols: [], protocols: [],
}), ...initialValues,
[], },
); });
const form = useForm<any>({ resolver: zodResolver(formSchema), defaultValues });
const { control } = form; const { control } = form;
const protocolsValues = useWatch({ control, name: 'protocols' }); const protocolsValues = useWatch({ control, name: 'protocols' });
useEffect(() => { useEffect(() => {
const normalized = normalizeValues(initialValues || {}); if (initialValues) {
const byType = new Map<string, any>(); const enabledProtocols = PROTOCOLS.filter((type) => {
(Array.isArray(normalized.protocols) ? normalized.protocols : []).forEach((p: any) => { const protocol = initialValues.protocols?.find((p) => p.type === type);
if (p && p.type) byType.set(String(p.type), p); return protocol && protocol.port && Number(protocol.port) > 0;
}); });
const full = PROTOCOLS.map((t) => byType.get(t) || getProtocolDefaultConfig(t)); setProtocolsEnabled(enabledProtocols);
form.reset({ ...normalized, protocols: full }); form.reset({
setActiveType('shadowsocks'); name: '',
address: '',
country: '',
city: '',
ratio: 1,
...initialValues,
protocols: PROTOCOLS.map((type) => {
const existingProtocol = initialValues.protocols?.find((p) => p.type === type);
return existingProtocol || getProtocolDefaultConfig(type);
}),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValues]); }, [initialValues]);
async function handleSubmit(data: { [x: string]: any }) { async function handleSubmit(values: Record<string, any>) {
const all = Array.isArray(data?.protocols) ? data.protocols : []; const filtered = (values?.protocols || [])
const filtered = all .filter((p: any, index: number) => {
.filter((p: any) => { const port = Number(p?.port);
const v = (p ?? {}).port; const protocolType = PROTOCOLS[index];
const n = Number(v); return (
return Number.isFinite(n) && n > 0 && n <= 65535; protocolType &&
protocolsEnabled.includes(protocolType) &&
Number.isFinite(port) &&
port > 0 &&
port <= 65535
);
}) })
.map((p: any) => ({ ...p, port: Number(p.port) })); .map((p: any) => ({ ...p, port: Number(p.port) }));
if (filtered.length === 0) { if (filtered.length === 0) {
toast.error(t('validation_failed')); toast.error(t('validation_failed'));
return; return;
} }
const body = {
name: data?.name,
country: data?.country,
city: data?.city,
ratio: Number(data?.ratio ?? 1),
address: data?.address,
protocols: filtered,
} as unknown as T;
const ok = await onSubmit(body as unknown as T);
if (ok) setOpen(false);
}
// inlined protocol editor below in TabsContent const result = {
name: values.name,
country: values.country,
city: values.city,
ratio: Number(values.ratio || 1),
address: values.address,
protocols: filtered,
};
const ok = await onSubmit(result);
if (ok) {
form.reset();
setOpen(false);
}
}
return ( return (
<Sheet open={open} onOpenChange={setOpen}> <Sheet open={open} onOpenChange={setOpen}>
@ -148,7 +150,15 @@ export default function ServerForm<T extends { [x: string]: any }>({
onClick={() => { onClick={() => {
if (!initialValues) { if (!initialValues) {
const full = PROTOCOLS.map((t) => getProtocolDefaultConfig(t)); const full = PROTOCOLS.map((t) => getProtocolDefaultConfig(t));
form.reset({ ...defaultValues, protocols: full }); form.reset({
name: '',
address: '',
country: '',
city: '',
ratio: 1,
protocols: full,
});
setProtocolsEnabled([]);
} }
setOpen(true); setOpen(true);
}} }}
@ -244,7 +254,7 @@ export default function ServerForm<T extends { [x: string]: any }>({
</div> </div>
<div className='pt-2'> <div className='pt-2'>
<Tabs value={activeType} onValueChange={(v) => setActiveType(v as any)}> <Tabs value={activeType} onValueChange={(v) => setActiveType(v as any)}>
<TabsList className='w-full flex-wrap'> <TabsList className='h-auto w-full flex-wrap'>
{PROTOCOLS.map((type) => ( {PROTOCOLS.map((type) => (
<TabsTrigger key={type} value={type}> <TabsTrigger key={type} value={type}>
{titleCase(type)} {titleCase(type)}
@ -257,11 +267,30 @@ export default function ServerForm<T extends { [x: string]: any }>({
PROTOCOLS.findIndex((t) => t === type), PROTOCOLS.findIndex((t) => t === type),
); );
const current = Array.isArray(protocolsValues) ? protocolsValues[i] || {} : {}; const current = Array.isArray(protocolsValues) ? protocolsValues[i] || {} : {};
const transport = (current?.transport as string | undefined) ?? 'tcp'; const transport = ((current as any)?.transport as string | undefined) ?? 'tcp';
const security = current?.security as string | undefined; const security = (current as any)?.security as string | undefined;
const cipher = current?.cipher as string | undefined; const cipher = (current as any)?.cipher as string | undefined;
const isEnabled = protocolsEnabled.includes(type);
return ( return (
<TabsContent key={type} value={type} className='pt-3'> <TabsContent key={type} value={type} className='space-y-4 pt-3'>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
{t('enabled')}
</span>
<Switch
checked={isEnabled}
onCheckedChange={(checked) => {
if (checked) {
setProtocolsEnabled([...protocolsEnabled, type]);
} else {
setProtocolsEnabled(protocolsEnabled.filter((p) => p !== type));
}
}}
/>
</div>
{isEnabled && (
<div className='space-y-4'> <div className='space-y-4'>
<div className='grid grid-cols-2 gap-2'> <div className='grid grid-cols-2 gap-2'>
<FormField <FormField
@ -507,7 +536,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<div className='pt-2'> <div className='pt-2'>
<Switch <Switch
checked={!!field.value} checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)} onCheckedChange={(checked) =>
field.onChange(checked)
}
/> />
</div> </div>
</FormControl> </FormControl>
@ -525,7 +556,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<div className='pt-2'> <div className='pt-2'>
<Switch <Switch
checked={!!field.value} checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)} onCheckedChange={(checked) =>
field.onChange(checked)
}
/> />
</div> </div>
</FormControl> </FormControl>
@ -758,7 +791,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
{...field} {...field}
placeholder={t('security_private_key_placeholder')} placeholder={t(
'security_private_key_placeholder',
)}
onValueChange={(v) => field.onChange(v)} onValueChange={(v) => field.onChange(v)}
/> />
</FormControl> </FormControl>
@ -841,7 +876,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<div className='pt-2'> <div className='pt-2'>
<Switch <Switch
checked={!!field.value} checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)} onCheckedChange={(checked) =>
field.onChange(checked)
}
/> />
</div> </div>
</FormControl> </FormControl>
@ -854,6 +891,7 @@ export default function ServerForm<T extends { [x: string]: any }>({
</Card> </Card>
)} )}
</div> </div>
)}
</TabsContent> </TabsContent>
); );
})} })}
@ -868,9 +906,10 @@ export default function ServerForm<T extends { [x: string]: any }>({
</Button> </Button>
<Button <Button
disabled={loading} disabled={loading}
onClick={form.handleSubmit(handleSubmit, (e) => { onClick={form.handleSubmit(handleSubmit, (errors) => {
console.log(e, form.getValues()); console.log(errors, form.getValues());
toast.error(t('validation_failed')); const key = Object.keys(errors)[0] as keyof typeof errors;
if (key) toast.error(String(errors[key]?.message));
return false; return false;
})} })}
> >

View File

@ -109,7 +109,6 @@ export function ProtocolForm() {
}, },
}); });
// API请求函数
const request = async ( const request = async (
pagination: { page: number; size: number }, pagination: { page: number; size: number },
filter: Record<string, unknown>, filter: Record<string, unknown>,
@ -125,7 +124,6 @@ export function ProtocolForm() {
}; };
}; };
// 表格列定义
const columns: ColumnDef<API.SubscribeApplication, any>[] = [ const columns: ColumnDef<API.SubscribeApplication, any>[] = [
{ {
accessorKey: 'is_default', accessorKey: 'is_default',