refactor: 完成全站国际化改造,统一多语言支持
此提交完成了全项目的国际化适配: 1. 新增多语言翻译文件与基础配置 2. 替换所有硬编码文本为i18n调用 3. 优化语言切换与文档语言同步逻辑 4. 重构部分业务逻辑以支持动态翻译 5. 移除过时代码与硬编码配置
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
@@ -31,13 +33,16 @@ export function HallBetAmountInput({
|
||||
disabled,
|
||||
hint,
|
||||
}: HallBetAmountInputProps) {
|
||||
const { t } = useTranslation("player");
|
||||
const min = formatMinorAsCurrency(minBetMinor, currencyCode);
|
||||
const max = formatMinorAsCurrency(maxBetMinor, currencyCode);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-end justify-between gap-2">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
限额 {formatMinorAsCurrency(minBetMinor, currencyCode)} —{" "}
|
||||
{formatMinorAsCurrency(maxBetMinor, currencyCode)}
|
||||
{t("hall.amountInput.limit", { min, max })}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
@@ -48,7 +53,7 @@ export function HallBetAmountInput({
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={cn("tabular-nums")}
|
||||
placeholder="例如 100.00"
|
||||
placeholder={t("hall.amountInput.placeholder")}
|
||||
/>
|
||||
{hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null}
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
/**
|
||||
* 下注业务码与玩家可见说明(对齐 Laravel `ErrorCode` 与产品文档 §6.3 / §6.4)。
|
||||
*/
|
||||
export function mapTicketBetError(code: number, fallbackMsg: string): string {
|
||||
export function mapTicketBetError(
|
||||
code: number,
|
||||
fallbackMsg: string,
|
||||
t?: (key: string) => string,
|
||||
): string {
|
||||
const msg = (key: string, fallback: string) => t?.(key) ?? fallback;
|
||||
switch (code) {
|
||||
case 4001:
|
||||
return "该号码本期赔付池不足,已售罄。请更换号码、金额或玩法后重试。";
|
||||
return msg(
|
||||
"hall.ticketError.4001",
|
||||
"该号码本期赔付池不足,已售罄。请更换号码、金额或玩法后重试。",
|
||||
);
|
||||
case 2003:
|
||||
case 1001:
|
||||
return "余额不足,请先转入后再下注。";
|
||||
return msg("hall.ticketError.1001", "余额不足,请先转入后再下注。");
|
||||
case 2001:
|
||||
return "本期已封盘,无法继续下注。";
|
||||
return msg("hall.ticketError.2001", "本期已封盘,无法继续下注。");
|
||||
case 2002:
|
||||
return "该玩法已关闭,请选择其他玩法。";
|
||||
return msg("hall.ticketError.2002", "该玩法已关闭,请选择其他玩法。");
|
||||
case 2004:
|
||||
return "号码格式或长度不符合该玩法要求。";
|
||||
return msg("hall.ticketError.2004", "号码格式或长度不符合该玩法要求。");
|
||||
case 2005:
|
||||
return "玩法参数不完整(如单双大小需选择位数与维度)。";
|
||||
return msg(
|
||||
"hall.ticketError.2005",
|
||||
"玩法参数不完整(如单双大小需选择位数与维度)。",
|
||||
);
|
||||
case 2006:
|
||||
return "当前期号不可下注。";
|
||||
return msg("hall.ticketError.2006", "当前期号不可下注。");
|
||||
case 2007:
|
||||
return "该玩法暂不支持或缺少赔率配置。";
|
||||
return msg("hall.ticketError.2007", "该玩法暂不支持或缺少赔率配置。");
|
||||
case 2008:
|
||||
return "赔率或玩法配置已更新,请关闭预览后重新操作。";
|
||||
return msg(
|
||||
"hall.ticketError.2008",
|
||||
"赔率或玩法配置已更新,请关闭预览后重新操作。",
|
||||
);
|
||||
case 1003:
|
||||
return "下注金额超出该玩法允许范围。";
|
||||
return msg("hall.ticketError.1003", "下注金额超出该玩法允许范围。");
|
||||
default:
|
||||
return fallbackMsg || "下注失败,请稍后重试。";
|
||||
return fallbackMsg || msg("hall.ticketError.fallback", "下注失败,请稍后重试。");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -36,6 +38,7 @@ export function HallBetNumberInput({
|
||||
disabled,
|
||||
helper,
|
||||
}: HallBetNumberInputProps) {
|
||||
const { t } = useTranslation("player");
|
||||
const handle = (raw: string) => {
|
||||
if (spec.mode === "roll") {
|
||||
onChange(sanitizeRoll(raw, spec.maxChars));
|
||||
@@ -56,7 +59,11 @@ export function HallBetNumberInput({
|
||||
value={value}
|
||||
onChange={(e) => handle(e.target.value)}
|
||||
className={cn("font-mono text-base tracking-widest")}
|
||||
placeholder={spec.mode === "roll" ? "如 12R4" : "0-9"}
|
||||
placeholder={
|
||||
spec.mode === "roll"
|
||||
? t("hall.numberInput.rollPlaceholder")
|
||||
: t("hall.numberInput.digitPlaceholder")
|
||||
}
|
||||
maxLength={spec.maxChars}
|
||||
/>
|
||||
{helper ? <p className="text-xs text-muted-foreground">{helper}</p> : null}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -28,14 +29,16 @@ type HallBetPreviewDialogProps = {
|
||||
};
|
||||
|
||||
function WarningsBlock({ warnings }: { warnings: TicketPreviewWarning[] }) {
|
||||
const { t } = useTranslation("player");
|
||||
|
||||
if (warnings.length === 0) return null;
|
||||
return (
|
||||
<Alert className="border-amber-500/40 bg-amber-500/5 text-amber-950 dark:text-amber-100">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle>赔付池预警</AlertTitle>
|
||||
<AlertTitle>{t("hall.preview.warningsTitle")}</AlertTitle>
|
||||
<AlertDescription className="space-y-1">
|
||||
<p className="text-xs leading-relaxed">
|
||||
产品文档 §6.4:以下号码本期赔付池占用较高,仍允许下注;若实际占用不足将售罄拒单。
|
||||
{t("hall.preview.warningsDescription")}
|
||||
</p>
|
||||
<ul className="list-inside list-disc text-xs">
|
||||
{warnings.map((w, i) => (
|
||||
@@ -61,6 +64,7 @@ export function HallBetPreviewDialog({
|
||||
allowSubmit = true,
|
||||
onConfirmPlace,
|
||||
}: HallBetPreviewDialogProps) {
|
||||
const { t } = useTranslation("player");
|
||||
const summary = data?.summary;
|
||||
const lines = data?.lines ?? [];
|
||||
|
||||
@@ -69,17 +73,17 @@ export function HallBetPreviewDialog({
|
||||
<DialogContent className="max-h-[min(90vh,560px)] gap-0 overflow-hidden p-0 sm:max-w-md">
|
||||
<div className="p-4 pb-2">
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认下注</DialogTitle>
|
||||
<DialogTitle>{t("hall.preview.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
请核对号码、玩法与实扣金额;确认后将扣减彩票钱包且不可撤单(产品文档 §6.3)。
|
||||
{t("hall.preview.description")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{!allowSubmit ? (
|
||||
<Alert className="mt-3 border-[#ff4d4f]/35 bg-[#ff4d4f]/8 text-[#ff4d4f] dark:bg-[#ff4d4f]/12">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle>已封盘</AlertTitle>
|
||||
<AlertTitle>{t("hall.preview.sealedTitle")}</AlertTitle>
|
||||
<AlertDescription className="text-xs leading-relaxed">
|
||||
当前期已停止接收注单,无法提交。请选择下一期(界面文档 §4.2)。
|
||||
{t("hall.preview.sealedDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
@@ -88,37 +92,38 @@ export function HallBetPreviewDialog({
|
||||
<ScrollArea className="max-h-[min(52vh,360px)] border-y px-4">
|
||||
<div className="space-y-4 py-3 pr-3">
|
||||
{!data ? (
|
||||
<p className="text-sm text-muted-foreground">暂无预览数据</p>
|
||||
<p className="text-sm text-muted-foreground">{t("hall.preview.empty")}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-xs">
|
||||
<p>
|
||||
期号{" "}
|
||||
<span className="font-mono font-semibold">{data.draw.draw_id}</span> · 状态{" "}
|
||||
{t("hall.preview.draw")}{" "}
|
||||
<span className="font-mono font-semibold">{data.draw.draw_id}</span> ·{" "}
|
||||
{t("hall.preview.status")}{" "}
|
||||
<span className="font-medium">{data.draw.status}</span>
|
||||
</p>
|
||||
{summary ? (
|
||||
<ul className="mt-2 space-y-1 tabular-nums">
|
||||
<li>
|
||||
总下注{" "}
|
||||
{t("hall.preview.totalBet")}{" "}
|
||||
<span className="font-medium">
|
||||
{formatMinorAsCurrency(summary.total_bet_amount, currencyCode)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
回水抵扣{" "}
|
||||
{t("hall.preview.rebateDeduct")}{" "}
|
||||
<span className="font-medium">
|
||||
{formatMinorAsCurrency(summary.total_rebate_amount, currencyCode)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
实扣金额{" "}
|
||||
{t("hall.preview.actualDeduct")}{" "}
|
||||
<span className="font-semibold text-primary">
|
||||
{formatMinorAsCurrency(summary.total_actual_deduct, currencyCode)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
预估最高赔付{" "}
|
||||
{t("hall.preview.estimatedPayout")}{" "}
|
||||
<span className="font-medium">
|
||||
{formatMinorAsCurrency(summary.total_estimated_payout, currencyCode)}
|
||||
</span>
|
||||
@@ -130,7 +135,9 @@ export function HallBetPreviewDialog({
|
||||
<WarningsBlock warnings={data.warnings} />
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">注项明细</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
{t("hall.preview.lines")}
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{lines.map((ln) => (
|
||||
<li
|
||||
@@ -146,15 +153,23 @@ export function HallBetPreviewDialog({
|
||||
<p className="mt-1 font-mono text-base">{ln.number}</p>
|
||||
<Separator className="my-2" />
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1 text-xs tabular-nums">
|
||||
<span className="text-muted-foreground">归一号码</span>
|
||||
<span className="text-muted-foreground">
|
||||
{t("hall.preview.normalizedNumber")}
|
||||
</span>
|
||||
<span className="text-right font-mono">{ln.normalized_number}</span>
|
||||
<span className="text-muted-foreground">组合数</span>
|
||||
<span className="text-muted-foreground">
|
||||
{t("hall.preview.combinationCount")}
|
||||
</span>
|
||||
<span className="text-right">{ln.combination_count}</span>
|
||||
<span className="text-muted-foreground">实扣</span>
|
||||
<span className="text-muted-foreground">
|
||||
{t("hall.preview.actual")}
|
||||
</span>
|
||||
<span className="text-right">
|
||||
{formatMinorAsCurrency(ln.actual_deduct_amount, currencyCode)}
|
||||
</span>
|
||||
<span className="text-muted-foreground">预估最高赔</span>
|
||||
<span className="text-muted-foreground">
|
||||
{t("hall.preview.estimatedMax")}
|
||||
</span>
|
||||
<span className="text-right">
|
||||
{formatMinorAsCurrency(ln.estimated_max_payout, currencyCode)}
|
||||
</span>
|
||||
@@ -170,10 +185,14 @@ export function HallBetPreviewDialog({
|
||||
|
||||
<div className="flex flex-col-reverse gap-2 border-t bg-muted/30 p-4 sm:flex-row sm:justify-between">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={placing}>
|
||||
返回修改
|
||||
{t("hall.preview.backEdit")}
|
||||
</Button>
|
||||
<Button type="button" onClick={onConfirmPlace} disabled={!data || placing || !allowSubmit}>
|
||||
{placing ? "提交中…" : allowSubmit ? "确认提交" : "已封盘"}
|
||||
{placing
|
||||
? t("hall.preview.submitting")
|
||||
: allowSubmit
|
||||
? t("hall.preview.confirmSubmit")
|
||||
: t("hall.preview.sealedTitle")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -47,12 +47,24 @@ export function playNeedsDigitSlot(playCode: string): boolean {
|
||||
}
|
||||
|
||||
/** 产品文档:iBox/Roll 单注金额;mBox 总金额摊分 */
|
||||
export function ticketAmountHint(playCode: string): string {
|
||||
export function ticketAmountHint(
|
||||
playCode: string,
|
||||
t?: (key: string) => string,
|
||||
): string {
|
||||
if (playCode === "ibox" || playCode === "roll") {
|
||||
return "本玩法金额为「单注金额」,系统按展开组合数计算总下注与实扣。";
|
||||
return (
|
||||
t?.("hall.amountHint.iboxRoll") ??
|
||||
"本玩法金额为「单注金额」,系统按展开组合数计算总下注与实扣。"
|
||||
);
|
||||
}
|
||||
if (playCode === "mbox") {
|
||||
return "本玩法金额为「总输入金额」,将均摊到各排列组合(向下取整到最小单位)。";
|
||||
return (
|
||||
t?.("hall.amountHint.mbox") ??
|
||||
"本玩法金额为「总输入金额」,将均摊到各排列组合(向下取整到最小单位)。"
|
||||
);
|
||||
}
|
||||
return "金额为该笔注单的下注额(最小货币单位整数,与钱包一致)。";
|
||||
return (
|
||||
t?.("hall.amountHint.default") ??
|
||||
"金额为该笔注单的下注额(最小货币单位整数,与钱包一致)。"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { CirclePlus, Cuboid, PackageOpen, Ticket, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getPlayEffective } from "@/api/play";
|
||||
@@ -205,6 +206,7 @@ function pickSimplePlay(
|
||||
|
||||
export function HallBettingGrid() {
|
||||
const { display, isBettable, reload: reloadDraw } = useHallDrawLive();
|
||||
const { t } = useTranslation("player");
|
||||
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
|
||||
const [boxMode, setBoxMode] = useState<BoxMode>("ibox");
|
||||
const [rows, setRows] = useState<DraftRow[]>(() => [
|
||||
@@ -239,10 +241,10 @@ export function HallBettingGrid() {
|
||||
setCatalogState({ kind: "ok", data });
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof LotteryApiBizError ? e.message : "加载玩法失败,请稍后重试。";
|
||||
e instanceof LotteryApiBizError ? e.message : t("hall.loadingError");
|
||||
setCatalogState({ kind: "error", message: msg });
|
||||
}
|
||||
}, [currencyParam]);
|
||||
}, [currencyParam, t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
@@ -373,21 +375,21 @@ export function HallBettingGrid() {
|
||||
|
||||
const handlePreview = async () => {
|
||||
if (!display) {
|
||||
toast.error("暂无当期期号,无法提交。");
|
||||
toast.error(t("hall.noDraw"));
|
||||
return;
|
||||
}
|
||||
if (!isBettable) {
|
||||
toast.error("当前已封盘或不可下注。");
|
||||
toast.error(t("hall.notBettable"));
|
||||
return;
|
||||
}
|
||||
if (catalogState.kind !== "ok") {
|
||||
toast.error("玩法配置尚未加载完成。");
|
||||
toast.error(t("hall.catalogNotReady"));
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = buildLines();
|
||||
if (lines.length === 0) {
|
||||
toast.error("请至少填写一组有效号码和下注金额。");
|
||||
toast.error(t("hall.emptyLines"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -403,8 +405,8 @@ export function HallBettingGrid() {
|
||||
setPreviewOpen(true);
|
||||
} catch (e) {
|
||||
const code = e instanceof LotteryApiBizError ? e.code : 0;
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : "预览失败";
|
||||
toast.error(mapTicketBetError(code, msg));
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : t("hall.previewFailed");
|
||||
toast.error(mapTicketBetError(code, msg, t));
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
@@ -413,13 +415,13 @@ export function HallBettingGrid() {
|
||||
const handlePlace = async () => {
|
||||
if (!display || !previewData) return;
|
||||
if (!isBettable) {
|
||||
toast.error("已封盘,无法提交。");
|
||||
toast.error(t("hall.closedSubmit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = buildLines();
|
||||
if (lines.length === 0) {
|
||||
toast.error("提交前数据已变化,请关闭预览后重试。");
|
||||
toast.error(t("hall.changedBeforeSubmit"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,7 +438,10 @@ export function HallBettingGrid() {
|
||||
expected_config_versions: previewData.config_versions,
|
||||
});
|
||||
toast.success(
|
||||
`下注成功,订单号 ${data.order_no},实扣 ${formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode)}。`,
|
||||
t("hall.placeSuccess", {
|
||||
orderNo: data.order_no,
|
||||
amount: formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode),
|
||||
}),
|
||||
);
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
@@ -445,8 +450,8 @@ export function HallBettingGrid() {
|
||||
void reloadDraw();
|
||||
} catch (e) {
|
||||
const code = e instanceof LotteryApiBizError ? e.code : 0;
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : "提交失败";
|
||||
toast.error(mapTicketBetError(code, msg));
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : t("hall.placeFailed");
|
||||
toast.error(mapTicketBetError(code, msg, t));
|
||||
} finally {
|
||||
setPlaceLoading(false);
|
||||
}
|
||||
@@ -454,7 +459,7 @@ export function HallBettingGrid() {
|
||||
|
||||
if (catalogState.kind === "loading") {
|
||||
return (
|
||||
<section className="space-y-3" aria-label="Betting table">
|
||||
<section className="space-y-3" aria-label={t("hall.aria")}>
|
||||
<Skeleton className="h-12 rounded-xl" />
|
||||
<Skeleton className="h-72 rounded-xl" />
|
||||
<Skeleton className="h-14 rounded-xl" />
|
||||
@@ -473,7 +478,7 @@ export function HallBettingGrid() {
|
||||
className="mt-3 border-red-200 bg-white text-red-700 hover:bg-red-50"
|
||||
onClick={() => void loadCatalog()}
|
||||
>
|
||||
重试
|
||||
{t("actions.retry")}
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
@@ -485,7 +490,7 @@ export function HallBettingGrid() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="space-y-4" aria-label="Betting table">
|
||||
<section className="space-y-4" aria-label={t("hall.aria")}>
|
||||
<div className="grid grid-cols-4 rounded-xl border border-[#e8eef7] bg-white p-1 shadow-[0_6px_18px_rgba(30,64,175,0.06)]">
|
||||
{categoryTabs.map((tab) => {
|
||||
const active = activeCategory === tab.value;
|
||||
@@ -539,9 +544,11 @@ export function HallBettingGrid() {
|
||||
<Cuboid className="size-4" aria-hidden />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-bold">iBox</span>
|
||||
<span className="block truncate text-sm font-bold">
|
||||
{t("hall.boxMode.iboxTitle")}
|
||||
</span>
|
||||
<span className="block truncate text-[10px] text-slate-500">
|
||||
Divide all by amount
|
||||
{t("hall.boxMode.iboxDesc")}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -561,9 +568,11 @@ export function HallBettingGrid() {
|
||||
<PackageOpen className="size-4" aria-hidden />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-bold">Box</span>
|
||||
<span className="block truncate text-sm font-bold">
|
||||
{t("hall.boxMode.boxTitle")}
|
||||
</span>
|
||||
<span className="block truncate text-[10px] text-slate-500">
|
||||
Multiply all by amount
|
||||
{t("hall.boxMode.boxDesc")}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -575,10 +584,12 @@ export function HallBettingGrid() {
|
||||
<div className="mx-auto flex size-14 items-center justify-center rounded-full bg-slate-200 text-slate-600">
|
||||
<Ticket className="size-7" aria-hidden />
|
||||
</div>
|
||||
<p className="mt-4 text-lg font-bold text-slate-900">Closed</p>
|
||||
<p className="mt-1 text-xs">This issue is now closed.</p>
|
||||
<p className="mt-4 text-lg font-bold text-slate-900">
|
||||
{t("hall.closed.title")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs">{t("hall.closed.subtitle")}</p>
|
||||
<div className="mt-5 rounded-lg border border-[#cbdcf7] bg-white px-3 py-3 text-left text-xs text-[#315a9f]">
|
||||
The betting window has closed. Please wait for the next issue to place your bets.
|
||||
{t("hall.closed.description")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -593,10 +604,10 @@ export function HallBettingGrid() {
|
||||
<thead>
|
||||
<tr className="border-b border-[#e8eef7] bg-[#f5f8fd] text-[11px] font-semibold text-[#32518d]">
|
||||
<th className="sticky left-0 z-20 w-12 bg-[#f5f8fd] px-2 py-3 text-center">
|
||||
No.
|
||||
{t("hall.table.no")}
|
||||
</th>
|
||||
<th className="sticky left-12 z-20 w-24 bg-[#f5f8fd] px-2 py-3 text-center">
|
||||
Number
|
||||
{t("hall.table.number")}
|
||||
<span className="block text-[10px] font-normal text-[#6b7896]">
|
||||
({numberPlaceholder})
|
||||
</span>
|
||||
@@ -609,12 +620,18 @@ export function HallBettingGrid() {
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<th className="min-w-28 px-2 py-3 text-center">Stake Amount</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">Commission / Rebate</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">Actual Deduction</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">
|
||||
{t("hall.table.stake")}
|
||||
</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">
|
||||
{t("hall.table.rebate")}
|
||||
</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">
|
||||
{t("hall.table.actual")}
|
||||
</th>
|
||||
</>
|
||||
)}
|
||||
<th className="w-10 px-2 py-3" aria-label="Delete" />
|
||||
<th className="w-10 px-2 py-3" aria-label={t("hall.table.delete")} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -695,7 +712,7 @@ export function HallBettingGrid() {
|
||||
disabled={tableDisabled || rows.length <= 1}
|
||||
onClick={() => removeRow(row.id)}
|
||||
className="inline-flex size-8 items-center justify-center rounded-full text-[#ff4d4f] hover:bg-red-50 disabled:text-slate-300 disabled:hover:bg-transparent"
|
||||
aria-label={`删除第 ${index + 1} 行`}
|
||||
aria-label={t("actions.deleteRow", { row: index + 1 })}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden />
|
||||
</button>
|
||||
@@ -713,13 +730,13 @@ export function HallBettingGrid() {
|
||||
className="flex h-11 w-full items-center justify-center gap-1.5 border-t border-[#edf2f9] text-sm font-semibold text-[#1d57b7] hover:bg-[#f7faff] disabled:text-slate-300"
|
||||
>
|
||||
<CirclePlus className="size-4" aria-hidden />
|
||||
Add Row
|
||||
{t("hall.table.addRow")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-[#e9eef7] bg-[#f8fbff] px-4 py-3 text-sm shadow-[0_6px_20px_rgba(15,23,42,0.04)]">
|
||||
<span className="font-medium text-slate-800">Draft Total</span>
|
||||
<span className="font-medium text-slate-800">{t("hall.table.draftTotal")}</span>
|
||||
<span className="text-lg font-bold tabular-nums text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(draftSummary.actual, currencyCode)}
|
||||
</span>
|
||||
@@ -727,7 +744,7 @@ export function HallBettingGrid() {
|
||||
|
||||
{sealedBetUi ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-600">
|
||||
已封盘:当前表格不可编辑,请等待下一期。
|
||||
{t("hall.table.sealedHint")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -738,7 +755,11 @@ export function HallBettingGrid() {
|
||||
className="h-12 w-full rounded-xl border-0 bg-[#e5002c] text-base font-bold text-white shadow-[0_8px_20px_rgba(229,0,44,0.26)] hover:bg-[#d10028]"
|
||||
>
|
||||
<Ticket className="size-5" aria-hidden />
|
||||
{previewLoading ? "Previewing..." : activeCategory === "JACKPOT" ? "Closed" : "Submit Bet"}
|
||||
{previewLoading
|
||||
? t("hall.table.previewing")
|
||||
: activeCategory === "JACKPOT"
|
||||
? t("hall.closed.title")
|
||||
: t("hall.table.submitBet")}
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Hourglass, Landmark, TimerReset, WalletCards } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -12,13 +13,14 @@ import { cn } from "@/lib/utils";
|
||||
import type { DrawCurrentPayload } from "@/types/api/draw-current";
|
||||
|
||||
function CurrentTime({ payload }: { payload: DrawCurrentPayload }) {
|
||||
const { t } = useTranslation("player");
|
||||
const source = payload.close_time ?? payload.draw_time ?? payload.start_time;
|
||||
const formatted = source ? formatLotteryInstant(source) : null;
|
||||
if (!formatted) {
|
||||
return (
|
||||
<>
|
||||
<span className="text-lg font-black tabular-nums text-[#0b3f96]">--:--:--</span>
|
||||
<span className="mt-1 text-[11px] text-slate-500">Current Time</span>
|
||||
<span className="mt-1 text-[11px] text-slate-500">{t("draw.currentTime")}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -42,18 +44,19 @@ function CloseTime({
|
||||
hud: ReturnType<typeof drawStatusHud>;
|
||||
payload: DrawCurrentPayload;
|
||||
}) {
|
||||
const { t } = useTranslation("player");
|
||||
const sealedCountdown = isHallSealedCountdownUi(payload.status);
|
||||
let seconds = 0;
|
||||
let label = "Closes In";
|
||||
let label = t("draw.closesIn");
|
||||
|
||||
if (hud.countdownKind === "close") {
|
||||
seconds = Math.max(0, payload.seconds_to_close);
|
||||
} else if (hud.countdownKind === "draw") {
|
||||
seconds = Math.max(0, payload.seconds_to_draw);
|
||||
label = sealedCountdown ? "Draws In" : "Closes In";
|
||||
label = sealedCountdown ? t("draw.drawsIn") : t("draw.closesIn");
|
||||
} else if (hud.countdownKind === "cooldown") {
|
||||
seconds = Math.max(0, payload.seconds_remaining_in_cooldown ?? 0);
|
||||
label = "Cool Down";
|
||||
label = t("draw.coolDown");
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -68,11 +71,12 @@ function CloseTime({
|
||||
|
||||
export function HallDrawPanel() {
|
||||
const { raw, display, error, reload } = useHallDrawLive();
|
||||
const { t } = useTranslation("player");
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="mb-4 rounded-xl border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-700">
|
||||
<p>{error}</p>
|
||||
<p>{t(error, { defaultValue: error })}</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -80,7 +84,7 @@ export function HallDrawPanel() {
|
||||
className="mt-2 border-red-200 bg-white text-red-700"
|
||||
onClick={() => void reload()}
|
||||
>
|
||||
重试
|
||||
{t("actions.retry")}
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
@@ -101,7 +105,7 @@ export function HallDrawPanel() {
|
||||
if (raw === null || display === null) {
|
||||
return (
|
||||
<section className="mb-4 rounded-xl border border-[#e3ebf6] bg-white px-3 py-4 text-center text-sm text-slate-500 shadow-sm">
|
||||
暂无可用期号,请稍后再试
|
||||
{t("draw.noIssue")}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -115,7 +119,7 @@ export function HallDrawPanel() {
|
||||
"mb-4 overflow-hidden rounded-xl border border-[#e1e9f5] bg-white shadow-[0_6px_20px_rgba(15,23,42,0.06)]",
|
||||
sealedUi && "border-red-200 bg-red-50/30",
|
||||
)}
|
||||
aria-label="Current issue"
|
||||
aria-label={t("draw.currentIssue")}
|
||||
>
|
||||
<div className="grid grid-cols-[1fr_1.05fr_1fr] divide-x divide-[#e7edf6]">
|
||||
<div className="flex min-w-0 items-center justify-center gap-2 px-2 py-3 text-center">
|
||||
@@ -123,7 +127,7 @@ export function HallDrawPanel() {
|
||||
<WalletCards className="size-4" aria-hidden />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-semibold text-slate-500">Issue No.</p>
|
||||
<p className="text-[11px] font-semibold text-slate-500">{t("draw.issueNo")}</p>
|
||||
<p className="truncate text-sm font-black tabular-nums text-[#ff143d]">
|
||||
{display.draw_no}
|
||||
</p>
|
||||
@@ -146,17 +150,17 @@ export function HallDrawPanel() {
|
||||
{sealedUi ? (
|
||||
<div className="flex items-center gap-2 border-t border-red-100 bg-red-50 px-3 py-2 text-xs font-medium text-red-600">
|
||||
<TimerReset className="size-4" aria-hidden />
|
||||
已封盘,请等待下一期。
|
||||
{t("draw.sealedNotice")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between border-t border-[#eef3f9] bg-[#fbfdff] px-3 py-1.5 text-[11px] text-slate-500">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className={cn("size-2 rounded-full", hud.dotClass)} />
|
||||
{hud.label}
|
||||
{t(hud.labelKey, { defaultValue: hud.labelKey })}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Landmark className="size-3.5" aria-hidden />
|
||||
Betting Hall
|
||||
{t("draw.hall")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getPlayEffective } from "@/api/play";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -76,6 +77,7 @@ function formatMoneyAmount(n: number): string {
|
||||
}
|
||||
|
||||
export function HallPlayCatalogPanel() {
|
||||
const { t } = useTranslation("player");
|
||||
const [state, setState] = useState<LoadState>({ kind: "loading" });
|
||||
const currencyParam = useMemo(() => {
|
||||
const fromEnv = process.env.NEXT_PUBLIC_LOTTERY_PLAY_CURRENCY?.trim();
|
||||
@@ -93,8 +95,7 @@ export function HallPlayCatalogPanel() {
|
||||
if (e instanceof LotteryApiBizError && e.code === 9004) {
|
||||
setState({
|
||||
kind: "error",
|
||||
message:
|
||||
"玩法配置尚未初始化。请在 Laravel 执行含 OperationalConfigV1Seeder 的 seed。",
|
||||
message: t("hall.playCatalog.notReady"),
|
||||
notReady: true,
|
||||
});
|
||||
return;
|
||||
@@ -102,10 +103,10 @@ export function HallPlayCatalogPanel() {
|
||||
const msg =
|
||||
e instanceof LotteryApiBizError
|
||||
? e.message
|
||||
: "加载玩法配置失败,请稍后重试。";
|
||||
: t("hall.playCatalog.loadFailed");
|
||||
setState({ kind: "error", message: msg });
|
||||
}
|
||||
}, [currencyParam]);
|
||||
}, [currencyParam, t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
@@ -123,7 +124,7 @@ export function HallPlayCatalogPanel() {
|
||||
const body = (() => {
|
||||
if (state.kind === "loading") {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">加载玩法与赔率…</p>
|
||||
<p className="text-sm text-muted-foreground">{t("hall.playCatalog.loading")}</p>
|
||||
);
|
||||
}
|
||||
if (state.kind === "error") {
|
||||
@@ -131,7 +132,7 @@ export function HallPlayCatalogPanel() {
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-destructive">{state.message}</p>
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => void load()}>
|
||||
重试
|
||||
{t("actions.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -145,19 +146,28 @@ export function HallPlayCatalogPanel() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
币种 {data.currency_code} · 配置版本 play#{data.effective_versions.play_config.version_no}
|
||||
/odds#{data.effective_versions.odds.version_no} · 限额单位为最小货币单位(与钱包一致)。
|
||||
{t("hall.playCatalog.meta", {
|
||||
currency: data.currency_code,
|
||||
playVersion: data.effective_versions.play_config.version_no,
|
||||
oddsVersion: data.effective_versions.odds.version_no,
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="min-w-[140px]">玩法</TableHead>
|
||||
<TableHead className="w-[88px] text-center">状态</TableHead>
|
||||
<TableHead className="min-w-[160px] whitespace-nowrap">下注限额</TableHead>
|
||||
<TableHead className="min-w-[100px]">赔率×</TableHead>
|
||||
<TableHead className="min-w-[200px]">说明</TableHead>
|
||||
<TableHead className="min-w-[140px]">{t("hall.playCatalog.play")}</TableHead>
|
||||
<TableHead className="w-[88px] text-center">
|
||||
{t("hall.playCatalog.status")}
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[160px] whitespace-nowrap">
|
||||
{t("hall.playCatalog.limit")}
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[100px]">{t("hall.playCatalog.odds")}</TableHead>
|
||||
<TableHead className="min-w-[200px]">
|
||||
{t("hall.playCatalog.description")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -188,11 +198,11 @@ export function HallPlayCatalogPanel() {
|
||||
<TableCell className="text-center">
|
||||
{open ? (
|
||||
<Badge variant="default" className="font-normal">
|
||||
开放
|
||||
{t("hall.playCatalog.open")}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="font-normal">
|
||||
关闭
|
||||
{t("hall.playCatalog.closed")}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
@@ -222,14 +232,16 @@ export function HallPlayCatalogPanel() {
|
||||
|
||||
{data.risk_cap_items.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-foreground">风控封顶(示例号码)</h3>
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
{t("hall.playCatalog.riskTitle")}
|
||||
</h3>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>号码</TableHead>
|
||||
<TableHead>封顶额</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>{t("hall.playCatalog.number")}</TableHead>
|
||||
<TableHead>{t("hall.playCatalog.capAmount")}</TableHead>
|
||||
<TableHead>{t("hall.playCatalog.type")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -257,10 +269,11 @@ export function HallPlayCatalogPanel() {
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-base">玩法与赔率</CardTitle>
|
||||
<CardTitle className="text-base">{t("hall.playCatalog.title")}</CardTitle>
|
||||
<CardDescription>
|
||||
数据来自 <code className="text-xs">GET /api/v1/play/effective</code>
|
||||
;后台修改并发布后,最长约 {DEFAULT_POLL_MS / 1000}s 内自动刷新(亦可手动刷新)。
|
||||
{t("hall.playCatalog.descriptionText", {
|
||||
seconds: DEFAULT_POLL_MS / 1000,
|
||||
})}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
@@ -270,7 +283,7 @@ export function HallPlayCatalogPanel() {
|
||||
className="shrink-0"
|
||||
onClick={() => void load()}
|
||||
>
|
||||
刷新配置
|
||||
{t("hall.playCatalog.refresh")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>{body}</CardContent>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type PlayChip = {
|
||||
@@ -23,15 +25,17 @@ export function HallPlaySwitcher({
|
||||
onChange,
|
||||
disabled,
|
||||
}: HallPlaySwitcherProps) {
|
||||
const { t } = useTranslation("player");
|
||||
|
||||
if (plays.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">当前币种下没有可下注的开放玩法。</p>
|
||||
<p className="text-sm text-muted-foreground">{t("hall.playSwitcher.empty")}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">玩法</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">{t("hall.playSwitcher.label")}</p>
|
||||
<div className="-mx-1 flex gap-1.5 overflow-x-auto pb-1">
|
||||
{plays.map((p) => {
|
||||
const active = p.play_code === value;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Bell } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { HallBettingGrid } from "@/features/hall/hall-betting-grid";
|
||||
@@ -11,10 +12,12 @@ import { HallWalletStrip } from "@/features/hall/hall-wallet-strip";
|
||||
* 下注大厅:钱包条 §4 + 当期期号 §4.2(封盘置灰 / 倒计时错误色 / WS+轮询);玩法目录 §12.3;下注表格 §13.3。
|
||||
*/
|
||||
export function HallScreen() {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[480px]">
|
||||
<section className="overflow-hidden rounded-[18px] border border-[#dce7f7] bg-white px-2.5 py-3 text-slate-900 shadow-[0_18px_50px_rgba(15,44,92,0.12)]">
|
||||
<div className="mb-3 flex items-center gap-2 px-1">
|
||||
<section className="overflow-hidden bg-white px-4 pb-8 pt-4 text-slate-900">
|
||||
<div className="mb-3 flex items-center gap-2 px-1 pt-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<div className="relative flex size-10 shrink-0 rotate-[-10deg] items-center justify-center rounded-lg bg-[#e60023] text-white shadow-[0_7px_14px_rgba(230,0,35,0.22)]">
|
||||
<span className="absolute -left-1.5 top-1 flex size-6 rotate-[18deg] items-center justify-center rounded-sm bg-[#0b56b7] text-xs font-black">
|
||||
@@ -35,14 +38,14 @@ export function HallScreen() {
|
||||
<button
|
||||
type="button"
|
||||
className="relative flex size-9 shrink-0 items-center justify-center rounded-full text-[#1d57b7] hover:bg-[#f4f7fb]"
|
||||
aria-label="Notifications"
|
||||
aria-label={t("navigation.notifications")}
|
||||
>
|
||||
<Bell className="size-5" aria-hidden />
|
||||
<span className="absolute right-2 top-2 size-2 rounded-full bg-[#ff143d]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<HallDrawPanel />
|
||||
<HallDrawPanel />
|
||||
|
||||
<HallWalletStrip />
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Wallet } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -18,6 +19,7 @@ import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
||||
export function HallWalletStrip() {
|
||||
const profile = usePlayerSessionStore((s) => s.profile);
|
||||
const mode = useNetworkConnectionStore((s) => s.mode);
|
||||
const { t } = useTranslation("player");
|
||||
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const degradedWalletPollRef = useRef<number | null>(null);
|
||||
@@ -83,7 +85,7 @@ export function HallWalletStrip() {
|
||||
const availableMinor = Number(balance?.available_balance ?? 0);
|
||||
|
||||
return (
|
||||
<section className="mb-4 space-y-3" aria-label="Wallet balance">
|
||||
<section className="mb-4 space-y-3" aria-label={t("wallet.balance")}>
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-xl bg-[#e5002c] px-4 py-4 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
|
||||
@@ -95,7 +97,7 @@ export function HallWalletStrip() {
|
||||
<Wallet className="size-7" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-semibold text-white/90">Wallet Balance</p>
|
||||
<p className="text-sm font-semibold text-white/90">{t("wallet.balance")}</p>
|
||||
{loading ? (
|
||||
<Skeleton className="mt-2 h-8 w-44 rounded-md bg-white/25" />
|
||||
) : (
|
||||
@@ -111,7 +113,7 @@ export function HallWalletStrip() {
|
||||
<TransferInDialog
|
||||
idPrefix="hall-"
|
||||
triggerVariant="hall"
|
||||
triggerLabel="Transfer In"
|
||||
triggerLabel={t("wallet.transferIn")}
|
||||
triggerClassName="h-12 rounded-lg text-base font-bold"
|
||||
currency={currency}
|
||||
lotteryMinor={lotteryMinor}
|
||||
@@ -120,7 +122,7 @@ export function HallWalletStrip() {
|
||||
<TransferOutDialog
|
||||
idPrefix="hall-"
|
||||
triggerVariant="hall"
|
||||
triggerLabel="Transfer Out"
|
||||
triggerLabel={t("wallet.transferOut")}
|
||||
triggerClassName="h-12 rounded-lg text-base font-bold"
|
||||
currency={currency}
|
||||
availableMinor={availableMinor}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { getDrawCurrent } from "@/api/draw";
|
||||
import { getLotteryEcho } from "@/lib/lottery-echo";
|
||||
@@ -76,17 +76,11 @@ export function useHallDrawLive(): {
|
||||
setRaw(d);
|
||||
setEmittedAtMs(Date.now());
|
||||
} catch {
|
||||
setError("加载失败,请下拉刷新");
|
||||
setError("draw.loadFailedRefresh");
|
||||
setRaw(undefined);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// WebSocket 正常时的轮询间隔(作为备用)
|
||||
const refreshMs = useMemo(() => {
|
||||
if (raw === undefined) return 30_000;
|
||||
return raw ? 60_000 : 30_000; // WebSocket正常时减少轮询频率
|
||||
}, [raw]);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
@@ -199,14 +193,22 @@ export function useHallDrawLive(): {
|
||||
if (!isWebSocketConnected && mode !== "websocket") {
|
||||
const currentPollingId = useNetworkConnectionStore.getState().drawPollingIntervalId;
|
||||
if (!currentPollingId) {
|
||||
// 立即执行一次
|
||||
void load();
|
||||
const initialLoadId = window.setTimeout(() => {
|
||||
void load();
|
||||
}, 0);
|
||||
|
||||
// 设置30秒轮询
|
||||
intervalId = window.setInterval(() => {
|
||||
void load();
|
||||
}, 30_000);
|
||||
setDrawPollingIntervalId(intervalId);
|
||||
return () => {
|
||||
window.clearTimeout(initialLoadId);
|
||||
if (intervalId) {
|
||||
window.clearInterval(intervalId);
|
||||
setDrawPollingIntervalId(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user