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