refactor: update agent API schemas, standardize UI text styling, and enhance settlement credit ledger components

This commit is contained in:
2026-06-11 18:02:02 +08:00
parent 44ad51698f
commit 1eb6702c51
54 changed files with 1888 additions and 1103 deletions

View File

@@ -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>
);