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,
|
|
|
|
|
|
} 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 {
|
|
|
|
|
|
open: boolean
|
|
|
|
|
|
onOpenChange: (open: boolean) => void
|
|
|
|
|
|
onSuccess?: () => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function AddUserDialog({
|
|
|
|
|
|
open,
|
|
|
|
|
|
onOpenChange,
|
|
|
|
|
|
onSuccess,
|
|
|
|
|
|
}: AddUserDialogProps) {
|
|
|
|
|
|
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,
|
2026-03-28 18:04:19 +08:00
|
|
|
|
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("添加用户成功")
|
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
|
resetAddForm()
|
|
|
|
|
|
onSuccess?.()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.error(result?.message || "添加失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast.error("添加失败,请稍后重试")
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsAdding(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenChange = (open: boolean) => {
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
resetAddForm()
|
|
|
|
|
|
}
|
|
|
|
|
|
onOpenChange(open)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
|
|
|
|
</Field>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Controller
|
|
|
|
|
|
name="phone"
|
|
|
|
|
|
control={control}
|
|
|
|
|
|
render={({ field, fieldState }) => (
|
|
|
|
|
|
<Field data-invalid={fieldState.invalid}>
|
|
|
|
|
|
<FieldLabel>手机号 *</FieldLabel>
|
|
|
|
|
|
<Input {...field} placeholder="请输入手机号" />
|
|
|
|
|
|
<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位)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<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="请再次输入密码"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<Input {...field} placeholder="请输入邮箱" />
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<Input {...field} placeholder="请输入QQ联系方式" />
|
|
|
|
|
|
<FieldError>{fieldState.error?.message}</FieldError>
|
|
|
|
|
|
</Field>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Controller
|
|
|
|
|
|
name="contact_wechat"
|
|
|
|
|
|
control={control}
|
|
|
|
|
|
render={({ field, fieldState }) => (
|
|
|
|
|
|
<Field data-invalid={fieldState.invalid}>
|
|
|
|
|
|
<FieldLabel>微信/联系方式</FieldLabel>
|
|
|
|
|
|
<Input {...field} placeholder="请输入微信或联系方式" />
|
|
|
|
|
|
<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"
|
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="submit" disabled={isAdding}>
|
|
|
|
|
|
{isAdding ? "添加中..." : "确定添加"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</FieldGroup>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|