Files
admin/src/app/(root)/cust/create.tsx

418 lines
13 KiB
TypeScript
Raw Normal View History

2026-03-28 14:28:32 +08:00
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useCallback, useEffect, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { getAllAdmin } from "@/actions/admin"
import { createCust } from "@/actions/cust"
import { getAllProductDiscount } from "@/actions/product_discount"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
2026-04-11 17:12:16 +08:00
DialogTrigger,
2026-03-28 14:28:32 +08:00
} from "@/components/ui/dialog"
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import type { Admin } from "@/models/admin"
import type { ProductDiscount } from "@/models/product_discount"
// 表单验证规则
const addUserSchema = z
.object({
2026-04-02 13:13:59 +08:00
username: z.string().optional(),
password: z
.string()
.optional()
.refine(val => !val || val.length >= 6, { message: "密码至少6位" }),
confirmPassword: z.string().optional(),
2026-03-28 14:28:32 +08:00
phone: z.string().regex(/^1[3-9]\d{9}$/, "请输入正确的手机号格式"),
email: z
.string()
.email("请输入正确的邮箱格式")
.optional()
.or(z.literal("")),
name: z.string().optional(),
admin_id: z.string().optional(),
discount_id: z.string().optional(),
source: z.string().optional(),
avatar: z.string().optional(),
status: z.string().optional(),
contact_qq: z.string().optional(),
contact_wechat: z.string().optional(),
})
2026-04-02 13:13:59 +08:00
.refine(
data => {
if (data.password) {
return data.password === data.confirmPassword
}
return true
},
{
message: "两次输入的密码不一致",
path: ["confirmPassword"],
},
)
2026-03-28 14:28:32 +08:00
export type AddUserFormValues = z.infer<typeof addUserSchema>
interface AddUserDialogProps {
onSuccess?: () => void
}
2026-04-11 17:12:16 +08:00
export function AddUserDialog({ onSuccess }: AddUserDialogProps) {
const [open, setOpen] = useState(false)
2026-03-28 14:28:32 +08:00
const [isAdding, setIsAdding] = useState(false)
const [discountList, setDiscountList] = useState<ProductDiscount[]>([])
const [isLoadingDiscount, setIsLoadingDiscount] = useState(false)
const [adminList, setAdminList] = useState<Admin[]>([])
const [isLoadingAdmin, setIsLoadingAdmin] = useState(false)
const {
control,
handleSubmit,
reset: resetAddForm,
} = useForm<AddUserFormValues>({
resolver: zodResolver(addUserSchema),
defaultValues: {
username: "",
password: "",
confirmPassword: "",
phone: "",
email: "",
name: "",
admin_id: "",
discount_id: "",
avatar: "",
status: "1",
contact_qq: "",
contact_wechat: "",
},
})
const statusOptions = [
{ value: "0", label: "禁用" },
{ value: "1", label: "正常" },
]
const fetchDiscountList = useCallback(async () => {
setIsLoadingDiscount(true)
try {
const res = await getAllProductDiscount()
if (res.success) {
setDiscountList(res.data || [])
} else {
toast.error(res.message || "获取折扣失败")
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`获取折扣失败: ${message}`)
} finally {
setIsLoadingDiscount(false)
}
}, [])
const fetchAdminList = useCallback(async () => {
setIsLoadingAdmin(true)
try {
const res = await getAllAdmin()
if (res.success) {
setAdminList(res.data || [])
} else {
toast.error(res.message || "获取管理员失败")
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`获取管理员失败: ${message}`)
} finally {
setIsLoadingAdmin(false)
}
}, [])
useEffect(() => {
if (open) {
fetchDiscountList()
fetchAdminList()
}
}, [open, fetchDiscountList, fetchAdminList])
const onSubmit = handleSubmit(async data => {
const payload = {
phone: data.phone,
2026-04-02 13:13:59 +08:00
username: data?.username,
password: data?.password,
2026-03-28 14:28:32 +08:00
email: data?.email || "",
name: data?.name,
admin_id: data.admin_id ? Number(data.admin_id) : undefined,
discount_id: data.discount_id ? Number(data.discount_id) : undefined,
avatar: data?.avatar,
status: data.status ? parseInt(data?.status) : 1,
2026-03-28 14:28:32 +08:00
contact_qq: data?.contact_qq,
contact_wechat: data?.contact_wechat,
}
setIsAdding(true)
try {
const result = await createCust(payload)
if (result?.success) {
toast.success("添加用户成功")
2026-04-11 17:12:16 +08:00
setOpen(false)
2026-03-28 14:28:32 +08:00
resetAddForm()
onSuccess?.()
} else {
toast.error(result?.message || "添加失败")
}
} catch (error) {
toast.error("添加失败,请稍后重试")
console.error(error)
} finally {
setIsAdding(false)
}
})
const handleOpenChange = (open: boolean) => {
if (!open) {
resetAddForm()
}
2026-04-11 17:12:16 +08:00
setOpen(open)
2026-03-28 14:28:32 +08:00
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
2026-04-11 17:12:16 +08:00
<DialogTrigger asChild>
<Button type="button"></Button>
</DialogTrigger>
2026-03-28 14:28:32 +08:00
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Controller
name="username"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel></FieldLabel>
<Input
{...field}
placeholder="请输入用户名"
autoComplete="off"
2026-05-14 16:04:35 +08:00
clearable
2026-03-28 14:28:32 +08:00
/>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="phone"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel> *</FieldLabel>
2026-05-14 16:04:35 +08:00
<Input {...field} placeholder="请输入手机号" clearable />
2026-03-28 14:28:32 +08:00
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
name="password"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
2026-04-02 13:13:59 +08:00
<FieldLabel>
<span className="text-gray-400 text-xs"></span>
</FieldLabel>
2026-03-28 14:28:32 +08:00
<Input
{...field}
type="password"
placeholder="请输入密码至少6位"
2026-05-14 16:04:35 +08:00
clearable
2026-03-28 14:28:32 +08:00
/>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="confirmPassword"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel></FieldLabel>
<Input
{...field}
type="password"
placeholder="请再次输入密码"
2026-05-14 16:04:35 +08:00
clearable
2026-03-28 14:28:32 +08:00
/>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
name="email"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel></FieldLabel>
2026-05-14 16:04:35 +08:00
<Input {...field} placeholder="请输入邮箱" clearable />
2026-03-28 14:28:32 +08:00
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="status"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel></FieldLabel>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="w-full h-9">
<SelectValue placeholder="请选择用户状态" />
</SelectTrigger>
<SelectContent>
{statusOptions.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
name="contact_qq"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel>QQ联系方式</FieldLabel>
2026-05-14 16:04:35 +08:00
<Input {...field} placeholder="请输入QQ联系方式" clearable />
2026-03-28 14:28:32 +08:00
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="contact_wechat"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel>/</FieldLabel>
2026-05-14 16:04:35 +08:00
<Input
{...field}
placeholder="请输入微信或联系方式"
clearable
/>
2026-03-28 14:28:32 +08:00
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
name="discount_id"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel></FieldLabel>
<Select
value={field.value}
onValueChange={field.onChange}
disabled={isLoadingDiscount}
>
<SelectTrigger className="w-full h-9">
<SelectValue placeholder="请选择折扣" />
</SelectTrigger>
<SelectContent>
{discountList.map(discount => (
<SelectItem
key={discount.id}
value={discount.id.toString()}
>
{discount.name}{discount.discount}%
</SelectItem>
))}
</SelectContent>
</Select>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="admin_id"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel></FieldLabel>
<Select
disabled={isLoadingAdmin}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger className="w-full h-9">
<SelectValue
placeholder={
isLoadingAdmin ? "加载中..." : "请选择管理员"
}
/>
</SelectTrigger>
<SelectContent>
{adminList.map(admin => (
<SelectItem key={admin.id} value={admin.id.toString()}>
{admin.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
</div>
<FieldGroup className="flex-row justify-end gap-2 pt-2">
<Button
type="button"
variant="outline"
2026-04-11 17:12:16 +08:00
onClick={() => setOpen(false)}
2026-03-28 14:28:32 +08:00
>
</Button>
<Button type="submit" disabled={isAdding}>
{isAdding ? "添加中..." : "确定添加"}
</Button>
</FieldGroup>
</form>
</DialogContent>
</Dialog>
)
}