From 035ac9266a6f71833843c870f37ee53b8bbd0a23 Mon Sep 17 00:00:00 2001 From: JetSprow Date: Fri, 1 May 2026 03:16:11 +1000 Subject: [PATCH] fix: submit recharge card form --- .../_components/recharge-card-form.tsx | 43 +++++- .../(admin)/admin/commerce/commerce-data.ts | 20 ++- src/app/(admin)/admin/commerce/page.tsx | 42 ++++-- .../wallet/_components/wallet-actions.tsx | 4 +- src/components/shared/pagination.tsx | 135 ++++++++++++------ src/lib/utils.ts | 9 +- 6 files changed, 182 insertions(+), 71 deletions(-) diff --git a/src/app/(admin)/admin/commerce/_components/recharge-card-form.tsx b/src/app/(admin)/admin/commerce/_components/recharge-card-form.tsx index 5490235..7773ed7 100644 --- a/src/app/(admin)/admin/commerce/_components/recharge-card-form.tsx +++ b/src/app/(admin)/admin/commerce/_components/recharge-card-form.tsx @@ -22,6 +22,7 @@ export function RechargeCardForm({ plans }: { plans: PlanOption[] }) { const router = useRouter(); const [type, setType] = useState<"BALANCE" | "PLAN">("BALANCE"); const [planId, setPlanId] = useState(plans[0]?.id ?? ""); + const [hasExpiry, setHasExpiry] = useState(false); const [pending, startTransition] = useTransition(); const selectedPlan = plans.find((plan) => plan.id === planId) ?? null; const planSoldOut = type === "PLAN" && selectedPlan?.remainingCount === 0; @@ -40,6 +41,7 @@ export function RechargeCardForm({ plans }: { plans: PlanOption[] }) { await createAdminRechargeCards(formData); toast.success("充值卡已生成"); form.reset(); + setHasExpiry(false); router.refresh(); } catch (error) { toast.error(getErrorMessage(error, "生成充值卡失败")); @@ -93,12 +95,19 @@ export function RechargeCardForm({ plans }: { plans: PlanOption[] }) { + + - diff --git a/src/app/(admin)/admin/commerce/commerce-data.ts b/src/app/(admin)/admin/commerce/commerce-data.ts index 3ac2aa5..ac507ec 100644 --- a/src/app/(admin)/admin/commerce/commerce-data.ts +++ b/src/app/(admin)/admin/commerce/commerce-data.ts @@ -1,4 +1,5 @@ import { prisma } from "@/lib/prisma"; +import { parsePage } from "@/lib/utils"; import { getPlanAvailability } from "@/services/plan-availability"; function getRechargeCardPlanRemaining( @@ -15,8 +16,11 @@ function getRechargeCardPlanRemaining( return limits.length > 0 ? Math.min(...limits) : null; } -export async function getCommerceData() { - const [coupons, promotions, rechargeCards, planRows] = await Promise.all([ +export async function getCommerceData( + searchParams: Record = {}, +) { + const { page, skip, pageSize } = parsePage(searchParams, 20); + const [coupons, promotions, rechargeCards, rechargeCardTotal, planRows] = await Promise.all([ prisma.coupon.findMany({ orderBy: { createdAt: "desc" }, include: { _count: { select: { orders: true, grants: true } } }, @@ -32,8 +36,10 @@ export async function getCommerceData() { plan: { select: { name: true, type: true } }, redeemedBy: { select: { email: true } }, }, - take: 50, + skip, + take: pageSize, }), + prisma.rechargeCard.count(), prisma.subscriptionPlan.findMany({ where: { isActive: true }, orderBy: [{ type: "asc" }, { sortOrder: "asc" }, { createdAt: "desc" }], @@ -60,5 +66,11 @@ export async function getCommerceData() { }), ); - return { coupons, promotions, rechargeCards, plans }; + return { + coupons, + promotions, + rechargeCards, + rechargeCardPagination: { total: rechargeCardTotal, page, pageSize }, + plans, + }; } diff --git a/src/app/(admin)/admin/commerce/page.tsx b/src/app/(admin)/admin/commerce/page.tsx index d1fd519..75ee816 100644 --- a/src/app/(admin)/admin/commerce/page.tsx +++ b/src/app/(admin)/admin/commerce/page.tsx @@ -4,6 +4,7 @@ import { createCoupon, createPromotionRule } from "@/actions/admin/commerce"; import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge"; import { CopyButton } from "@/components/shared/copy-button"; import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell"; +import { Pagination } from "@/components/shared/pagination"; import { PendingSubmitButton } from "@/components/shared/pending-submit-button"; import { BooleanToggle } from "@/components/ui/boolean-toggle"; import { Input } from "@/components/ui/input"; @@ -28,8 +29,25 @@ export const metadata: Metadata = { description: "管理优惠券与满减规则。", }; -export default async function AdminCommercePage() { - const { coupons, promotions, rechargeCards, plans } = await getCommerceData(); +const rechargeCardStatusLabels: Record = { + UNUSED: "未使用", + REDEEMED: "已兑换", + EXPIRED: "已过期", + DISABLED: "已停用", +}; + +function normalizeCommerceTab(value: string | string[] | undefined) { + return value === "manage" || value === "cards" ? value : "create"; +} + +export default async function AdminCommercePage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const params = await searchParams; + const activeTab = normalizeCommerceTab(params.tab); + const { coupons, promotions, rechargeCards, rechargeCardPagination, plans } = await getCommerceData(params); return ( @@ -38,7 +56,7 @@ export default async function AdminCommercePage() { title="优惠与奖励" /> - + 新建规则 规则列表 @@ -185,7 +203,7 @@ export default async function AdminCommercePage() {
- +
{rechargeCards.map((card) => (
@@ -197,13 +215,7 @@ export default async function AdminCommercePage() {

{card.code}

- {card.status === "UNUSED" - ? "未使用" - : card.status === "REDEEMED" - ? "已兑换" - : card.status === "EXPIRED" - ? "已过期" - : "已停用"} + {rechargeCardStatusLabels[card.status] ?? "未知状态"}

@@ -216,7 +228,7 @@ export default async function AdminCommercePage() {

{card.batchName && {card.batchName}} {card.redeemedBy && {card.redeemedBy.email}} - {card.expiresAt && 到期 {formatDate(card.expiresAt)}} + {card.expiresAt ? `到期 ${formatDate(card.expiresAt)}` : "永不过期"}
@@ -227,6 +239,12 @@ export default async function AdminCommercePage() {

暂无充值卡

)}
+
diff --git a/src/app/(user)/wallet/_components/wallet-actions.tsx b/src/app/(user)/wallet/_components/wallet-actions.tsx index d64cee4..257c90e 100644 --- a/src/app/(user)/wallet/_components/wallet-actions.tsx +++ b/src/app/(user)/wallet/_components/wallet-actions.tsx @@ -46,7 +46,7 @@ export function WalletActions() {
- @@ -82,7 +82,7 @@ export function WalletActions() { - diff --git a/src/components/shared/pagination.tsx b/src/components/shared/pagination.tsx index 7e3ab26..1c711c9 100644 --- a/src/components/shared/pagination.tsx +++ b/src/components/shared/pagination.tsx @@ -3,75 +3,116 @@ import { usePathname, useSearchParams, useRouter } from "next/navigation"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; interface PaginationProps { total: number; pageSize: number; page: number; + pageSizeOptions?: number[]; + fixedParams?: Record; } -export function Pagination({ total, pageSize, page }: PaginationProps) { +const defaultPageSizeOptions = [10, 20, 30, 50, 100]; + +export function Pagination({ + total, + pageSize, + page, + pageSizeOptions = defaultPageSizeOptions, + fixedParams, +}: PaginationProps) { const pathname = usePathname(); const searchParams = useSearchParams(); const router = useRouter(); const totalPages = Math.ceil(total / pageSize); - if (totalPages <= 1) return null; + if (total === 0) return null; function go(p: number) { const params = new URLSearchParams(searchParams.toString()); + for (const [key, value] of Object.entries(fixedParams ?? {})) { + params.set(key, value); + } params.set("page", String(p)); router.push(`${pathname}?${params.toString()}`); } + function changePageSize(nextPageSize: string | null) { + if (!nextPageSize) return; + const params = new URLSearchParams(searchParams.toString()); + for (const [key, value] of Object.entries(fixedParams ?? {})) { + params.set(key, value); + } + params.set("page", "1"); + params.set("pageSize", nextPageSize); + router.push(`${pathname}?${params.toString()}`); + } + return (
-

- 共 {total} 条,第 {page}/{totalPages} 页 -

-
- - {Array.from({ length: totalPages }, (_, i) => i + 1) - .filter((pageNumber) => pageNumber === 1 || pageNumber === totalPages || Math.abs(pageNumber - page) <= 1) - .reduce<(number | "...")[]>((acc, pageNumber, index, pages) => { - if (index > 0 && pageNumber - (pages[index - 1] as number) > 1) acc.push("..."); - acc.push(pageNumber); - return acc; - }, []) - .map((pageNumber, index) => - pageNumber === "..." ? ( - ... - ) : ( - - ), - )} - +
+

+ 共 {total} 条,第 {page}/{totalPages || 1} 页 +

+
+ {totalPages > 1 && ( +
+ + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter((pageNumber) => pageNumber === 1 || pageNumber === totalPages || Math.abs(pageNumber - page) <= 1) + .reduce<(number | "...")[]>((acc, pageNumber, index, pages) => { + if (index > 0 && pageNumber - (pages[index - 1] as number) > 1) acc.push("..."); + acc.push(pageNumber); + return acc; + }, []) + .map((pageNumber, index) => + pageNumber === "..." ? ( + ... + ) : ( + + ), + )} + +
+ )}
); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3fbaf1a..14aa47a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -32,8 +32,15 @@ export function formatDateShort(date: Date | string): string { return format(new Date(date), "yyyy-MM-dd", { locale: zhCN }); } -export function parsePage(searchParams: Record, pageSize = 20) { +const PAGE_SIZE_OPTIONS = [10, 20, 30, 50, 100] as const; + +export function parsePage(searchParams: Record, defaultPageSize = 20) { const raw = searchParams.page; + const rawPageSize = searchParams.pageSize; + const requestedPageSize = parseInt(typeof rawPageSize === "string" ? rawPageSize : "", 10); + const pageSize = PAGE_SIZE_OPTIONS.includes(requestedPageSize as (typeof PAGE_SIZE_OPTIONS)[number]) + ? requestedPageSize + : defaultPageSize; const page = Math.max(1, parseInt(typeof raw === "string" ? raw : "1", 10) || 1); const skip = (page - 1) * pageSize; return { page, skip, pageSize };