From 85ad369cf9a06788743b11d5dbcbcd939c74755b Mon Sep 17 00:00:00 2001 From: wchino Date: Wed, 22 Jul 2026 20:53:43 +0800 Subject: [PATCH] feat(admin): enhance operations guidance and session handling --- next.config.ts | 18 +- package.json | 2 +- src/app/admin/(shell)/draws/[drawId]/page.tsx | 7 +- .../(shell)/draws/[drawId]/tickets/page.tsx | 16 ++ src/components/admin/admin-breadcrumb.tsx | 4 +- src/components/admin/admin-field-label.tsx | 87 +++++--- .../admin/admin-language-switcher.tsx | 5 +- .../admin/admin-no-resource-state.tsx | 2 +- .../admin/admin-page-guide-dialog.tsx | 82 ++++++++ src/components/admin/admin-sidebar.tsx | 14 +- src/components/admin/auth-gate.tsx | 27 +++ src/components/admin/login-form.tsx | 25 ++- src/components/providers.tsx | 64 +++++- src/hooks/use-async-effect.ts | 12 ++ src/i18n/index.ts | 1 - src/i18n/locales/en/common.json | 4 +- src/i18n/locales/en/config.json | 47 ++++- src/i18n/locales/en/draws.json | 45 ++++- src/i18n/locales/en/jackpot.json | 32 ++- src/i18n/locales/en/risk.json | 15 +- src/i18n/locales/ne/common.json | 4 +- src/i18n/locales/ne/config.json | 47 ++++- src/i18n/locales/ne/draws.json | 45 ++++- src/i18n/locales/ne/jackpot.json | 32 ++- src/i18n/locales/ne/risk.json | 15 +- src/i18n/locales/zh/common.json | 4 +- src/i18n/locales/zh/config.json | 47 ++++- src/i18n/locales/zh/draws.json | 45 ++++- src/i18n/locales/zh/jackpot.json | 32 ++- src/i18n/locales/zh/risk.json | 15 +- src/lib/admin-auth-reject.ts | 60 ++++-- src/lib/admin-http.ts | 4 +- src/lib/admin-nav-label.ts | 1 + src/lib/lottery-api-proxy.ts | 6 + src/modules/audit/audit-logs-console.tsx | 10 +- src/modules/config/config-chip-group.tsx | 4 +- .../config/doc/odds-config-doc-screen.tsx | 159 +++++++++++++-- .../config/doc/odds-config-play-nav.tsx | 22 +- .../config/doc/play-config-doc-screen.tsx | 189 +++++++++++++++--- src/modules/draws/draw-detail-console.tsx | 166 ++++++++------- src/modules/draws/draw-finance-console.tsx | 100 ++++++--- src/modules/draws/draw-results-console.tsx | 4 + src/modules/draws/draw-review-console.tsx | 9 +- src/modules/draws/draw-subnav.tsx | 9 +- src/modules/draws/draw-tickets-console.tsx | 22 ++ src/modules/jackpot/jackpot-config-screen.tsx | 55 ++++- src/modules/jackpot/jackpot-pools-console.tsx | 138 ++++++++++--- src/modules/reports/reports-console.tsx | 2 +- src/modules/risk/risk-pools-console.tsx | 17 ++ .../tickets/player-tickets-console.tsx | 27 ++- src/modules/wallet/wallet-cell-mono-id.tsx | 4 +- src/modules/wallet/wallet-console.tsx | 8 +- src/stores/admin-session.ts | 2 +- src/types/api/admin-draw-finance.ts | 4 + src/types/api/admin-settlement.ts | 2 + 55 files changed, 1477 insertions(+), 342 deletions(-) create mode 100644 src/app/admin/(shell)/draws/[drawId]/tickets/page.tsx create mode 100644 src/components/admin/admin-page-guide-dialog.tsx create mode 100644 src/modules/draws/draw-tickets-console.tsx diff --git a/next.config.ts b/next.config.ts index 35abba7..f48b639 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,7 +7,23 @@ const allowedDevOrigins = parseAllowedDevOrigins(process.env.ALLOWED_DEV_ORIGINS const nextConfig: NextConfig = { output: "standalone", ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), - reactCompiler: true, + reactCompiler: process.env.NODE_ENV !== "development", + turbopack: {}, + experimental: { + preloadEntriesOnStart: false, + webpackMemoryOptimizations: true, + }, + onDemandEntries: { + maxInactiveAge: 10_000, + pagesBufferLength: 1, + }, + webpack(config, { dev }) { + if (dev) { + config.cache = false; + } + + return config; + }, images: { unoptimized: true, }, diff --git a/package.json b/package.json index 679dc68..14f38ed 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "NEXT_DISABLE_MEM_OVERRIDE=1 NODE_OPTIONS='--max-old-space-size=3072' next dev --port 3801", + "dev": "NEXT_DISABLE_MEM_OVERRIDE=1 NODE_OPTIONS='--max-old-space-size=2048' next dev --webpack --disable-source-maps --port 3801", "build": "next build && cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && echo '✅ standalone 静态资源已复制完毕'", "start": "next start --port 3801", "lint": "eslint" diff --git a/src/app/admin/(shell)/draws/[drawId]/page.tsx b/src/app/admin/(shell)/draws/[drawId]/page.tsx index add4291..8565e05 100644 --- a/src/app/admin/(shell)/draws/[drawId]/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/page.tsx @@ -6,13 +6,10 @@ import type { Metadata } from "next"; export const metadata: Metadata = buildPageMetadata("draws", "drawDetail"); -export default async function AdminDrawDetailPage(props: { - params: Promise<{ drawId: string }>; -}) { - const { drawId } = await props.params; +export default function AdminDrawDetailPage() { return ( - + ); } diff --git a/src/app/admin/(shell)/draws/[drawId]/tickets/page.tsx b/src/app/admin/(shell)/draws/[drawId]/tickets/page.tsx new file mode 100644 index 0000000..422beeb --- /dev/null +++ b/src/app/admin/(shell)/draws/[drawId]/tickets/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; + +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { PRD_TICKETS_ACCESS_ANY } from "@/lib/admin-prd"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import { DrawTicketsConsole } from "@/modules/draws/draw-tickets-console"; + +export const metadata: Metadata = buildPageMetadata("draws", "subnav.tickets"); + +export default function AdminDrawTicketsPage(): React.ReactElement { + return ( + + + + ); +} diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index 84e0eba..33cf0d9 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -41,8 +41,8 @@ const TOP_ROUTE_LABELS: Record = { }; const AGENT_ROUTE_LABELS: Record = { - list: "agents.listTitle", - provision: "agents.subnav.provision", + list: "listTitle", + provision: "subnav.provision", "settlement-bills": "settlementCenter.title", }; diff --git a/src/components/admin/admin-field-label.tsx b/src/components/admin/admin-field-label.tsx index 837bf30..0bfdd27 100644 --- a/src/components/admin/admin-field-label.tsx +++ b/src/components/admin/admin-field-label.tsx @@ -10,6 +10,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; type AdminFieldLabelProps = { htmlFor: string; @@ -19,6 +20,64 @@ type AdminFieldLabelProps = { className?: string; }; +type AdminHelpIconProps = { + helpText: string; + helpAriaLabel: string; + helpId: string; +}; + +type AdminHelpTextProps = AdminHelpIconProps & { + children: ReactNode; + className?: string; +}; + +export function AdminHelpIcon({ + helpText, + helpAriaLabel, + helpId, +}: AdminHelpIconProps): React.ReactElement { + return ( + + + } + > + + +

{helpText}

+
+
+ ); +} + +export function AdminHelpText({ + children, + helpText, + helpAriaLabel, + helpId, + className, +}: AdminHelpTextProps): React.ReactElement { + return ( + + {children} + + + ); +} + export function AdminFieldLabel({ htmlFor, children, @@ -39,29 +98,11 @@ export function AdminFieldLabel({ - - - } - > - - -

{helpText}

-
-
+ ); } diff --git a/src/components/admin/admin-language-switcher.tsx b/src/components/admin/admin-language-switcher.tsx index a8a0207..75fbcba 100644 --- a/src/components/admin/admin-language-switcher.tsx +++ b/src/components/admin/admin-language-switcher.tsx @@ -25,7 +25,7 @@ import { cn } from "@/lib/utils"; export function AdminLanguageSwitcher() { const { i18n, t } = useTranslation("common"); // Match SSR: do not read document/localStorage until after mount. - const [locale, setLocale] = useState("en"); + const [locale, setLocale] = useState("zh"); useEffect(() => { const syncLocale = () => { @@ -44,8 +44,9 @@ export function AdminLanguageSwitcher() { applyAdminUiLocale(next); await i18n.changeLanguage(next); setLocale(next); + const nextT = i18n.getFixedT(next, "common"); toast.success( - t("language.changed", { + nextT("language.changed", { language: ADMIN_LOCALE_LABELS[next], }), ); diff --git a/src/components/admin/admin-no-resource-state.tsx b/src/components/admin/admin-no-resource-state.tsx index e5a6353..eece3ed 100644 --- a/src/components/admin/admin-no-resource-state.tsx +++ b/src/components/admin/admin-no-resource-state.tsx @@ -39,7 +39,7 @@ export function AdminNoResourceState({ src={NOTDATA_IMAGE} alt="" width={compact ? 120 : 160} - height={compact ? 120 : 160} + height={compact ? 80 : 107} style={{ width: "auto", height: "auto" }} className={cn( "h-auto w-auto object-contain self-center object-center -mt-4", diff --git a/src/components/admin/admin-page-guide-dialog.tsx b/src/components/admin/admin-page-guide-dialog.tsx new file mode 100644 index 0000000..2fa0a22 --- /dev/null +++ b/src/components/admin/admin-page-guide-dialog.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { BookOpenText } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +export type AdminPageGuideItem = { + label: string; + description: string; +}; + +export type AdminPageGuideSection = { + title: string; + description?: string; + items?: AdminPageGuideItem[]; +}; + +type AdminPageGuideDialogProps = { + triggerLabel: string; + title: string; + description: string; + sections: AdminPageGuideSection[]; +}; + +export function AdminPageGuideDialog({ + triggerLabel, + title, + description, + sections, +}: AdminPageGuideDialogProps): React.ReactElement { + return ( + + }> + + + + {title} + {description} + + +
+ {sections.map((section) => ( +
+

+ {section.title} +

+ {section.description ? ( +

+ {section.description} +

+ ) : null} + {section.items && section.items.length > 0 ? ( +
+ {section.items.map((item) => ( +
+
{item.label}
+
+ {item.description} +
+
+ ))} +
+ ) : null} +
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/admin/admin-sidebar.tsx b/src/components/admin/admin-sidebar.tsx index 50a278b..a935957 100644 --- a/src/components/admin/admin-sidebar.tsx +++ b/src/components/admin/admin-sidebar.tsx @@ -31,10 +31,10 @@ function AdminSidebarSkeleton(): ReactElement { N lotto @@ -50,6 +50,7 @@ function AdminSidebarSkeleton(): ReactElement { alt="" fill sizes="var(--sidebar-width)" + loading="eager" className="h-full w-full object-cover object-bottom" />
@@ -91,10 +92,10 @@ export function AdminAppSidebar() { N lotto
@@ -120,6 +121,7 @@ export function AdminAppSidebar() { alt="" fill sizes="var(--sidebar-width)" + loading="eager" className="h-full w-full object-cover object-bottom" />
diff --git a/src/components/admin/auth-gate.tsx b/src/components/admin/auth-gate.tsx index e04f442..0b4fc17 100644 --- a/src/components/admin/auth-gate.tsx +++ b/src/components/admin/auth-gate.tsx @@ -13,12 +13,17 @@ type ShellAuthGateProps = { type GateStatus = "pending" | "authed" | "guest"; +const ADMIN_SESSION_CHECK_INTERVAL_MS = 30_000; + /** * Shell 路由守卫:无有效 HttpOnly Cookie 会话或 `/auth/me` 校验失败时跳转登录页。 */ export function ShellAuthGate({ children }: ShellAuthGateProps) { const router = useRouter(); const bearerToken = useAdminSessionStore((s) => s.bearerToken); + const refreshAdminProfile = useAdminSessionStore( + (s) => s.refreshAdminProfile, + ); const [status, setStatus] = useState("pending"); const setShellAuthPending = useAdminSessionStore((s) => s.setShellAuthPending); @@ -63,6 +68,28 @@ export function ShellAuthGate({ children }: ShellAuthGateProps) { } }, [status, router]); + useEffect(() => { + if (status !== "authed") { + return; + } + + const timer = window.setInterval( + () => void refreshAdminProfile(), + ADMIN_SESSION_CHECK_INTERVAL_MS, + ); + const refreshWhenVisible = () => { + if (document.visibilityState === "visible") { + void refreshAdminProfile(); + } + }; + document.addEventListener("visibilitychange", refreshWhenVisible); + + return () => { + window.clearInterval(timer); + document.removeEventListener("visibilitychange", refreshWhenVisible); + }; + }, [refreshAdminProfile, status]); + if (status === "pending") { return ; } diff --git a/src/components/admin/login-form.tsx b/src/components/admin/login-form.tsx index 6b61c73..8e4dad3 100644 --- a/src/components/admin/login-form.tsx +++ b/src/components/admin/login-form.tsx @@ -76,6 +76,29 @@ export function LoginForm() { return; } + const currentUrl = new URL(window.location.href); + const sessionReason = currentUrl.searchParams.get("session"); + if (sessionReason === "replaced") { + toast.error( + tRef.current("auth.sessionReplaced", { + ns: "common", + defaultValue: + "该账号已在其他地方登录,当前会话已退出,请重新登录", + }), + ); + } else if (sessionReason === "expired") { + toast.error( + tRef.current("auth.sessionExpired", { + ns: "common", + defaultValue: "登录已失效,请重新登录", + }), + ); + } + if (sessionReason) { + currentUrl.searchParams.delete("session"); + window.history.replaceState(null, "", currentUrl); + } + setCheckingSession(false); void loadCaptcha(); } @@ -85,7 +108,7 @@ export function LoginForm() { return () => { cancelled = true; }; - }, [loadCaptcha, router]); + }, [loadCaptcha, router, tRef]); async function onSubmit(e: React.FormEvent) { e.preventDefault(); diff --git a/src/components/providers.tsx b/src/components/providers.tsx index 3c2704e..3b1f948 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -1,7 +1,8 @@ "use client"; +import { Loader2 } from "lucide-react"; import type { ReactNode } from "react"; -import { useEffect } from "react"; +import { useSyncExternalStore } from "react"; import i18n, { ensureAdminI18nReady, normalizeAdminLanguage } from "@/i18n"; import { Toaster } from "@/components/ui/sonner"; @@ -13,9 +14,15 @@ type ProvidersProps = { children: ReactNode; }; -function AdminSessionHydrator() { - useEffect(() => { - void (async () => { +const startupListeners = new Set<() => void>(); +let startupReady = false; +let startupPromise: Promise | null = null; + +function ensureAdminStartup(): void { + if (startupPromise) return; + + startupPromise = (async () => { + try { await ensureAdminI18nReady(); const locale = hydrateAdminUiLocale(); @@ -23,19 +30,56 @@ function AdminSessionHydrator() { if (locale && locale !== current) { await i18n.changeLanguage(locale); } - + } finally { useAdminSessionStore.getState().rehydrate(); - })(); - }, []); + } + })() + .catch(() => undefined) + .then(() => { + startupReady = true; + startupListeners.forEach((listener) => listener()); + }); +} - return null; +function subscribeAdminStartup(onStoreChange: () => void): () => void { + startupListeners.add(onStoreChange); + ensureAdminStartup(); + return () => startupListeners.delete(onStoreChange); +} + +function getAdminStartupSnapshot(): boolean { + return startupReady; +} + +function getAdminStartupServerSnapshot(): boolean { + return false; +} + +function AdminStartupGate({ children }: { children: ReactNode }) { + const ready = useSyncExternalStore( + subscribeAdminStartup, + getAdminStartupSnapshot, + getAdminStartupServerSnapshot, + ); + + if (!ready) { + return ( +
+ +
+ ); + } + + return children; } export function Providers({ children }: ProvidersProps) { return ( - - {children} + {children} ); diff --git a/src/hooks/use-async-effect.ts b/src/hooks/use-async-effect.ts index a4a5a76..b78c085 100644 --- a/src/hooks/use-async-effect.ts +++ b/src/hooks/use-async-effect.ts @@ -14,13 +14,25 @@ export function useAsyncEffect( useEffect(() => { let cleanup: void | (() => void); + let cancelled = false; + queueMicrotask(() => { + if (cancelled) { + return; + } + void Promise.resolve(factoryRef.current()).then((result) => { + if (cancelled) { + result?.(); + return; + } + cleanup = result; }); }); return () => { + cancelled = true; cleanup?.(); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 86c9f31..0f39a29 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -69,7 +69,6 @@ if (!i18n.isInitialized) { }, }); - syncAdminLanguage(initialLanguage); i18n.on("languageChanged", (lang) => { syncAdminLanguage(normalizeAdminLanguage(lang)); }); diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index e63a0de..cbf1ed4 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -183,6 +183,7 @@ "draws": "Draws", "rules_plays": "Play rules", "rules_odds": "Odds & rebate", + "bet_providers": "Bet providers", "rules": "Betting rules", "risk_cap": "Risk cap rules", "risk": "Risk center", @@ -212,7 +213,8 @@ "auth": { "checking": "Checking sign-in status…", "checkingShort": "Loading workspace…", - "sessionExpired": "Your session has expired. Please sign in again." + "sessionExpired": "Your session has expired. Please sign in again.", + "sessionReplaced": "This account was signed in elsewhere. Please sign in again." }, "confirm": { "cancel": "Cancel", diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index 75d0d83..9ced231 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -282,6 +282,28 @@ }, "tabs": { "all": "All" + }, + "help": { + "openGuide": "Page guide", + "guideTitle": "Odds and base rebate guide", + "guideDescription": "This page controls standard payout multipliers and the platform base rebate. It does not control jackpot bonus payouts or whether a play is available for sale.", + "aria": "View help for {{field}}", + "fields": { + "provider": "Selects the provider scope. GLOBAL is the default; a provider-specific value overrides it, while providers without an override inherit GLOBAL.", + "category": "Filters the list to 4D, 3D, 2D, and other groups. Changing this filter does not change any configuration.", + "playType": "Selects the exact play whose prize-tier odds and base rebate you are viewing or editing.", + "multiplier": "The standard winning payout multiplier. It is roughly the valid winning amount multiplied by this value, subject to the system's amount rounding rules.", + "scopeMultiplier": "Controls the standard payout multiplier when the {{scope}} tier wins. It is not the jackpot burst payout rate.", + "rebateRate": "The platform base rebate rate. Player or agent additions are configured elsewhere, and actual deduction and settlement also follow the account mode." + }, + "guide": { + "scopeTitle": "Choose the correct scope", + "scopeDescription": "Choose the provider, category, and play first. Category is only a filter; provider and play identify the odds set being viewed or edited.", + "settingsTitle": "What odds and rebate control", + "settingsDescription": "Prize-tier multipliers control standard winning payouts, while base rebate controls the platform's base betting discount. Neither is the jackpot bonus payout.", + "workflowTitle": "When changes take effect", + "workflowDescription": "Odds use a versioned workflow: create a draft, edit and save it, then publish. Saving alone does not affect players. New tickets use the published values, while existing tickets keep their saved odds and rebate snapshots." + } } }, "play": { @@ -389,6 +411,29 @@ "toggleDisable": "Disable", "toggleEnable": "Enable", "toggleInstantFailed": "Failed to apply play switch. Try again later.", + "help": { + "openGuide": "Page guide", + "guideTitle": "Play rules guide", + "guideDescription": "This page controls whether a play can be sold, the player-facing name and order, and the amount allowed on each betting line. It does not control odds or jackpot eligibility.", + "aria": "View help for {{field}}", + "fields": { + "status": "After publishing, this controls whether players can buy the play. Disabling it hides or blocks the play and rejects new bets; historical tickets are unchanged.", + "displayName": "The player-facing play name. Changes stay in the current draft until it is saved and published.", + "order": "Controls the order in player-facing lists. Lower numbers usually appear first, although fixed betting grids may keep their own layout.", + "minBet": "The minimum amount allowed for one submitted betting line. New bets below it are rejected.", + "maxBet": "The maximum amount allowed for one submitted betting line. Expanded combination bets are recalculated from their actual resulting lines.", + "batchSwitches": "Enables or disables a group of plays at once. It only changes the current draft and still requires save and publish.", + "batchGroup": "Sets all {{count}} plays in “{{group}}” to the same state in the current draft only." + }, + "guide": { + "settingsTitle": "What each setting controls", + "settingsDescription": "Status controls availability; display name and order control what players see; minimum and maximum bet define the amount accepted per betting line.", + "batchTitle": "Using batch switches", + "batchDescription": "Batch switches update many draft rows at once. They do not bypass the save and publish workflow.", + "workflowTitle": "When changes take effect", + "workflowDescription": "The active version is read-only. Create a draft, edit and save it, then publish. Saving alone does not affect players; publishing affects new bets, while historical tickets keep their saved rule snapshot." + } + }, "validation": { "displayNameRequired": "Display name is required", "minMaxInvalid": "{{playCode}}: min bet cannot exceed max bet" @@ -582,7 +627,7 @@ "autoApprove": "After cooldown ends and settlement completes, whether batches are automatically marked as approved.", "autoPayout": "After a batch is approved, whether tick automatically credits winnings to player wallets.", "autoSettlement": "When disabled, tick will not run settlement automatically and admins must trigger it manually.", - "cooldownMinutes": "How long to wait after publishing before entering settling. Use 0 to settle immediately.", + "cooldownMinutes": "How long to wait after publishing before settlement. Use 0 to settle immediately. Changes apply only to results published afterward; draws already in cooldown are not recalculated.", "manualReview": "When enabled, RNG draw results enter pending review and must be published manually in admin." }, "loadFailed": "Failed to load system settings", diff --git a/src/i18n/locales/en/draws.json b/src/i18n/locales/en/draws.json index 1891547..bd2d25e 100644 --- a/src/i18n/locales/en/draws.json +++ b/src/i18n/locales/en/draws.json @@ -78,6 +78,8 @@ "sequenceNo": "Sequence no.", "plannedDraw": "Planned draw", "coolingEndTime": "Cooling ends at", + "automaticSettlementAt": "Automatic settlement: {{time}}", + "cooldownRemaining": "Remaining {{time}}", "resultSource": "Result source", "resultSourceOptions": { "rng": "RNG auto-generated", @@ -92,6 +94,7 @@ "published": "Published" }, "currentResultVersion": "Current result version", + "resultVersion": "Result version", "settleVersion": "Settlement version", "isReopened": "Reopened", "yes": "Yes", @@ -109,8 +112,15 @@ "rngDraw": "RNG draw", "rngAutoGenerate": "Auto draw", "reopen": "Reopen", - "cooldownReopen": "Reopen in cooldown", + "cooldownReopen": "Withdraw result and reopen", + "reopenUsageHint": "Use this only when published numbers are wrong. It requires generating and publishing a new result and does not speed up settlement.", "runSettlement": "Run settlement", + "settleEarly": "Settle early", + "retrySettlement": "Retry settlement", + "settleEarlySuccess": "Remaining confirmation window skipped; settlement started", + "retrySettlementSuccess": "Settlement retry started", + "settlementManagePermissionRequired": "Only payout managers may start or retry settlement.", + "settlementUnavailable": "No manual settlement action is needed while the draw is {{status}}.", "processing": "Processing…", "actionSuccess": "{{name}} succeeded", "actionFailed": "{{name}} failed", @@ -121,7 +131,8 @@ "currentPayout": "Current payout total", "grossProfit": "Approx. gross profit", "settlementBatchList": "Settlement records", - "relatedSettlementBatches": "Settlement batches", + "relatedSettlementBatches": "Settlement processing records for this draw", + "settlementBatchesHint": "A record is normally created for each provider that has bets in this draw. Select a batch number to view its ticket details.", "noSettlementBatches": "No settlement batch records.", "ticketCount": "Tickets", "winCount": "Wins", @@ -153,7 +164,7 @@ "clear": "Clear", "saveDraft": "Save draft", "saving": "Saving…", - "pendingBatches": "Pending publish", + "pendingBatches": "Results awaiting review (not visible to players)", "noPendingBatches": "None", "batchId": "Batch", "numberCount": "Numbers", @@ -173,6 +184,8 @@ "checkBeforePublish": "Please review the numbers below before publishing", "checkBeforePublishDesc": "Confirm the numbers are correct and click Publish.", "publishedView": "View published result", + "officialResultsTitle": "Official results (visible to players)", + "officialResultsHint": "Only published, player-visible numbers appear here. Review drafts are kept separate.", "confirmPublish": "Confirm publish", "submitting": "Submitting…", "publishSuccess": "Published · {{drawNo}} · status {{status}}", @@ -181,13 +194,27 @@ "subnav": { "status": "Overview", "results": "Results", - "finance": "Finance", + "tickets": "Bets", + "finance": "Finance & settlement", "review": "Publish", "riskOccupancy": "Risk occupancy", "riskLockLogs": "Lock logs", "riskHot": "Hot numbers", "riskSoldOut": "Sold-out numbers", - "riskPools": "Risk pools" + "riskPools": "Number limits" + }, + "lifecycle": { + "currentStage": "Current stage: {{status}}", + "pending": "Betting has not opened for this draw.", + "open": "Players may place bets until the close time.", + "closing": "Betting is closed and the draw is waiting for its scheduled result time.", + "closed": "The draw is closed and waiting for result generation or entry.", + "drawing": "The system is generating the result.", + "review": "The result is under review and is not yet visible to players.", + "cooldown": "The result is published and in its verification window. It will settle automatically or a payout manager may settle early.", + "settling": "Settlement records are being processed. This page refreshes every 5 seconds.", + "settled": "Settlement and payout processing is complete.", + "cancelled": "This draw was cancelled and will not proceed to result or settlement." }, "statusOptions": { "all": "All", @@ -216,10 +243,14 @@ "cancelDrawDescription": "This draw will not be drawn. Ensure there is no outstanding bet risk.", "rngDrawTitle": "Confirm RNG draw?", "rngDrawDescription": "The system will generate draw numbers and continue the pipeline.", - "reopenTitle": "Confirm cooldown reopen?", - "reopenDescription": "Results may need re-review; displayed numbers may change.", + "reopenTitle": "Withdraw the result and reopen?", + "reopenDescription": "Use only when published numbers are wrong. The draw returns to closed and a new result must be generated, reviewed, and published. This does not speed up settlement.", "runSettlementTitle": "Confirm run settlement?", "runSettlementDescription": "A settlement batch will be created from the published result.", + "settleEarlyTitle": "Settle this draw early?", + "settleEarlyDescription": "This skips the remaining result verification window, creates settlement records immediately, and continues automatic review and payout. Confirm the official result first.", + "retrySettlementTitle": "Retry settlement?", + "retrySettlementDescription": "This safely retries settlement without duplicating an existing valid batch.", "saveManualDraftTitle": "Confirm save manual draft?", "saveManualDraftDescription": "23 numbers will be saved for review.", "publishTitle": "Confirm publish results?", diff --git a/src/i18n/locales/en/jackpot.json b/src/i18n/locales/en/jackpot.json index 1ff62a0..3b0e0a5 100644 --- a/src/i18n/locales/en/jackpot.json +++ b/src/i18n/locales/en/jackpot.json @@ -92,5 +92,35 @@ "play_combo": "Triggered by play combo", "threshold": "Threshold reached" }, - "winnerCount": "Winner count" + "winnerCount": "Winner count", + "help": { + "openGuide": "Page guide", + "guideTitle": "Jackpot page guide", + "guideDescription": "The jackpot is an extra reward pool separate from ordinary odds payouts. This page controls how each currency pool grows, when it is released, and where its records are reviewed.", + "aria": "View help for {{field}}", + "fields": { + "currentAmount": "The available balance currently accumulated for this currency. Save cannot edit it directly; qualifying contributions, jackpot payouts, or audited balance adjustments change it.", + "status": "Controls whether this currency pool participates in future activity. When disabled, new tickets do not contribute and automatic or manual bursts cannot run; the existing balance remains.", + "contributionRate": "After a successful ticket meets the jackpot minimum, its nominal bet amount is multiplied by this percentage and recorded into the pool. It is internal allocation, not an extra player charge.", + "minBetAmount": "Only decides whether a successful ticket contributes to the jackpot. It does not decide whether the play may be purchased; play betting limits are configured on Betting rules.", + "triggerThreshold": "Once the pool reaches this amount, a draw with at least one first-prize winning ticket may burst by threshold. Reaching the amount alone does not pay out without a first-prize winner.", + "payoutRate": "The percentage of the pre-burst balance released for threshold, combo-play, or manual bursts. A forced-gap burst releases the full pool instead.", + "forceTriggerGap": "After this many settled draws since the last burst, the next draw with a first-prize winner may force a burst. Set to 0 to disable this condition.", + "comboTriggerPlays": "A first-prize win on any selected play may trigger a burst before the balance reaches the threshold. This controls the trigger, not which first-prize winners share the payout.", + "balanceAdjustment": "Manually increases or decreases the current pool balance. A reason is required, every operation is logged, and a decrease cannot make the balance negative.", + "adjustmentDirection": "Choose whether this manual operation adds to or subtracts from the pool balance.", + "adjustmentAmount": "The amount to add or subtract in major currency units, not the desired balance after adjustment.", + "adjustmentReason": "Explains why the balance is being changed. At least 3 characters are required for audit and reconciliation.", + "manualBurst": "Super admin only. Select a draw with published results, a settlement batch, and first-prize winners to release the pool using the current payout rate.", + "records": "Review read-only jackpot payout and per-ticket contribution records, filterable by draw number and exportable." + }, + "guide": { + "settingsTitle": "How the pool grows and triggers", + "settingsDescription": "Successful tickets first add to the pool when eligible. During settlement, at least one first-prize winner must exist before threshold, forced-gap, or combo-play conditions are checked.", + "actionsTitle": "Manual operations and records", + "actionsDescription": "Balance adjustments and manual bursts change real pool funds immediately. Verify currency, amount, draw, and reason first.", + "effectiveTitle": "When changes take effect", + "effectiveDescription": "Jackpot settings have no draft-and-publish workflow. Save applies them immediately to later contributions and unfinished jackpot settlement. A draw already sold but not yet settled may use the new trigger settings and payout rate." + } + } } diff --git a/src/i18n/locales/en/risk.json b/src/i18n/locales/en/risk.json index 47ff770..c2673e7 100644 --- a/src/i18n/locales/en/risk.json +++ b/src/i18n/locales/en/risk.json @@ -14,7 +14,9 @@ "poolsTitle": "Risk pools", "hotPageTitle": "Hot numbers monitor", "soldOutPageTitle": "Sold-out numbers", - "allPoolsPageTitle": "All risk pools", + "allPoolsPageTitle": "Number payout limits (risk pool)", + "poolExplanationTitle": "This page shows how much potential payout each number can still accept", + "poolExplanation": "Each provider and four-digit number has a maximum potential payout limit. Bets reserve capacity using their worst-case payout. The number is sold out when capacity is insufficient and capacity is released after settlement or refund. These amounts are risk reserves, not actual payouts.", "sourceReasonOptions": { "ticket_place": "Bet placement", "ticket_rollback": "Ticket rollback", @@ -36,10 +38,13 @@ "sortRemainingAsc": "Remaining ↑", "sortNumberAsc": "Number ↑", "loadPoolsFailed": "Failed to load risk pools", - "capAmount": "Cap", - "lockedAmount": "Locked", - "remainingAmount": "Remaining", - "usageRatio": "Usage", + "capAmount": "Maximum payout limit", + "lockedAmount": "Reserved by current bets", + "remainingAmount": "Remaining payout capacity", + "usageRatio": "Usage ratio", + "emptyPools": "No number-limit records match this filter. Numbers with no reserved bet exposure are normally omitted.", + "emptyHighRiskPools": "No number currently has a usage ratio of 80% or more.", + "emptySoldOutPools": "No number is currently sold out because of insufficient capacity or manual closure.", "poolStatus": "Status", "soldOut": "Sold out", "warning": "Warning", diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index 68a973b..bfc8ca2 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -43,7 +43,8 @@ "auth": { "checking": "लगइन स्थिति जाँच हुँदैछ…", "checkingShort": "कार्यस्थान खोल्दै…", - "sessionExpired": "लगइन समाप्त भयो। कृपया पुनः लगइन गर्नुहोस्।" + "sessionExpired": "लगइन समाप्त भयो। कृपया पुनः लगइन गर्नुहोस्।", + "sessionReplaced": "यो खाता अर्को स्थानमा लगइन भएको छ। कृपया पुनः लगइन गर्नुहोस्।" }, "cancel": "रद्द गर्नुहोस्", "confirm": { @@ -198,6 +199,7 @@ "rules": "खेल नियम", "rules_odds": "बाधा र रिबेट", "rules_plays": "खेल नियम", + "bet_providers": "बेट प्रदायक", "settings": "सेटिङ", "settlement": "सेटलमेन्ट", "settlement_center": "क्रेडिट सेटलमेन्ट", diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index 8a62934..205be6f 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -282,6 +282,28 @@ }, "tabs": { "all": "सबै" + }, + "help": { + "openGuide": "पृष्ठ निर्देशिका", + "guideTitle": "अड्स र आधारभूत रिबेट निर्देशिका", + "guideDescription": "यस पृष्ठले सामान्य पुरस्कार गुणक र प्लेटफर्मको आधारभूत रिबेट नियन्त्रण गर्छ। यसले ज्याकपोटको अतिरिक्त भुक्तानी वा खेल उपलब्धता नियन्त्रण गर्दैन।", + "aria": "{{field}} को सहायता हेर्नुहोस्", + "fields": { + "provider": "अड्स लागू हुने प्रदायक दायरा छान्छ। GLOBAL पूर्वनिर्धारित हो; प्रदायक-विशेष मानले यसलाई ओभरराइड गर्छ, नभए GLOBAL नै प्रयोग हुन्छ।", + "category": "4D, 3D, 2D आदि समूह फिल्टर गर्छ। फिल्टर बदल्दा कन्फिगरेसन बदलिँदैन।", + "playType": "हेर्न वा सम्पादन गर्न चाहेको ठ्याक्कै खेल छान्छ। प्रत्येक खेलको पुरस्कार-स्तर अड्स र आधारभूत रिबेट फरक हुन सक्छ।", + "multiplier": "सामान्य विजेता भुक्तानी गुणक। रकम प्रणालीको राउन्डिङ नियमअनुसार गणना हुन्छ।", + "scopeMultiplier": "{{scope}} पुरस्कार जित्दा प्रयोग हुने सामान्य भुक्तानी गुणक नियन्त्रण गर्छ। यो ज्याकपोट burst भुक्तानी दर होइन।", + "rebateRate": "प्लेटफर्मको आधारभूत रिबेट दर। खेलाडी वा एजेन्टको थप रिबेट अन्यत्र सेट हुन्छ; वास्तविक कटौती र सेटलमेन्ट खाता मोडमा पनि निर्भर हुन्छ।" + }, + "guide": { + "scopeTitle": "सही दायरा छान्नुहोस्", + "scopeDescription": "पहिले प्रदायक, श्रेणी र खेल छान्नुहोस्। श्रेणी केवल फिल्टर हो; प्रदायक र खेलले कुन अड्स सेट हेरिँदै वा सम्पादन हुँदैछ भन्ने तय गर्छ।", + "settingsTitle": "अड्स र रिबेटले के नियन्त्रण गर्छ", + "settingsDescription": "पुरस्कार-स्तर गुणकले सामान्य विजेता भुक्तानी र आधारभूत रिबेटले प्लेटफर्मको आधार छुट नियन्त्रण गर्छ। यी ज्याकपोट अतिरिक्त भुक्तानी होइनन्।", + "workflowTitle": "परिवर्तन कहिले लागू हुन्छ", + "workflowDescription": "ड्राफ्ट बनाउनुहोस्, सम्पादन गरी सेभ गर्नुहोस्, अनि प्रकाशित गर्नुहोस्। सेभ मात्र गर्दा खेलाडीमा असर पर्दैन। नयाँ टिकटले प्रकाशित मान प्रयोग गर्छ; पुराना टिकटले सुरक्षित अड्स र रिबेट snapshot राख्छन्।" + } } }, "play": { @@ -389,6 +411,29 @@ "toggleDisable": "निष्क्रिय", "toggleEnable": "सक्रिय", "toggleInstantFailed": "खेल स्विच तुरुन्त लागू गर्न असफल। पछि पुनः प्रयास गर्नुहोस्।", + "help": { + "openGuide": "पृष्ठ निर्देशिका", + "guideTitle": "खेल नियम निर्देशिका", + "guideDescription": "यस पृष्ठले खेल बेच्न मिल्ने वा नमिल्ने, खेलाडीले देख्ने नाम र क्रम, तथा प्रत्येक बेट लाइनको रकम सीमा नियन्त्रण गर्छ। यसले अड्स वा ज्याकपोट योग्यता नियन्त्रण गर्दैन।", + "aria": "{{field}} को सहायता हेर्नुहोस्", + "fields": { + "status": "प्रकाशनपछि खेल किन्न मिल्ने वा नमिल्ने नियन्त्रण गर्छ। बन्द गर्दा नयाँ बेट अस्वीकार हुन्छ; पुराना टिकट बदलिँदैनन्।", + "displayName": "खेलाडीले देख्ने खेल नाम। परिवर्तन सेभ र प्रकाशित नभएसम्म हालको ड्राफ्टमै रहन्छ।", + "order": "खेलाडी सूचीमा देखिने क्रम नियन्त्रण गर्छ। सानो संख्या सामान्यतया पहिले देखिन्छ; स्थिर बेटिङ ग्रिडले आफ्नै लेआउट राख्न सक्छ।", + "minBet": "एउटा बेट लाइनमा स्वीकार हुने न्यूनतम रकम। यसभन्दा कम नयाँ बेट अस्वीकार हुन्छ।", + "maxBet": "एउटा बेट लाइनमा स्वीकार हुने अधिकतम रकम। संयोजन बेट विस्तार भएपछि वास्तविक लाइनअनुसार फेरि गणना हुन्छ।", + "batchSwitches": "एउटै समूहका धेरै खेल एकैपटक खोल्छ वा बन्द गर्छ। यसले हालको ड्राफ्ट मात्र बदल्छ र अझै सेभ तथा प्रकाशन चाहिन्छ।", + "batchGroup": "“{{group}}” का सबै {{count}} खेललाई हालको ड्राफ्टमा मात्र एउटै अवस्थामा राख्छ।" + }, + "guide": { + "settingsTitle": "प्रत्येक सेटिङले के नियन्त्रण गर्छ", + "settingsDescription": "स्थितिले उपलब्धता, नाम र क्रमले खेलाडीले देख्ने सामग्री, तथा न्यूनतम र अधिकतम बेटले प्रत्येक लाइनको स्वीकार्य रकम नियन्त्रण गर्छ।", + "batchTitle": "समूह स्विचको प्रयोग", + "batchDescription": "समूह स्विचले धेरै ड्राफ्ट पङ्क्ति एकैपटक बदल्छ। यसले सेभ र प्रकाशन प्रक्रिया छोड्दैन।", + "workflowTitle": "परिवर्तन कहिले लागू हुन्छ", + "workflowDescription": "सक्रिय संस्करण पढ्न मात्र हो। ड्राफ्ट बनाएर सम्पादन र सेभ गर्नुहोस्, अनि प्रकाशित गर्नुहोस्। सेभ मात्र गर्दा असर पर्दैन; प्रकाशनले नयाँ बेटमा असर गर्छ, पुराना टिकटले सुरक्षित नियम snapshot राख्छन्।" + } + }, "validation": { "displayNameRequired": "प्रदर्शित नाम अनिवार्य छ", "minMaxInvalid": "{{playCode}}: न्यूनतम बेट अधिकतम बेटभन्दा ठूलो हुन सक्दैन" @@ -581,7 +626,7 @@ "autoApprove": "कूलडाउन सकिएर सेटलमेन्ट पूरा भएपछि ब्याच स्वतः अनुमोदित हुने हो कि होइन।", "autoPayout": "ब्याच अनुमोदित भएपछि tick ले जित रकम खेलाडीको वालेटमा स्वतः जम्मा गर्ने हो कि होइन।", "autoSettlement": "बन्द हुँदा tick ले सेटलमेन्ट स्वतः चलाउँदैन र एडमिनले म्यानुअल रूपमा ट्रिगर गर्नुपर्छ।", - "cooldownMinutes": "प्रकाशनपछि settling मा जानुअघि कति समय पर्खने। 0 राखे तुरुन्त सेटलमेन्ट सुरु हुन्छ।", + "cooldownMinutes": "प्रकाशनपछि सेटलमेन्ट सुरु हुनुअघि कति समय पर्खने। 0 राखे तुरुन्त सुरु हुन्छ। परिवर्तनपछि प्रकाशित हुने ड्रमा मात्र लागू हुन्छ; पहिले नै कूलडाउनमा रहेका ड्र पुनः गणना हुँदैनन्।", "manualReview": "सक्रिय हुँदा RNG ड्रअ परिणाम pending review मा जान्छ र एडमिनबाट म्यानुअल रूपमा प्रकाशित गर्नुपर्छ।" }, "loadFailed": "प्रणाली सेटिङ लोड असफल भयो", diff --git a/src/i18n/locales/ne/draws.json b/src/i18n/locales/ne/draws.json index 7ed6456..7c33417 100644 --- a/src/i18n/locales/ne/draws.json +++ b/src/i18n/locales/ne/draws.json @@ -52,19 +52,26 @@ "manualCloseTitle": "म्यानुअल बन्द पुष्टि?", "publishDescription": "खेलाडीहरूले नतिजा देख्नेछन्।", "publishTitle": "नतिजा प्रकाशन पुष्टि?", - "reopenDescription": "नतिजा पुनः समीक्षा हुन सक्छ।", - "reopenTitle": "कुलडाउन पुनः खोल्ने पुष्टि?", + "reopenDescription": "प्रकाशित नम्बर गलत हुँदा मात्र प्रयोग गर्नुहोस्। ड्र फेरि बन्द अवस्थामा फर्किन्छ र नयाँ नतिजा सिर्जना, समीक्षा र प्रकाशित गर्नुपर्छ। यसले सेटलमेन्ट छिटो गर्दैन।", + "reopenTitle": "नतिजा फिर्ता लिएर पुनः खोल्ने?", "rngDrawDescription": "प्रणालीले नतिजा सिर्जना गर्नेछ।", "rngDrawTitle": "RNG ड्रअ पुष्टि?", "runSettlementDescription": "प्रकाशित नतिजाबाट सेटलमेन्ट ब्याच बन्नेछ।", "runSettlementTitle": "सेटलमेन्ट सुरु पुष्टि?", + "settleEarlyDescription": "बाँकी नतिजा जाँच समय छोडेर तुरुन्त सेटलमेन्ट र स्वचालित भुक्तानी प्रक्रिया सुरु हुनेछ। पहिले आधिकारिक नतिजा सही भएको पुष्टि गर्नुहोस्।", + "settleEarlyTitle": "अगावै सेटल गर्ने?", + "retrySettlementDescription": "अवस्थित वैध ब्याच नदोहोर्याई सेटलमेन्ट सुरक्षित रूपमा पुनः प्रयास हुनेछ।", + "retrySettlementTitle": "सेटलमेन्ट पुनः प्रयास गर्ने?", "saveManualDraftDescription": "२३ नम्बर समीक्षाका लागि सुरक्षित हुनेछ।", "saveManualDraftTitle": "म्यानुअल ड्राफ्ट सुरक्षित पुष्टि?" }, "confirmPublish": "प्रकाशन पुष्टि गर्नुहोस्", "confirmedAt": "पुष्टि समय {{time}}", - "cooldownReopen": "कुलिङमा पुनःखोल्नुहोस्", + "cooldownReopen": "नतिजा फिर्ता लिएर पुनः खोल्नुहोस्", + "reopenUsageHint": "प्रकाशित नम्बर गलत हुँदा मात्र प्रयोग गर्नुहोस्। यसले नयाँ नतिजा पुनः प्रकाशित गर्नुपर्छ र सेटलमेन्ट छिटो गर्दैन।", "coolingEndTime": "कुलिङ समाप्ति", + "automaticSettlementAt": "स्वचालित सेटलमेन्ट: {{time}}", + "cooldownRemaining": "बाँकी {{time}}", "createDraw": { "description": "स्थानीय समयक्षेत्र {{tz}} मा मिति र समय प्रविष्ट गर्नुहोस्।", "drawNoPlaceholder": "ड्रअ नम्बर प्रविष्ट गर्नुहोस्, जस्तै 20260526-008", @@ -79,6 +86,7 @@ }, "currentPayout": "हालको कुल भुक्तानी", "currentResultVersion": "हालको नतिजा संस्करण", + "resultVersion": "नतिजा संस्करण", "currentStatusAndDraft": "हालको स्थिति {{status}} · सेभ गरेपछि pending batch बन्छ, सिधै प्रकाशित हुँदैन", "currentStatusDraftHint": "सेभ गरेपछि समीक्षा बाँकी ब्याच बन्छ, सिधै प्रकाशित हुँदैन", "currentStatusLabel": "हालको स्थिति", @@ -148,7 +156,7 @@ "overviewTitle": "ड्र अवलोकन", "pageGuide": "ड्र जीवनचक्र व्यवस्थापन: योजना, बन्द, नतिजा समीक्षा र सेटलमेन्ट।", "payoutTotal": "कुल भुक्तानी", - "pendingBatches": "बाँकी ब्याच", + "pendingBatches": "समीक्षाको प्रतीक्षामा नतिजा (खेलाडीलाई नदेखिने)", "pendingReview": "समीक्षा बाँकी", "plannedDraw": "योजनाबद्ध ड्रअ", "prize": "पुरस्कार", @@ -160,8 +168,11 @@ "publishTitle": "प्रकाशित", "published": "प्रकाशित", "publishedView": "प्रकाशित नतिजा हेर्नुहोस्", + "officialResultsTitle": "आधिकारिक नतिजा (खेलाडीलाई देखिने)", + "officialResultsHint": "यहाँ प्रकाशित र खेलाडीलाई लागू भएका नम्बर मात्र देखिन्छन्; समीक्षा ड्राफ्ट अलग राखिन्छ।", "queryDraw": "ड्रअ खोज्नुहोस्", - "relatedSettlementBatches": "सम्बन्धित सेटलमेन्ट ब्याच", + "relatedSettlementBatches": "यस ड्रको सेटलमेन्ट प्रक्रिया अभिलेख", + "settlementBatchesHint": "सामान्यतया यस ड्रमा बाजी भएको प्रत्येक प्रदायकका लागि अभिलेख बन्छ। टिकट विवरण हेर्न ब्याच नम्बर छान्नुहोस्।", "reopen": "पुनःखोल्नुहोस्", "reset": "रिसेट", "resultBatchesTitle": "नतिजा ब्याच", @@ -190,6 +201,12 @@ "rngDraw": "RNG ड्रअ", "rngSummary": "RNG ह्यास {{hash}}", "runSettlement": "सेटलमेन्ट चलाउनुहोस्", + "settleEarly": "अगावै सेटल गर्नुहोस्", + "retrySettlement": "सेटलमेन्ट पुनः चलाउनुहोस्", + "settleEarlySuccess": "बाँकी पुष्टि समय छोडेर सेटलमेन्ट सुरु भयो", + "retrySettlementSuccess": "सेटलमेन्ट पुनः प्रयास सुरु भयो", + "settlementManagePermissionRequired": "भुक्तानी व्यवस्थापकले मात्र सेटलमेन्ट सुरु वा पुनः प्रयास गर्न सक्छ।", + "settlementUnavailable": "ड्र {{status}} अवस्थामा हुँदा म्यानुअल सेटलमेन्ट आवश्यक छैन।", "saveDraft": "ड्राफ्ट सुरक्षित गर्नुहोस्", "saveFailed": "सेभ असफल भयो", "saving": "सेभ हुँदैछ…", @@ -218,16 +235,30 @@ }, "submitting": "पेश हुँदैछ…", "subnav": { - "finance": "ड्रअ वित्त", + "finance": "वित्त र सेटलमेन्ट", "results": "परिणाम", + "tickets": "बाजी अवस्था", "review": "समीक्षा र प्रकाशन", "riskHot": "हट नम्बर", "riskLockLogs": "लक लग", "riskOccupancy": "जोखिम अकुपेन्सी", - "riskPools": "जोखिम पूल", + "riskPools": "नम्बर सीमा", "riskSoldOut": "बिक्री समाप्त नम्बर", "status": "ड्रअ स्थिति" }, + "lifecycle": { + "currentStage": "हालको चरण: {{status}}", + "pending": "यस ड्रका लागि बाजी खुलिसकेको छैन।", + "open": "बन्द समयसम्म खेलाडीले बाजी लगाउन सक्छन्।", + "closing": "बाजी बन्द छ र निर्धारित नतिजा समय पर्खिरहेको छ।", + "closed": "ड्र बन्द छ र नतिजा सिर्जना वा प्रविष्टि पर्खिरहेको छ।", + "drawing": "प्रणालीले नतिजा सिर्जना गर्दैछ।", + "review": "नतिजा समीक्षामा छ र खेलाडीलाई अझै लागू भएको छैन।", + "cooldown": "नतिजा प्रकाशित छ र जाँच अवधिमा छ। समय पुगेपछि स्वचालित सेटल हुनेछ वा व्यवस्थापकले अगावै सेटल गर्न सक्छ।", + "settling": "सेटलमेन्ट अभिलेख प्रशोधन हुँदैछ। पृष्ठ प्रत्येक ५ सेकेन्डमा अद्यावधिक हुन्छ।", + "settled": "सेटलमेन्ट र भुक्तानी प्रक्रिया पूरा भयो।", + "cancelled": "यो ड्र रद्द भयो र नतिजा वा सेटलमेन्टमा जाने छैन।" + }, "tail2": "अन्तिम 2", "tail3": "अन्तिम 3", "ticketCount": "टिकट", diff --git a/src/i18n/locales/ne/jackpot.json b/src/i18n/locales/ne/jackpot.json index 46a295e..19ca361 100644 --- a/src/i18n/locales/ne/jackpot.json +++ b/src/i18n/locales/ne/jackpot.json @@ -92,5 +92,35 @@ "tabConfig": "कन्फिगरेसन", "tabRecords": "रेकर्ड", "confirmSavePoolTitle": "ज्याकपट सेटिङ बचत गर्ने?", - "confirmSavePoolDescription": "यसले संचय दर, थ्रेसहोल्ड, भुक्तानी अनुपात र सम्बन्धित प्यारामिटरहरू अद्यावधिक गर्नेछ (पूल ब्यालेन्स होइन)।" + "confirmSavePoolDescription": "यसले संचय दर, थ्रेसहोल्ड, भुक्तानी अनुपात र सम्बन्धित प्यारामिटरहरू अद्यावधिक गर्नेछ (पूल ब्यालेन्स होइन)।", + "help": { + "openGuide": "पृष्ठ जानकारी", + "guideTitle": "ज्याकपट पृष्ठ जानकारी", + "guideDescription": "ज्याकपट सामान्य odds भुक्तानीभन्दा अलग अतिरिक्त पुरस्कार पूल हो। यस पृष्ठले मुद्रा अनुसार पूल कसरी बढ्छ, कहिले रिलिज हुन्छ र रेकर्ड कहाँ हेर्ने भन्ने नियन्त्रण गर्छ।", + "aria": "{{field}} को जानकारी हेर्नुहोस्", + "fields": { + "currentAmount": "यस मुद्राको पूलमा अहिले जम्मा भएको उपलब्ध ब्यालेन्स। साधारण Save बाट सिधै बदलिँदैन; योग्य योगदान, ज्याकपट भुक्तानी वा कारणसहितको समायोजनले बदल्छ।", + "status": "यस मुद्राको पूल भविष्यका कारोबारमा चल्ने वा नचल्ने नियन्त्रण गर्छ। बन्द हुँदा नयाँ टिकटले योगदान गर्दैन र स्वतः वा म्यानुअल बर्स्ट चल्दैन; पुरानो ब्यालेन्स रहन्छ।", + "contributionRate": "सफल टिकटले न्यूनतम रकम पुगेपछि नाममात्र बेट रकमलाई यो प्रतिशतले गुणा गरी पूलमा लेखिन्छ। यो आन्तरिक बाँडफाँट हो, खेलाडीबाट थप शुल्क होइन।", + "minBetAmount": "सफल टिकटले ज्याकपटमा योगदान गर्ने वा नगर्ने मात्र निर्णय गर्छ। प्ले किन्न मिल्ने न्यूनतम वा अधिकतम रकम Betting rules मा सेट हुन्छ।", + "triggerThreshold": "पूल यो रकममा पुगेपछि र सो ड्रअमा प्रथम पुरस्कार विजेता भए threshold बाट बर्स्ट हुन सक्छ। विजेता नभए रकम पुगेको मात्र कारणले भुक्तानी हुँदैन।", + "payoutRate": "Threshold, combo-play वा manual burst हुँदा बर्स्टअघिको ब्यालेन्सबाट रिलिज हुने प्रतिशत। Forced-gap burst ले पूरा पूल रिलिज गर्छ।", + "forceTriggerGap": "पछिल्लो बर्स्टपछि यति settled draws पुगेपछि, अर्को प्रथम पुरस्कार विजेता भएको ड्रअमा forced burst हुन सक्छ। 0 राख्दा यो सर्त बन्द हुन्छ।", + "comboTriggerPlays": "छानिएको प्लेमा प्रथम पुरस्कार परे threshold नपुगे पनि बर्स्ट हुन सक्छ। यसले ट्रिगर मात्र निर्धारण गर्छ, भुक्तानी पाउने विजेता सीमित गर्दैन।", + "balanceAdjustment": "हालको पूल ब्यालेन्स म्यानुअल रूपमा बढाउने वा घटाउने। कारण अनिवार्य हुन्छ, सबै काम रेकर्ड हुन्छ र ब्यालेन्स 0 भन्दा तल जान सक्दैन।", + "adjustmentDirection": "यो समायोजनले पूल ब्यालेन्स बढाउने वा घटाउने छान्नुहोस्।", + "adjustmentAmount": "मुख्य मुद्रा एकाइमा थप्ने वा घटाउने रकम; समायोजनपछि चाहिएको कुल ब्यालेन्स होइन।", + "adjustmentReason": "ब्यालेन्स किन बदलिएको हो भन्ने विवरण। Audit र reconciliation का लागि कम्तीमा 3 अक्षर चाहिन्छ।", + "manualBurst": "सुपर एडमिन मात्र। प्रकाशित नतिजा, settlement batch र प्रथम पुरस्कार विजेता भएको ड्रअ छानेर हालको payout rate अनुसार पूल रिलिज गर्छ।", + "records": "पढ्न मात्र मिल्ने jackpot payout र प्रत्येक टिकटको contribution रेकर्ड draw number अनुसार फिल्टर र export गर्नुहोस्।" + }, + "guide": { + "settingsTitle": "पूल कसरी बढ्छ र ट्रिगर हुन्छ", + "settingsDescription": "योग्य सफल टिकटले पहिले पूल बढाउँछ। Settlement मा threshold, forced-gap वा combo-play जाँच्नुअघि कम्तीमा एक प्रथम पुरस्कार विजेता हुनुपर्छ।", + "actionsTitle": "म्यानुअल काम र रेकर्ड", + "actionsDescription": "Balance adjustment र manual burst ले वास्तविक पूल रकम तुरुन्त बदल्छ। पहिले मुद्रा, रकम, ड्रअ र कारण जाँच गर्नुहोस्।", + "effectiveTitle": "परिवर्तन कहिले लागू हुन्छ", + "effectiveDescription": "Jackpot सेटिङमा draft र publish प्रक्रिया छैन। Save गरेपछि पछिल्ला contribution र नसकिएको jackpot settlement मा तुरुन्त लागू हुन्छ। बेचिसकिएको तर settle नभएको ड्रअले पनि नयाँ trigger र payout rate प्रयोग गर्न सक्छ।" + } + } } diff --git a/src/i18n/locales/ne/risk.json b/src/i18n/locales/ne/risk.json index cc04a45..85f85b7 100644 --- a/src/i18n/locales/ne/risk.json +++ b/src/i18n/locales/ne/risk.json @@ -4,12 +4,14 @@ "actionFilter": "कार्य", "actions": "कार्य", "all": "सबै", - "allPoolsPageTitle": "सबै जोखिम पूल", + "allPoolsPageTitle": "नम्बर भुक्तानी सीमा (जोखिम पूल)", + "poolExplanationTitle": "हरेक नम्बरले अझै कति सम्भावित भुक्तानी स्वीकार्न सक्छ भन्ने यहाँ देखिन्छ", + "poolExplanation": "हरेक प्रदायक र चार-अङ्क नम्बरको अधिकतम सम्भावित भुक्तानी सीमा हुन्छ। बाजीले सबैभन्दा खराब सम्भावित भुक्तानी अनुसार क्षमता सुरक्षित गर्छ। क्षमता अपुग हुँदा नम्बर बिक्री बन्द हुन्छ र सेटलमेन्ट वा फिर्तापछि क्षमता खुल्छ। यो जोखिम आरक्षण हो, वास्तविक भुक्तानी होइन।", "amount": "रकम", "applyFilter": "फिल्टर लागू गर्नुहोस्", "backToAllPools": "सबै जोखिम पूलमा फर्कनुहोस्", "backToList": "सूचीमा फर्कनुहोस्", - "capAmount": "क्याप", + "capAmount": "अधिकतम भुक्तानी सीमा", "center": "जोखिम केन्द्र", "changeDraw": "ड्रअ परिवर्तन गर्नुहोस्", "close": "बन्द", @@ -48,7 +50,7 @@ "lockLogsGroupedHint": "पूर्वनिर्धारित रूपमा टिकट अनुसार समूह। विस्तारित प्लेको संयोजन टिकट विवरणमा हेर्नुहोस्।", "lockLogsTitle": "जोखिम लक लग", "lockReleaseSummary": "लक / रिलिज पङ्क्ति", - "lockedAmount": "लक गरिएको", + "lockedAmount": "हालको बाजीले सुरक्षित गरेको", "lockedWorstCase": "लक गरिएको (अधिकतम भुक्तानी सुरक्षित)", "manualCloseSuccess": "नम्बर बेटिङ म्यानुअल रूपमा बन्द गरियो", "no": "होइन", @@ -66,7 +68,10 @@ "recoverSuccess": "नम्बर बेटिङ पुनर्स्थापित गरियो", "refresh": "रिफ्रेस", "release": "रिलिज", - "remainingAmount": "बाँकी", + "remainingAmount": "बाँकी भुक्तानी क्षमता", + "emptyPools": "यो फिल्टरमा नम्बर सीमा अभिलेख छैन। बाजी जोखिम सुरक्षित नभएका नम्बर सामान्यतया देखिँदैनन्।", + "emptyHighRiskPools": "हाल ८०% वा बढी प्रयोग भएको उच्च जोखिम नम्बर छैन।", + "emptySoldOutPools": "क्षमता अपुग वा म्यानुअल बन्दका कारण बिक्री रोकिएको नम्बर छैन।", "remainingSellable": "बाँकी बिक्रीयोग्य", "riskFilter": "जोखिम फिल्टर", "provider": "प्रोभाइडर", @@ -109,7 +114,7 @@ "time": "समय", "title": "जोखिम", "totalCap": "क्याप रकम", - "usageRatio": "प्रयोग अनुपात", + "usageRatio": "अकुपेन्सी अनुपात", "view": "हेर्नुहोस्", "viewDetail": "विवरण", "viewTicket": "टिकट", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 9eec73a..25cfd7f 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -183,6 +183,7 @@ "draws": "期号列表", "rules_plays": "投注规则", "rules_odds": "赔率与基础回水", + "bet_providers": "开注商管理", "rules": "投注规则", "risk_cap": "限额版本", "risk": "风控中心", @@ -212,7 +213,8 @@ "auth": { "checking": "正在校验登录状态…", "checkingShort": "正在进入工作台…", - "sessionExpired": "登录已失效,请重新登录" + "sessionExpired": "登录已失效,请重新登录", + "sessionReplaced": "该账号已在其他地方登录,当前会话已退出,请重新登录" }, "confirm": { "cancel": "取消", diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index 87fbbb2..50053be 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -304,7 +304,7 @@ }, "hints": { "manualReview": "开启后,RNG 开奖结果会先进入待审核,必须由后台人工发布。", - "cooldownMinutes": "结果发布后等待多久再进入 settling。填 0 表示发布后直接进入结算。", + "cooldownMinutes": "结果发布后等待多久再进入结算;填 0 表示立即结算。修改仅影响之后发布开奖结果的期号,已经进入冷静期的期号不会自动重算。", "autoSettlement": "关闭后,tick 不会自动跑结算,只能由后台手工执行。", "autoApprove": "冷静期结束并跑完结算后,是否自动将批次标记为已审核。", "autoPayout": "批次已审核后,是否由 tick 自动把中奖金额打入玩家钱包。", @@ -487,6 +487,29 @@ "title": "规则文案(多语言)", "description": "玩法 {{playCode}};修改内容只会暂存到草稿,保存并发布后才会生效。", "apply": "应用到草稿" + }, + "help": { + "openGuide": "页面说明", + "guideTitle": "投注规则页面说明", + "guideDescription": "这个页面决定玩家能不能购买某个玩法、玩家看到的玩法名称和顺序,以及每一条下注允许输入的金额范围。它不控制赔率,也不控制奖池资格。", + "aria": "查看“{{field}}”的说明", + "fields": { + "status": "发布后控制玩家是否可以购买这个玩法。关闭后,前台会隐藏或禁用该玩法,后端也会拒绝新的下注;历史注单不受影响。", + "displayName": "玩家端展示的玩法名称。修改只进入当前草稿,保存并发布后才会展示给玩家。", + "order": "控制玩法在玩家端列表中的显示先后。数字越小通常越靠前;部分固定式投注表可能仍按自己的版面排列。", + "minBet": "玩家一次提交的一条玩法下注,允许输入的最小金额。低于该值时,新下注会被拒绝。", + "maxBet": "玩家一次提交的一条玩法下注,允许输入的最大金额。组合下注拆成多条后,系统会按实际展开结果重新计算。", + "batchSwitches": "按玩法分组一次开启或关闭多项玩法。操作只修改当前草稿,仍需保存并发布后才会对玩家生效。", + "batchGroup": "一次将“{{group}}”下 {{count}} 个玩法切换为同一状态;只修改当前草稿。" + }, + "guide": { + "settingsTitle": "每个设置控制什么", + "settingsDescription": "状态控制是否可售;显示名称和排序控制玩家看到的内容;最小与最大下注控制每条下注可提交的金额范围。", + "batchTitle": "批量开关怎么用", + "batchDescription": "批量开关适合一次调整整组玩法。它只是帮你修改草稿里的多行数据,不会绕过保存和发布流程。", + "workflowTitle": "什么时候真正生效", + "workflowDescription": "线上生效版本只能查看。要修改时先创建草稿,在草稿中编辑并保存,最后发布。仅保存不会影响玩家;发布后只影响新的下注,历史注单继续使用下单时保存的规则快照。" + } } }, "prizeScopes": { @@ -574,6 +597,28 @@ "currentActive": "当前生效", "afterPublish": "发布后" } + }, + "help": { + "openGuide": "页面说明", + "guideTitle": "赔率与基础回水页面说明", + "guideDescription": "这个页面设置普通中奖赔付倍数和平台基础回水。它不控制奖池额外派彩,也不决定玩法是否开放。", + "aria": "查看“{{field}}”的说明", + "fields": { + "provider": "选择赔率适用的供应商范围。GLOBAL 是通用默认值;供应商专属配置会覆盖通用值,没有专属配置时则继承 GLOBAL。", + "category": "只用来筛选 4D、3D、2D 等玩法范围,方便查找;切换分类本身不会修改配置。", + "playType": "选择当前要查看或修改的具体玩法。每个玩法可以有不同奖级赔率和基础回水。", + "multiplier": "普通中奖的赔付倍数。大致可理解为有效中奖金额乘以该倍数,最终金额仍按系统精度规则取整。", + "scopeMultiplier": "控制“{{scope}}”这个奖级中奖时使用的普通赔付倍数。它不是奖池爆池的额外派彩比例。", + "rebateRate": "平台基础回水比例。玩家或代理的额外回水会在其他配置中叠加;实际扣款和结算还会遵循当前账户模式。" + }, + "guide": { + "scopeTitle": "先选对配置范围", + "scopeDescription": "先选择供应商、分类和玩法。分类只是筛选条件;供应商和玩法共同决定你正在查看或修改哪一组赔率。", + "settingsTitle": "赔率和回水控制什么", + "settingsDescription": "各奖级倍数控制普通中奖赔付,基础回水控制平台给下注的基础优惠。两者都不等于奖池页面中的爆池派彩。", + "workflowTitle": "什么时候真正生效", + "workflowDescription": "赔率使用版本化流程:先创建草稿、修改并保存,再发布。保存草稿不会影响玩家;发布后新注单使用新配置,已经成功下单的注单继续使用下单时保存的赔率和回水快照。" + } } }, "rebate": { diff --git a/src/i18n/locales/zh/draws.json b/src/i18n/locales/zh/draws.json index 8de05e0..c3cff42 100644 --- a/src/i18n/locales/zh/draws.json +++ b/src/i18n/locales/zh/draws.json @@ -78,6 +78,8 @@ "sequenceNo": "流水序号", "plannedDraw": "计划开奖", "coolingEndTime": "冷静期结束", + "automaticSettlementAt": "自动结算时间:{{time}}", + "cooldownRemaining": "剩余 {{time}}", "resultSource": "结果来源", "resultSourceOptions": { "rng": "RNG 自动生成", @@ -92,6 +94,7 @@ "published": "已发布" }, "currentResultVersion": "当前结果版本", + "resultVersion": "开奖结果版本", "settleVersion": "结算版本", "isReopened": "是否重开", "yes": "是", @@ -109,8 +112,15 @@ "rngDraw": "RNG开奖", "rngAutoGenerate": "自动开奖", "reopen": "重开", - "cooldownReopen": "冷静期重开", + "cooldownReopen": "撤回开奖结果并重开", + "reopenUsageHint": "“撤回开奖结果并重开”仅用于已发布号码有误,需要重新生成和审核;它不会加快结算。", "runSettlement": "触发结算", + "settleEarly": "提前结算", + "retrySettlement": "重新触发结算", + "settleEarlySuccess": "已跳过剩余确认窗口并开始结算", + "retrySettlementSuccess": "已重新触发本期结算", + "settlementManagePermissionRequired": "仅派彩管理人员可提前或重新触发结算。", + "settlementUnavailable": "当前状态为“{{status}}”,无需提供手动结算操作。", "processing": "处理中…", "actionSuccess": "{{name}}成功", "actionFailed": "{{name}}失败", @@ -121,7 +131,8 @@ "currentPayout": "当期派彩合计", "grossProfit": "近似毛损益", "settlementBatchList": "结算记录", - "relatedSettlementBatches": "结算批次", + "relatedSettlementBatches": "本期结算处理记录", + "settlementBatchesHint": "通常按本期有下注的开注商生成处理记录;点击批次编号可查看对应注单明细。", "noSettlementBatches": "暂无结算批次记录。", "ticketCount": "票数", "winCount": "中奖数", @@ -153,7 +164,7 @@ "clear": "清空", "saveDraft": "保存草稿", "saving": "保存中…", - "pendingBatches": "待发布", + "pendingBatches": "待审核结果(尚未对玩家生效)", "noPendingBatches": "暂无", "batchId": "批次", "numberCount": "号码条数", @@ -173,6 +184,8 @@ "checkBeforePublish": "请核对以下号码后再发布", "checkBeforePublishDesc": "确认无误后点击发布。", "publishedView": "查看已发布展示", + "officialResultsTitle": "正式开奖结果(已对玩家显示)", + "officialResultsHint": "这里只展示已经发布并对玩家生效的号码;待审核草稿不会出现在这里。", "confirmPublish": "确认发布", "submitting": "提交中…", "publishSuccess": "已发布 · {{drawNo}} · 状态 {{status}}", @@ -181,13 +194,27 @@ "subnav": { "status": "概览", "results": "开奖结果", - "finance": "收支", + "tickets": "下注情况", + "finance": "收支结算", "review": "开奖发布", "riskOccupancy": "风控占用", "riskLockLogs": "占用流水", "riskHot": "热门号码", "riskSoldOut": "售罄号码", - "riskPools": "风险池" + "riskPools": "号码限额" + }, + "lifecycle": { + "currentStage": "当前阶段:{{status}}", + "pending": "本期尚未开放下注。", + "open": "玩家可以下注;到封盘时间后系统会停止接单。", + "closing": "本期已停止接单,等待计划开奖时间。", + "closed": "已封盘待开奖,系统将生成或等待录入开奖结果。", + "drawing": "系统正在生成开奖结果。", + "review": "开奖结果尚在审核,发布前不会对玩家生效。", + "cooldown": "结果已发布并进入核错窗口;到期会自动结算,也可由派彩管理人员提前结算。", + "settling": "正在生成或处理本期结算记录,页面每 5 秒自动更新。", + "settled": "本期结算与派彩流程已完成。", + "cancelled": "本期已取消,不再进入开奖与结算流程。" }, "statusOptions": { "all": "不限", @@ -216,10 +243,14 @@ "cancelDrawDescription": "取消后该期将不再开奖,请确认无未处理注单风险。", "rngDrawTitle": "确认 RNG 自动生成开奖?", "rngDrawDescription": "将按系统规则生成本期开奖号码并进入后续流程。", - "reopenTitle": "确认冷静期重开?", - "reopenDescription": "重开后需重新审核/发布结果,可能影响已展示的开奖信息。", + "reopenTitle": "确认撤回开奖结果并重开?", + "reopenDescription": "仅在已发布号码有误时使用。操作后期号回到已封盘待开奖,需要重新生成、审核并发布结果;这不会加快结算。", "runSettlementTitle": "确认触发结算?", "runSettlementDescription": "将按已发布开奖结果生成本期结算批次。", + "settleEarlyTitle": "确认提前结算?", + "settleEarlyDescription": "将跳过本期剩余的开奖结果核错窗口,立即生成结算处理记录,并继续自动审核与派彩。请先确认正式开奖结果无误。", + "retrySettlementTitle": "确认重新触发结算?", + "retrySettlementDescription": "将幂等重试本期结算处理,不会重复生成已存在的有效批次。", "saveManualDraftTitle": "确认保存人工开奖草稿?", "saveManualDraftDescription": "将写入 23 个开奖号码草稿,提交后进入审核流程。", "publishTitle": "确认发布开奖结果?", diff --git a/src/i18n/locales/zh/jackpot.json b/src/i18n/locales/zh/jackpot.json index 56ab751..d121286 100644 --- a/src/i18n/locales/zh/jackpot.json +++ b/src/i18n/locales/zh/jackpot.json @@ -93,5 +93,35 @@ "time": "时间", "ticketNo": "注单", "player": "玩家", - "contributionAmount": "蓄水额" + "contributionAmount": "蓄水额", + "help": { + "openGuide": "页面说明", + "guideTitle": "奖池页面说明", + "guideDescription": "奖池是普通赔率奖金之外的额外奖励资金。本页按币种控制奖池如何累积、何时释放,以及如何查看相关流水。", + "aria": "查看“{{field}}”说明", + "fields": { + "currentAmount": "该币种奖池当前累计的可用余额。它不能通过普通保存直接修改,只能由合格注单蓄水、爆池派彩或带原因的余额调整改变。", + "status": "控制该币种奖池是否参与后续业务。关闭后新成功注单不再蓄水,自动爆池与手动爆池也不会执行;现有余额会保留。", + "contributionRate": "成功下注且达到奖池最低下注额后,系统按名义下注额乘以该百分比计入奖池。这是内部计提,不会在下注额之外再向玩家多扣一笔。", + "minBetAmount": "只决定一笔成功注单是否参与奖池蓄水,不决定该玩法能否下注。真正的玩法下注上下限在“投注规则”页面配置。", + "triggerThreshold": "奖池余额达到此金额后,如果当期存在头奖中奖注单,可按“达到阈值”触发爆池。只有余额达到阈值但没有头奖得主时不会派发。", + "payoutRate": "达到阈值、组合玩法或手动爆池时,从爆池前余额中释放的百分比。连续未爆强制触发走全池释放,不使用此比例。", + "forceTriggerGap": "从上次爆池后累计的已结算期数达到此值后,下一次出现头奖得主时可强制爆池。设为 0 表示不启用此条件。", + "comboTriggerPlays": "勾选的玩法只要在某期产生头奖得主,就可在未达到余额阈值时触发爆池。它只决定触发条件,不限定最终哪些头奖得主参与分配。", + "balanceAdjustment": "人工增加或减少当前奖池余额。必须填写原因,每次操作都会写入调整流水;减少后余额不能小于 0。", + "adjustmentDirection": "选择本次人工调整是增加奖池余额还是减少奖池余额。", + "adjustmentAmount": "本次人工增加或减少的主币金额,不是调整后的目标余额。", + "adjustmentReason": "记录为什么要人工调整奖池,至少填写 3 个字符,后续可用于审计和对账。", + "manualBurst": "仅超级管理员可用。指定已发布结果且已有结算批次、存在头奖得主的期号,按当前派彩比例人工释放奖池。", + "records": "查看只读的奖池派彩记录和逐笔蓄水记录,可按期号筛选并导出。" + }, + "guide": { + "settingsTitle": "奖池如何累积与触发", + "settingsDescription": "成功注单先按蓄水条件增加奖池;开奖结算时必须先有头奖得主,再判断阈值、连续未爆或组合玩法条件。", + "actionsTitle": "人工操作与流水", + "actionsDescription": "余额调整和手动爆池都会直接改变真实奖池资金,请先确认币种、金额、期号和原因。", + "effectiveTitle": "生效方式", + "effectiveDescription": "奖池配置没有草稿和发布流程,点击保存后立即用于后续蓄水与尚未完成的奖池结算。已经卖票但尚未结算的期号,也可能受到新触发参数和派彩比例影响。" + } + } } diff --git a/src/i18n/locales/zh/risk.json b/src/i18n/locales/zh/risk.json index f392143..a8f2ffa 100644 --- a/src/i18n/locales/zh/risk.json +++ b/src/i18n/locales/zh/risk.json @@ -14,7 +14,9 @@ "poolsTitle": "风险池", "hotPageTitle": "热门号码监控", "soldOutPageTitle": "售罄号码列表", - "allPoolsPageTitle": "全部风险池", + "allPoolsPageTitle": "号码赔付限额(风险池)", + "poolExplanationTitle": "这里控制每个号码还能承接多少潜在赔付", + "poolExplanation": "每个开注商、每个四位号码都有最高潜在赔付额度。下注会按最坏赔付预留额度;额度不足时该号码停售,结算或退款后释放。这里的金额是风险预留,不是实际派彩金额。", "sourceReasonOptions": { "ticket_place": "下注占用", "ticket_rollback": "注单回滚", @@ -36,10 +38,13 @@ "sortRemainingAsc": "剩余额 ↑(紧俏)", "sortNumberAsc": "号码 ↑", "loadPoolsFailed": "加载风险池失败", - "capAmount": "封顶", - "lockedAmount": "已占用", - "remainingAmount": "剩余", - "usageRatio": "占用比", + "capAmount": "最高赔付额度", + "lockedAmount": "当前下注预留", + "remainingAmount": "剩余可承接赔付", + "usageRatio": "占用比例", + "emptyPools": "当前筛选下没有号码额度记录;未产生下注预留的号码通常不会列出。", + "emptyHighRiskPools": "当前没有占用比例达到 80% 的高风险号码。", + "emptySoldOutPools": "当前没有因额度不足或人工关闭而停售的号码。", "poolStatus": "状态", "soldOut": "售罄", "warning": "预警", diff --git a/src/lib/admin-auth-reject.ts b/src/lib/admin-auth-reject.ts index b59ec42..6844151 100644 --- a/src/lib/admin-auth-reject.ts +++ b/src/lib/admin-auth-reject.ts @@ -1,7 +1,5 @@ import { isAxiosError } from "axios"; -import { toast } from "sonner"; -import i18n from "@/i18n"; import { getAdminSessionState } from "@/stores/admin-session"; import { readToken } from "@/stores/admin-token"; import { isApiEnvelope } from "@/types/api/envelope"; @@ -13,6 +11,9 @@ export const ADMIN_UNAUTHENTICATED_CODE = 8110; /** 与后端 {@see ErrorCode::AdminAccountDisabled} 一致 */ export const ADMIN_ACCOUNT_DISABLED_CODE = 8113; +/** 与后端 ErrorCode::AdminSessionReplaced 一致 */ +export const ADMIN_SESSION_REPLACED_CODE = 8115; + /** 登录验证码错误等,HTTP 可能为 401/422,不应踢出会话 */ const LOGIN_FORM_ERROR_CODES = new Set([8111, 8112]); @@ -31,7 +32,8 @@ export function isAdminAuthRejected(err: unknown): boolean { if (err instanceof LotteryApiBizError) { return ( err.code === ADMIN_UNAUTHENTICATED_CODE || - err.code === ADMIN_ACCOUNT_DISABLED_CODE + err.code === ADMIN_ACCOUNT_DISABLED_CODE || + err.code === ADMIN_SESSION_REPLACED_CODE ); } @@ -54,7 +56,8 @@ export function isAdminAuthRejected(err: unknown): boolean { if ( envelopeCode === ADMIN_UNAUTHENTICATED_CODE || - envelopeCode === ADMIN_ACCOUNT_DISABLED_CODE + envelopeCode === ADMIN_ACCOUNT_DISABLED_CODE || + envelopeCode === ADMIN_SESSION_REPLACED_CODE ) { return true; } @@ -62,7 +65,8 @@ export function isAdminAuthRejected(err: unknown): boolean { if (isApiEnvelope(body) && body.code !== 0) { return ( body.code === ADMIN_UNAUTHENTICATED_CODE || - body.code === ADMIN_ACCOUNT_DISABLED_CODE + body.code === ADMIN_ACCOUNT_DISABLED_CODE || + body.code === ADMIN_SESSION_REPLACED_CODE ); } @@ -78,12 +82,32 @@ export function isAdminAuthRejected(err: unknown): boolean { return false; } -export function redirectToAdminLogin(): void { +function adminAuthErrorCode(err: unknown): number | null { + if (err instanceof LotteryApiBizError) { + return err.code; + } + + if (!isAxiosError(err)) { + return null; + } + + const body = err.response?.data; + if (!body || typeof body !== "object" || !("code" in body)) { + return null; + } + + const code = (body as { code?: unknown }).code; + return typeof code === "number" ? code : null; +} + +export function redirectToAdminLogin( + reason: "expired" | "replaced" = "expired", +): void { if (typeof window === "undefined" || isAdminLoginPath()) { return; } - const loginPath = "/admin/login"; + const loginPath = "/admin/login?session=" + reason; if (window.location.pathname !== loginPath) { window.location.replace(loginPath); } @@ -92,7 +116,7 @@ export function redirectToAdminLogin(): void { /** * 清除本地会话并跳转登录页。可在 axios 拦截器、/auth/me 刷新等任意上下文调用。 */ -export function handleAdminAuthRejected(): void { +export function handleAdminAuthRejected(err?: unknown): void { if (typeof window === "undefined" || isAdminLoginPath()) { return; } @@ -104,21 +128,19 @@ export function handleAdminAuthRejected(): void { const session = getAdminSessionState(); const hadSession = session.bearerToken != null || readToken() != null; - void fetch("/api/v1/admin/auth/logout", { method: "POST" }).catch(() => { + void fetch("/api/v1/admin/auth/logout", { + method: "POST", + keepalive: true, + }).catch(() => { // Best-effort HttpOnly cookie cleanup; the local session is cleared below. }); session.clearSession(); - if (hadSession) { - toast.error( - i18n.t("auth.sessionExpired", { - ns: "common", - defaultValue: "Sign-in expired. Please log in again.", - }), - ); - } - - redirectToAdminLogin(); + const reason = + hadSession && adminAuthErrorCode(err) === ADMIN_SESSION_REPLACED_CODE + ? "replaced" + : "expired"; + redirectToAdminLogin(reason); queueMicrotask(() => { authRejectHandling = false; diff --git a/src/lib/admin-http.ts b/src/lib/admin-http.ts index 056bffc..f8eed64 100644 --- a/src/lib/admin-http.ts +++ b/src/lib/admin-http.ts @@ -26,7 +26,7 @@ adminHttp.interceptors.response.use( (response) => response, (error: unknown) => { if (isAdminAuthRejected(error)) { - handleAdminAuthRejected(); + handleAdminAuthRejected(error); } if (isAxiosError(error) && typeof window !== "undefined") { const status = error.response?.status; @@ -43,7 +43,7 @@ adminHttp.interceptors.response.use( function rejectAfterAuthCheck(err: unknown): never { if (isAdminAuthRejected(err)) { - handleAdminAuthRejected(); + handleAdminAuthRejected(err); } throw err; } diff --git a/src/lib/admin-nav-label.ts b/src/lib/admin-nav-label.ts index 11e4729..494c163 100644 --- a/src/lib/admin-nav-label.ts +++ b/src/lib/admin-nav-label.ts @@ -11,6 +11,7 @@ const NAV_SEGMENT_I18N_KEYS: Record = { draws: "draws", rules_plays: "rules_plays", rules_odds: "rules_odds", + bet_providers: "bet_providers", jackpot: "jackpot", risk_cap: "risk_cap", risk: "risk", diff --git a/src/lib/lottery-api-proxy.ts b/src/lib/lottery-api-proxy.ts index 4feb652..93f8a59 100644 --- a/src/lib/lottery-api-proxy.ts +++ b/src/lib/lottery-api-proxy.ts @@ -94,6 +94,12 @@ export async function proxyLotteryApi( "", expiredAdminTokenCookieOptions(isSecureRequest(request)), ); + } else if (path.startsWith("v1/admin/") && upstream.status === 401) { + response.cookies.set( + ADMIN_TOKEN_STORAGE_KEY, + "", + expiredAdminTokenCookieOptions(isSecureRequest(request)), + ); } return response; diff --git a/src/modules/audit/audit-logs-console.tsx b/src/modules/audit/audit-logs-console.tsx index 5d21d2b..e084c91 100644 --- a/src/modules/audit/audit-logs-console.tsx +++ b/src/modules/audit/audit-logs-console.tsx @@ -141,7 +141,9 @@ export function AuditLogsConsole(): React.ReactElement { setOperatorType(value ?? "all")}> - + + {operatorType === "all" ? t("filterOperatorTypeAll") : t(`operatorTypes.${operatorType}`)} + {t("filterOperatorTypeAll")} @@ -304,4 +308,4 @@ export function AuditLogsConsole(): React.ReactElement { ); -} \ No newline at end of file +} diff --git a/src/modules/config/config-chip-group.tsx b/src/modules/config/config-chip-group.tsx index 173ca4a..8fb2919 100644 --- a/src/modules/config/config-chip-group.tsx +++ b/src/modules/config/config-chip-group.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; type ConfigChipGroupProps = { - label?: string; + label?: ReactNode; children: ReactNode; className?: string; }; @@ -14,7 +14,7 @@ type ConfigChipGroupProps = { export function ConfigChipGroup({ label, children, className }: ConfigChipGroupProps) { return (
- {label ?

{label}

: null} + {label ?
{label}
: null}
{children}
); diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx index 8bc7042..69720c6 100644 --- a/src/modules/config/doc/odds-config-doc-screen.tsx +++ b/src/modules/config/doc/odds-config-doc-screen.tsx @@ -15,6 +15,8 @@ import { putOddsItems, } from "@/api/admin-config"; import { getAdminBetProviders } from "@/api/admin-bet-providers"; +import { AdminHelpText } from "@/components/admin/admin-field-label"; +import { AdminPageGuideDialog } from "@/components/admin/admin-page-guide-dialog"; import { Button } from "@/components/ui/button"; import { ConfigChip, ConfigChipGroup } from "@/modules/config/config-chip-group"; import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page"; @@ -28,7 +30,6 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { ratioToPercentUi } from "@/lib/admin-rate-percent"; import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value"; @@ -602,7 +603,20 @@ export function OddsConfigDocScreen({ const filtersInner = ( <> - + + {t("betProviders.title", { ns: "config", defaultValue: "开注商" })} + + } + > setProviderCode(GLOBAL_PROVIDER_CODE)} @@ -619,7 +633,20 @@ export function OddsConfigDocScreen({ ))} - + + {t("odds.category", { ns: "config" })} + + } + > {catTabs.map((tab) => ( ) : ( - + + {t("odds.playType", { ns: "config" })} + + } + > {filteredTypes.length === 0 ? ( {t("odds.noPlayTypes", { ns: "config" })} ) : ( @@ -705,18 +745,63 @@ export function OddsConfigDocScreen({ /> } actions={ - void refreshList()} - onNewDraft={() => void handleNewDraft()} - onSaveDraft={() => void handleSave()} - onPublish={() => void requestPublishConfirm()} - /> + <> + + void refreshList()} + onNewDraft={() => void handleNewDraft()} + onSaveDraft={() => void handleSave()} + onPublish={() => void requestPublishConfirm()} + /> + } /> ); @@ -743,10 +828,21 @@ export function OddsConfigDocScreen({ {scopeEditorRows.map(({ scope, row, hint, sourceLabel, sourceClassName }) => (
- + {row ? ( {sourceLabel} @@ -779,9 +875,17 @@ export function OddsConfigDocScreen({
))}
- + {canEditDraft ? (