From 1eb6702c51aa7f07c4e9e01e935140ce3566cb54 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 11 Jun 2026 18:02:02 +0800 Subject: [PATCH] refactor: update agent API schemas, standardize UI text styling, and enhance settlement credit ledger components --- src/api/admin-agents.ts | 7 + src/app/admin/(shell)/agents/list/page.tsx | 9 +- src/app/admin/(shell)/agents/page.tsx | 9 +- .../admin/(shell)/agents/provision/page.tsx | 22 +- src/components/admin/admin-breadcrumb.tsx | 12 +- .../admin/admin-no-resource-state.tsx | 2 +- src/components/admin/admin-sidebar-nav.tsx | 7 +- .../admin/player-funding-badges.tsx | 11 +- src/i18n/locales/en/agents.json | 10 +- src/i18n/locales/en/common.json | 1 + src/i18n/locales/en/config.json | 18 +- src/i18n/locales/zh/agents.json | 26 +- src/i18n/locales/zh/common.json | 1 + src/i18n/locales/zh/config.json | 18 +- src/i18n/locales/zh/settlementCenter.json | 2 +- src/lib/admin-nav-label.ts | 1 + src/lib/admin-rate-percent.ts | 6 +- src/modules/_config/admin-nav-icons.tsx | 1 + src/modules/_config/admin-nav.ts | 2 + .../agents/agent-line-detail-panel.tsx | 472 ++++++++++-------- .../agents/agent-line-provision-wizard.tsx | 179 +++++-- src/modules/agents/agent-line-sidebar.tsx | 32 +- src/modules/agents/agent-profile-fields.tsx | 136 +++-- src/modules/agents/agents-console.tsx | 293 ++++++----- .../agents/agents-directory-console.tsx | 308 ++++++++++++ src/modules/agents/agents-players-panel.tsx | 260 ++++++---- src/modules/agents/agents-subnav.tsx | 175 +++---- .../config/doc/odds-config-doc-screen.tsx | 20 +- .../config/doc/odds-config-summary-panel.tsx | 4 +- .../config/doc/play-config-doc-screen.tsx | 8 +- .../config/doc/rebate-config-doc-screen.tsx | 12 +- .../config/doc/risk-cap-doc-screen.tsx | 27 +- src/modules/config/risk-cap-runtime-panel.tsx | 6 +- .../dashboard/agent-dashboard-console.tsx | 1 - .../integration/integration-sites-console.tsx | 136 ++++- src/modules/jackpot/jackpot-pools-console.tsx | 24 +- .../jackpot/jackpot-records-console.tsx | 4 +- src/modules/risk/risk-lock-logs-console.tsx | 2 +- src/modules/risk/risk-pool-detail-console.tsx | 8 +- src/modules/risk/risk-pools-console.tsx | 6 +- .../settings/panels/draw-settings-panel.tsx | 54 +- .../panels/frontend-settings-panel.tsx | 24 +- .../panels/settlement-settings-panel.tsx | 88 ++-- src/modules/settlement/agent-bill-detail.tsx | 82 +-- .../settlement/settlement-bill-breakdown.tsx | 22 +- .../settlement/settlement-bills-table.tsx | 39 +- .../settlement/settlement-center-shell.tsx | 136 +++-- .../settlement-credit-ledger-panel.tsx | 41 +- .../settlement/settlement-main-panel.tsx | 194 +++---- .../settlement-period-workbench.tsx | 5 +- src/types/api/admin-agent-line.ts | 1 - src/types/api/admin-agent.ts | 7 +- src/types/api/admin-dashboard.ts | 1 - src/types/api/admin-integration-site.ts | 19 +- 54 files changed, 1888 insertions(+), 1103 deletions(-) create mode 100644 src/modules/agents/agents-directory-console.tsx diff --git a/src/api/admin-agents.ts b/src/api/admin-agents.ts index 9c313b5..901c232 100644 --- a/src/api/admin-agents.ts +++ b/src/api/admin-agents.ts @@ -6,6 +6,7 @@ import type { AgentAdminUserListData, AgentAdminUserRoleSyncPayload, AgentNodeCreatePayload, + AgentNodeListData, AgentNodeRow, AgentNodeUpdatePayload, AgentProfilePayload, @@ -26,6 +27,12 @@ export async function getAgentTree(adminSiteId?: number): Promise }); } +export async function getAgentNodes(adminSiteId?: number): Promise { + return adminRequest.get(`${A}/agent-nodes`, { + params: adminSiteId ? { admin_site_id: adminSiteId } : undefined, + }); +} + export async function postAgentNode(body: AgentNodeCreatePayload): Promise { return adminRequest.post(`${A}/agent-nodes`, body); } diff --git a/src/app/admin/(shell)/agents/list/page.tsx b/src/app/admin/(shell)/agents/list/page.tsx index 8130be1..9bdecd8 100644 --- a/src/app/admin/(shell)/agents/list/page.tsx +++ b/src/app/admin/(shell)/agents/list/page.tsx @@ -1,5 +1,10 @@ -import { redirect } from "next/navigation"; +import type { Metadata } from "next"; + +import { AgentsDirectoryConsole } from "@/modules/agents/agents-directory-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; + +export const metadata: Metadata = buildPageMetadata("agents", "directoryTitle"); export default function AgentsListPage() { - redirect("/admin/agents"); + return ; } diff --git a/src/app/admin/(shell)/agents/page.tsx b/src/app/admin/(shell)/agents/page.tsx index d47f6b9..79499e5 100644 --- a/src/app/admin/(shell)/agents/page.tsx +++ b/src/app/admin/(shell)/agents/page.tsx @@ -1,7 +1,10 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { AgentsConsole } from "@/modules/agents/agents-console"; -import { PRD_AGENTS_ACCESS_ANY } from "@/lib/admin-prd"; +import { + PRD_AGENT_LINE_PROVISION_ACCESS_ANY, + PRD_AGENTS_ACCESS_ANY, +} from "@/lib/admin-prd"; import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; @@ -10,7 +13,9 @@ export const metadata: Metadata = buildPageMetadata("agents", "title"); export default function AgentsPage() { return ( - + diff --git a/src/app/admin/(shell)/agents/provision/page.tsx b/src/app/admin/(shell)/agents/provision/page.tsx index e9087f8..bac8f04 100644 --- a/src/app/admin/(shell)/agents/provision/page.tsx +++ b/src/app/admin/(shell)/agents/provision/page.tsx @@ -1,21 +1,5 @@ -import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { AgentLineProvisionWizard } from "@/modules/agents/agent-line-provision-wizard"; -import { PRD_AGENT_LINE_PROVISION_ACCESS_ANY } from "@/lib/admin-prd"; -import { buildPageMetadata } from "@/lib/page-metadata"; -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -export const metadata: Metadata = buildPageMetadata("agents", "lineProvision.title"); - -export default function AgentLineProvisionPage(): React.ReactElement { - return ( - - - - - - ); +export default function AgentProvisionRedirectPage(): never { + redirect("/admin/agents"); } diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index f56889e..233354e 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -93,11 +93,13 @@ export function AdminBreadcrumb() { const navItem = navItems .filter( (item) => - pathname === item.href || - pathname.startsWith(`${item.href}/`) || - (item.activeMatchPrefix != null && - (pathname === item.activeMatchPrefix || - pathname.startsWith(`${item.activeMatchPrefix}/`))), + (item.activeExact === true + ? pathname === item.href + : pathname === item.href || + pathname.startsWith(`${item.href}/`) || + (item.activeMatchPrefix != null && + (pathname === item.activeMatchPrefix || + pathname.startsWith(`${item.activeMatchPrefix}/`)))), ) .sort((a, b) => b.href.length - a.href.length)[0]; diff --git a/src/components/admin/admin-no-resource-state.tsx b/src/components/admin/admin-no-resource-state.tsx index 28ab89c..e5a6353 100644 --- a/src/components/admin/admin-no-resource-state.tsx +++ b/src/components/admin/admin-no-resource-state.tsx @@ -75,7 +75,7 @@ export function AdminTableNoResourceRow({ compact?: boolean; }): ReactElement { return ( - + diff --git a/src/components/admin/admin-sidebar-nav.tsx b/src/components/admin/admin-sidebar-nav.tsx index 9ef0ceb..b43465d 100644 --- a/src/components/admin/admin-sidebar-nav.tsx +++ b/src/components/admin/admin-sidebar-nav.tsx @@ -34,9 +34,12 @@ const SUB_NAV = function isActive( pathname: string, - item: { href: string; activeMatchPrefix?: string; segment?: string }, + item: { href: string; activeMatchPrefix?: string; activeExact?: boolean; segment?: string }, ): boolean { - const { href, activeMatchPrefix, segment } = item; + const { href, activeMatchPrefix, activeExact, segment } = item; + if (activeExact) { + return pathname === href; + } const prefix = activeMatchPrefix ?? href; if (prefix === ADMIN_BASE || prefix === `${ADMIN_BASE}/`) { return pathname === ADMIN_BASE || pathname === `${ADMIN_BASE}/`; diff --git a/src/components/admin/player-funding-badges.tsx b/src/components/admin/player-funding-badges.tsx index 4cd3b68..69f8cfd 100644 --- a/src/components/admin/player-funding-badges.tsx +++ b/src/components/admin/player-funding-badges.tsx @@ -54,16 +54,7 @@ export function PlayerLedgerSourceBadge({ return null; } - const sourceClass = - ledgerSource === "wallet_txn" - ? "border-sky-200 bg-sky-50 text-sky-900" - : ledgerSource === "payment_record" - ? "border-emerald-200 bg-emerald-50 text-emerald-900" - : ledgerSource === "settlement_adjustment" - ? "border-amber-200 bg-amber-50 text-amber-900" - : ledgerSource === "share_ledger" - ? "border-indigo-200 bg-indigo-50 text-indigo-900" - : "border-violet-200 bg-violet-50 text-violet-900"; + const sourceClass = "border-border bg-muted/30 text-muted-foreground"; const label = ledgerSource === "wallet_txn" diff --git a/src/i18n/locales/en/agents.json b/src/i18n/locales/en/agents.json index fdb94dd..f8846d0 100644 --- a/src/i18n/locales/en/agents.json +++ b/src/i18n/locales/en/agents.json @@ -11,14 +11,17 @@ "selectAgentHint": "Settlement boundaries follow the agent tree; share, credit and rebate are configured per node.", "allocatedCredit": "Allocated", "availableCredit": "Available", - "profileFootnote": "Rebate cap {{rebate}}% · Default {{defaultRebate}}% · {{cycle}}", + "profileFootnote": "Rebate cap {{rebate}}% · Default {{defaultRebate}}%", "tabOverview": "Overview", "tabProfile": "Share & credit", "tabProfileReadOnly": "Share & credit (read-only)", "currentSite": "Site", "viewAll": "View all", "shareRebateCap": "Rebate cap {{rate}}%", - "overviewDownlineCard": "{{count}} direct downline — manage in the Downline tab.", + "overviewDownlineCard": "View and manage direct downline agents", + "overviewDownlineCount": "{{count}}", + "overviewPlayersHint": "View direct players and credit status", + "overviewPlayersSummary": "Player management", "downlineEmptyTitle": "No direct downline yet", "editAccount": "Account & status", "saveProfile": "Save share & credit", @@ -28,6 +31,7 @@ }, "listTitle": "Agents", "listSearch": "Search name / code / login", + "siteSearch": "Search site name", "parentAgent": "Parent", "childrenCount": "Direct downline", "subnav": { @@ -189,7 +193,7 @@ "externalIdHint": "Leave blank to auto-generate", "creditLimit": "Credit limit", "rebateRate": "Rebate rate (%)", - "rebateRateHint": "Enter percent, e.g. 0.5 = 0.5%", + "rebateRateHint": "Enter percent, e.g. 5 = 5%", "availableToGrant": "Agent available to grant: {{amount}}", "riskTags": "Risk tags", "riskTagsPlaceholder": "Comma-separated", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 0f91f9b..a5bcc8e 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -163,6 +163,7 @@ "account": "Account settings", "integration": "Integration", "agents": "Agent lines", + "agent_list": "Agent list", "settlement_center": "Settlement center", "config": "Operations config" }, diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index 67257f1..db4d0c3 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -53,6 +53,7 @@ "loadFailed": "Failed to load integration sites", "saveFailed": "Save failed", "createSuccess": "Created site {{code}}", + "adminAccountCreated": "Created site admin account {{username}}", "updateSuccess": "Updated site {{code}}", "connectivityTest": "Test connectivity", "connectivityTitle": "Partner wallet connectivity", @@ -85,7 +86,10 @@ "dialogDescription": "Default wallet paths are fine unless the partner uses custom URLs.", "form": { "required": "Site name is required", - "codeRequired": "site_code is required" + "codeRequired": "site_code is required", + "adminUsernameRequired": "Site admin username is required", + "adminNicknameRequired": "Site admin nickname is required", + "adminPasswordRequired": "Initial site admin password must be at least 8 characters" }, "columns": { "code": "site_code", @@ -97,6 +101,10 @@ "fields": { "code": "site_code", "name": "Site name", + "adminUsername": "Admin username", + "adminNickname": "Admin nickname", + "adminPassword": "Initial password", + "adminEmail": "Email (optional)", "currency": "Default currency", "status": "Status", "walletApiUrl": "Partner wallet base URL", @@ -109,13 +117,19 @@ "placeholders": { "code": "Enter site identifier, for example partner-a", "name": "Enter site name", + "adminUsername": "Enter admin username", + "adminNickname": "Enter account nickname", + "adminPassword": "At least 8 characters", + "adminEmail": "Enter email", "currency": "Enter currency code, for example NPR", "walletApiUrl": "Enter wallet API URL", "lotteryH5BaseUrl": "Enter H5 URL", "iframeOrigins": "Enter allowed origins, for example https://www.example.com", "notes": "Enter notes", "connectivityPlayerId": "Enter player ID, for example 10001" - } + }, + "adminAccountSectionTitle": "Site admin account", + "adminAccountSectionDescription": "Creating a site will also create one admin account bound to that site." }, "versionStatus": { "active": "Active", diff --git a/src/i18n/locales/zh/agents.json b/src/i18n/locales/zh/agents.json index 6e6411b..f273a90 100644 --- a/src/i18n/locales/zh/agents.json +++ b/src/i18n/locales/zh/agents.json @@ -11,21 +11,25 @@ "selectAgentHint": "信用占成盘以代理树为结算边界,占成、授信与回水均在代理节点配置。", "allocatedCredit": "已下发", "availableCredit": "可下发", - "profileFootnote": "回水上限 {{rebate}}% · 默认回水 {{defaultRebate}}% · {{cycle}}", + "profileFootnote": "回水上限 {{rebate}}% · 默认回水 {{defaultRebate}}%", "tabOverview": "概览", "currentSite": "当前站点", "viewAll": "查看全部", "shareRebateCap": "回水上限 {{rate}}%", - "overviewDownlineCard": "{{count}} 个,可在对应 Tab 管理下级代理。", + "overviewDownlineCard": "查看并管理直属下级代理", + "overviewDownlineCount": "{{count}} 个", "downlineEmptyTitle": "暂无直属下级", "tabProfile": "占成与授信", "tabProfileReadOnly": "占成与授信(只读)", "profileReadOnlyHint": "占成、授信与回水由上级配置,如需调整请联系上级代理或平台。", "selfAgentOverviewHint": "以下为上级为您分配的授信额度,占成与回水由上级在后台维护,本账号不可查看或修改。", "overviewDownlineHint": "直属下级 {{count}} 个,可在「直属下级」Tab 管理。", - "overviewPlayersHint": "直属玩家请在「直属玩家」Tab 维护。", + "overviewPlayersHint": "查看直属玩家与授信情况", + "overviewPlayersSummary": "玩家管理", "tabDownline": "直属下级", "tabPlayers": "直属玩家", + "playersUnavailableHint": "当前代理未开启“允许创建玩家”,如需新增请先调整该代理配置。", + "playersNoPermissionHint": "当前账号没有该节点的玩家管理权限。", "downlineColumns": { "email": "邮箱", "downlineCount": "下级数" @@ -41,11 +45,14 @@ "downlineEmpty": "暂无直属下级。创建下级代理后将在此展示。", "downlineEmptyShort": "暂无直属下级。", "noDelegatedTabs": "该代理未开放创建下级或玩家。请使用「编辑本代理」维护占成、授信与风控标签。", + "sidebarShareRate": "占成 {{rate}}%", + "sidebarAvailableCredit": "可下发 {{amount}}", "expand": "展开", "collapse": "收起" }, "listTitle": "代理列表", "listSearch": "搜索代理名称 / 编码 / 登录名", + "siteSearch": "搜索站点名称", "parentAgent": "上级代理", "childrenCount": "直属下级", "subnav": { @@ -130,6 +137,8 @@ "profile": { "section": "占成与授信", "totalShareRate": "占成比例 (%)", + "relativeShareRate": "占成比例(占上级 %)", + "relativeShareRateValue": "占上级 {{rate}}%", "creditLimit": "授信额度", "rebateLimit": "回水上限 (%)", "defaultPlayerRebate": "默认玩家回水 (%)", @@ -292,12 +301,15 @@ "siteCode": "接入站点", "siteCodePlaceholder": "选择站点", "siteRequired": "请选择接入站点", + "codeRequired": "请填写代理编码", + "codePatternInvalid": "代理编码仅支持字母、数字、下划线和中划线,且需以字母或数字开头", "noUnboundSite": "暂无未绑定一级代理的站点", "openIntegrationSites": "前往接入站点", "code": "代理编码", "name": "一级代理名称", "username": "后台登录账号", "password": "初始密码", + "passwordHint": "至少 8 位", "submit": "创建一级代理", "success": "一级代理已创建", "link": "创建一级代理" @@ -305,18 +317,24 @@ "noAccess": "您没有代理经营相关权限,请联系管理员开通。", "playersPanel": { "create": "创建玩家", + "siteCode": "所属线路", "scopedTo": "直属玩家:{{agent}}", "allUnderSite": "当前一级代理线路下可见玩家", "filterHint": "可按上级代理查看其直属玩家。", "loginRequired": "请填写登录账号与初始密码", "loginUsername": "登录账号", "initialPassword": "初始密码", + "passwordHint": "至少 8 位", + "passwordMinLength": "初始密码至少 8 位", "externalIdOptional": "外部 ID(可选)", "externalIdHint": "留空则系统自动生成", "creditLimit": "授信额度", "rebateRate": "回水比例 (%)", - "rebateRateHint": "填写百分比,如 0.5 表示 0.5%", + "rebateRateHint": "填写百分比,如 5 表示 5%", "availableToGrant": "代理剩余可下发:{{amount}}", + "creditLimitInvalid": "授信额度必须为不小于 0 的整数", + "creditLimitExceeded": "授信额度不能超过当前代理可下发额度", + "rebateRateInvalid": "回水比例须在 0–100% 之间", "riskTags": "风控标签", "riskTagsPlaceholder": "逗号分隔", "fundingMode": "资金模式", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 53a7441..ffef1bf 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -163,6 +163,7 @@ "account": "账号设置", "integration": "接入配置", "agents": "代理线路", + "agent_list": "代理列表", "settlement_center": "结算中心", "config": "运营配置" }, diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index f6c400c..ce43224 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -53,6 +53,7 @@ "loadFailed": "加载接入站点失败", "saveFailed": "保存失败", "createSuccess": "已创建站点 {{code}}", + "adminAccountCreated": "已同时创建站点后台账号 {{username}}", "updateSuccess": "已更新站点 {{code}}", "connectivityTest": "联通检测", "connectivityTitle": "主站钱包联通检测", @@ -85,7 +86,10 @@ "dialogDescription": "钱包路径使用默认值即可,除非主站 URL 规范不同。", "form": { "required": "请填写站点名称", - "codeRequired": "请填写 site_code" + "codeRequired": "请填写 site_code", + "adminUsernameRequired": "请填写站点后台登录名", + "adminNicknameRequired": "请填写站点后台账号昵称", + "adminPasswordRequired": "请填写至少 8 位的站点后台初始密码" }, "columns": { "code": "site_code", @@ -106,6 +110,10 @@ "fields": { "code": "site_code", "name": "站点名称", + "adminUsername": "后台登录名", + "adminNickname": "账号昵称", + "adminPassword": "初始密码", + "adminEmail": "邮箱(可选)", "currency": "默认币种", "status": "状态", "walletApiUrl": "主站钱包根 URL", @@ -118,13 +126,19 @@ "placeholders": { "code": "请输入站点标识,如 partner-a", "name": "请输入站点名称", + "adminUsername": "请输入后台登录名", + "adminNickname": "请输入账号昵称", + "adminPassword": "至少 8 位", + "adminEmail": "请输入邮箱", "currency": "请输入币种代码,如 NPR", "walletApiUrl": "请输入钱包接口地址", "lotteryH5BaseUrl": "请输入 H5 地址", "iframeOrigins": "请输入允许的来源地址,如 https://www.example.com", "notes": "请输入备注说明", "connectivityPlayerId": "请输入玩家 ID,如 10001" - } + }, + "adminAccountSectionTitle": "站点后台管理账号", + "adminAccountSectionDescription": "创建站点时将同步创建一个绑定该站点的后台管理账号。" }, "versionStatus": { "active": "生效中", diff --git a/src/i18n/locales/zh/settlementCenter.json b/src/i18n/locales/zh/settlementCenter.json index 6432ba5..bdf1489 100644 --- a/src/i18n/locales/zh/settlementCenter.json +++ b/src/i18n/locales/zh/settlementCenter.json @@ -93,7 +93,7 @@ "reason": "业务类型", "ref": "关联", "amount": "金额", - "channel": "渠道", + "channel": "来源", "status": "状态", "time": "时间" }, diff --git a/src/lib/admin-nav-label.ts b/src/lib/admin-nav-label.ts index c21ca11..2182912 100644 --- a/src/lib/admin-nav-label.ts +++ b/src/lib/admin-nav-label.ts @@ -23,6 +23,7 @@ const NAV_SEGMENT_I18N_KEYS: Record = { settings: "settings", integration: "integration", agents: "agents", + agent_list: "agent_list", config: "config", }; diff --git a/src/lib/admin-rate-percent.ts b/src/lib/admin-rate-percent.ts index bb0bd80..019f28c 100644 --- a/src/lib/admin-rate-percent.ts +++ b/src/lib/admin-rate-percent.ts @@ -1,4 +1,4 @@ -/** API / 存储用小数比例(0–1);后台表单统一用百分比(0–100)展示与录入。 */ +/** API 统一用百分比(0–100);后台表单统一用百分比(0–100)展示与录入。 */ const RATIO_TO_PERCENT = 100; @@ -20,7 +20,7 @@ export function percentValueToUi( return formatPercentNumber(n, decimals); } -/** 存库小数 0–1 → 表单百分比,如 0.2 → "20",0.005 → "0.5" */ +/** @deprecated API 已统一为百分比,不再需要此转换 */ export function ratioToPercentUi( ratio: number | string | null | undefined, decimals = 2, @@ -32,7 +32,7 @@ export function ratioToPercentUi( return formatPercentNumber(n * RATIO_TO_PERCENT, decimals); } -/** "0.5" → 0.005 */ +/** @deprecated API 已统一为百分比,不再需要此转换 */ export function percentUiToRatio(percent: number | string | null | undefined): number { const n = typeof percent === "string" ? Number.parseFloat(percent.trim()) : percent; if (n == null || !Number.isFinite(n)) { diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index 82b1a98..183fbca 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -32,6 +32,7 @@ export const adminNavIconBySegment: Record { dashboard: LayoutDashboard, agents: Network, + agent_list: Users, players: Users, draws: CalendarClock, rules_plays: ClipboardList, diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index 7a866f3..65ecbc9 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -11,6 +11,7 @@ export type AdminNavGroup = export type AdminNavSegment = | "dashboard" | "agents" + | "agent_list" | "players" | "draws" | "rules_plays" @@ -39,5 +40,6 @@ export type AdminNavItem = { platform_only?: boolean; agent_hidden?: boolean; activeMatchPrefix?: string; + activeExact?: boolean; requiredAny?: readonly string[]; }; diff --git a/src/modules/agents/agent-line-detail-panel.tsx b/src/modules/agents/agent-line-detail-panel.tsx index 24e9a04..4f1cc58 100644 --- a/src/modules/agents/agent-line-detail-panel.tsx +++ b/src/modules/agents/agent-line-detail-panel.tsx @@ -5,7 +5,7 @@ import { ChevronRight, Network, Pencil, Plus, Trash2, Users } from "lucide-react import { useTranslation } from "react-i18next"; import { AdminSubnav, AdminSubnavButton } from "@/components/admin/admin-subnav"; -import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; +import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { @@ -26,17 +26,16 @@ import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { cn } from "@/lib/utils"; import type { AgentNodeProfileSummary, AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent"; -function settlementCycleLabel( - cycle: AgentNodeProfileSummary["settlement_cycle"] | undefined, - t: (key: string, opts?: { defaultValue?: string }) => string, -): string { - if (cycle === "daily") { - return t("profile.cycleDaily", { defaultValue: "日结" }); +function relativeShareRate(totalShareRate: number | undefined, parentShareRate: number | undefined): string | null { + if ( + totalShareRate == null || + parentShareRate == null || + parentShareRate <= 0 + ) { + return null; } - if (cycle === "monthly") { - return t("profile.cycleMonthly", { defaultValue: "月结" }); - } - return t("profile.cycleWeekly", { defaultValue: "周结" }); + + return percentValueToUi((totalShareRate / parentShareRate) * 100); } export type AgentDetailTab = "overview" | "profile" | "downline" | "players"; @@ -57,18 +56,22 @@ export type AgentLineDetailPanelProps = { profileReadOnly: boolean; canViewDownlineTab: boolean; canViewPlayersTab: boolean; + playersTabHint?: string | null; canManageNode: boolean; canCreateChild: boolean; canCreateChildAgent: boolean; + canCreatePlayerAction: boolean; canDeleteChild: (node: AgentNodeRow) => boolean; onEditChild: (node: AgentNodeRow) => void; onDeleteChild: (node: AgentNodeRow) => void; onAddChild: () => void; + onAddPlayer: () => void; onEditCurrent: () => void; onSelectChild: (node: AgentNodeRow) => void; profileFields: AgentProfileFieldsProps | null; profileSaving: boolean; onSaveProfile: () => void; + playerCreateRequestKey?: number; }; export function AgentLineDetailPanel({ @@ -87,18 +90,22 @@ export function AgentLineDetailPanel({ profileReadOnly, canViewDownlineTab, canViewPlayersTab, + playersTabHint, canManageNode, canCreateChild, canCreateChildAgent, + canCreatePlayerAction, canDeleteChild, onEditChild, onDeleteChild, onAddChild, + onAddPlayer, onEditCurrent, onSelectChild, profileFields, profileSaving, onSaveProfile, + playerCreateRequestKey = 0, }: AgentLineDetailPanelProps): React.ReactElement { const { t } = useTranslation(["agents", "common"]); @@ -120,13 +127,6 @@ export function AgentLineDetailPanel({ ); } - const cycleLabel = - profile?.settlement_cycle === "daily" - ? t("profile.cycleDaily", { defaultValue: "日结" }) - : profile?.settlement_cycle === "monthly" - ? t("profile.cycleMonthly", { defaultValue: "月结" }) - : t("profile.cycleWeekly", { defaultValue: "周结" }); - const tabs: { key: AgentDetailTab; label: string; count?: number; visible: boolean }[] = [ { key: "overview", @@ -157,8 +157,6 @@ export function AgentLineDetailPanel({ siteLabel && siteCode.trim() !== "" ? `${siteLabel} (${siteCode})` : siteLabel ?? siteCode; - const codeText = typeof node.code === "string" ? node.code.trim() : ""; - const usernameText = typeof node.username === "string" ? node.username.trim() : ""; const childActionHint = canCreateChild ? null : canCreateChildAgent @@ -168,11 +166,22 @@ export function AgentLineDetailPanel({ : t("lineUi.addChildNoPermissionHint", { defaultValue: "当前账号没有为该节点创建下级代理的权限。", }); + const playerActionHint = + canViewPlayersTab && !canCreatePlayerAction ? playersTabHint ?? null : null; + const showPrimaryAction = detailTab === "downline" || detailTab === "players"; + const primaryActionEnabled = + detailTab === "players" ? canCreatePlayerAction : canCreateChild; + const primaryActionLabel = + detailTab === "players" + ? t("lineUi.createDirectPlayer", { defaultValue: "创建直属玩家" }) + : t("createChild", { defaultValue: "添加下级代理" }); + const primaryActionHint = + detailTab === "players" ? playerActionHint : childActionHint; return (
-
-
+
+

