feat: 优化下注结果展示与大厅表单交互,适配新端口配置
This commit is contained in:
@@ -14,7 +14,7 @@ pnpm dev
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
Open [http://localhost:3800](http://localhost:3800) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --port 3800",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
<div class="control-group">
|
||||
<label>彩票系统地址:</label>
|
||||
<input type="text" id="lotteryUrl" value="http://localhost:3000/hall?token=">
|
||||
<input type="text" id="lotteryUrl" value="http://localhost:3800/hall?token=">
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
@@ -166,7 +166,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const lotteryOrigin = 'http://localhost:3000';
|
||||
const lotteryOrigin = 'http://localhost:3800';
|
||||
let currentToken = null;
|
||||
let tokenExpiryTime = null;
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
|
||||
const allowedOrigins = [
|
||||
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
|
||||
process.env.NEXT_PUBLIC_PARENT_ORIGIN,
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:3800",
|
||||
"http://127.0.0.1:3800",
|
||||
].filter(Boolean);
|
||||
|
||||
if (
|
||||
|
||||
@@ -87,7 +87,7 @@ function Calendar({
|
||||
: "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none",
|
||||
|
||||
@@ -171,7 +171,7 @@ export function HallBetPreviewDialog({
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-[#dfe8f6]">
|
||||
<table className="min-w-[520px] w-full border-collapse text-xs">
|
||||
<table className="min-w-[640px] w-full border-collapse text-xs">
|
||||
<thead className="bg-[#f4f7fd] text-[#304f86]">
|
||||
<tr>
|
||||
<th className="w-10 border-r border-[#dfe8f6] px-2 py-3 text-center font-black">No.</th>
|
||||
@@ -187,6 +187,9 @@ export function HallBetPreviewDialog({
|
||||
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
|
||||
{t("hall.preview.rebate")}
|
||||
</th>
|
||||
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
|
||||
{t("hall.preview.estimatedMax")}
|
||||
</th>
|
||||
<th className="px-2 py-3 text-center font-black">
|
||||
{t("hall.preview.actual")}
|
||||
</th>
|
||||
@@ -213,6 +216,9 @@ export function HallBetPreviewDialog({
|
||||
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold tabular-nums text-emerald-600">
|
||||
-{formatMinorAsCurrency(ln.rebate_amount, currencyCode).replace(`${currencyCode} `, "")}
|
||||
</td>
|
||||
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-black tabular-nums text-[#e5002c]">
|
||||
{formatMinorAsCurrency(ln.estimated_max_payout, currencyCode)}
|
||||
</td>
|
||||
<td className="px-2 py-3 text-center font-black tabular-nums text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(ln.actual_deduct_amount, currencyCode)}
|
||||
</td>
|
||||
@@ -223,7 +229,7 @@ export function HallBetPreviewDialog({
|
||||
</div>
|
||||
|
||||
{summary ? (
|
||||
<div className="grid grid-cols-3 overflow-hidden rounded-xl border border-[#dfe8f6] bg-[#f8fbff] text-center text-xs">
|
||||
<div className="grid grid-cols-2 overflow-hidden rounded-xl border border-[#dfe8f6] bg-[#f8fbff] text-center text-xs sm:grid-cols-4">
|
||||
<div className="border-r border-[#dfe8f6] px-2 py-3">
|
||||
<p className="font-bold text-[#304f86]">{t("hall.preview.totalBet")}</p>
|
||||
<p className="mt-1 font-black tabular-nums text-[#0b3f96]">
|
||||
@@ -240,12 +246,18 @@ export function HallBetPreviewDialog({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-2 py-3">
|
||||
<div className="border-r border-[#dfe8f6] px-2 py-3">
|
||||
<p className="font-bold text-[#304f86]">{t("hall.preview.actualDeduct")}</p>
|
||||
<p className="mt-1 font-black tabular-nums text-[#e5002c]">
|
||||
{formatMinorAsCurrency(summary.total_actual_deduct, currencyCode)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-2 py-3">
|
||||
<p className="font-bold text-[#304f86]">{t("hall.preview.estimatedPayout")}</p>
|
||||
<p className="mt-1 font-black tabular-nums text-[#e5002c]">
|
||||
{formatMinorAsCurrency(summary.total_estimated_payout, currencyCode)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -31,9 +31,10 @@ export function HallBetResultDialog({
|
||||
}: HallBetResultDialogProps) {
|
||||
const { t } = useTranslation("player");
|
||||
|
||||
const totalItems = data?.items.length ?? 0;
|
||||
const totalSuccess = totalItems;
|
||||
const totalFailure = 0;
|
||||
const successItems = data?.items.filter((item) => (item.status ?? "success") === "success") ?? [];
|
||||
const failedItems = data?.items.filter((item) => item.status === "failed") ?? [];
|
||||
const totalSuccess = data?.summary.success_count ?? successItems.length;
|
||||
const totalFailure = data?.summary.failure_count ?? failedItems.length;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -131,7 +132,7 @@ export function HallBetResultDialog({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((item, index) => (
|
||||
{successItems.map((item, index) => (
|
||||
<tr
|
||||
key={`${item.ticket_no}-${index}`}
|
||||
className="border-t border-[#e8eef7] bg-white"
|
||||
@@ -162,7 +163,27 @@ export function HallBetResultDialog({
|
||||
<div className="rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-3 text-sm font-semibold text-emerald-700">
|
||||
{t("hall.result.noFailures", { defaultValue: "本次提交没有失败注项。" })}
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="space-y-2 rounded-lg border border-rose-100 bg-rose-50 px-3 py-3">
|
||||
<p className="text-sm font-black text-[#e5002c]">
|
||||
{t("hall.result.failedItems", { defaultValue: "失败注项明细" })}
|
||||
</p>
|
||||
{failedItems.map((item, index) => (
|
||||
<div
|
||||
key={`${item.ticket_no}-${index}`}
|
||||
className="flex items-center justify-between gap-3 rounded-md bg-white px-3 py-2 text-xs"
|
||||
>
|
||||
<span className="min-w-0 truncate font-semibold text-slate-700">
|
||||
<span className="font-mono font-black text-slate-950">{item.number}</span>{" "}
|
||||
{playLabel(item.play_code, t)}
|
||||
</span>
|
||||
<span className="shrink-0 font-bold text-[#e5002c]">
|
||||
{item.fail_reason_text ?? item.fail_reason_code ?? t("hall.result.failed", { defaultValue: "失败" })}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -44,12 +44,20 @@ type DraftRow = {
|
||||
type DraftEntry = {
|
||||
rowId: string;
|
||||
rowNo: number;
|
||||
amountKey: string;
|
||||
play: PlayEffectivePlayRow;
|
||||
digitSlot?: number;
|
||||
number: string;
|
||||
amountMinor: number;
|
||||
line: TicketLineInput;
|
||||
};
|
||||
|
||||
type PlayColumn = {
|
||||
key: string;
|
||||
play: PlayEffectivePlayRow;
|
||||
digitSlot?: number;
|
||||
};
|
||||
|
||||
type ClosedPlayCleanupData = {
|
||||
cleanup_hint?: string;
|
||||
cleanup_lines?: Array<{ client_line_no?: number; play_code?: string }>;
|
||||
@@ -113,6 +121,42 @@ function pickDisplayName(row: PlayEffectivePlayRow): string {
|
||||
return row.display_name_en ?? row.display_name_zh ?? row.play_code;
|
||||
}
|
||||
|
||||
function digitSlotOptions(category: Exclude<HallCategory, "JACKPOT">): number[] {
|
||||
if (category === "D2") return [2, 3];
|
||||
if (category === "D3") return [1, 2, 3];
|
||||
return [0, 1, 2, 3];
|
||||
}
|
||||
|
||||
function digitSlotLabel(category: Exclude<HallCategory, "JACKPOT">, slot: number): string {
|
||||
const labels: Record<Exclude<HallCategory, "JACKPOT">, Record<number, string>> = {
|
||||
D2: { 2: "十", 3: "个" },
|
||||
D3: { 1: "百", 2: "十", 3: "个" },
|
||||
D4: { 0: "千", 1: "百", 2: "十", 3: "个" },
|
||||
};
|
||||
return labels[category][slot] ?? String(slot + 1);
|
||||
}
|
||||
|
||||
function amountKeyForPlay(playCode: string, digitSlot?: number): string {
|
||||
return digitSlot === undefined ? playCode : `${playCode}@${digitSlot}`;
|
||||
}
|
||||
|
||||
function playColumnsForCategory(
|
||||
plays: PlayEffectivePlayRow[],
|
||||
category: Exclude<HallCategory, "JACKPOT">,
|
||||
): PlayColumn[] {
|
||||
return plays.flatMap((play) => {
|
||||
if (!playNeedsDigitSlot(play.play_code)) {
|
||||
return [{ key: amountKeyForPlay(play.play_code), play }];
|
||||
}
|
||||
|
||||
return digitSlotOptions(category).map((digitSlot) => ({
|
||||
key: amountKeyForPlay(play.play_code, digitSlot),
|
||||
play,
|
||||
digitSlot,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function inferCategory(row: PlayEffectivePlayRow): Exclude<HallCategory, "JACKPOT"> {
|
||||
if (row.play_code.startsWith("pos_2")) return "D2";
|
||||
if (row.play_code.startsWith("pos_3")) return "D3";
|
||||
@@ -159,16 +203,12 @@ function normalizeNumberForPlay(number: string, playCode: string): string {
|
||||
return number;
|
||||
}
|
||||
|
||||
function pickDigitSlot(category: HallCategory): number {
|
||||
if (category === "D2") return 3;
|
||||
return 3;
|
||||
}
|
||||
|
||||
function lineForPlay(
|
||||
category: Exclude<HallCategory, "JACKPOT">,
|
||||
play: PlayEffectivePlayRow,
|
||||
displayNumber: string,
|
||||
amountMinor: number,
|
||||
digitSlot?: number,
|
||||
): TicketLineInput | null {
|
||||
const number = normalizeNumberForPlay(displayNumber, play.play_code);
|
||||
const spec = ticketNumberSpec(play.play_code);
|
||||
@@ -186,7 +226,8 @@ function lineForPlay(
|
||||
line.dimension = category;
|
||||
}
|
||||
if (playNeedsDigitSlot(play.play_code)) {
|
||||
line.digit_slot = pickDigitSlot(category);
|
||||
if (digitSlot === undefined) return null;
|
||||
line.digit_slot = digitSlot;
|
||||
}
|
||||
|
||||
return line;
|
||||
@@ -256,6 +297,7 @@ function matchesRiskAlert(
|
||||
playCode: string,
|
||||
rowNumber: string,
|
||||
category: Exclude<HallCategory, "JACKPOT">,
|
||||
digitSlot?: number,
|
||||
): boolean {
|
||||
const normalizedRow = rowNumber.toUpperCase();
|
||||
|
||||
@@ -297,7 +339,8 @@ function matchesRiskAlert(
|
||||
: ["0", "2", "4", "6", "8"].includes(last);
|
||||
}
|
||||
if (playCode === "digit_big" || playCode === "digit_small") {
|
||||
const last = alertNumber[pickDigitSlot(category)] ?? "";
|
||||
const slot = digitSlot ?? digitSlotOptions(category).at(-1) ?? 3;
|
||||
const last = alertNumber[slot] ?? "";
|
||||
return playCode === "digit_big"
|
||||
? ["5", "6", "7", "8", "9"].includes(last)
|
||||
: ["0", "1", "2", "3", "4"].includes(last);
|
||||
@@ -311,6 +354,7 @@ function cellRiskState(
|
||||
rowNumber: string,
|
||||
category: Exclude<HallCategory, "JACKPOT">,
|
||||
alertRows: DrawCurrentRiskPoolAlert[] | undefined,
|
||||
digitSlot?: number,
|
||||
): CellRiskState {
|
||||
const alerts = alertRows ?? [];
|
||||
if (alerts.length === 0) return "open";
|
||||
@@ -318,7 +362,7 @@ function cellRiskState(
|
||||
if (!normalizedRow) return "open";
|
||||
|
||||
for (const alert of alerts) {
|
||||
if (matchesRiskAlert(alert.normalized_number, play.play_code, normalizedRow, category)) {
|
||||
if (matchesRiskAlert(alert.normalized_number, play.play_code, normalizedRow, category, digitSlot)) {
|
||||
return alert.is_sold_out ? "sold_out" : "warning";
|
||||
}
|
||||
}
|
||||
@@ -427,6 +471,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
);
|
||||
}, [activeCategory, catalogState, openPlays]);
|
||||
|
||||
const playColumns = useMemo(() => {
|
||||
if (activeCategory === "JACKPOT") return [];
|
||||
return playColumnsForCategory(categoryPlays, activeCategory);
|
||||
}, [activeCategory, categoryPlays]);
|
||||
|
||||
const activeRow = useMemo(
|
||||
() => rows.find((row) => row.id === activeRowId) ?? rows[0] ?? null,
|
||||
[activeRowId, rows],
|
||||
@@ -532,15 +581,17 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
if (activeCategory === "JACKPOT") return [];
|
||||
const entries: DraftEntry[] = [];
|
||||
rows.forEach((row, rowIndex) => {
|
||||
categoryPlays.forEach((play) => {
|
||||
const amount = parseDecimalInputToMinor(row.amounts[play.play_code] ?? "");
|
||||
playColumns.forEach((column) => {
|
||||
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "");
|
||||
if (amount === null || amount <= 0) return;
|
||||
const line = lineForPlay(activeCategory, play, row.number, amount);
|
||||
const line = lineForPlay(activeCategory, column.play, row.number, amount, column.digitSlot);
|
||||
if (!line) return;
|
||||
entries.push({
|
||||
rowId: row.id,
|
||||
rowNo: rowIndex + 1,
|
||||
play,
|
||||
amountKey: column.key,
|
||||
play: column.play,
|
||||
digitSlot: column.digitSlot,
|
||||
number: row.number,
|
||||
amountMinor: amount,
|
||||
line,
|
||||
@@ -548,7 +599,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
});
|
||||
});
|
||||
return entries;
|
||||
}, [activeCategory, categoryPlays, rows]);
|
||||
}, [activeCategory, playColumns, rows]);
|
||||
|
||||
const draftEntries = collectEntries();
|
||||
const draftSummary = useMemo(() => {
|
||||
@@ -587,7 +638,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
if (!Number.isInteger(clientLineNo) || clientLineNo <= 0 || playCode.trim() === "") return;
|
||||
const entry = entries[clientLineNo - 1];
|
||||
if (!entry) return;
|
||||
cleanupPairs.add(`${entry.rowId}::${playCode}`);
|
||||
cleanupPairs.add(`${entry.rowId}::${entry.amountKey}`);
|
||||
});
|
||||
|
||||
if (cleanupPairs.size === 0) return false;
|
||||
@@ -596,9 +647,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
current.map((row) => {
|
||||
const nextAmounts = { ...row.amounts };
|
||||
let changed = false;
|
||||
Object.keys(nextAmounts).forEach((playCode) => {
|
||||
if (!cleanupPairs.has(`${row.id}::${playCode}`)) return;
|
||||
nextAmounts[playCode] = "";
|
||||
Object.keys(nextAmounts).forEach((amountKey) => {
|
||||
if (!cleanupPairs.has(`${row.id}::${amountKey}`)) return;
|
||||
nextAmounts[amountKey] = "";
|
||||
changed = true;
|
||||
});
|
||||
return changed ? { ...row, amounts: nextAmounts } : row;
|
||||
@@ -699,6 +750,15 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
amount: formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode),
|
||||
}),
|
||||
);
|
||||
if ((data.summary.failure_count ?? 0) > 0) {
|
||||
toast.warning(
|
||||
t("hall.placePartialFailed", {
|
||||
success: data.summary.success_count ?? 0,
|
||||
failed: data.summary.failure_count ?? 0,
|
||||
defaultValue: "{{success}} 个成功,{{failed}} 个失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
const code = e instanceof LotteryApiBizError ? e.code : 0;
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : t("hall.placeFailed");
|
||||
@@ -945,9 +1005,14 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
<span className="block">{t("hall.table.number", { defaultValue: "Number" })}</span>
|
||||
<span className="block text-[9px] font-medium text-[#9aa8bd]">({numberPlaceholder})</span>
|
||||
</th>
|
||||
{categoryPlays.map((play) => (
|
||||
<th key={play.play_code} className="min-w-16 px-1 py-2 text-center font-bold">
|
||||
<span className="block truncate">{pickDisplayName(play)}</span>
|
||||
{playColumns.map((column) => (
|
||||
<th key={column.key} className="min-w-16 px-1 py-2 text-center font-bold">
|
||||
<span className="block truncate">
|
||||
{pickDisplayName(column.play)}
|
||||
{column.digitSlot !== undefined
|
||||
? `-${digitSlotLabel(activeCategory, column.digitSlot)}`
|
||||
: ""}
|
||||
</span>
|
||||
<span className="block text-[9px] font-medium text-[#9aa8bd]">
|
||||
{t("hall.table.amountPlaceholder", { defaultValue: "金额" })}
|
||||
</span>
|
||||
@@ -976,20 +1041,22 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
className="h-8 rounded-md border-[#e1e8f3] bg-white px-1 text-center font-mono text-sm font-black tracking-[0.1em] text-slate-950 shadow-sm focus-visible:ring-[#1d57b7]"
|
||||
/>
|
||||
</td>
|
||||
{categoryPlays.map((play) => {
|
||||
const amountText = row.amounts[play.play_code] ?? "";
|
||||
{playColumns.map((column) => {
|
||||
const { play } = column;
|
||||
const amountText = row.amounts[column.key] ?? "";
|
||||
const status = cellRiskState(
|
||||
play,
|
||||
row.number,
|
||||
activeCategory as Exclude<HallCategory, "JACKPOT">,
|
||||
alertRows,
|
||||
column.digitSlot,
|
||||
);
|
||||
const disabled = tableDisabled || status === "sold_out" || (play.config !== null && !play.config.is_enabled);
|
||||
const hasAmount = amountText.trim().length > 0;
|
||||
|
||||
return (
|
||||
<td
|
||||
key={`${rowKey}-${play.play_code}`}
|
||||
key={`${rowKey}-${column.key}`}
|
||||
className={cn(
|
||||
"px-1 py-2 align-top",
|
||||
status === "warning" && "bg-amber-50/70",
|
||||
@@ -1007,7 +1074,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
||||
}
|
||||
onFocus={() => setActiveRowId(row.id)}
|
||||
onClick={() => setActiveRowId(row.id)}
|
||||
onChange={(event) => updateAmount(row.id, play.play_code, event.target.value)}
|
||||
onChange={(event) => updateAmount(row.id, column.key, event.target.value)}
|
||||
className={cn(
|
||||
"h-8 rounded-md border-[#e1e8f3] bg-white px-1 text-center text-xs font-bold tabular-nums shadow-sm focus-visible:ring-[#1d57b7]",
|
||||
hasAmount && "border-[#9bbcff] bg-[#f5f9ff] text-[#0b3f96]",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Hourglass, Landmark, TimerReset } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -49,20 +49,18 @@ function CloseTime({
|
||||
}) {
|
||||
const { t } = useTranslation("player");
|
||||
const sealedCountdown = isHallSealedCountdownUi(payload.status);
|
||||
const [nowMs, setNowMs] = useState(serverNowMs);
|
||||
|
||||
useEffect(() => {
|
||||
setNowMs(serverNowMs);
|
||||
}, [serverNowMs]);
|
||||
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = window.setInterval(() => {
|
||||
setNowMs((current) => current + 1000);
|
||||
setElapsedSeconds((current) => current + 1);
|
||||
}, 1000);
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const nowMs = serverNowMs + elapsedSeconds * 1000;
|
||||
|
||||
let seconds = 0;
|
||||
let label = t("draw.closesIn");
|
||||
|
||||
@@ -150,7 +148,12 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot })
|
||||
<CurrentTime payload={display} />
|
||||
</div>
|
||||
<div className="relative flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center">
|
||||
<CloseTime serverNowMs={serverNowMs} hud={hud} payload={display} />
|
||||
<CloseTime
|
||||
key={`${display.draw_no}-${display.status}-${serverNowMs}`}
|
||||
serverNowMs={serverNowMs}
|
||||
hud={hud}
|
||||
payload={display}
|
||||
/>
|
||||
<Hourglass
|
||||
className={cn(
|
||||
"absolute right-2 top-1/2 size-5 -translate-y-1/2",
|
||||
|
||||
@@ -43,6 +43,10 @@ export function TicketOrdersListScreen() {
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation("player");
|
||||
const drawNoFilter = useMemo(() => (searchParams.get("draw_no") ?? "").trim(), [searchParams]);
|
||||
const statusFilter = useMemo(
|
||||
() => searchParams.getAll("status").map((s) => s.trim()).filter(Boolean),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const [items, setItems] = useState<TicketItemListRow[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -53,7 +57,7 @@ export function TicketOrdersListScreen() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [queryDrawNo, setQueryDrawNo] = useState(drawNoFilter);
|
||||
const [queryNumber, setQueryNumber] = useState("");
|
||||
const [queryStatuses, setQueryStatuses] = useState<string[]>([]);
|
||||
const [queryStatuses, setQueryStatuses] = useState<string[]>(statusFilter);
|
||||
const [fromDate, setFromDate] = useState("");
|
||||
const [toDate, setToDate] = useState("");
|
||||
const [rangeOpen, setRangeOpen] = useState(false);
|
||||
@@ -179,7 +183,7 @@ export function TicketOrdersListScreen() {
|
||||
setQueryNumber("");
|
||||
setFromDate("");
|
||||
setToDate("");
|
||||
setQueryStatuses([]);
|
||||
setQueryStatuses(statusFilter);
|
||||
setRangeOpen(false);
|
||||
setStatusOpen(false);
|
||||
}}
|
||||
|
||||
@@ -271,7 +271,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
||||
{t("results.hitHint")}
|
||||
</p>
|
||||
<Link
|
||||
href={`/orders?draw_no=${encodeURIComponent(data.draw_no)}`}
|
||||
href={`/orders?draw_no=${encodeURIComponent(data.draw_no)}&status=settled_win`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default", size: "sm" }),
|
||||
"mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028] sm:w-auto",
|
||||
|
||||
@@ -268,7 +268,7 @@ export function DrawResultsListScreen() {
|
||||
<div className="pt-4">
|
||||
<TwentyThreeResultsGrid numbers={featured.results} />
|
||||
<Link
|
||||
href={`/orders?draw_no=${encodeURIComponent(featured.draw_no)}`}
|
||||
href={`/orders?draw_no=${encodeURIComponent(featured.draw_no)}&status=settled_win`}
|
||||
className="mt-4 inline-flex h-10 w-full items-center justify-center rounded-xl bg-[#e5002c] px-4 text-sm font-bold text-white transition-colors hover:bg-[#d10028]"
|
||||
>
|
||||
{t("results.viewMyWinning")}
|
||||
|
||||
@@ -121,8 +121,8 @@ export function useTokenRefresh(): {
|
||||
const allowedOrigins = [
|
||||
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
|
||||
// 开发环境允许本地
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:3800",
|
||||
"http://127.0.0.1:3800",
|
||||
].filter(Boolean);
|
||||
|
||||
if (
|
||||
|
||||
@@ -9,8 +9,8 @@ const ALLOWED_PARENT_ORIGINS: string[] = [
|
||||
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
|
||||
process.env.NEXT_PUBLIC_PARENT_ORIGIN,
|
||||
// 开发环境
|
||||
"http://localhost:3001",
|
||||
"http://127.0.0.1:3001",
|
||||
"http://localhost:3801",
|
||||
"http://127.0.0.1:3801",
|
||||
// 生产环境应从环境变量读取
|
||||
].filter((o): o is string => Boolean(o));
|
||||
|
||||
|
||||
@@ -65,5 +65,6 @@ export type TicketDrawMyMatchPayload = {
|
||||
hit_numbers_4d: string[];
|
||||
total_win_minor: number;
|
||||
total_jackpot_win_minor: number;
|
||||
winning_ticket_count: number;
|
||||
has_bets: boolean;
|
||||
};
|
||||
|
||||
@@ -66,12 +66,18 @@ export type TicketPlaceItem = {
|
||||
actual_deduct_amount: number;
|
||||
estimated_max_payout: number;
|
||||
combination_count: number;
|
||||
status?: "success" | "failed" | string;
|
||||
fail_reason_code?: string | null;
|
||||
fail_reason_text?: string | null;
|
||||
};
|
||||
|
||||
export type TicketPlaceData = {
|
||||
order_no: string;
|
||||
draw: { draw_id: string; status: string };
|
||||
summary: TicketPreviewData["summary"];
|
||||
summary: TicketPreviewData["summary"] & {
|
||||
success_count?: number;
|
||||
failure_count?: number;
|
||||
};
|
||||
balance_after: number;
|
||||
items: TicketPlaceItem[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user