mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|