添加优惠券发放的功能 & 切换后台线上和线下首页区别
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import { ComputerIcon } from "lucide-react"
|
||||
import { getNodeEnv } from "@/actions/env"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
109
src/app/(root)/client/coupon/coupon.tsx
Normal file
109
src/app/(root)/client/coupon/coupon.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
275
src/app/(root)/client/coupon/page.tsx
Normal file
275
src/app/(root)/client/coupon/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user