@@ -184,60 +193,38 @@ export function AgentLineDetailPanel({ : t("common:status.disabled", { defaultValue: "停用" })}

- {(codeText !== "" || usernameText !== "" || parentName) ? ( -
- {codeText !== "" ? ( - - {t("lineUi.agentCode", { defaultValue: "编码" })} {codeText} - - ) : null} - {usernameText !== "" ? ( - - {t("lineUi.agentUsername", { defaultValue: "账号" })} {usernameText} - - ) : null} - {parentName ? ( - - {t("parentAgent", { defaultValue: "上级代理" })} {parentName} - - ) : null} -
+ {siteDisplay ? ( +

+ {siteDisplay} +

) : null}
-
- {siteDisplay ? ( -
- - {t("lineUi.currentSite", { defaultValue: "当前站点" })} - - | - {siteDisplay} -
- ) : null} +
{canManageNode ? ( -
+ <>
- {canCreateChild ? ( - ) : null}
- {childActionHint ? ( -

- {childActionHint} + {primaryActionHint ? ( +

+ {primaryActionHint}

) : null} -
+ ) : null}
@@ -266,10 +253,10 @@ export function AgentLineDetailPanel({ onDetailTabChange("downline")} onGoToPlayers={() => onDetailTabChange("players")} @@ -319,6 +306,7 @@ export function AgentLineDetailPanel({ ) : null}
@@ -345,20 +334,20 @@ export function AgentLineDetailPanel({ function OverviewTab({ profile, profileLoading, - cycleLabel, profileReadOnly, canViewDownlineTab, canViewPlayersTab, + playersTabHint, childCount, onGoToDownline, onGoToPlayers, }: { profile: AgentProfileRow | null; profileLoading: boolean; - cycleLabel: string; profileReadOnly: boolean; canViewDownlineTab: boolean; canViewPlayersTab: boolean; + playersTabHint?: string | null; childCount: number; onGoToDownline: () => void; onGoToPlayers: () => void; @@ -367,6 +356,10 @@ function OverviewTab({ const rebateCap = profile && !profileLoading ? percentValueToUi(profile.rebate_limit ?? 0) : null; + const parentRelativeShare = relativeShareRate( + profile?.total_share_rate, + profile?.parent_caps?.total_share_rate, + ); return (
@@ -392,12 +385,17 @@ function OverviewTab({ label={t("profile.totalShareRate", { defaultValue: "占成比例" })} value={profileLoading ? "…" : `${profile?.total_share_rate ?? 0}%`} subtitle={ - rebateCap !== null - ? t("lineUi.shareRebateCap", { - defaultValue: "回水上限 {{rate}}%", - rate: rebateCap, + parentRelativeShare + ? t("profile.relativeShareRateValue", { + defaultValue: "占上级 {{rate}}%", + rate: parentRelativeShare, }) - : undefined + : rebateCap !== null + ? t("lineUi.shareRebateCap", { + defaultValue: "回水上限 {{rate}}%", + rate: rebateCap, + }) + : undefined } accent /> @@ -418,17 +416,24 @@ function OverviewTab({ )} {!profileReadOnly && !profileLoading && profile ? ( -

- {t("lineUi.profileFootnote", { - defaultValue: "回水上限 {{rebate}}% · 默认回水 {{defaultRebate}}% · {{cycle}}", - rebate: percentValueToUi(profile.rebate_limit ?? 0), - defaultRebate: percentValueToUi(profile.default_player_rebate ?? 0), - cycle: cycleLabel, - })} - {(profile.risk_tags?.length ?? 0) > 0 - ? ` · ${t("profile.riskTags", { defaultValue: "风控" })}: ${profile.risk_tags?.join(", ")}` - : ""} -

+
+ + + 0 + ? profile.risk_tags!.join(", ") + : t("common:states.none", { defaultValue: "无" }) + } + /> +
) : null} {profileReadOnly ? ( @@ -440,14 +445,18 @@ function OverviewTab({

) : null} - {canViewDownlineTab || canViewPlayersTab ? ( + {canViewDownlineTab || canViewPlayersTab || playersTabHint ? (
{canViewDownlineTab ? ( ) : null} + {!canViewPlayersTab && playersTabHint ? ( + + +
+ +
+
+

+ {t("lineUi.tabPlayers", { defaultValue: "直属玩家" })} +

+

{playersTabHint}

+
+
+
+ ) : null}
) : null}
@@ -474,38 +501,55 @@ function OverviewTab({ function OverviewLinkCard({ icon: Icon, title, + summary, description, actionLabel, onAction, }: { icon: ComponentType<{ className?: string }>; title: string; + summary: string; description: string; actionLabel: string; onAction: () => void; }): React.ReactElement { return ( - - -
-
- -
-
-

{title}

-

{description}

+ + +
+
+
+ +
+ +
+

{title}

+

5 ? "text-xl" : "text-2xl tabular-nums" + )}> + {summary} +

+

+ {description} +

-
); @@ -514,6 +558,7 @@ function OverviewLinkCard({ function DownlineTable({ childAgents, childCountById, + parentTotalShareRate, canManageNode, canCreateChild, canDeleteChild, @@ -524,6 +569,7 @@ function DownlineTable({ }: { childAgents: AgentNodeRow[]; childCountById: Map; + parentTotalShareRate?: number; canManageNode: boolean; canCreateChild: boolean; canDeleteChild: (node: AgentNodeRow) => boolean; @@ -537,122 +583,120 @@ function DownlineTable({ const editChildLabel = t("lineUi.editDownline", { defaultValue: "编辑代理" }); const deleteChildLabel = t("lineUi.deleteDownline", { defaultValue: "删除代理" }); - if (childAgents.length === 0) { - return ( -
- - {canManageNode && canCreateChild ? ( - - ) : null} - -
- ); - } - return (
-
- - - - {t("agentCode", { defaultValue: "代理编码" })} - {t("agentName", { defaultValue: "代理名称" })} - {t("loginUsername", { defaultValue: "登录名" })} - {t("lineUi.downlineColumns.email", { defaultValue: "邮箱" })} - - {t("profile.totalShareRate", { defaultValue: "占成 (%)" })} - - - {t("profile.creditLimit", { defaultValue: "授信额度" })} - - - {t("lineUi.allocatedCredit", { defaultValue: "已下发" })} - - - {t("profile.settlementCycle", { defaultValue: "结算周期" })} - - - {t("lineUi.downlineColumns.downlineCount", { defaultValue: "下级数" })} - - {t("common:status.label", { defaultValue: "状态" })} - {canManageNode ? ( - - {t("common:table.actions", { defaultValue: "操作" })} +
+
+ + + {t("agentCode", { defaultValue: "代理编码" })} + {t("agentName", { defaultValue: "代理名称" })} + {t("loginUsername", { defaultValue: "登录名" })} + {t("lineUi.downlineColumns.email", { defaultValue: "邮箱" })} + + {t("profile.totalShareRate", { defaultValue: "占成 (%)" })} - ) : null} - - - - {childAgents.map((child) => { - const summary = child.profile_summary; - return ( - onSelectChild(child)} - > - {child.code} - {child.name} - {child.username ?? "—"} - - {child.email ?? "—"} - - - {summary ? `${summary.total_share_rate ?? 0}%` : "—"} - - - {summary ? formatCredit(summary.credit_limit) : "—"} - - - {summary ? formatCredit(summary.allocated_credit) : "—"} - - - {summary ? settlementCycleLabel(summary.settlement_cycle, t) : "—"} - - - {childCountById.get(child.id) ?? 0} - - - - {child.status === 1 - ? t("common:status.enabled", { defaultValue: "启用" }) - : t("common:status.disabled", { defaultValue: "停用" })} - - - {canManageNode ? ( - e.stopPropagation()} + + {t("profile.creditLimit", { defaultValue: "授信额度" })} + + + {t("lineUi.allocatedCredit", { defaultValue: "已下发" })} + + + {t("lineUi.downlineColumns.downlineCount", { defaultValue: "下级数" })} + + {t("common:status.label", { defaultValue: "状态" })} + {canManageNode ? ( + + {t("common:table.actions", { defaultValue: "操作" })} + + ) : null} + + + + {childAgents.length === 0 ? ( + + ) : ( + childAgents.map((child) => { + const summary = child.profile_summary; + return ( + onSelectChild(child)} > - onEditChild(child), - }, - { - key: "delete", - label: deleteChildLabel, - icon: Trash2, - destructive: true, - disabled: !canDeleteChild(child), - onClick: () => onDeleteChild(child), - }, - ]} - /> - - ) : null} - - ); - })} - -
-
+ {child.code} + {child.name} + {child.username ?? "—"} + + {child.email ?? "—"} + + + {summary ? ( +
+
{`${summary.total_share_rate ?? 0}%`}
+ {parentTotalShareRate && parentTotalShareRate > 0 ? ( +
+ {t("profile.relativeShareRateValue", { + defaultValue: "占上级 {{rate}}%", + rate: relativeShareRate( + summary.total_share_rate, + parentTotalShareRate, + ) ?? "0", + })} +
+ ) : null} +
+ ) : "—"} +
+ + {summary ? formatCredit(summary.credit_limit) : "—"} + + + {summary ? formatCredit(summary.allocated_credit) : "—"} + + + {childCountById.get(child.id) ?? 0} + + + + {child.status === 1 + ? t("common:status.enabled", { defaultValue: "启用" }) + : t("common:status.disabled", { defaultValue: "停用" })} + + + {canManageNode ? ( + e.stopPropagation()} + > + onEditChild(child), + }, + { + key: "delete", + label: deleteChildLabel, + icon: Trash2, + destructive: true, + disabled: !canDeleteChild(child), + onClick: () => onDeleteChild(child), + }, + ]} + /> + + ) : null} + + ); + }) + )} + + +
); } diff --git a/src/modules/agents/agent-line-provision-wizard.tsx b/src/modules/agents/agent-line-provision-wizard.tsx index c29ad58..177ad71 100644 --- a/src/modules/agents/agent-line-provision-wizard.tsx +++ b/src/modules/agents/agent-line-provision-wizard.tsx @@ -20,12 +20,20 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { useAsyncEffect } from "@/hooks/use-async-effect"; -import { percentValueToUi } from "@/lib/admin-rate-percent"; import { adminSiteCodeLabel } from "@/lib/admin-select-display"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminIntegrationSiteRow } from "@/types/api/admin-integration-site"; +import type { AdminAgentLineProvisionResult } from "@/types/api/admin-agent-line"; -export function AgentLineProvisionWizard(): React.ReactElement { +type AgentLineProvisionWizardProps = { + embedded?: boolean; + onSuccess?: (result: AdminAgentLineProvisionResult) => void | Promise; +}; + +export function AgentLineProvisionWizard({ + embedded = false, + onSuccess, +}: AgentLineProvisionWizardProps): React.ReactElement { const { t } = useTranslation(["agents", "common"]); const [submitting, setSubmitting] = useState(false); const [sitesLoading, setSitesLoading] = useState(true); @@ -40,7 +48,6 @@ export function AgentLineProvisionWizard(): React.ReactElement { credit_limit: "0", rebate_limit: "0", default_player_rebate: "0", - settlement_cycle: "weekly" as "daily" | "weekly" | "monthly", can_grant_extra_rebate: false, }); @@ -63,32 +70,114 @@ export function AgentLineProvisionWizard(): React.ReactElement { toast.error(t("agents:lineProvision.siteRequired", { defaultValue: "请选择接入站点" })); return; } + if (!form.code.trim()) { + toast.error(t("agents:lineProvision.codeRequired", { defaultValue: "请填写代理编码" })); + return; + } + if (!/^[a-z0-9][a-z0-9_-]*$/i.test(form.code.trim())) { + toast.error( + t("agents:lineProvision.codePatternInvalid", { + defaultValue: "代理编码仅支持字母、数字、下划线和中划线,且需以字母或数字开头", + }), + ); + return; + } + if (!form.name.trim()) { + toast.error(t("agents:nameRequired", { defaultValue: "请填写代理名称" })); + return; + } + if (!form.username.trim()) { + toast.error(t("agents:usernameRequired", { defaultValue: "请填写登录名" })); + return; + } + if (!form.password.trim()) { + toast.error(t("agents:passwordRequired", { defaultValue: "请填写密码" })); + return; + } + if (form.password.trim().length < 8) { + toast.error(t("agents:passwordMinLength", { defaultValue: "密码至少 8 位" })); + return; + } + + const shareRate = Number.parseFloat(form.total_share_rate); + const creditLimit = Number.parseInt(form.credit_limit, 10); + const rebateLimit = Number.parseFloat(form.rebate_limit); + const defaultPlayerRebate = Number.parseFloat(form.default_player_rebate); + + if (Number.isNaN(shareRate) || shareRate < 0 || shareRate > 100) { + toast.error( + t("agents:profile.validation.shareRange", { + defaultValue: "占成比例须在 0–100 之间", + }), + ); + return; + } + if (Number.isNaN(creditLimit) || creditLimit < 0) { + toast.error( + t("agents:profile.validation.creditInvalid", { + defaultValue: "授信额度不能为负数", + }), + ); + return; + } + if (Number.isNaN(rebateLimit) || rebateLimit < 0 || rebateLimit > 100) { + toast.error( + t("agents:profile.validation.rebateLimitRange", { + defaultValue: "回水上限须在 0–100% 之间", + }), + ); + return; + } + if ( + Number.isNaN(defaultPlayerRebate) || + defaultPlayerRebate < 0 || + defaultPlayerRebate > 100 + ) { + toast.error( + t("agents:profile.validation.defaultRebateRange", { + defaultValue: "默认玩家回水须在 0–100% 之间", + }), + ); + return; + } + if (defaultPlayerRebate > rebateLimit) { + toast.error( + t("agents:profile.validation.defaultExceedsLimit", { + defaultValue: "默认玩家回水不能超过回水上限", + }), + ); + return; + } setSubmitting(true); try { - await postAdminAgentLine({ + const result = await postAdminAgentLine({ site_code: form.site_code.trim().toLowerCase(), code: form.code.trim().toLowerCase(), name: form.name.trim(), username: form.username.trim(), password: form.password, - total_share_rate: Number.parseFloat(form.total_share_rate) || 0, - credit_limit: Number.parseInt(form.credit_limit, 10) || 0, - rebate_limit: Number.parseFloat(form.rebate_limit) || 0, - default_player_rebate: Number.parseFloat(form.default_player_rebate) || 0, - settlement_cycle: form.settlement_cycle, + total_share_rate: shareRate, + credit_limit: creditLimit, + rebate_limit: rebateLimit, + default_player_rebate: defaultPlayerRebate, can_grant_extra_rebate: form.can_grant_extra_rebate, }); toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" })); setForm((f) => ({ - ...f, site_code: "", code: "", name: "", username: "", password: "", + total_share_rate: "0", + credit_limit: "0", + rebate_limit: "0", + default_player_rebate: "0", + can_grant_extra_rebate: false, })); const data = await getAdminIntegrationSites(); setSites(data.items); + await onSuccess?.(result); } catch (err) { const msg = err instanceof LotteryApiBizError ? err.message : t("common:error.generic"); @@ -98,18 +187,20 @@ export function AgentLineProvisionWizard(): React.ReactElement { } } - return ( - -

- {t("agents:subnav.provisionHint", { - defaultValue: + const content = ( + <> + {!embedded ? ( +

+ {t("agents:subnav.provisionHint", { + defaultValue: "请先在「平台管理 → 接入配置」创建接入站点;对接密钥在站点创建时一次性展示。", - })} -

+ })} +

+ ) : null}

{t("agents:lineProvision.description", { defaultValue: - "将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水、结算周期。代理编码创建后不可修改。", + "将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水。代理编码创建后不可修改。", })}{" "} +

+ {t("agents:lineProvision.passwordHint", { defaultValue: "至少 8 位" })} +

@@ -224,7 +318,7 @@ export function AgentLineProvisionWizard(): React.ReactElement { max={100} step="0.01" value={form.rebate_limit} - placeholder="0.5" + placeholder="50" onChange={(e) => setForm((f) => ({ ...f, rebate_limit: e.target.value }))} />

@@ -236,46 +330,11 @@ export function AgentLineProvisionWizard(): React.ReactElement { max={100} step="0.01" value={form.default_player_rebate} - placeholder="0.5" + placeholder="50" onChange={(e) => setForm((f) => ({ ...f, default_player_rebate: e.target.value }))} />
-
- - -
+ + ); + + if (embedded) { + return
{content}
; + } + + return ( + + {content} ); } diff --git a/src/modules/agents/agent-line-sidebar.tsx b/src/modules/agents/agent-line-sidebar.tsx index 70e9361..cd0887d 100644 --- a/src/modules/agents/agent-line-sidebar.tsx +++ b/src/modules/agents/agent-line-sidebar.tsx @@ -4,10 +4,8 @@ import { ChevronRight, Search } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { Input } from "@/components/ui/input"; -import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { cn } from "@/lib/utils"; import { formatAdminCreditMajorDecimal } from "@/lib/money"; import type { AgentNodeRow } from "@/types/api/admin-agent"; @@ -66,10 +64,6 @@ function collectExpandableIds(nodes: AgentNodeRow[], into: Set): void { } } -function unwrapSiteRoots(nodes: AgentNodeRow[]): AgentNodeRow[] { - return nodes.flatMap((node) => (node.is_root ? (node.children ?? []) : [node])); -} - export type AgentLineSidebarProps = { siteLabel: string | null; /** API 返回的嵌套树(含 children) */ @@ -110,8 +104,8 @@ function TreeRow({
  • @@ -120,7 +114,7 @@ function TreeRow({ type="button" aria-expanded={expanded} aria-label={expanded ? t("lineUi.collapse", { defaultValue: "收起" }) : t("lineUi.expand", { defaultValue: "展开" })} - className="mt-1 flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted" + className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted" onClick={(e) => { e.stopPropagation(); onToggleExpand(node.id); @@ -132,7 +126,7 @@ function TreeRow({ /> ) : ( - + )} @@ -192,9 +176,7 @@ export function AgentLineSidebar({ const normalizedKeyword = keyword.trim().toLowerCase(); const displayForest = useMemo(() => { - const pruned = pruneTreeForSearch(tree, normalizedKeyword, parentNameMap); - - return unwrapSiteRoots(pruned); + return pruneTreeForSearch(tree, normalizedKeyword, parentNameMap); }, [normalizedKeyword, parentNameMap, tree]); useEffect(() => { diff --git a/src/modules/agents/agent-profile-fields.tsx b/src/modules/agents/agent-profile-fields.tsx index c645980..2466a80 100644 --- a/src/modules/agents/agent-profile-fields.tsx +++ b/src/modules/agents/agent-profile-fields.tsx @@ -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 ( -
    +
    {(parentCaps || availableCredit !== null) && !loading ? ( -
    - {parentCaps ? ( -

    - {t("profile.parentCaps", { - defaultValue: "上级占成 {{share}}%,可下发额度 {{credit}}", - share: parentCaps.total_share_rate, - credit: formatAdminCreditMajorDecimal(parentCaps.available_credit, currencyCode), - })} -

    - ) : null} - {availableCredit !== null ? ( -

    - {t("profile.availableCredit", { - defaultValue: "可下发额度:{{amount}}", - amount: formatAdminCreditMajorDecimal(availableCredit, currencyCode), - })} -

    - ) : null} +
    + +
    + {parentCaps ? ( +
    +

    + {t("profile.parentCaps", { + defaultValue: "上级占成 {{share}}%,可下发 {{credit}}", + share: parentCaps.total_share_rate, + credit: formatAdminCreditMajorDecimal(parentCaps.available_credit, currencyCode), + })} +

    +
    + ) : null} + {availableCredit !== null ? ( +

    + {t("profile.availableCredit", { + defaultValue: "可下发额度 {{amount}}", + amount: formatAdminCreditMajorDecimal(availableCredit, currencyCode), + })} +

    + ) : null} +
    ) : null} + {loading ? ( -

    +

    {t("profile.loading", { defaultValue: "正在加载占成与授信…" })}

    ) : null}
    -
    -
    -
    -
    -
    -
    - - -
    - {!isCard ? ( -

    - {t("profile.capabilityHint", { - defaultValue: - "保存后约束该代理主账号能否开玩家/下级;与平台「代理」角色叠加,以本开关为准。", - })} -

    - ) : null} -
    +
    + {!isCard ? ( +

    + {t("profile.capabilityHint", { + defaultValue: + "保存后约束该代理主账号能否开玩家/下级;与平台「代理」角色叠加,以本开关为准。", + })} +

    + ) : null}
    ); @@ -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 ( -
    - +
    +
    ); diff --git a/src/modules/agents/agents-console.tsx b/src/modules/agents/agents-console.tsx index 7b8f7ec..a2fdf85 100644 --- a/src/modules/agents/agents-console.tsx +++ b/src/modules/agents/agents-console.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -40,11 +41,11 @@ import { import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { PRD_AGENT_MANAGE, + PRD_AGENT_LINE_PROVISION_ACCESS_ANY, PRD_AGENT_PROFILE_MANAGE, PRD_AGENTS_ACCESS_ANY, PRD_USERS_MANAGE, } from "@/lib/admin-prd"; -import { normalizeAgentSettlementCycle } from "@/lib/agent-settlement-cycle"; import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options"; import { useAdminProfile } from "@/stores/admin-session"; import { useAgentManagementSiteStore } from "@/stores/agent-management-site"; @@ -80,8 +81,10 @@ function flattenTree(nodes: AgentNodeRow[]): AgentNodeRow[] { export function AgentsConsole(): React.ReactElement { const { t } = useTranslation(["agents", "common"]); const tRef = useTranslationRef(["agents", "common"]); + const searchParams = useSearchParams(); const profile = useAdminProfile(); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); + const boundAgent = profile?.agent ?? null; const isSuperAdmin = profile?.is_super_admin === true; const canManageNode = @@ -95,6 +98,10 @@ export function AgentsConsole(): React.ReactElement { PRD_AGENT_PROFILE_MANAGE, PRD_AGENT_MANAGE, ]); + const canProvisionLine = + boundAgent === null && + (isSuperAdmin || + adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_LINE_PROVISION_ACCESS_ANY])); const { sites: siteOptions } = useAdminSiteCodeOptions(); const adminSiteId = useAgentManagementSiteStore((s) => s.adminSiteId); const setAdminSiteId = useAgentManagementSiteStore((s) => s.setAdminSiteId); @@ -107,7 +114,7 @@ export function AgentsConsole(): React.ReactElement { const [profileSaving, setProfileSaving] = useState(false); const [selectedProfile, setSelectedProfile] = useState(null); const [selectedProfileLoading, setSelectedProfileLoading] = useState(false); - + const [playerCreateRequestKey, setPlayerCreateRequestKey] = useState(0); const [nodeDialogOpen, setNodeDialogOpen] = useState(false); const [nodeDialogMode, setNodeDialogMode] = useState<"create" | "edit">("create"); const [targetParentId, setTargetParentId] = useState(null); @@ -121,9 +128,6 @@ export function AgentsConsole(): React.ReactElement { const [profileCreditLimit, setProfileCreditLimit] = useState("0"); const [profileRebateLimit, setProfileRebateLimit] = useState("0"); const [profileDefaultRebate, setProfileDefaultRebate] = useState("0"); - const [profileSettlementCycle, setProfileSettlementCycle] = useState< - "daily" | "weekly" | "monthly" - >("weekly"); const [profileExtraRebate, setProfileExtraRebate] = useState(false); const [profileCanCreateChild, setProfileCanCreateChild] = useState(false); const [profileCanCreatePlayer, setProfileCanCreatePlayer] = useState(true); @@ -134,7 +138,6 @@ export function AgentsConsole(): React.ReactElement { const [profileAvailableCredit, setProfileAvailableCredit] = useState(null); const [editingNodeNeedsPrimaryAccount, setEditingNodeNeedsPrimaryAccount] = useState(false); - const boundAgent = profile?.agent ?? null; /** 登录账号是否可向子代理下放「允许创建下级」 */ const canCreateChildAgent = isSuperAdmin || boundAgent?.can_create_child_agent !== false; @@ -142,13 +145,13 @@ export function AgentsConsole(): React.ReactElement { isSuperAdmin || adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]); const [rootProfile, setRootProfile] = useState(null); + const selectedNodeIdFromUrl = Number.parseInt(searchParams.get("agent_node_id") ?? "", 10); const resetProfileForm = (mode: "create" | "edit" = "create") => { setProfileShareRate("0"); setProfileCreditLimit("0"); setProfileRebateLimit("0"); setProfileDefaultRebate("0"); - setProfileSettlementCycle("weekly"); setProfileExtraRebate(false); setProfileCanCreateChild(mode === "create" ? false : false); setProfileCanCreatePlayer(true); @@ -168,7 +171,6 @@ export function AgentsConsole(): React.ReactElement { setProfileCreditLimit(String(row.credit_limit ?? 0)); setProfileRebateLimit(percentValueToUi(row.rebate_limit ?? 0)); setProfileDefaultRebate(percentValueToUi(row.default_player_rebate ?? 0)); - setProfileSettlementCycle(normalizeAgentSettlementCycle(row.settlement_cycle)); setProfileExtraRebate(Boolean(row.can_grant_extra_rebate)); setProfileCanCreateChild(Boolean(row.can_create_child_agent)); setProfileCanCreatePlayer(row.can_create_player !== false); @@ -183,7 +185,6 @@ 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: normalizeAgentSettlementCycle(profileSettlementCycle), can_grant_extra_rebate: profileExtraRebate, can_create_child_agent: profileCanCreateChild, can_create_player: profileCanCreatePlayer, @@ -239,7 +240,7 @@ export function AgentsConsole(): React.ReactElement { () => new Map(flatNodes.map((node) => [node.id, node.name])), [flatNodes], ); - const businessRows = useMemo(() => flatNodes.filter((node) => !node.is_root), [flatNodes]); + const visibleAgentRows = flatNodes; const selectedSiteLabel = useMemo( () => siteOptions.find((site) => site.id === adminSiteId)?.name ?? null, [adminSiteId, siteOptions], @@ -324,6 +325,16 @@ export function AgentsConsole(): React.ReactElement { } }, [adminSiteId, canViewAgents, isSuperAdmin, profile?.agent?.admin_site_id, setAdminSiteId, siteOptions]); + useEffect(() => { + if (!Number.isInteger(selectedNodeIdFromUrl) || selectedNodeIdFromUrl <= 0) { + return; + } + if (!flatNodes.some((node) => node.id === selectedNodeIdFromUrl)) { + return; + } + setSelectedNodeId(selectedNodeIdFromUrl); + }, [flatNodes, selectedNodeIdFromUrl]); + useAsyncEffect(() => { if (selectedNode === null) { setSelectedProfile(null); @@ -374,6 +385,26 @@ export function AgentsConsole(): React.ReactElement { [hasUsersManagePermission, selectedNode, selectedProfile, selectedProfileLoading], ); + const playersTabHint = useMemo(() => { + if (selectedNode === null || selectedProfileLoading) { + return null; + } + + if (selectedProfile?.can_create_player !== true) { + return t("lineUi.playersUnavailableHint", { + defaultValue: "当前代理未开启“允许创建玩家”,如需新增请先调整该代理配置。", + }); + } + + if (!hasUsersManagePermission) { + return t("lineUi.playersNoPermissionHint", { + defaultValue: "当前账号没有该节点的玩家管理权限。", + }); + } + + return null; + }, [hasUsersManagePermission, selectedNode, selectedProfile, selectedProfileLoading, t]); + const canCreateChildOnSelected = useMemo( () => canManageNode && selectedProfile?.can_create_child_agent === true, [canManageNode, selectedProfile?.can_create_child_agent], @@ -401,15 +432,15 @@ export function AgentsConsole(): React.ReactElement { ]); useAsyncEffect(() => { - if (businessRows.length === 0) { + if (visibleAgentRows.length === 0) { setSelectedNodeId(null); return; } - if (selectedNodeId === null || !businessRows.some((row) => row.id === selectedNodeId)) { - setSelectedNodeId(businessRows[0]?.id ?? null); + if (selectedNodeId === null || !visibleAgentRows.some((row) => row.id === selectedNodeId)) { + setSelectedNodeId(visibleAgentRows[0]?.id ?? null); } - }, [businessRows, selectedNodeId]); + }, [visibleAgentRows, selectedNodeId]); useEffect(() => { setDetailTab("overview"); @@ -422,10 +453,14 @@ export function AgentsConsole(): React.ReactElement { }, [detailTab, isOwnAgentNode]); useAsyncEffect(() => { + if (!canViewAgents) { + setLoading(false); + return; + } if (adminSiteId !== null) { void loadTree(adminSiteId); } - }, [adminSiteId, loadTree]); + }, [adminSiteId, canViewAgents, loadTree]); const openCreateChildForNode = (node: AgentNodeRow) => { setNodeDialogMode("create"); @@ -435,9 +470,10 @@ export function AgentsConsole(): React.ReactElement { setNodeStatus(1); setNodeUsername(""); setNodePassword(""); - setProfileLoading(false); - setProfileLoaded(true); + setProfileLoading(canManageProfile); + setProfileLoaded(!canManageProfile); setEditingNodeNeedsPrimaryAccount(false); + resetProfileForm("create"); setNodeDialogOpen(true); if (canManageProfile) { void getAgentNodeProfile(node.id) @@ -450,15 +486,18 @@ export function AgentsConsole(): React.ReactElement { available_credit: p.available_credit ?? 0, }); setProfileAvailableCredit(p.available_credit ?? null); - resetProfileForm("create"); + setProfileLoaded(true); }) .catch(() => { setProfileParentCaps(null); setProfileAvailableCredit(null); - resetProfileForm("create"); + setProfileLoaded(false); + }) + .finally(() => { + setProfileLoading(false); }); } else { - resetProfileForm("create"); + setProfileLoading(false); } }; @@ -573,8 +612,6 @@ export function AgentsConsole(): React.ReactElement { onRebateLimitChange: setProfileRebateLimit, defaultRebate: profileDefaultRebate, onDefaultRebateChange: setProfileDefaultRebate, - settlementCycle: profileSettlementCycle, - onSettlementCycleChange: setProfileSettlementCycle, extraRebate: profileExtraRebate, onExtraRebateChange: setProfileExtraRebate, canCreatePlayer: profileCanCreatePlayer, @@ -598,12 +635,11 @@ export function AgentsConsole(): React.ReactElement { profileParentCaps, profileRebateLimit, profileRiskTags, - profileSettlementCycle, profileShareRate, selectedProfileLoading, ]); - const showAgentSidebar = businessRows.length > 1; + const showAgentSidebar = visibleAgentRows.length > 0; const openAddAgent = (): void => { const parent = selectedNode ?? rootNode; @@ -742,7 +778,7 @@ export function AgentsConsole(): React.ReactElement { return null; }, [addParent, rootNode, rootProfile, selectedNodeId, selectedProfile]); - if (!canViewAgents) { + if (!canViewAgents && !canProvisionLine) { return (

    {t("noAccess", { defaultValue: "您没有代理经营相关权限,请联系管理员开通。" })} @@ -750,7 +786,7 @@ export function AgentsConsole(): React.ReactElement { ); } - if (loading && tree.length === 0) { + if (canViewAgents && loading && tree.length === 0) { return ; } @@ -758,17 +794,18 @@ export function AgentsConsole(): React.ReactElement {

    - {err ?

    {err}

    : null} + {canViewAgents && err ?

    {err}

    : null} -
    - {showAgentSidebar ? ( + {canViewAgents ? ( +
    + {showAgentSidebar ? ( { setKeyword(value); }} @@ -798,12 +835,19 @@ export function AgentsConsole(): React.ReactElement { profileReadOnly={isOwnAgentNode} canViewDownlineTab={canShowDownlineTab} canViewPlayersTab={canShowPlayersTab} + playersTabHint={playersTabHint} canManageNode={canManageNode} canCreateChild={canCreateChildOnSelected} canCreateChildAgent={canCreateChildAgent} + canCreatePlayerAction={ + isSuperAdmin || + (selectedProfile?.can_create_player === true && + adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE])) + } canDeleteChild={canDeleteNode} onEditChild={(node) => openEditForNode(node)} onAddChild={() => selectedNode && openCreateChildForNode(selectedNode)} + onAddPlayer={() => setPlayerCreateRequestKey((value) => value + 1)} onEditCurrent={() => selectedNode && openEditForNode(selectedNode)} onDeleteChild={(node) => handleDeleteNode(node)} onSelectChild={(child) => { @@ -812,8 +856,16 @@ export function AgentsConsole(): React.ReactElement { profileFields={inlineProfileFields} profileSaving={profileSaving} onSaveProfile={() => void saveInlineProfile()} - /> -
    + playerCreateRequestKey={playerCreateRequestKey} + /> +
    + ) : ( +
    + {t("lineUi.provisionOnlyHint", { + defaultValue: "当前账号仅可开通一级代理线路,请前往「开通一级代理」页面创建新线路。", + })} +
    + )} -
    -
    - - setNodeName(e.target.value)} - autoComplete="off" - /> -
    +
    +
    +
    + + setNodeName(e.target.value)} + autoComplete="off" + className="bg-background/50 transition-colors focus:bg-background" + /> +
    -
    - - setNodeUsername(e.target.value)} - autoComplete="off" - /> -
    +
    +
    + + setNodeUsername(e.target.value)} + autoComplete="off" + className="bg-background/50 transition-colors focus:bg-background" + /> +
    -
    - - {nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount ? ( -

    - {t("bindAccountHint", { - defaultValue: "该代理尚无登录账号,保存时将自动创建并绑定。", - })} -

    - ) : null} - setNodePassword(e.target.value)} - placeholder={ - nodeDialogMode === "edit" && !editingNodeNeedsPrimaryAccount - ? t("passwordOptionalHint") - : t("passwordPlaceholder") - } - autoComplete="new-password" - /> -
    +
    + + setNodePassword(e.target.value)} + placeholder={ + nodeDialogMode === "edit" && !editingNodeNeedsPrimaryAccount + ? t("passwordOptionalHint") + : t("passwordPlaceholder") + } + autoComplete="new-password" + className="bg-background/50 transition-colors focus:bg-background" + /> + {nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount ? ( +

    + {t("bindAccountHint", { + defaultValue: "该代理尚无登录账号,保存时将自动创建并绑定。", + })} +

    + ) : null} +
    +
    -
    - setNodeStatus(value ? 1 : 0)} /> - -
    +
    + + setNodeStatus(value ? 1 : 0)} /> +
    - {canManageProfile && - (nodeDialogMode === "create" || - (editingNodeId !== null && boundAgent?.id !== editingNodeId)) ? ( -
    -

    - {t("profile.section", { defaultValue: "占成与授信" })} -

    - + {canManageProfile && + (nodeDialogMode === "create" || + (editingNodeId !== null && boundAgent?.id !== editingNodeId)) ? ( +
    +

    + {t("profile.section", { defaultValue: "占成与授信配置" })} +

    + +
    + ) : null}
    - ) : null}
    diff --git a/src/modules/agents/agents-directory-console.tsx b/src/modules/agents/agents-directory-console.tsx new file mode 100644 index 0000000..d5abda2 --- /dev/null +++ b/src/modules/agents/agents-directory-console.tsx @@ -0,0 +1,308 @@ +"use client"; + +import Link from "next/link"; +import { RefreshCw, Search } from "lucide-react"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { getAgentNodes } from "@/api/admin-agents"; +import { AdminPageCard } from "@/components/admin/admin-page-card"; +import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; +import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; +import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useAsyncEffect } from "@/hooks/use-async-effect"; +import { useTranslationRef } from "@/hooks/use-translation-ref"; +import type { AgentNodeRow } from "@/types/api/admin-agent"; +import { cn } from "@/lib/utils"; + +function formatPercent(value: number | null | undefined): string { + if (value == null || Number.isNaN(value)) { + return "-"; + } + + return `${Number(value).toFixed(2).replace(/\.?0+$/, "")}%`; +} + +function formatCredit(value: number | null | undefined): string { + if (value == null || Number.isNaN(value)) { + return "-"; + } + + return new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }).format(value); +} + +function statusLabel(status: number, t: (key: string, options?: { defaultValue?: string }) => string): string { + return status === 1 + ? t("statusEnabled", { defaultValue: "启用" }) + : t("statusDisabled", { defaultValue: "停用" }); +} + +export function AgentsDirectoryConsole(): React.ReactElement { + const { t } = useTranslation(["agents", "common"]); + const tRef = useTranslationRef(["agents", "common"]); + + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(null); + const [keyword, setKeyword] = useState(""); + const [status, setStatus] = useState<"all" | "enabled" | "disabled">("all"); + const [includeRoots, setIncludeRoots] = useState(false); + const [reloadKey, setReloadKey] = useState(0); + + const parentNameMap = useMemo( + () => new Map(items.map((item) => [item.id, item.name])), + [items], + ); + + const load = useCallback(async () => { + setLoading(true); + setErr(null); + try { + const data = await getAgentNodes(); + setItems(data.items); + } catch (error) { + const message = + error instanceof Error + ? error.message + : tRef.current("agents:loadFailed", { defaultValue: "加载代理列表失败" }); + setErr(message); + } finally { + setLoading(false); + } + }, [tRef]); + + useAsyncEffect(load, [load, reloadKey]); + + const filteredItems = useMemo(() => { + const normalized = keyword.trim().toLowerCase(); + + return items.filter((item) => { + if (!includeRoots && item.is_root) { + return false; + } + if (status === "enabled" && item.status !== 1) { + return false; + } + if (status === "disabled" && item.status === 1) { + return false; + } + if (!normalized) { + return true; + } + + const parentName = item.parent_id != null ? parentNameMap.get(item.parent_id) ?? "" : ""; + return [item.name, item.code, item.username ?? "", item.email ?? "", parentName] + .join(" ") + .toLowerCase() + .includes(normalized); + }); + }, [includeRoots, items, keyword, parentNameMap, status]); + + const totalOperatingAgents = useMemo( + () => items.filter((item) => !item.is_root).length, + [items], + ); + const enabledOperatingAgents = useMemo( + () => items.filter((item) => !item.is_root && item.status === 1).length, + [items], + ); + + return ( +
    +
    +
    +

    + {t("summary.visibleAgents", { defaultValue: "当前可见经营代理数" })} +

    +

    {totalOperatingAgents}

    +
    +
    +

    + {t("summary.enabledAgents", { defaultValue: "启用中的经营代理数" })} +

    +

    {enabledOperatingAgents}

    +
    +
    +

    + {t("summary.visibleList", { defaultValue: "当前平铺列表条数" })} +

    +

    {filteredItems.length}

    +
    +
    + + setReloadKey((value) => value + 1)} + disabled={loading} + > + + {t("common:actions.refresh", { defaultValue: "刷新" })} + + } + > +
    +
    + + setKeyword(event.target.value)} + placeholder={t("listSearch", { defaultValue: "搜索代理名称 / 编码 / 登录名" })} + className="pl-9" + /> +
    +
    + + +
    +
    + + {err ? ( +
    + {err} +
    + ) : null} + +
    + + + + {t("name", { defaultValue: "名称" })} + {t("code", { defaultValue: "编码" })} + {t("depth", { defaultValue: "层级" })} + {t("status", { defaultValue: "状态" })} + {t("parentAgent", { defaultValue: "上级代理" })} + {t("username", { defaultValue: "登录名" })} + + {t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })} + + + {t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })} + + + {t("profile.creditLimit", { defaultValue: "授信额度" })} + + + {t("lineUi.availableCredit", { defaultValue: "可下发" })} + + + {t("common:actions.title", { defaultValue: "操作" })} + + + + + {loading ? ( + + ) : filteredItems.length === 0 ? ( + + ) : ( + filteredItems.map((item) => { + const parentName = + item.parent_id != null ? parentNameMap.get(item.parent_id) ?? "-" : "-"; + const profile = item.profile_summary; + + return ( + + +
    + {item.name} +
    +
    + + {item.code} + + + {item.depth} + + + {item.is_root ? ( + + {t("isRoot", { defaultValue: "根节点" })} + + ) : ( + + {statusLabel(item.status, t)} + + )} + + + {parentName} + + + {item.username ?? "-"} + + + {formatPercent(profile?.total_share_rate)} + + + {formatPercent(profile?.rebate_limit)} + + + {formatCredit(profile?.credit_limit)} + + + {formatCredit(profile?.available_credit)} + + + + {t("common:actions.view", { defaultValue: "查看" })} + + +
    + ); + }) + )} +
    +
    +
    +
    +
    + ); +} diff --git a/src/modules/agents/agents-players-panel.tsx b/src/modules/agents/agents-players-panel.tsx index 3bbd23c..87ea4c9 100644 --- a/src/modules/agents/agents-players-panel.tsx +++ b/src/modules/agents/agents-players-panel.tsx @@ -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({ })}

    ) : ( -

    - {t("playersPanel.creditListHint", { - defaultValue: "信用占成盘:下列为玩家授信额度与可用信用,非主站钱包余额。", - })} -

    +
    )} - {canCreatePlayer ? ( + {canCreatePlayer && !embedded ? ( @@ -1174,10 +1250,10 @@ export function AgentsPlayersPanel({ step="0.01" value={editRebateRate} onChange={(e) => setEditRebateRate(e.target.value)} - placeholder="0.5" + placeholder="0" />

    - {t("playersPanel.rebateRateHint", { defaultValue: "填写百分比,如 0.5 表示 0.5%" })} + {t("playersPanel.rebateRateHint", { defaultValue: "填写百分比,如 5 表示 5%" })}

    diff --git a/src/modules/agents/agents-subnav.tsx b/src/modules/agents/agents-subnav.tsx index ba7552e..c1407c6 100644 --- a/src/modules/agents/agents-subnav.tsx +++ b/src/modules/agents/agents-subnav.tsx @@ -1,52 +1,23 @@ "use client"; +import { Check, ChevronDown, Search } from "lucide-react"; +import { useDeferredValue, useEffect, useMemo, useState } from "react"; import { usePathname } from "next/navigation"; -import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { - AdminSubnav, AdminSubnavBar, - AdminSubnavLink, } from "@/components/admin/admin-subnav"; +import { buttonVariants } from "@/components/ui/button"; import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options"; -import { isAgentLineSubnavTabVisible } from "@/modules/agents/agent-line-subnav-visibility"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { PRD_INTEGRATION_ACCESS_ANY } from "@/lib/admin-prd"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { useAdminProfile } from "@/stores/admin-session"; import { useAgentManagementSiteStore } from "@/stores/agent-management-site"; - -const primaryTabs: { - href: string; - labelKey: string; - matchPrefix: string; -}[] = [ - { - href: "/admin/agents", - labelKey: "subnav.operations", - matchPrefix: "/admin/agents", - }, -]; - -const provisionTab = { - href: "/admin/agents/provision", - labelKey: "subnav.provision", - matchPrefix: "/admin/agents/provision", -} as const; - -function isTabActive(pathname: string, href: string, matchPrefix: string): boolean { - if (href === "/admin/agents") { - return ( - pathname === "/admin/agents" || - pathname === "/admin/agents/list" || - (pathname.startsWith("/admin/agents/") && - !pathname.startsWith("/admin/agents/provision")) - ); - } - - return pathname === href || pathname.startsWith(`${matchPrefix}/`) || pathname === matchPrefix; -} +import { cn } from "@/lib/utils"; export function AgentsSubnav(): React.ReactElement { const { t } = useTranslation("agents"); @@ -55,18 +26,14 @@ export function AgentsSubnav(): React.ReactElement { const { sites: siteOptions } = useAdminSiteCodeOptions(); const adminSiteId = useAgentManagementSiteStore((s) => s.adminSiteId); const setAdminSiteId = useAgentManagementSiteStore((s) => s.setAdminSiteId); + const [sitePickerOpen, setSitePickerOpen] = useState(false); + const [siteKeyword, setSiteKeyword] = useState(""); + const deferredKeyword = useDeferredValue(siteKeyword); const canSwitchSite = profile?.is_super_admin === true || adminHasAnyPermission(profile?.permissions, [...PRD_INTEGRATION_ACCESS_ANY]); - const showProvision = isAgentLineSubnavTabVisible(provisionTab.href, profile); - - const visiblePrimaryTabs = useMemo( - () => primaryTabs.filter((tab) => isAgentLineSubnavTabVisible(tab.href, profile)), - [profile], - ); - useEffect(() => { if (adminSiteId !== null || siteOptions.length === 0) { return; @@ -80,58 +47,94 @@ export function AgentsSubnav(): React.ReactElement { }, [adminSiteId, profile?.agent?.admin_site_id, setAdminSiteId, siteOptions]); const selectSiteId = adminSiteId ?? siteOptions[0]?.id ?? null; - const selectedSiteLabel = useMemo(() => { + const selectedSite = useMemo(() => { const site = siteOptions.find((item) => item.id === selectSiteId); - return site ? `${site.name} (${site.code})` : null; + return site ?? null; }, [selectSiteId, siteOptions]); - if (visiblePrimaryTabs.length === 0 && !showProvision) { - return <>; - } + const filteredSites = useMemo(() => { + const normalized = deferredKeyword.trim().toLowerCase(); + if (normalized === "") { + return siteOptions; + } + + return siteOptions.filter((site) => site.name.toLowerCase().includes(normalized)); + }, [deferredKeyword, siteOptions]); const siteSelector = - canSwitchSite && siteOptions.length > 0 && selectSiteId !== null ? ( - + pathname !== "/admin/agents/list" && canSwitchSite && siteOptions.length > 0 && selectSiteId !== null ? ( + + + + {selectedSite?.name ?? t("lineFilter", { defaultValue: "一级代理" })} + + + {selectedSite?.code ?? ""} + + + + +
    +
    + + setSiteKeyword(event.target.value)} + placeholder={t("siteSearch", { defaultValue: "搜索站点名称" })} + className="pl-9" + /> +
    +
    + +
    + {filteredSites.map((site) => { + const active = site.id === selectSiteId; + + return ( + + ); + })} + {filteredSites.length === 0 ? ( +
    + {t("common:states.empty", { defaultValue: "暂无数据" })} +
    + ) : null} +
    +
    +
    +
    ) : null; return ( - - {visiblePrimaryTabs.map((tab) => ( - - {t(tab.labelKey)} - - ))} - - {showProvision ? ( - - {t(provisionTab.labelKey)} - - ) : null} - +
    +

    + {t("title", { defaultValue: "代理管理" })} +

    +
    ); } diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx index f624083..588c1e5 100644 --- a/src/modules/config/doc/odds-config-doc-screen.tsx +++ b/src/modules/config/doc/odds-config-doc-screen.tsx @@ -659,14 +659,14 @@ export function OddsConfigDocScreen({ id="odds-rebate-rate" type="text" inputMode="decimal" - className="h-9 font-mono tabular-nums" + className="h-9 text-base font-semibold" disabled={saving} value={rebatePercentUi} placeholder={t("odds.placeholders.rebateRate", { ns: "config" })} onChange={(e) => setRebateForPlayPercent(e.target.value)} /> ) : ( - + {rebatePercentUi} )} @@ -703,7 +703,7 @@ export function OddsConfigDocScreen({ ) : ( - + {oddsMultiplierLabel(row.odds_value)} ) @@ -743,7 +743,7 @@ export function OddsConfigDocScreen({ ) : ( - + {oddsMultiplierLabel(row.odds_value)} ) @@ -771,14 +771,14 @@ export function OddsConfigDocScreen({ setRebateForPlayPercent(e.target.value)} /> ) : ( - + {rebatePercentUi} )} @@ -857,10 +857,10 @@ export function OddsConfigDocScreen({ {publishDiffRows.map((row) => (
    {row.label} - + {row.oldValue === null ? "—" : oddsMultiplierLabel(row.oldValue)} - + {row.newValue === null ? "—" : oddsMultiplierLabel(row.newValue)}
    diff --git a/src/modules/config/doc/odds-config-summary-panel.tsx b/src/modules/config/doc/odds-config-summary-panel.tsx index d143fbd..6571b9b 100644 --- a/src/modules/config/doc/odds-config-summary-panel.tsx +++ b/src/modules/config/doc/odds-config-summary-panel.tsx @@ -92,7 +92,7 @@ export function OddsConfigSummaryPanel({ return (
    {prizeScopeLabel(scope, t)}
    -
    +
    {row ? oddsMultiplierLabel(row.odds_value) : "—"}
    @@ -101,7 +101,7 @@ export function OddsConfigSummaryPanel({ {playRebatePercent ? (
    {t("odds.rebateRate")}
    -
    {playRebatePercent}
    +
    {playRebatePercent}
    ) : null} diff --git a/src/modules/config/doc/play-config-doc-screen.tsx b/src/modules/config/doc/play-config-doc-screen.tsx index ab23403..d41423d 100644 --- a/src/modules/config/doc/play-config-doc-screen.tsx +++ b/src/modules/config/doc/play-config-doc-screen.tsx @@ -818,7 +818,7 @@ export function PlayConfigDocScreen() { ) : ( - + {formatAdminMinorDecimal(row.min_bet_amount, amountCurrencyCode)} )} @@ -840,7 +840,7 @@ export function PlayConfigDocScreen() { ) : ( - + {formatAdminMinorDecimal(row.max_bet_amount, amountCurrencyCode)} )} diff --git a/src/modules/config/doc/rebate-config-doc-screen.tsx b/src/modules/config/doc/rebate-config-doc-screen.tsx index 800f065..4f03dbe 100644 --- a/src/modules/config/doc/rebate-config-doc-screen.tsx +++ b/src/modules/config/doc/rebate-config-doc-screen.tsx @@ -553,14 +553,14 @@ export function RebateConfigDocScreen({ type="number" step="0.01" min={0} - className="font-mono tabular-nums" + className="h-9 text-base font-semibold" disabled={saving} value={p2} placeholder={t("rebate.placeholders.d2", { ns: "config" })} onChange={(e) => setP2(e.target.value)} /> ) : ( - {p2} + {p2} )}
    @@ -570,14 +570,14 @@ export function RebateConfigDocScreen({ type="number" step="0.01" min={0} - className="font-mono tabular-nums" + className="h-9 text-base font-semibold" disabled={saving} value={p3} placeholder={t("rebate.placeholders.d3", { ns: "config" })} onChange={(e) => setP3(e.target.value)} /> ) : ( - {p3} + {p3} )}
    @@ -587,14 +587,14 @@ export function RebateConfigDocScreen({ type="number" step="0.01" min={0} - className="font-mono tabular-nums" + className="h-9 text-base font-semibold" disabled={saving} value={p4} placeholder={t("rebate.placeholders.d4", { ns: "config" })} onChange={(e) => setP4(e.target.value)} /> ) : ( - {p4} + {p4} )}
    diff --git a/src/modules/config/doc/risk-cap-doc-screen.tsx b/src/modules/config/doc/risk-cap-doc-screen.tsx index ac99e59..fe7afb6 100644 --- a/src/modules/config/doc/risk-cap-doc-screen.tsx +++ b/src/modules/config/doc/risk-cap-doc-screen.tsx @@ -98,6 +98,10 @@ function defaultRiskRowFromAmount(amount: number): DraftRiskRow { }; } +function formatMinorToEditableMajor(minor: number, currencyCode: string): string { + return formatAdminMinorDecimal(minor, currencyCode).replace(/,/g, ""); +} + export function RiskCapDocScreen() { const { t } = useTranslation(["config", "adminUsers", "common"]); const tRef = useTranslationRef(["config", "common"]); @@ -161,7 +165,7 @@ export function RiskCapDocScreen() { setDefaultCapStr(""); return; } - setDefaultCapStr(formatAdminMinorDecimal(defaultRow.cap_amount, amountCurrencyCode)); + setDefaultCapStr(formatMinorToEditableMajor(defaultRow.cap_amount, amountCurrencyCode)); } const loadDetail = useCallback(async (id: number) => { @@ -381,7 +385,12 @@ export function RiskCapDocScreen() { () => specialRows.filter(({ row }) => row.draw_id != null), [specialRows], ); - const defaultCapDisplay = defaultCapStr || formatAdminMinorDecimal(0, amountCurrencyCode); + const defaultCapDisplay = detail + ? formatAdminMinorDecimal( + draftRows.find(isDefaultRiskRow)?.cap_amount ?? 0, + amountCurrencyCode, + ) + : formatAdminMinorDecimal(0, amountCurrencyCode); async function handleDeleteVersion(row: ConfigVersionSummary) { try { @@ -524,7 +533,7 @@ export function RiskCapDocScreen() { ].map((card) => (

    {card.label}

    -

    {card.value}

    +

    {card.value}

    {card.hint}

    ))} @@ -539,15 +548,15 @@ export function RiskCapDocScreen() { id="default-cap" type="text" inputMode="decimal" - className="w-[220px] font-mono tabular-nums" + className="h-9 w-[220px] text-base font-semibold" disabled={saving} value={defaultCapStr} placeholder={t("riskCap.placeholders.defaultCap", { ns: "config" })} onChange={(e) => setDefaultCapStr(e.target.value)} /> ) : ( - - {defaultCapStr || formatAdminMinorDecimal(0, amountCurrencyCode)} + + {defaultCapDisplay} )}
    @@ -679,9 +688,9 @@ export function RiskCapDocScreen() { updateRow(idx, { @@ -691,7 +700,7 @@ export function RiskCapDocScreen() { } /> ) : ( - + {formatAdminMinorDecimal(r.cap_amount, amountCurrencyCode)} )} diff --git a/src/modules/config/risk-cap-runtime-panel.tsx b/src/modules/config/risk-cap-runtime-panel.tsx index afc9981..4fa89d9 100644 --- a/src/modules/config/risk-cap-runtime-panel.tsx +++ b/src/modules/config/risk-cap-runtime-panel.tsx @@ -258,13 +258,13 @@ export function RiskCapRuntimePanel() { )} > {row.normalized_number} - + {formatAdminMinorUnits(row.locked_amount, currencyCode ?? undefined)} - + {formatAdminMinorUnits(row.remaining_amount, currencyCode ?? undefined)} - + {row.usage_ratio != null ? `${Math.round(row.usage_ratio * 100)}%` : "—"} diff --git a/src/modules/dashboard/agent-dashboard-console.tsx b/src/modules/dashboard/agent-dashboard-console.tsx index d52a696..5ecbef4 100644 --- a/src/modules/dashboard/agent-dashboard-console.tsx +++ b/src/modules/dashboard/agent-dashboard-console.tsx @@ -251,7 +251,6 @@ export function AgentDashboardConsole(): ReactElement {
    {t("agent.shareRate", { rate: overview.total_share_rate })} - {t("agent.settlementCycle", { cycle: overview.settlement_cycle })} {overview.latest_bet_at ? t("agent.latestBetAt", { time: formatDt(overview.latest_bet_at) }) diff --git a/src/modules/integration/integration-sites-console.tsx b/src/modules/integration/integration-sites-console.tsx index bb4661d..4712627 100644 --- a/src/modules/integration/integration-sites-console.tsx +++ b/src/modules/integration/integration-sites-console.tsx @@ -113,6 +113,10 @@ function MaskedValueWithCopy({ type FormState = { code: string; name: string; + admin_username: string; + admin_nickname: string; + admin_password: string; + admin_email: string; currency_code: string; status: number; wallet_api_url: string; @@ -128,6 +132,10 @@ type FormState = { const EMPTY_FORM: FormState = { code: "", name: "", + admin_username: "", + admin_nickname: "", + admin_password: "", + admin_email: "", currency_code: "NPR", status: 1, wallet_api_url: "", @@ -155,6 +163,10 @@ function rowToForm(row: AdminIntegrationSiteDetail): FormState { return { code: row.code, name: row.name, + admin_username: "", + admin_nickname: "", + admin_password: "", + admin_email: "", currency_code: row.currency_code, status: row.status, wallet_api_url: row.wallet_api_url ?? "", @@ -189,7 +201,16 @@ function formToPayload( }; if (includeCode) { - return { code: form.code.trim(), ...base }; + return { + code: form.code.trim(), + admin_account: { + username: form.admin_username.trim(), + nickname: form.admin_nickname.trim(), + password: form.admin_password, + email: form.admin_email.trim() || null, + }, + ...base, + }; } return base; @@ -303,11 +324,34 @@ export function IntegrationSitesConsole({ return; } + if (mode === "create" && form.admin_username.trim() === "") { + toast.error(t("integrationSites.form.adminUsernameRequired")); + return; + } + + if (mode === "create" && form.admin_nickname.trim() === "") { + toast.error(t("integrationSites.form.adminNicknameRequired")); + return; + } + + if (mode === "create" && form.admin_password.trim().length < 8) { + toast.error(t("integrationSites.form.adminPasswordRequired")); + return; + } + setSaving(true); try { if (mode === "create") { const created = await postAdminIntegrationSite(formToPayload(form, true)); toast.success(t("integrationSites.createSuccess", { code: created.code })); + if (created.admin_user?.username) { + toast.success( + t("integrationSites.adminAccountCreated", { + username: created.admin_user.username, + defaultValue: "已同时创建站点后台账号 {{username}}", + }), + ); + } showSecretsOnce(created); } else if (editingId !== null) { await putAdminIntegrationSite(editingId, formToPayload(form, false)); @@ -487,7 +531,6 @@ export function IntegrationSitesConsole({ {t("integrationSites.columns.status")} {t("integrationSites.columns.lineRoot")} {t("integrationSites.columns.walletUrl")} - {t("integrationSites.columns.h5Url")} {t("integrationSites.columns.ssoSecret")} {t("integrationSites.columns.walletApiKey")} {t("integrationSites.columns.actions")} @@ -541,9 +584,6 @@ export function IntegrationSitesConsole({ ) : null}
    - - {row.lottery_h5_base_url ?? "—"} - updateForm("name", e.target.value)} />
    + {mode === "create" ? ( + <> +
    +
    +

    + {t("integrationSites.adminAccountSectionTitle", { + defaultValue: "站点后台管理账号", + })} +

    +

    + {t("integrationSites.adminAccountSectionDescription", { + defaultValue: "创建站点时将同步创建一个绑定该站点的后台管理账号。", + })} +

    +
    +
    +
    +
    + + updateForm("admin_username", e.target.value)} + /> +
    +
    + + updateForm("admin_nickname", e.target.value)} + /> +
    +
    +
    +
    + + updateForm("admin_password", e.target.value)} + /> +
    +
    + + updateForm("admin_email", e.target.value)} + /> +
    +
    +
    +
    + + ) : null}
    @@ -673,15 +790,6 @@ export function IntegrationSitesConsole({ onChange={(e) => updateForm("wallet_api_url", e.target.value)} />
    -
    - - updateForm("lottery_h5_base_url", e.target.value)} - /> -