实现价格折扣动态调整

This commit is contained in:
2026-03-24 17:14:50 +08:00
parent 8751ac19a6
commit 523d46874b
13 changed files with 1189 additions and 56 deletions

View File

@@ -0,0 +1,122 @@
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 { createProductDiscount } from "@/actions/product_discount"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
const schema = z.object({
name: z.string().min(1, "请输入折扣名称"),
discount: z.string().min(1, "请输入折扣代码"),
})
export function CreateDiscount(props: { onSuccess?: () => void }) {
const [open, setOpen] = useState(false)
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
discount: "",
},
})
const onSubmit = async (data: z.infer<typeof schema>) => {
try {
const resp = await createProductDiscount(data)
if (resp.success) {
form.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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form id="discount-create" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
control={form.control}
name="name"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="discount-create-name"></FieldLabel>
<Input
id="discount-create-name"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
control={form.control}
name="discount"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="discount-create-discount">
</FieldLabel>
<Input
id="discount-create-discount"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost"></Button>
</DialogClose>
<Button type="submit" form="discount-create">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,131 @@
"use client"
import { format } from "date-fns"
import { Suspense, useState } from "react"
import { toast } from "sonner"
import {
deleteProductDiscount,
getPageProductDiscount,
} from "@/actions/product_discount"
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 { ProductDiscount } from "@/models/product_discount"
import { CreateDiscount } from "./create"
import { UpdateDiscount } from "./update"
export default function DiscountPage() {
const table = useDataTable((page, size) =>
getPageProductDiscount({ page, size }),
)
return (
<div className="flex flex-col gap-3">
{/* 操作栏 */}
<div className="flex justify-between items-stretch">
<div className="flex gap-3">
<CreateDiscount onSuccess={table.refresh} />
</div>
</div>
{/* 数据表 */}
<Suspense>
<DataTable<ProductDiscount>
{...table}
columns={[
{ header: "名称", accessorKey: "name" },
{ header: "折扣", accessorKey: "discount" },
{
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 }) => (
<div className="flex gap-2">
<UpdateDiscount
discount={row.original}
onSuccess={table.refresh}
/>
<DeleteButton
discount={row.original}
onSuccess={table.refresh}
/>
</div>
),
},
]}
/>
</Suspense>
</div>
)
}
function DeleteButton({
discount,
onSuccess,
}: {
discount: ProductDiscount
onSuccess?: () => void
}) {
const [loading, setLoading] = useState(false)
const handleConfirm = async () => {
setLoading(true)
try {
const resp = await deleteProductDiscount(discount.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 (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" variant="destructive" disabled={loading}>
</Button>
</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{discount.name}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleConfirm}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -0,0 +1,140 @@
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 { updateProductDiscount } from "@/actions/product_discount"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import type { ProductDiscount } from "@/models/product_discount"
const schema = z.object({
name: z.string().min(1, "请输入折扣名称"),
discount: z.string().min(1, "请输入折扣代码"),
})
export function UpdateDiscount(props: {
discount: ProductDiscount
onSuccess?: () => void
}) {
const [open, setOpen] = useState(false)
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: props.discount.name,
discount: String(props.discount.discount),
},
})
const onSubmit = async (data: z.infer<typeof schema>) => {
try {
const resp = await updateProductDiscount({
id: props.discount.id,
...data,
})
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) {
form.reset({
name: props.discount.name,
discount: String(props.discount.discount),
})
}
setOpen(value)
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button size="sm" variant="secondary">
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form id="discount-update" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
control={form.control}
name="name"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="discount-update-name"></FieldLabel>
<Input
id="discount-update-name"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
control={form.control}
name="discount"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="discount-update-discount">
</FieldLabel>
<Input
id="discount-update-discount"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost"></Button>
</DialogClose>
<Button type="submit" form="discount-update">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}