Files
J-Board-Lite/src/app/(admin)/admin/plans/proxy-config-section.tsx
2026-05-01 00:58:46 +10:00

240 lines
7.4 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 type { Dispatch, SetStateAction } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import type {
InboundOption,
NodeOption,
PlanFormValue,
PlanPricingMode,
} from "./plan-form-types";
type FieldId = (name: string) => string;
export function ProxyNodeFields({
fieldId,
nodes,
nodeId,
setNodeId,
inbounds,
setInbounds,
selectedInboundIds,
setSelectedInboundIds,
toggleInbound,
}: {
fieldId: FieldId;
nodes: NodeOption[];
nodeId: string;
setNodeId: Dispatch<SetStateAction<string>>;
inbounds: InboundOption[];
setInbounds: Dispatch<SetStateAction<InboundOption[]>>;
selectedInboundIds: string[];
setSelectedInboundIds: Dispatch<SetStateAction<string[]>>;
toggleInbound: (inboundId: string) => void;
}) {
return (
<>
<div className="space-y-1.5">
<Label htmlFor={fieldId("nodeId")}></Label>
<Select
value={nodeId}
onValueChange={(value) => {
setNodeId(value ?? "");
setInbounds([]);
setSelectedInboundIds([]);
}}
>
<SelectTrigger id={fieldId("nodeId")}>
<SelectValue placeholder="选择节点">
{(value) => {
const match = nodes.find((node) => node.id === value);
return match ? match.name : "选择节点";
}}
</SelectValue>
</SelectTrigger>
<SelectContent>
{nodes.map((node) => (
<SelectItem key={node.id} value={node.id}>
{node.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label id={fieldId("inboundIds-label")}></Label>
<input type="hidden" name="inboundIds" value={selectedInboundIds.join(",")} />
<div className="grid gap-2 sm:grid-cols-2" role="group" aria-labelledby={fieldId("inboundIds-label")}>
{inbounds.map((inbound) => {
const selected = selectedInboundIds.includes(inbound.id);
return (
<button
key={inbound.id}
type="button"
className={cn(
"choice-card flex min-h-8 items-center justify-between gap-2 px-2.5 py-1.5 text-left text-xs leading-4",
selected
? "border-primary/30 bg-primary/10 text-primary"
: "hover:bg-muted/45",
)}
onClick={() => toggleInbound(inbound.id)}
>
<span className="shrink-0 font-medium leading-4">
{inbound.protocol} · {inbound.port}
</span>
<span className="min-w-0 truncate text-[11px] leading-4 text-muted-foreground">{inbound.tag}</span>
</button>
);
})}
</div>
{nodeId && inbounds.length === 0 && (
<p className="mt-2 text-xs text-muted-foreground"></p>
)}
</div>
</>
);
}
export function ProxyPricingFields({
fieldId,
plan,
pricingMode,
setPricingMode,
}: {
fieldId: FieldId;
plan?: PlanFormValue;
pricingMode: PlanPricingMode;
setPricingMode: Dispatch<SetStateAction<PlanPricingMode>>;
}) {
const pricingModeLabels: Record<string, string> = {
TRAFFIC_SLIDER: "用户自选流量",
FIXED_PACKAGE: "固定流量套餐",
};
return (
<>
<div className="space-y-1.5">
<Label htmlFor={fieldId("pricingMode")}></Label>
<Select value={pricingMode} onValueChange={(value) => setPricingMode(value as PlanPricingMode)}>
<SelectTrigger id={fieldId("pricingMode")}>
<SelectValue placeholder="选择售卖方式">
{(value) => pricingModeLabels[value] ?? value}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="TRAFFIC_SLIDER"></SelectItem>
<SelectItem value="FIXED_PACKAGE"></SelectItem>
</SelectContent>
</Select>
</div>
{pricingMode === "TRAFFIC_SLIDER" ? (
<div className="grid gap-3 sm:grid-cols-3">
<div className="space-y-1.5">
<Label htmlFor={fieldId("pricePerGb")}>¥/GB</Label>
<Input
id={fieldId("pricePerGb")}
name="pricePerGb"
type="number"
step="0.01"
defaultValue={plan?.pricePerGb ?? ""}
placeholder="0.5"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor={fieldId("minTrafficGb")}> GB</Label>
<Input
id={fieldId("minTrafficGb")}
name="minTrafficGb"
type="number"
defaultValue={plan?.minTrafficGb ?? ""}
placeholder="10"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor={fieldId("maxTrafficGb")}> GB</Label>
<Input
id={fieldId("maxTrafficGb")}
name="maxTrafficGb"
type="number"
defaultValue={plan?.maxTrafficGb ?? ""}
placeholder="1000"
/>
</div>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor={fieldId("fixedTrafficGb")}>GB</Label>
<Input
id={fieldId("fixedTrafficGb")}
name="fixedTrafficGb"
type="number"
min={1}
defaultValue={plan?.fixedTrafficGb ?? plan?.minTrafficGb ?? ""}
placeholder="200"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor={fieldId("fixedPrice")}>¥</Label>
<Input
id={fieldId("fixedPrice")}
name="fixedPrice"
type="number"
step="0.01"
min={0.01}
defaultValue={plan?.fixedPrice ?? ""}
placeholder="29.9"
/>
</div>
</div>
)}
<div className="space-y-1.5">
<Label htmlFor={fieldId("totalTrafficGb")}>GB</Label>
<Input
id={fieldId("totalTrafficGb")}
name="totalTrafficGb"
type="number"
min={1}
defaultValue={plan?.totalTrafficGb ?? ""}
placeholder="空=不限"
/>
</div>
</>
);
}
/** @deprecated Use ProxyNodeFields + ProxyPricingFields instead */
export function ProxyConfigSection(props: {
fieldId: FieldId;
plan?: PlanFormValue;
nodes: NodeOption[];
nodeId: string;
setNodeId: Dispatch<SetStateAction<string>>;
inbounds: InboundOption[];
setInbounds: Dispatch<SetStateAction<InboundOption[]>>;
selectedInboundIds: string[];
setSelectedInboundIds: Dispatch<SetStateAction<string[]>>;
toggleInbound: (inboundId: string) => void;
pricingMode: PlanPricingMode;
setPricingMode: Dispatch<SetStateAction<PlanPricingMode>>;
}) {
return (
<>
<ProxyNodeFields {...props} />
<ProxyPricingFields {...props} />
</>
);
}