feat: add wallet and recharge cards

This commit is contained in:
JetSprow
2026-05-01 02:31:29 +10:00
parent 6d6489817d
commit 018bed3f36
32 changed files with 2058 additions and 170 deletions

View File

@@ -1,16 +1,19 @@
import type { Metadata } from "next";
import { Gift, Sparkles } from "lucide-react";
import { Gift, Sparkles, WalletCards } from "lucide-react";
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 { PendingSubmitButton } from "@/components/shared/pending-submit-button";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { formatDate } from "@/lib/utils";
import { getCommerceData } from "./commerce-data";
import { CommerceToggleButton } from "./_components/commerce-actions";
import { DiscountTypeSelect } from "./_components/discount-type-select";
import { RechargeCardForm } from "./_components/recharge-card-form";
function formatCouponDiscount(type: string, value: unknown) {
const numericValue = Number(value);
@@ -26,7 +29,7 @@ export const metadata: Metadata = {
};
export default async function AdminCommercePage() {
const { coupons, promotions } = await getCommerceData();
const { coupons, promotions, rechargeCards, plans } = await getCommerceData();
return (
<PageShell>
@@ -39,6 +42,7 @@ export default async function AdminCommercePage() {
<TabsList variant="line" className="surface-card p-1">
<TabsTrigger value="create"></TabsTrigger>
<TabsTrigger value="manage"></TabsTrigger>
<TabsTrigger value="cards"></TabsTrigger>
</TabsList>
<TabsContent value="create">
@@ -175,6 +179,57 @@ export default async function AdminCommercePage() {
</div>
</section>
</TabsContent>
<TabsContent value="cards" className="space-y-6">
<section className="grid gap-5 xl:grid-cols-[minmax(22rem,0.75fr)_1fr]">
<RechargeCardForm plans={plans} />
<div className="space-y-4">
<SectionHeader title="最近充值卡" />
<div className="surface-card divide-y divide-border/60 overflow-hidden rounded-xl">
{rechargeCards.map((card) => (
<article key={card.id} className="grid gap-4 px-4 py-4 lg:grid-cols-[minmax(0,1fr)_minmax(14rem,0.6fr)_auto] lg:items-center">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<WalletCards className="size-4" />
</span>
<div className="min-w-0">
<div className="flex min-h-6 flex-wrap items-center gap-2">
<h3 className="min-w-0 truncate font-mono font-semibold">{card.code}</h3>
<StatusBadge tone={card.status === "UNUSED" ? "success" : "neutral"}>
{card.status === "UNUSED"
? "未使用"
: card.status === "REDEEMED"
? "已兑换"
: card.status === "EXPIRED"
? "已过期"
: "已停用"}
</StatusBadge>
</div>
<p className="mt-1 text-sm text-muted-foreground">
{card.type === "BALANCE"
? `余额卡 ¥${Number(card.balanceAmount ?? 0).toFixed(2)}`
: `套餐卡 ${card.plan?.name ?? "套餐已删除"}`}
</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{card.batchName && <StatusBadge>{card.batchName}</StatusBadge>}
{card.redeemedBy && <StatusBadge tone="info">{card.redeemedBy.email}</StatusBadge>}
{card.expiresAt && <StatusBadge> {formatDate(card.expiresAt)}</StatusBadge>}
</div>
<div className="flex justify-start lg:justify-end">
<CopyButton text={card.code} />
</div>
</article>
))}
{rechargeCards.length === 0 && (
<p className="px-4 py-8 text-center text-sm text-muted-foreground"></p>
)}
</div>
</div>
</section>
</TabsContent>
</Tabs>
</PageShell>
);