mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: polish wallet recharge cards
This commit is contained in:
@@ -2,10 +2,10 @@ import type { Metadata } from "next";
|
||||
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 { Pagination } from "@/components/shared/pagination";
|
||||
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
|
||||
import type { StatusTone } from "@/components/shared/status-badge";
|
||||
import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -14,6 +14,7 @@ import { formatDate } from "@/lib/utils";
|
||||
import { getCommerceData } from "./commerce-data";
|
||||
import { CommerceToggleButton } from "./_components/commerce-actions";
|
||||
import { DiscountTypeSelect } from "./_components/discount-type-select";
|
||||
import { RechargeCardActions, type RechargeCardActionItem } from "./_components/recharge-card-actions";
|
||||
import { RechargeCardForm } from "./_components/recharge-card-form";
|
||||
|
||||
function formatCouponDiscount(type: string, value: unknown) {
|
||||
@@ -36,6 +37,76 @@ const rechargeCardStatusLabels: Record<string, string> = {
|
||||
DISABLED: "已停用",
|
||||
};
|
||||
|
||||
const rechargeCardStatusTones: Record<string, StatusTone> = {
|
||||
UNUSED: "success",
|
||||
REDEEMED: "info",
|
||||
EXPIRED: "warning",
|
||||
DISABLED: "danger",
|
||||
};
|
||||
|
||||
function userLabel(user: { name: string | null; email: string } | null | undefined, fallback = "系统") {
|
||||
if (!user) return fallback;
|
||||
return user.name ? `${user.name} · ${user.email}` : user.email;
|
||||
}
|
||||
|
||||
function formatRechargeCardValue(card: {
|
||||
type: "BALANCE" | "PLAN";
|
||||
balanceAmount: unknown;
|
||||
plan: { name: string } | null;
|
||||
}) {
|
||||
return card.type === "BALANCE"
|
||||
? `¥${Number(card.balanceAmount ?? 0).toFixed(2)}`
|
||||
: card.plan?.name ?? "套餐已删除";
|
||||
}
|
||||
|
||||
type RechargeCardRow = {
|
||||
id: string;
|
||||
code: string;
|
||||
type: "BALANCE" | "PLAN";
|
||||
status: "UNUSED" | "REDEEMED" | "EXPIRED" | "DISABLED";
|
||||
balanceAmount: unknown;
|
||||
plan: { name: string } | null;
|
||||
batchName: string | null;
|
||||
expiresAt: Date | null;
|
||||
redeemedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
createdBy: { name: string | null; email: string } | null;
|
||||
redeemedBy: { name: string | null; email: string } | null;
|
||||
transactions: Array<{
|
||||
amount: unknown;
|
||||
balanceAfter: unknown;
|
||||
createdAt: Date;
|
||||
}>;
|
||||
};
|
||||
|
||||
function formatRechargeCardAction(card: RechargeCardRow): RechargeCardActionItem {
|
||||
const transaction = card.transactions[0] ?? null;
|
||||
return {
|
||||
id: card.id,
|
||||
code: card.code,
|
||||
type: card.type,
|
||||
typeLabel: card.type === "BALANCE" ? "余额卡" : "套餐卡",
|
||||
status: card.status,
|
||||
statusLabel: rechargeCardStatusLabels[card.status] ?? "未知状态",
|
||||
statusTone: rechargeCardStatusTones[card.status] ?? "neutral",
|
||||
valueLabel: formatRechargeCardValue(card),
|
||||
batchName: card.batchName,
|
||||
createdByLabel: userLabel(card.createdBy),
|
||||
redeemedByLabel: userLabel(card.redeemedBy, "未兑换"),
|
||||
createdAtLabel: formatDate(card.createdAt),
|
||||
updatedAtLabel: formatDate(card.updatedAt),
|
||||
expiresAtLabel: card.expiresAt ? formatDate(card.expiresAt) : "永不过期",
|
||||
redeemedAtLabel: card.redeemedAt ? formatDate(card.redeemedAt) : "未兑换",
|
||||
transactionLabel: transaction
|
||||
? `余额入账 ¥${Number(transaction.amount).toFixed(2)} · 兑换后余额 ¥${Number(transaction.balanceAfter).toFixed(2)} · ${formatDate(transaction.createdAt)}`
|
||||
: null,
|
||||
releasesPlanStock: card.type === "PLAN"
|
||||
&& card.status === "UNUSED"
|
||||
&& (!card.expiresAt || card.expiresAt > new Date()),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCommerceTab(value: string | string[] | undefined) {
|
||||
return value === "manage" || value === "cards" ? value : "create";
|
||||
}
|
||||
@@ -205,36 +276,35 @@ export default async function AdminCommercePage({
|
||||
<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"}>
|
||||
{rechargeCardStatusLabels[card.status] ?? "未知状态"}
|
||||
</StatusBadge>
|
||||
{rechargeCards.map((card) => {
|
||||
const actionCard = formatRechargeCardAction(card);
|
||||
return (
|
||||
<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={actionCard.statusTone}>
|
||||
{actionCard.statusLabel}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{actionCard.typeLabel} · {actionCard.valueLabel}
|
||||
</p>
|
||||
</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>}
|
||||
<StatusBadge>{card.expiresAt ? `到期 ${formatDate(card.expiresAt)}` : "永不过期"}</StatusBadge>
|
||||
</div>
|
||||
<div className="flex justify-start lg:justify-end">
|
||||
<CopyButton text={card.code} />
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{card.batchName && <StatusBadge>{card.batchName}</StatusBadge>}
|
||||
{card.redeemedBy && <StatusBadge tone="info">{userLabel(card.redeemedBy, "已兑换")}</StatusBadge>}
|
||||
<StatusBadge>{card.expiresAt ? `到期 ${formatDate(card.expiresAt)}` : "永不过期"}</StatusBadge>
|
||||
</div>
|
||||
<RechargeCardActions card={actionCard} />
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
{rechargeCards.length === 0 && (
|
||||
<p className="px-4 py-8 text-center text-sm text-muted-foreground">暂无充值卡</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user