feat: 增强投注结果弹窗与投注网格功能

更新 HallBetResultDialog:根据成功与失败数量显示不同的结果图标与标题。
优化 HallBettingGrid:新增 place trace ID 管理机制,更好地处理投注提交流程,并防止重复扣费。
增强注单详情页面:针对临时状态中的注单自动刷新,提升用户体验。
新增多语言翻译:补充投注结果与状态相关文案,支持更完善的用户反馈。
This commit is contained in:
2026-05-26 14:50:21 +08:00
parent d768a3f6ba
commit 51b2a36cc5
11 changed files with 257 additions and 90 deletions

View File

@@ -450,6 +450,17 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
number: null,
longPress: false,
});
/** 单次预览→确认共用,重试 place 复用,避免重复扣款 */
const placeTraceIdRef = useRef<string | null>(null);
const newPlaceTraceId = (): string =>
typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: `pl-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const clearPlaceTraceId = () => {
placeTraceIdRef.current = null;
};
const loadCatalog = useCallback(async () => {
setCatalogState((s) => (s.kind === "ok" ? s : { kind: "loading" }));
try {
@@ -466,7 +477,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const wallet = await getWalletBalance({ currency: currencyParam });
setAvailableMinor(Number(wallet.available_balance ?? 0));
} catch {
setAvailableMinor(0);
// 保留上次可用余额,避免短暂失败导致误报余额不足
}
}, [currencyParam]);
@@ -892,16 +903,17 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
return;
}
if (previewLoading || placeLoading) {
return;
}
setPreviewLoading(true);
try {
placeTraceIdRef.current = newPlaceTraceId();
const data = await postTicketPreview({
draw_id: display.draw_no,
currency_code: currencyCode,
client_trace_id: `pv-${
typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: String(Date.now())
}`,
client_trace_id: placeTraceIdRef.current,
lines,
});
setPreviewData(data);
@@ -925,6 +937,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const handlePlace = async () => {
if (!display || !previewData) return;
if (placeLoading) {
return;
}
if (!isBettable) {
toast.error(t("hall.closedSubmit"));
return;
@@ -942,18 +957,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
return;
}
const traceId = placeTraceIdRef.current ?? newPlaceTraceId();
placeTraceIdRef.current = traceId;
setPlaceLoading(true);
try {
const data = await postTicketPlace({
draw_id: display.draw_no,
currency_code: currencyCode,
client_trace_id:
typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: `pl-${Date.now()}`,
client_trace_id: traceId,
lines,
expected_config_versions: previewData.config_versions,
});
clearPlaceTraceId();
setPreviewOpen(false);
setPreviewData(null);
setResultData(data);
@@ -963,17 +979,26 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
triggerWalletPollingAfterBet();
void refreshWallet();
void reloadDraw();
toast.success(
t("hall.placeSuccess", {
orderNo: data.order_no,
amount: formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode),
}),
);
if ((data.summary.failure_count ?? 0) > 0) {
const failureCount = data.summary.failure_count ?? 0;
const successCount = data.summary.success_count ?? 0;
if (failureCount > 0 && successCount === 0) {
toast.error(
t("hall.placeAllFailed", {
failed: failureCount,
}),
);
} else if (failureCount > 0) {
toast.warning(
t("hall.placePartialFailed", {
success: data.summary.success_count ?? 0,
failed: data.summary.failure_count ?? 0,
success: successCount,
failed: failureCount,
}),
);
} else {
toast.success(
t("hall.placeSuccess", {
orderNo: data.order_no,
amount: formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode),
}),
);
}
@@ -1268,7 +1293,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
className="w-[4.5rem] min-w-[4.5rem] max-w-[4.5rem] px-0.5 py-2 text-center font-bold"
>
<span className="block whitespace-nowrap text-[10px] leading-tight">
{playColumnHeaderLabel(column.play, activeCategory, column.digitSlot, t)}
{playColumnHeaderLabel(
column.play,
activeCategory as Exclude<HallCategory, "JACKPOT">,
column.digitSlot,
t,
)}
</span>
<span className="mt-0.5 block text-[9px] font-medium text-[#9aa8bd]">
{t("hall.table.amountPlaceholder")}
@@ -1451,7 +1481,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
open={previewOpen}
onOpenChange={(open) => {
setPreviewOpen(open);
if (!open) setPreviewData(null);
if (!open) {
setPreviewData(null);
if (!placeLoading) {
clearPlaceTraceId();
}
}
}}
currencyCode={currencyCode}
data={previewData}