refactor: update agent API schemas, standardize UI text styling, and enhance settlement credit ledger components
This commit is contained in:
@@ -16,6 +16,8 @@ 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;
|
||||
@@ -31,8 +33,6 @@ export type AgentProfileFieldsProps = {
|
||||
onRebateLimitChange: (value: string) => void;
|
||||
defaultRebate: string;
|
||||
onDefaultRebateChange: (value: string) => void;
|
||||
settlementCycle: "daily" | "weekly" | "monthly";
|
||||
onSettlementCycleChange: (value: "daily" | "weekly" | "monthly") => void;
|
||||
extraRebate: boolean;
|
||||
onExtraRebateChange: (value: boolean) => void;
|
||||
canCreatePlayer: boolean;
|
||||
@@ -62,8 +62,6 @@ export function AgentProfileFields({
|
||||
onRebateLimitChange,
|
||||
defaultRebate,
|
||||
onDefaultRebateChange,
|
||||
settlementCycle,
|
||||
onSettlementCycleChange,
|
||||
extraRebate,
|
||||
onExtraRebateChange,
|
||||
canCreatePlayer,
|
||||
@@ -81,47 +79,48 @@ export function AgentProfileFields({
|
||||
const isCard = variant === "card";
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-6">
|
||||
{(parentCaps || availableCredit !== null) && !loading ? (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg text-xs text-muted-foreground",
|
||||
isCard ? "border border-border/60 bg-muted/25 px-3 py-2.5 space-y-1" : "space-y-1",
|
||||
)}
|
||||
>
|
||||
{parentCaps ? (
|
||||
<p>
|
||||
{t("profile.parentCaps", {
|
||||
defaultValue: "上级占成 {{share}}%,可下发额度 {{credit}}",
|
||||
share: parentCaps.total_share_rate,
|
||||
credit: formatAdminCreditMajorDecimal(parentCaps.available_credit, currencyCode),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{availableCredit !== null ? (
|
||||
<p>
|
||||
{t("profile.availableCredit", {
|
||||
defaultValue: "可下发额度:{{amount}}",
|
||||
amount: formatAdminCreditMajorDecimal(availableCredit, currencyCode),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<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">
|
||||
<p className="text-sm text-muted-foreground animate-pulse">
|
||||
{t("profile.loading", { defaultValue: "正在加载占成与授信…" })}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 sm:grid-cols-2",
|
||||
"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`}>
|
||||
<Label htmlFor={`${idPrefix}-share-rate`} className="text-muted-foreground">
|
||||
{parentCaps
|
||||
? t("profile.relativeShareRate", { defaultValue: "占成比例(占上级 %)" })
|
||||
: t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
||||
@@ -132,11 +131,12 @@ export function AgentProfileFields({
|
||||
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">
|
||||
<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)),
|
||||
@@ -145,19 +145,20 @@ export function AgentProfileFields({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`${idPrefix}-credit-limit`}>
|
||||
<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`}>
|
||||
<Label htmlFor={`${idPrefix}-rebate-limit`} className="text-muted-foreground">
|
||||
{t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -166,13 +167,14 @@ export function AgentProfileFields({
|
||||
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="0.5"
|
||||
placeholder="50"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`${idPrefix}-default-rebate`}>
|
||||
<Label htmlFor={`${idPrefix}-default-rebate`} className="text-muted-foreground">
|
||||
{t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -181,17 +183,19 @@ export function AgentProfileFields({
|
||||
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="0.5"
|
||||
placeholder="50"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 sm:col-span-2">
|
||||
<Label htmlFor={`${idPrefix}-risk-tags`}>
|
||||
<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", {
|
||||
@@ -199,49 +203,15 @@ export function AgentProfileFields({
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 sm:col-span-2">
|
||||
<Label htmlFor={`${idPrefix}-settlement-cycle`}>
|
||||
{t("profile.settlementCycle", { defaultValue: "结算周期" })}
|
||||
</Label>
|
||||
<Select
|
||||
value={settlementCycle}
|
||||
onValueChange={(value) =>
|
||||
onSettlementCycleChange((value as "daily" | "weekly" | "monthly") ?? "weekly")
|
||||
}
|
||||
>
|
||||
<SelectTrigger id={`${idPrefix}-settlement-cycle`}>
|
||||
<SelectValue>
|
||||
{settlementCycle === "daily"
|
||||
? t("profile.cycleDaily", { defaultValue: "日结" })
|
||||
: settlementCycle === "monthly"
|
||||
? t("profile.cycleMonthly", { defaultValue: "月结" })
|
||||
: t("profile.cycleWeekly", { defaultValue: "周结" })}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="daily">{t("profile.cycleDaily", { defaultValue: "日结" })}</SelectItem>
|
||||
<SelectItem value="weekly">{t("profile.cycleWeekly", { defaultValue: "周结" })}</SelectItem>
|
||||
<SelectItem value="monthly">{t("profile.cycleMonthly", { defaultValue: "月结" })}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-4 border-t border-border/60 pt-4",
|
||||
"pt-2",
|
||||
fieldDisabled ? "pointer-events-none opacity-50" : "",
|
||||
)}
|
||||
>
|
||||
{!isCard ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("profile.capabilityHint", {
|
||||
defaultValue:
|
||||
"保存后约束该代理主账号能否开玩家/下级;与平台「代理」角色叠加,以本开关为准。",
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid gap-4 sm:grid-cols-1">
|
||||
<div className="rounded-xl border border-border/70 bg-card overflow-hidden shadow-sm">
|
||||
<SwitchRow
|
||||
checked={extraRebate}
|
||||
onCheckedChange={onExtraRebateChange}
|
||||
@@ -257,8 +227,17 @@ export function AgentProfileFields({
|
||||
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>
|
||||
);
|
||||
@@ -269,15 +248,20 @@ function SwitchRow({
|
||||
onCheckedChange,
|
||||
label,
|
||||
disabled = false,
|
||||
isLast = false,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onCheckedChange: (value: boolean) => void;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
isLast?: boolean;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-border/60 bg-muted/20 px-3 py-2.5">
|
||||
<Label className="font-normal">{label}</Label>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user