feat: enhance wallet strip for mobile responsiveness
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- Added mobile-specific styles to the HallWalletStrip component for better usability on smaller screens.
- Adjusted padding, font sizes, and spacing based on device type.
- Introduced a new hook `useIsMobile` to determine the device type.

feat: sort ticket items by provider code

- Updated the sorting logic in `sortTicketGroupItems` to include provider code for better organization of ticket items.

feat: display provider information in ticket order details

- Added a `providerLabel` function to format provider information.
- Updated `TicketOrderDetailScreen` and `TicketOrdersListScreen` to display provider name and code.

feat: enhance ticket item types with provider information

- Extended `TicketItemListRow` and `TicketItemDetailPayload` types to include optional provider code and name.

feat: add API for fetching bet providers

- Created a new API endpoint to retrieve bet providers.
- Defined types for bet provider data in `bet-provider.ts`.

chore: update player spacing constants

- Adjusted padding values in `player-spacing.ts` for improved layout consistency.
This commit is contained in:
2026-07-08 15:57:09 +08:00
parent 45376473f3
commit add3d1fc05
17 changed files with 619 additions and 104 deletions

6
src/api/bet-providers.ts Normal file
View File

@@ -0,0 +1,6 @@
import { lotteryRequest } from "@/lib/lottery-http";
import type { BetProvidersData } from "@/types/api/bet-provider";
export function getBetProviders(): Promise<BetProvidersData> {
return lotteryRequest.get<BetProvidersData>("/bet-providers");
}

View File

@@ -144,7 +144,7 @@
}
}
/* 下注表格横向滚动:右侧渐变提示可继续滑动 */
/* 下注表格横向滚动 */
.player-table-scroll {
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
@@ -154,17 +154,6 @@
position: relative;
}
.player-table-scroll-wrap::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 1.25rem;
pointer-events: none;
background: linear-gradient(to left, rgba(255, 255, 255, 0.95), transparent);
}
/* 玩家端侧栏:浅色底 + 与后台一致的红色激活态 */
.player-sidebar {
--sidebar: #ffffff;
@@ -330,4 +319,4 @@
to {
transform: rotate(360deg);
}
}
}

View File

