Files
J-Board-Lite/src/app/(admin)/admin/commerce/_components/recharge-card-form.tsx
2026-05-01 03:16:11 +10:00

158 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { createAdminRechargeCards } from "@/actions/admin/recharge-cards";
import { BooleanToggle } from "@/components/ui/boolean-toggle";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { getErrorMessage } from "@/lib/errors";
interface PlanOption {
id: string;
name: string;
type: "PROXY" | "STREAMING";
remainingCount: number | null;
}
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;
return (
<form
className="form-panel space-y-4"
onSubmit={(event) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
formData.set("type", type);
if (type === "PLAN") formData.set("planId", planId);
startTransition(async () => {
try {
await createAdminRechargeCards(formData);
toast.success("充值卡已生成");
form.reset();
setHasExpiry(false);
router.refresh();
} catch (error) {
toast.error(getErrorMessage(error, "生成充值卡失败"));
}
});
}}
>
<div className="flex items-center justify-between gap-3">
<div>
<h3 className="font-semibold"></h3>
<p className="mt-1 text-sm text-muted-foreground"></p>
</div>
<BooleanToggle
value={type === "PLAN"}
onChange={(value) => setType(value ? "PLAN" : "BALANCE")}
trueLabel="套餐卡"
falseLabel="余额卡"
ariaLabel="充值卡类型"
className="w-36"
/>
</div>
<input type="hidden" name="type" value={type} />
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="recharge-card-quantity"></Label>
<Input
id="recharge-card-quantity"
name="quantity"
type="number"
min="1"
max={type === "PLAN" && selectedPlan?.remainingCount != null ? Math.min(200, selectedPlan.remainingCount) : 200}
defaultValue={1}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="recharge-card-batch"></Label>
<Input id="recharge-card-batch" name="batchName" placeholder="五月活动" />
</div>
</div>
{type === "BALANCE" ? (
<div className="space-y-2">
<Label htmlFor="recharge-card-amount"></Label>
<Input id="recharge-card-amount" name="balanceAmount" type="number" min="0.01" step="0.01" placeholder="100.00" required />
</div>
) : (
<div className="space-y-2">
<Label></Label>
<Select value={planId} onValueChange={(value) => setPlanId(value ?? "")}>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择套餐">
{(value) => {
const plan = plans.find((item) => item.id === value);
return plan
? `${plan.name} · ${plan.type === "PROXY" ? "代理套餐" : "流媒体套餐"}`
: "选择套餐";
}}
</SelectValue>
</SelectTrigger>
<SelectContent>
{plans.map((plan) => (
<SelectItem key={plan.id} value={plan.id}>
{plan.name} · {plan.type === "PROXY" ? "代理套餐" : "流媒体套餐"} · {plan.remainingCount ?? "不限"}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedPlan && (
<p className="text-xs text-muted-foreground">
{selectedPlan.remainingCount == null ? "库存不限" : `当前可生成 ${selectedPlan.remainingCount}`}
</p>
)}
</div>
)}
<div className="space-y-2">
<Label htmlFor="recharge-card-expiry-mode"></Label>
<BooleanToggle
id="recharge-card-expiry-mode"
value={hasExpiry}
onChange={setHasExpiry}
trueLabel="设置到期"
falseLabel="永不过期"
ariaLabel="充值卡有效期"
/>
</div>
{hasExpiry && (
<div className="space-y-2">
<Label htmlFor="recharge-card-expires"></Label>
<Input id="recharge-card-expires" name="expiresAt" type="datetime-local" required />
</div>
)}
{!hasExpiry && (
<input type="hidden" name="expiresAt" value="" />
)}
{type === "PLAN" && plans.length === 0 && (
<div className="rounded-lg border border-destructive/15 bg-destructive/10 px-3 py-2 text-xs font-medium text-destructive">
</div>
)}
<Button type="submit" className="w-full" disabled={pending || (type === "PLAN" && (!planId || planSoldOut))}>
{pending ? "生成中..." : planSoldOut ? "套餐已售罄" : "生成充值卡"}
</Button>
</form>
);
}