feat(i18n, agents): enhance multi-language support with new agent-related translations

Added new translations for agent management features in English, Nepali, and Chinese, including terms related to sub-agents, player management, and validation messages. This update improves the user experience by ensuring consistent terminology across different languages in the agents console.
This commit is contained in:
2026-06-04 10:15:19 +08:00
parent cbc499e5b2
commit c2eac2fafc
5 changed files with 355 additions and 38 deletions

View File

@@ -37,6 +37,11 @@
"loadFailed": "Failed to load agent tree",
"siteLabel": "Site",
"createChild": "Add child agent",
"viewDownline": "View sub-agents and players",
"downlineDialogTitle": "{{name}} — sub-agents and players",
"downlineAgentsSection": "Sub-agents",
"downlinePlayersSection": "Direct players",
"downlineNoAgents": "No sub-agents yet",
"editNode": "Edit node",
"deleteNode": "Delete node",
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
@@ -59,6 +64,12 @@
"deleteSuccess": "Deleted agent {{name}}",
"saveFailed": "Save failed",
"codeRequired": "Code and name are required",
"nameRequired": "Agent name is required",
"usernameRequired": "Login name is required",
"passwordRequired": "Password is required",
"passwordMinLength": "Password must be at least 8 characters",
"bindAccountPasswordRequired": "This agent has no login account yet. Enter an initial password to create one.",
"bindAccountHint": "No login account is bound yet. Saving will create and bind one automatically.",
"modelGuide": "Agent layer controls data scope and delegation ceiling. Account permissions are assigned through roles.",
"pageGuide": "Manage the agent tree, agent roles, agent accounts, and delegation ceilings here. Platform accounts and platform roles stay in platform governance pages.",
"summary": {
@@ -83,7 +94,17 @@
"canCreateChildAgent": "Allow creating sub-agents",
"cycleDaily": "Daily",
"cycleWeekly": "Weekly",
"cycleMonthly": "Monthly"
"cycleMonthly": "Monthly",
"loading": "Loading share and credit settings…",
"loadFailed": "Failed to load share and credit settings. Close and try again.",
"loadingBlocked": "Share and credit settings are still loading. Please wait before saving.",
"validation": {
"shareRange": "Share rate must be between 0 and 100",
"creditInvalid": "Credit limit cannot be negative",
"rebateLimitRange": "Rebate ceiling must be between 0 and 1 (e.g. 0.005 = 0.5%)",
"defaultRebateRange": "Default player rebate must be between 0 and 1",
"defaultExceedsLimit": "Default player rebate cannot exceed the rebate ceiling"
}
},
"settlementBills": {
"title": "Agent bills",

View File

@@ -37,6 +37,11 @@
"loadFailed": "Failed to load agent tree",
"siteLabel": "Site",
"createChild": "Add child agent",
"viewDownline": "चाइल्ड एजेन्ट र खेलाडी हेर्नुहोस्",
"downlineDialogTitle": "{{name}} — चाइल्ड एजेन्ट र खेलाडी",
"downlineAgentsSection": "चाइल्ड एजेन्ट",
"downlinePlayersSection": "प्रत्यक्ष खेलाडी",
"downlineNoAgents": "कुनै चाइल्ड एजेन्ट छैन",
"editNode": "Edit node",
"deleteNode": "Delete node",
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
@@ -59,6 +64,12 @@
"deleteSuccess": "Deleted agent {{name}}",
"saveFailed": "Save failed",
"codeRequired": "Code and name are required",
"nameRequired": "Agent name is required",
"usernameRequired": "Login name is required",
"passwordRequired": "Password is required",
"passwordMinLength": "Password must be at least 8 characters",
"bindAccountPasswordRequired": "This agent has no login account yet. Enter an initial password.",
"bindAccountHint": "No login account is bound yet. Saving will create one.",
"modelGuide": "एजेन्ट तहले डाटा स्कोप र delegation ceiling नियन्त्रण गर्छ; खाताको अनुमति भूमिका मार्फत बाँडिन्छ।",
"pageGuide": "यहाँ एजेन्ट ट्री, एजेन्ट भूमिका, एजेन्ट खाता र delegation ceiling व्यवस्थापन गरिन्छ। प्लेटफर्म खाता र प्लेटफर्म भूमिका अलग पृष्ठमा राखिन्छ।",
"summary": {
@@ -83,7 +94,17 @@
"canCreateChildAgent": "अधीनस्थ एजेन्ट सिर्जना अनुमति",
"cycleDaily": "दैनिक",
"cycleWeekly": "साप्ताहिक",
"cycleMonthly": "मासिक"
"cycleMonthly": "मासिक",
"loading": "Loading share and credit settings…",
"loadFailed": "Failed to load share and credit settings.",
"loadingBlocked": "Share and credit settings are still loading.",
"validation": {
"shareRange": "Share rate must be between 0 and 100",
"creditInvalid": "Credit limit cannot be negative",
"rebateLimitRange": "Rebate ceiling must be between 0 and 1",
"defaultRebateRange": "Default player rebate must be between 0 and 1",
"defaultExceedsLimit": "Default player rebate cannot exceed the rebate ceiling"
}
},
"settlementBills": {
"title": "एजेन्ट बिल",

View File

@@ -37,6 +37,11 @@
"loadFailed": "加载代理列表失败",
"siteLabel": "站点",
"createChild": "添加下级代理",
"viewDownline": "查看下级代理和玩家",
"downlineDialogTitle": "{{name}} — 下级代理与玩家",
"downlineAgentsSection": "下级代理",
"downlinePlayersSection": "直属玩家",
"downlineNoAgents": "暂无下级代理",
"editNode": "编辑代理",
"deleteNode": "删除代理",
"deleteNodeConfirm": "删除后不可恢复,请确认该代理无下级、无账号、无角色绑定。",
@@ -59,6 +64,12 @@
"deleteSuccess": "已删除代理 {{name}}",
"saveFailed": "保存失败",
"codeRequired": "请填写代理名称和登录名",
"nameRequired": "请填写代理名称",
"usernameRequired": "请填写登录名",
"passwordRequired": "请填写密码",
"passwordMinLength": "密码至少 8 位",
"bindAccountPasswordRequired": "该代理尚未绑定登录账号,请填写初始密码以创建账号",
"bindAccountHint": "该代理尚无登录账号,保存时将自动创建并绑定。",
"modelGuide": "代理层负责数据范围Scope与授权上限Ceiling账号权限请通过角色分配。",
"pageGuide": "这里统一管理代理树、代理角色、代理账号与下放上限。平台账号和平台角色请到各自的平台治理页面维护。",
"summary": {
@@ -83,7 +94,17 @@
"canCreateChildAgent": "允许创建下级代理",
"cycleDaily": "日结",
"cycleWeekly": "周结",
"cycleMonthly": "月结"
"cycleMonthly": "月结",
"loading": "正在加载占成与授信…",
"loadFailed": "加载占成与授信失败,请关闭后重试",
"loadingBlocked": "占成与授信尚未加载完成,请稍候再保存",
"validation": {
"shareRange": "占成比例须在 0100 之间",
"creditInvalid": "授信额度不能为负数",
"rebateLimitRange": "回水上限须在 01 之间(如 0.005 表示 0.5%",
"defaultRebateRange": "默认玩家回水须在 01 之间",
"defaultExceedsLimit": "默认玩家回水不能超过回水上限"
}
},
"settlementBills": {
"title": "代理账单",

View File

@@ -0,0 +1,13 @@
export const AGENT_SETTLEMENT_CYCLES = ["daily", "weekly", "monthly"] as const;
export type AgentSettlementCycle = (typeof AGENT_SETTLEMENT_CYCLES)[number];
export function normalizeAgentSettlementCycle(value: unknown): AgentSettlementCycle {
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
if (AGENT_SETTLEMENT_CYCLES.includes(normalized as AgentSettlementCycle)) {
return normalized as AgentSettlementCycle;
}
return "weekly";
}

View File

@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { Pencil, Plus, Search, Trash2 } from "lucide-react";
import { Eye, Pencil, Plus, Search, Trash2 } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -61,6 +61,7 @@ import {
PRD_USERS_MANAGE,
} from "@/lib/admin-prd";
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
import { normalizeAgentSettlementCycle } from "@/lib/agent-settlement-cycle";
import { cn } from "@/lib/utils";
import { useAdminProfile } from "@/stores/admin-session";
import type { AgentNodeRow } from "@/types/api/admin-agent";
@@ -91,18 +92,21 @@ export function AgentsConsole(): React.ReactElement {
const profile = useAdminProfile();
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const canManageNode = adminHasAnyPermission(profile?.permissions, [PRD_AGENT_MANAGE]);
const canViewAgents =
profile?.is_super_admin === true ||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENTS_ACCESS_ANY]);
const canManageProfile = adminHasAnyPermission(profile?.permissions, [
PRD_AGENT_PROFILE_MANAGE,
PRD_AGENT_MANAGE,
]);
const canProvision =
profile?.is_super_admin === true ||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_LINE_PROVISION_ACCESS_ANY]);
const isSuperAdmin = profile?.is_super_admin === true;
const canManageNode =
isSuperAdmin || adminHasAnyPermission(profile?.permissions, [PRD_AGENT_MANAGE]);
const canViewAgents =
isSuperAdmin ||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENTS_ACCESS_ANY]);
const canManageProfile =
isSuperAdmin ||
adminHasAnyPermission(profile?.permissions, [
PRD_AGENT_PROFILE_MANAGE,
PRD_AGENT_MANAGE,
]);
const canProvision =
isSuperAdmin ||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_LINE_PROVISION_ACCESS_ANY]);
const canViewSiteList = adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_SITES_ACCESS_ANY]);
const canSwitchSite = isSuperAdmin || adminHasAnyPermission(profile?.permissions, [...PRD_INTEGRATION_ACCESS_ANY]);
@@ -137,10 +141,15 @@ export function AgentsConsole(): React.ReactElement {
const [profileExtraRebate, setProfileExtraRebate] = useState(false);
const [profileCanCreateChild, setProfileCanCreateChild] = useState(false);
const [profileCanCreatePlayer, setProfileCanCreatePlayer] = useState(true);
const [profileLoading, setProfileLoading] = useState(false);
const [profileLoaded, setProfileLoaded] = useState(true);
const [editingNodeNeedsPrimaryAccount, setEditingNodeNeedsPrimaryAccount] = useState(false);
const [downlineDialogNode, setDownlineDialogNode] = useState<AgentNodeRow | null>(null);
const boundAgent = profile?.agent ?? null;
const canCreateChildAgent =
isSuperAdmin || boundAgent?.can_create_child_agent !== false;
const canCreateChildForNode = (_node: AgentNodeRow) => canManageNode && canCreateChildAgent;
const canViewPlayersTab =
isSuperAdmin ||
(boundAgent?.can_create_player !== false &&
@@ -162,12 +171,51 @@ export function AgentsConsole(): React.ReactElement {
credit_limit: Number.parseInt(profileCreditLimit, 10) || 0,
rebate_limit: Number.parseFloat(profileRebateLimit) || 0,
default_player_rebate: Number.parseFloat(profileDefaultRebate) || 0,
settlement_cycle: profileSettlementCycle,
settlement_cycle: normalizeAgentSettlementCycle(profileSettlementCycle),
can_grant_extra_rebate: profileExtraRebate,
can_create_child_agent: profileCanCreateChild,
can_create_player: profileCanCreatePlayer,
});
const validateProfileFields = (): string | null => {
const shareRate = Number.parseFloat(profileShareRate);
const creditLimit = Number.parseInt(profileCreditLimit, 10);
const rebateLimit = Number.parseFloat(profileRebateLimit);
const defaultRebate = Number.parseFloat(profileDefaultRebate);
if (Number.isNaN(shareRate) || shareRate < 0 || shareRate > 100) {
return t("profile.validation.shareRange", {
defaultValue: "占成比例须在 0100 之间",
});
}
if (Number.isNaN(creditLimit) || creditLimit < 0) {
return t("profile.validation.creditInvalid", {
defaultValue: "授信额度不能为负数",
});
}
if (Number.isNaN(rebateLimit) || rebateLimit < 0 || rebateLimit > 1) {
return t("profile.validation.rebateLimitRange", {
defaultValue: "回水上限须在 01 之间(如 0.005 表示 0.5%",
});
}
if (Number.isNaN(defaultRebate) || defaultRebate < 0 || defaultRebate > 1) {
return t("profile.validation.defaultRebateRange", {
defaultValue: "默认玩家回水须在 01 之间",
});
}
if (rebateLimit > 0 && defaultRebate > rebateLimit) {
return t("profile.validation.defaultExceedsLimit", {
defaultValue: "默认玩家回水不能超过回水上限",
});
}
return null;
};
const flatNodes = useMemo(() => flattenTree(tree), [tree]);
const parentNameMap = useMemo(
() => new Map<number, string>(flatNodes.map((node) => [node.id, node.name])),
@@ -221,6 +269,13 @@ export function AgentsConsole(): React.ReactElement {
const start = (currentPage - 1) * perPage;
return filteredRows.slice(start, start + perPage);
}, [currentPage, filteredRows, perPage]);
const downlineChildAgents = useMemo(() => {
if (downlineDialogNode === null) {
return [];
}
return flatNodes.filter((node) => node.parent_id === downlineDialogNode.id);
}, [downlineDialogNode, flatNodes]);
const loadTree = useCallback(async (siteId?: number | null) => {
setLoading(true);
@@ -300,6 +355,9 @@ export function AgentsConsole(): React.ReactElement {
setNodeUsername("");
setNodePassword("");
resetProfileForm("create");
setProfileLoading(false);
setProfileLoaded(true);
setEditingNodeNeedsPrimaryAccount(false);
setNodeDialogOpen(true);
};
@@ -310,23 +368,40 @@ export function AgentsConsole(): React.ReactElement {
setNodeName(node.name);
setNodeStatus(node.status);
setNodeUsername(node.username ?? "");
setEditingNodeNeedsPrimaryAccount((node.username ?? "").trim() === "");
setNodePassword("");
resetProfileForm("edit");
setNodeDialogOpen(true);
if (canManageProfile) {
void getAgentNodeProfile(node.id)
.then((p) => {
setProfileShareRate(String(p.total_share_rate ?? 0));
setProfileCreditLimit(String(p.credit_limit ?? 0));
setProfileRebateLimit(String(p.rebate_limit ?? 0));
setProfileDefaultRebate(String(p.default_player_rebate ?? 0));
setProfileSettlementCycle(p.settlement_cycle ?? "weekly");
setProfileExtraRebate(Boolean(p.can_grant_extra_rebate));
setProfileCanCreateChild(Boolean(p.can_create_child_agent));
setProfileCanCreatePlayer(p.can_create_player !== false);
})
.catch(() => undefined);
if (!canManageProfile) {
setProfileLoading(false);
setProfileLoaded(true);
return;
}
setProfileLoading(true);
setProfileLoaded(false);
void getAgentNodeProfile(node.id)
.then((p) => {
setProfileShareRate(String(p.total_share_rate ?? 0));
setProfileCreditLimit(String(p.credit_limit ?? 0));
setProfileRebateLimit(String(p.rebate_limit ?? 0));
setProfileDefaultRebate(String(p.default_player_rebate ?? 0));
setProfileSettlementCycle(normalizeAgentSettlementCycle(p.settlement_cycle));
setProfileExtraRebate(Boolean(p.can_grant_extra_rebate));
setProfileCanCreateChild(Boolean(p.can_create_child_agent));
setProfileCanCreatePlayer(p.can_create_player !== false);
setProfileLoaded(true);
})
.catch(() => {
toast.error(
t("profile.loadFailed", { defaultValue: "加载占成与授信失败,请关闭后重试" }),
);
setProfileLoaded(false);
})
.finally(() => {
setProfileLoading(false);
});
};
const renderRowActions = (node: AgentNodeRow) => {
@@ -349,9 +424,15 @@ export function AgentsConsole(): React.ReactElement {
key: "create-child",
label: t("createChild", { defaultValue: "添加下级代理" }),
icon: Plus,
hidden: !canManageNode || !canCreateChildAgent,
hidden: !canCreateChildForNode(node),
onClick: () => openCreateChildForNode(node),
},
{
key: "view-downline",
label: t("viewDownline", { defaultValue: "查看下级代理和玩家" }),
icon: Eye,
onClick: () => setDownlineDialogNode(node),
},
{
key: "delete",
label: t("deleteNode", { defaultValue: "删除代理" }),
@@ -384,8 +465,13 @@ export function AgentsConsole(): React.ReactElement {
};
const saveNode = async () => {
if (!nodeName.trim() || !nodeUsername.trim()) {
toast.error(t("codeRequired", { defaultValue: "请填写代理名称和登录名" }));
if (!nodeName.trim()) {
toast.error(t("nameRequired", { defaultValue: "请填写代理名称" }));
return;
}
if (!nodeUsername.trim()) {
toast.error(t("usernameRequired", { defaultValue: "请填写登录名" }));
return;
}
@@ -394,7 +480,38 @@ export function AgentsConsole(): React.ReactElement {
return;
}
if (!nodePassword.trim()) {
toast.error(t("users.password", { defaultValue: "密码" }));
toast.error(t("passwordRequired", { defaultValue: "请填写密码" }));
return;
}
if (nodePassword.trim().length < 8) {
toast.error(t("passwordMinLength", { defaultValue: "密码至少 8 位" }));
return;
}
} else if (nodePassword.trim() && nodePassword.trim().length < 8) {
toast.error(t("passwordMinLength", { defaultValue: "密码至少 8 位" }));
return;
} else if (nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount && !nodePassword.trim()) {
toast.error(
t("bindAccountPasswordRequired", {
defaultValue: "该代理尚未绑定登录账号,请填写初始密码以创建账号",
}),
);
return;
}
if (canManageProfile) {
if (nodeDialogMode === "edit" && !profileLoaded) {
toast.error(
t("profile.loadingBlocked", {
defaultValue: "占成与授信尚未加载完成,请稍候再保存",
}),
);
return;
}
const profileError = validateProfileFields();
if (profileError !== null) {
toast.error(profileError);
return;
}
}
@@ -415,7 +532,9 @@ export function AgentsConsole(): React.ReactElement {
await putAgentNode(editingNodeId, {
name: nodeName.trim(),
username: nodeUsername.trim(),
password: nodePassword.trim() || undefined,
password: editingNodeNeedsPrimaryAccount
? nodePassword.trim()
: nodePassword.trim() || undefined,
status: nodeStatus,
});
if (canManageProfile) {
@@ -426,6 +545,14 @@ export function AgentsConsole(): React.ReactElement {
setNodeDialogOpen(false);
await loadTree(adminSiteId);
if (nodeDialogMode === "create" && targetParentId !== null && downlineDialogNode?.id === targetParentId) {
const refreshedParent = flattenTree(
(await getAgentTree(adminSiteId ?? undefined)).tree,
).find((node) => node.id === targetParentId);
if (refreshedParent) {
setDownlineDialogNode(refreshedParent);
}
}
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed"));
} finally {
@@ -634,6 +761,104 @@ export function AgentsConsole(): React.ReactElement {
) : null}
</AdminPageCard>
<Dialog
open={downlineDialogNode !== null}
onOpenChange={(open) => {
if (!open) {
setDownlineDialogNode(null);
}
}}
>
<DialogContent className="flex max-h-[min(90vh,52rem)] max-w-4xl flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>
{downlineDialogNode
? t("downlineDialogTitle", {
name: downlineDialogNode.name,
defaultValue: "{{name}} — 下级代理与玩家",
})
: t("viewDownline", { defaultValue: "查看下级代理和玩家" })}
</DialogTitle>
</DialogHeader>
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto pr-1">
<section className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 className="text-sm font-medium">
{t("downlineAgentsSection", { defaultValue: "下级代理" })}
</h3>
{downlineDialogNode && canCreateChildForNode(downlineDialogNode) ? (
<Button
type="button"
size="sm"
onClick={() => openCreateChildForNode(downlineDialogNode)}
>
<Plus className="size-4" aria-hidden />
{t("createChild", { defaultValue: "添加下级代理" })}
</Button>
) : null}
</div>
<div className="admin-table-shell">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("name", { defaultValue: "名称" })}</TableHead>
<TableHead>{t("code", { defaultValue: "编码" })}</TableHead>
<TableHead>{t("users.username", { defaultValue: "登录名" })}</TableHead>
<TableHead className="w-20">{t("childrenCount", { defaultValue: "直属下级" })}</TableHead>
<TableHead className="w-24">{t("status", { defaultValue: "状态" })}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{downlineChildAgents.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-muted-foreground">
{t("downlineNoAgents", { defaultValue: "暂无下级代理" })}
</TableCell>
</TableRow>
) : (
downlineChildAgents.map((child) => (
<TableRow key={child.id}>
<TableCell className="font-medium">{child.name}</TableCell>
<TableCell className="font-mono text-xs">{child.code}</TableCell>
<TableCell className="font-mono text-xs">{child.username ?? "—"}</TableCell>
<TableCell>{child.children?.length ?? 0}</TableCell>
<TableCell>
<AdminStatusBadge tone={resolveRoleStatusTone(child.status)}>
{child.status === 1
? t("common:status.enabled", { defaultValue: "Enabled" })
: t("common:status.disabled", { defaultValue: "Disabled" })}
</AdminStatusBadge>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</section>
{canViewPlayersTab && downlineDialogNode ? (
<section className="space-y-3">
<h3 className="text-sm font-medium">
{t("downlinePlayersSection", { defaultValue: "直属玩家" })}
</h3>
<AgentsPlayersPanel
siteCode={activeSiteCode}
agentNodeId={downlineDialogNode.id}
/>
</section>
) : null}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDownlineDialogNode(null)}>
{t("common:actions.close", { defaultValue: "关闭" })}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={nodeDialogOpen} onOpenChange={setNodeDialogOpen}>
<DialogContent>
<DialogHeader>
@@ -668,17 +893,24 @@ export function AgentsConsole(): React.ReactElement {
<div className="space-y-2">
<Label htmlFor="agent-password">
{nodeDialogMode === "create"
{nodeDialogMode === "create" || editingNodeNeedsPrimaryAccount
? t("users.password", { defaultValue: "密码" })
: t("resetPassword", { defaultValue: "重置密码" })}
</Label>
{nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount ? (
<p className="text-xs text-muted-foreground">
{t("bindAccountHint", {
defaultValue: "该代理尚无登录账号,保存时将自动创建并绑定。",
})}
</p>
) : null}
<Input
id="agent-password"
type="password"
value={nodePassword}
onChange={(e) => setNodePassword(e.target.value)}
placeholder={
nodeDialogMode === "edit"
nodeDialogMode === "edit" && !editingNodeNeedsPrimaryAccount
? t("passwordOptionalHint")
: t("passwordPlaceholder")
}
@@ -696,7 +928,12 @@ export function AgentsConsole(): React.ReactElement {
<p className="text-sm font-medium">
{t("profile.section", { defaultValue: "占成与授信" })}
</p>
<div className="grid gap-3 sm:grid-cols-2">
{profileLoading ? (
<p className="text-sm text-muted-foreground">
{t("profile.loading", { defaultValue: "正在加载占成与授信…" })}
</p>
) : null}
<div className={cn("grid gap-3 sm:grid-cols-2", profileLoading ? "pointer-events-none opacity-50" : "")}>
<div className="space-y-2">
<Label htmlFor="agent-share-rate">
{t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
@@ -813,7 +1050,11 @@ export function AgentsConsole(): React.ReactElement {
<Button type="button" variant="outline" onClick={() => setNodeDialogOpen(false)}>
{t("common:actions.cancel", { defaultValue: "取消" })}
</Button>
<Button type="button" disabled={nodeSaving} onClick={() => void saveNode()}>
<Button
type="button"
disabled={nodeSaving || (nodeDialogMode === "edit" && canManageProfile && profileLoading)}
onClick={() => void saveNode()}
>
{t("common:actions.save", { defaultValue: "保存" })}
</Button>
</DialogFooter>