feat(agents, validation): enhance agent profile and player management with input validation
Added input validation for admin login and player creation forms, ensuring usernames and passwords meet specified criteria. Introduced new components for numeric input handling in agent profile fields, improving user experience. Updated agent line detail and provision wizard to reflect these changes, enhancing overall data integrity and user interaction.
This commit is contained in:
@@ -20,7 +20,8 @@ import {
|
||||
postAdminPlayer,
|
||||
putAdminPlayer,
|
||||
} from "@/api/admin-player";
|
||||
import { formatCredit } from "@/modules/agents/agent-line-sidebar";
|
||||
import { AdminNumericStepper } from "@/components/admin/admin-numeric-stepper";
|
||||
import { PlayerCreditLimitField } from "@/components/admin/player-credit-limit-field";
|
||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
@@ -55,12 +56,18 @@ import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
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 { playerBalanceCells } from "@/lib/admin-player-display";
|
||||
import { formatAdminMinorDecimal, formatAdminMinorUnits, parseAdminMajorToMinor } from "@/lib/money";
|
||||
import { parsePercentUi, percentValueToUi } from "@/lib/admin-rate-percent";
|
||||
import { adminPlayerDetailPath } from "@/lib/admin-player-paths";
|
||||
import { AGENT_PERCENT_HARD_MAX } from "@/lib/agent-profile-caps";
|
||||
import {
|
||||
validateNativePlayerPassword,
|
||||
validateNativePlayerUsername,
|
||||
} from "@/lib/admin-input-validation";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { PRD_USERS_MANAGE } from "@/lib/admin-prd";
|
||||
import { isSiteAdminOperator } from "@/lib/admin-session-variants";
|
||||
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
@@ -86,15 +93,6 @@ function playerStatusLabel(
|
||||
return String(status);
|
||||
}
|
||||
|
||||
function creditAdjustModeLabel(
|
||||
mode: "increase" | "decrease",
|
||||
t: (key: string, opts?: { defaultValue?: string }) => string,
|
||||
): string {
|
||||
return mode === "increase"
|
||||
? t("playersPanel.creditIncrease", { defaultValue: "增加授信" })
|
||||
: t("playersPanel.creditDecrease", { defaultValue: "减少授信" });
|
||||
}
|
||||
|
||||
function resolvePlayerRebateRate(row: AdminPlayerRow): number | null {
|
||||
if (row.rebate_rate != null) {
|
||||
return row.rebate_rate;
|
||||
@@ -171,6 +169,7 @@ export function AgentsPlayersPanel({
|
||||
const profile = useAdminProfile();
|
||||
const boundAgent = profile?.agent ?? null;
|
||||
const isSuperAdmin = profile?.is_super_admin === true;
|
||||
const isSiteAdmin = isSiteAdminOperator(profile);
|
||||
const { request: requestConfirm, ConfirmDialog, busy: confirmBusy } = useConfirmAction();
|
||||
|
||||
const profileAllowsCreate =
|
||||
@@ -206,6 +205,15 @@ export function AgentsPlayersPanel({
|
||||
const [creditLimit, setCreditLimit] = useState("");
|
||||
const [rebateRate, setRebateRate] = useState("");
|
||||
const [parentAvailableCredit, setParentAvailableCredit] = useState<number | null>(null);
|
||||
const [agentRebateLimitPercent, setAgentRebateLimitPercent] = useState<number | null>(null);
|
||||
|
||||
const maxPlayerRebatePercent = useMemo(() => {
|
||||
if (agentRebateLimitPercent === null || !Number.isFinite(agentRebateLimitPercent)) {
|
||||
return AGENT_PERCENT_HARD_MAX;
|
||||
}
|
||||
return Math.min(AGENT_PERCENT_HARD_MAX, Math.max(0, agentRebateLimitPercent));
|
||||
}, [agentRebateLimitPercent]);
|
||||
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editSaving, setEditSaving] = useState(false);
|
||||
const [editingPlayer, setEditingPlayer] = useState<Awaited<ReturnType<typeof getAdminPlayers>>["items"][number] | null>(null);
|
||||
@@ -213,9 +221,7 @@ export function AgentsPlayersPanel({
|
||||
const [editNickname, setEditNickname] = useState("");
|
||||
const [editDefaultCurrency, setEditDefaultCurrency] = useState("");
|
||||
const [editStatus, setEditStatus] = useState(0);
|
||||
const [editCreditBase, setEditCreditBase] = useState(0);
|
||||
const [editCreditAdjustMode, setEditCreditAdjustMode] = useState<"increase" | "decrease">("increase");
|
||||
const [editCreditDelta, setEditCreditDelta] = useState("");
|
||||
const [editCreditLimit, setEditCreditLimit] = useState("");
|
||||
const [editRebateRate, setEditRebateRate] = useState("");
|
||||
const [editRiskTags, setEditRiskTags] = useState("");
|
||||
const [editDetailLoading, setEditDetailLoading] = useState(false);
|
||||
@@ -273,9 +279,19 @@ export function AgentsPlayersPanel({
|
||||
toast.error(t("playersPanel.loginRequired", { defaultValue: "请填写登录账号与初始密码" }));
|
||||
return;
|
||||
}
|
||||
if (password.trim().length < 8) {
|
||||
const usernameIssue = validateNativePlayerUsername(username);
|
||||
if (usernameIssue === "invalid_charset") {
|
||||
toast.error(
|
||||
t("playersPanel.passwordMinLength", { defaultValue: "初始密码至少 8 位" }),
|
||||
t("playersPanel.usernameInvalidCharset", {
|
||||
defaultValue: "登录账号只能使用字母、数字、点(.)、下划线和连字符",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const passwordIssue = validateNativePlayerPassword(password);
|
||||
if (passwordIssue === "too_short") {
|
||||
toast.error(
|
||||
t("playersPanel.passwordMinLength", { defaultValue: "初始密码至少 6 位" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -314,6 +330,16 @@ export function AgentsPlayersPanel({
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedRebateRate !== null && parsedRebateRate > maxPlayerRebatePercent) {
|
||||
toast.error(
|
||||
t("playersPanel.rebateRateExceedsAgent", {
|
||||
defaultValue: "回水比例不能超过代理回水上限 {{max}}%",
|
||||
max: maxPlayerRebatePercent,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await postAdminPlayer({
|
||||
@@ -321,7 +347,9 @@ export function AgentsPlayersPanel({
|
||||
username: username.trim(),
|
||||
password: password,
|
||||
nickname: nickname.trim() || null,
|
||||
...(isSuperAdmin && effectiveAgentId ? { agent_node_id: effectiveAgentId } : {}),
|
||||
...(effectiveAgentId != null && (isSuperAdmin || isSiteAdmin || boundAgent !== null)
|
||||
? { agent_node_id: effectiveAgentId }
|
||||
: {}),
|
||||
credit_limit: parsedCreditLimit,
|
||||
...(parsedRebateRate !== null
|
||||
? { rebate_rate: parsedRebateRate }
|
||||
@@ -356,10 +384,18 @@ export function AgentsPlayersPanel({
|
||||
setRebateRate("");
|
||||
if (effectiveAgentId !== null) {
|
||||
void getAgentNodeProfile(effectiveAgentId)
|
||||
.then((p) => setParentAvailableCredit(p.available_credit ?? null))
|
||||
.catch(() => setParentAvailableCredit(null));
|
||||
.then((p) => {
|
||||
setParentAvailableCredit(p.available_credit ?? null);
|
||||
const rebate = Number(p.rebate_limit);
|
||||
setAgentRebateLimitPercent(Number.isFinite(rebate) ? rebate : null);
|
||||
})
|
||||
.catch(() => {
|
||||
setParentAvailableCredit(null);
|
||||
setAgentRebateLimitPercent(null);
|
||||
});
|
||||
} else {
|
||||
setParentAvailableCredit(null);
|
||||
setAgentRebateLimitPercent(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,9 +415,7 @@ export function AgentsPlayersPanel({
|
||||
setEditNickname(form.nickname);
|
||||
setEditDefaultCurrency(form.currency);
|
||||
setEditStatus(form.status);
|
||||
setEditCreditBase(form.creditLimit);
|
||||
setEditCreditAdjustMode("increase");
|
||||
setEditCreditDelta("");
|
||||
setEditCreditLimit(String(form.creditLimit));
|
||||
setEditRebateRate(form.rebateRate);
|
||||
setEditRiskTags(form.riskTags);
|
||||
};
|
||||
@@ -391,6 +425,18 @@ export function AgentsPlayersPanel({
|
||||
applyEditForm(row);
|
||||
setEditDialogOpen(true);
|
||||
setEditDetailLoading(true);
|
||||
if (effectiveAgentId !== null) {
|
||||
void getAgentNodeProfile(effectiveAgentId)
|
||||
.then((p) => {
|
||||
setParentAvailableCredit(p.available_credit ?? null);
|
||||
const rebate = Number(p.rebate_limit);
|
||||
setAgentRebateLimitPercent(Number.isFinite(rebate) ? rebate : null);
|
||||
})
|
||||
.catch(() => {
|
||||
setParentAvailableCredit(null);
|
||||
setAgentRebateLimitPercent(null);
|
||||
});
|
||||
}
|
||||
void getAdminPlayer(row.id)
|
||||
.then((full) => {
|
||||
setEditingPlayer(full);
|
||||
@@ -418,6 +464,15 @@ export function AgentsPlayersPanel({
|
||||
|
||||
const body: Parameters<typeof putAdminPlayer>[1] = {};
|
||||
if (editUsername.trim() !== "" && editUsername.trim() !== (editingPlayer.username ?? "")) {
|
||||
const usernameIssue = validateNativePlayerUsername(editUsername);
|
||||
if (usernameIssue === "invalid_charset") {
|
||||
toast.error(
|
||||
t("playersPanel.usernameInvalidCharset", {
|
||||
defaultValue: "登录账号只能使用字母、数字、点(.)、下划线和连字符",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
body.username = editUsername.trim();
|
||||
}
|
||||
if (editNickname.trim() !== (editingPlayer.nickname ?? "")) {
|
||||
@@ -430,17 +485,56 @@ export function AgentsPlayersPanel({
|
||||
if (editStatus !== editingPlayer.status) {
|
||||
body.status = editStatus;
|
||||
}
|
||||
const creditDelta = editCreditDelta.trim() === "" ? 0 : Number.parseInt(editCreditDelta, 10);
|
||||
if (!Number.isNaN(creditDelta) && creditDelta > 0) {
|
||||
const signedDelta = editCreditAdjustMode === "increase" ? creditDelta : -creditDelta;
|
||||
const nextCredit = Math.max(0, (editingPlayer.credit_limit ?? 0) + signedDelta);
|
||||
if (nextCredit !== (editingPlayer.credit_limit ?? 0)) {
|
||||
body.credit_limit = nextCredit;
|
||||
}
|
||||
const baselineCredit = editingPlayer.credit_limit ?? 0;
|
||||
const parsedCreditLimit =
|
||||
editCreditLimit.trim() === "" ? baselineCredit : Number.parseInt(editCreditLimit, 10);
|
||||
if (
|
||||
Number.isNaN(parsedCreditLimit) ||
|
||||
parsedCreditLimit < 0 ||
|
||||
!Number.isInteger(parsedCreditLimit)
|
||||
) {
|
||||
toast.error(
|
||||
t("playersPanel.creditLimitInvalid", { defaultValue: "授信额度必须为不小于 0 的整数" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
parentAvailableCredit !== null &&
|
||||
parsedCreditLimit > baselineCredit + parentAvailableCredit
|
||||
) {
|
||||
toast.error(
|
||||
t("playersPanel.creditLimitExceeded", {
|
||||
defaultValue: "授信额度不能超过当前代理可下发额度",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (parsedCreditLimit !== baselineCredit) {
|
||||
body.credit_limit = parsedCreditLimit;
|
||||
}
|
||||
const prevRebate = resolvePlayerRebateRate(editingPlayer);
|
||||
const nextPercent = parsePercentUi(editRebateRate);
|
||||
const nextRebate = nextPercent === null ? null : nextPercent;
|
||||
if (
|
||||
nextRebate !== null &&
|
||||
(nextRebate < 0 || nextRebate > AGENT_PERCENT_HARD_MAX)
|
||||
) {
|
||||
toast.error(
|
||||
t("playersPanel.rebateRateInvalid", {
|
||||
defaultValue: "回水比例须在 0–100% 之间",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (nextRebate !== null && nextRebate > maxPlayerRebatePercent) {
|
||||
toast.error(
|
||||
t("playersPanel.rebateRateExceedsAgent", {
|
||||
defaultValue: "回水比例不能超过代理回水上限 {{max}}%",
|
||||
max: maxPlayerRebatePercent,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (nextRebate !== null && nextRebate !== (prevRebate ?? 0)) {
|
||||
body.rebate_rate = nextRebate;
|
||||
}
|
||||
@@ -505,13 +599,6 @@ export function AgentsPlayersPanel({
|
||||
);
|
||||
const billingCurrency =
|
||||
selectedBill?.currency_code ?? billingPlayer?.default_currency ?? "NPR";
|
||||
const projectedCreditLimit = useMemo(() => {
|
||||
const delta = editCreditDelta.trim() === "" ? 0 : Number.parseInt(editCreditDelta, 10);
|
||||
if (Number.isNaN(delta) || delta <= 0) {
|
||||
return editCreditBase;
|
||||
}
|
||||
return Math.max(0, editCreditBase + (editCreditAdjustMode === "increase" ? delta : -delta));
|
||||
}, [editCreditAdjustMode, editCreditBase, editCreditDelta]);
|
||||
|
||||
function resetBillingForm(): void {
|
||||
setPayAmount("");
|
||||
@@ -957,44 +1044,33 @@ export function AgentsPlayersPanel({
|
||||
</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>
|
||||
<PlayerCreditLimitField
|
||||
id="agent-player-credit"
|
||||
value={creditLimit}
|
||||
onValueChange={setCreditLimit}
|
||||
parentAvailableCredit={parentAvailableCredit}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-rebate" className="text-muted-foreground">
|
||||
{t("playersPanel.rebateRate", { defaultValue: "回水比例 (%)" })}
|
||||
</Label>
|
||||
<Input
|
||||
<AdminNumericStepper
|
||||
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"
|
||||
onValueChange={setRebateRate}
|
||||
min={0}
|
||||
max={maxPlayerRebatePercent}
|
||||
step={0.5}
|
||||
suffix="%"
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
{t("playersPanel.rebateRateHint", { defaultValue: "填写百分比,如 5 表示 5%" })}
|
||||
{agentRebateLimitPercent !== null
|
||||
? t("playersPanel.rebateAgentCapHint", {
|
||||
defaultValue: "不得超过代理回水上限 {{max}}%",
|
||||
max: maxPlayerRebatePercent,
|
||||
})
|
||||
: t("playersPanel.rebateRateStepHint", { defaultValue: "使用 +/- 调整回水比例" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1160,14 +1236,14 @@ export function AgentsPlayersPanel({
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={editDialogOpen} onOpenChange={handleEditDialogOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogContent className="sm:max-w-[460px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("players:editDialogTitle", { defaultValue: "编辑玩家" })}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{editDetailLoading ? <AdminLoadingState minHeight="6rem" /> : null}
|
||||
<div className={editDetailLoading ? "hidden" : "space-y-3"}>
|
||||
<div className={editDetailLoading ? "hidden" : "grid gap-5 py-2"}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-edit-username">
|
||||
<Label htmlFor="agent-player-edit-username" className="text-muted-foreground">
|
||||
{t("players:username", { defaultValue: "用户名" })}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -1175,10 +1251,11 @@ export function AgentsPlayersPanel({
|
||||
value={editUsername}
|
||||
onChange={(e) => setEditUsername(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-edit-nickname">
|
||||
<Label htmlFor="agent-player-edit-nickname" className="text-muted-foreground">
|
||||
{t("players:nickname", { defaultValue: "昵称" })}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -1186,10 +1263,11 @@ export function AgentsPlayersPanel({
|
||||
value={editNickname}
|
||||
onChange={(e) => setEditNickname(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-edit-currency">
|
||||
<Label htmlFor="agent-player-edit-currency" className="text-muted-foreground">
|
||||
{t("players:defaultCurrency", { defaultValue: "默认币种" })}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -1197,87 +1275,41 @@ export function AgentsPlayersPanel({
|
||||
value={editDefaultCurrency}
|
||||
onChange={(e) => setEditDefaultCurrency(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
<PlayerCreditLimitField
|
||||
id="agent-player-edit-credit"
|
||||
value={editCreditLimit}
|
||||
onValueChange={setEditCreditLimit}
|
||||
parentAvailableCredit={parentAvailableCredit}
|
||||
baselineCreditLimit={editingPlayer?.credit_limit ?? 0}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-edit-credit">
|
||||
{t("playersPanel.creditLimit", { defaultValue: "授信额度" })}
|
||||
</Label>
|
||||
<div className="rounded-xl border border-border/70 bg-muted/20 p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t("playersPanel.currentCredit", { defaultValue: "当前授信" })}
|
||||
</span>
|
||||
<span className="font-semibold">
|
||||
{formatPlayerCreditAmount(editCreditBase, editDefaultCurrency || "NPR")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-[9rem_minmax(0,1fr)]">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="agent-player-edit-credit-mode">
|
||||
{t("playersPanel.creditAdjustType", { defaultValue: "调整方式" })}
|
||||
</Label>
|
||||
<Select
|
||||
value={editCreditAdjustMode}
|
||||
onValueChange={(value) => setEditCreditAdjustMode(value as "increase" | "decrease")}
|
||||
>
|
||||
<SelectTrigger id="agent-player-edit-credit-mode">
|
||||
<SelectValue>
|
||||
{creditAdjustModeLabel(editCreditAdjustMode, t)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="increase">
|
||||
{creditAdjustModeLabel("increase", t)}
|
||||
</SelectItem>
|
||||
<SelectItem value="decrease">
|
||||
{creditAdjustModeLabel("decrease", t)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="agent-player-edit-credit-delta">
|
||||
{t("playersPanel.creditAdjustAmount", { defaultValue: "调整额度" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="agent-player-edit-credit-delta"
|
||||
type="number"
|
||||
min={0}
|
||||
value={editCreditDelta}
|
||||
onChange={(e) => setEditCreditDelta(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-muted-foreground">
|
||||
{t("playersPanel.creditProjected", {
|
||||
defaultValue: "调整后授信:{{amount}}",
|
||||
amount: formatPlayerCreditAmount(projectedCreditLimit, editDefaultCurrency || "NPR"),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-edit-rebate">
|
||||
<Label htmlFor="agent-player-edit-rebate" className="text-muted-foreground">
|
||||
{t("playersPanel.rebateRate", { defaultValue: "回水比例 (%)" })}
|
||||
</Label>
|
||||
<Input
|
||||
<AdminNumericStepper
|
||||
id="agent-player-edit-rebate"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step="0.01"
|
||||
value={editRebateRate}
|
||||
onChange={(e) => setEditRebateRate(e.target.value)}
|
||||
placeholder="0"
|
||||
onValueChange={setEditRebateRate}
|
||||
min={0}
|
||||
max={maxPlayerRebatePercent}
|
||||
step={0.5}
|
||||
suffix="%"
|
||||
preserveOverMaxOnBlur
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("playersPanel.rebateRateHint", { defaultValue: "填写百分比,如 5 表示 5%" })}
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
{agentRebateLimitPercent !== null
|
||||
? t("playersPanel.rebateAgentCapHint", {
|
||||
defaultValue: "不得超过代理回水上限 {{max}}%",
|
||||
max: maxPlayerRebatePercent,
|
||||
})
|
||||
: t("playersPanel.rebateRateStepHint", { defaultValue: "使用 +/- 调整回水比例" })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-edit-risk-tags">
|
||||
<Label htmlFor="agent-player-edit-risk-tags" className="text-muted-foreground">
|
||||
{t("playersPanel.riskTags", { defaultValue: "风控标签" })}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -1287,10 +1319,11 @@ export function AgentsPlayersPanel({
|
||||
placeholder={t("playersPanel.riskTagsPlaceholder", {
|
||||
defaultValue: "逗号分隔",
|
||||
})}
|
||||
className="bg-background/50 transition-colors focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-player-edit-status">
|
||||
<Label htmlFor="agent-player-edit-status" className="text-muted-foreground">
|
||||
{t("players:status", { defaultValue: "状态" })}
|
||||
</Label>
|
||||
<Select value={String(editStatus)} onValueChange={(value) => setEditStatus(Number(value))}>
|
||||
@@ -1311,7 +1344,7 @@ export function AgentsPlayersPanel({
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="mt-2">
|
||||
<Button type="button" variant="outline" onClick={() => setEditDialogOpen(false)}>
|
||||
{t("common:actions.cancel", { defaultValue: "取消" })}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user