diff --git a/package.json b/package.json index 0a0cad6..8cbd417 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.1.0", "private": true, "scripts": { - "dev": "next dev -H 0.0.0.0 --turbopack", + "dev": "next dev -H 0.0.0.0 -p 3001 --turbopack", "build": "next build --turbopack", "lint": "biome check --write" }, diff --git a/src/app/(root)/product/create.tsx b/src/app/(root)/product/create.tsx index b2015db..bbfce20 100644 --- a/src/app/(root)/product/create.tsx +++ b/src/app/(root)/product/create.tsx @@ -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) => { + 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 ( - + @@ -119,25 +124,6 @@ export function CreateProductSku(props: {
- ( - - 套餐编码 - - {fieldState.invalid && ( - - )} - - )} - /> - )} /> + + + + {props.product && ( + + )}
diff --git a/src/app/(root)/product/page.tsx b/src/app/(root)/product/page.tsx index ef4066a..6d740c7 100644 --- a/src/app/(root)/product/page.tsx +++ b/src/app/(root)/product/page.tsx @@ -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(undefined) + const [selected, setSelected] = useState( + undefined, + ) return (
@@ -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([]) @@ -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 })} >

{item.name}

@@ -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 (
- +
@@ -118,7 +125,18 @@ function ProductSkus(props: { selected?: number }) { }} {...table} columns={[ - { header: "套餐编码", accessorKey: "code" }, + { + header: "套餐编码", + cell: ({ row }) => + row.original.product ? ( + + ) : ( + row.original.code + ), + }, { header: "套餐名称", accessorKey: "name" }, { header: "单价", accessorFn: row => Number(row.price).toFixed(2) }, { header: "折扣", accessorFn: row => row.discount?.name ?? "—" }, diff --git a/src/app/(root)/product/type.ts b/src/app/(root)/product/type.ts new file mode 100644 index 0000000..ade5369 --- /dev/null +++ b/src/app/(root)/product/type.ts @@ -0,0 +1,6 @@ +import type { ProductCode } from "@/lib/base" + +export type SelectedProduct = { + id: number + code: ProductCode +} diff --git a/src/app/(root)/product/update.tsx b/src/app/(root)/product/update.tsx index 73ffaaf..e0b1e4e 100644 --- a/src/app/(root)/product/update.tsx +++ b/src/app/(root)/product/update.tsx @@ -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: {
- ( - - 套餐编码 - - {fieldState.invalid && ( - - )} - - )} - /> - )} /> + + + + {props.sku.product && ( + + )}
diff --git a/src/components/products/format.tsx b/src/components/products/format.tsx new file mode 100644 index 0000000..339d218 --- /dev/null +++ b/src/components/products/format.tsx @@ -0,0 +1,62 @@ +import type { ReactNode } from "react" +import { Badge } from "@/components/ui/badge" +import { ProductCode } from "@/lib/base" + +interface SkuCodeBadgeProps { + productCode: ProductCode + skuCode: string +} + +export function SkuCodeBadge({ + productCode, + skuCode, +}: SkuCodeBadgeProps): ReactNode { + switch (productCode) { + case ProductCode.Short: + case ProductCode.Long: + return + default: + return {skuCode} + } +} + +function ParsedSkuCodeBadge({ skuCode }: { skuCode: string }): ReactNode { + const params = new URLSearchParams(skuCode) + const modeStr = params.get("mode") + + let mode: string | undefined + let modeClass: string | undefined + switch (modeStr) { + case "time": + mode = "包时" + modeClass = "bg-green-50" + break + case "quota": + mode = "包量" + modeClass = "bg-blue-50" + break + } + + const live = params.get("live") + const expire = params.get("expire") + + if (!mode || !live || !expire) { + return ( + + {skuCode}(解析失败) + + ) + } + + return ( +
+ + 类型:{mode} + + 有效时间:{live} 分钟 + {expire !== "0" && ( + 过期时间:{expire} 天 + )} +
+ ) +} diff --git a/src/components/products/index.tsx b/src/components/products/index.tsx new file mode 100644 index 0000000..c6fd1f2 --- /dev/null +++ b/src/components/products/index.tsx @@ -0,0 +1,200 @@ +import type { ChangeEvent } from "react" +import { + type Control, + type Path, + type UseControllerReturn, + useController, +} from "react-hook-form" +import { ProductCode } from "@/lib/base" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, + FieldLegend, +} from "../ui/field" +import { Input } from "../ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select" + +export function ProductCodeField< + T extends { + code: string + }, +>(props: { control: Control; name: Path; code: ProductCode }) { + const rt = useController(props) + + switch (props.code) { + case ProductCode.Short: + return + case ProductCode.Long: + return + } + + return null +} + +function ProductShortCode( + props: UseControllerReturn, +) { + const { field, fieldState } = props + + const params = new URLSearchParams(field.value) + const setParams = (data: { + mode?: string + live?: string + expire?: string + }) => { + if (data.mode) params.set("mode", data.mode) + if (data.live) params.set("live", data.live) + if (data.expire) params.set("expire", data.expire) + field.onChange(params.toString()) + } + + const onModeChange = (value: string) => { + setParams({ mode: value }) + } + const onLiveChange = (e: ChangeEvent) => { + let value = e.target.value || "0" + if (value.length > 1 && value[0] === "0") { + value = value.substring(1, value.length) + } + if (!/^([0-9]+)$/.test(value)) return + setParams({ live: value }) + } + const onExpireChange = (e: ChangeEvent) => { + let value = e.target.value || "0" + if (value.length > 1 && value[0] === "0") { + value = value.substring(1, value.length) + } + if (!/^([0-9]+)$/.test(value)) return + setParams({ expire: value }) + } + + return ( + + 短效套餐详情 + + + 套餐类型 + + + + 有效期(分钟) + + + {params.get("mode") === "time" && ( + + 过期时间(天) + + + )} + {fieldState.error && } + + + ) +} + +function ProductLongCode( + props: UseControllerReturn, +) { + const { field, fieldState } = props + + const params = new URLSearchParams(field.value) + const setParams = (data: { + mode?: string + live?: string + expire?: string + }) => { + if (data.mode) params.set("mode", data.mode) + if (data.live) params.set("live", data.live) + if (data.expire) params.set("expire", data.expire) + field.onChange(params.toString()) + } + + const onModeChange = (value: string) => { + setParams({ mode: value }) + } + const onLiveChange = (e: ChangeEvent) => { + let value = e.target.value || "0" + if (value.length > 1 && value[0] === "0") { + value = value.substring(1, value.length) + } + if (!/^([0-9]+)$/.test(value)) return + setParams({ live: value }) + } + const onExpireChange = (e: ChangeEvent) => { + let value = e.target.value || "0" + if (value.length > 1 && value[0] === "0") { + value = value.substring(1, value.length) + } + if (!/^([0-9]+)$/.test(value)) return + setParams({ expire: value }) + } + + return ( + + 长效套餐详情 + + + 套餐类型 + + + + 有效期(分钟) + + + {params.get("mode") === "time" && ( + + 过期时间(天) + + + )} + {fieldState.error && } + + + ) +} diff --git a/src/models/product.ts b/src/models/product.ts index 98eebce..aab8432 100644 --- a/src/models/product.ts +++ b/src/models/product.ts @@ -1,8 +1,9 @@ +import type { ProductCode } from "@/lib/base" import type { Model } from "./base/model" import type { ProductSku } from "./product_sku" export type Product = Model & { - code: string + code: ProductCode name: string description?: string sort: number