From e0bdcabbfe12e74d4049830966ac65ef6124ec26 Mon Sep 17 00:00:00 2001 From: Eamon <17516219072@163.com> Date: Mon, 25 May 2026 18:12:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BC=98=E6=83=A0=E5=88=B8?= =?UTF-8?q?=E5=8F=91=E6=94=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/coupon.ts | 4 + src/app/(root)/_navigation/index.tsx | 6 + src/app/(root)/appbar.tsx | 1 + src/app/(root)/client/coupon/coupon.tsx | 109 ----------- src/app/(root)/client/coupon/page.tsx | 234 ++---------------------- src/app/(root)/coupon/page.tsx | 54 ++---- src/app/(root)/couponList/page.tsx | 64 +++++++ src/models/coupon.ts | 95 ++++++++++ src/models/formatDate.ts | 11 ++ 9 files changed, 210 insertions(+), 368 deletions(-) delete mode 100644 src/app/(root)/client/coupon/coupon.tsx create mode 100644 src/app/(root)/couponList/page.tsx create mode 100644 src/models/formatDate.ts diff --git a/src/actions/coupon.ts b/src/actions/coupon.ts index 6a17a89..92ed1d9 100644 --- a/src/actions/coupon.ts +++ b/src/actions/coupon.ts @@ -56,3 +56,7 @@ export async function getUserCoupon(data: { data, ) } + +export async function getCouponList(data: { page: number; size: number }) { + return callByUser>("/api/admin/coupon-user/page ", data) +} diff --git a/src/app/(root)/_navigation/index.tsx b/src/app/(root)/_navigation/index.tsx index adfbfaf..eedf1b7 100644 --- a/src/app/(root)/_navigation/index.tsx +++ b/src/app/(root)/_navigation/index.tsx @@ -18,6 +18,7 @@ import { Shield, ShoppingBag, SquarePercent, + TicketCheck, TicketPercent, Users, } from "lucide-react" @@ -228,6 +229,11 @@ const menuSections: { title: string; items: NavItemProps[] }[] = [ label: "优惠券", requiredScope: ScopeCouponRead, }, + { + href: "/couponList", + icon: TicketCheck, + label: "已发放优惠券", + }, { href: "/resources", icon: Package, diff --git a/src/app/(root)/appbar.tsx b/src/app/(root)/appbar.tsx index fe19e72..97eab58 100644 --- a/src/app/(root)/appbar.tsx +++ b/src/app/(root)/appbar.tsx @@ -81,6 +81,7 @@ export default function Appbar(props: { admin: Admin }) { statistics: "数据统计", balance: "余额明细", gateway: "网关列表", + couponList: "已发放优惠券", } return labels[path] || path diff --git a/src/app/(root)/client/coupon/coupon.tsx b/src/app/(root)/client/coupon/coupon.tsx deleted file mode 100644 index dab463b..0000000 --- a/src/app/(root)/client/coupon/coupon.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client" -import { useState } from "react" -import { toast } from "sonner" -import { getPagCoupon } from "@/actions/coupon" -import { DataTable, useDataTable } from "@/components/data-table" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import type { Coupon } from "@/models/coupon" - -export function IssueCouponForUser(props: { - userId: number - userPhone: string - onSuccess?: () => void -}) { - const [open, setOpen] = useState(false) - - const table = useDataTable((page, size) => - getPagCoupon({ page, size }), - ) - - const handleIssue = async (coupon: Coupon) => { - // try { - // const result = await issueCouponToUser({ - // user_id: props.userId, - // coupon_id: coupon.id, - // coupon_name: coupon.name, - // }) - // if (result.success) { - // toast.success(`成功发放「${coupon.name}」给用户 ${props.userPhone}`) - // props.onSuccess?.() - // setOpen(false) - // } else { - // toast.error(result.message || "发放失败") - // } - // } catch (error) { - // const message = error instanceof Error ? error.message : "发放失败" - // toast.error(`发放优惠券失败: ${message}`) - // } - } - - return ( - { - setOpen(newOpen) - if (newOpen) { - table.refresh() - } - }} - > - - - - - - 发放优惠券给 {props.userPhone} - - - {...table} - columns={[ - { header: "优惠券名称", accessorKey: "name" }, - { 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 - - }, - }, - { - id: "action", - meta: { pin: "right" }, - header: "操作", - cell: ({ row }) => ( - - ), - }, - ]} - /> - - - - - - - - ) -} diff --git a/src/app/(root)/client/coupon/page.tsx b/src/app/(root)/client/coupon/page.tsx index adad755..b6fa758 100644 --- a/src/app/(root)/client/coupon/page.tsx +++ b/src/app/(root)/client/coupon/page.tsx @@ -1,41 +1,12 @@ "use client" -import { format } from "date-fns" import { useRouter, useSearchParams } from "next/navigation" -import { Suspense, useState } from "react" -import { toast } from "sonner" +import { Suspense } from "react" import { getUserCoupon } from "@/actions/coupon" import { DataTable, useDataTable } from "@/components/data-table" import { Page } from "@/components/page" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog" -import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import type { Coupon } from "@/models/coupon" +import { type Coupon, getStatus } from "@/models/coupon" +import { formatDate } from "@/models/formatDate" export default function CouponPage() { const router = useRouter() @@ -50,22 +21,6 @@ export default function CouponPage() { size, }) }) - console.log(table, "table") - - const expireLabel = (coupon: Coupon) => { - switch (coupon.expire_type) { - case 0: - return "永久有效" - case 1: - return coupon.expire_at - ? format(new Date(coupon.expire_at), "yyyy-MM-dd") - : "" - case 2: - return coupon.expire_in ? `发放后${coupon.expire_in}天` : "" - default: - return "" - } - } return ( @@ -82,194 +37,39 @@ export default function CouponPage() { {...table} columns={[ - { header: "优惠券名称", accessorKey: "name" }, - { header: "券码", accessorKey: "code" }, - { header: "金额", accessorKey: "amount" }, - { header: "最低消费", accessorKey: "min_amount" }, + { header: "优惠券名称", accessorKey: "coupon.name" }, + { header: "金额", accessorKey: "coupon.amount" }, + { header: "最低消费", accessorKey: "coupon.min_amount" }, { - header: "状态", - accessorKey: "coupon_status", + header: "优惠券使用状态", + accessorKey: "coupon.status", + cell: ({ row }) => { + const { text, color } = getStatus(row.original.status, "use") + return {text} + }, }, { header: "过期信息", - id: "expire", - cell: ({ row }) => expireLabel(row.original), + accessorKey: "expire_at", + cell: ({ row }) => formatDate(row.original.expire_at), }, { header: "发放时间", - accessorKey: "issued_at", - // cell: ({ row }) => - // format(new Date(row.original.issued_at), "yyyy-MM-dd HH:mm:ss"), + accessorKey: "coupon.created_at", + cell: ({ row }) => formatDate(row.original.coupon.created_at), }, { header: "备注", accessorKey: "remark", cell: ({ row }) => ( - {row.original.remark || "-"} + {row.original.remark || ""} ), }, - { - id: "action", - meta: { pin: "right" }, - header: "操作", - cell: ({ row }) => ( -
- - -
- ), - }, ]} />
) } - -function UpdateStatusDialog({ - coupon, - userId, - onSuccess, -}: { - coupon: Coupon - userId: number - onSuccess?: () => void -}) { - const [open, setOpen] = useState(false) - const [selectedStatus, setSelectedStatus] = - useState( - // String(coupon.coupon_status), - ) - const [loading, setLoading] = useState(false) - - const handleConfirm = async () => { - // setLoading(true) - // try { - // const status = Number(selectedStatus) - // const result = await updateUserCouponStatus({ - // user_id: userId, - // coupon_id: coupon.id, - // coupon_status: status, - // }) - // if (result.success) { - // toast.success("状态更新成功") - // onSuccess?.() - // setOpen(false) - // } else { - // toast.error(result.message || "更新失败") - // } - // } catch (error) { - // const message = error instanceof Error ? error.message : "更新失败" - // toast.error(`状态更新失败: ${message}`) - // } finally { - // setLoading(false) - // } - } - - return ( - - - - - - - 修改优惠券状态 - -
-

- 优惠券: {coupon.name} ({coupon.code}) -

- -
- - - - - - -
-
- ) -} - -function RevokeDialog({ - coupon, - userId, - onSuccess, -}: { - coupon: Coupon - userId: number - onSuccess?: () => void -}) { - const [loading, setLoading] = useState(false) - - const handleConfirm = async () => { - // setLoading(true) - // try { - // const result = await revokeUserCoupon({ - // user_id: userId, - // coupon_id: coupon.id, - // }) - // if (result.success) { - // toast.success("撤销成功") - // onSuccess?.() - // } else { - // toast.error(result.message || "撤销失败") - // } - // } catch (error) { - // const message = error instanceof Error ? error.message : "撤销失败" - // toast.error(`撤销失败: ${message}`) - // } finally { - // setLoading(false) - // } - } - - return ( - - - - - - - 确认撤销 - - 确定要撤销「{coupon.name}」({coupon.code}) - 吗?此操作将标记优惠券为已撤销状态。 - - - - 取消 - - 撤销 - - - - - ) -} diff --git a/src/app/(root)/coupon/page.tsx b/src/app/(root)/coupon/page.tsx index da48c31..729afff 100644 --- a/src/app/(root)/coupon/page.tsx +++ b/src/app/(root)/coupon/page.tsx @@ -1,5 +1,4 @@ "use client" -import { format } from "date-fns" import { Suspense, useState } from "react" import { toast } from "sonner" import { deleteCoupon, getPagCoupon } from "@/actions/coupon" @@ -19,7 +18,8 @@ import { } from "@/components/ui/alert-dialog" import { Button } from "@/components/ui/button" import { ScopeCouponWriteAssign } from "@/lib/scopes" -import type { Coupon } from "@/models/coupon" +import { type Coupon, getExpireType, getStatus } from "@/models/coupon" +import { formatDate } from "@/models/formatDate" import { CreateDiscount } from "./create" import { ReleaseCoupon } from "./release" import { UpdateCoupon } from "./update" @@ -47,34 +47,13 @@ export default function CouponPage() { header: "优惠券状态", accessorKey: "status", cell: ({ row }) => { - const status = row.original.status - if (status === 0) { - return 禁用 - } - if (status === 1) { - return 正常 - } - return - + const { text, color } = getStatus(row.original.status, "coupon") + return {text} }, }, { header: "过期类型", - accessorFn: row => { - switch (row.expire_type) { - case 0: - return "不过期" - case 1: - return "固定日期" - case 2: - return "相对日期" - default: - return "" - } - }, - }, - { - header: "过期时长(天)", - accessorKey: "expire_in", + cell: ({ row }) => getExpireType(row.original.expire_type), }, { header: "过期时间", @@ -82,16 +61,11 @@ export default function CouponPage() { cell: ({ row }) => { const coupon = row.original if (coupon.expire_type === 2 && coupon.expire_in) { - const expireDate = new Date(coupon.created_at) - expireDate.setDate(expireDate.getDate() + coupon.expire_in) - return format(expireDate, "yyyy-MM-dd HH:mm:ss") + return `${coupon.expire_in}天` } if (coupon.expire_type === 1 && coupon.expire_at) { - return format( - new Date(coupon.expire_at), - "yyyy-MM-dd HH:mm:ss", - ) + return formatDate(row.original.expire_at) } return 永久有效 }, @@ -99,11 +73,7 @@ export default function CouponPage() { { header: "创建时间", accessorKey: "created_at", - cell: ({ row }) => - format( - new Date(row.original.created_at), - "yyyy-MM-dd HH:mm:ss", - ), + cell: ({ row }) => formatDate(row.original.created_at), }, { id: "action", @@ -115,16 +85,16 @@ export default function CouponPage() { coupon={row.original} onSuccess={table.refresh} /> - + ), }, diff --git a/src/app/(root)/couponList/page.tsx b/src/app/(root)/couponList/page.tsx new file mode 100644 index 0000000..b623aee --- /dev/null +++ b/src/app/(root)/couponList/page.tsx @@ -0,0 +1,64 @@ +"use client" +import { Suspense } from "react" +import { getCouponList } from "@/actions/coupon" +import { DataTable, useDataTable } from "@/components/data-table" +import { Page } from "@/components/page" +import { type Coupon, getExpireTypeText, getStatus } from "@/models/coupon" +import { formatDate } from "@/models/formatDate" + +export default function CouponList() { + const table = useDataTable((page, size) => getCouponList({ page, size })) + console.log(table, "table") + + return ( + + + + {...table} + columns={[ + { header: "优惠券名称", accessorKey: "coupon.name" }, + // { header: "优惠券数量", accessorKey: "coupon.count" }, + { header: "优惠券金额", accessorKey: "coupon.amount" }, + { header: "最低消费金额", accessorKey: "coupon.min_amount" }, + { header: "用户", accessorKey: "user.name" }, + // { + // header: "优惠券状态", + // cell: ({ row }) => { + // const { text, color } = getStatus( + // row.original.coupon.status, + // "coupon", + // ) + // return {text} + // }, + // }, + { + header: "使用状态", + cell: ({ row }) => { + const { text, color } = getStatus(row.original.status, "use") + return {text} + }, + }, + { + header: "过期类型", + cell: ({ row }) => { + const expireType = row.original.coupon.expire_type + const expireAt = row.original.expire_at + return getExpireTypeText(expireType, expireAt) + }, + }, + { + header: "过期时间", + accessorKey: "expire_at", + cell: ({ row }) => formatDate(row.original.expire_at), + }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => formatDate(row.original.created_at), + }, + ]} + /> + + + ) +} diff --git a/src/models/coupon.ts b/src/models/coupon.ts index 25010a5..83941b9 100644 --- a/src/models/coupon.ts +++ b/src/models/coupon.ts @@ -1,3 +1,5 @@ +import type { User } from "./user" + export type Coupon = { id: number name: string @@ -13,4 +15,97 @@ export type Coupon = { updated_at: Date expire_at: Date expire_in: number + coupon: useCoupon + user: User +} +type useCoupon = { + id: number + name: string + expire_type: number + status: number + created_at: Date +} + +// 优惠券使用状态 +export const couponUseStatusMap = { + 0: { text: "未使用", color: "text-green-600" }, + 1: { text: "已使用", color: "text-blue-600" }, + 2: { text: "已禁用", color: "text-green-600" }, +} as const + +// 优惠券状态 +export const couponStatusMap = { + 0: { text: "禁用", color: "text-yellow-600" }, + 1: { text: "正常", color: "text-green-600" }, +} as const + +// 优惠券过期类型 +export const expireTypeMap = { + 0: "不过期", + 1: "固定日期", + 2: "相对日期", +} as const + +// 优惠券状态 & 使用状态 +export const getStatus = (status: number, type: "coupon" | "use") => { + if (type === "coupon") { + return ( + couponStatusMap[status as keyof typeof couponStatusMap] || { + text: "", + color: "text-gray-400", + } + ) + } + return ( + couponUseStatusMap[status as keyof typeof couponUseStatusMap] || { + text: "", + color: "text-gray-400", + } + ) +} + +export const getDaysToExpire = (expireAt: Date | string): number => { + if (!expireAt) return 0 + + const targetDate = new Date(expireAt) + const now = new Date() + + const targetDay = new Date( + targetDate.getFullYear(), + targetDate.getMonth(), + targetDate.getDate(), + ) + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + + const diffTime = targetDay.getTime() - today.getTime() + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + + return diffDays +} + +// 过期类型 +export const getExpireType = (expireType: number): string => { + return expireTypeMap[expireType as keyof typeof expireTypeMap] || "" +} + +// 获取过期类型显示(带天数) +export const getExpireTypeText = ( + expireType: number, + expireAt: Date, +): string => { + const typeText = getExpireType(expireType) + console.log(typeText, "typeText") + + if (expireType === 0) return typeText + + const days = getDaysToExpire(expireAt) + console.log(days, "days") + + if (days === 0) { + return `${typeText}` + } else if (days > 0) { + return `${typeText}(${days}天后)` + } else { + return `${typeText}(已过期${Math.abs(days)}天)` + } } diff --git a/src/models/formatDate.ts b/src/models/formatDate.ts new file mode 100644 index 0000000..ecac21c --- /dev/null +++ b/src/models/formatDate.ts @@ -0,0 +1,11 @@ +import { format } from "date-fns" + +export const formatDate = ( + date: Date | string | null | undefined, + formatStr: string = "yyyy-MM-dd HH:mm:ss", +): string => { + if (!date) return "" + const d = new Date(date) + if (!Number.isNaN(d.getTime())) return format(d, formatStr) + return "" +}