feat: 优化下注结果展示与大厅表单交互,适配新端口配置

This commit is contained in:
2026-05-18 11:28:41 +08:00
parent 5f5ce6c29d
commit 418b446c09
16 changed files with 170 additions and 56 deletions

View File

@@ -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]",