添加优惠券发放的功能 & 切换后台线上和线下首页区别

This commit is contained in:
Eamon
2026-05-08 16:02:48 +08:00
parent 7cd1a7cbe7
commit 85f9e68e32
9 changed files with 409 additions and 25 deletions

View File

@@ -39,9 +39,20 @@ export async function deleteCoupon(id: number) {
})
}
export async function issueCoupon(data: {
export async function getReleaseCoupon(data: {
coupon_id: number
user_id: number
}) {
return callByUser<Coupon>("/api/admin/coupon/update/assign", data)
}
export async function getUserCoupon(data: {
page: number
size: number
user_id: number
}) {
return callByUser<PageRecord<Coupon>>(
"/api/admin/coupon-user/page/of-user",
data,
)
}

View File

@@ -24,7 +24,7 @@ import {
} from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { createContext, type ReactNode, useContext, useState } from "react"
import { createContext, type ReactNode, Suspense, useContext, useState } from "react"
import { twJoin } from "tailwind-merge"
import { Auth } from "@/components/auth"
import { Button } from "@/components/ui/button"
@@ -307,7 +307,7 @@ export default function Navigation() {
)}
>
{/*Logo 区域 */}
<Logo collapsed={collapsed} />
<Suspense><Logo collapsed={collapsed} /></Suspense>
{/* Navigation Menu */}
<ScrollArea className="flex-1 py-3 overflow-hidden">

View File

@@ -1,3 +1,4 @@
import { ComputerIcon } from "lucide-react"
import { getNodeEnv } from "@/actions/env"
import { cn } from "@/lib/utils"

View File

@@ -0,0 +1,109 @@
"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>
)
}

View File

@@ -0,0 +1,275 @@
"use client"
import { format } from "date-fns"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useState } from "react"
import { toast } from "sonner"
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"
export default function CouponPage() {
const router = useRouter()
const searchParams = useSearchParams()
const userId = searchParams.get("userId")
const userPhone = searchParams.get("phone")
const table = useDataTable<Coupon>((page, size) => {
return getUserCoupon({
user_id: Number(userId),
page,
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 (
<Page>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button onClick={() => router.back()} className="gap-2">
</Button>
<span className="text-gray-600">: {userPhone}</span>
</div>
</div>
<Suspense>
<DataTable<Coupon>
{...table}
columns={[
{ header: "优惠券名称", accessorKey: "name" },
{ header: "券码", accessorKey: "code" },
{ header: "金额", accessorKey: "amount" },
{ header: "最低消费", accessorKey: "min_amount" },
{
header: "状态",
accessorKey: "coupon_status",
},
{
header: "过期信息",
id: "expire",
cell: ({ row }) => expireLabel(row.original),
},
{
header: "发放时间",
accessorKey: "issued_at",
// cell: ({ row }) =>
// format(new Date(row.original.issued_at), "yyyy-MM-dd HH:mm"),
},
{
header: "备注",
accessorKey: "remark",
cell: ({ row }) => (
<span className="text-gray-500">
{row.original.remark || "-"}
</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>
</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>
)
}

View File

@@ -1,10 +0,0 @@
"use client"
import { useRouter, useSearchParams } from "next/navigation"
export default function IssuePage() {
const router = useRouter()
const searchParams = useSearchParams()
const userId = searchParams.get("userId")
const userPhone = searchParams.get("phone")
return <div></div>
}

View File

@@ -21,7 +21,7 @@ import { Button } from "@/components/ui/button"
import { ScopeCouponWriteAssign } from "@/lib/scopes"
import type { Coupon } from "@/models/coupon"
import { CreateDiscount } from "./create"
import { IssueCoupon } from "./issue"
import { ReleaseCoupon } from "./release"
import { UpdateCoupon } from "./update"
export default function CouponPage() {
@@ -114,7 +114,7 @@ export default function CouponPage() {
onSuccess={table.refresh}
/>
<Auth scope={ScopeCouponWriteAssign}>
<IssueCoupon
<ReleaseCoupon
coupon={row.original}
onSuccess={table.refresh}
/>

View File

@@ -4,7 +4,7 @@ import { Suspense, useCallback, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import z from "zod"
import { issueCoupon } from "@/actions/coupon"
import { getReleaseCoupon } from "@/actions/coupon"
import { getPageUser } from "@/actions/user"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@@ -33,7 +33,10 @@ const filterSchema = z.object({
})
type FormValues = z.infer<typeof filterSchema>
export function IssueCoupon(props: { coupon: Coupon; onSuccess?: () => void }) {
export function ReleaseCoupon(props: {
coupon: Coupon
onSuccess?: () => void
}) {
const [open, setOpen] = useState(false)
const [userList, setUserList] = useState<User[]>([])
const [loading, setLoading] = useState(false)
@@ -104,7 +107,7 @@ export function IssueCoupon(props: { coupon: Coupon; onSuccess?: () => void }) {
return
}
try {
const result = await issueCoupon({
const result = await getReleaseCoupon({
coupon_id: coupon.id,
user_id: targetUser.id,
})

View File

@@ -18,12 +18,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
Select,
@@ -452,7 +447,7 @@ export default function CustPage() {
<DropdownMenuItem
onClick={() =>
router.push(
`/client/issue?userId=${user.id}&phone=${user.phone}`,
`/client/coupon?userId=${user.id}&phone=${user.phone}`,
)
}
>