Files
lotteryAdmin/src/modules/agents/agent-profile-fields.tsx

269 lines
9.3 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 { 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>
);
}