feat: add admin wallet recharge order actions

This commit is contained in:
JetSprow
2026-05-01 05:28:20 +10:00
parent 8034392408
commit 1dd3157177
9 changed files with 333 additions and 21 deletions

View File

@@ -8,9 +8,10 @@ import {
DataTableHeaderRow,
DataTableRow,
} from "@/components/shared/data-table";
import { OrderStatusBadge } from "@/components/shared/domain-badges";
import { StatusBadge, type StatusTone } from "@/components/shared/status-badge";
import { getPaymentProviderName } from "@/services/payment/catalog";
import { formatDateShort } from "@/lib/utils";
import { RechargeOrderActions } from "../recharge-order-actions";
import type { AdminRechargeOrderRow } from "../orders-data";
interface RechargeOrdersTableProps {
@@ -22,9 +23,28 @@ function formatAmount(amount: { toString(): string }) {
}
function getPaymentLabel(provider: string | null) {
if (provider === "manual") return "手动确认";
return provider ? getPaymentProviderName(provider) : "未选择支付";
}
const rechargeStatusLabels: Record<AdminRechargeOrderRow["status"], string> = {
PENDING: "待支付",
PAID: "已入账",
CANCELLED: "已取消",
REFUNDED: "已退款",
};
function getRechargeStatusTone(status: AdminRechargeOrderRow["status"]): StatusTone {
if (status === "PAID") return "success";
if (status === "PENDING") return "warning";
if (status === "CANCELLED") return "neutral";
return "danger";
}
function RechargeStatusBadge({ status }: { status: AdminRechargeOrderRow["status"] }) {
return <StatusBadge tone={getRechargeStatusTone(status)}>{rechargeStatusLabels[status]}</StatusBadge>;
}
export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps) {
return (
<DataTableShell
@@ -38,7 +58,7 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
<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} />
<RechargeStatusBadge status={order.status} />
</div>
<div className="rounded-lg bg-muted/25 p-3">
<div className="flex items-center justify-between gap-3">
@@ -50,10 +70,11 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
</p>
<p className="mt-1 text-xs text-muted-foreground">{formatDateShort(order.createdAt)}</p>
</div>
<RechargeOrderActions orderId={order.id} status={order.status} />
</article>
))}
>
<DataTable aria-label="充值订单列表" className="min-w-[980px]">
<DataTable aria-label="充值订单列表" className="min-w-[1120px]">
<DataTableHead>
<DataTableHeaderRow>
<DataTableHeadCell></DataTableHeadCell>
@@ -62,6 +83,7 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell></DataTableHeadCell>
<DataTableHeadCell className="text-right"></DataTableHeadCell>
</DataTableHeaderRow>
</DataTableHead>
<DataTableBody>
@@ -81,7 +103,7 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
</div>
</DataTableCell>
<DataTableCell>
<OrderStatusBadge status={order.status} />
<RechargeStatusBadge status={order.status} />
</DataTableCell>
<DataTableCell className="max-w-64 whitespace-normal break-words text-xs text-muted-foreground">
{order.note || order.paymentRef || "—"}
@@ -89,6 +111,9 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
<DataTableCell className="whitespace-nowrap text-muted-foreground">
{formatDateShort(order.createdAt)}
</DataTableCell>
<DataTableCell>
<RechargeOrderActions orderId={order.id} status={order.status} />
</DataTableCell>
</DataTableRow>
))}
</DataTableBody>

View File

@@ -0,0 +1,84 @@
"use client";
import { useRouter } from "next/navigation";
import { CheckCircle2, Trash2, XCircle } from "lucide-react";
import {
cancelAdminWalletRecharge,
confirmAdminWalletRecharge,
deleteAdminWalletRecharge,
} from "@/actions/admin/recharge-orders";
import { ConfirmActionButton } from "@/components/shared/confirm-action-button";
type RechargeOrderStatus = "PENDING" | "PAID" | "CANCELLED" | "REFUNDED";
export function RechargeOrderActions({
orderId,
status,
}: {
orderId: string;
status: RechargeOrderStatus;
}) {
const router = useRouter();
const isPending = status === "PENDING";
const isPaid = status === "PAID";
return (
<div className="flex flex-wrap justify-start gap-2 lg:justify-end">
{isPending && (
<>
<ConfirmActionButton
title="确认这笔充值?"
description="会立即把充值金额入账到用户钱包,并将订单标记为已入账。"
confirmLabel="确认入账"
successMessage="充值订单已确认入账"
errorMessage="确认充值订单失败"
size="sm"
onConfirm={async () => {
await confirmAdminWalletRecharge(orderId);
}}
onSuccess={() => router.refresh()}
>
<CheckCircle2 className="size-3.5" />
</ConfirmActionButton>
<ConfirmActionButton
title="取消这笔充值?"
description="取消后用户不能继续支付这笔充值订单,钱包余额不会变化。"
confirmLabel="取消充值"
successMessage="充值订单已取消"
errorMessage="取消充值订单失败"
variant="destructive"
size="sm"
onConfirm={async () => {
await cancelAdminWalletRecharge(orderId);
}}
onSuccess={() => router.refresh()}
>
<XCircle className="size-3.5" />
</ConfirmActionButton>
</>
)}
<ConfirmActionButton
title="删除这条充值记录?"
description={
isPaid
? "只删除充值订单记录,不回滚已入账余额,钱包流水会保留。"
: "删除后这条充值记录不可恢复,不会改变用户钱包余额。"
}
confirmLabel="删除记录"
successMessage="充值记录已删除"
errorMessage="删除充值记录失败"
variant="destructive"
size="sm"
onConfirm={async () => {
await deleteAdminWalletRecharge(orderId);
}}
onSuccess={() => router.refresh()}
>
<Trash2 className="size-3.5" />
</ConfirmActionButton>
</div>
);
}