feat: 优化下注结果展示与大厅表单交互,适配新端口配置
This commit is contained in:
@@ -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]",
|
||||
|
||||
Reference in New Issue
Block a user