263 lines
10 KiB
TypeScript
263 lines
10 KiB
TypeScript
"use client"
|
||
|
||
import { cn } from "@/lib/utils"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { z } from "zod"
|
||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
||
import { useForm } from "react-hook-form"
|
||
import { zodResolver } from "@hookform/resolvers/zod"
|
||
import { useRouter } from "next/navigation"
|
||
import { useEffect, useState, useCallback } from "react"
|
||
import { gql, useMutation } from "@apollo/client"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { DialogClose, DialogFooter } from "@/components/ui/dialog"
|
||
import { IconCircleCheckFilled, IconLoader } from "@tabler/icons-react"
|
||
|
||
const CREATE_USER = gql`
|
||
mutation CreateUser($username: String!, $email: String!, $role: String!, $password: String!) {
|
||
createUser(input: {
|
||
username: $username
|
||
email: $email
|
||
role: $role
|
||
password: $password
|
||
}) {
|
||
id
|
||
}
|
||
}
|
||
`
|
||
|
||
|
||
const schema = z.object({
|
||
username: z.string().min(1, "用户名不能为空"),
|
||
password: z.string().min(1, "密码不能为空"),
|
||
role: z.enum(["ADMIN", "USER"]),
|
||
email: z.email()
|
||
})
|
||
|
||
export default function CreateUserForm({
|
||
className,
|
||
...props
|
||
}: React.ComponentProps<"form">) {
|
||
const router = useRouter();
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [isGenerating, setIsGenerating] = useState(false)
|
||
|
||
const [createUser, { loading }] = useMutation(CREATE_USER)
|
||
|
||
const form = useForm<z.infer<typeof schema>>({
|
||
resolver: zodResolver(schema),
|
||
defaultValues: {
|
||
username: "",
|
||
email: "",
|
||
password: "",
|
||
role: "USER" as const,
|
||
},
|
||
})
|
||
|
||
async function onSubmit(values: z.infer<typeof schema>) {
|
||
try {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
await createUser({
|
||
variables: {
|
||
username: values.username,
|
||
password: values.password,
|
||
role: values.role,
|
||
email: values.email
|
||
}
|
||
});
|
||
|
||
// 重置表单
|
||
form.reset();
|
||
// 重新生成密码
|
||
generateRandomPassword();
|
||
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '创建用户失败,请重试');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
|
||
|
||
// 生成随机密码的函数
|
||
const generateRandomPassword = useCallback(() => {
|
||
setIsGenerating(true)
|
||
|
||
// 模拟生成过程
|
||
setTimeout(() => {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
|
||
let password = ''
|
||
|
||
// 确保包含至少一个大写字母、一个小写字母、一个数字和一个特殊字符
|
||
password += chars[Math.floor(Math.random() * 26)] // 大写字母
|
||
password += chars[26 + Math.floor(Math.random() * 26)] // 小写字母
|
||
password += chars[52 + Math.floor(Math.random() * 10)] // 数字
|
||
password += chars[62 + Math.floor(Math.random() * 8)] // 特殊字符
|
||
|
||
// 添加剩余的随机字符,总长度为12
|
||
for (let i = 4; i < 12; i++) {
|
||
password += chars[Math.floor(Math.random() * chars.length)]
|
||
}
|
||
|
||
// 打乱密码字符顺序
|
||
password = password.split('').sort(() => Math.random() - 0.5).join('')
|
||
form.setValue('password', password)
|
||
setIsGenerating(false)
|
||
// toast.success('随机密码已生成')
|
||
}, 500)
|
||
}, [form])
|
||
|
||
useEffect(() => {
|
||
generateRandomPassword()
|
||
}, [generateRandomPassword])
|
||
|
||
|
||
return (
|
||
<Form {...form} >
|
||
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||
{error && (
|
||
<div className="text-sm text-red-600 bg-red-50 p-3 rounded-md">
|
||
{error}
|
||
</div>
|
||
)}
|
||
<div className="grid gap-4">
|
||
<div className="grid gap-3">
|
||
<FormField
|
||
control={form.control}
|
||
name="username"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>用户名 *</FormLabel>
|
||
<FormControl>
|
||
<Input id="username" placeholder="请输入用户名" {...field} required />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
|
||
)} />
|
||
</div>
|
||
|
||
<div className="grid gap-3">
|
||
<FormField
|
||
control={form.control}
|
||
name="email"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>邮箱 *</FormLabel>
|
||
<FormControl>
|
||
<Input id="email" placeholder="请输入邮箱" {...field} required />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)} />
|
||
</div>
|
||
|
||
<div className="grid gap-3">
|
||
<FormField
|
||
control={form.control}
|
||
name="role"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>角色 *</FormLabel>
|
||
<FormControl>
|
||
<Select
|
||
value={field.value}
|
||
onValueChange={(value) => field.onChange(value)}
|
||
>
|
||
<SelectTrigger id="role" className="w-full">
|
||
<SelectValue placeholder="选择用户角色" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="ADMIN">管理员</SelectItem>
|
||
<SelectItem value="USER">普通用户</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</FormControl>
|
||
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
</div>
|
||
|
||
<div className="grid gap-3">
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="password"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<FormLabel>密码 *</FormLabel>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={generateRandomPassword}
|
||
disabled={isGenerating}
|
||
>
|
||
{isGenerating ? (
|
||
<>
|
||
<IconLoader className="mr-2 h-4 w-4 animate-spin" />
|
||
生成中...
|
||
</>
|
||
) : (
|
||
'重新生成'
|
||
)}
|
||
</Button>
|
||
</div>
|
||
|
||
<FormControl>
|
||
<div className="flex gap-2">
|
||
|
||
<Input id="password" placeholder="请输入密码" {...field} required />
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={() => {
|
||
navigator.clipboard.writeText(field.value)
|
||
// toast.success('密码已复制到剪贴板')
|
||
}}
|
||
disabled={!field.value}
|
||
>
|
||
<IconCircleCheckFilled className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<DialogClose asChild>
|
||
<Button type="button" variant="outline">
|
||
取消
|
||
</Button>
|
||
</DialogClose>
|
||
<Button type="submit" disabled={isLoading}>
|
||
{isLoading ? (
|
||
<>
|
||
<IconLoader className="mr-2 h-4 w-4 animate-spin" />
|
||
创建中...
|
||
</>
|
||
) : (
|
||
'创建用户'
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</form>
|
||
</Form >
|
||
|
||
)
|
||
}
|