import { memo, useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { Key, Lock } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { hostApi } from '@/api' import { PasswordInput } from '@/components/PasswordInput' import { toast } from 'sonner' // 表单 schema:zod 为唯一校验源,消息中文且具体 // (port 在表单内为字符串、提交时转数字,避免 z.coerce 输入/输出类型分叉导致 RHF 泛型不兼容) const hostFormSchema = z .object({ name: z.string().min(1, '请输入主机名称'), hostIP: z.string().min(1, '请输入主机 IP'), port: z .string() .min(1, '请输入端口') .regex(/^\d+$/, '端口必须为数字') .refine((v) => Number(v) >= 1 && Number(v) <= 65535, '端口必须在 1-65535 之间'), user: z.string().min(1, '请输入用户名'), authType: z.enum(['password', 'key']), password: z.string().optional(), privateKey: z.string().optional(), keyPhrase: z.string().optional(), labels: z.string().optional(), }) .superRefine((data, ctx) => { if (EDITING_REF.current) return if (data.authType === 'password' && !data.password?.trim()) { ctx.addIssue({ code: 'custom', path: ['password'], message: '请填写 SSH 登录密码', }) } if (data.authType === 'key' && !data.privateKey?.trim()) { ctx.addIssue({ code: 'custom', path: ['privateKey'], message: '请粘贴 SSH 私钥内容', }) } }) type HostFormValues = z.infer const DEFAULT_VALUES: HostFormValues = { name: '', hostIP: '', port: '22', user: 'root', authType: 'password', password: '', privateKey: '', keyPhrase: '', labels: '', } interface HostFormDialogProps { open: boolean onOpenChange: (open: boolean) => void /** 创建成功回调(父级刷新列表并关闭弹窗) */ onCreated: () => void /** 编辑目标(编辑模式:凭证留空=保留) */ editing?: import('@/types').Host | null } /** 添加 SSH 主机弹窗 — react-hook-form + zod,自包含表单逻辑,页面只传开关与回调 */ const EDITING_REF = { current: false as boolean | null } export const HostFormDialog = memo(function HostFormDialog({ open, onOpenChange, onCreated, editing, }: HostFormDialogProps) { const [submitting, setSubmitting] = useState(false) const form = useForm({ resolver: zodResolver(hostFormSchema), defaultValues: DEFAULT_VALUES, }) const authType = form.watch('authType') // 打开时重置(默认值),关闭/重开时机明确 EDITING_REF.current = !!editing useEffect(() => { if (open) { form.reset(editing ? { name: editing.name, hostIP: editing.hostIP, port: String(editing.port), user: editing.user, authType: editing.authType, password: '', privateKey: '', keyPhrase: '', labels: editing.labels || '', } as never : DEFAULT_VALUES) } }, [open, form, editing]) const onSubmit = form.handleSubmit(async (values) => { setSubmitting(true) try { await (editing ? hostApi.update(editing.id, { name: values.name, hostIP: values.hostIP, port: Number(values.port), user: values.user, authType: values.authType, password: values.password || undefined, privateKey: values.privateKey || undefined, keyPhrase: values.keyPhrase || undefined, labels: values.labels, }) : hostApi.create({ name: values.name, hostIP: values.hostIP, port: Number(values.port), user: values.user, authType: values.authType, password: values.password, privateKey: values.privateKey, keyPhrase: values.keyPhrase, labels: values.labels, })) onOpenChange(false) onCreated() } catch (err) { toast.error(err instanceof Error ? err.message : '添加主机失败') } finally { setSubmitting(false) } }) return ( {editing ? `编辑主机${editing.name ? ` · ${editing.name}` : ''}` : '添加 SSH 主机'}
( 名称 * )} /> ( 主机 IP * )} />
( 端口 )} /> ( 用户名 * )} />
( 认证方式
)} /> {authType === 'password' ? ( ( 密码{editing ? '(留空保持不变)' : ' *'} )} /> ) : ( <> ( 私钥{editing ? '(留空保持不变)' : ' *'}