完善优惠券发放功能
This commit is contained in:
@@ -56,3 +56,7 @@ export async function getUserCoupon(data: {
|
|||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCouponList(data: { page: number; size: number }) {
|
||||||
|
return callByUser<PageRecord<Coupon>>("/api/admin/coupon-user/page ", data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
SquarePercent,
|
SquarePercent,
|
||||||
|
TicketCheck,
|
||||||
TicketPercent,
|
TicketPercent,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
@@ -228,6 +229,11 @@ const menuSections: { title: string; items: NavItemProps[] }[] = [
|
|||||||
label: "优惠券",
|
label: "优惠券",
|
||||||
requiredScope: ScopeCouponRead,
|
requiredScope: ScopeCouponRead,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/couponList",
|
||||||
|
icon: TicketCheck,
|
||||||
|
label: "已发放优惠券",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "/resources",
|
href: "/resources",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default function Appbar(props: { admin: Admin }) {
|
|||||||
statistics: "数据统计",
|
statistics: "数据统计",
|
||||||
balance: "余额明细",
|
balance: "余额明细",
|
||||||
gateway: "网关列表",
|
gateway: "网关列表",
|
||||||
|
couponList: "已发放优惠券",
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels[path] || path
|
return labels[path] || path
|
||||||
|
|||||||
@@ -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<Coupon>((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 (
|
|
||||||
<Dialog
|
|
||||||
open={open}
|
|
||||||
onOpenChange={newOpen => {
|
|
||||||
setOpen(newOpen)
|
|
||||||
if (newOpen) {
|
|
||||||
table.refresh()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>发放优惠券</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
className="max-h-[85vh] overflow-y-auto"
|
|
||||||
style={{ width: "auto", minWidth: "800px", maxWidth: "90vw" }}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>发放优惠券给 {props.userPhone}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DataTable<Coupon>
|
|
||||||
{...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 <span className="text-yellow-600">禁用</span>
|
|
||||||
}
|
|
||||||
if (status === 1) {
|
|
||||||
return <span className="text-green-600">正常</span>
|
|
||||||
}
|
|
||||||
return <span>-</span>
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "action",
|
|
||||||
meta: { pin: "right" },
|
|
||||||
header: "操作",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button size="sm" onClick={() => handleIssue(row.original)}>
|
|
||||||
发放
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="ghost">取消</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,41 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { format } from "date-fns"
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { Suspense, useState } from "react"
|
import { Suspense } from "react"
|
||||||
import { toast } from "sonner"
|
|
||||||
import { getUserCoupon } from "@/actions/coupon"
|
import { getUserCoupon } from "@/actions/coupon"
|
||||||
import { DataTable, useDataTable } from "@/components/data-table"
|
import { DataTable, useDataTable } from "@/components/data-table"
|
||||||
import { Page } from "@/components/page"
|
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 { Button } from "@/components/ui/button"
|
||||||
import {
|
import { type Coupon, getStatus } from "@/models/coupon"
|
||||||
Dialog,
|
import { formatDate } from "@/models/formatDate"
|
||||||
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"
|
|
||||||
|
|
||||||
export default function CouponPage() {
|
export default function CouponPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -50,22 +21,6 @@ export default function CouponPage() {
|
|||||||
size,
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -82,194 +37,39 @@ export default function CouponPage() {
|
|||||||
<DataTable<Coupon>
|
<DataTable<Coupon>
|
||||||
{...table}
|
{...table}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: "优惠券名称", accessorKey: "name" },
|
{ header: "优惠券名称", accessorKey: "coupon.name" },
|
||||||
{ header: "券码", accessorKey: "code" },
|
{ header: "金额", accessorKey: "coupon.amount" },
|
||||||
{ header: "金额", accessorKey: "amount" },
|
{ header: "最低消费", accessorKey: "coupon.min_amount" },
|
||||||
{ header: "最低消费", accessorKey: "min_amount" },
|
|
||||||
{
|
{
|
||||||
header: "状态",
|
header: "优惠券使用状态",
|
||||||
accessorKey: "coupon_status",
|
accessorKey: "coupon.status",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { text, color } = getStatus(row.original.status, "use")
|
||||||
|
return <span className={color}>{text}</span>
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "过期信息",
|
header: "过期信息",
|
||||||
id: "expire",
|
accessorKey: "expire_at",
|
||||||
cell: ({ row }) => expireLabel(row.original),
|
cell: ({ row }) => formatDate(row.original.expire_at),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "发放时间",
|
header: "发放时间",
|
||||||
accessorKey: "issued_at",
|
accessorKey: "coupon.created_at",
|
||||||
// cell: ({ row }) =>
|
cell: ({ row }) => formatDate(row.original.coupon.created_at),
|
||||||
// format(new Date(row.original.issued_at), "yyyy-MM-dd HH:mm:ss"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "备注",
|
header: "备注",
|
||||||
accessorKey: "remark",
|
accessorKey: "remark",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="text-gray-500">
|
<span className="text-gray-500">
|
||||||
{row.original.remark || "-"}
|
{row.original.remark || ""}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "action",
|
|
||||||
meta: { pin: "right" },
|
|
||||||
header: "操作",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<UpdateStatusDialog
|
|
||||||
coupon={row.original}
|
|
||||||
userId={Number(userId)}
|
|
||||||
onSuccess={table.refresh}
|
|
||||||
/>
|
|
||||||
<RevokeDialog
|
|
||||||
coupon={row.original}
|
|
||||||
userId={Number(userId)}
|
|
||||||
onSuccess={table.refresh}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button size="sm" variant="secondary">
|
|
||||||
修改状态
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-sm">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>修改优惠券状态</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="py-4">
|
|
||||||
<p className="text-sm text-gray-600 mb-3">
|
|
||||||
优惠券: {coupon.name} ({coupon.code})
|
|
||||||
</p>
|
|
||||||
<Select value={selectedStatus}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="请选择状态" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="1">未使用</SelectItem>
|
|
||||||
<SelectItem value="2">已使用</SelectItem>
|
|
||||||
<SelectItem value="3">已过期</SelectItem>
|
|
||||||
<SelectItem value="0">已撤销</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="ghost">取消</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button onClick={handleConfirm} disabled={loading}>
|
|
||||||
确认
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button size="sm" variant="destructive" disabled={loading}>
|
|
||||||
撤销
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent size="sm">
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>确认撤销</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
确定要撤销「{coupon.name}」({coupon.code})
|
|
||||||
吗?此操作将标记优惠券为已撤销状态。
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
|
||||||
<AlertDialogAction variant="destructive" onClick={handleConfirm}>
|
|
||||||
撤销
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { format } from "date-fns"
|
|
||||||
import { Suspense, useState } from "react"
|
import { Suspense, useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { deleteCoupon, getPagCoupon } from "@/actions/coupon"
|
import { deleteCoupon, getPagCoupon } from "@/actions/coupon"
|
||||||
@@ -19,7 +18,8 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { ScopeCouponWriteAssign } from "@/lib/scopes"
|
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 { CreateDiscount } from "./create"
|
||||||
import { ReleaseCoupon } from "./release"
|
import { ReleaseCoupon } from "./release"
|
||||||
import { UpdateCoupon } from "./update"
|
import { UpdateCoupon } from "./update"
|
||||||
@@ -47,34 +47,13 @@ export default function CouponPage() {
|
|||||||
header: "优惠券状态",
|
header: "优惠券状态",
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.original.status
|
const { text, color } = getStatus(row.original.status, "coupon")
|
||||||
if (status === 0) {
|
return <span className={color}>{text}</span>
|
||||||
return <span className="text-yellow-600">禁用</span>
|
|
||||||
}
|
|
||||||
if (status === 1) {
|
|
||||||
return <span className="text-green-600">正常</span>
|
|
||||||
}
|
|
||||||
return <span>-</span>
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "过期类型",
|
header: "过期类型",
|
||||||
accessorFn: row => {
|
cell: ({ row }) => getExpireType(row.original.expire_type),
|
||||||
switch (row.expire_type) {
|
|
||||||
case 0:
|
|
||||||
return "不过期"
|
|
||||||
case 1:
|
|
||||||
return "固定日期"
|
|
||||||
case 2:
|
|
||||||
return "相对日期"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "过期时长(天)",
|
|
||||||
accessorKey: "expire_in",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "过期时间",
|
header: "过期时间",
|
||||||
@@ -82,16 +61,11 @@ export default function CouponPage() {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const coupon = row.original
|
const coupon = row.original
|
||||||
if (coupon.expire_type === 2 && coupon.expire_in) {
|
if (coupon.expire_type === 2 && coupon.expire_in) {
|
||||||
const expireDate = new Date(coupon.created_at)
|
return `${coupon.expire_in}天`
|
||||||
expireDate.setDate(expireDate.getDate() + coupon.expire_in)
|
|
||||||
return format(expireDate, "yyyy-MM-dd HH:mm:ss")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coupon.expire_type === 1 && coupon.expire_at) {
|
if (coupon.expire_type === 1 && coupon.expire_at) {
|
||||||
return format(
|
return formatDate(row.original.expire_at)
|
||||||
new Date(coupon.expire_at),
|
|
||||||
"yyyy-MM-dd HH:mm:ss",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return <span>永久有效</span>
|
return <span>永久有效</span>
|
||||||
},
|
},
|
||||||
@@ -99,11 +73,7 @@ export default function CouponPage() {
|
|||||||
{
|
{
|
||||||
header: "创建时间",
|
header: "创建时间",
|
||||||
accessorKey: "created_at",
|
accessorKey: "created_at",
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) => formatDate(row.original.created_at),
|
||||||
format(
|
|
||||||
new Date(row.original.created_at),
|
|
||||||
"yyyy-MM-dd HH:mm:ss",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "action",
|
id: "action",
|
||||||
@@ -115,16 +85,16 @@ export default function CouponPage() {
|
|||||||
coupon={row.original}
|
coupon={row.original}
|
||||||
onSuccess={table.refresh}
|
onSuccess={table.refresh}
|
||||||
/>
|
/>
|
||||||
<DeleteCoupon
|
|
||||||
coupon={row.original}
|
|
||||||
onSuccess={table.refresh}
|
|
||||||
/>
|
|
||||||
<Auth scope={ScopeCouponWriteAssign}>
|
<Auth scope={ScopeCouponWriteAssign}>
|
||||||
<ReleaseCoupon
|
<ReleaseCoupon
|
||||||
coupon={row.original}
|
coupon={row.original}
|
||||||
onSuccess={table.refresh}
|
onSuccess={table.refresh}
|
||||||
/>
|
/>
|
||||||
</Auth>
|
</Auth>
|
||||||
|
<DeleteCoupon
|
||||||
|
coupon={row.original}
|
||||||
|
onSuccess={table.refresh}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
64
src/app/(root)/couponList/page.tsx
Normal file
64
src/app/(root)/couponList/page.tsx
Normal file
@@ -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 (
|
||||||
|
<Page>
|
||||||
|
<Suspense>
|
||||||
|
<DataTable<Coupon>
|
||||||
|
{...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 <span className={color}>{text}</span>
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
header: "使用状态",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { text, color } = getStatus(row.original.status, "use")
|
||||||
|
return <span className={color}>{text}</span>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { User } from "./user"
|
||||||
|
|
||||||
export type Coupon = {
|
export type Coupon = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -13,4 +15,97 @@ export type Coupon = {
|
|||||||
updated_at: Date
|
updated_at: Date
|
||||||
expire_at: Date
|
expire_at: Date
|
||||||
expire_in: number
|
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)}天)`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/models/formatDate.ts
Normal file
11
src/models/formatDate.ts
Normal file
@@ -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 ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user