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