diff --git a/src/actions/admin.ts b/src/actions/admin.ts index fe9ed30..3f49e3e 100644 --- a/src/actions/admin.ts +++ b/src/actions/admin.ts @@ -4,6 +4,10 @@ import type { PageRecord } from "@/lib/api" import type { Admin } from "@/models/admin" import { callByUser } from "./base" +export async function getAllAdmin() { + return callByUser("/api/admin/admin/all") +} + export async function getPageAdmin(params: { page: number; size: number }) { return callByUser>("/api/admin/admin/page", params) } diff --git a/src/actions/cust.ts b/src/actions/cust.ts index d746f0b..af2ba9b 100644 --- a/src/actions/cust.ts +++ b/src/actions/cust.ts @@ -10,15 +10,20 @@ export async function updateCust(data: { phone: string email: string }) { - return callByUser>("/api/admin/user/updateCust", data) + return callByUser>("/api/admin/user/update", data) } export async function createCust(data: { - username: string password: string + username: string phone: string + admin_id?: number + discount_id?: number email?: string name?: string + avatar?: string + status?: number + contact_qq?: string contact_wechat?: string }) { return callByUser>("/api/admin/user/create", data) diff --git a/src/app/(root)/cust/create.tsx b/src/app/(root)/cust/create.tsx new file mode 100644 index 0000000..464ba77 --- /dev/null +++ b/src/app/(root)/cust/create.tsx @@ -0,0 +1,417 @@ +"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({ + username: z.string().min(1, "账号不能为空"), + password: z.string().min(6, "密码至少6位"), + confirmPassword: z.string().min(1, "请确认密码"), + 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(), + }) + .refine(data => data.password === data.confirmPassword, { + message: "两次输入的密码不一致", + path: ["confirmPassword"], + }) + +export type AddUserFormValues = z.infer + +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([]) + const [isLoadingDiscount, setIsLoadingDiscount] = useState(false) + const [adminList, setAdminList] = useState([]) + const [isLoadingAdmin, setIsLoadingAdmin] = useState(false) + + const { + control, + handleSubmit, + reset: resetAddForm, + } = useForm({ + 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() + console.log("折扣res", res) + + 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() + console.log(res, "管理员res") + + 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 = { + username: data.username, + password: data.password, + phone: data.phone, + 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, + 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 ( + + + + 添加用户 + +
+
+ ( + + 用户名 + + {fieldState.error?.message} + + )} + /> + ( + + 手机号 * + + {fieldState.error?.message} + + )} + /> +
+ +
+ ( + + 用户密码 + + {fieldState.error?.message} + + )} + /> + ( + + 确认密码 + + {fieldState.error?.message} + + )} + /> +
+ +
+ ( + + 真实姓名 + + {fieldState.error?.message} + + )} + /> + ( + + 邮箱 + + {fieldState.error?.message} + + )} + /> +
+ +
+ ( + + 用户状态 + + {fieldState.error?.message} + + )} + /> +
+ +
+ ( + + QQ联系方式 + + {fieldState.error?.message} + + )} + /> + ( + + 微信/联系方式 + + {fieldState.error?.message} + + )} + /> +
+ +
+ ( + + 折扣 + + {fieldState.error?.message} + + )} + /> + ( + + 管理员 + + {fieldState.error?.message} + + )} + /> +
+ + + + + +
+
+
+ ) +} diff --git a/src/app/(root)/cust/page.tsx b/src/app/(root)/cust/page.tsx index 3039399..b596696 100644 --- a/src/app/(root)/cust/page.tsx +++ b/src/app/(root)/cust/page.tsx @@ -1,20 +1,15 @@ "use client" + import { zodResolver } from "@hookform/resolvers/zod" import { format } from "date-fns" -import { Suspense, useCallback, useState } from "react" +import { Suspense, useCallback, useRef, useState } from "react" import { Controller, useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" -import { createCust, getPageCusts, updateCust } from "@/actions/cust" +import { getPageCusts, updateCust } from "@/actions/cust" import { DataTable, useDataTable } from "@/components/data-table" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" import { Field, FieldError, @@ -30,9 +25,10 @@ import { SelectValue, } from "@/components/ui/select" import type { Cust } from "@/models/cust" +import { AddUserDialog } from "./create" type FilterValues = { - phone?: string + account?: string name?: string identified?: boolean enabled?: boolean @@ -42,7 +38,7 @@ type FilterValues = { const filterSchema = z .object({ - phone: z.string().optional(), + account: z.string().optional(), name: z.string().optional(), identified: z.string().optional(), enabled: z.string().optional(), @@ -64,17 +60,6 @@ const filterSchema = z } }) -const addUserSchema = z.object({ - username: z.string().min(1, "账号不能为空"), - password: z.string().min(6, "密码至少6位"), - phone: z.string().regex(/^1[3-9]\d{9}$/, "请输入正确的手机号格式"), - email: z.string().email("请输入正确的邮箱格式").optional().or(z.literal("")), - name: z.string().optional(), - contact_wechat: z.string().optional(), -}) - -type AddUserFormValues = z.infer - type FormValues = z.infer export default function UserPage() { @@ -84,12 +69,13 @@ export default function UserPage() { const [editEmail, setEditEmail] = useState("") const [isSaving, setIsSaving] = useState(false) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) - const [isAdding, setIsAdding] = useState(false) + + const editingRowRef = useRef(null) const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(filterSchema), defaultValues: { - phone: "", + account: "", name: "", identified: "all", enabled: "all", @@ -98,75 +84,55 @@ export default function UserPage() { }, }) - // 添加用户表单 - const { - control: addControl, - handleSubmit: handleAddSubmit, - reset: resetAddForm, - formState: { errors: addErrors }, - } = useForm({ - resolver: zodResolver(addUserSchema), - defaultValues: { - username: "", - password: "", - phone: "", - email: "", - name: "", - contact_wechat: "", - }, - }) - const fetchUsers = useCallback( (page: number, size: number) => getPageCusts({ page, size, ...filters }), [filters], ) const table = useDataTable(fetchUsers) + console.log(table, "table") const onFilter = handleSubmit(data => { const result: FilterValues = {} - - if (data.phone) result.phone = data.phone + if (data.account) result.account = data.account if (data.name) result.name = data.name if (data.identified && data.identified !== "all") result.identified = data.identified === "1" if (data.enabled && data.enabled !== "all") result.enabled = data.enabled === "1" - setFilters(result) table.pagination.onPageChange(1) }) const refreshTable = useCallback(() => { - table.pagination.onPageChange(table.pagination.page) - }, [table.pagination]) + table.refresh() + }, [table]) - // 开始编辑行 const startEdit = (row: Cust) => { setEditingRowId(row.id) setEditPhone(row.phone || "") setEditEmail(row.email || "") + editingRowRef.current = row } - // 取消编辑 const cancelEdit = () => { setEditingRowId(null) setEditPhone("") setEditEmail("") + editingRowRef.current = null } - // 保存编辑 const saveEdit = async (row: Cust) => { const phoneRegex = /^1[3-9]\d{9}$/ if (editPhone && !phoneRegex.test(editPhone)) { toast.error("请输入正确的手机号格式") - return + return false } const emailRegex = /^[^\s@]+@([^\s@]+\.)+[^\s@]+$/ if (editEmail && !emailRegex.test(editEmail)) { toast.error("请输入正确的邮箱格式") - return + return false } setIsSaving(true) @@ -178,52 +144,24 @@ export default function UserPage() { }) if (result.success) { toast.success("更新成功") - table.pagination.onPageChange(table.pagination.page) + refreshTable() cancelEdit() + return true } else { toast.error(result.message || "更新失败") + return false } } catch (error) { toast.error("更新失败,请稍后重试") console.error(error) + return false } finally { setIsSaving(false) } } - const onAddUser = handleAddSubmit(async data => { - const payload = { - username: data.username, - password: data.password, - phone: data.phone || "", - email: data.email || "", - name: data.name || "", - contact_wechat: data.contact_wechat || "", - } - - setIsAdding(true) - try { - const result = await createCust(payload) - if (result?.success) { - toast.success("添加用户成功") - setIsAddDialogOpen(false) - resetAddForm() - refreshTable() - } else { - toast.error(result?.message || "添加失败") - } - } catch (error) { - toast.error("添加失败,请稍后重试") - console.error(error) - } finally { - setIsAdding(false) - } - }) - - // 打开添加对话框时重置表单 - const openAddDialog = () => { - resetAddForm() - setIsAddDialogOpen(true) + const handleAddUserSuccess = () => { + refreshTable() } return ( @@ -231,15 +169,15 @@ export default function UserPage() {
( - 手机号 - + 账号/手机号/邮箱 + {fieldState.error?.message} )} @@ -253,8 +191,8 @@ export default function UserPage() { data-invalid={fieldState.invalid} className="w-40 flex-none" > - 账户 - + 姓名 + {fieldState.error?.message} )} @@ -267,7 +205,7 @@ export default function UserPage() { 实名状态 setEditPhone(e.target.value)} + onBlur={() => { + if (editingRowRef.current) { + saveEdit(editingRowRef.current) + } + }} + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault() + if (editingRowRef.current) { + saveEdit(editingRowRef.current) + } + } + if (e.key === "Escape") { + e.preventDefault() + cancelEdit() + } + }} placeholder="手机号" className="w-32" + autoFocus /> ) } @@ -392,6 +348,23 @@ export default function UserPage() { setEditEmail(e.target.value)} + onBlur={() => { + if (editingRowRef.current) { + saveEdit(editingRowRef.current) + } + }} + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault() + if (editingRowRef.current) { + saveEdit(editingRowRef.current) + } + } + if (e.key === "Escape") { + e.preventDefault() + cancelEdit() + } + }} placeholder="邮箱" className="w-40" /> @@ -401,6 +374,19 @@ export default function UserPage() { }, }, { header: "姓名", accessorKey: "name" }, + { + header: "客户来源", + accessorKey: "source", + cell: ({ row }) => { + const sourceMap: Record = { + 0: "官网注册", + 1: "管理员添加", + 2: "代理商注册", + 3: "代理商添加", + } + return sourceMap[row.original.source] ?? "未知" + }, + }, { header: "余额", accessorKey: "balance", @@ -417,6 +403,7 @@ export default function UserPage() { ) }, }, + { header: "折扣", accessorKey: "discount.name" }, { header: "实名状态", accessorKey: "id_type", @@ -447,7 +434,6 @@ export default function UserPage() { cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"), }, { header: "联系方式", accessorKey: "contact_wechat" }, - { header: "客户来源", accessorKey: "" }, { header: "客户经理", accessorKey: "admin.name" }, { header: "最后登录时间", @@ -506,103 +492,12 @@ export default function UserPage() { ]} /> - {/* 添加用户对话框 */} - - - - 添加用户 - - - ( - - 账号 * - - {fieldState.error?.message} - - )} - /> - ( - - 密码 * - - {fieldState.error?.message} - - )} - /> - ( - - 手机号 * - - {fieldState.error?.message} - - )} - /> - ( - - 姓名 - - {fieldState.error?.message} - - )} - /> - ( - - 邮箱 - - {fieldState.error?.message} - - )} - /> - ( - - 微信/联系方式 - - {fieldState.error?.message} - - )} - /> - - - - - - - + +
) } diff --git a/src/app/(root)/user/page.tsx b/src/app/(root)/user/page.tsx index 881fee4..d0f4e3b 100644 --- a/src/app/(root)/user/page.tsx +++ b/src/app/(root)/user/page.tsx @@ -15,13 +15,6 @@ import { FieldLabel, } from "@/components/ui/field" import { Input } from "@/components/ui/input" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" import { useFetch } from "@/hooks/data" import type { User } from "@/models/user" @@ -67,6 +60,7 @@ export default function UserPage() { done: "用户已认领", fail: "用户认领失败", }) + console.log(table, "table") const onFilter = handleSubmit(data => { const result: FilterValues = {} @@ -94,10 +88,10 @@ export default function UserPage() { render={({ field, fieldState }) => ( - 账号 - + 账号/手机号/邮箱 + {fieldState.error?.message} )} @@ -111,14 +105,14 @@ export default function UserPage() { data-invalid={fieldState.invalid} className="w-40 flex-none" > - 用户名-手机号-邮箱 - + 姓名 + {fieldState.error?.message} )} /> - ( @@ -179,7 +173,7 @@ export default function UserPage() { {fieldState.error?.message} )} - /> + /> */} @@ -253,6 +247,19 @@ export default function UserPage() { return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : "-" }, }, + { + header: "客户来源", + accessorKey: "source", + cell: ({ row }) => { + const sourceMap: Record = { + 0: "官网注册", + 1: "管理员添加", + 2: "代理商注册", + 3: "代理商添加", + } + return sourceMap[row.original.source] ?? "未知" + }, + }, { header: "账号状态", accessorKey: "status", diff --git a/src/models/cust.ts b/src/models/cust.ts index fcca484..3ef0446 100644 --- a/src/models/cust.ts +++ b/src/models/cust.ts @@ -1,8 +1,11 @@ +import type { ProductDiscount } from "./product_discount" + export type Cust = { id: number admin_id?: number phone: string admin?: Admin + source: number has_password: boolean username: string email: string @@ -21,6 +24,7 @@ export type Cust = { last_login_ip: string created_at: Date updated_at: Date + discount: ProductDiscount } export type Admin = { name: string diff --git a/src/models/user.ts b/src/models/user.ts index 6b706ad..21efc6f 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -4,7 +4,8 @@ export type User = { id: number admin_id?: number admin?: Admin - phone: string + account: string + source: number has_password: boolean username: string email: string