套餐编码可视化改造

This commit is contained in:
2026-04-08 15:41:32 +08:00
parent 0789462a8d
commit 30af977543
8 changed files with 332 additions and 56 deletions

View File

@@ -5,6 +5,7 @@ import { toast } from "sonner"
import z from "zod"
import { createProductSku } from "@/actions/product"
import { getAllProductDiscount } from "@/actions/product_discount"
import { ProductCodeField } from "@/components/products"
import { Button } from "@/components/ui/button"
import {
Dialog,
@@ -20,6 +21,7 @@ import {
FieldError,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
@@ -30,6 +32,7 @@ import {
SelectValue,
} from "@/components/ui/select"
import type { ProductDiscount } from "@/models/product_discount"
import type { SelectedProduct } from "./type"
const schema = z.object({
code: z.string().min(1, "请输入套餐编码"),
@@ -45,7 +48,7 @@ const schema = z.object({
})
export function CreateProductSku(props: {
productId: number
product?: SelectedProduct
onSuccess?: () => void
}) {
const [open, setOpen] = useState(false)
@@ -74,9 +77,11 @@ export function CreateProductSku(props: {
}, [open])
const onSubmit = async (data: z.infer<typeof schema>) => {
if (!props.product) return
try {
const resp = await createProductSku({
product_id: props.productId,
product_id: props.product.id,
code: data.code,
name: data.name,
price: data.price,
@@ -109,7 +114,7 @@ export function CreateProductSku(props: {
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button disabled={!props.productId}></Button>
<Button disabled={!props.product}></Button>
</DialogTrigger>
<DialogContent>
@@ -119,25 +124,6 @@ export function CreateProductSku(props: {
<form id="sku-create" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
control={form.control}
name="code"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="sku-create-code"></FieldLabel>
<Input
id="sku-create-code"
placeholder="请输入套餐编码"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
control={form.control}
name="name"
@@ -203,6 +189,16 @@ export function CreateProductSku(props: {
</Field>
)}
/>
<FieldSeparator />
{props.product && (
<ProductCodeField
control={form.control}
name="code"
code={props.product.code}
/>
)}
</FieldGroup>
</form>

View File

@@ -9,6 +9,7 @@ import {
getPageProductSku,
} from "@/actions/product"
import { DataTable, useDataTable } from "@/components/data-table"
import { SkuCodeBadge } from "@/components/products/format"
import {
AlertDialog,
AlertDialogAction,
@@ -22,15 +23,19 @@ import {
} from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import type { ProductCode } from "@/lib/base"
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 type { SelectedProduct } from "./type"
import { UpdateProductSku } from "./update"
export default function ProductPage() {
const [selected, setSelected] = useState<number | undefined>(undefined)
const [selected, setSelected] = useState<SelectedProduct | undefined>(
undefined,
)
return (
<div className="size-full flex gap-6 items-stretch">
@@ -41,8 +46,8 @@ export default function ProductPage() {
}
function Products(props: {
selected?: number
onSelect?: (id: number) => void
selected?: SelectedProduct
onSelect?: (id: SelectedProduct) => void
}) {
const [list, setList] = useState<Product[]>([])
@@ -54,7 +59,7 @@ function Products(props: {
}, [])
const selected = useMemo(() => {
return list.find(item => item.id === props.selected)
return list.find(item => item.id === props.selected?.id)
}, [list, props.selected])
useEffect(() => {
@@ -75,7 +80,7 @@ function Products(props: {
"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)}
onClick={() => props.onSelect?.({ id: item.id, code: item.code })}
>
<div>
<p>{item.name}</p>
@@ -90,10 +95,15 @@ function Products(props: {
)
}
function ProductSkus(props: { selected?: number }) {
function ProductSkus(props: {
selected?: {
id: number
code: ProductCode
}
}) {
const action = useCallback(
(page: number, size: number) =>
getPageProductSku({ page, size, product_id: props.selected }),
getPageProductSku({ page, size, product_id: props.selected?.id }),
[props.selected],
)
@@ -102,12 +112,9 @@ function ProductSkus(props: { selected?: number }) {
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}
/>
<CreateProductSku product={props.selected} onSuccess={table.refresh} />
<BatchUpdateDiscount
productId={props.selected ?? 0}
productId={props.selected?.id ?? 0}
onSuccess={table.refresh}
/>
</div>
@@ -118,7 +125,18 @@ function ProductSkus(props: { selected?: number }) {
}}
{...table}
columns={[
{ header: "套餐编码", accessorKey: "code" },
{
header: "套餐编码",
cell: ({ row }) =>
row.original.product ? (
<SkuCodeBadge
productCode={row.original.product.code}
skuCode={row.original.code}
/>
) : (
row.original.code
),
},
{ header: "套餐名称", accessorKey: "name" },
{ header: "单价", accessorFn: row => Number(row.price).toFixed(2) },
{ header: "折扣", accessorFn: row => row.discount?.name ?? "—" },

View File

@@ -0,0 +1,6 @@
import type { ProductCode } from "@/lib/base"
export type SelectedProduct = {
id: number
code: ProductCode
}

View File

@@ -5,6 +5,7 @@ import { toast } from "sonner"
import z from "zod"
import { updateProductSku } from "@/actions/product"
import { getAllProductDiscount } from "@/actions/product_discount"
import { ProductCodeField } from "@/components/products"
import { Button } from "@/components/ui/button"
import {
Dialog,
@@ -20,6 +21,7 @@ import {
FieldError,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
@@ -124,25 +126,6 @@ export function UpdateProductSku(props: {
<form id="sku-update" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
control={form.control}
name="code"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="sku-update-code"></FieldLabel>
<Input
id="sku-update-code"
placeholder="请输入套餐编码"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
control={form.control}
name="name"
@@ -208,6 +191,16 @@ export function UpdateProductSku(props: {
</Field>
)}
/>
<FieldSeparator />
{props.sku.product && (
<ProductCodeField
control={form.control}
name="code"
code={props.sku.product.code}
/>
)}
</FieldGroup>
</form>