@@ -25,7 +25,7 @@ export function PlayerPanel({
<div className={cn(playerContentColumnClass, containerClassName)}>
<section
className={cn(
"bg-white text-slate-900 lg:rounded-2xl lg:border lg:border-[#e4ebf5] lg:shadow-sm",
"bg-white text-slate-900 lg:bg-transparent",
playerPageShellPadding,
className,
)}
@@ -34,4 +34,4 @@ export function PlayerPanel({
</section>
</div>
);
}
}

View File

@@ -61,6 +61,10 @@ function WarningsBlock({ warnings }: { warnings: TicketPreviewWarning[] }) {
);
}
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
return providerName || providerCode || "-";
}
function SubmittingPanel() {
const { t } = useTranslation("player");
@@ -206,7 +210,7 @@ export function HallBetPreviewDialog({
) : null}
<div className="overflow-x-auto rounded-xl border border-[#dfe8f6]">
<table className="min-w-[640px] w-full border-collapse text-xs">
<table className="min-w-[700px] 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>
@@ -216,6 +220,9 @@ export function HallBetPreviewDialog({
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("orders.play")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.providers.title", { defaultValue: "开注商" })}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.preview.amount")}
</th>
@@ -245,6 +252,11 @@ export function HallBetPreviewDialog({
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold text-slate-700">
{ln.play_code}
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center">
<span className="inline-flex rounded-full bg-[#eef5ff] px-2 py-0.5 font-black text-[#0b56b7]">
{providerLabel(ln.provider_name, ln.provider_code)}
</span>
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold tabular-nums">
{formatMinorAsCurrency(ln.total_bet_amount, currencyCode)}
</td>

View File

@@ -29,6 +29,10 @@ type HallBetResultDialogProps = {
const SUCCESS_ITEM_STATUSES = new Set(["pending_draw", "placed"]);
const FAILURE_ITEM_STATUSES = new Set(["failed", "refunded"]);
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
return providerName || providerCode || "-";
}
export function HallBetResultDialog({
open,
onOpenChange,
@@ -202,7 +206,7 @@ export function HallBetResultDialog({
{t("hall.result.items")}
</p>
<div className="overflow-x-auto rounded-xl border border-[#dfe8f6]">
<table className="min-w-[430px] w-full border-collapse text-xs">
<table className="min-w-[520px] w-full border-collapse text-xs">
<thead className="bg-[#f4f7fd] text-[#304f86]">
<tr>
<th className="w-10 border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
@@ -214,6 +218,9 @@ export function HallBetResultDialog({
<th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("orders.play")}
</th>
<th className="border-r border-[#dfe8f6] px-2 py-2.5 text-center font-black">
{t("hall.providers.title", { defaultValue: "开注商" })}
</th>
<th className="px-2 py-2.5 text-center font-black">
{t("hall.result.actualDeduct")}
</th>
@@ -239,6 +246,11 @@ export function HallBetResultDialog({
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold text-[#0755c7]">
{playLabel(item.play_code, t)}
</td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center">
<span className="inline-flex rounded-full bg-[#eef5ff] px-2 py-0.5 font-black text-[#0b56b7]">
{providerLabel(item.provider_name, item.provider_code)}
</span>
</td>
<td className="px-2 py-3 text-center font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(item.actual_deduct_amount, currencyCode)}
</td>
@@ -265,7 +277,7 @@ export function HallBetResultDialog({
>
<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)}
{playLabel(item.play_code, t)} · {providerLabel(item.provider_name, item.provider_code)}
</span>
<span className="shrink-0 font-bold text-[#e5002c]">
{item.fail_reason_text ?? item.fail_reason_code ?? t("hall.result.failed")}

View File

@@ -1,10 +1,11 @@
"use client";
import { CirclePlus, Lock, Ticket, Trash2, Star } from "lucide-react";
import { CirclePlus, ChevronDown, ChevronUp, Lock, Ticket, Trash2, Star } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState, memo } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { getBetProviders } from "@/api/bet-providers";
import { getPlayEffective } from "@/api/play";
import { getWalletBalance } from "@/api/wallet";
import { postTicketPlace, postTicketPreview } from "@/api/ticket";
@@ -30,6 +31,7 @@ import { usePlayerSessionStore } from "@/stores/player-session-store";
import { getLotteryEcho } from "@/lib/lottery-echo";
import { formatMinorAsCurrency, parseDecimalInputToMinor } from "@/lib/money";
import { playLabel } from "@/lib/play-labels";
import { playerViewportFixedBarClass } from "@/lib/player-viewport";
import {
PLAY_CATALOG_REFRESH_EVENT,
type PlayCatalogRefreshSource,
@@ -41,6 +43,7 @@ import { LotteryApiBizError } from "@/types/api/errors";
import type { PlayEffectivePayload, PlayEffectivePlayRow } from "@/types/api/play-effective";
import type { TicketLineInput, TicketPlaceData, TicketPreviewData } from "@/types/api/ticket";
import type { DrawCurrentRiskPoolAlert } from "@/types/api/draw-current";
import type { BetProviderRow } from "@/types/api/bet-provider";
const MAX_ROWS = 50;
@@ -113,10 +116,16 @@ type RiskWarningWsEvent = {
type CellRiskState = "open" | "warning" | "sold_out";
type QuickFillState = Record<HallCategory, { favorites: string[]; history: string[] }>;
const FALLBACK_BET_PROVIDERS: BetProviderRow[] = [
{ code: "SG", name: "Singapore", sort_order: 10, is_default: true },
{ code: "MY", name: "Malaysia", sort_order: 20, is_default: false },
{ code: "TH", name: "Thailand", sort_order: 30, is_default: false },
];
const categoryTabs: { value: HallCategory; label: string }[] = [
{ value: "D2", label: "2D" },
{ value: "D3", label: "3D" },
{ value: "D4", label: "4D" },
{ value: "D3", label: "3D" },
{ value: "D2", label: "2D" },
];
const D2_PLAY_ORDER = ["pos_2a", "pos_2b", "pos_2c", "pos_2abc"] as const;
@@ -141,6 +150,7 @@ const D4_PLAY_ORDER = [
"digit_big",
"digit_small",
] as const;
const MOBILE_QUICK_AMOUNT_PRESETS = ["10", "50", "100"] as const;
function newDraftRow(): DraftRow {
const id =
@@ -445,7 +455,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const [resultOpen, setResultOpen] = useState(false);
const [resultData, setResultData] = useState<TicketPlaceData | null>(null);
const [activeCategory, setActiveCategory] = useState<Exclude<HallCategory, "JACKPOT">>("D4");
const [betProviders, setBetProviders] = useState<BetProviderRow[]>(FALLBACK_BET_PROVIDERS);
const [selectedProviderCodes, setSelectedProviderCodes] = useState<string[]>([]);
const [quickFillState, setQuickFillState] = useState<QuickFillState>(() => loadQuickFillState());
const [quickFillExpanded, setQuickFillExpanded] = useState(false);
const [providersExpanded, setProvidersExpanded] = useState(false);
const [riskStateDrawNo, setRiskStateDrawNo] = useState<string | null>(display?.draw_no ?? null);
const [liveSoldOutNumbers, setLiveSoldOutNumbers] = useState<Set<string>>(() => new Set());
const [liveWarningNumbers, setLiveWarningNumbers] = useState<Set<string>>(() => new Set());
@@ -501,6 +515,27 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
});
}, [loadCatalog, refreshWallet]);
useEffect(() => {
let cancelled = false;
void getBetProviders()
.then((data) => {
if (cancelled) return;
const items = data.items.length > 0 ? data.items : FALLBACK_BET_PROVIDERS;
setBetProviders(items);
setSelectedProviderCodes((current) => {
const available = new Set(items.map((item) => item.code));
return current.filter((code) => available.has(code));
});
})
.catch(() => {
if (cancelled) return;
setBetProviders(FALLBACK_BET_PROVIDERS);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
const onCatalogRefresh = (ev: Event) => {
void loadCatalog();
@@ -554,13 +589,20 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
return playColumnsForCategory(activeCategoryPlays, activeCategory);
}, [activeCategory, activeCategoryPlays]);
const mobileIndexColClass = "w-[1.75rem] min-w-[1.75rem]";
const mobileNumberColClass = "w-[4.25rem] min-w-[4.25rem]";
const desktopIndexColClass = "w-[2.15rem] min-w-[2.15rem]";
const desktopNumberColClass = "w-[5.5rem] min-w-[5.5rem]";
const indexColClass = isMobile ? mobileIndexColClass : desktopIndexColClass;
const numberColClass = isMobile ? mobileNumberColClass : desktopNumberColClass;
const stickyNumberLeftClass = isMobile ? "left-[1.75rem]" : "left-[2.15rem]";
const amountColClass = !isMobile
? "w-[3.9rem] min-w-[3.9rem]"
: "w-[4rem] min-w-[4rem]";
const tableMinWidthPx = useMemo(() => {
const indexCol = 34;
const numberCol = 88;
const indexCol = !isMobile ? 34 : 28;
const numberCol = !isMobile ? 88 : 68;
const amountCol = !isMobile ? 62 : 64;
const deleteCol = 32;
return indexCol + numberCol + playColumns.length * amountCol + deleteCol;
@@ -581,7 +623,10 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
setLiveWarningNumbers(new Set());
}
const alertRows = display?.risk_pool_alerts ?? [];
const alertRows = useMemo(
() => display?.risk_pool_alerts ?? [],
[display?.risk_pool_alerts],
);
const jackpot = display?.jackpot;
const currentQuickFill = quickFillState[activeCategory] ?? { favorites: [], history: [] };
const favorites = currentQuickFill.favorites;
@@ -638,6 +683,21 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
setActiveRowId(next[0].id);
};
const clearActiveRowAmounts = useCallback(() => {
const targetId = activeRowId ?? rows[0]?.id;
if (!targetId || tableDisabled) return;
setRows((current) =>
current.map((row) => {
if (row.id !== targetId) return row;
const nextAmounts = { ...row.amounts };
playColumns.forEach((column) => {
nextAmounts[column.key] = "";
});
return { ...row, amounts: nextAmounts };
}),
);
}, [activeRowId, playColumns, rows, tableDisabled]);
const fillCurrentRow = (number: string) => {
if (tableDisabled) return;
const targetId = activeRowId ?? rows[0]?.id;
@@ -645,6 +705,78 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
updateRowNumber(targetId, number);
};
const applyQuickAmountToActiveRow = useCallback(
(amount: string) => {
const targetId = activeRowId ?? rows[0]?.id;
if (!targetId || tableDisabled) return;
setRows((current) =>
current.map((row) => {
if (row.id !== targetId) return row;
if (
row.number.trim().length === 0 ||
playColumns.every(
(column) =>
draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) !== null,
)
) {
return row;
}
const nextAmounts = { ...row.amounts };
playColumns.forEach((column) => {
const isInputValid =
draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) === null;
const status = cellRiskState(
column.play,
row.number,
playCategory(column.play.play_code),
alertRows,
liveSoldOutNumbers,
liveWarningNumbers,
column.digitSlot,
);
if (!isInputValid || status === "sold_out") return;
nextAmounts[column.key] = amount;
});
return { ...row, amounts: nextAmounts };
}),
);
},
[
activeRowId,
alertRows,
liveSoldOutNumbers,
liveWarningNumbers,
playColumns,
rows,
tableDisabled,
],
);
const copyPreviousRowToActiveRow = useCallback(() => {
const targetId = activeRowId ?? rows[0]?.id;
if (!targetId || tableDisabled) return;
const targetIndex = rows.findIndex((row) => row.id === targetId);
if (targetIndex <= 0) return;
const previousRow = rows[targetIndex - 1];
if (!previousRow) return;
setRows((current) =>
current.map((row, index) => {
if (index !== targetIndex) return row;
const nextAmounts = { ...row.amounts };
playColumns.forEach((column) => {
nextAmounts[column.key] = previousRow.amounts[column.key] ?? "";
});
return {
...row,
number: previousRow.number,
amounts: nextAmounts,
};
}),
);
}, [activeRowId, playColumns, rows, tableDisabled]);
const toggleFavoriteNumber = (number: string) => {
const keys = quickFillKeys(activeCategory);
setQuickFillState((current) => {
@@ -680,6 +812,15 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
});
};
const toggleProvider = (code: string) => {
setSelectedProviderCodes((current) => {
if (current.includes(code)) {
return current.filter((item) => item !== code);
}
return [...current, code];
});
};
const hasAmountsForPlay = useCallback(
(playCode: string): boolean =>
rows.some((row) =>
@@ -835,8 +976,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
}, [allPlayColumns, currencyCode, rows]);
const draftEntries = collectEntries();
const effectiveProviderCount = selectedProviderCodes.length > 0 ? selectedProviderCodes.length : 1;
const draftSummary = useMemo(() => {
return draftEntries.reduce(
const base = draftEntries.reduce(
(acc, entry) => {
const rebateRate = parseRebateRate(entry.play.odds?.rebate_rate);
const rebate = Math.round(entry.amountMinor * rebateRate);
@@ -847,7 +989,12 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
},
{ bet: 0, rebate: 0, actual: 0 },
);
}, [draftEntries]);
return {
bet: base.bet * effectiveProviderCount,
rebate: base.rebate * effectiveProviderCount,
actual: base.actual * effectiveProviderCount,
};
}, [draftEntries, effectiveProviderCount]);
useEffect(() => {
const id = window.setTimeout(() => {
@@ -929,6 +1076,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
draw_id: display.draw_no,
currency_code: currencyCode,
client_trace_id: placeTraceIdRef.current,
provider_codes: selectedProviderCodes.length > 0 ? selectedProviderCodes : undefined,
lines,
});
setPreviewData(data);
@@ -996,6 +1144,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
draw_id: drawIdForBet,
currency_code: currencyCode,
client_trace_id: traceId,
provider_codes: selectedProviderCodes.length > 0 ? selectedProviderCodes : undefined,
lines,
expected_config_versions: previewData.config_versions,
});
@@ -1065,7 +1214,10 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (catalogState.kind === "loading") {
return (
<section className="space-y-3" aria-label={t("hall.aria")}>
<section
className={cn("space-y-3", isMobile && "pb-44")}
aria-label={t("hall.aria")}
>
<Skeleton className="h-12 rounded-xl" />
<Skeleton className="h-72 rounded-xl" />
<Skeleton className="h-14 rounded-xl" />
@@ -1096,6 +1248,23 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
!tableDisabled && draftEntries.length > 0 && availableMinor >= submitActualMinor;
const favoriteChips = favorites.slice(0, 10);
const historyChips = historyNumbers.slice(0, 20);
const selectedProviderNames = betProviders
.filter((provider) => selectedProviderCodes.includes(provider.code))
.map((provider) => provider.name);
const implicitDefaultProvider =
betProviders.find((provider) => provider.is_default) ?? betProviders[0] ?? null;
const providerSummaryCount = selectedProviderCodes.length > 0 ? selectedProviderCodes.length : 1;
const providerSummaryText =
selectedProviderCodes.length > 0
? t("hall.providers.summary", {
defaultValue: "已选 {{count}} 家,金额按每家公司计算",
count: providerSummaryCount,
})
: t("hall.providers.platformDefaultSummary", {
defaultValue: implicitDefaultProvider
? `未选择时走平台默认(${implicitDefaultProvider.name}`
: "未选择时走平台默认",
});
return (
<>
@@ -1123,17 +1292,55 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</div>
) : null}
<div className="rounded-xl border border-[#e3ebf7] bg-white px-3 py-3 shadow-[0_8px_24px_rgba(15,23,42,0.045)]">
<div
className={cn(
"bg-white",
isMobile
? "rounded-lg border border-[#e8eef7] px-3 py-2.5"
: "rounded-xl border border-[#e3ebf7] px-3 py-3 shadow-[0_8px_24px_rgba(15,23,42,0.045)]",
)}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-bold leading-5 text-slate-950">
{t("hall.quickFill.title")}
</p>
<p className="mt-0.5 text-xs leading-5 text-slate-500">
{t("hall.quickFill.description")}
</p>
{!isMobile ? (
<p className="mt-0.5 text-xs leading-5 text-slate-500">
{t("hall.quickFill.description")}
</p>
) : (
<p className="mt-0.5 text-[11px] leading-5 text-slate-500">
{t("hall.mobile.quickFillSummary", {
defaultValue: "收藏 {{favorites}} 个,历史 {{history}} 个",
favorites: favorites.length,
history: historyNumbers.length,
})}
</p>
)}
</div>
<div className="flex shrink-0 items-center gap-1.5">
{isMobile ? (
<Button
type="button"
variant="ghost"
size="icon"
aria-label={
quickFillExpanded
? t("hall.mobile.collapseQuickFill", { defaultValue: "收起快捷填单" })
: t("hall.mobile.expandQuickFill", { defaultValue: "展开快捷填单" })
}
aria-pressed={quickFillExpanded}
className="size-8 rounded-full border border-[#dfe6f0] bg-[#f8fafc] text-slate-500 hover:bg-slate-100 hover:text-slate-900"
onClick={() => setQuickFillExpanded((current) => !current)}
>
{quickFillExpanded ? (
<ChevronUp className="size-4" aria-hidden />
) : (
<ChevronDown className="size-4" aria-hidden />
)}
</Button>
) : null}
{activeRow?.number ? (
<Button
type="button"
@@ -1180,7 +1387,46 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</div>
</div>
<div className="mt-3 space-y-2">
<div className={cn("space-y-2", isMobile ? "mt-2" : "mt-3")}>
{isMobile && quickFillExpanded ? (
<div className="rounded-xl border border-[#e7eef8] bg-[#f8fbff] px-3 py-2.5">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[11px] font-semibold text-[#5b6f95]">
{t("hall.quickFill.batchTitle", { defaultValue: "Quick amount" })}
</span>
{MOBILE_QUICK_AMOUNT_PRESETS.map((preset) => (
<button
key={preset}
type="button"
disabled={tableDisabled}
onClick={() => applyQuickAmountToActiveRow(preset)}
className="inline-flex h-8 min-w-12 items-center justify-center rounded-full border border-[#cfe0fb] bg-white px-3 text-xs font-bold text-[#0b4ab3] transition-colors hover:border-[#aac7f5] hover:bg-[#edf5ff] disabled:opacity-40"
>
{preset}
</button>
))}
<button
type="button"
disabled={tableDisabled || !activeRow?.number}
onClick={copyPreviousRowToActiveRow}
className="inline-flex h-8 items-center justify-center rounded-full border border-[#dfe6f0] bg-white px-3 text-xs font-semibold text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40"
>
{t("hall.quickFill.copyPrev", { defaultValue: "Copy prev row" })}
</button>
<button
type="button"
disabled={tableDisabled}
onClick={clearActiveRowAmounts}
className="inline-flex h-8 items-center justify-center rounded-full border border-[#ffe0e5] bg-white px-3 text-xs font-semibold text-[#d81435] transition-colors hover:bg-[#fff4f6] disabled:opacity-40"
>
{t("hall.quickFill.clearRow", { defaultValue: "Clear row amounts" })}
</button>
</div>
</div>
) : null}
{!isMobile || quickFillExpanded ? (
<>
{favoriteChips.length > 0 ? (
<div className="flex flex-wrap items-center gap-1.5">
<span className="mr-0.5 text-[11px] font-semibold text-[#d81435]">
@@ -1251,10 +1497,115 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</span>
)}
</div>
</>
) : null}
</div>
</div>
<div className="flex items-end gap-2 overflow-x-auto pb-1">
<div
className={cn(
"rounded-lg border border-[#dfe8f6] bg-[#f8fbff]",
isMobile ? "px-3 py-2.5" : "px-2.5 py-2",
)}
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-1.5">
<span className="mr-1 text-[11px] font-black text-[#304f86]">
{t("hall.providers.title", { defaultValue: "开注商" })}
</span>
{!isMobile ? betProviders.map((provider) => {
const checked = selectedProviderCodes.includes(provider.code);
return (
<button
key={provider.code}
type="button"
disabled={tableDisabled}
aria-pressed={checked}
onClick={() => toggleProvider(provider.code)}
className={cn(
"inline-flex h-8 items-center gap-1 rounded-md border px-2 text-xs font-bold transition-colors disabled:cursor-not-allowed disabled:opacity-50",
checked
? "border-[#2d63e2] bg-[#2d63e2] text-white"
: "border-[#cfdbee] bg-white text-[#304f86] hover:border-[#8fb0eb]",
)}
>
<span
className={cn(
"size-3 rounded-[3px] border",
checked ? "border-white bg-white/25" : "border-[#9aa8bd]",
)}
aria-hidden
/>
{provider.name}
</button>
);
}) : (
<span className="text-xs font-semibold text-[#58709d]">
{selectedProviderNames.length > 0
? selectedProviderNames.join(" / ")
: t("hall.providers.platformDefault", {
defaultValue: implicitDefaultProvider
? `平台默认(${implicitDefaultProvider.name}`
: "平台默认",
})}
</span>
)}
</div>
<div className="flex items-center gap-2">
<p className="text-xs font-semibold text-[#58709d]">{providerSummaryText}</p>
{isMobile ? (
<button
type="button"
disabled={tableDisabled}
aria-pressed={providersExpanded}
onClick={() => setProvidersExpanded((current) => !current)}
className="inline-flex h-7 items-center justify-center rounded-full border border-[#d4dff1] bg-white px-2 text-[#58709d] transition-colors hover:bg-[#f3f7ff] disabled:opacity-40"
>
{providersExpanded ? (
<ChevronUp className="size-3.5" aria-hidden />
) : (
<ChevronDown className="size-3.5" aria-hidden />
)}
</button>
) : null}
</div>
</div>
{isMobile && providersExpanded ? (
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
{betProviders.map((provider) => {
const checked = selectedProviderCodes.includes(provider.code);
return (
<button
key={provider.code}
type="button"
disabled={tableDisabled}
aria-pressed={checked}
onClick={() => toggleProvider(provider.code)}
className={cn(
"inline-flex h-8 items-center gap-1 rounded-md border px-2 text-xs font-bold transition-colors disabled:cursor-not-allowed disabled:opacity-50",
checked
? "border-[#2d63e2] bg-[#2d63e2] text-white"
: "border-[#cfdbee] bg-white text-[#304f86] hover:border-[#8fb0eb]",
)}
>
<span
className={cn(
"size-3 rounded-[3px] border",
checked ? "border-white bg-white/25" : "border-[#9aa8bd]",
)}
aria-hidden
/>
{provider.name}
</button>
);
})}
</div>
) : null}
</div>
<div className={cn("overflow-x-auto pb-1", isMobile ? "-mx-1 flex gap-1.5 px-1" : "flex items-end gap-2")}>
{categoryTabs.map((tab) => {
const hasPlays = openPlays.some(
(play) => playCategory(play.play_code) === tab.value,
@@ -1268,7 +1619,10 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
disabled={!hasPlays}
onClick={() => setActiveCategory(tab.value)}
className={cn(
"inline-flex min-w-[6rem] items-center justify-center rounded-t-xl border px-4 py-3 text-sm font-bold transition-colors",
"inline-flex items-center justify-center border font-bold transition-colors",
isMobile
? "min-w-[5.25rem] rounded-t-xl border-b-0 px-4 py-3 text-sm"
: "min-w-[6rem] rounded-t-xl px-4 py-3 text-sm",
active
? "border-[#d7e1f3] border-b-white bg-white text-[#21335b] shadow-[0_-1px_0_rgba(255,255,255,0.8)]"
: "border-transparent bg-transparent text-[#6d8fd6] hover:text-[#2d63e2]",
@@ -1294,13 +1648,10 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</div>
) : null}
{showWideTableHint ? (
<p className="mb-2 text-xs leading-5 text-[#58709d]">{t("hall.table.scrollHint")}</p>
) : null}
<div
className={cn(
"overflow-hidden rounded-xl border border-[#e6edf8] bg-white shadow-[0_8px_24px_rgba(15,23,42,0.05)] transition-opacity",
"overflow-hidden border border-[#e6edf8] bg-white transition-opacity",
isMobile ? "rounded-lg" : "rounded-xl shadow-[0_8px_24px_rgba(15,23,42,0.05)]",
tableDisabled && "opacity-55",
showWideTableHint && "player-table-scroll-wrap",
)}
@@ -1361,13 +1712,14 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
>
<thead>
<tr className="border-b border-[#edf2f8] bg-[#f8fafd] text-[#58709d]">
<th className="sticky left-0 z-30 w-[2.15rem] min-w-[2.15rem] bg-[#f8fafd] px-0.5 py-1.5 text-center font-bold shadow-[2px_0_6px_rgba(15,23,42,0.04)]">
<th className={cn("sticky left-0 z-30 bg-[#f8fafd] px-0.5 py-1.5 text-center font-bold shadow-[2px_0_6px_rgba(15,23,42,0.04)]", indexColClass)}>
{t("hall.table.no", { defaultValue: "No." })}
</th>
<th
className={cn(
"sticky left-[2.15rem] z-30 bg-[#f8fafd] px-1 py-1.5 text-center font-bold shadow-[2px_0_6px_rgba(15,23,42,0.04)]",
"w-[5.5rem] min-w-[5.5rem]",
"sticky z-30 bg-[#f8fafd] px-1 py-1.5 text-center font-bold shadow-[2px_0_6px_rgba(15,23,42,0.04)]",
stickyNumberLeftClass,
numberColClass,
)}
>
<span className="block text-xs">{t("hall.table.number", { defaultValue: "Number" })}</span>
@@ -1416,31 +1768,27 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
t={t}
isMobile={isMobile}
removable={rows.length > 1}
indexColClass={indexColClass}
numberColClass={numberColClass}
stickyNumberLeftClass={stickyNumberLeftClass}
/>
))}
</tbody>
</table>
</div>
<button
type="button"
disabled={tableDisabled || rows.length >= MAX_ROWS}
onClick={addRow}
className="flex h-10 w-full items-center justify-center gap-1.5 border-t border-[#edf2f9] bg-white text-xs font-bold text-[#1d57b7] hover:bg-[#f7faff] disabled:text-slate-300"
>
<CirclePlus className="size-4" aria-hidden />
{t("hall.table.addRow")}
</button>
</div>
<div className="flex items-center justify-between rounded-xl border border-[#e9eef7] bg-[#f8fbff] px-3 py-2.5 text-sm shadow-[0_6px_20px_rgba(15,23,42,0.04)]">
<span className="text-xs font-medium text-slate-500">
{t("hall.table.draftTotal")}
</span>
<span className="text-base font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(debouncedSummary.actual, currencyCode)}
</span>
</div>
{!isMobile ? (
<div className="flex items-center justify-between rounded-xl border border-[#e9eef7] bg-[#f8fbff] px-3 py-2.5 text-sm shadow-[0_6px_20px_rgba(15,23,42,0.04)]">
<span className="text-xs font-medium text-slate-500">
{t("hall.table.draftTotal")}
</span>
<span className="text-base font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(debouncedSummary.actual, currencyCode)}
</span>
</div>
) : null}
{sealedBetUi ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-600">
@@ -1448,32 +1796,80 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</p>
) : null}
<Button
type="button"
disabled={!canSubmit || previewLoading}
onClick={() => void handlePreview()}
className={cn(
"h-12 w-full gap-2 rounded-xl border-0 text-base font-bold text-white",
!isBettable
? "bg-slate-500 shadow-none hover:bg-slate-500 disabled:opacity-100"
: "bg-[#e5002c] shadow-[0_8px_20px_rgba(229,0,44,0.26)] hover:bg-[#d10028]",
)}
>
{!isBettable ? (
<Lock className="size-5 shrink-0" aria-hidden />
) : (
<Ticket className="size-5 shrink-0" aria-hidden />
)}
{previewLoading
? t("hall.table.previewing")
: !isBettable
? t("hall.closed.title")
: availableMinor < submitActualMinor
? t("hall.table.insufficientBalance")
: t("hall.table.submitBet")}
</Button>
{!isMobile ? (
<Button
type="button"
disabled={!canSubmit || previewLoading}
onClick={() => void handlePreview()}
className={cn(
"h-12 w-full gap-2 rounded-xl border-0 text-base font-bold text-white",
!isBettable
? "bg-slate-500 shadow-none hover:bg-slate-500 disabled:opacity-100"
: "bg-[#e5002c] shadow-[0_8px_20px_rgba(229,0,44,0.26)] hover:bg-[#d10028]",
)}
>
{!isBettable ? (
<Lock className="size-5 shrink-0" aria-hidden />
) : (
<Ticket className="size-5 shrink-0" aria-hidden />
)}
{previewLoading
? t("hall.table.previewing")
: !isBettable
? t("hall.closed.title")
: availableMinor < submitActualMinor
? t("hall.table.insufficientBalance")
: t("hall.table.submitBet")}
</Button>
) : null}
</section>
{isMobile ? (
<div
className={cn(
playerViewportFixedBarClass,
"bottom-[calc(3.25rem+env(safe-area-inset-bottom,0px))] z-40",
)}
>
<div className="mx-auto w-full border-t border-[#dfe7f3] bg-white">
<div className="flex items-center gap-2 px-3 pt-2 pb-2.5">
<div className="min-w-0 flex flex-1 items-center justify-between gap-2">
<p className="shrink-0 text-[11px] font-semibold text-slate-500">
{t("hall.table.draftTotal")}
</p>
<p className="truncate text-[19px] font-black tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(debouncedSummary.actual, currencyCode)}
</p>
</div>
<Button
type="button"
disabled={!canSubmit || previewLoading}
onClick={() => void handlePreview()}
className={cn(
"h-9 min-w-[7.2rem] shrink-0 gap-1.5 rounded-lg border-0 px-3.5 text-[14px] font-bold text-white",
!isBettable
? "bg-slate-500 shadow-none hover:bg-slate-500 disabled:opacity-100"
: "bg-[#e5002c] shadow-none hover:bg-[#d10028]",
)}
>
{!isBettable ? (
<Lock className="size-4.5 shrink-0" aria-hidden />
) : (
<Ticket className="size-4.5 shrink-0" aria-hidden />
)}
{previewLoading
? t("hall.table.previewing")
: !isBettable
? t("hall.closed.title")
: availableMinor < submitActualMinor
? t("hall.table.insufficientBalance")
: t("hall.table.submitBet")}
</Button>
</div>
</div>
</div>
) : null}
<HallBetPreviewDialog
open={previewOpen}
onOpenChange={(open) => {
@@ -1524,7 +1920,10 @@ const DraftRowItem = memo(function DraftRowItem({
removeRow,
t,
isMobile,
removable
removable,
indexColClass,
numberColClass,
stickyNumberLeftClass,
}: {
row: DraftRow;
index: number;
@@ -1542,6 +1941,9 @@ const DraftRowItem = memo(function DraftRowItem({
t: HallTranslate;
isMobile: boolean;
removable: boolean;
indexColClass: string;
numberColClass: string;
stickyNumberLeftClass: string;
}) {
return (
<tr
@@ -1552,7 +1954,8 @@ const DraftRowItem = memo(function DraftRowItem({
>
<td
className={cn(
"sticky left-0 z-20 w-[2.15rem] min-w-[2.15rem] px-0.5 py-1.5 text-center font-black text-[#17408d] shadow-[2px_0_6px_rgba(15,23,42,0.04)]",
"sticky left-0 z-20 px-0.5 py-1.5 text-center font-black text-[#17408d] shadow-[2px_0_6px_rgba(15,23,42,0.04)]",
indexColClass,
rowActive ? "bg-[#f5f9ff]" : "bg-white",
)}
>
@@ -1560,15 +1963,18 @@ const DraftRowItem = memo(function DraftRowItem({
</td>
<td
className={cn(
"sticky left-[2.15rem] z-20 px-1 py-1.5 shadow-[2px_0_6px_rgba(15,23,42,0.04)]",
"w-[5.5rem] min-w-[5.5rem]",
"sticky z-20 px-1 py-1.5 shadow-[2px_0_6px_rgba(15,23,42,0.04)]",
stickyNumberLeftClass,
numberColClass,
rowActive ? "bg-[#f5f9ff]" : "bg-white",
)}
>
<Input
value={row.number}
disabled={tableDisabled}
inputMode="text"
inputMode="numeric"
pattern="[0-9Rr]*"
autoComplete="off"
placeholder={numberPlaceholder}
onFocus={() => setActiveRowId(row.id)}
onClick={() => setActiveRowId(row.id)}

View File

@@ -15,6 +15,7 @@ import {
} from "@/features/wallet/wallet-transfer-dialogs";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { useApiQuery } from "@/hooks/use-api-query";
import { useIsMobile } from "@/hooks/use-mobile";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
@@ -30,6 +31,7 @@ export function HallWalletStrip() {
const { activeCurrency } = useActivePlayerCurrency();
const { mutate } = useSWRConfig();
const degradedWalletPollRef = useRef<number | null>(null);
const isMobile = useIsMobile();
const currency = activeCurrency;
const isDegraded = mode === "polling" || mode === "offline";
@@ -75,7 +77,7 @@ export function HallWalletStrip() {
return (
<section
className="mb-3 space-y-2.5"
className={cn("mb-3", isMobile ? "space-y-2" : "space-y-2.5")}
aria-label={
isCreditPlayer
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
@@ -84,7 +86,8 @@ export function HallWalletStrip() {
>
<div
className={cn(
"relative overflow-hidden rounded-xl bg-[#e5002c] px-3 py-3 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
"relative overflow-hidden rounded-xl bg-[#e5002c] text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
isMobile ? "px-3 py-2.5" : "px-3 py-3",
)}
>
<Image
@@ -95,27 +98,34 @@ export function HallWalletStrip() {
className="pointer-events-none object-cover object-center"
aria-hidden
/>
<div className="relative flex items-center gap-3">
<div className="flex size-14 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
<Wallet className="size-7" aria-hidden />
<div className={cn("relative flex items-center", isMobile ? "gap-2.5" : "gap-3")}>
<div
className={cn(
"shrink-0 rounded-full bg-white text-[#d81435] shadow-sm",
isMobile
? "flex size-11 items-center justify-center"
: "flex size-14 items-center justify-center",
)}
>
<Wallet className={cn(isMobile ? "size-5.5" : "size-7")} aria-hidden />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-white/90">
<p className={cn("font-semibold text-white/90", isMobile ? "text-[13px]" : "text-sm")}>
{isCreditPlayer
? t("wallet.creditAvailable", { defaultValue: "可用信用" })
: t("wallet.balance")}
</p>
{loading ? (
<Skeleton className="mt-2 h-8 w-44 rounded-md bg-white/25" />
<Skeleton className={cn("rounded-md bg-white/25", isMobile ? "mt-1.5 h-7 w-36" : "mt-2 h-8 w-44")} />
) : (
<PlayerMoneyDisplay
amountMinor={headlineMinor}
currency={currency}
className="mt-1 text-white"
className={cn("text-white", isMobile ? "mt-0.5" : "mt-1")}
/>
)}
{isCreditPlayer && !loading && balance ? (
<p className="mt-2 text-xs text-white/75">
<p className={cn("text-white/75", isMobile ? "mt-1 text-[11px]" : "mt-2 text-xs")}>
{t("wallet.creditSummary", {
defaultValue: "授信 {{limit}} · 已用 {{used}}",
limit: formatMinorAsCurrency(balance.credit_limit ?? 0, currency),
@@ -128,12 +138,15 @@ export function HallWalletStrip() {
</div>
{isCreditPlayer ? null : (
<div className="grid grid-cols-2 gap-2.5">
<div className={cn("grid grid-cols-2", isMobile ? "gap-2" : "gap-2.5")}>
<TransferInDialog
idPrefix="hall-"
triggerVariant="hall"
triggerLabel={t("wallet.transferIn")}
triggerClassName="h-12 rounded-lg text-base font-bold"
triggerClassName={cn(
"rounded-lg font-bold",
isMobile ? "h-10 text-sm" : "h-12 text-base",
)}
currency={currency}
lotteryMinor={transferInLotteryMinor}
mainMinor={mainMinor}
@@ -143,7 +156,10 @@ export function HallWalletStrip() {
idPrefix="hall-"
triggerVariant="hall"
triggerLabel={t("wallet.transferOut")}
triggerClassName="h-12 rounded-lg text-base font-bold"
triggerClassName={cn(
"rounded-lg font-bold",
isMobile ? "h-10 text-sm" : "h-12 text-base",
)}
currency={currency}
availableMinor={availableMinor}
onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }}

View File

@@ -83,6 +83,8 @@ export function sortTicketGroupItems(items: TicketItemListRow[]): TicketItemList
return [...items].sort((a, b) => {
const byPlay = a.play_code.localeCompare(b.play_code);
if (byPlay !== 0) return byPlay;
const byProvider = (a.provider_code ?? "").localeCompare(b.provider_code ?? "");
if (byProvider !== 0) return byProvider;
return (a.original_number ?? "").localeCompare(b.original_number ?? "");
});
}
@@ -135,4 +137,4 @@ export function groupTicketItems(items: TicketItemListRow[]): TicketItemGroup[]
export function ticketDetailHref(ticketNo: string): string {
return `/orders/${encodeURIComponent(ticketNo)}`;
}
}

View File

@@ -76,6 +76,10 @@ type TicketItemDetailWithExtras = TicketItemDetailPayload & {
};
};
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
return providerName || providerCode || "-";
}
/** 界面文档 §4.8 注单详情 */
export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
const { t } = useTranslation("player");
@@ -282,6 +286,10 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
{playLabel(row.play_code, t)} · {row.original_number ?? row.play_code}
</p>
<p className="mt-0.5 text-xs text-slate-500">
<span className="font-bold text-[#0b56b7]">
{providerLabel(row.provider_name, row.provider_code)}
</span>
{" · "}
{t("orders.deduction")}{" "}
<span className="font-bold tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(row.actual_deduct_amount, lineCur)}
@@ -356,6 +364,14 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
{playLabel(data.play_code, t)} ({data.dimension ?? "—"}D)
</span>
</div>
<div className="flex items-baseline justify-between gap-3">
<span className="shrink-0 text-slate-500">
{t("hall.providers.title", { defaultValue: "开注商" })}
</span>
<span className="text-right font-black text-[#0b56b7]">
{providerLabel(data.provider_name, data.provider_code)}
</span>
</div>
<div className="flex items-baseline justify-between gap-3">
<span className="shrink-0 text-slate-500">{t("orders.amount")}</span>
<span className="text-right font-black tabular-nums text-[#d81435]">

View File

@@ -47,6 +47,10 @@ const STATUS_OPTIONS = [
"refunded",
] as const;
function providerLabel(providerName?: string | null, providerCode?: string | null): string {
return providerName || providerCode || "-";
}
export function TicketOrdersListScreen() {
const router = useRouter();
const searchParams = useSearchParams();
@@ -528,6 +532,10 @@ export function TicketOrdersListScreen() {
{playLabel(row.play_code, t)} · {row.original_number ?? row.play_code}
</p>
<p className="mt-0.5 text-xs text-slate-500">
<span className="font-bold text-[#0b56b7]">
{providerLabel(row.provider_name, row.provider_code)}
</span>
{" · "}
{t("orders.deduction")}{" "}
<span className="font-bold tabular-nums text-[#0b3f96]">
{formatMinorAsCurrency(row.actual_deduct_amount, lineCur)}
@@ -573,4 +581,3 @@ export function TicketOrdersListScreen() {
);
}

View File

@@ -255,7 +255,10 @@
"quickFill": {
"title": "Quick fill",
"description": "Fill the current row with favorite or recent numbers in one tap.",
"batchTitle": "Quick amount",
"clearAll": "Clear all",
"clearRow": "Clear row amounts",
"copyPrev": "Copy prev row",
"favorite": "Favorite number",
"unfavorite": "Remove favorite",
"favorites": "Favorites",
@@ -264,6 +267,13 @@
"history": "Recent 20 numbers",
"emptyHistory": "No recent numbers"
},
"mobile": {
"availableBalance": "Available {{amount}}",
"quickFillSummary": "{{favorites}} favorites, {{history}} recent",
"collapseQuickFill": "Collapse quick fill",
"expandQuickFill": "Expand quick fill",
"noProvidersSelected": "None selected"
},
"preview": {
"title": "Confirm bet",
"description": "Check the number, play type, and actual deduction. After confirmation, the lottery wallet will be debited and the ticket cannot be cancelled.",

View File

@@ -255,7 +255,10 @@
"quickFill": {
"title": "छिटो भर्ने",
"description": "मनपर्ने वा पछिल्ला नम्बरहरू एक ट्यापमा हालको पंक्तिमा भर्नुहोस्।",
"batchTitle": "छिटो रकम",
"clearAll": "सबै हटाउनुहोस्",
"clearRow": "यो पङ्क्तिको रकम हटाउनुहोस्",
"copyPrev": "अघिल्लो पङ्क्ति कपी गर्नुहोस्",
"favorite": "नम्बर मनपर्नेमा राख्नुहोस्",
"unfavorite": "मनपर्नेबाट हटाउनुहोस्",
"favorites": "मनपर्ने",
@@ -264,6 +267,13 @@
"history": "पछिल्ला 20 नम्बर",
"emptyHistory": "इतिहास नम्बर छैन"
},
"mobile": {
"availableBalance": "उपलब्ध मौज्दात {{amount}}",
"quickFillSummary": "{{favorites}} मनपर्ने, {{history}} इतिहास",
"collapseQuickFill": "छिटो भर्ने खुम्च्याउनुहोस्",
"expandQuickFill": "छिटो भर्ने विस्तार गर्नुहोस्",
"noProvidersSelected": "छानिएको छैन"
},
"preview": {
"title": "बेट पुष्टि गर्नुहोस्",
"description": "नम्बर, प्ले प्रकार र वास्तविक कट्टा जाँच गर्नुहोस्। पुष्टि गरेपछि वालेटबाट रकम कट्टा हुनेछ र टिकट रद्द गर्न सकिँदैन।",

View File

@@ -254,7 +254,10 @@
"quickFill": {
"title": "快速填单",
"description": "收藏号码、最近号码可一键填入当前行。",
"batchTitle": "快捷金额",
"clearAll": "批量清空",
"clearRow": "清空本行金额",
"copyPrev": "复制上一行",
"favorite": "收藏号码",
"unfavorite": "取消收藏",
"favorites": "收藏",
@@ -263,6 +266,13 @@
"history": "最近 20 个历史号码",
"emptyHistory": "暂无历史号码"
},
"mobile": {
"availableBalance": "可用余额 {{amount}}",
"quickFillSummary": "收藏 {{favorites}} 个,历史 {{history}} 个",
"collapseQuickFill": "收起快捷填单",
"expandQuickFill": "展开快捷填单",
"noProvidersSelected": "未选择"
},
"preview": {
"title": "确认下注",
"description": "请核对号码、玩法与实扣金额;确认后将扣减彩票钱包且不可撤单。",

View File

@@ -1,8 +1,8 @@
/** 主滚动区外边距(灰底区域与顶栏/底栏留白) */
export const playerMainInset = "px-3 pt-3 pb-3 lg:px-8 lg:pt-6 lg:pb-8";
export const playerMainInset = "px-2 pt-2 pb-2 lg:px-6 lg:pt-4 lg:pb-6";
/** 页内白卡片内边距 */
export const playerPageShellPadding = "px-3 pt-3 pb-6 lg:px-8 lg:pt-8 lg:pb-8";
export const playerPageShellPadding = "px-2 pt-2 pb-4 lg:px-6 lg:pt-6 lg:pb-6";
/** 大厅等直接写在 section 上的内边距 */
export const playerPageInset = playerPageShellPadding;

View File

@@ -0,0 +1,10 @@
export type BetProviderRow = {
code: string;
name: string;
sort_order: number;
is_default: boolean;
};
export type BetProvidersData = {
items: BetProviderRow[];
};

View File

@@ -6,6 +6,8 @@ export type TicketItemListRow = {
order_status?: string | null;
draw_no: string | null | undefined;
currency_code: string | null | undefined;
provider_code?: string | null;
provider_name?: string | null;
play_code: string;
original_number: string | null;
total_bet_amount: number;
@@ -38,6 +40,8 @@ export type TicketItemDetailPayload = {
order_status?: string | null;
draw_no: string | null | undefined;
currency_code: string | null | undefined;
provider_code?: string | null;
provider_name?: string | null;
play_code: string;
dimension: number | null;
digit_slot: number | null;

View File

@@ -17,6 +17,7 @@ export type TicketPreviewPayload = {
draw_id: string;
currency_code: string;
client_trace_id?: string | null;
provider_codes?: string[];
lines: TicketLineInput[];
};
@@ -26,6 +27,8 @@ export type TicketPlacePayload = TicketPreviewPayload & {
export type TicketPreviewLine = {
client_line_no: number;
provider_code?: string;
provider_name?: string;
number: string;
play_code: string;
normalized_number: string;
@@ -61,6 +64,8 @@ export type TicketPreviewData = {
export type TicketPlaceItem = {
ticket_no: string;
provider_code?: string;
provider_name?: string;
play_code: string;
number: string;
total_bet_amount: number;