Files
admin/src/app/(root)/product/page.tsx

205 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { format } from "date-fns"
import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
import { toast } from "sonner"
import {
deleteProductSku,
getAllProduct,
getPageProductSku,
} from "@/actions/product"
import { DataTable, useDataTable } from "@/components/data-table"
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 { cn } from "@/lib/utils"
import type { Product } from "@/models/product"
import type { ProductSku } from "@/models/product_sku"
import { BatchUpdateDiscount } from "./batch-discount"
import { CreateProductSku } from "./create"
import { UpdateProductSku } from "./update"
export default function ProductPage() {
const [selected, setSelected] = useState<number | undefined>(undefined)
return (
<div className="size-full flex gap-6 items-stretch">
<Products selected={selected} onSelect={setSelected} />
<ProductSkus selected={selected} />
</div>
)
}
function Products(props: {
selected?: number
onSelect?: (id: number) => void
}) {
const [list, setList] = useState<Product[]>([])
const refresh = useCallback(async () => {
const resp = await getAllProduct()
if (resp.success) {
setList(resp.data)
}
}, [])
const selected = useMemo(() => {
return list.find(item => item.id === props.selected)
}, [list, props.selected])
useEffect(() => {
refresh()
}, [refresh])
return (
<section className="flex-none basis-64 bg-background rounded-lg">
<header className="pl-3 pr-1 h-10 border-b flex items-center justify-between">
<h3 className="text-sm"></h3>
</header>
<ul className="flex flex-col gap-1 py-1">
{list.map(item => (
<li key={item.id} className="px-1">
<Button
variant="ghost"
className={cn(
"size-full box-border p-2 rounded-md flex justify-between items-center select-none",
selected?.id === item.id && "bg-primary/20",
)}
onClick={() => props.onSelect?.(item.id)}
>
<div>
<p>{item.name}</p>
<p className="text-sm text-gray-500">{item.description}</p>
</div>
<Badge className="bg-green-600/60"></Badge>
</Button>
</li>
))}
</ul>
</section>
)
}
function ProductSkus(props: { selected?: number }) {
const action = useCallback(
(page: number, size: number) => {
return getPageProductSku({ page, size, product_id: props.selected })
},
[props.selected],
)
const table = useDataTable(action)
return (
<div className="flex-auto overflow-hidden flex flex-col items-stretch gap-3">
<div className="flex gap-3">
<CreateProductSku
productId={props.selected ?? 0}
onSuccess={table.refresh}
/>
<BatchUpdateDiscount
productId={props.selected ?? 0}
onSuccess={table.refresh}
/>
</div>
<Suspense>
<DataTable<ProductSku>
classNames={{
root: "overflow-auto",
}}
{...table}
columns={[
{ header: "套餐编码", accessorKey: "code" },
{ header: "套餐名称", accessorKey: "name" },
{ header: "单价", accessorKey: "price" },
{ header: "折扣", accessorFn: row => row.discount?.name ?? "—" },
{
header: "最终价格",
accessorFn: row =>
row.discount
? (Number(row.price) * Number(row.discount.discount)) / 100
: Number(row.price),
},
{
header: "创建时间",
accessorFn: row => format(row.created_at, "yyyy-MM-dd HH:mm"),
},
{
header: "更新时间",
accessorFn: row => format(row.updated_at, "yyyy-MM-dd HH:mm"),
},
{
header: "操作",
cell: ({ row }) => (
<div className="flex gap-1">
<UpdateProductSku
sku={row.original}
onSuccess={table.refresh}
/>
<DeleteButton sku={row.original} onSuccess={table.refresh} />
</div>
),
},
]}
/>
</Suspense>
</div>
)
}
function DeleteButton(props: { sku: ProductSku; onSuccess?: () => void }) {
const [loading, setLoading] = useState(false)
const handleConfirm = async () => {
setLoading(true)
try {
const resp = await deleteProductSku(props.sku.id)
if (resp.success) {
toast.success("删除成功")
props.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>
{props.sku.name}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleConfirm}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}