diff --git a/scripts/merge-en-translations.mjs b/scripts/merge-en-translations.mjs index c129276..67b5c7f 100644 --- a/scripts/merge-en-translations.mjs +++ b/scripts/merge-en-translations.mjs @@ -155,10 +155,10 @@ const translations = { "lineProvision.siteRequired": "Select an integration site", "lineProvision.noUnboundSite": "No sites without a level-1 agent", "lineProvision.openIntegrationSites": "Go to integration sites", - "lineProvision.passwordHint": "At least 8 characters", + "lineProvision.passwordHint": "At least 6 characters", "playersPanel.siteCode": "Line site", - "playersPanel.passwordHint": "At least 8 characters", - "playersPanel.passwordMinLength": "Initial password must be at least 8 characters", + "playersPanel.passwordHint": "At least 6 characters", + "playersPanel.passwordMinLength": "Initial password must be at least 6 characters", "playersPanel.creditLimitInvalid": "Credit limit must be an integer ≥ 0", "playersPanel.creditLimitExceeded": "Credit limit cannot exceed this agent’s available grant", "playersPanel.rebateRateInvalid": "Rebate rate must be between 0 and 100%", diff --git a/src/app/admin/(shell)/agents/list/page.tsx b/src/app/admin/(shell)/agents/list/page.tsx index b3882d2..36eeaa6 100644 --- a/src/app/admin/(shell)/agents/list/page.tsx +++ b/src/app/admin/(shell)/agents/list/page.tsx @@ -1,24 +1,5 @@ -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { AgentsDirectoryConsole } from "@/modules/agents/agents-directory-console"; -import { - PRD_AGENT_LINE_PROVISION_ACCESS_ANY, - PRD_AGENTS_ACCESS_ANY, -} from "@/lib/admin-prd"; -import { buildPageMetadata } from "@/lib/page-metadata"; - -export const metadata: Metadata = buildPageMetadata("agents", "listTitle"); - -export default function AgentsListPage() { - return ( - - - - - - ); -} +export default function AgentsListRedirectPage() { + redirect("/admin/agents?view=list"); +} \ No newline at end of file diff --git a/src/app/admin/(shell)/agents/page.tsx b/src/app/admin/(shell)/agents/page.tsx index 79499e5..e32ea4a 100644 --- a/src/app/admin/(shell)/agents/page.tsx +++ b/src/app/admin/(shell)/agents/page.tsx @@ -1,6 +1,6 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { AgentsConsole } from "@/modules/agents/agents-console"; +import { AgentsManagementScreen } from "@/modules/agents/agents-management-screen"; import { PRD_AGENT_LINE_PROVISION_ACCESS_ANY, PRD_AGENTS_ACCESS_ANY, @@ -16,8 +16,8 @@ export default function AgentsPage() { - + ); -} +} \ No newline at end of file diff --git a/src/app/admin/(shell)/audit-logs/page.tsx b/src/app/admin/(shell)/audit-logs/page.tsx index aad8014..a34582c 100644 --- a/src/app/admin/(shell)/audit-logs/page.tsx +++ b/src/app/admin/(shell)/audit-logs/page.tsx @@ -15,4 +15,4 @@ export default function AdminAuditLogsPage() { ); -} +} \ No newline at end of file diff --git a/src/app/admin/(shell)/draws/[drawId]/layout.tsx b/src/app/admin/(shell)/draws/[drawId]/layout.tsx index fd22817..cdf38de 100644 --- a/src/app/admin/(shell)/draws/[drawId]/layout.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/layout.tsx @@ -1,5 +1,5 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { DrawSubnav } from "@/modules/draws/draw-subnav"; +import { DrawDetailShell } from "@/modules/draws/draw-detail-shell"; export default async function AdminDrawSegmentLayout(props: { children: React.ReactNode; @@ -9,8 +9,7 @@ export default async function AdminDrawSegmentLayout(props: { return ( - - {props.children} + {props.children} ); } diff --git a/src/app/admin/(shell)/reports/[category]/page.tsx b/src/app/admin/(shell)/reports/[category]/page.tsx index aa3b7cc..7700755 100644 --- a/src/app/admin/(shell)/reports/[category]/page.tsx +++ b/src/app/admin/(shell)/reports/[category]/page.tsx @@ -1,17 +1,33 @@ -import { notFound, redirect } from "next/navigation"; -import { buildPageMetadata } from "@/lib/page-metadata"; +import { notFound } from "next/navigation"; +import { Suspense } from "react"; import type { Metadata } from "next"; +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { AdminLoadingState } from "@/components/admin/admin-loading-state"; +import { PRD_REPORTS_PAGE_ACCESS_ANY } from "@/lib/admin-prd"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import { ReportsConsole } from "@/modules/reports/reports-console"; +import type { ReportCategory } from "@/modules/reports/reports-definitions"; + export const metadata: Metadata = buildPageMetadata("reports", "title"); +const VALID_CATEGORIES = new Set(["profit", "wallet", "risk", "audit"]); + export default async function AdminReportsCategoryPage({ params, }: { params: Promise<{ category: string }>; }) { const { category } = await params; - if (!["profit", "wallet", "risk", "audit"].includes(category)) { + if (!VALID_CATEGORIES.has(category as ReportCategory)) { notFound(); } - redirect("/admin/reports"); -} + + return ( + + }> + + + + ); +} \ No newline at end of file diff --git a/src/app/admin/(shell)/reports/page.tsx b/src/app/admin/(shell)/reports/page.tsx index 32a6a26..9a1ff68 100644 --- a/src/app/admin/(shell)/reports/page.tsx +++ b/src/app/admin/(shell)/reports/page.tsx @@ -1,15 +1,5 @@ -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { AdminLoadingState } from "@/components/admin/admin-loading-state"; -import { PRD_REPORTS_VIEW_ACCESS_ANY } from "@/lib/admin-prd"; -import { ReportsConsole } from "@/modules/reports/reports-console"; -import { Suspense } from "react"; +import { redirect } from "next/navigation"; export default function AdminReportsPage() { - return ( - - }> - - - - ); -} + redirect("/admin/reports/profit"); +} \ No newline at end of file diff --git a/src/app/admin/(shell)/wallet/layout.tsx b/src/app/admin/(shell)/wallet/layout.tsx index 37ae7cd..c2420fa 100644 --- a/src/app/admin/(shell)/wallet/layout.tsx +++ b/src/app/admin/(shell)/wallet/layout.tsx @@ -1,14 +1,12 @@ import type { ReactNode } from "react"; -import { WalletScopeHint } from "@/modules/wallet/wallet-scope-hint"; import { WalletSubnav } from "@/modules/wallet/wallet-subnav"; export default function AdminWalletLayout({ children }: { children: ReactNode }) { return (
-
+
-
{children}
diff --git a/src/app/globals.css b/src/app/globals.css index 3ee2169..1f9e53e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -168,6 +168,24 @@ @apply overflow-x-auto rounded-2xl border border-border/80 bg-card shadow-sm; } + /* Flat table inside an existing card — same white surface, no nested panel or blue header tint */ + .admin-table-inset { + @apply overflow-x-auto rounded-xl border border-border/80; + } + + .admin-table-inset [data-slot="table-header"] { + @apply bg-card; + } + + .admin-table-inset [data-slot="table-head"][class*="sticky"], + .admin-table-inset [data-slot="table-cell"][class*="sticky"] { + background-color: var(--card); + } + + .admin-table-inset [data-slot="table-row"]:hover > [data-slot="table-cell"][class*="sticky"] { + @apply bg-muted/20; + } + /* Sticky columns need an opaque background so scrolled cells/headers do not show through */ [data-slot="table-head"][class*="sticky"] { @apply bg-muted; diff --git a/src/components/admin/admin-permission-package-selector.tsx b/src/components/admin/admin-permission-package-selector.tsx index ad449a9..77ec31a 100644 --- a/src/components/admin/admin-permission-package-selector.tsx +++ b/src/components/admin/admin-permission-package-selector.tsx @@ -1,20 +1,17 @@ "use client"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; -import { Checkbox } from "@/components/ui/checkbox"; -import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; import { cn } from "@/lib/utils"; -import { ADMIN_PERMISSION_PACKAGES } from "@/lib/admin-permission-packages"; +import { + applyProfileLevel, + permissionProfileForGroup, + resolveProfileLevel, + type PermissionAccessLevel, + type PermissionGroupProfile, +} from "@/lib/admin-permission-profiles"; import type { AdminPermissionCatalogData } from "@/types/api/admin-user"; type PackageSelectorProps = { @@ -22,10 +19,7 @@ type PackageSelectorProps = { selectedSlugs: string[]; onChange: (next: string[]) => void; resolveGroupLabel: (key: string, fallback: string) => string; - resolvePackageLabel: (key: string, fallback: string) => string; - selectableSlugs?: string[] | null; - helperText?: string; - summaryText?: string; + isSuperAdmin?: boolean; emptyText: string; heightClassName?: string; }; @@ -33,24 +27,8 @@ type PackageSelectorProps = { type RenderGroup = { key: string; label: string; - packages: Array<{ key: string; label: string; slugs: string[] }>; -}; - -const PACKAGE_LEVEL_ORDER: Record = { - view: 10, - node_view: 10, - role_view: 11, - user_view: 12, - review: 20, - export: 20, - manage: 30, - node_manage: 30, - role_manage: 31, - user_manage: 32, - config: 30, - control: 30, - reopen: 32, - special: 40, + profile: PermissionGroupProfile; + levels: Array<{ key: PermissionAccessLevel; label: string; slugs: string[] }>; }; export function AdminPermissionPackageSelector({ @@ -58,197 +36,101 @@ export function AdminPermissionPackageSelector({ selectedSlugs, onChange, resolveGroupLabel, - resolvePackageLabel, - selectableSlugs = null, - helperText, - summaryText, + isSuperAdmin = false, emptyText, heightClassName = "h-[52vh]", }: PackageSelectorProps): React.ReactElement { - const selectedSet = useMemo(() => new Set(selectedSlugs), [selectedSlugs]); + const { t } = useTranslation("adminUsers"); const catalogSlugSet = useMemo( () => new Set((catalog?.permissions ?? []).map((permission) => permission.slug)), [catalog], ); - const allowedSet = useMemo( - () => (selectableSlugs ? new Set(selectableSlugs) : null), - [selectableSlugs], - ); const groups = useMemo(() => { const defs = catalog?.permission_menu_groups ?? []; const out: RenderGroup[] = []; + for (const group of defs) { - const bundles = ADMIN_PERMISSION_PACKAGES[group.key] ?? []; - if (bundles.length === 0) { + const profile = permissionProfileForGroup(group.key); + if (profile === undefined) { continue; } - const renderedPackages = bundles - .map((bundle) => { - const slugs = bundle.slugs.filter((slug) => { - if (!catalogSlugSet.has(slug)) { - return false; - } - if (allowedSet && !allowedSet.has(slug)) { - return false; - } - return true; - }); - return { - key: bundle.key, - label: resolvePackageLabel(bundle.key, bundle.label), - slugs, - }; - }) - .filter((bundle) => bundle.slugs.length > 0); - if (renderedPackages.length === 0) { + if (profile.platformOnly && !isSuperAdmin) { continue; } + + const levels = profile.levels + .map((level) => ({ + key: level.key, + label: t(level.labelKey), + slugs: level.slugs.filter((slug) => catalogSlugSet.has(slug)), + })) + .filter((level) => level.key === "none" || level.slugs.length > 0); + + if (levels.length <= 1) { + continue; + } + out.push({ key: group.key, label: resolveGroupLabel(group.key, group.label), - packages: renderedPackages, + profile, + levels, }); } + return out; - }, [allowedSet, catalog, catalogSlugSet, resolveGroupLabel, resolvePackageLabel]); + }, [catalog, catalogSlugSet, isSuperAdmin, resolveGroupLabel, t]); - const bundleCount = useMemo( - () => groups.reduce((sum, group) => sum + group.packages.length, 0), - [groups], - ); - - if (groups.length === 0 || bundleCount === 0) { + if (groups.length === 0) { return ( -
- +
+
); } - const toggleBundle = (group: RenderGroup, bundleKey: string, slugs: string[], checked: boolean) => { - const next = new Set(selectedSet); - const currentLevel = PACKAGE_LEVEL_ORDER[bundleKey] ?? 10; - - if (checked) { - const implied = group.packages - .filter((item) => (PACKAGE_LEVEL_ORDER[item.key] ?? 10) <= currentLevel) - .flatMap((item) => item.slugs); - for (const slug of implied.length > 0 ? implied : slugs) { - next.add(slug); - } - for (const item of group.packages) { - if ((PACKAGE_LEVEL_ORDER[item.key] ?? 10) > currentLevel) { - for (const slug of item.slugs) { - next.delete(slug); - } - } - } - } else { - for (const slug of slugs) { - next.delete(slug); - } - if (bundleKey === "manage" || bundleKey === "reopen") { - for (const item of group.packages) { - if ((PACKAGE_LEVEL_ORDER[item.key] ?? 10) > currentLevel) { - for (const slug of item.slugs) { - next.delete(slug); - } - } - } - } - } - - onChange(Array.from(next).sort()); - }; - - const toggleGroup = (group: RenderGroup, checked: boolean) => { - const next = new Set(selectedSet); - if (checked) { - const viewBundle = - group.packages.find((item) => item.key === "view") ?? group.packages[0]; - for (const slug of viewBundle?.slugs ?? []) { - next.add(slug); - } - } else { - for (const slug of group.packages.flatMap((item) => item.slugs)) { - next.delete(slug); - } - } - onChange(Array.from(next).sort()); + const setGroupLevel = (group: RenderGroup, levelKey: PermissionAccessLevel) => { + onChange(applyProfileLevel(group.profile, levelKey, selectedSlugs)); }; return ( -
- {helperText || summaryText ? ( -
- {helperText} - {summaryText} -
- ) : null} -
- - - - - - - - - {groups.map((group) => { - const groupAllSlugs = group.packages.flatMap((p) => p.slugs); - const groupSelectedCount = groupAllSlugs.filter((slug) => selectedSet.has(slug)).length; - const groupChecked = groupSelectedCount === groupAllSlugs.length && groupAllSlugs.length > 0; +
+
    + {groups.map((group) => { + const activeLevel = resolveProfileLevel(group.profile, selectedSlugs); - return ( -
- - - - ); - })} - -
模块具体权限
- -
- {group.packages.map((bundle) => { - const checked = bundle.slugs.every((slug) => selectedSet.has(slug)); - return ( - - ); - })} -
-
-
+ {level.label} + + ); + })} +
+ + ); + })} +
); -} +} \ No newline at end of file diff --git a/src/components/admin/login-form.tsx b/src/components/admin/login-form.tsx index 0ff51bb..7b98a1f 100644 --- a/src/components/admin/login-form.tsx +++ b/src/components/admin/login-form.tsx @@ -118,7 +118,7 @@ export function LoginForm() { const passwordIssue = validateAdminPassword(password); if (passwordIssue === "too_short") { toast.error( - t("passwordMinLength", { defaultValue: "密码至少需要 8 个字符" }), + t("passwordMinLength", { defaultValue: "密码至少需要 6 个字符" }), ); return; } diff --git a/src/i18n/locales/en/adminUsers.json b/src/i18n/locales/en/adminUsers.json index 4d22e61..cbdbcb3 100644 --- a/src/i18n/locales/en/adminUsers.json +++ b/src/i18n/locales/en/adminUsers.json @@ -11,7 +11,7 @@ "noRoles": "No roles available yet. Wait for the list to finish loading and try again.", "password": "Password", "passwordOptional": "Password (optional)", - "passwordPlaceholderCreate": "At least 8 characters", + "passwordPlaceholderCreate": "at least 6 characters", "passwordPlaceholderEdit": "Leave empty to keep unchanged", "rolesDescription": "After creation, adjust per-site role bindings in Assign Roles.", "rolesRequired": "Roles (at least one)", @@ -56,9 +56,9 @@ "listTitle": "Admin user list", "loadFailed": "Failed to load admin list", "modelGuide": "Accounts bind roles only. Maintain functional permissions in Role Management.", - "newPasswordMin": "New password must be at least 8 characters", + "newPasswordMin": "New password must be at least 6 characters", "nicknameRequired": "Enter a nickname", - "passwordMin": "Password must be at least 8 characters", + "passwordMin": "Password must be at least 6 characters", "permissionDialog": { "rolePermissionCount": "Contains {{count}} functional permissions", "rolesDescription": "Admins only bind roles here. Maintain detailed permissions in Role Management. Save roles per site.", @@ -179,9 +179,18 @@ "roleListTitle": "Role Management", "roleLoadFailed": "Failed to load role list", "rolePermissionDialog": { - "packageHint": "Checking a module row on the left grants View only. Select Manage separately for data entry, close, draw, and other admin actions.", - "title": "Role Permissions" + "title": "Role permissions", + "titleWithName": "{{name}}" }, + "permissionAccess": { + "none": "No access", + "view": "View only", + "review": "Can review", + "export": "Can export", + "manage": "Can manage", + "special": "Includes privileged actions" + }, + "rolePermissionSaveFailed": "Failed to save role permissions", "rolePermissionSaveSuccess": "Role permissions updated", "roleRequired": "Select at least one role", @@ -190,6 +199,7 @@ "actions": "Actions", "name": "Role", "permissions": "Permission count", + "enabledAreas": "Enabled modules", "slug": "Role Code", "status": "Status", "type": "Type", diff --git a/src/i18n/locales/en/agents.json b/src/i18n/locales/en/agents.json index 3765d5c..1753218 100644 --- a/src/i18n/locales/en/agents.json +++ b/src/i18n/locales/en/agents.json @@ -56,7 +56,7 @@ "noUnboundSite": "No sites without a level-1 agent", "openIntegrationSites": "Go to integration sites", "password": "Initial password", - "passwordHint": "At least 8 characters", + "passwordHint": "at least 6 characters", "secretsOnce": "Integration secrets are shown once — save them now", "siteCode": "Integration site", "siteCodePlaceholder": "Select site", @@ -82,7 +82,8 @@ "downlineEmptyShort": "No direct downline yet.", "downlineEmptyTitle": "No direct downline yet", "editAccount": "Account & status", - "editAgent": "Edit agent", + "editAgent": "Edit", + "parentMeta": "Parent {{name}}", "editCurrent": "Edit this agent", "expand": "Expand", "kicker": "Credit share · Agent tree", @@ -112,6 +113,8 @@ "tabDownline": "Downline", "tabOverview": "Overview", "tabPlayers": "Players", + "tabProfileShort": "Share & credit", + "openInTree": "Open in tree", "createDirectPlayer": "Create direct player", "createDownline": "Create child agent", "editDownline": "Edit agent", @@ -125,6 +128,11 @@ }, "listFlatHint": "All operating agents in a flat list. Use row actions to add a child under a specific agent.", "listSearch": "Search name / code / login", + "viewModes": { + "label": "View", + "tree": "Tree", + "list": "List" + }, "listTitle": "Agents", "loadFailed": "Failed to load agent tree", "modelGuide": "Agent layer controls data scope and delegation ceiling. Account permissions are assigned through roles.", @@ -134,9 +142,9 @@ "noAccess": "You do not have permission to manage agents. Contact an administrator.", "pageGuide": "Manage the agent tree, agent roles, agent accounts, and delegation ceilings here. Platform accounts and platform roles stay in platform governance pages.", "parentAgent": "Parent", - "passwordMinLength": "Password must be at least 8 characters", - "passwordOptionalHint": "Leave empty to keep unchanged, or enter an 8-character password", - "passwordPlaceholder": "Enter an 8-character password", + "passwordMinLength": "Password must be at least 6 characters", + "passwordOptionalHint": "Leave empty to keep unchanged, or enter a 6-character password", + "passwordPlaceholder": "Enter a 6-character password", "passwordRequired": "Password is required", "path": "Path", "playersPanel": { @@ -157,8 +165,8 @@ "initialPassword": "Initial password", "loginRequired": "Enter login username and initial password", "loginUsername": "Login username", - "passwordHint": "At least 8 characters", - "passwordMinLength": "Initial password must be at least 8 characters", + "passwordHint": "at least 6 characters", + "passwordMinLength": "Initial password must be at least 6 characters", "playerRef": "Player ref", "rebateInherited": "Inherit agent default rebate", "rebateRate": "Rebate rate (%)", @@ -168,7 +176,11 @@ "riskTagsPlaceholder": "Comma-separated", "scopedTo": "Direct players: {{agent}}", "siteCode": "Line site", - "usernameNickname": "Username / nickname" + "usernameNickname": "Username / nickname", + "searchPh": "Search player account / ID", + "manageSettlement": "Process bill", + "noPendingBillsGoCenter": "No pending bills for this player. Opened settlement center.", + "billingLoadFailed": "Failed to load bills" }, "profile": { "availableCredit": "Available to grant {{amount}}", diff --git a/src/i18n/locales/en/audit.json b/src/i18n/locales/en/audit.json index f80f7ef..f9d8e90 100644 --- a/src/i18n/locales/en/audit.json +++ b/src/i18n/locales/en/audit.json @@ -1,20 +1,37 @@ { - "title": "Audit Logs", - "moduleCode": "Module code", - "actionCode": "Action code", - "operatorType": "Operator type", - "operatorIdPlaceholder": "Enter operator ID", - "exactMatch": "Exact match", - "operatorTypePlaceholder": "For example admin / system", - "operator": "Operator", - "module": "Module", - "action": "Action", - "target": "Target", + "title": "Activity Log", + "filterModule": "Business area", + "filterModuleAll": "All areas", + "filterOperatorType": "Actor type", + "filterOperatorTypeAll": "All", + "operatorIdPlaceholder": "Optional operator ID", + "operator": "Actor", + "summary": "What happened", + "target": "Related to", "time": "Time", - "empty": "No data", + "empty": "No records", + "moduleOptions": { + "agent": "Agents", + "system": "System admin", + "settings": "Settings", + "integration": "Integration sites", + "player_manage": "Player management", + "player_service": "Player service", + "risk_cap": "Risk caps", + "odds": "Odds", + "play_config": "Play config", + "settlement": "Settlement", + "report_jobs": "Report export", + "reconcile_jobs": "Reconcile jobs", + "draw": "Draws", + "wallet": "Wallet", + "reconcile": "Reconcile", + "jackpot": "Jackpot", + "dashboard": "Dashboard" + }, "operatorTypes": { - "admin": "Admin", + "admin": "Admin user", "player": "Player", "system": "System" } -} +} \ No newline at end of file diff --git a/src/i18n/locales/en/auth.json b/src/i18n/locales/en/auth.json index 0e9c8b9..04c327f 100644 --- a/src/i18n/locales/en/auth.json +++ b/src/i18n/locales/en/auth.json @@ -19,6 +19,7 @@ "captchaLoadFailed": "Failed to load captcha. Check the API or network.", "apiBaseMissingToast": "API proxy is not enabled: check LOTTERY_API_UPSTREAM points to Laravel", "captchaRequired": "Refresh the captcha first", + "passwordMinLength": "Password must be at least 6 characters", "welcome": "Welcome, {{name}}", "networkFailed": "Network request failed", "loginFailed": "Login failed" diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 0415ee2..55ebb33 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -51,7 +51,8 @@ }, "validation": { "required": "{{field}} is required", - "passwordMismatch": "Passwords do not match" + "passwordMismatch": "Passwords do not match", + "passwordMinLength": "Password must be at least 6 characters" }, "aria": { "expand": "Expand", @@ -156,7 +157,7 @@ "nav": { "home": "Home", "dashboard": "Dashboard", - "admin_users": "Admin Users", + "admin_users": "Platform accounts", "admin_roles": "Role Management", "players": "Players", "currencies": "Currencies", @@ -171,21 +172,20 @@ "settlement": "Settlement", "jackpot": "Jackpot", "reconcile": "Reconcile", - "tickets": "Ticket list", + "tickets": "Tickets", "audit": "Audit Logs", "settings": "Settings", "account": "Account settings", "integration": "Integration", - "agents": "Agent lines", - "agent_list": "Agent list", - "settlement_center": "Settlement center", + "agents": "Agent management", + "settlement_center": "Credit settlement", "config": "Operations config" }, "sidebar": { "workspace": "Workspace", "group": { "overview": "Overview", - "agent": "Agent organization", + "agent": "Agents", "operations": "Operations", "finance": "Finance & reports", "rules": "Rules & parameters", diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index e92ceca..6db3f7a 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -133,7 +133,7 @@ }, "form": { "adminNicknameRequired": "Site admin nickname is required", - "adminPasswordRequired": "Initial site admin password must be at least 8 characters", + "adminPasswordRequired": "Initial site admin password must be at least 6 characters", "adminUsernameRequired": "Site admin username is required", "codeRequired": "site_code is required", "required": "Site name is required" @@ -147,7 +147,7 @@ "placeholders": { "adminEmail": "Enter email", "adminNickname": "Enter account nickname", - "adminPassword": "At least 8 characters", + "adminPassword": "at least 6 characters", "adminUsername": "Enter admin username", "code": "Enter site identifier, for example partner-a", "connectivityPlayerId": "Enter player ID, for example 10001", @@ -527,10 +527,12 @@ } }, "system": { + "pageTitle": "System settings", + "confirmSaveCurrencyFormatDescription": "This updates decimal places and separators.", "confirmSaveCurrencyFormatTitle": "Save currency display format?", "confirmSaveDescription": "This updates draw review, cooldown, auto settlement/approval/payout, and play-rules display. It may affect site-wide operation.", - "confirmSaveDrawDescription": "This updates draw review, schedule timing, and cooldown in this block only.", + "confirmSaveDrawDescription": "This updates manual draw review and cooldown duration only.", "confirmSaveDrawTitle": "Save draw parameters?", "confirmSaveFrontendDescription": "This updates play-rules HTML on the player site. Draw and settlement logic are not changed.", "confirmSaveFrontendTitle": "Save front-end display settings?", @@ -591,12 +593,33 @@ "saveSettlementSuccess": "Settlement automation saved", "saveSuccess": "System settings saved", "sections": { + "deployment": "Deployment (read-only)", + "deploymentDescription": "Default currency and draw schedule are set in server .env; this block shows effective values.", "currencyFormat": "Currency display format", "currencyFormatDescription": "Decimals and separators for amounts across the site (separate from currency master data).", - "draw": "Draw schedule and review", - "drawDescription": "Controls draw timing, close window, manual review, and cooldown. Only changed fields in this block are submitted.", + "draw": "Draw review", + "drawDescription": "Manual review after RNG and post-publish cooldown.", "settlement": "Settlement automation", - "settlementDescription": "Controls whether tick auto-runs settlement, approval, and payout. Only changed fields in this block are submitted." + "settlementDescription": "In order: auto settlement → auto approve → auto payout. When off, handle manually in the relevant module." + }, + "deployment": { + "footer": "Restart PHP or refresh config cache after changes. Contact ops if unsure.", + "fields": { + "default_currency": "Default currency", + "draw_timezone": "Draw timezone", + "draw_interval_minutes": "Draw interval (minutes)", + "draw_betting_window_seconds": "Betting window (seconds)", + "draw_close_before_draw_seconds": "Close before draw (seconds)", + "draw_buffer_draws_ahead": "Pre-generated future draws" + }, + "hints": { + "default_currency": "Default for new players when currency is unspecified", + "draw_timezone": "Timezone for draw schedule and display", + "draw_interval_minutes": "Minutes between consecutive draw times", + "draw_betting_window_seconds": "How long betting stays open each draw", + "draw_close_before_draw_seconds": "Seconds before draw time to stop accepting bets", + "draw_buffer_draws_ahead": "How many future draws the scheduler pre-creates" + } }, "states": { "disabled": "Disabled", @@ -630,19 +653,17 @@ "count": "{{count}} items", "current": "Current", "delete": "Delete", - "deleteConfirmDescription": "Version ID {{id}} (version_no {{version}}) will be permanently deleted. Active versions cannot be deleted.", + "deleteConfirmDescription": "Version v{{version}} will be deleted. This cannot be undone.", "deleteConfirmTitle": "Delete this version?", "effectiveAt": "Effective at: {{value}}", "empty": "No version records yet.", "loading": "Loading…", "moreActions": "More actions for v{{version}}", - "noneSelected": "No version selected", + "noneSelected": "Select version", "note": "Note: {{value}}", "rollback": "Rollback", "selected": "Selected", - "sheetDescription": "Choose a version to view on this page. Drafts are editable, while active and archived versions are read-only.", - "sheetTitle": "Switch configuration version", - "switch": "Switch version", + "sheetTitle": "Versions", "view": "View" }, "versionToolbar": { diff --git a/src/i18n/locales/en/draws.json b/src/i18n/locales/en/draws.json index a794ed7..dcca677 100644 --- a/src/i18n/locales/en/draws.json +++ b/src/i18n/locales/en/draws.json @@ -70,8 +70,8 @@ "batchSummaryPending": "{{count}} pending", "batchSummaryPublished": "{{count}} published", "noResultBatchesYet": "No result batches yet.", - "reviewQueueHint": "After results are generated, continue in Review & publish.", - "goToReviewTab": "Review & publish", + "reviewQueueHint": "", + "goToReviewTab": "Publish", "businessDate": "Business date", "sequenceNo": "Sequence no.", "plannedDraw": "Planned draw", @@ -96,12 +96,12 @@ "published": "Published", "viewFinance": "View draw finance", "drawActions": "Draw actions", - "drawActionsDesc": "Manual close / cancel / RNG / reopen / settlement all call backend APIs directly.", + "drawActionsDesc": "", "manualClose": "Manual close", "cancelDraw": "Cancel draw", "cancelBeforeDraw": "Cancel before draw", "rngDraw": "RNG draw", - "rngAutoGenerate": "RNG auto generate", + "rngAutoGenerate": "Auto draw", "reopen": "Reopen", "cooldownReopen": "Reopen in cooldown", "runSettlement": "Run settlement", @@ -114,8 +114,8 @@ "actualBet": "Actual bet deducted", "currentPayout": "Current payout total", "grossProfit": "Approx. gross profit", - "settlementBatchList": "Settlement batch list (filter by draw)", - "relatedSettlementBatches": "Related settlement batches", + "settlementBatchList": "Settlement records", + "relatedSettlementBatches": "Settlement batches", "noSettlementBatches": "No settlement batch records.", "ticketCount": "Tickets", "winCount": "Wins", @@ -124,7 +124,7 @@ "resultsTitle": "Results", "reviewAndPublish": "Review / publish", "viewReviewQueue": "View review queue", - "noPublishedBatch": "No published batches.", + "noPublishedBatch": "None", "version": "Version v{{version}}", "sourceType": "Source {{source}}", "manualEntry": "Manual", @@ -135,28 +135,28 @@ "tail3": "Last 3", "tail2": "Last 2", "headTail": "Head/Tail", - "manualResultEntry": "Manual result entry", - "currentStatusAndDraft": "Current status {{status}}. Saving creates a pending batch and does not publish it.", - "currentStatusLabel": "Current status", - "currentStatusDraftHint": "Saving creates a pending batch and does not publish it.", + "manualResultEntry": "Enter numbers", + "currentStatusAndDraft": "", + "currentStatusLabel": "Status", + "currentStatusDraftHint": "", "hallPreviewStatusLabel": "Hall preview", "enter23Numbers": "Please enter all 23 groups of 4 digits", - "draftSaved": "Draft v{{version}} saved, waiting to be published", + "draftSaved": "Saved v{{version}}", "saveFailed": "Failed to save", "fillRandomNumbers": "Fill randomly", "clear": "Clear", "saveDraft": "Save draft", "saving": "Saving…", - "pendingBatches": "Pending batches", - "noPendingBatches": "There are no batches pending review.", - "batchId": "Batch ID", - "numberCount": "Number count", - "reviewAndPublishAction": "Review and publish", - "discardPendingBatch": "Delete draft", + "pendingBatches": "Pending publish", + "noPendingBatches": "None", + "batchId": "Batch", + "numberCount": "Numbers", + "reviewAndPublishAction": "Publish", + "discardPendingBatch": "Delete", "discardingPendingBatch": "Deleting…", "discardPendingBatchSuccess": "Pending batch removed. You can re-enter numbers or run RNG.", "discardPendingBatchFailed": "Delete failed", - "publishReadOnlyHint": "This page is read-only for verification. To change numbers, delete this batch and save a new draft under manual entry.", + "publishReadOnlyHint": "", "noPublishPermission": "No publish permission", "batchNotFound": "Batch not found", "batchNotFoundDesc": "Return to the review list and confirm the batch ID.", @@ -164,8 +164,8 @@ "publishTitle": "Publish", "cannotPublish": "Cannot publish", "cannotPublishDesc": "Current batch status is '{{status}}'.", - "checkBeforePublish": "Check the numbers before publishing", - "checkBeforePublishDesc": "Publish only after confirming the numbers.", + "checkBeforePublish": "", + "checkBeforePublishDesc": "", "publishedView": "View published result", "confirmPublish": "Confirm publish", "submitting": "Submitting…", @@ -173,10 +173,10 @@ "publishFailed": "Publish failed", "sourceTypeFull": "Source: {{source}} · Items: {{count}}/23 · RNG hash: {{hash}}", "subnav": { - "status": "Draw status", + "status": "Overview", "results": "Results", - "finance": "Draw finance", - "review": "Review & publish", + "finance": "Finance", + "review": "Publish", "riskOccupancy": "Risk occupancy", "riskLockLogs": "Lock logs", "riskHot": "Hot numbers", diff --git a/src/i18n/locales/en/reconcile.json b/src/i18n/locales/en/reconcile.json index bef2d2b..0e3738b 100644 --- a/src/i18n/locales/en/reconcile.json +++ b/src/i18n/locales/en/reconcile.json @@ -1,7 +1,10 @@ { "title": "Reconcile", - "createTitle": "Run reconcile scan", - "createHint": "Scans transfer orders in the selected period, compares lottery wallet ledgers, and checks main-site idempotent records when the wallet API is configured.", + "workflowHint": "Run dated reconcile scans here. For live unresolved exceptions, use the link above (opens Wallet → Transfer orders with abnormal-only filter).", + "shortcutAbnormalTransfers": "Live abnormal transfers", + "viewOnlyHint": "View-only: historical reconcile jobs.", + "createTitle": "Run scan", + "createHint": "", "reconcileType": "Reconcile type", "reconcileTypeFixed": "Wallet transfer (main site ⇄ lottery)", "dateRange": "Reconcile date range", @@ -12,13 +15,18 @@ "periodRequired": "Enter both reconcile start and end dates", "periodOrderInvalid": "End time must be later than or equal to start time", "confirmCreateTitle": "Start reconcile scan?", - "confirmCreateDescription": "Scan transfer orders in the selected date range{{playerHint}} and generate discrepancy items.", + "confirmCreateDescription": "Scan transfer orders in the selected period{{playerHint}}.", "confirmCreatePlayer": " for the selected player", "confirmCreateAllPlayers": " for all players", "createSuccess": "Scan finished: {{count}} issue(s) found", "createSuccessEmpty": "Scan finished: no issues found", "createFailed": "Scan failed", "noCreatePermission": "Current account cannot start reconcile scans.", + "filterOpen": "Open only", + "filterAll": "All items", + "filterCount": "{{shown}} on page / {{total}} total", + "filterOpenEmpty": "No open items on this page. Try All items or another page.", + "playerEmpty": "No matching players", "jobsTitle": "Reconcile jobs", "refresh": "Refresh", "jobNo": "Job no.", @@ -31,13 +39,22 @@ "finishedAt": "Finished at", "createdAt": "Created at", "operate": "Action", - "viewDetails": "View discrepancy details", + "viewDetails": "View details", + "hideDetails": "Hide", + "closeDetails": "Close", "detailsTitle": "Discrepancy details", + "detailsEmpty": "No items", + "itemStatusColumn": "Status", + "itemIssueColumn": "Issue type", + "itemHandlingColumn": "Handling", + "itemScanFindingColumn": "Scan finding", + "itemCurrentStatusColumn": "Current status", + "itemScanFindingResolvedHint": "Recorded at scan; now handled", "transferNo": "Transfer no.", "walletTxnNo": "Lottery wallet txn", "mainSiteRef": "Main-site ref", "mainSiteCheck": "Main-site check", - "differenceAmount": "Difference (minor)", + "differenceAmount": "Difference", "itemResult": "Check result", "processingStatus": "Processing status", "actions": "Actions", diff --git a/src/i18n/locales/en/reports.json b/src/i18n/locales/en/reports.json index 6e27372..5148574 100644 --- a/src/i18n/locales/en/reports.json +++ b/src/i18n/locales/en/reports.json @@ -1,6 +1,12 @@ { "title": "Reports", - "subtitle": "Centralized operational, finance, risk, and audit reports with unified export filters.", + "subtitle": "", + "profitScopeHint": "Ticket-level totals; not credit settlement periods", + "shortcuts": { + "wallet": "Transfer orders", + "risk": "Risk center", + "audit": "Audit logs" + }, "pageGuide": "P&L and risk by draw, player, and play type; export permission enables async jobs.", "exportPanel": "Export setup", "chooseReport": "Choose a report to export", @@ -86,8 +92,12 @@ }, "preview": { "title": "Preview", - "subtitle": "Results appear below. Export as CSV or Excel.", - "empty": "No data. Adjust filters and try again.", + "subtitle": "", + "empty": "No data", + "sections": { + "settlementBatches": "Settlement batches", + "lockLogs": "Lock logs" + }, "exportableRows": "rows exportable", "summaryScopeHint": "Except for the total record count, the stat cards above summarize the current preview page. Use full CSV/Excel export for full-range numbers.", "scope": { diff --git a/src/i18n/locales/en/settlementCenter.json b/src/i18n/locales/en/settlementCenter.json index b8e395e..c5e160d 100644 --- a/src/i18n/locales/en/settlementCenter.json +++ b/src/i18n/locales/en/settlementCenter.json @@ -38,6 +38,7 @@ "unsettledTickets": "Unsettled tickets", "openReportHint": "Open period: share/win-loss from in-period ledger; bill count updates after close.", "viewDetail": "View details", + "processBills": "Process bills", "close": "Close", "closeNow": "Close now", "hasOpen": "Period {{range}} is open. Close it before opening a new one.", @@ -66,6 +67,7 @@ "periods": "Periods", "bills": "Bills", "operations": "Payments & adjustments", + "aria": "Period views", "ledger": "Account ledger", "creditLedger": "Credit ledger", "playerBills": "Player bills", @@ -213,7 +215,8 @@ "actions": { "detail": "Detail", "viewBill": "View bill", - "billDetail": "Bill detail" + "billDetail": "Bill detail", + "billDetailWithId": "Bill #{{id}}" }, "billDisplay": { "settlementFlow": "Who pays whom", @@ -250,6 +253,7 @@ "unpaidAwaitingPayment": "Record offline payment", "fullySettled": "Fully settled this period", "confirmHint": "Confirm the bill before recording payment.", + "advancedActions": "Adjust / bad debt", "recordReceiptFrom": "Record receipt ({{payer}} → {{payee}})", "recordPayoutTo": "Record payout ({{payer}} → {{payee}})", "rebateAllocationsHint": "How rebate is allocated across agent tiers.", @@ -282,6 +286,12 @@ }, "billsPanel": { "intro": "Share bills after period close. Filter by type or status; open detail to confirm or record payment.", + "confirmOneTitle": "Confirm bill #{{id}}?", + "confirmOneDesc": "After confirm, record offline payment.", + "confirmOneBtn": "Confirm", + "confirmedOne": "Confirmed", + "confirmFailed": "Confirm failed", + "payBtn": "Pay", "hierarchyHint": "One period creates multiple bills: players pay their agent first; each agent keeps share profit and remits the rest upline. Gross win/loss may match across rows while settlement amounts step down.", "quickFilter": { "title": "Which settlement layer do you want to review", diff --git a/src/i18n/locales/en/wallet.json b/src/i18n/locales/en/wallet.json index c7b37a3..ac97a04 100644 --- a/src/i18n/locales/en/wallet.json +++ b/src/i18n/locales/en/wallet.json @@ -3,9 +3,7 @@ "subnavLabel": "Wallet sub pages", "subnavTransactions": "Main-site wallet txns", "subnavTransferOrders": "Main-site transfers", - "scopeHint": "This area is for main-site wallet mode (wallet txns and transfers). For credit-line period settlement, see", - "scopeHintSettlementLink": "Settlement center", - "scopeHintSettlement": "Settlement center", + "ledgerChannel": "Ledger", "ledgerCredit": "Credit ledger", "ledgerWallet": "Wallet txn", @@ -43,6 +41,9 @@ "options": "Options", "abnormalOnly": "Abnormal only", "abnormalOnlyPending": "Abnormal only (pending reconcile)", + "abnormalFilterActive": "Showing transfer orders flagged by reconciliation (not wallet transactions). Uncheck “Abnormal only” below to see all.", + "txnAbnormalFilterActive": "Showing only pending-reconcile transactions. Status filter is ignored while this is on (same as abnormal=1).", + "txnDeepLinkActive": "Filters were applied from the link. Reset clears URL parameters.", "search": "Search", "resetFilters": "Reset filters", "refreshCurrentPage": "Refresh current page", diff --git a/src/i18n/locales/ne/adminUsers.json b/src/i18n/locales/ne/adminUsers.json index fcb8cbb..6ca45e9 100644 --- a/src/i18n/locales/ne/adminUsers.json +++ b/src/i18n/locales/ne/adminUsers.json @@ -11,7 +11,7 @@ "noRoles": "अहिले भूमिका डाटा छैन। सूची लोड भएपछि फेरि प्रयास गर्नुहोस्।", "password": "पासवर्ड", "passwordOptional": "पासवर्ड (वैकल्पिक)", - "passwordPlaceholderCreate": "कम्तीमा 8 वर्ण", + "passwordPlaceholderCreate": "कम्तीमा 6 वर्ण", "passwordPlaceholderEdit": "परिवर्तन नगर्न खाली छोड्नुहोस्", "rolesDescription": "सिर्जना भएपछि \"भूमिका तोक्नुहोस्\" मा गएर भूमिका बाइन्डिङ थप समायोजन गर्न सकिन्छ।", "rolesRequired": "भूमिका (पूर्वनिर्धारित साइट, कम्तीमा एक)", @@ -46,9 +46,9 @@ "listTitle": "प्रशासक सूची", "loadFailed": "प्रशासक सूची लोड असफल भयो", "modelGuide": "खाता तहमा भूमिका मात्र बाँधिन्छ; कार्य अनुमति भूमिका व्यवस्थापनमा मिलाउनुहोस्।", - "newPasswordMin": "नयाँ पासवर्ड कम्तीमा 8 वर्ण हुनुपर्छ", + "newPasswordMin": "नयाँ पासवर्ड कम्तीमा 6 वर्ण हुनुपर्छ", "nicknameRequired": "उपनाम लेख्नुहोस्", - "passwordMin": "पासवर्ड कम्तीमा 8 वर्ण हुनुपर्छ", + "passwordMin": "पासवर्ड कम्तीमा 6 वर्ण हुनुपर्छ", "permissionDialog": { "rolePermissionCount": "{{count}} वटा कार्य अनुमति समावेश", "rolesDescription": "यहाँ प्रशासकलाई भूमिका मात्र जोडिन्छ। विस्तृत अनुमति भूमिका व्यवस्थापनमा मिलाउनुहोस्।", @@ -168,8 +168,18 @@ "roleListTitle": "भूमिका व्यवस्थापन", "roleLoadFailed": "भूमिका सूची लोड असफल भयो", "rolePermissionDialog": { - "title": "भूमिका अनुमति" + "title": "भूमिका अनुमति", + "titleWithName": "{{name}}" }, + "permissionAccess": { + "none": "चाहिँदैन", + "view": "हेर्न मात्र", + "review": "समीक्षा", + "export": "निर्यात", + "manage": "व्यवस्थापन", + "special": "विशेष अधिकार" + }, + "rolePermissionSaveFailed": "भूमिका अनुमति सुरक्षित गर्न असफल", "rolePermissionSaveSuccess": "भूमिका अनुमति अद्यावधिक भयो", "roleRequired": "कम्तीमा एउटा भूमिका छान्नुहोस्", @@ -178,7 +188,8 @@ "actions": "कार्य", "name": "भूमिका", "permissions": "अनुमति संख्या", - "slug": "角色 कोड", + "enabledAreas": "खुला मोड्युल", + "slug": "भूमिका कोड", "status": "स्थिति", "type": "प्रकार", "users": "सम्बन्धित प्रयोगकर्ता" diff --git a/src/i18n/locales/ne/agents.json b/src/i18n/locales/ne/agents.json index cc67169..d1daa59 100644 --- a/src/i18n/locales/ne/agents.json +++ b/src/i18n/locales/ne/agents.json @@ -100,6 +100,11 @@ }, "listFlatHint": "सबै सञ्चालन एजेन्ट सूचीमा; अधीनस्थ थप्न पङ्क्ति मेनु प्रयोग गर्नुहोस्।", "listSearch": "नाम / कोड / लगइन खोज्नुहोस्", + "viewModes": { + "label": "दृश्य", + "tree": "रूख", + "list": "सूची" + }, "listTitle": "एजेन्ट सूची", "loadFailed": "Failed to load agent tree", "modelGuide": "एजेन्ट तहले डाटा स्कोप र delegation ceiling नियन्त्रण गर्छ; खाताको अनुमति भूमिका मार्फत बाँडिन्छ।", @@ -109,9 +114,9 @@ "noAccess": "एजेन्ट सञ्चालन अनुमति छैन। प्रशासकलाई सम्पर्क गर्नुहोस्।", "pageGuide": "यहाँ एजेन्ट ट्री, एजेन्ट भूमिका, एजेन्ट खाता र delegation ceiling व्यवस्थापन गरिन्छ। प्लेटफर्म खाता र प्लेटफर्म भूमिका अलग पृष्ठमा राखिन्छ।", "parentAgent": "माथिल्लो", - "passwordMinLength": "Password must be at least 8 characters", - "passwordOptionalHint": "परिवर्तन नगर्ने भए खाली छोड्नुहोस्, परिवर्तन गर्न ८-अक्षरको पासवर्ड प्रविष्ट गर्नुहोस्", - "passwordPlaceholder": "८-अक्षरको पासवर्ड प्रविष्ट गर्नुहोस्", + "passwordMinLength": "Password must be at least 6 characters", + "passwordOptionalHint": "परिवर्तन नगर्ने भए खाली छोड्नुहोस्, परिवर्तन गर्न कम्तीमा ६-अक्षरको पासवर्ड प्रविष्ट गर्नुहोस्", + "passwordPlaceholder": "कम्तीमा ६-अक्षरको पासवर्ड प्रविष्ट गर्नुहोस्", "passwordRequired": "Password is required", "path": "Path", "playersPanel": { diff --git a/src/i18n/locales/ne/audit.json b/src/i18n/locales/ne/audit.json index 8f6b09a..e228de0 100644 --- a/src/i18n/locales/ne/audit.json +++ b/src/i18n/locales/ne/audit.json @@ -1,20 +1,37 @@ { - "title": "अडिट लग", - "moduleCode": "मोड्युल कोड", - "actionCode": "कार्य कोड", - "operatorType": "अपरेटर प्रकार", - "operatorIdPlaceholder": "अपरेटर ID प्रविष्ट गर्नुहोस्", - "exactMatch": "ठ्याक्कै मिलान", - "operatorTypePlaceholder": "जस्तै admin / system", + "title": "सञ्चालन रेकर्ड", + "filterModule": "व्यवसाय प्रकार", + "filterModuleAll": "सबै", + "filterOperatorType": "अपरेटर प्रकार", + "filterOperatorTypeAll": "सबै", + "operatorIdPlaceholder": "अपरेटर ID थाहा भए मात्र", "operator": "अपरेटर", - "module": "मोड्युल", - "action": "कार्य", - "target": "लक्ष्य", + "summary": "के भयो", + "target": "सम्बन्धित", "time": "समय", - "empty": "डाटा छैन", + "empty": "रेकर्ड छैन", + "moduleOptions": { + "agent": "एजेन्ट", + "system": "प्रणाली", + "settings": "सेटिङ", + "integration": "इन्टिग्रेसन", + "player_manage": "खेलाडी व्यवस्थापन", + "player_service": "खेलाडी सेवा", + "risk_cap": "जोखिम सीमा", + "odds": "बाधा", + "play_config": "खेल कन्फिग", + "settlement": "बन्दोबस्त", + "report_jobs": "रिपोर्ट", + "reconcile_jobs": "मिलान", + "draw": "ड्र", + "wallet": "वालेट", + "reconcile": "मिलान", + "jackpot": "ज्याकपट", + "dashboard": "ड्यासबोर्ड" + }, "operatorTypes": { "admin": "प्रशासक", "player": "खेलाडी", "system": "प्रणाली" } -} +} \ No newline at end of file diff --git a/src/i18n/locales/ne/auth.json b/src/i18n/locales/ne/auth.json index 576d65f..0ef1f38 100644 --- a/src/i18n/locales/ne/auth.json +++ b/src/i18n/locales/ne/auth.json @@ -19,6 +19,7 @@ "captchaLoadFailed": "क्याप्चा लोड गर्न सकिएन। API वा नेटवर्क जाँच गर्नुहोस्।", "apiBaseMissingToast": "API proxy सक्षम छैन: LOTTERY_API_UPSTREAM Laravel तर्फ छ कि छैन जाँच गर्नुहोस्", "captchaRequired": "पहिले क्याप्चा रिफ्रेस गर्नुहोस्", + "passwordMinLength": "पासवर्ड कम्तीमा ६ वर्ण हुनुपर्छ", "welcome": "स्वागत छ, {{name}}", "networkFailed": "नेटवर्क अनुरोध असफल भयो", "loginFailed": "लगइन असफल भयो" diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index 9b2b5ac..e1c9cc2 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -165,9 +165,8 @@ "nav": { "account": "खाता सेटिङ", "admin_roles": "भूमिका व्यवस्थापन", - "admin_users": "प्रशासक सूची", - "agent_list": "एजेन्ट सूची", - "agents": "एजेन्ट लाइन", + "admin_users": "प्लेटफर्म खाता", + "agents": "एजेन्ट व्यवस्थापन", "audit": "अडिट लग", "config": "सञ्चालन कन्फिगरेसन", "currencies": "मुद्रा व्यवस्थापन", @@ -186,8 +185,8 @@ "rules_plays": "खेल नियम", "settings": "सेटिङ", "settlement": "सेटलमेन्ट", - "settlement_center": "सेटलमेन्ट केन्द्र", - "tickets": "टिकट सूची", + "settlement_center": "क्रेडिट सेटलमेन्ट", + "tickets": "टिकट", "wallet": "वालेट" }, "pagination": { @@ -217,9 +216,9 @@ "securitySettingsDesc": "लगइन पासवर्ड परिवर्तन गर्नुहोस्। नपरिवर्तन गर्दा खाली छोड्नुहोस्।", "sidebar": { "group": { - "agent": "एजेन्ट संगठन", + "agent": "एजेन्ट", "finance": "वित्त र रिपोर्ट", - "operations": "दैनिक सञ्चालन", + "operations": "सञ्चालन", "overview": "सारांश", "platform": "प्लेटफर्म", "rules": "नियम र प्यारामिटर" @@ -257,6 +256,7 @@ }, "validation": { "passwordMismatch": "पासवर्ड मिलेन", + "passwordMinLength": "पासवर्ड कम्तीमा ६ वर्ण हुनुपर्छ", "required": "{{field}} अनिवार्य छ" } } diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index 65a2846..67beb46 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -518,6 +518,7 @@ } }, "system": { + "pageTitle": "प्रणाली सेटिङ", "confirmSaveCurrencyFormatDescription": "यसले दशमलव स्थान र विभाजक अद्यावधिक गर्छ।", "confirmSaveCurrencyFormatTitle": "मुद्रा प्रदर्शन ढाँचा बचत गर्ने?", "confirmSaveDescription": "ड्रअ समीक्षा, कूलडाउन, स्वचालित सेटलमेन्ट/अनुमोदन/पेआउट र खेल नियम प्रदर्शन अद्यावधिक हुन्छ। साइटव्यापी सञ्चालनमा असर पर्न सक्छ।", @@ -582,12 +583,33 @@ "saveSettlementSuccess": "सेटलमेन्ट स्वचालन बचत भयो", "saveSuccess": "प्रणाली सेटिङ सुरक्षित भयो", "sections": { + "deployment": "डिप्लोयमेन्ट (पढ्न मात्र)", + "deploymentDescription": "पूर्वनिर्धारित मुद्रा र ड्र तालिका सर्भर .env मा सेट हुन्छ; यहाँ प्रभावी मान देखिन्छ।", "currencyFormat": "मुद्रा प्रदर्शन ढाँचा", "currencyFormatDescription": "साइटभरि रकमका दशमलव र विभाजक (मुद्रा मास्टर डाटाबाट अलग)।", - "draw": "ड्र तालिका र समीक्षा", - "drawDescription": "ड्र समय, बन्द सञ्झ्याल, म्यानुअल समीक्षा र कूलडाउन नियन्त्रण। यो ब्लकमा परिवर्तित फिल्ड मात्र पेश हुन्छ।", + "draw": "ड्रअ समीक्षा", + "drawDescription": "RNG पछि म्यानुअल समीक्षा र प्रकाशनपछि कूलडाउन।", "settlement": "सेटलमेन्ट स्वचालन", - "settlementDescription": "टिक स्वतः सेटलमेन्ट, अनुमोदन र भुक्तानी चलाउने नियन्त्रण। यो ब्लकमा परिवर्तित फिल्ड मात्र पेश हुन्छ।" + "settlementDescription": "क्रम: स्वतः सेटलमेन्ट → स्वतः अनुमोदन → स्वतः भुक्तानी। बन्द हुँदा सम्बन्धित मोड्युलमा म्यानुअल गर्नुहोस्।" + }, + "deployment": { + "footer": "परिवर्तनपछि PHP पुनः सुरु वा config cache refresh गर्नुहोस्।", + "fields": { + "default_currency": "पूर्वनिर्धारित मुद्रा", + "draw_timezone": "ड्रअ समय क्षेत्र", + "draw_interval_minutes": "ड्रअ अन्तराल (मिनेट)", + "draw_betting_window_seconds": "बेटिङ विन्डो (सेकेन्ड)", + "draw_close_before_draw_seconds": "ड्रअ अघि बन्द (सेकेन्ड)", + "draw_buffer_draws_ahead": "अग्रिम सिर्जना गरिने ड्रअ" + }, + "hints": { + "default_currency": "नयाँ खेलाडीको पूर्वनिर्धारित मुद्रा", + "draw_timezone": "ड्रअ तालिका र प्रदर्शनको समय क्षेत्र", + "draw_interval_minutes": "लगातार ड्रअ बीचको मिनेट", + "draw_betting_window_seconds": "प्रति ड्रअ बेटिङ खुला रहने समय", + "draw_close_before_draw_seconds": "ड्रअ अघि बेट बन्द हुने सेकेन्ड", + "draw_buffer_draws_ahead": "अग्रिम सिर्जना गरिने भविष्य ड्रअ संख्या" + } }, "states": { "disabled": "बन्द", diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json index 1323317..e4432b9 100644 --- a/src/i18n/locales/zh/adminUsers.json +++ b/src/i18n/locales/zh/adminUsers.json @@ -6,10 +6,10 @@ "loadFailed": "加载管理员列表失败", "roleLoadFailed": "加载角色列表失败", "nicknameRequired": "请填写昵称", - "newPasswordMin": "新密码至少 8 位", + "newPasswordMin": "新密码至少 6 位", "roleRequired": "请至少选择一个角色", "usernameRequired": "请填写登录账号", - "passwordMin": "密码至少 8 位", + "passwordMin": "密码至少 6 位", "createSuccess": "已创建管理员 {{name}}", "updateSuccess": "已更新 {{name}}", "saveAccountFailed": "保存账号失败", @@ -69,6 +69,7 @@ "status": "状态", "users": "关联用户", "permissions": "权限数", + "enabledAreas": "开放模块", "actions": "操作" }, "roleActions": { @@ -101,8 +102,17 @@ }, "rolePermissionDialog": { "title": "角色权限", - "packageHint": "勾选左侧模块行仅授予「查看」;录入、封盘、开奖等管理操作请单独勾选「管理」。" + "titleWithName": "{{name}}" }, + "permissionAccess": { + "none": "不需要", + "view": "仅查看", + "review": "可审核", + "export": "可导出", + "manage": "可管理", + "special": "含特权操作" + }, + "roleDialog": { "createTitle": "新增角色", "editTitle": "编辑角色", @@ -128,7 +138,7 @@ "emailPlaceholder": "留空则不填", "password": "密码", "passwordOptional": "密码(可选)", - "passwordPlaceholderCreate": "至少 8 位", + "passwordPlaceholderCreate": "至少 6 位", "passwordPlaceholderEdit": "不修改请留空", "site": "绑定站点", "sitePlaceholder": "选择该账号可访问的数据站点", @@ -159,7 +169,7 @@ "permissionGroups": { "all": "全部权限", "dashboard": "仪表盘", - "admin_users": "管理列表", + "admin_users": "平台账号", "admin_roles": "角色管理", "agents": "代理管理", "players": "玩家列表", diff --git a/src/i18n/locales/zh/agents.json b/src/i18n/locales/zh/agents.json index 9bbf74d..1aecfd1 100644 --- a/src/i18n/locales/zh/agents.json +++ b/src/i18n/locales/zh/agents.json @@ -20,6 +20,7 @@ "overviewDownlineCount": "{{count}} 个", "downlineEmptyTitle": "暂无直属下级", "tabProfile": "占成与授信", + "tabProfileShort": "占成授信", "tabProfileReadOnly": "占成与授信(只读)", "profileReadOnlyHint": "占成、授信与回水由上级配置,如需调整请联系上级代理或平台。", "selfAgentOverviewHint": "以下为上级为您分配的授信额度,占成与回水由上级在后台维护,本账号不可查看或修改。", @@ -42,7 +43,8 @@ "downlineCount": "下级数" }, "editAccount": "账号与状态", - "editAgent": "编辑代理", + "editAgent": "编辑", + "parentMeta": "上级 {{name}}", "editCurrent": "编辑本代理", "saveProfile": "保存占成与授信", "profileTabHint": "占成、授信、回水与风控标签在此维护;登录名与密码请用「账号与状态」。", @@ -56,7 +58,13 @@ "sidebarShareRate": "占成 {{rate}}%", "sidebarAvailableCredit": "可下发 {{amount}}", "expand": "展开", - "collapse": "收起" + "collapse": "收起", + "openInTree": "在树中打开" + }, + "viewModes": { + "label": "视图", + "tree": "树形", + "list": "列表" }, "listTitle": "代理列表", "listSearch": "搜索代理名称 / 编码 / 登录名", @@ -132,7 +140,7 @@ "usernameRequired": "请填写登录名", "usernameInvalidCharset": "登录名只能使用字母、数字、点(.)、下划线和连字符", "passwordRequired": "请填写密码", - "passwordMinLength": "密码至少 8 位", + "passwordMinLength": "密码至少 6 位", "bindAccountPasswordRequired": "该代理尚未绑定登录账号,请填写初始密码以创建账号", "bindAccountHint": "该代理尚无登录账号,保存时将自动创建并绑定。", "modelGuide": "代理层负责数据范围(Scope)与授权上限(Ceiling),账号权限请通过角色分配。", @@ -328,7 +336,7 @@ "name": "一级代理名称", "username": "后台登录账号", "password": "初始密码", - "passwordHint": "至少 8 位", + "passwordHint": "至少 6 位", "submit": "创建一级代理", "success": "一级代理已创建", "link": "创建一级代理" @@ -343,8 +351,8 @@ "loginRequired": "请填写登录账号与初始密码", "loginUsername": "登录账号", "initialPassword": "初始密码", - "passwordHint": "至少 8 位", - "passwordMinLength": "初始密码至少 8 位", + "passwordHint": "至少 6 位", + "passwordMinLength": "初始密码至少 6 位", "externalIdOptional": "外部 ID(可选)", "externalIdHint": "留空则系统自动生成", "creditLimit": "授信额度", @@ -363,7 +371,11 @@ "playerRef": "玩家标识", "usernameNickname": "用户名 / 昵称", "creditLimitAvailable": "授信 / 可用", - "createSuccessNative": "玩家 {{name}} 已创建,请使用彩票端 /login 登录" + "createSuccessNative": "玩家 {{name}} 已创建,请使用彩票端 /login 登录", + "searchPh": "搜索玩家账号 / 标识", + "manageSettlement": "处理账单", + "noPendingBillsGoCenter": "该玩家暂无待处理账单,已打开结算中心。", + "billingLoadFailed": "加载账单失败" }, "delegation": { "title": "下放权限上限", @@ -406,6 +418,6 @@ "deleteSuccess": "已删除账号 {{name}}" }, "usernamePlaceholder": "请输入登录名", - "passwordPlaceholder": "请输入8位数密码", - "passwordOptionalHint": "留空则不修改,修改请输入8位数密码" + "passwordPlaceholder": "请输入至少6位密码", + "passwordOptionalHint": "留空则不修改,修改请输入至少6位密码" } diff --git a/src/i18n/locales/zh/audit.json b/src/i18n/locales/zh/audit.json index cc090e4..00c4207 100644 --- a/src/i18n/locales/zh/audit.json +++ b/src/i18n/locales/zh/audit.json @@ -1,20 +1,37 @@ { - "title": "审计日志", - "moduleCode": "模块", - "actionCode": "动作", - "operatorType": "操作者类型", - "operatorIdPlaceholder": "请输入操作人 ID", - "exactMatch": "请输入完整名称", - "operatorTypePlaceholder": "如管理员、系统", - "operator": "操作者", - "module": "模块", - "action": "动作", - "target": "目标", + "title": "操作记录", + "filterModule": "业务类型", + "filterModuleAll": "全部业务", + "filterOperatorType": "操作人类型", + "filterOperatorTypeAll": "全部", + "operatorIdPlaceholder": "知道操作人编号时可填写", + "operator": "操作人", + "summary": "操作说明", + "target": "涉及对象", "time": "时间", - "empty": "无数据", + "empty": "暂无记录", + "moduleOptions": { + "agent": "代理管理", + "system": "系统管理", + "settings": "系统设置", + "integration": "接入站点", + "player_manage": "玩家管理", + "player_service": "玩家服务", + "risk_cap": "限额规则", + "odds": "赔率配置", + "play_config": "玩法配置", + "settlement": "派彩结算", + "report_jobs": "报表导出", + "reconcile_jobs": "对账任务", + "draw": "开奖期号", + "wallet": "钱包资金", + "reconcile": "对账核对", + "jackpot": "奖池", + "dashboard": "工作台" + }, "operatorTypes": { - "admin": "管理员", + "admin": "后台管理员", "player": "玩家", - "system": "系统" + "system": "系统自动" } -} +} \ No newline at end of file diff --git a/src/i18n/locales/zh/auth.json b/src/i18n/locales/zh/auth.json index 4008845..c102d9f 100644 --- a/src/i18n/locales/zh/auth.json +++ b/src/i18n/locales/zh/auth.json @@ -19,6 +19,7 @@ "captchaLoadFailed": "无法获取验证码,请检查接口或网络", "apiBaseMissingToast": "API 代理未启用:请检查 LOTTERY_API_UPSTREAM 是否指向 Laravel", "captchaRequired": "请先刷新验证码", + "passwordMinLength": "密码至少需要 6 个字符", "welcome": "欢迎,{{name}}", "networkFailed": "网络请求失败", "loginFailed": "登录失败" diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 9a88aa2..4d075bc 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -51,7 +51,8 @@ }, "validation": { "required": "请填写{{field}}", - "passwordMismatch": "两次输入的密码不一致" + "passwordMismatch": "两次输入的密码不一致", + "passwordMinLength": "密码至少需要 6 个字符" }, "aria": { "expand": "展开", @@ -156,7 +157,7 @@ "nav": { "home": "首页", "dashboard": "仪表盘", - "admin_users": "管理列表", + "admin_users": "平台账号", "admin_roles": "角色管理", "players": "玩家列表", "currencies": "币种管理", @@ -171,25 +172,24 @@ "settlement": "结算", "jackpot": "奖池", "reconcile": "对账", - "tickets": "注单列表", + "tickets": "注单", "audit": "审计日志", "settings": "系统设置", "account": "账号设置", "integration": "接入配置", - "agents": "代理线路", - "agent_list": "代理列表", - "settlement_center": "结算中心", + "agents": "代理管理", + "settlement_center": "信用结算", "config": "运营配置" }, "sidebar": { "workspace": "工作台", "group": { "overview": "总览", - "agent": "代理组织", - "operations": "日常运营", + "agent": "代理", + "operations": "运营", "finance": "资金与报表", "rules": "规则与参数", - "platform": "平台管理" + "platform": "平台" } }, "auth": { diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index 4211a25..4d5ef5a 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -17,7 +17,7 @@ "rulesPlaysTitle": "投注规则", "rulesOddsTitle": "赔率与基础回水", "rulesOddsDescription": "赔率矩阵与基础回水在同一页维护,共用赔率版本线。", - "rulesOddsDescriptionShort": "左侧选玩法,右侧修改赔率与基础回水;代理/玩家回水需在此基础上叠加,修改后记得保存草稿并发布。", + "rulesOddsDescriptionShort": "", "riskCapTitle": "限额版本" }, "hub": { @@ -96,7 +96,7 @@ "codeRequired": "请填写 site_code", "adminUsernameRequired": "请填写站点后台登录名", "adminNicknameRequired": "请填写站点后台账号昵称", - "adminPasswordRequired": "请填写至少 8 位的站点后台初始密码" + "adminPasswordRequired": "请填写至少 6 位的站点后台初始密码" }, "columns": { "code": "site_code", @@ -135,7 +135,7 @@ "name": "请输入站点名称", "adminUsername": "请输入后台登录名", "adminNickname": "请输入账号昵称", - "adminPassword": "至少 8 位", + "adminPassword": "至少 6 位", "adminEmail": "请输入邮箱", "currency": "请输入币种代码,如 NPR", "walletApiUrl": "请输入钱包接口地址", @@ -153,11 +153,9 @@ "archived": "已归档" }, "versionSwitcher": { - "sheetTitle": "切换配置版本", - "sheetDescription": "选择一条版本在本页查看;草稿可编辑,生效中与已归档为只读。", + "sheetTitle": "版本", "loading": "加载中…", - "noneSelected": "未选择版本", - "switch": "切换版本", + "noneSelected": "选择版本", "empty": "暂无版本记录。", "count": "{{count}} 条", "effectiveAt": "生效时间:{{value}}", @@ -169,7 +167,7 @@ "rollback": "回滚", "delete": "删除", "deleteConfirmTitle": "确认删除版本?", - "deleteConfirmDescription": "将永久删除版本 ID {{id}}(version_no {{version}})。生效中的版本不可删除。" + "deleteConfirmDescription": "将删除版本 v{{version}},此操作不可恢复。" }, "versionToolbar": { "draftEditing": "正在编辑草稿,保存并发布后生效" @@ -221,6 +219,7 @@ "confirmSaveDescription": "将更新转入/转出单笔限额,立即影响玩家钱包转账。" }, "system": { + "pageTitle": "系统设置", "title": "开奖与结算运行参数", "runtimeTitle": "全局运行参数", "runtimeIntro1": "这里放不属于玩法版本、赔率版本、风控版本的全局系统参数。它们会直接影响钱包转账、任务开关或系统运行策略。", @@ -234,12 +233,33 @@ "saveSettlementSuccess": "结算自动化参数已保存", "saveFrontendSuccess": "前端展示配置已保存", "sections": { - "draw": "开奖节奏与审核", - "drawDescription": "控制期号节奏、封盘与开奖后人工审核、冷静期。仅保存本区块内修改过的项。", + "deployment": "部署参数(只读)", + "deploymentDescription": "默认币种、期号节奏等由运维在服务器 .env 配置,此处展示当前生效值。", + "draw": "开奖审核", + "drawDescription": "控制开奖后是否必须人工审核,以及结果发布后的冷静期。", "currencyFormat": "金额显示格式", "currencyFormatDescription": "全站金额展示的小数位与分隔符,与币种主数据无关。", "settlement": "结算自动化", - "settlementDescription": "控制 tick 是否自动结算、审核与派彩。修改后只提交本区块变更项。" + "settlementDescription": "按顺序:自动结算 → 自动审核批次 → 自动派彩。关闭后需到对应模块手工处理。" + }, + "deployment": { + "footer": "修改后需重启 PHP 应用或刷新配置缓存方生效;如有疑问请联系运维。", + "fields": { + "default_currency": "默认币种", + "draw_timezone": "期号时区", + "draw_interval_minutes": "开奖间隔(分钟)", + "draw_betting_window_seconds": "下注窗口(秒)", + "draw_close_before_draw_seconds": "封盘提前(秒)", + "draw_buffer_draws_ahead": "预生成未来期数" + }, + "hints": { + "default_currency": "新玩家建档、未指定币种时的默认值", + "draw_timezone": "期号计划与展示使用的时区", + "draw_interval_minutes": "相邻两期开奖时间的间隔", + "draw_betting_window_seconds": "每期允许下注的时长", + "draw_close_before_draw_seconds": "开奖前停止收单的提前秒数", + "draw_buffer_draws_ahead": "调度提前生成的未开奖期数" + } }, "saveFailed": "系统设置保存失败", "unsavedChanges": "有未保存的更改", @@ -291,7 +311,7 @@ "confirmSaveRuntimeTitle": "确认保存开奖与结算参数?", "confirmSaveRuntimeDescription": "将更新开奖审核、期号节奏、冷静期、自动结算/审核/派彩等,不影响玩法规则 HTML。", "confirmSaveDrawTitle": "确认保存开奖参数?", - "confirmSaveDrawDescription": "将更新开奖审核、期号节奏与冷静期等本区块字段。", + "confirmSaveDrawDescription": "将更新开奖人工审核开关与冷静期时长。", "confirmSaveCurrencyFormatTitle": "确认保存金额显示格式?", "confirmSaveCurrencyFormatDescription": "将更新小数位与千分位/小数分隔符。", "confirmSaveSettlementTitle": "确认保存结算自动化?", @@ -367,15 +387,15 @@ "publishFailed": "发布失败", "publishDialog": { "title": "确认发布玩法配置?", - "description": "新配置将影响后续下注;已下注注单仍按各自快照结算。", + "description": "发布后对新注单生效。", "confirm": "确认发布" }, "batchSwitchConfirmTitle": "确认批量{{action}}?", - "batchSwitchConfirmDescription": "将{{action}}「{{group}}」下 {{count}} 个玩法,并写入当前草稿。", + "batchSwitchConfirmDescription": "将{{action}}「{{group}}」下 {{count}} 个玩法。", "batchSwitchEnable": "开启", "batchSwitchDisable": "关闭", "toggleConfirmTitle": "确认{{action}}玩法 {{playCode}}?", - "toggleConfirmDescription": "将写入当前草稿;保存并发布后才会影响玩家端。", + "toggleConfirmDescription": "将写入当前草稿。", "batchPartialEnabled": "{{enabledCount}}/{{total}} 已开启(未全开,打开开关将全部开启)", "toggleEnable": "开启", "toggleDisable": "关闭", @@ -513,8 +533,8 @@ "loadingDetails": "正在加载详情…", "multiplier": "倍数 x{{value}} · {{currency}}", "missingScopeRow": "缺少 {{scope}} 对应行,请检查种子或版本数据。", - "rebateRate": "基础回水比例 (%)", - "rebateRateHint": "这里维护的是平台基础回水,会把 rebate_rate 写入该玩法下所有奖级范围;代理/玩家回水需在此基础上叠加。", + "rebateRate": "基础回水 (%)", + "rebateRateHint": "", "placeholders": { "multiplier": "请输入赔率倍数", "rebateRate": "请输入基础回水比例" @@ -532,7 +552,7 @@ }, "publishDialog": { "title": "确认发布赔率版本?", - "description": "新赔率会立即影响后续新注单;已成功下注的历史注单仍按各自保存的赔率快照结算。", + "description": "发布后对新注单生效。", "confirm": "确认发布", "columns": { "prizeScope": "奖级范围", @@ -608,14 +628,13 @@ "readOnlyHint": "当前为只读,请先创建草稿。", "readOnly": "只读", "defaultCap": { - "title": "默认封顶", - "description": "没有单独特殊封顶的号码,统一使用这条默认封顶模板。", - "fieldLabel": "封顶金额(主币单位)" + "title": "默认限额", + "fieldLabel": "限额金额" }, "specialCaps": { - "title": "特殊封顶", - "description": "不选期号时表示全局号码限额;选择期号后表示该期单独限额。" + "title": "号码限额" }, + "scope": { "global": "全局号码", "drawId": "期号 ID:{{id}}" @@ -641,6 +660,7 @@ "noDetailRows": "暂无明细行。", "table": { "scope": "作用范围", + "draw": "期号", "number": "号码", "capAmount": "封顶金额", "used": "已占用", @@ -668,7 +688,8 @@ }, "actions": { "update": "更新", - "addSpecialCap": "+ 新增特殊封顶", + "addNumberCap": "添加号码限额", + "addSpecialCap": "新增", "filterPresets": "筛选预设…", "exportCsv": "导出 CSV", "close": "关闭" diff --git a/src/i18n/locales/zh/draws.json b/src/i18n/locales/zh/draws.json index 5ba792b..bcfbd9f 100644 --- a/src/i18n/locales/zh/draws.json +++ b/src/i18n/locales/zh/draws.json @@ -67,11 +67,11 @@ "scheduleTitle": "时间安排", "resultBatchesTitle": "开奖批次", "batchSummaryTotal": "共 {{count}} 批", - "batchSummaryPending": "待审 {{count}}", + "batchSummaryPending": "待发布 {{count}}", "batchSummaryPublished": "已发 {{count}}", "noResultBatchesYet": "尚无开奖批次。", - "reviewQueueHint": "结果生成后,可前往审核与发布处理。", - "goToReviewTab": "去审核与发布", + "reviewQueueHint": "", + "goToReviewTab": "开奖发布", "businessDate": "业务日", "sequenceNo": "流水序号", "plannedDraw": "计划开奖", @@ -96,12 +96,12 @@ "published": "已发布", "viewFinance": "查看期号收支", "drawActions": "期号操作", - "drawActionsDesc": "手动封盘 / 取消 / RNG / 重开 / 触发结算均直接调用后台接口。", + "drawActionsDesc": "", "manualClose": "手动封盘", "cancelDraw": "取消期号", "cancelBeforeDraw": "未开奖前取消", "rngDraw": "RNG开奖", - "rngAutoGenerate": "RNG 自动生成", + "rngAutoGenerate": "自动开奖", "reopen": "重开", "cooldownReopen": "冷静期重开", "runSettlement": "触发结算", @@ -114,8 +114,8 @@ "actualBet": "当期实扣投注", "currentPayout": "当期派彩合计", "grossProfit": "近似毛损益", - "settlementBatchList": "结算批次列表(按期号筛选)", - "relatedSettlementBatches": "本关联期结算批次", + "settlementBatchList": "结算记录", + "relatedSettlementBatches": "结算批次", "noSettlementBatches": "暂无结算批次记录。", "ticketCount": "票数", "winCount": "中奖数", @@ -124,7 +124,7 @@ "resultsTitle": "开奖结果", "reviewAndPublish": "去审核 / 发布", "viewReviewQueue": "查看审核队列", - "noPublishedBatch": "暂无已发布批次。", + "noPublishedBatch": "暂无", "version": "版本 v{{version}}", "sourceType": "生成方式 {{source}}", "manualEntry": "人工录入", @@ -135,28 +135,28 @@ "tail3": "尾3", "tail2": "尾2", "headTail": "头/尾", - "manualResultEntry": "人工录入开奖结果", - "currentStatusAndDraft": "当前状态 {{status}} · 保存后生成待确认批次,不会直接发布", - "currentStatusLabel": "当前状态", - "currentStatusDraftHint": "保存后生成待确认批次,不会直接发布", + "manualResultEntry": "录入号码", + "currentStatusAndDraft": "", + "currentStatusLabel": "状态", + "currentStatusDraftHint": "", "hallPreviewStatusLabel": "大厅预览", "enter23Numbers": "请完整输入 23 组 4 位数字", - "draftSaved": "已保存草稿 v{{version}},等待确认发布", + "draftSaved": "已保存 v{{version}}", "saveFailed": "保存失败", "fillRandomNumbers": "随机填满", "clear": "清空", "saveDraft": "保存草稿", "saving": "保存中…", - "pendingBatches": "待确认批次", - "noPendingBatches": "当前没有待审核批次。", - "batchId": "批次 ID", + "pendingBatches": "待发布", + "noPendingBatches": "暂无", + "batchId": "批次", "numberCount": "号码条数", - "reviewAndPublishAction": "核对并发布", - "discardPendingBatch": "删除草稿", + "reviewAndPublishAction": "发布", + "discardPendingBatch": "删除", "discardingPendingBatch": "删除中…", "discardPendingBatchSuccess": "已删除待确认批次,可重新录入或 RNG", "discardPendingBatchFailed": "删除失败", - "publishReadOnlyHint": "发布页仅用于核对;若要改号请先删除本批次,回到上方「人工录入」重新保存。", + "publishReadOnlyHint": "", "noPublishPermission": "无发布权限", "batchNotFound": "未找到批次", "batchNotFoundDesc": "请返回审核列表确认 batch id。", @@ -173,10 +173,10 @@ "publishFailed": "发布失败", "sourceTypeFull": "生成方式:{{source}} · 号码条数:{{count}}/23 · RNG 摘要:{{hash}}", "subnav": { - "status": "期号状态", + "status": "概览", "results": "开奖结果", - "finance": "期号收支", - "review": "审核与发布", + "finance": "收支", + "review": "开奖发布", "riskOccupancy": "风控占用", "riskLockLogs": "占用流水", "riskHot": "热门号码", diff --git a/src/i18n/locales/zh/jackpot.json b/src/i18n/locales/zh/jackpot.json index 7fcb2f3..c1578c7 100644 --- a/src/i18n/locales/zh/jackpot.json +++ b/src/i18n/locales/zh/jackpot.json @@ -1,14 +1,9 @@ { "title": "奖池", - "configTitle": "奖池配置", - "pageDescription": "维护各币种奖池参数,下方可查询蓄水与派彩流水。", - "poolsSectionDescription": "蓄水比例、爆池阈值、开关与手动爆池。", - "rulesTitle": "规则说明", - "rulesJoin": "只有提交成功且满足最低参与下注额的注项,才会按蓄水比例进入奖池。", - "rulesBurst": "奖池会在达到爆池阈值、达到强制爆池间隔,或命中指定组合触发玩法时释放。", - "rulesManual": "手动爆池仅限超管兜底使用,可填写后台期号数字 ID 或期号编码。", - "recordsSectionTitle": "蓄水与派彩流水", - "recordsSectionDescription": "按条件筛选派彩记录与蓄水明细,只读查询。", + "configTitle": "奖池", + "pageTabs": "奖池", + "tabConfig": "配置", + "tabRecords": "流水", "loadFailed": "加载失败", "saveSuccess": "已保存", "saveFailed": "保存失败", @@ -19,7 +14,6 @@ "displayBalance": "展示余额 {{amount}}", "currentAmount": "当前池余额(主币单位)", "balanceAdjustmentTitle": "余额调整", - "balanceAdjustmentHint": "须填写原因并写入调整流水;不可在「保存」中直接改余额。", "adjustmentDirection": "方向", "adjustmentIncrease": "增加", "adjustmentDecrease": "减少", @@ -35,31 +29,25 @@ "confirmAdjustmentTitle": "确认提交奖池余额调整?", "confirmAdjustmentDescription": "将写入调整流水并更新当前池余额,请确认金额与原因无误。", "recentAdjustments": "最近调整记录", - "contributionRate": "蓄水比例 (%)", - "contributionRatePlaceholder": "如 2 表示 2%", - "triggerThreshold": "爆池阈值(主币单位)", - "triggerThresholdPlaceholder": "请输入触发阈值", - "payoutRate": "爆池派彩比例 (%)", - "payoutRatePlaceholder": "如 5 表示 5%", - "forceTriggerGap": "强制爆池间隔(已结算期数)", - "forceTriggerGapPlaceholder": "请输入强制触发间隔期数", - "minBetAmount": "最低参与下注额(主币单位)", - "minBetAmountPlaceholder": "请输入最低下注金额", - "comboTriggerPlays": "组合触发玩法(逗号分隔)", - "comboTriggerPlaysPlaceholder": "请输入玩法编码,多个用逗号分隔,如 straight,ibox", + "contributionRate": "蓄水比例 %", + "triggerThreshold": "爆池阈值", + "payoutRate": "派彩比例 %", + "forceTriggerGap": "强制爆池间隔(期)", + "minBetAmount": "最低下注额", + "comboTriggerPlays": "组合触发玩法", + "comboTriggerPlaysPlaceholder": "straight,ibox", "status": "开关", "disabled": "关闭", "enabled": "开启", "saving": "保存中…", "save": "保存", "confirmSavePoolTitle": "确认保存奖池配置?", - "confirmSavePoolDescription": "将更新蓄水比例、阈值、派彩比例等参数(不含池余额);余额请使用「余额调整」。", - "manualBurstDrawId": "手动爆池期号(ID 或编码)", - "manualBurstHint": "仅超级管理员可在紧急情况下触发;可填写后台期号数字 ID 或期号编码。须该期已开奖结算且存在头奖中奖注单,按当前「爆池派彩比例」释放并派彩入账。", + "confirmSavePoolDescription": "将更新奖池参数(不含余额)。", + "manualBurstDrawId": "爆池期号", "manualBurstConfirmTitle": "确认手动爆池?", - "manualBurstConfirmDescription": "将对期号 {{drawId}} 的头奖中奖玩家按奖池派彩比例分配 Jackpot,并扣减奖池余额。此操作不可自动撤销。", + "manualBurstConfirmDescription": "确认对期号 {{drawId}} 执行手动爆池?", "processing": "处理中…", - "manualBurst": "手动触发爆池(仅超管)", + "manualBurst": "手动爆池", "manualBurstConfirm": "确认爆池", "cancel": "取消", "filter": "筛选", @@ -72,7 +60,7 @@ "title": "奖池记录", "description": "派彩记录与奖池蓄水流水" }, - "poolsSectionTitle": "各币种奖池参数", + "payoutLoadFailed": "派彩记录加载失败", "contributionLoadFailed": "蓄水记录加载失败", "trigger": "触发", diff --git a/src/i18n/locales/zh/reconcile.json b/src/i18n/locales/zh/reconcile.json index 1d8b460..305040c 100644 --- a/src/i18n/locales/zh/reconcile.json +++ b/src/i18n/locales/zh/reconcile.json @@ -1,7 +1,10 @@ { "title": "对账", - "createTitle": "发起对账扫描", - "createHint": "将扫描所选日期内的转账单,比对彩票钱包流水,并在已配置主站 API 时核对主站幂等记录。", + "workflowHint": "本页按日期扫描并留存对账任务。处理当前仍未解决的异常转账单,请点右上角入口(打开钱包 → 转账单,并自动勾选「仅异常单」)。", + "shortcutAbnormalTransfers": "当前异常转账单", + "viewOnlyHint": "当前账号仅可查看历史对账任务。", + "createTitle": "发起扫描", + "createHint": "", "reconcileType": "对账类型", "reconcileTypeFixed": "钱包划转(主站 ⇄ 彩票)", "dateRange": "对账日期范围", @@ -12,13 +15,18 @@ "periodRequired": "请填写对账日期范围(开始与结束)", "periodOrderInvalid": "结束时间需晚于或等于开始时间", "confirmCreateTitle": "确认发起对账扫描?", - "confirmCreateDescription": "将扫描所选日期范围{{playerHint}}内的转账单,并自动生成差异明细。", + "confirmCreateDescription": "扫描所选日期{{playerHint}}内的转账单。", "confirmCreatePlayer": "内指定玩家", "confirmCreateAllPlayers": "内全部玩家", "createSuccess": "扫描完成,发现 {{count}} 条异常", "createSuccessEmpty": "扫描完成,未发现异常", "createFailed": "扫描失败", "noCreatePermission": "当前账号无发起对账扫描权限。", + "filterOpen": "仅待处理", + "filterAll": "全部明细", + "filterCount": "本页 {{shown}} / 共 {{total}} 条", + "filterOpenEmpty": "本页无待处理明细,可切换「全部明细」或翻页。", + "playerEmpty": "无匹配玩家", "jobsTitle": "对账任务", "refresh": "刷新", "jobNo": "任务号", @@ -31,16 +39,25 @@ "finishedAt": "完成时间", "createdAt": "创建时间", "operate": "操作", - "viewDetails": "查看差异明细", + "viewDetails": "查看明细", + "hideDetails": "收起", + "closeDetails": "关闭明细", "detailsTitle": "差异明细", + "detailsEmpty": "暂无明细", + "itemStatusColumn": "状态", + "itemIssueColumn": "异常类型", + "itemHandlingColumn": "处理进度", + "itemScanFindingColumn": "扫描发现", + "itemCurrentStatusColumn": "当前状态", + "itemScanFindingResolvedHint": "扫描时记录,现已处理", "transferNo": "转账单号", "walletTxnNo": "彩票钱包流水号", "mainSiteRef": "主站流水号", "mainSiteCheck": "主站核对", - "differenceAmount": "差额(分)", + "differenceAmount": "差额", "itemResult": "检查结果", "processingStatus": "处理状态", - "actions": "处理", + "actions": "操作", "openTransferOrder": "查看转账单", "openWalletTxn": "查看钱包流水", "detectedAt": "发现时间", diff --git a/src/i18n/locales/zh/reports.json b/src/i18n/locales/zh/reports.json index e1a9f2c..f0d31aa 100644 --- a/src/i18n/locales/zh/reports.json +++ b/src/i18n/locales/zh/reports.json @@ -1,6 +1,12 @@ { "title": "报表中心", - "subtitle": "集中查看运营、资金、风控与审计报表,统一按维度筛选后导出。", + "subtitle": "", + "profitScopeHint": "注单口径,非信用账期结算", + "shortcuts": { + "wallet": "转账单列表", + "risk": "风控中心", + "audit": "审计日志" + }, "pageGuide": "", "exportPanel": "导出设置", "chooseReport": "选择要导出的报表", @@ -86,8 +92,12 @@ }, "preview": { "title": "数据预览", - "subtitle": "查询结果将显示在下方表格,可导出 CSV 或 Excel。", - "empty": "暂无数据,请调整筛选条件后重试。", + "subtitle": "", + "empty": "暂无数据", + "sections": { + "settlementBatches": "结算批次", + "lockLogs": "占用日志" + }, "exportableRows": "行可导出", "summaryScopeHint": "上方统计卡除“记录数”外,默认按当前预览页汇总;需要全量口径请使用“导出 CSV/Excel(全量)”。", "scope": { diff --git a/src/i18n/locales/zh/settlementCenter.json b/src/i18n/locales/zh/settlementCenter.json index 95279fb..8d83182 100644 --- a/src/i18n/locales/zh/settlementCenter.json +++ b/src/i18n/locales/zh/settlementCenter.json @@ -38,6 +38,7 @@ "unsettledTickets": "未结算注单", "openReportHint": "进行中账期:占成/输赢来自账期内流水;账单数在关账后更新。", "viewDetail": "查看详情", + "processBills": "处理账单", "close": "关账", "closeNow": "立即关账", "hasOpen": "已有进行中账期 {{range}},须先关账才能开新期。", @@ -51,6 +52,7 @@ "periods": "账期", "bills": "账单", "operations": "收付与调账", + "aria": "账期视图", "ledger": "账务流水", "creditLedger": "信用流水", "playerBills": "玩家账单", @@ -198,7 +200,8 @@ "actions": { "detail": "详情", "viewBill": "查看账单", - "billDetail": "账单详情" + "billDetail": "账单详情", + "billDetailWithId": "账单 #{{id}}" }, "billDisplay": { "settlementFlow": "谁付谁", @@ -235,6 +238,7 @@ "unpaidAwaitingPayment": "请登记线下收付", "fullySettled": "本期已结清", "confirmHint": "确认后才可以登记收款或付款。", + "advancedActions": "调账 / 坏账", "recordReceiptFrom": "登记收款({{payer}} 付给 {{payee}})", "recordPayoutTo": "登记付款({{payer}} 付给 {{payee}})", "rebateAllocationsHint": "各层级代理对回水的承担明细。", @@ -321,7 +325,13 @@ "hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。", "emptyFiltered": "当前筛选下暂无账单,请改为「全部状态」或重置筛选。", "emptyClosed": "本期已关账但暂无账单。常见原因:账期内无信用盘玩家的已结算注单,或占成流水不在本账期时间范围内。", - "intro": "关账后生成的占成账单。可按类型或状态筛选,详情内确认或登记收付。" + "intro": "关账后生成的占成账单。可按类型或状态筛选,详情内确认或登记收付。", + "confirmOneTitle": "确认账单 #{{id}}?", + "confirmOneDesc": "确认后进入待收付,可登记线下收付。", + "confirmOneBtn": "确认", + "confirmedOne": "已确认", + "confirmFailed": "确认失败", + "payBtn": "收付" }, "panels": { "workbench": { diff --git a/src/i18n/locales/zh/wallet.json b/src/i18n/locales/zh/wallet.json index 3312350..287427c 100644 --- a/src/i18n/locales/zh/wallet.json +++ b/src/i18n/locales/zh/wallet.json @@ -3,9 +3,7 @@ "subnavLabel": "钱包子页", "subnavTransactions": "主站钱包流水", "subnavTransferOrders": "主站转账单", - "scopeHint": "本模块为主站钱包模式:钱包流水与主站转账单。信用盘玩家的账期结账请查看", - "scopeHintSettlementLink": "结算中心", - "scopeHintSettlement": "结算中心", + "ledgerChannel": "账本", "ledgerCredit": "信用流水", "ledgerWallet": "钱包流水", @@ -43,6 +41,9 @@ "options": "选项", "abnormalOnly": "仅异常单", "abnormalOnlyPending": "仅异常(待对账)", + "abnormalFilterActive": "当前仅显示对账标记的异常转账单(非钱包流水)。可在下方取消勾选「仅异常单」查看全部。", + "txnAbnormalFilterActive": "当前仅显示状态为「待对账」的流水。勾选后状态下拉无效,与接口 abnormal=1 一致。", + "txnDeepLinkActive": "已按链接带入筛选条件并自动查询。重置可清空 URL 参数。", "search": "搜索", "resetFilters": "重置筛选", "refreshCurrentPage": "刷新当前页", diff --git a/src/lib/admin-input-validation.ts b/src/lib/admin-input-validation.ts index 700cd93..27de3d0 100644 --- a/src/lib/admin-input-validation.ts +++ b/src/lib/admin-input-validation.ts @@ -1,8 +1,10 @@ /** 后台/彩票端登录账号:字母、数字、点、下划线、连字符。 */ export const ADMIN_ACCOUNT_PATTERN = /^[a-zA-Z0-9._-]+$/; -export const ADMIN_PASSWORD_MIN_LENGTH = 8; -export const NATIVE_PLAYER_PASSWORD_MIN_LENGTH = 6; +/** 全项目统一:后台账号、代理账号、彩票端玩家密码最短长度 */ +export const PASSWORD_MIN_LENGTH = 6; +export const ADMIN_PASSWORD_MIN_LENGTH = PASSWORD_MIN_LENGTH; +export const NATIVE_PLAYER_PASSWORD_MIN_LENGTH = PASSWORD_MIN_LENGTH; export type AccountValidationIssue = "empty" | "invalid_charset"; export type PasswordValidationIssue = "empty" | "too_short"; diff --git a/src/lib/admin-nav-label.ts b/src/lib/admin-nav-label.ts index 2182912..11e4729 100644 --- a/src/lib/admin-nav-label.ts +++ b/src/lib/admin-nav-label.ts @@ -23,16 +23,19 @@ const NAV_SEGMENT_I18N_KEYS: Record = { settings: "settings", integration: "integration", agents: "agents", - agent_list: "agent_list", config: "config", }; +const LEGACY_NAV_SEGMENT_I18N_KEYS: Record = { + agent_list: "agents", +}; + export function adminNavLabel( segment: string, t: TFunction, apiLabel?: string | null, ): string { - const key = NAV_SEGMENT_I18N_KEYS[segment]; + const key = NAV_SEGMENT_I18N_KEYS[segment] ?? LEGACY_NAV_SEGMENT_I18N_KEYS[segment]; if (key) { return t(`nav.${key}`, { ns: "common" }); } diff --git a/src/lib/admin-page-title.ts b/src/lib/admin-page-title.ts index 6a364a6..b42e6aa 100644 --- a/src/lib/admin-page-title.ts +++ b/src/lib/admin-page-title.ts @@ -12,11 +12,14 @@ const EXACT_ROUTES: Record = { "/admin/settlement-batches": { ns: "settlement", key: "batchList" }, "/admin/reconcile": { ns: "reconcile", key: "title" }, "/admin/reports": { ns: "reports", key: "title" }, + "/admin/reports/profit": { ns: "reports", key: "title" }, + "/admin/reports/wallet": { ns: "reports", key: "title" }, + "/admin/reports/risk": { ns: "reports", key: "title" }, + "/admin/reports/audit": { ns: "reports", key: "title" }, "/admin/audit-logs": { ns: "audit", key: "title" }, "/admin/admin-users": { ns: "adminUsers", key: "title" }, "/admin/admin-roles": { ns: "adminRoles", key: "title" }, "/admin/agents": { ns: "agents", key: "title" }, - "/admin/agents/list": { ns: "agents", key: "listTitle" }, "/admin/agents/provision": { ns: "agents", key: "subnav.provision" }, "/admin/agents/sites": { ns: "config", key: "integrationSites.title" }, "/admin/settlement-center": { ns: "settlementCenter", key: "title" }, diff --git a/src/lib/admin-permission-profiles.ts b/src/lib/admin-permission-profiles.ts new file mode 100644 index 0000000..6f13355 --- /dev/null +++ b/src/lib/admin-permission-profiles.ts @@ -0,0 +1,313 @@ +export type PermissionAccessLevel = "none" | "view" | "review" | "export" | "manage" | "special"; + +export type PermissionGroupProfile = { + key: string; + /** 仅平台超管在角色配置里可见(接入规则、全局 RBAC 等) */ + platformOnly?: boolean; + levels: Array<{ + key: PermissionAccessLevel; + labelKey: string; + slugs: readonly string[]; + }>; +}; + +/** 客户配角色时用的「能做什么」档位,底层仍映射到 prd.* slug。 */ +export const ADMIN_PERMISSION_GROUP_PROFILES: PermissionGroupProfile[] = [ + { + key: "dashboard", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.dashboard.view"] }, + ], + }, + { + key: "agents", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { + key: "view", + labelKey: "permissionAccess.view", + slugs: ["prd.agent.view", "prd.agent.role.view", "prd.agent.user.view"], + }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: [ + "prd.agent.view", + "prd.agent.manage", + "prd.agent.role.view", + "prd.agent.role.manage", + "prd.agent.user.view", + "prd.agent.user.manage", + ], + }, + ], + }, + { + key: "players", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { + key: "view", + labelKey: "permissionAccess.view", + slugs: ["prd.users.view_finance", "prd.users.view_cs"], + }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: ["prd.users.view_finance", "prd.users.view_cs", "prd.users.manage", "prd.player_freeze.manage"], + }, + ], + }, + { + key: "draws", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.draw_result.view"] }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: ["prd.draw_result.view", "prd.draw_result.manage", "prd.draw_reopen.manage"], + }, + ], + }, + { + key: "tickets", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.tickets.view"] }, + ], + }, + { + key: "settlement", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { + key: "view", + labelKey: "permissionAccess.view", + slugs: ["prd.payout.view", "prd.settlement.agent.view"], + }, + { + key: "review", + labelKey: "permissionAccess.review", + slugs: ["prd.payout.view", "prd.payout.review", "prd.settlement.agent.view"], + }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: [ + "prd.payout.view", + "prd.payout.review", + "prd.payout.manage", + "prd.settlement.agent.view", + "prd.settlement.agent.manage", + ], + }, + ], + }, + { + key: "wallet", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { + key: "view", + labelKey: "permissionAccess.view", + slugs: ["prd.wallet_reconcile.view", "prd.wallet_reconcile.view_cs", "prd.users.view_finance"], + }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: [ + "prd.wallet_reconcile.view", + "prd.wallet_reconcile.view_cs", + "prd.users.view_finance", + "prd.wallet_reconcile.manage", + "prd.wallet_adjust.manage", + ], + }, + ], + }, + { + key: "reconcile", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { + key: "view", + labelKey: "permissionAccess.view", + slugs: ["prd.wallet_reconcile.view", "prd.wallet_reconcile.view_cs"], + }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.wallet_reconcile.view", "prd.wallet_reconcile.view_cs", "prd.wallet_reconcile.manage"] }, + ], + }, + { + key: "reports", + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.report.view"] }, + { + key: "export", + labelKey: "permissionAccess.export", + slugs: ["prd.report.view", "prd.report.export"], + }, + ], + }, + { + key: "admin_users", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.admin_user.manage"] }, + ], + }, + { + key: "admin_roles", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.admin_role.manage"] }, + ], + }, + { + key: "integration", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.integration.view"] }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.integration.view", "prd.integration.manage"] }, + ], + }, + { + key: "rules_plays", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.odds.view"] }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: ["prd.odds.view", "prd.play_switch.manage", "prd.odds.manage"], + }, + ], + }, + { + key: "rules_odds", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.rebate.view"] }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: ["prd.rebate.view", "prd.rebate.manage", "prd.odds.manage"], + }, + ], + }, + { + key: "risk_cap", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.risk_cap.view"] }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.risk_cap.view", "prd.risk_cap.manage"] }, + ], + }, + { + key: "risk", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.risk.view"] }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.risk.view", "prd.risk.manage"] }, + ], + }, + { + key: "jackpot", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.jackpot.view"] }, + { + key: "manage", + labelKey: "permissionAccess.manage", + slugs: ["prd.jackpot.view", "prd.jackpot.manage"], + }, + { + key: "special", + labelKey: "permissionAccess.special", + slugs: ["prd.jackpot.view", "prd.jackpot.manage", "prd.jackpot.manual_burst"], + }, + ], + }, + { + key: "currencies", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.currency.manage"] }, + ], + }, + { + key: "audit", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "view", labelKey: "permissionAccess.view", slugs: ["prd.audit.view"] }, + ], + }, + { + key: "settings", + platformOnly: true, + levels: [ + { key: "none", labelKey: "permissionAccess.none", slugs: [] }, + { key: "manage", labelKey: "permissionAccess.manage", slugs: ["prd.wallet_reconcile.manage", "prd.currency.manage"] }, + ], + }, +]; + +const PROFILE_BY_KEY = new Map(ADMIN_PERMISSION_GROUP_PROFILES.map((profile) => [profile.key, profile])); + +export function permissionProfileForGroup(key: string): PermissionGroupProfile | undefined { + return PROFILE_BY_KEY.get(key); +} + +export function allSlugsForProfile(profile: PermissionGroupProfile): string[] { + const out = new Set(); + for (const level of profile.levels) { + for (const slug of level.slugs) { + out.add(slug); + } + } + return Array.from(out); +} + +export function resolveProfileLevel(profile: PermissionGroupProfile, selectedSlugs: readonly string[]): PermissionAccessLevel { + const selected = new Set(selectedSlugs); + let matched: PermissionAccessLevel = "none"; + const order: PermissionAccessLevel[] = ["view", "review", "export", "manage", "special"]; + + for (const levelKey of order) { + const level = profile.levels.find((item) => item.key === levelKey); + if (level === undefined || level.slugs.length === 0) { + continue; + } + if (level.slugs.every((slug) => selected.has(slug))) { + matched = level.key; + } + } + + return matched; +} + +export function applyProfileLevel( + profile: PermissionGroupProfile, + levelKey: PermissionAccessLevel, + selectedSlugs: readonly string[], +): string[] { + const groupSlugs = new Set(allSlugsForProfile(profile)); + const next = selectedSlugs.filter((slug) => !groupSlugs.has(slug)); + const level = profile.levels.find((item) => item.key === levelKey); + if (level) { + next.push(...level.slugs); + } + return Array.from(new Set(next)).sort(); +} \ No newline at end of file diff --git a/src/lib/admin-prd.ts b/src/lib/admin-prd.ts index e44c67a..e165847 100644 --- a/src/lib/admin-prd.ts +++ b/src/lib/admin-prd.ts @@ -103,6 +103,15 @@ export const PRD_WALLET_TRANSFER_ACCESS_ANY = [ PRD_WALLET_ADJUST_MANAGE, ] as const; +/** 报表中心页(含资金/风控/审计类导出报表入口) */ +export const PRD_REPORTS_PAGE_ACCESS_ANY = [ + PRD_REPORT_VIEW, + PRD_REPORT_EXPORT, + ...PRD_WALLET_TRANSFER_ACCESS_ANY, + ...PRD_RISK_ACCESS_ANY, + PRD_AUDIT_VIEW, +] as const; + /** 单玩家钱包查询 */ export const PRD_WALLET_PLAYER_ACCESS_ANY = [ PRD_USERS_VIEW_FINANCE, diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index 183fbca..49e2163 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -32,7 +32,6 @@ export const adminNavIconBySegment: Record { dashboard: LayoutDashboard, agents: Network, - agent_list: Users, players: Users, draws: CalendarClock, rules_plays: ClipboardList, @@ -56,6 +55,7 @@ export const adminNavIconBySegment: Record /** 旧版 localStorage / 接口缓存中的 segment,避免首屏侧栏崩溃 */ const legacyAdminNavIconBySegment: Record = { + agent_list: Users, config: SlidersHorizontal, }; diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index 65ecbc9..e36548d 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -11,7 +11,6 @@ export type AdminNavGroup = export type AdminNavSegment = | "dashboard" | "agents" - | "agent_list" | "players" | "draws" | "rules_plays" diff --git a/src/modules/account/account-settings-console.tsx b/src/modules/account/account-settings-console.tsx index 8560e07..17f834e 100644 --- a/src/modules/account/account-settings-console.tsx +++ b/src/modules/account/account-settings-console.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { Loader2 } from "lucide-react"; import { putAdminUser } from "@/api/admin-users"; +import { validateAdminPassword } from "@/lib/admin-input-validation"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -62,6 +63,10 @@ export function AccountSettingsConsole() { toast.error(t("validation.passwordMismatch")); return; } + if (validateAdminPassword(password) === "too_short") { + toast.error(t("validation.passwordMinLength", { defaultValue: "密码至少需要 6 个字符" })); + return; + } if (!adminProfile) { toast.error(t("actions.updateFailed")); return; diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx index a33be4d..bf87ea9 100644 --- a/src/modules/admin-roles/admin-roles-console.tsx +++ b/src/modules/admin-roles/admin-roles-console.tsx @@ -22,6 +22,7 @@ import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { AdminPermissionPackageSelector } from "@/components/admin/admin-permission-package-selector"; +import { permissionProfileForGroup, resolveProfileLevel } from "@/lib/admin-permission-profiles"; import { Badge } from "@/components/ui/badge"; import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; @@ -57,8 +58,26 @@ function permissionGroupLabel(key: string, fallback: string, t: (key: string) => return translated === `permissionGroups.${key}` ? fallback : translated; } -function permissionPackageLabel(key: string, fallback: string, t: (key: string, options?: { defaultValue?: string }) => string): string { - return t(`permissionLevels.${key}`, { defaultValue: fallback }); +function countEnabledAreas( + permissionSlugs: string[], + catalog: AdminPermissionCatalogData | null, + isSuperAdmin: boolean, +): number { + if (!catalog) { + return permissionSlugs.length; + } + + let count = 0; + for (const group of catalog.permission_menu_groups ?? []) { + const profile = permissionProfileForGroup(group.key); + if (profile === undefined || (profile.platformOnly && !isSuperAdmin)) { + continue; + } + if (resolveProfileLevel(profile, permissionSlugs) !== "none") { + count += 1; + } + } + return count; } export function AdminRolesConsole(): React.ReactElement { @@ -67,6 +86,7 @@ export function AdminRolesConsole(): React.ReactElement { const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const profile = useAdminProfile(); const canManageRoles = adminHasAnyPermission(profile?.permissions, [PRD_ADMIN_ROLE_MANAGE]); + const isSuperAdmin = profile?.is_super_admin === true; const exportLabels = useExportLabels("adminRoles"); const [catalog, setCatalog] = useState(null); const [roles, setRoles] = useState([]); @@ -270,11 +290,7 @@ export function AdminRolesConsole(): React.ReactElement {
-

- {t("roleListHint", { - defaultValue: "可新增自定义角色并配置权限;内置角色(超级管理员、站点管理员、代理)不可删除。", - })} -

+ {err ?

{err}

: null}
@@ -282,19 +298,19 @@ export function AdminRolesConsole(): React.ReactElement { {t("table.id", { ns: "common" })} {t("roleTable.name")} - {t("roleTable.slug")} + {isSuperAdmin ? {t("roleTable.slug")} : null} {t("roleTable.type")} {t("roleTable.status")} {t("roleTable.users")} - {t("roleTable.permissions")} + {t("roleTable.enabledAreas")} {t("roleTable.actions")} {loading && roles.length === 0 ? ( - + ) : roles.length === 0 ? ( - + ) : ( roles.map((role) => { const fixedRole = isPlatformFixedRole(role); @@ -306,7 +322,7 @@ export function AdminRolesConsole(): React.ReactElement { {role.name} - {role.slug} + {isSuperAdmin ? {role.slug} : null} {role.is_system ? ( {t("roleType.system")} @@ -320,7 +336,9 @@ export function AdminRolesConsole(): React.ReactElement { {role.user_count} - {role.permission_slugs.length} + + {countEnabledAreas(role.permission_slugs, catalog, isSuperAdmin)} + {canManageRoles ? ( - {t("rolePermissionDialog.title")} + {selectedRole + ? t("rolePermissionDialog.titleWithName", { name: selectedRole.name }) + : t("rolePermissionDialog.title")} - - {selectedRole ? selectedRole.name : null} -
permissionGroupLabel(key, fallback, t)} - resolvePackageLabel={(key, fallback) => permissionPackageLabel(key, fallback, t)} - helperText={t("rolePermissionDialog.packageHint", { - defaultValue: - "勾选左侧模块行仅授予「查看」;录入、封盘、开奖等管理操作请单独勾选「管理」。", - })} + isSuperAdmin={isSuperAdmin} emptyText={t("states.noData", { ns: "common" })} heightClassName="h-[min(56vh,520px)]" /> @@ -404,7 +417,6 @@ export function AdminRolesConsole(): React.ReactElement { title: t("confirmSaveRolePermissionsTitle"), description: t("confirmSaveRolePermissionsDescription", { name: selectedRole.name }), confirmLabel: t("confirm.confirmSave", { ns: "common" }), - confirmVariant: "destructive", onConfirm: () => saveRolePermissions(), }) } @@ -421,7 +433,6 @@ export function AdminRolesConsole(): React.ReactElement { {editingRoleId === null ? t("roleDialog.createTitle") : t("roleDialog.editTitle")} - {t("roleDialog.description")}
diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx index 332ac7e..d579369 100644 --- a/src/modules/admin-users/admin-users-console.tsx +++ b/src/modules/admin-users/admin-users-console.tsx @@ -24,6 +24,7 @@ import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { Badge } from "@/components/ui/badge"; +import { validateAdminPassword } from "@/lib/admin-input-validation"; import { resolveAdminUserStatusTone } from "@/lib/admin-status-tone"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; @@ -266,7 +267,7 @@ export function AdminUsersConsole(): React.ReactElement { toast.error(t("nicknameRequired")); return; } - if (accountMode === "edit" && formPassword !== "" && formPassword.length < 8) { + if (accountMode === "edit" && formPassword !== "" && validateAdminPassword(formPassword) === "too_short") { toast.error(t("newPasswordMin")); return; } @@ -287,7 +288,7 @@ export function AdminUsersConsole(): React.ReactElement { toast.error(t("usernameRequired")); return; } - if (formPassword.length < 8) { + if (validateAdminPassword(formPassword) === "too_short") { toast.error(t("passwordMin")); return; } @@ -404,12 +405,6 @@ export function AdminUsersConsole(): React.ReactElement { ) : null}
-
- {t("modelGuidePlatform", { - defaultValue: - "这里只管理平台账号与平台角色。代理账号请到「代理经营」中创建和维护;账号层只绑定角色,不直接分配功能权限。", - })} -
- - - {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("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)} - > - {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} -
- ); - }) - )} -
-
+
+ {canManageNode && canCreateChild ? ( +
+
+ ) : null} +
); } -function MetricCard({ - label, - value, - subtitle, - accent = false, - highlight = false, - money = true, +function DownlineTable({ + childAgents, + childCountById, + canManageNode, + canDeleteChild, + onEditChild, + onDeleteChild, + onSelectChild, }: { - label: string; - value: string; - subtitle?: string; - accent?: boolean; - highlight?: boolean; - /** 金额类指标:自适应字号 + 换行 */ - money?: boolean; + childAgents: AgentNodeRow[]; + childCountById: Map; + canManageNode: boolean; + canDeleteChild: (node: AgentNodeRow) => boolean; + onEditChild: (node: AgentNodeRow) => void; + onDeleteChild: (node: AgentNodeRow) => void; + onSelectChild: (node: AgentNodeRow) => void; }): React.ReactElement { + const { t } = useTranslation(["agents", "common"]); + return ( -
-

{label}

- {money ? ( - - {value} - - ) : ( -

- {value} -

- )} - {subtitle ?

{subtitle}

: null} +
+
+ + + + {t("agentName", { defaultValue: "名称" })} + {t("loginUsername", { defaultValue: "登录名" })} + + {t("profile.totalShareRate", { defaultValue: "占成" })} + + + {t("profile.creditLimit", { defaultValue: "授信" })} + + + {t("lineUi.availableCredit", { defaultValue: "可下发" })} + + + {t("lineUi.downlineColumns.downlineCount", { defaultValue: "下级" })} + + {t("common:status.label", { defaultValue: "状态" })} + {canManageNode ? ( + + {t("common:table.actions", { defaultValue: "操作" })} + + ) : null} + + + + {childAgents.map((child) => { + const summary = child.profile_summary; + return ( + onSelectChild(child)} + > + +
{child.name}
+
{child.code}
+
+ {child.username ?? "—"} + + {summary ? `${summary.total_share_rate ?? 0}%` : "—"} + + + {summary ? {formatCredit(summary.credit_limit)} : "—"} + + + {summary ? ( + {formatCredit(summary.available_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: t("lineUi.deleteDownline", { defaultValue: "删除" }), + icon: Trash2, + destructive: true, + disabled: !canDeleteChild(child), + onClick: () => onDeleteChild(child), + }, + ]} + /> + + ) : null} +
+ ); + })} +
+
+
); -} +} \ No newline at end of file diff --git a/src/modules/agents/agent-line-provision-wizard.tsx b/src/modules/agents/agent-line-provision-wizard.tsx index 1f34162..c39b626 100644 --- a/src/modules/agents/agent-line-provision-wizard.tsx +++ b/src/modules/agents/agent-line-provision-wizard.tsx @@ -22,6 +22,7 @@ import { Switch } from "@/components/ui/switch"; import { useAsyncEffect } from "@/hooks/use-async-effect"; import { adminSiteCodeLabel } from "@/lib/admin-select-display"; import { + ADMIN_PASSWORD_MIN_LENGTH, validateAdminLoginAccount, validateAdminPassword, } from "@/lib/admin-input-validation"; @@ -75,8 +76,9 @@ export function AgentLineProvisionWizard({ return; } const normalized = defaultSiteCode.trim().toLowerCase(); - if (unboundSites.some((row) => row.code.toLowerCase() === normalized)) { - setForm((f) => ({ ...f, site_code: normalized })); + const matched = unboundSites.find((row) => row.code.toLowerCase() === normalized); + if (matched) { + setForm((f) => ({ ...f, site_code: matched.code })); } }, [defaultSiteCode, form.site_code, sitesLoading, unboundSites]); @@ -109,7 +111,7 @@ export function AgentLineProvisionWizard({ } const passwordIssue = validateAdminPassword(form.password); if (passwordIssue === "too_short") { - toast.error(t("agents:passwordMinLength", { defaultValue: "密码至少 8 位" })); + toast.error(t("agents:passwordMinLength", { defaultValue: "密码至少 6 位" })); return; } @@ -282,10 +284,13 @@ export function AgentLineProvisionWizard({ value={form.password} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} required - minLength={8} + minLength={ADMIN_PASSWORD_MIN_LENGTH} />

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

diff --git a/src/modules/agents/agent-line-sidebar.tsx b/src/modules/agents/agent-line-sidebar.tsx index aa73e51..412e482 100644 --- a/src/modules/agents/agent-line-sidebar.tsx +++ b/src/modules/agents/agent-line-sidebar.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { AdminLoadingInline } from "@/components/admin/admin-loading-state"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { formatAdminCreditMajorDecimal } from "@/lib/money"; @@ -56,6 +57,36 @@ function pruneTreeForSearch( return out; } +function collectSearchExpandIds( + nodes: AgentNodeRow[], + normalized: string, + parentNameMap: Map, +): Set { + const ids = new Set(); + + const walk = (list: AgentNodeRow[], ancestors: number[]): void => { + for (const node of list) { + const children = node.children ?? []; + const prunedChildren = pruneTreeForSearch(children, normalized, parentNameMap); + const selfMatch = nodeMatchesKeyword(node, normalized, parentNameMap); + + if (selfMatch || prunedChildren.length > 0) { + for (const id of ancestors) { + ids.add(id); + } + if (prunedChildren.length > 0) { + ids.add(node.id); + } + walk(prunedChildren, [...ancestors, node.id]); + } + } + }; + + walk(nodes, []); + + return ids; +} + export type AgentLineSidebarProps = { siteLabel: string | null; /** API 返回的嵌套树(含 children) */ @@ -67,6 +98,8 @@ export type AgentLineSidebarProps = { loading?: boolean; onKeywordChange: (value: string) => void; onSelect: (node: AgentNodeRow) => void; + errorMessage?: string | null; + onRetry?: () => void; }; type TreeRowProps = { @@ -98,7 +131,7 @@ function TreeRow({
@@ -163,6 +196,8 @@ export function AgentLineSidebar({ loading = false, onKeywordChange, onSelect, + errorMessage = null, + onRetry, }: AgentLineSidebarProps): React.ReactElement { const { t } = useTranslation(["agents", "common"]); const [expandedIds, setExpandedIds] = useState>(() => new Set()); @@ -220,6 +255,19 @@ export function AgentLineSidebar({ }); }, [selectedId, tree]); + useEffect(() => { + if (normalizedKeyword === "") { + return; + } + + const expandIds = collectSearchExpandIds(tree, normalizedKeyword, parentNameMap); + if (expandIds.size === 0) { + return; + } + + setExpandedIds((prev) => new Set([...prev, ...expandIds])); + }, [normalizedKeyword, parentNameMap, tree]); + const toggleExpand = useCallback((id: number) => { setExpandedIds((prev) => { const next = new Set(prev); @@ -236,19 +284,8 @@ export function AgentLineSidebar({ const hasAnyAgent = displayForest.length > 0; return ( -