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

View File

@ -190,6 +190,8 @@ export const formSchema = z.object({
protocols: z.array(protocolApiScheme),
});
export type ServerFormValues = z.infer<typeof formSchema>;
export type ProtocolType = (typeof protocols)[number];
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 { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import {
@ -44,102 +44,104 @@ import {
LABELS,
protocols as PROTOCOLS,
SECURITY,
ServerFormValues,
SS_CIPHERS,
TRANSPORTS,
TUIC_CONGESTION,
TUIC_UDP_RELAY_MODES,
} 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) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function normalizeValues(raw: any) {
return {
name: raw?.name ?? '',
address: raw?.address ?? '',
country: raw?.country ?? '',
city: raw?.city ?? '',
ratio: Number(raw?.ratio ?? 1),
protocols: Array.isArray(raw?.protocols) ? raw.protocols : [],
};
}
export default function ServerForm<T extends { [x: string]: any }>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: Readonly<ServerFormProps<T>>) {
export default function ServerForm(props: {
trigger: string;
title: string;
loading?: boolean;
initialValues?: Partial<ServerFormValues>;
onSubmit: (values: ServerFormValues) => Promise<boolean> | boolean;
}) {
const { trigger, title, loading, initialValues, onSubmit } = props;
const t = useTranslations('servers');
const [open, setOpen] = useState(false);
const [activeType, setActiveType] = useState<(typeof PROTOCOLS)[number]>('shadowsocks');
const [protocolsEnabled, setProtocolsEnabled] = useState<string[]>([]);
const defaultValues = useMemo(
() =>
normalizeValues({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
address: '',
country: '',
city: '',
ratio: 1,
protocols: [],
}),
[],
);
const form = useForm<any>({ resolver: zodResolver(formSchema), defaultValues });
...initialValues,
},
});
const { control } = form;
const protocolsValues = useWatch({ control, name: 'protocols' });
useEffect(() => {
const normalized = normalizeValues(initialValues || {});
const byType = new Map<string, any>();
(Array.isArray(normalized.protocols) ? normalized.protocols : []).forEach((p: any) => {
if (p && p.type) byType.set(String(p.type), p);
if (initialValues) {
const enabledProtocols = PROTOCOLS.filter((type) => {
const protocol = initialValues.protocols?.find((p) => p.type === type);
return protocol && protocol.port && Number(protocol.port) > 0;
});
const full = PROTOCOLS.map((t) => byType.get(t) || getProtocolDefaultConfig(t));
form.reset({ ...normalized, protocols: full });
setActiveType('shadowsocks');
setProtocolsEnabled(enabledProtocols);
form.reset({
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
}, [initialValues]);
async function handleSubmit(data: { [x: string]: any }) {
const all = Array.isArray(data?.protocols) ? data.protocols : [];
const filtered = all
.filter((p: any) => {
const v = (p ?? {}).port;
const n = Number(v);
return Number.isFinite(n) && n > 0 && n <= 65535;
async function handleSubmit(values: Record<string, any>) {
const filtered = (values?.protocols || [])
.filter((p: any, index: number) => {
const port = Number(p?.port);
const protocolType = PROTOCOLS[index];
return (
protocolType &&
protocolsEnabled.includes(protocolType) &&
Number.isFinite(port) &&
port > 0 &&
port <= 65535
);
})
.map((p: any) => ({ ...p, port: Number(p.port) }));
if (filtered.length === 0) {
toast.error(t('validation_failed'));
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 (
<Sheet open={open} onOpenChange={setOpen}>
@ -148,7 +150,15 @@ export default function ServerForm<T extends { [x: string]: any }>({
onClick={() => {
if (!initialValues) {
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);
}}
@ -244,7 +254,7 @@ export default function ServerForm<T extends { [x: string]: any }>({
</div>
<div className='pt-2'>
<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) => (
<TabsTrigger key={type} value={type}>
{titleCase(type)}
@ -257,11 +267,30 @@ export default function ServerForm<T extends { [x: string]: any }>({
PROTOCOLS.findIndex((t) => t === type),
);
const current = Array.isArray(protocolsValues) ? protocolsValues[i] || {} : {};
const transport = (current?.transport as string | undefined) ?? 'tcp';
const security = current?.security as string | undefined;
const cipher = current?.cipher as string | undefined;
const transport = ((current as any)?.transport as string | undefined) ?? 'tcp';
const security = (current as any)?.security as string | undefined;
const cipher = (current as any)?.cipher as string | undefined;
const isEnabled = protocolsEnabled.includes(type);
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='grid grid-cols-2 gap-2'>
<FormField
@ -507,7 +536,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<div className='pt-2'>
<Switch
checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)}
onCheckedChange={(checked) =>
field.onChange(checked)
}
/>
</div>
</FormControl>
@ -525,7 +556,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<div className='pt-2'>
<Switch
checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)}
onCheckedChange={(checked) =>
field.onChange(checked)
}
/>
</div>
</FormControl>
@ -758,7 +791,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<FormControl>
<EnhancedInput
{...field}
placeholder={t('security_private_key_placeholder')}
placeholder={t(
'security_private_key_placeholder',
)}
onValueChange={(v) => field.onChange(v)}
/>
</FormControl>
@ -841,7 +876,9 @@ export default function ServerForm<T extends { [x: string]: any }>({
<div className='pt-2'>
<Switch
checked={!!field.value}
onCheckedChange={(checked) => field.onChange(checked)}
onCheckedChange={(checked) =>
field.onChange(checked)
}
/>
</div>
</FormControl>
@ -854,6 +891,7 @@ export default function ServerForm<T extends { [x: string]: any }>({
</Card>
)}
</div>
)}
</TabsContent>
);
})}
@ -868,9 +906,10 @@ export default function ServerForm<T extends { [x: string]: any }>({
</Button>
<Button
disabled={loading}
onClick={form.handleSubmit(handleSubmit, (e) => {
console.log(e, form.getValues());
toast.error(t('validation_failed'));
onClick={form.handleSubmit(handleSubmit, (errors) => {
console.log(errors, form.getValues());
const key = Object.keys(errors)[0] as keyof typeof errors;
if (key) toast.error(String(errors[key]?.message));
return false;
})}
>

View File

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