269 lines
9.3 KiB
TypeScript
269 lines
9.3 KiB
TypeScript
"use client";
|
||
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { formatAdminCreditMajorDecimal } from "@/lib/money";
|
||
import { cn } from "@/lib/utils";
|
||
import type { AgentParentCaps } from "@/types/api/admin-agent";
|
||
|
||
import { Info } from "lucide-react";
|
||
|
||
export type AgentProfileFieldsProps = {
|
||
disabled?: boolean;
|
||
loading?: boolean;
|
||
parentCaps: AgentParentCaps | null;
|
||
availableCredit: number | null;
|
||
canCreateChildAgent: boolean;
|
||
isSuperAdmin: boolean;
|
||
shareRate: string;
|
||
onShareRateChange: (value: string) => void;
|
||
creditLimit: string;
|
||
onCreditLimitChange: (value: string) => void;
|
||
rebateLimit: string;
|
||
onRebateLimitChange: (value: string) => void;
|
||
defaultRebate: string;
|
||
onDefaultRebateChange: (value: string) => void;
|
||
extraRebate: boolean;
|
||
onExtraRebateChange: (value: boolean) => void;
|
||
canCreatePlayer: boolean;
|
||
onCanCreatePlayerChange: (value: boolean) => void;
|
||
canCreateChild: boolean;
|
||
onCanCreateChildChange: (value: boolean) => void;
|
||
riskTags: string;
|
||
onRiskTagsChange: (value: string) => void;
|
||
idPrefix?: string;
|
||
currencyCode?: string;
|
||
/** card:用于代理线路详情 Tab 内的卡片表单 */
|
||
variant?: "default" | "card";
|
||
};
|
||
|
||
export function AgentProfileFields({
|
||
disabled = false,
|
||
loading = false,
|
||
parentCaps,
|
||
availableCredit,
|
||
canCreateChildAgent,
|
||
isSuperAdmin,
|
||
shareRate,
|
||
onShareRateChange,
|
||
creditLimit,
|
||
onCreditLimitChange,
|
||
rebateLimit,
|
||
onRebateLimitChange,
|
||
defaultRebate,
|
||
onDefaultRebateChange,
|
||
extraRebate,
|
||
onExtraRebateChange,
|
||
canCreatePlayer,
|
||
onCanCreatePlayerChange,
|
||
canCreateChild,
|
||
onCanCreateChildChange,
|
||
riskTags,
|
||
onRiskTagsChange,
|
||
idPrefix = "agent-profile",
|
||
currencyCode = "NPR",
|
||
variant = "default",
|
||
}: AgentProfileFieldsProps): React.ReactElement {
|
||
const { t } = useTranslation(["agents", "common"]);
|
||
const fieldDisabled = disabled || loading;
|
||
const isCard = variant === "card";
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{(parentCaps || availableCredit !== null) && !loading ? (
|
||
<div className="flex items-start gap-3 rounded-xl border border-primary/20 bg-primary/5 p-4 text-primary shadow-sm">
|
||
<Info className="mt-0.5 size-5 shrink-0 opacity-80" aria-hidden />
|
||
<div className="flex flex-col gap-1.5 min-w-0">
|
||
{parentCaps ? (
|
||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||
<p className="text-sm font-medium leading-snug">
|
||
{t("profile.parentCaps", {
|
||
defaultValue: "上级占成 {{share}}%,可下发 {{credit}}",
|
||
share: parentCaps.total_share_rate,
|
||
credit: formatAdminCreditMajorDecimal(parentCaps.available_credit, currencyCode),
|
||
})}
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
{availableCredit !== null ? (
|
||
<p className={cn("text-sm", parentCaps ? "text-primary/80" : "font-medium")}>
|
||
{t("profile.availableCredit", {
|
||
defaultValue: "可下发额度 {{amount}}",
|
||
amount: formatAdminCreditMajorDecimal(availableCredit, currencyCode),
|
||
})}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{loading ? (
|
||
<p className="text-sm text-muted-foreground animate-pulse">
|
||
{t("profile.loading", { defaultValue: "正在加载占成与授信…" })}
|
||
</p>
|
||
) : null}
|
||
|
||
<div
|
||
className={cn(
|
||
"grid gap-x-6 gap-y-5 sm:grid-cols-2",
|
||
fieldDisabled ? "pointer-events-none opacity-50" : "",
|
||
)}
|
||
>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-share-rate`} className="text-muted-foreground">
|
||
{parentCaps
|
||
? t("profile.relativeShareRate", { defaultValue: "占成比例(占上级 %)" })
|
||
: t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-share-rate`}
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
step="0.01"
|
||
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
||
value={shareRate}
|
||
onChange={(e) => onShareRateChange(e.target.value)}
|
||
/>
|
||
{parentCaps && shareRate ? (
|
||
<p className="text-xs text-muted-foreground/80">
|
||
{t("profile.actualShareRate", {
|
||
defaultValue: "实际占成 {{rate}}%",
|
||
rate: Number((Number(parentCaps.total_share_rate) * Number(shareRate) / 100).toFixed(2)),
|
||
})}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-credit-limit`} className="text-muted-foreground">
|
||
{t("profile.creditLimit", { defaultValue: "授信额度" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-credit-limit`}
|
||
type="number"
|
||
min={0}
|
||
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
||
value={creditLimit}
|
||
onChange={(e) => onCreditLimitChange(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-rebate-limit`} className="text-muted-foreground">
|
||
{t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-rebate-limit`}
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
step="0.01"
|
||
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
||
value={rebateLimit}
|
||
onChange={(e) => onRebateLimitChange(e.target.value)}
|
||
placeholder="50"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-default-rebate`} className="text-muted-foreground">
|
||
{t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-default-rebate`}
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
step="0.01"
|
||
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
||
value={defaultRebate}
|
||
onChange={(e) => onDefaultRebateChange(e.target.value)}
|
||
placeholder="50"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2 sm:col-span-2">
|
||
<Label htmlFor={`${idPrefix}-risk-tags`} className="text-muted-foreground">
|
||
{t("profile.riskTags", { defaultValue: "风控标签" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-risk-tags`}
|
||
className="h-10 bg-background/50 transition-colors focus:bg-background"
|
||
value={riskTags}
|
||
onChange={(e) => onRiskTagsChange(e.target.value)}
|
||
placeholder={t("profile.riskTagsPlaceholder", {
|
||
defaultValue: "逗号分隔,如 overdue, high_turnover",
|
||
})}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className={cn(
|
||
"pt-2",
|
||
fieldDisabled ? "pointer-events-none opacity-50" : "",
|
||
)}
|
||
>
|
||
<div className="rounded-xl border border-border/70 bg-card overflow-hidden shadow-sm">
|
||
<SwitchRow
|
||
checked={extraRebate}
|
||
onCheckedChange={onExtraRebateChange}
|
||
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
|
||
/>
|
||
<SwitchRow
|
||
checked={canCreatePlayer}
|
||
onCheckedChange={onCanCreatePlayerChange}
|
||
label={t("profile.canCreatePlayer", { defaultValue: "允许创建玩家" })}
|
||
/>
|
||
<SwitchRow
|
||
checked={canCreateChild}
|
||
onCheckedChange={onCanCreateChildChange}
|
||
disabled={!canCreateChildAgent && !isSuperAdmin}
|
||
label={t("profile.canCreateChildAgent", { defaultValue: "允许创建下级代理" })}
|
||
isLast
|
||
/>
|
||
</div>
|
||
{!isCard ? (
|
||
<p className="mt-3 px-1 text-xs text-muted-foreground/80">
|
||
{t("profile.capabilityHint", {
|
||
defaultValue:
|
||
"保存后约束该代理主账号能否开玩家/下级;与平台「代理」角色叠加,以本开关为准。",
|
||
})}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SwitchRow({
|
||
checked,
|
||
onCheckedChange,
|
||
label,
|
||
disabled = false,
|
||
isLast = false,
|
||
}: {
|
||
checked: boolean;
|
||
onCheckedChange: (value: boolean) => void;
|
||
label: string;
|
||
disabled?: boolean;
|
||
isLast?: boolean;
|
||
}): React.ReactElement {
|
||
return (
|
||
<div className={cn(
|
||
"flex items-center justify-between gap-4 px-4 py-3.5 bg-background/50 transition-colors hover:bg-muted/30",
|
||
!isLast && "border-b border-border/50"
|
||
)}>
|
||
<Label className={cn("font-medium cursor-pointer", disabled && "opacity-50")} onClick={() => !disabled && onCheckedChange(!checked)}>{label}</Label>
|
||
<Switch checked={checked} onCheckedChange={onCheckedChange} disabled={disabled} />
|
||
</div>
|
||
);
|
||
}
|