From 76d2d480eded822d6e86477e2d6b8e19c1a29e1c Mon Sep 17 00:00:00 2001 From: Eamon <17516219072@163.com> Date: Fri, 27 Mar 2026 15:51:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BC=98=E6=83=A0=E5=88=B8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20&=20=E6=9B=B4=E6=96=B0=E6=8A=98=E6=89=A3?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/coupon.ts | 34 +++++ src/actions/product_discount.ts | 10 +- src/actions/user.ts | 2 +- src/app/(root)/appbar.tsx | 4 + src/app/(root)/coupon/create.tsx | 202 +++++++++++++++++++++++++ src/app/(root)/coupon/page.tsx | 148 ++++++++++++++++++ src/app/(root)/coupon/update.tsx | 247 +++++++++++++++++++++++++++++++ src/app/(root)/navigation.tsx | 5 +- src/app/(root)/user/page.tsx | 22 +-- src/models/coupon.ts | 12 ++ 10 files changed, 667 insertions(+), 19 deletions(-) create mode 100644 src/actions/coupon.ts create mode 100644 src/app/(root)/coupon/create.tsx create mode 100644 src/app/(root)/coupon/page.tsx create mode 100644 src/app/(root)/coupon/update.tsx create mode 100644 src/models/coupon.ts diff --git a/src/actions/coupon.ts b/src/actions/coupon.ts new file mode 100644 index 0000000..c41f276 --- /dev/null +++ b/src/actions/coupon.ts @@ -0,0 +1,34 @@ +import type { PageRecord } from "@/lib/api" +import type { Coupon } from "@/models/coupon" +import { callByUser } from "./base" + +export async function getPagCoupon(params: { page: number; size: number }) { + return callByUser>("/api/admin/coupon/page", params) +} + +export async function createCoupon(data: { + code: string + amount: number + remark?: string + min_amount?: number + expire_at?: Date +}) { + return callByUser("/api/admin/coupon/create", data) +} + +export async function updateCoupon(data: { + code: string + amount: number + remark?: string + min_amount?: number + expire_at?: Date + status?: number +}) { + return callByUser("/api/admin/coupon/update", data) +} + +export async function deleteCoupon(id: number) { + return callByUser("/api/admin/coupon/remove", { + id, + }) +} diff --git a/src/actions/product_discount.ts b/src/actions/product_discount.ts index 7c7c244..5fcb37c 100644 --- a/src/actions/product_discount.ts +++ b/src/actions/product_discount.ts @@ -5,7 +5,7 @@ import type { ProductDiscount } from "@/models/product_discount" import { callByUser } from "./base" export async function getAllProductDiscount() { - return callByUser("/api/admin/product/discount/all") + return callByUser("/api/admin/discount/all") } export async function getPageProductDiscount(params: { @@ -13,7 +13,7 @@ export async function getPageProductDiscount(params: { size: number }) { return callByUser>( - "/api/admin/product/discount/page", + "/api/admin/discount/page", params, ) } @@ -22,7 +22,7 @@ export async function createProductDiscount(data: { name: string discount: string }) { - return callByUser("/api/admin/product/discount/create", { + return callByUser("/api/admin/discount/create", { name: data.name, discount: Number(data.discount), }) @@ -33,7 +33,7 @@ export async function updateProductDiscount(data: { name?: string discount?: string }) { - return callByUser("/api/admin/product/discount/update", { + return callByUser("/api/admin/discount/update", { id: data.id, name: data.name, discount: data.discount ? Number(data.discount) : undefined, @@ -41,7 +41,7 @@ export async function updateProductDiscount(data: { } export async function deleteProductDiscount(id: number) { - return callByUser("/api/admin/product/discount/remove", { + return callByUser("/api/admin/discount/remove", { id, }) } diff --git a/src/actions/user.ts b/src/actions/user.ts index b6b8b47..3ed5037 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -8,7 +8,7 @@ export async function getPageUsers(params: { page: number; size: number }) { export async function bindAdmin(params: { id: number - phone?: string + account?: string name?: string identified?: boolean enabled?: boolean diff --git a/src/app/(root)/appbar.tsx b/src/app/(root)/appbar.tsx index c373106..69f42b6 100644 --- a/src/app/(root)/appbar.tsx +++ b/src/app/(root)/appbar.tsx @@ -108,6 +108,10 @@ export default function Appbar(props: { admin: Admin }) { batch: "提取记录", channel: "IP管理", pools: "IP池管理", + coupon: "优惠券", + admin: "管理员", + permissions: "权限列表", + discount: "折扣管理", } return labels[path] || path diff --git a/src/app/(root)/coupon/create.tsx b/src/app/(root)/coupon/create.tsx new file mode 100644 index 0000000..d8c12ea --- /dev/null +++ b/src/app/(root)/coupon/create.tsx @@ -0,0 +1,202 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Plus } from "lucide-react" +import { useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import z from "zod" +import { createCoupon } from "@/actions/coupon" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" + +const schema = z.object({ + code: z.string().min(1, "请输入优惠券名称"), + amount: z.string().min(1, "请输入优惠券金额"), + remark: z.string().optional(), + min_amount: z.string().optional(), + expire_at: z.string().optional(), +}) + +export function CreateDiscount(props: { onSuccess?: () => void }) { + const [open, setOpen] = useState(false) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + code: "", + remark: "", + amount: "0", + min_amount: "0", + expire_at: "", + }, + mode: "onChange", + }) + + const { control, handleSubmit, reset } = form + + const onSubmit = async (data: z.infer) => { + try { + const payload = { + code: data.code, + amount: Number(data.amount), + remark: data?.remark, + min_amount: Number(data?.min_amount), + expire_at: data?.expire_at ? new Date(data.expire_at) : undefined, + } + const resp = await createCoupon(payload) + if (resp.success) { + reset() + toast.success("优惠券创建成功") + props.onSuccess?.() + setOpen(false) + } else { + toast.error(resp.message) + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } + } + + return ( + { + setOpen(newOpen) + if (!newOpen) { + reset() + } + }} + > + + + + + + + 创建优惠券 + + +
+ + ( +
+ 名称: +
+ + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 备注: +
+ + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 金额: +
+ { + const value = e.target.value + if (value === "" || parseFloat(value) >= 0) { + field.onChange(value) + } + }} + /> + {fieldState.error?.message} +
+
+ )} + /> + ( +
+ 最低消费: +
+ { + const value = e.target.value + if (value === "" || parseFloat(value) >= 0) { + field.onChange(value) + } + }} + /> + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 过期时间: + +
+ {fieldState.error?.message} +
+
+ )} + /> +
+
+ + + + + + + +
+
+ ) +} diff --git a/src/app/(root)/coupon/page.tsx b/src/app/(root)/coupon/page.tsx new file mode 100644 index 0000000..1e054a1 --- /dev/null +++ b/src/app/(root)/coupon/page.tsx @@ -0,0 +1,148 @@ +"use client" +import { format } from "date-fns" +import { Suspense, useState } from "react" +import { toast } from "sonner" +import { deleteCoupon, getPagCoupon } from "@/actions/coupon" +import { DataTable, useDataTable } from "@/components/data-table" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import type { Coupon } from "@/models/coupon" +import { CreateDiscount } from "./create" +import { UpdateCoupon } from "./update" + +export default function CouponPage() { + const table = useDataTable((page, size) => getPagCoupon({ page, size })) + + return ( +
+
+
+ +
+
+ + + + {...table} + columns={[ + { header: "ID", accessorKey: "id" }, + { header: "所属用户", accessorKey: "user_id" }, + { header: "代码", accessorKey: "code" }, + { header: "备注", accessorKey: "remark" }, + { header: "金额", accessorKey: "amount" }, + { header: "最低消费金额", accessorKey: "min_amount" }, + { + header: "状态", + accessorKey: "status", + cell: ({ row }) => { + const status = row.original.status + if (status === 0) { + return 未使用 + } + if (status === 1) { + return 已使用 + } + return - + }, + }, + { + header: "过期时间", + accessorKey: "expire_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + { + header: "更新时间", + accessorKey: "updated_at", + cell: ({ row }) => + format(new Date(row.original.updated_at), "yyyy-MM-dd HH:mm"), + }, + { + header: "操作", + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]} + /> +
+
+ ) +} + +function DeleteCoupon({ + coupon, + onSuccess, +}: { + coupon: Coupon + onSuccess?: () => void +}) { + const [loading, setLoading] = useState(false) + + const handleConfirm = async () => { + setLoading(true) + try { + const resp = await deleteCoupon(coupon.id) + if (resp.success) { + toast.success("删除成功") + onSuccess?.() + } else { + toast.error(resp.message ?? "删除失败") + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } finally { + setLoading(false) + } + } + + return ( + + + + + + + 确认删除 + + 确定要删除折扣「{coupon.code}」吗?此操作不可撤销。 + + + + 取消 + + 删除 + + + + + ) +} diff --git a/src/app/(root)/coupon/update.tsx b/src/app/(root)/coupon/update.tsx new file mode 100644 index 0000000..48d310c --- /dev/null +++ b/src/app/(root)/coupon/update.tsx @@ -0,0 +1,247 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import z from "zod" +import { updateCoupon } from "@/actions/coupon" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { 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 { Coupon } from "@/models/coupon" + +const schema = z.object({ + code: z.string().min(1, "请输入优惠券名称"), + amount: z.string().min(1, "请输入优惠券金额"), + remark: z.string().optional(), + min_amount: z.string().optional(), + expire_at: z.string().optional(), + status: z.string().optional(), +}) + +export function UpdateCoupon(props: { + coupon: Coupon + onSuccess?: () => void +}) { + const [open, setOpen] = useState(false) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + code: props.coupon.code || "", + remark: props.coupon.remark || "", + amount: String(props.coupon.amount || 0), + min_amount: String(props.coupon.min_amount || 0), + expire_at: props.coupon.expire_at + ? new Date(props.coupon.expire_at).toISOString().split("T")[0] + : "", + status: String(props.coupon.status || "0"), + }, + mode: "onChange", + }) + + const { control, handleSubmit, reset } = form + + const onSubmit = async (data: z.infer) => { + try { + const payload = { + id: props.coupon.id, + code: data.code, + amount: Number(data.amount), + remark: data.remark, + min_amount: Number(data.min_amount), + expire_at: data.expire_at ? new Date(data.expire_at) : undefined, + status: Number(data.status), + } + const resp = await updateCoupon(payload) + if (resp.success) { + toast.success("优惠券修改成功") + props.onSuccess?.() + setOpen(false) + } else { + toast.error(resp.message) + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } + } + + const handleOpenChange = (value: boolean) => { + if (value) { + reset({ + code: props.coupon.code || "", + remark: props.coupon.remark || "", + amount: String(props.coupon.amount || 0), + min_amount: String(props.coupon.min_amount || 0), + expire_at: props.coupon.expire_at + ? new Date(props.coupon.expire_at).toISOString().split("T")[0] + : "", + status: String(props.coupon.status || "0"), + }) + } + setOpen(value) + } + + return ( + + + + + + + + 修改优惠券 + + +
+ + ( +
+ 名称: +
+ + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 备注: +
+ + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 金额: +
+ { + const value = e.target.value + if (value === "" || parseFloat(value) >= 0) { + field.onChange(value) + } + }} + /> + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 最低消费: +
+ { + const value = e.target.value + if (value === "" || parseFloat(value) >= 0) { + field.onChange(value) + } + }} + /> + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 过期时间: +
+ + {fieldState.error?.message} +
+
+ )} + /> + + ( +
+ 状态: +
+ + {fieldState.error?.message} +
+
+ )} + /> +
+
+ + + + + + + +
+
+ ) +} diff --git a/src/app/(root)/navigation.tsx b/src/app/(root)/navigation.tsx index 85986f7..4c204c1 100644 --- a/src/app/(root)/navigation.tsx +++ b/src/app/(root)/navigation.tsx @@ -16,7 +16,7 @@ import { Shield, ShoppingBag, SquarePercent, - SquarePercentIcon, + TicketPercent, Users, } from "lucide-react" import Link from "next/link" @@ -187,9 +187,9 @@ export default function Navigation() { {/* 客户 */} + - @@ -202,6 +202,7 @@ export default function Navigation() { icon={SquarePercent} label="折扣管理" /> + diff --git a/src/app/(root)/user/page.tsx b/src/app/(root)/user/page.tsx index 15a00e3..881fee4 100644 --- a/src/app/(root)/user/page.tsx +++ b/src/app/(root)/user/page.tsx @@ -26,7 +26,7 @@ import { useFetch } from "@/hooks/data" import type { User } from "@/models/user" type FilterValues = { - phone?: string + account?: string name?: string identified?: boolean enabled?: boolean @@ -34,7 +34,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(), @@ -49,7 +49,7 @@ export default function UserPage() { const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(filterSchema), defaultValues: { - phone: "", + account: "", name: "", identified: "all", enabled: "all", @@ -71,7 +71,7 @@ export default function UserPage() { 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" @@ -89,15 +89,15 @@ export default function UserPage() {
( - 手机号 - + 账号 + {fieldState.error?.message} )} @@ -111,8 +111,8 @@ export default function UserPage() { data-invalid={fieldState.invalid} className="w-40 flex-none" > - 账户 - + 用户名-手机号-邮箱 + {fieldState.error?.message} )} @@ -189,7 +189,7 @@ export default function UserPage() { variant="outline" onClick={() => { reset({ - phone: "", + account: "", name: "", identified: "all", enabled: "all", @@ -260,7 +260,7 @@ export default function UserPage() { }, { header: "联系方式", accessorKey: "contact_wechat" }, { - header: "管理员", + header: "客户经理", cell: ({ row }) => row.original.admin?.name || "-", }, { diff --git a/src/models/coupon.ts b/src/models/coupon.ts new file mode 100644 index 0000000..29b068f --- /dev/null +++ b/src/models/coupon.ts @@ -0,0 +1,12 @@ +export type Coupon = { + id: number + created_at: Date + updated_at: Date + user_id: number + code: string + remark: string + amount: number + min_amount: number + status: number + expire_at: Date +}