"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { getPlayEffective } from "@/api/play"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { getLotteryRequestLocale } from "@/lib/lottery-locale"; import { cn } from "@/lib/utils"; import { LotteryApiBizError } from "@/types/api/errors"; import type { PlayEffectivePayload, PlayEffectivePlayRow, } from "@/types/api/play-effective"; const DEFAULT_POLL_MS = 120_000; function pickDisplayName(row: PlayEffectivePlayRow): string { const loc = getLotteryRequestLocale(); if (loc === "zh") { return row.display_name_zh ?? row.display_name_en ?? row.play_code; } if (loc === "ne") { return row.display_name_ne ?? row.display_name_en ?? row.play_code; } return row.display_name_en ?? row.display_name_zh ?? row.play_code; } function pickRuleText(row: PlayEffectivePlayRow): string | null { const c = row.config; if (!c) { return null; } const loc = getLotteryRequestLocale(); if (loc === "zh") { return c.rule_text_zh ?? c.rule_text_en; } if (loc === "ne") { return c.rule_text_ne ?? c.rule_text_en; } return c.rule_text_en ?? c.rule_text_zh; } /** 主数据开关 + 配置版本内开关,同时有 config 行才算对客开放 */ function isPlayOpenForPlayer(row: PlayEffectivePlayRow): boolean { if (!row.master_enabled || row.config === null) { return false; } return row.config.is_enabled; } type LoadState = | { kind: "loading" } | { kind: "ok"; data: PlayEffectivePayload } | { kind: "error"; message: string; notReady?: boolean }; function formatMoneyAmount(n: number): string { return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format( n, ); } export function HallPlayCatalogPanel() { const [state, setState] = useState({ kind: "loading" }); const currencyParam = useMemo(() => { const fromEnv = process.env.NEXT_PUBLIC_LOTTERY_PLAY_CURRENCY?.trim(); return fromEnv !== undefined && fromEnv !== "" ? fromEnv : undefined; }, []); const load = useCallback(async () => { setState((s) => (s.kind === "ok" ? s : { kind: "loading" })); try { const data = await getPlayEffective( currencyParam !== undefined ? { currency: currencyParam } : undefined, ); setState({ kind: "ok", data }); } catch (e) { if (e instanceof LotteryApiBizError && e.code === 9004) { setState({ kind: "error", message: "玩法配置尚未初始化。请在 Laravel 执行含 OperationalConfigV1Seeder 的 seed。", notReady: true, }); return; } const msg = e instanceof LotteryApiBizError ? e.message : "加载玩法配置失败,请稍后重试。"; setState({ kind: "error", message: msg }); } }, [currencyParam]); useEffect(() => { queueMicrotask(() => { void load(); }); }, [load]); useEffect(() => { const id = window.setInterval(() => { void load(); }, DEFAULT_POLL_MS); return () => window.clearInterval(id); }, [load]); const body = (() => { if (state.kind === "loading") { return (

加载玩法与赔率…

); } if (state.kind === "error") { return (

{state.message}

); } const { data } = state; const ordered = [...data.plays].sort( (a, b) => a.sort_order - b.sort_order || a.play_code.localeCompare(b.play_code), ); return (

币种 {data.currency_code} · 配置版本 play#{data.effective_versions.play_config.version_no} /odds#{data.effective_versions.odds.version_no} · 限额单位为最小货币单位(与钱包一致)。

玩法 状态 下注限额 赔率× 说明 {ordered.map((row) => { const open = isPlayOpenForPlayer(row); const rule = pickRuleText(row); const oddsMul = row.odds === null ? "—" : typeof row.odds.odds_multiplier === "number" ? row.odds.odds_multiplier.toFixed(4) : (row.odds.odds_value / 10000).toFixed(4); return (
{pickDisplayName(row)} {row.play_code} {row.dimension != null ? ` · ${row.dimension}D` : ""}
{open ? ( 开放 ) : ( 关闭 )} {row.config ? ( <> {formatMoneyAmount(row.config.min_bet_amount)} {" — "} {formatMoneyAmount(row.config.max_bet_amount)} ) : ( "—" )} {row.odds ? oddsMul : "—"} {rule ?? "—"}
); })}
{data.risk_cap_items.length > 0 ? (

风控封顶(示例号码)

号码 封顶额 类型 {data.risk_cap_items.map((r, i) => ( {r.normalized_number} {formatMoneyAmount(r.cap_amount)} {r.cap_type} ))}
) : null}
); })(); return (
玩法与赔率 数据来自 GET /api/v1/play/effective ;后台修改并发布后,最长约 {DEFAULT_POLL_MS / 1000}s 内自动刷新(亦可手动刷新)。
{body}
); }