refactor: update agent API schemas, standardize UI text styling, and enhance settlement credit ledger components
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Eye, Pencil, Plus, ReceiptText, Trash2 } from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -57,7 +57,7 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"
|
||||
import { PlayerFundingModeBadge } from "@/components/admin/player-funding-badges";
|
||||
import { formatPlayerCreditAmount, playerBalanceCells } from "@/lib/admin-player-display";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
import { parsePercentUi, percentUiToRatio, ratioToPercentUi } from "@/lib/admin-rate-percent";
|
||||
import { parsePercentUi, percentValueToUi } from "@/lib/admin-rate-percent";
|
||||
import { adminPlayerDetailPath } from "@/lib/admin-player-paths";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { PRD_USERS_MANAGE } from "@/lib/admin-prd";
|
||||
@@ -136,7 +136,7 @@ function fillEditFormFromPlayer(row: AdminPlayerRow): {
|
||||
currency: row.default_currency ?? "",
|
||||
status: row.status,
|
||||
creditLimit: row.credit_limit ?? 0,
|
||||
rebateRate: rebate != null ? ratioToPercentUi(rebate) : "",
|
||||
rebateRate: rebate != null ? percentValueToUi(rebate) : "",
|
||||
riskTags: (row.risk_tags ?? []).join(", "),
|
||||
};
|
||||
}
|
||||
@@ -149,6 +149,8 @@ type AgentsPlayersPanelProps = {
|
||||
allowCreatePlayer?: boolean;
|
||||
/** 嵌入代理线路详情 Tab 时使用紧凑顶栏 */
|
||||
embedded?: boolean;
|
||||
/** 外部触发创建直属玩家的计数器 */
|
||||
createRequestKey?: number;
|
||||
};
|
||||
|
||||
export function AgentsPlayersPanel({
|
||||
@@ -156,6 +158,7 @@ export function AgentsPlayersPanel({
|
||||
agentNodeId,
|
||||
allowCreatePlayer,
|
||||
embedded = false,
|
||||
createRequestKey = 0,
|
||||
}: AgentsPlayersPanelProps): React.ReactElement {
|
||||
const { t } = useTranslation(["agents", "players", "common"]);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
@@ -226,6 +229,7 @@ export function AgentsPlayersPanel({
|
||||
const [payMethod, setPayMethod] = useState("");
|
||||
const [payProof, setPayProof] = useState("");
|
||||
const [badDebtReason, setBadDebtReason] = useState("");
|
||||
const lastCreateRequestKeyRef = useRef(createRequestKey);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (siteCode.trim() === "") {
|
||||
@@ -269,6 +273,46 @@ export function AgentsPlayersPanel({
|
||||
toast.error(t("playersPanel.loginRequired", { defaultValue: "请填写登录账号与初始密码" }));
|
||||
return;
|
||||
}
|
||||
if (password.trim().length < 8) {
|
||||
toast.error(
|
||||
t("playersPanel.passwordMinLength", { defaultValue: "初始密码至少 8 位" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedCreditLimit =
|
||||
creditLimit.trim() === "" ? 0 : Number.parseInt(creditLimit, 10);
|
||||
if (
|
||||
Number.isNaN(parsedCreditLimit) ||
|
||||
parsedCreditLimit < 0 ||
|
||||
!Number.isInteger(parsedCreditLimit)
|
||||
) {
|
||||
toast.error(
|
||||
t("playersPanel.creditLimitInvalid", { defaultValue: "授信额度必须为不小于 0 的整数" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
parentAvailableCredit !== null &&
|
||||
parsedCreditLimit > parentAvailableCredit
|
||||
) {
|
||||
toast.error(
|
||||
t("playersPanel.creditLimitExceeded", {
|
||||
defaultValue: "授信额度不能超过当前代理可下发额度",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedRebateRate = rebateRate.trim() === "" ? null : parsePercentUi(rebateRate);
|
||||
if (rebateRate.trim() !== "" && (parsedRebateRate === null || parsedRebateRate < 0 || parsedRebateRate > 100)) {
|
||||
toast.error(
|
||||
t("playersPanel.rebateRateInvalid", {
|
||||
defaultValue: "回水比例须在 0–100% 之间",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -278,10 +322,9 @@ export function AgentsPlayersPanel({
|
||||
password: password,
|
||||
nickname: nickname.trim() || null,
|
||||
...(isSuperAdmin && effectiveAgentId ? { agent_node_id: effectiveAgentId } : {}),
|
||||
credit_limit:
|
||||
creditLimit.trim() === "" ? 0 : Math.max(0, Number.parseInt(creditLimit, 10) || 0),
|
||||
...(rebateRate.trim() !== ""
|
||||
? { rebate_rate: percentUiToRatio(rebateRate) }
|
||||
credit_limit: parsedCreditLimit,
|
||||
...(parsedRebateRate !== null
|
||||
? { rebate_rate: parsedRebateRate }
|
||||
: {}),
|
||||
});
|
||||
toast.success(
|
||||
@@ -306,6 +349,11 @@ export function AgentsPlayersPanel({
|
||||
|
||||
function openCreateDialog(): void {
|
||||
setDialogOpen(true);
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setNickname("");
|
||||
setCreditLimit("");
|
||||
setRebateRate("");
|
||||
if (effectiveAgentId !== null) {
|
||||
void getAgentNodeProfile(effectiveAgentId)
|
||||
.then((p) => setParentAvailableCredit(p.available_credit ?? null))
|
||||
@@ -315,6 +363,16 @@ export function AgentsPlayersPanel({
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (createRequestKey === 0 || createRequestKey === lastCreateRequestKeyRef.current) {
|
||||
return;
|
||||
}
|
||||
lastCreateRequestKeyRef.current = createRequestKey;
|
||||
if (canCreatePlayer) {
|
||||
openCreateDialog();
|
||||
}
|
||||
}, [canCreatePlayer, createRequestKey]);
|
||||
|
||||
const applyEditForm = (row: AdminPlayerRow): void => {
|
||||
const form = fillEditFormFromPlayer(row);
|
||||
setEditUsername(form.username);
|
||||
@@ -382,7 +440,7 @@ export function AgentsPlayersPanel({
|
||||
}
|
||||
const prevRebate = resolvePlayerRebateRate(editingPlayer);
|
||||
const nextPercent = parsePercentUi(editRebateRate);
|
||||
const nextRebate = nextPercent === null ? null : percentUiToRatio(nextPercent);
|
||||
const nextRebate = nextPercent === null ? null : nextPercent;
|
||||
if (nextRebate !== null && nextRebate !== (prevRebate ?? 0)) {
|
||||
body.rebate_rate = nextRebate;
|
||||
}
|
||||
@@ -648,13 +706,9 @@ export function AgentsPlayersPanel({
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("playersPanel.creditListHint", {
|
||||
defaultValue: "信用占成盘:下列为玩家授信额度与可用信用,非主站钱包余额。",
|
||||
})}
|
||||
</p>
|
||||
<div />
|
||||
)}
|
||||
{canCreatePlayer ? (
|
||||
{canCreatePlayer && !embedded ? (
|
||||
<Button type="button" size="sm" className="shrink-0" onClick={openCreateDialog}>
|
||||
<Plus className="mr-1.5 size-3.5" />
|
||||
{createPlayerLabel}
|
||||
@@ -693,7 +747,7 @@ export function AgentsPlayersPanel({
|
||||
{!embedded ? (
|
||||
<TableHead className="w-24">{t("players:status", { defaultValue: "状态" })}</TableHead>
|
||||
) : null}
|
||||
<TableHead className="sticky right-0 z-10 w-14 bg-muted/40 text-center shadow-[-1px_0_0_var(--border)]">
|
||||
<TableHead className="sticky right-0 z-20 w-14 bg-muted whitespace-nowrap text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
||||
{t("common:table.actions", { defaultValue: "操作" })}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -750,7 +804,7 @@ export function AgentsPlayersPanel({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{rebate != null ? `${ratioToPercentUi(rebate)}%` : "—"}
|
||||
{rebate != null ? `${percentValueToUi(rebate)}%` : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
||||
{row.last_login_at ? formatDt(row.last_login_at) : "—"}
|
||||
@@ -763,7 +817,7 @@ export function AgentsPlayersPanel({
|
||||
</TableCell>
|
||||
) : null}
|
||||
<TableCell
|
||||
className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_var(--border)]"
|
||||
className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AdminRowActionsMenu
|
||||
@@ -844,83 +898,105 @@ export function AgentsPlayersPanel({
|
||||
)}
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogContent className="sm:max-w-[460px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{createPlayerLabel}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("agents:lineProvision.code", { defaultValue: "代理编码" })}</Label>
|
||||
<Input value={siteCode} readOnly disabled />
|
||||
|
||||
<div className="grid gap-5 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-muted-foreground">{t("playersPanel.siteCode", { defaultValue: "所属线路" })}</Label>
|
||||
<Input value={siteCode} readOnly disabled className="bg-muted/40 text-muted-foreground opacity-100 font-mono" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-x-4 gap-y-5 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-username" className="text-muted-foreground">
|
||||
{t("playersPanel.loginUsername", { defaultValue: "登录账号" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-password" className="text-muted-foreground">
|
||||
{t("playersPanel.initialPassword", { defaultValue: "初始密码" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
{t("playersPanel.passwordHint", { defaultValue: "至少 8 位" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-nickname" className="text-muted-foreground">
|
||||
{t("players:nickname", { defaultValue: "昵称" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-nickname"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-x-4 gap-y-5 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-credit" className="text-muted-foreground">
|
||||
{t("playersPanel.creditLimit", { defaultValue: "授信额度" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-credit"
|
||||
type="number"
|
||||
min={0}
|
||||
value={creditLimit}
|
||||
onChange={(e) => setCreditLimit(e.target.value)}
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
{parentAvailableCredit !== null ? (
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
{t("playersPanel.availableToGrant", {
|
||||
defaultValue: "代理剩余可下发:{{amount}}",
|
||||
amount: formatCredit(parentAvailableCredit),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-rebate" className="text-muted-foreground">
|
||||
{t("playersPanel.rebateRate", { defaultValue: "回水比例 (%)" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-rebate"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step="0.01"
|
||||
value={rebateRate}
|
||||
placeholder="0"
|
||||
onChange={(e) => setRebateRate(e.target.value)}
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
{t("playersPanel.rebateRateHint", { defaultValue: "填写百分比,如 5 表示 5%" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-username">
|
||||
{t("playersPanel.loginUsername", { defaultValue: "登录账号" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-password">
|
||||
{t("playersPanel.initialPassword", { defaultValue: "初始密码" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-nickname">
|
||||
{t("players:nickname", { defaultValue: "昵称" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-nickname"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-credit">
|
||||
{t("playersPanel.creditLimit", { defaultValue: "授信额度" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-credit"
|
||||
type="number"
|
||||
min={0}
|
||||
value={creditLimit}
|
||||
onChange={(e) => setCreditLimit(e.target.value)}
|
||||
/>
|
||||
{parentAvailableCredit !== null ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("playersPanel.availableToGrant", {
|
||||
defaultValue: "代理剩余可下发:{{amount}}",
|
||||
amount: formatCredit(parentAvailableCredit),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-rebate">
|
||||
{t("playersPanel.rebateRate", { defaultValue: "回水比例 (%)" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-rebate"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step="0.01"
|
||||
value={rebateRate}
|
||||
placeholder="0.5"
|
||||
onChange={(e) => setRebateRate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
<DialogFooter className="mt-2">
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
{t("common:actions.cancel", { defaultValue: "取消" })}
|
||||
</Button>
|
||||
@@ -1174,10 +1250,10 @@ export function AgentsPlayersPanel({
|
||||
step="0.01"
|
||||
value={editRebateRate}
|
||||
onChange={(e) => setEditRebateRate(e.target.value)}
|
||||
placeholder="0.5"
|
||||
placeholder="0"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("playersPanel.rebateRateHint", { defaultValue: "填写百分比,如 0.5 表示 0.5%" })}
|
||||
{t("playersPanel.rebateRateHint", { defaultValue: "填写百分比,如 5 表示 5%" })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user