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:
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { deleteAdminRechargeCard } from "@/actions/admin/recharge-cards";
|
||||
import { ConfirmActionButton } from "@/components/shared/confirm-action-button";
|
||||
import { CopyButton } from "@/components/shared/copy-button";
|
||||
import { StatusBadge, type StatusTone } from "@/components/shared/status-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface RechargeCardActionItem {
|
||||
id: string;
|
||||
code: string;
|
||||
type: "BALANCE" | "PLAN";
|
||||
typeLabel: string;
|
||||
status: "UNUSED" | "REDEEMED" | "EXPIRED" | "DISABLED";
|
||||
statusLabel: string;
|
||||
statusTone: StatusTone;
|
||||
valueLabel: string;
|
||||
batchName: string | null;
|
||||
createdByLabel: string;
|
||||
redeemedByLabel: string;
|
||||
createdAtLabel: string;
|
||||
updatedAtLabel: string;
|
||||
expiresAtLabel: string;
|
||||
redeemedAtLabel: string;
|
||||
transactionLabel: string | null;
|
||||
releasesPlanStock: boolean;
|
||||
}
|
||||
|
||||
function DetailItem({
|
||||
label,
|
||||
value,
|
||||
mono,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-muted/25 px-3 py-2">
|
||||
<p className="text-[11px] font-medium text-muted-foreground">{label}</p>
|
||||
<p className={cn("mt-1 break-words text-sm font-medium leading-5", mono && "font-mono text-xs")}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RechargeCardActions({ card }: { card: RechargeCardActionItem }) {
|
||||
const router = useRouter();
|
||||
const deleteDescription = card.releasesPlanStock
|
||||
? "这张套餐卡尚未兑换,删除后会释放 1 个套餐库存。已兑换记录不会受影响。"
|
||||
: card.status === "REDEEMED"
|
||||
? "删除只移除卡密记录,不会回滚已兑换的余额或套餐。"
|
||||
: "删除后卡密不可恢复,请确认不再需要保留。";
|
||||
|
||||
const details = [
|
||||
{ label: "卡密", value: card.code, mono: true },
|
||||
{ label: "类型", value: card.typeLabel },
|
||||
{ label: card.type === "BALANCE" ? "充值金额" : "绑定套餐", value: card.valueLabel },
|
||||
{ label: "批次", value: card.batchName ?? "未设置" },
|
||||
{ label: "创建人", value: card.createdByLabel },
|
||||
{ label: "创建时间", value: card.createdAtLabel },
|
||||
{ label: "有效期", value: card.expiresAtLabel },
|
||||
{ label: "更新时间", value: card.updatedAtLabel },
|
||||
{ label: "兑换人", value: card.redeemedByLabel },
|
||||
{ label: "兑换时间", value: card.redeemedAtLabel },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start gap-2 lg:justify-end">
|
||||
<CopyButton text={card.code} />
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button type="button" variant="outline" size="sm" />}>
|
||||
<Eye className="size-3.5" />
|
||||
详情
|
||||
</DialogTrigger>
|
||||
<DialogContent className="flex max-h-[min(90dvh,34rem)] flex-col overflow-hidden p-0 sm:max-w-[36rem]">
|
||||
<DialogHeader className="border-b border-border/60 px-4 py-3 pr-10">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DialogTitle>充值卡详情</DialogTitle>
|
||||
<StatusBadge tone={card.statusTone}>{card.statusLabel}</StatusBadge>
|
||||
</div>
|
||||
<DialogDescription>{card.valueLabel}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogBody className="flex-1 px-4 py-3">
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{details.map((item) => (
|
||||
<DetailItem key={item.label} {...item} />
|
||||
))}
|
||||
</div>
|
||||
{card.transactionLabel && (
|
||||
<div className="mt-3 rounded-lg border border-primary/15 bg-primary/5 px-3 py-2 text-sm text-primary">
|
||||
{card.transactionLabel}
|
||||
</div>
|
||||
)}
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ConfirmActionButton
|
||||
title={`删除充值卡 ${card.code}`}
|
||||
description={deleteDescription}
|
||||
confirmLabel="删除卡密"
|
||||
successMessage={card.releasesPlanStock ? "充值卡已删除,套餐库存已释放" : "充值卡已删除"}
|
||||
errorMessage="删除充值卡失败"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onConfirm={async () => {
|
||||
await deleteAdminRechargeCard(card.id);
|
||||
}}
|
||||
onSuccess={() => router.refresh()}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
删除
|
||||
</ConfirmActionButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,17 @@ export async function getCommerceData(
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: {
|
||||
plan: { select: { name: true, type: true } },
|
||||
redeemedBy: { select: { email: true } },
|
||||
redeemedBy: { select: { email: true, name: true } },
|
||||
createdBy: { select: { email: true, name: true } },
|
||||
transactions: {
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 1,
|
||||
select: {
|
||||
amount: true,
|
||||
balanceAfter: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: pageSize,
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { DataTableShell } from "@/components/admin/data-table-shell";
|
||||
import {
|
||||
DataTable,
|
||||
DataTableBody,
|
||||
DataTableCell,
|
||||
DataTableHead,
|
||||
DataTableHeadCell,
|
||||
DataTableHeaderRow,
|
||||
DataTableRow,
|
||||
} from "@/components/shared/data-table";
|
||||
import { OrderStatusBadge } from "@/components/shared/domain-badges";
|
||||
import { getPaymentProviderName } from "@/services/payment/catalog";
|
||||
import { formatDateShort } from "@/lib/utils";
|
||||
import type { AdminRechargeOrderRow } from "../orders-data";
|
||||
|
||||
interface RechargeOrdersTableProps {
|
||||
rechargeOrders: AdminRechargeOrderRow[];
|
||||
}
|
||||
|
||||
function formatAmount(amount: { toString(): string }) {
|
||||
return `¥${Number(amount).toFixed(2)}`;
|
||||
}
|
||||
|
||||
function getPaymentLabel(provider: string | null) {
|
||||
return provider ? getPaymentProviderName(provider) : "未选择支付";
|
||||
}
|
||||
|
||||
export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps) {
|
||||
return (
|
||||
<DataTableShell
|
||||
isEmpty={rechargeOrders.length === 0}
|
||||
emptyTitle="暂无充值订单"
|
||||
emptyDescription="用户发起钱包充值后,会在这里显示支付与入账状态。"
|
||||
mobileCards={rechargeOrders.map((order) => (
|
||||
<article key={order.id} className="space-y-3 p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="break-all text-sm font-semibold">{order.user.email}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{order.user.name || "未设置昵称"}</p>
|
||||
</div>
|
||||
<OrderStatusBadge status={order.status} />
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted/25 p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-xs text-muted-foreground">充值金额</p>
|
||||
<p className="font-semibold tabular-nums">{formatAmount(order.amount)}</p>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{getPaymentLabel(order.paymentMethod)} · {order.tradeNo || "无交易号"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{formatDateShort(order.createdAt)}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
>
|
||||
<DataTable aria-label="充值订单列表" className="min-w-[980px]">
|
||||
<DataTableHead>
|
||||
<DataTableHeaderRow>
|
||||
<DataTableHeadCell>用户</DataTableHeadCell>
|
||||
<DataTableHeadCell>金额</DataTableHeadCell>
|
||||
<DataTableHeadCell>支付</DataTableHeadCell>
|
||||
<DataTableHeadCell>状态</DataTableHeadCell>
|
||||
<DataTableHeadCell>备注</DataTableHeadCell>
|
||||
<DataTableHeadCell>时间</DataTableHeadCell>
|
||||
</DataTableHeaderRow>
|
||||
</DataTableHead>
|
||||
<DataTableBody>
|
||||
{rechargeOrders.map((order) => (
|
||||
<DataTableRow key={order.id}>
|
||||
<DataTableCell className="max-w-56 whitespace-normal break-all">
|
||||
<p className="font-medium">{order.user.email}</p>
|
||||
<p className="text-xs text-muted-foreground">{order.user.name || "未设置昵称"}</p>
|
||||
</DataTableCell>
|
||||
<DataTableCell className="font-semibold tabular-nums">{formatAmount(order.amount)}</DataTableCell>
|
||||
<DataTableCell>
|
||||
<div className="space-y-1">
|
||||
<p>{getPaymentLabel(order.paymentMethod)}</p>
|
||||
<p className="max-w-56 break-all text-xs text-muted-foreground">
|
||||
{order.tradeNo || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</DataTableCell>
|
||||
<DataTableCell>
|
||||
<OrderStatusBadge status={order.status} />
|
||||
</DataTableCell>
|
||||
<DataTableCell className="max-w-64 whitespace-normal break-words text-xs text-muted-foreground">
|
||||
{order.note || order.paymentRef || "—"}
|
||||
</DataTableCell>
|
||||
<DataTableCell className="whitespace-nowrap text-muted-foreground">
|
||||
{formatDateShort(order.createdAt)}
|
||||
</DataTableCell>
|
||||
</DataTableRow>
|
||||
))}
|
||||
</DataTableBody>
|
||||
</DataTable>
|
||||
</DataTableShell>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,14 @@ export type AdminOrderRow = Prisma.OrderGetPayload<{
|
||||
include: typeof adminOrderInclude;
|
||||
}>;
|
||||
|
||||
const adminRechargeOrderInclude = {
|
||||
user: true,
|
||||
} satisfies Prisma.WalletRechargeOrderInclude;
|
||||
|
||||
export type AdminRechargeOrderRow = Prisma.WalletRechargeOrderGetPayload<{
|
||||
include: typeof adminRechargeOrderInclude;
|
||||
}>;
|
||||
|
||||
export async function getAdminOrders(
|
||||
searchParams: Record<string, string | string[] | undefined>,
|
||||
) {
|
||||
@@ -52,3 +60,38 @@ export async function getAdminOrders(
|
||||
|
||||
return { orders, total, page, pageSize, filters: { q, status, kind, reviewStatus } };
|
||||
}
|
||||
|
||||
export async function getAdminRechargeOrders(
|
||||
searchParams: Record<string, string | string[] | undefined>,
|
||||
) {
|
||||
const { page, skip, pageSize } = parsePage(searchParams);
|
||||
const q = typeof searchParams.q === "string" ? searchParams.q.trim() : "";
|
||||
const status = typeof searchParams.status === "string" ? searchParams.status : "";
|
||||
|
||||
const where = {
|
||||
...(status ? { status: status as "PENDING" | "PAID" | "CANCELLED" | "REFUNDED" } : {}),
|
||||
...(q
|
||||
? {
|
||||
OR: [
|
||||
{ user: { email: { contains: q } } },
|
||||
{ user: { name: { contains: q } } },
|
||||
{ tradeNo: { contains: q } },
|
||||
{ paymentRef: { contains: q } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} satisfies Prisma.WalletRechargeOrderWhereInput;
|
||||
|
||||
const [rechargeOrders, total] = await Promise.all([
|
||||
prisma.walletRechargeOrder.findMany({
|
||||
where,
|
||||
include: adminRechargeOrderInclude,
|
||||
orderBy: { createdAt: "desc" },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.walletRechargeOrder.count({ where }),
|
||||
]);
|
||||
|
||||
return { rechargeOrders, total, page, pageSize, filters: { q, status } };
|
||||
}
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { AdminFilterBar } from "@/components/admin/filter-bar";
|
||||
import { PageHeader, PageShell } from "@/components/shared/page-shell";
|
||||
import { Pagination } from "@/components/shared/pagination";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { OrdersTable } from "./_components/orders-table";
|
||||
import { getAdminOrders } from "./orders-data";
|
||||
import { RechargeOrdersTable } from "./_components/recharge-orders-table";
|
||||
import { getAdminOrders, getAdminRechargeOrders } from "./orders-data";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "订单管理",
|
||||
description: "跟踪订单状态、审查结果与支付记录。",
|
||||
};
|
||||
|
||||
function normalizeOrdersTab(value: string | string[] | undefined) {
|
||||
return value === "recharge" ? "recharge" : "orders";
|
||||
}
|
||||
|
||||
function OrdersTabLink({
|
||||
href,
|
||||
active,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
active: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
buttonVariants({ variant: active ? "default" : "outline", size: "sm" }),
|
||||
"min-w-28",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function OrdersPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const { orders, total, page, pageSize, filters } = await getAdminOrders(await searchParams);
|
||||
const params = await searchParams;
|
||||
const activeTab = normalizeOrdersTab(params.tab);
|
||||
const orderData = activeTab === "orders" ? await getAdminOrders(params) : null;
|
||||
const rechargeData = activeTab === "recharge" ? await getAdminRechargeOrders(params) : null;
|
||||
const filters = activeTab === "orders" ? orderData!.filters : rechargeData!.filters;
|
||||
|
||||
return (
|
||||
<PageShell>
|
||||
@@ -23,45 +58,78 @@ export default async function OrdersPage({
|
||||
eyebrow="商品与订单"
|
||||
title="订单管理"
|
||||
/>
|
||||
<div className="surface-card flex w-fit flex-wrap gap-2 rounded-xl p-1">
|
||||
<OrdersTabLink href="/admin/orders?tab=orders" active={activeTab === "orders"}>
|
||||
商品订单
|
||||
</OrdersTabLink>
|
||||
<OrdersTabLink href="/admin/orders?tab=recharge" active={activeTab === "recharge"}>
|
||||
充值订单
|
||||
</OrdersTabLink>
|
||||
</div>
|
||||
<AdminFilterBar
|
||||
q={filters.q}
|
||||
searchPlaceholder="搜索邮箱、套餐、交易号"
|
||||
searchPlaceholder={activeTab === "orders" ? "搜索邮箱、套餐、交易号" : "搜索邮箱、交易号"}
|
||||
selects={[
|
||||
{
|
||||
name: "status",
|
||||
value: filters.status,
|
||||
options: [
|
||||
{ label: "全部状态", value: "" },
|
||||
{ label: "待确认", value: "PENDING" },
|
||||
{ label: "已支付", value: "PAID" },
|
||||
{ label: activeTab === "orders" ? "待确认" : "待支付", value: "PENDING" },
|
||||
{ label: activeTab === "orders" ? "已支付" : "已入账", value: "PAID" },
|
||||
{ label: "已取消", value: "CANCELLED" },
|
||||
{ label: "已退款", value: "REFUNDED" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "kind",
|
||||
value: filters.kind,
|
||||
options: [
|
||||
{ label: "全部类型", value: "" },
|
||||
{ label: "新购", value: "NEW_PURCHASE" },
|
||||
{ label: "续费", value: "RENEWAL" },
|
||||
{ label: "增流量", value: "TRAFFIC_TOPUP" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "reviewStatus",
|
||||
value: filters.reviewStatus,
|
||||
options: [
|
||||
{ label: "全部审查", value: "" },
|
||||
{ label: "正常", value: "NORMAL" },
|
||||
{ label: "异常", value: "FLAGGED" },
|
||||
{ label: "已解决", value: "RESOLVED" },
|
||||
],
|
||||
},
|
||||
...(activeTab === "orders"
|
||||
? [
|
||||
{
|
||||
name: "kind",
|
||||
value: orderData!.filters.kind,
|
||||
options: [
|
||||
{ label: "全部类型", value: "" },
|
||||
{ label: "新购", value: "NEW_PURCHASE" },
|
||||
{ label: "续费", value: "RENEWAL" },
|
||||
{ label: "增流量", value: "TRAFFIC_TOPUP" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "reviewStatus",
|
||||
value: orderData!.filters.reviewStatus,
|
||||
options: [
|
||||
{ label: "全部审查", value: "" },
|
||||
{ label: "正常", value: "NORMAL" },
|
||||
{ label: "异常", value: "FLAGGED" },
|
||||
{ label: "已解决", value: "RESOLVED" },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
<OrdersTable orders={orders} />
|
||||
<Pagination total={total} pageSize={pageSize} page={page} />
|
||||
>
|
||||
<input type="hidden" name="tab" value={activeTab} />
|
||||
</AdminFilterBar>
|
||||
{activeTab === "orders" ? (
|
||||
<>
|
||||
<OrdersTable orders={orderData!.orders} />
|
||||
<Pagination
|
||||
total={orderData!.total}
|
||||
pageSize={orderData!.pageSize}
|
||||
page={orderData!.page}
|
||||
fixedParams={{ tab: "orders" }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RechargeOrdersTable rechargeOrders={rechargeData!.rechargeOrders} />
|
||||
<Pagination
|
||||
total={rechargeData!.total}
|
||||
pageSize={rechargeData!.pageSize}
|
||||
page={rechargeData!.page}
|
||||
fixedParams={{ tab: "recharge" }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user