From 295cfb97cd4eea5d7d3742dddbac18ce26c06d24 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 15 Jul 2026 10:38:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(player):=20PC=20=E7=AB=AF=E5=88=97?= =?UTF-8?q?=E8=A1=A8/=E5=B8=83=E5=B1=80=E9=80=82=E9=85=8D=E4=B8=8E?= =?UTF-8?q?=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 玩家端桌面:全宽居中 Logo、圆角侧栏与主内容面板;开奖结果/注单改 shadcn Table+分页(移动仍无限滚动),注单组默认折叠;信用/钱包筛选与分页;详情页与奖号网格密度优化。 --- src/app/globals.css | 4 +- src/components/layout/player-app-shell.tsx | 55 +- .../layout/player-desktop-header.tsx | 23 +- .../layout/player-list-pagination.tsx | 157 +++ src/components/layout/player-sidebar.tsx | 27 +- src/components/ui/pagination.tsx | 127 +++ src/features/hall/hall-betting-grid.tsx | 826 ++++++++++----- src/features/hall/hall-draw-panel.tsx | 138 ++- src/features/hall/hall-screen.tsx | 46 +- src/features/hall/hall-wallet-strip.tsx | 95 +- src/features/hall/selection-type.ts | 141 +++ .../orders/ticket-order-detail-screen.tsx | 372 +++---- .../orders/ticket-orders-list-screen.tsx | 958 ++++++++++++------ .../results/draw-result-detail-screen.tsx | 171 ++-- .../results/draw-results-list-screen.tsx | 577 ++++++----- .../results/jackpot-results-strip.tsx | 54 +- .../results/twenty-three-results-grid.tsx | 76 +- src/features/wallet/wallet-logs-block.tsx | 200 ++-- src/features/wallet/wallet-screen.tsx | 293 ++++-- src/i18n/locales/en/player.json | 18 + src/i18n/locales/ne/player.json | 18 + src/i18n/locales/zh/player.json | 18 + src/lib/player-spacing.ts | 6 +- 23 files changed, 2989 insertions(+), 1411 deletions(-) create mode 100644 src/components/layout/player-list-pagination.tsx create mode 100644 src/components/ui/pagination.tsx create mode 100644 src/features/hall/selection-type.ts diff --git a/src/app/globals.css b/src/app/globals.css index 0eebfc2..d30c38b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -165,10 +165,10 @@ --sidebar-border: #e4ebf5; } -/* 玩家端桌面壳:侧栏宽度供遮罩/弹层定位 */ +/* 玩家端桌面壳:侧栏宽度供遮罩/弹层定位(含外侧留白) */ @media (min-width: 1024px) { .player-desktop-shell { - --player-sidebar-width: 14rem; + --player-sidebar-width: 15rem; } } diff --git a/src/components/layout/player-app-shell.tsx b/src/components/layout/player-app-shell.tsx index d6c0a7c..588aba6 100644 --- a/src/components/layout/player-app-shell.tsx +++ b/src/components/layout/player-app-shell.tsx @@ -18,8 +18,8 @@ type PlayerAppShellProps = { /** * 玩家端外壳: - * - 移动:主体 + 底部 Tab(H5) - * - 桌面:侧栏 + 顶栏 + 宽主内容区(UI 设计稿) + * - 移动:顶栏 + 主体 + 底部 Tab + * - 桌面:全宽顶栏(Logo 整页居中)+ 侧栏 + 主内容 */ export function PlayerAppShell({ children }: PlayerAppShellProps): ReactNode { const isMobile = useIsMobile(); @@ -33,34 +33,37 @@ export function PlayerAppShell({ children }: PlayerAppShellProps): ReactNode { }); return ( -
- +
+ -
- {isMobile ? ( - - ) : null} - - +
+ -
- {children} -
+
+ {isMobile ? ( + + ) : null} + -
- +
+ {children} +
+ +
+ +
); -} \ No newline at end of file +} diff --git a/src/components/layout/player-desktop-header.tsx b/src/components/layout/player-desktop-header.tsx index bbaff4a..0b0e198 100644 --- a/src/components/layout/player-desktop-header.tsx +++ b/src/components/layout/player-desktop-header.tsx @@ -10,15 +10,16 @@ type PlayerDesktopHeaderProps = { className?: string; }; -/** 全局顶栏:移动显示 Logo;桌面 Logo 在侧栏顶部,此处仅语言切换 */ +/** 全局顶栏:移动 Logo 左对齐;桌面 Logo 整页水平居中,语言切换靠右 */ export function PlayerDesktopHeader({ className }: PlayerDesktopHeaderProps) { return (
+ {/* 移动:左 Logo */} -
+ {/* 桌面:整页水平居中 Logo */} + + N lotto + + +
i + 1); + } + const set = new Set([1, last]); + for (let p = current - delta; p <= current + delta; p++) { + if (p >= 1 && p <= last) set.add(p); + } + const sorted = [...set].sort((a, b) => a - b); + const out: (number | "ellipsis")[] = []; + let prev = 0; + for (const p of sorted) { + if (prev && p - prev > 1) out.push("ellipsis"); + out.push(p); + prev = p; + } + return out; +} + +type PlayerListPaginationProps = { + page: number; + lastPage: number; + total?: number; + loading?: boolean; + onPageChange: (page: number) => void; + className?: string; + /** 隐藏「共 x 条」摘要 */ + hideSummary?: boolean; +}; + +/** PC 列表底部分页(移动端仍用无限滚动,勿挂载本组件) */ +export function PlayerListPagination({ + page, + lastPage, + total, + loading = false, + onPageChange, + className, + hideSummary = false, +}: PlayerListPaginationProps) { + const { t } = useTranslation("player"); + + if (lastPage <= 1 && (total == null || total <= 0)) { + return null; + } + + return ( +
+ {!hideSummary ? ( +

+ {total != null + ? t("pagination.summary", { + defaultValue: "共 {{total}} 条,第 {{page}} / {{lastPage}} 页", + total, + page, + lastPage: Math.max(1, lastPage), + }) + : t("pagination.pageOf", { + defaultValue: "第 {{page}} / {{lastPage}} 页", + page, + lastPage: Math.max(1, lastPage), + })} +

+ ) : ( + + )} + + {lastPage > 1 ? ( + + + + { + if (page <= 1 || loading) return; + onPageChange(Math.max(1, page - 1)); + }} + /> + + {playerListPageSlices(page, lastPage).map((item, idx) => + item === "ellipsis" ? ( + + + + ) : ( + + { + if (loading || item === page) return; + onPageChange(item); + }} + > + {item} + + + ), + )} + + = lastPage || loading} + className="h-8 rounded-lg border border-[#dce7f7] bg-white text-[#32518d] hover:bg-[#f8fbff]" + onClick={() => { + if (page >= lastPage || loading) return; + onPageChange(Math.min(lastPage, page + 1)); + }} + /> + + + + ) : null} +
+ ); +} diff --git a/src/components/layout/player-sidebar.tsx b/src/components/layout/player-sidebar.tsx index 7160c89..9938eb0 100644 --- a/src/components/layout/player-sidebar.tsx +++ b/src/components/layout/player-sidebar.tsx @@ -1,6 +1,5 @@ "use client"; -import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -57,14 +56,13 @@ const navItems = [ }, ] as const; -/** 与 lotteryadmin `NAV_BTN` / `NAV_ACTIVE` 一致 */ const playerNavLinkClass = - "flex h-8 w-full items-center gap-2 rounded-md px-2.5 text-[13px] leading-snug font-normal text-sidebar-foreground/90 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0"; + "flex h-10 w-full items-center gap-2 rounded-xl px-3 text-sm leading-none font-medium text-sidebar-foreground/90 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0"; const playerNavLinkActiveClass = - "bg-sidebar-primary/90 font-medium text-sidebar-primary-foreground shadow-sm hover:bg-sidebar-primary/90 hover:text-sidebar-primary-foreground"; + "bg-sidebar-primary/90 font-semibold text-sidebar-primary-foreground shadow-sm hover:bg-sidebar-primary/90 hover:text-sidebar-primary-foreground"; -/** 桌面端左侧导航(激活态对齐后台侧栏) */ +/** 桌面端左侧导航:与内容区同高的圆角面板,不贴边 */ export function PlayerSidebar() { const pathname = usePathname() ?? ""; const { t } = useTranslation("player"); @@ -72,23 +70,10 @@ export function PlayerSidebar() { return ( ); -} \ No newline at end of file +} diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx new file mode 100644 index 0000000..fb32bd8 --- /dev/null +++ b/src/components/ui/pagination.tsx @@ -0,0 +1,127 @@ +import * as React from "react"; +import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +function Pagination({ className, ...props }: React.ComponentProps<"nav">) { + return ( +
+ ) : null} +
+ + {t("hall.quickFill.history")} + + {historyChips.length > 0 ? ( + historyChips.map((number) => ( + + )) + ) : ( + + {t("hall.quickFill.emptyHistory")} + + )} +
+
+ ) : null}
{isMobile ? ( @@ -1425,167 +1654,176 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } ) : null} {activeRow?.number ? ( - - ) : null} - -
-
+ + ) : null} + +
+ -
- {isMobile && quickFillExpanded ? ( -
-
- - {t("hall.quickFill.batchTitle", { defaultValue: "Quick amount" })} - - {MOBILE_QUICK_AMOUNT_PRESETS.map((preset) => ( - - ))} - + {isMobile ? ( +
+ {quickFillExpanded ? ( +
+
+ + {t("hall.quickFill.batchTitle", { defaultValue: "Quick amount" })} + + {MOBILE_QUICK_AMOUNT_PRESETS.map((preset) => ( -
-
- ) : null} - - {!isMobile || quickFillExpanded ? ( - <> - {favoriteChips.length > 0 ? ( -
- - {t("hall.quickFill.favorites")} - - {favoriteChips.map((number) => ( - ))} + +
- ) : null} - -
- - {t("hall.quickFill.history")} - - {historyChips.length > 0 ? ( - historyChips.map((number) => ( - - )) - ) : ( - - {t("hall.quickFill.emptyHistory")} - - )}
- - ) : null} -
-
+ ) : null} -
+ {quickFillExpanded ? ( + <> + {favoriteChips.length > 0 ? ( +
+ + {t("hall.quickFill.favorites")} + + {favoriteChips.map((number) => ( + + ))} +
+ ) : null} + +
+ + {t("hall.quickFill.history")} + + {historyChips.length > 0 ? ( + historyChips.map((number) => ( + + )) + ) : ( + + {t("hall.quickFill.emptyHistory")} + + )} +
+ + ) : null} +
+ ) : null} +
+ +
{categoryTabs.map((tab) => { const hasPlays = openPlays.some( (play) => playCategory(play.play_code) === tab.value, @@ -1599,18 +1837,22 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } disabled={!hasPlays} onClick={() => setActiveCategory(tab.value)} className={cn( - "inline-flex items-center justify-center border font-bold transition-colors", + "inline-flex items-center justify-center 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]", + ? "min-w-[5.25rem] rounded-t-xl border border-b-0 px-4 py-3 text-sm" + : "min-w-[4.5rem] rounded-lg px-3.5 py-2 text-sm", + isMobile + ? 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]" + : active + ? "bg-[#2d63e2] text-white shadow-[0_4px_12px_rgba(45,99,226,0.28)]" + : "bg-transparent text-[#5b7fbf] hover:bg-[#f3f7ff] hover:text-[#2d63e2]", !hasPlays && "cursor-not-allowed opacity-40", )} aria-pressed={active} > - + {tab.label} @@ -1618,6 +1860,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } ); })}
+
{activeCategoryPlays.length === 0 ? (
-
+
@@ -1686,9 +1932,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
-
+
@@ -1745,8 +1991,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } defaultValue="" disabled={tableDisabled} onChange={(event) => { - const selectionType = event.target.value as DraftRow["selectionType"] | ""; - if (selectionType) updateAllSelectionTypes(selectionType); + const selectionType = event.target.value as SelectionType | ""; + if (selectionType) requestAllSelectionTypes(selectionType); event.currentTarget.value = ""; }} className="mx-auto mt-1 h-5 w-full rounded border border-[#d7e1f3] bg-white px-0.5 text-[10px] font-semibold text-[#304f86]" @@ -1754,11 +2000,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } title={t("hall.table.setAllTypes")} > - - - - - {activeCategory === "D4" ? : null} + {selectionTypeOptions.map((type) => ( + + ))} ) : null} @@ -1803,7 +2049,8 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } liveSoldOutNumbers={liveSoldOutNumbers} liveWarningNumbers={liveWarningNumbers} updateRowNumber={updateRowNumber} - updateRowSelectionType={updateRowSelectionType} + updateRowSelectionType={requestRowSelectionType} + selectionTypeOptions={selectionTypeOptions} updateAmount={updateAmount} toggleRowProvider={toggleRowProvider} setActiveRowId={setActiveRowId} @@ -1826,20 +2073,20 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot } {!isMobile ? ( - updateRowSelectionType(row.id, event.target.value as DraftRow["selectionType"])} + onChange={(event) => updateRowSelectionType(row.id, event.target.value as SelectionType)} className="h-8 w-full rounded-md border border-[#e1e8f3] bg-white px-1 text-[10px] font-semibold text-[#304f86]" aria-label={t("hall.table.selectionTypeForRow", { row: index + 1 })} > - - - - - {activeCategory === "D4" ? : null} + {selectionTypeOptions.map((type) => ( + + ))} + {comboCount > 1 && displayNumber.length >= 2 ? ( +

+ {t("hall.table.comboCount", { count: comboCount })} +

+ ) : null} ) : null} {betProviders.map((provider, providerIndex) => ( diff --git a/src/features/hall/hall-draw-panel.tsx b/src/features/hall/hall-draw-panel.tsx index 6f46a7a..916394c 100644 --- a/src/features/hall/hall-draw-panel.tsx +++ b/src/features/hall/hall-draw-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { Hourglass, Landmark, TimerReset } from "lucide-react"; +import { Hourglass, Landmark } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; @@ -8,7 +8,6 @@ import { Skeleton } from "@/components/ui/skeleton"; import { drawStatusHud, isHallAwaitingDrawProcessing, - isHallBlockedForBetting, isHallSealedCountdownUi, } from "@/features/draw/draw-status-meta"; import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live"; @@ -17,7 +16,13 @@ import { formatPlayerInstant } from "@/lib/player-datetime"; import { cn } from "@/lib/utils"; import type { DrawCurrentPayload } from "@/types/api/draw-current"; -function ScheduleAnchorTime({ payload }: { payload: DrawCurrentPayload }) { +function ScheduleAnchorTime({ + payload, + layout, +}: { + payload: DrawCurrentPayload; + layout: "mobile" | "desktop"; +}) { const { t } = useTranslation("player"); const isPending = payload.status === "pending"; const isOpen = payload.status === "open"; @@ -30,6 +35,30 @@ function ScheduleAnchorTime({ payload }: { payload: DrawCurrentPayload }) { ? "draw.scheduledEnd" : "draw.scheduledClose"; const formatted = source ? formatPlayerInstant(source) : null; + + if (layout === "desktop") { + if (!formatted) { + return ( +
+ {t(labelKey)} + --:--:-- +
+ ); + } + const parts = formatted.split(" "); + const date = parts.slice(0, -1).join(" "); + const time = parts.at(-1); + return ( +
+ {t(labelKey)} + + {time} + + {date ? {date} : null} +
+ ); + } + if (!formatted) { return ( <> @@ -56,10 +85,12 @@ function CloseTime({ nowMs, hud, payload, + layout, }: { nowMs: number; hud: ReturnType; payload: DrawCurrentPayload; + layout: "mobile" | "desktop"; }) { const { t } = useTranslation("player"); const sealedCountdown = isHallSealedCountdownUi(payload.status); @@ -119,11 +150,31 @@ function CloseTime({ label = t("draw.coolDown"); } + const clock = showClock ? formatSecondsClock(seconds) : "--:--"; + + if (layout === "desktop") { + return ( +
+ {label} + + {clock} + + +
+ ); + } + return ( <> - - {showClock ? formatSecondsClock(seconds) : "--:--"} - + {clock} {label} ); @@ -135,7 +186,7 @@ export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) if (error) { return ( -
+

{t(error, { defaultValue: error })}

+ } + /> + +

+ {t("orders.dateRangeHint", { tz: scheduleTzLabel })} +

+ { + if (!range?.from && !range?.to) { + setFromDate(""); + setToDate(""); + return; + } + setFromDate(range?.from ? formatSchedulePickerYmd(range.from) : ""); + setToDate(range?.to ? formatSchedulePickerYmd(range.to) : ""); + }} + /> +
+ +
+ + +
+
+
+ + + + + + {t("orders.status")} + {queryStatuses.length ? ( + + {t(`ticketStatus.${queryStatuses[0]}`, { defaultValue: queryStatuses[0] })} + + ) : null} + + + + + + } + /> + +
+

+ {t("orders.status")} +

+ {queryStatuses.length ? ( + + ) : null} +
+
+ + {STATUS_OPTIONS.map((status) => { + const checked = queryStatuses[0] === status; + return ( + + ); + })} +
+
+
+ + ); return ( -
-
-
+
+ {/* 移动端筛选卡 */} +
+

{drawNoFilter ? t("orders.filteredIssue") : t("orders.totalRecords")} @@ -218,7 +436,7 @@ export function TicketOrdersListScreen() { {drawNoFilter || total}

-
+
) : null}
- {filtersExpanded ? ( -
-
- - setQueryDrawNoState({ base: drawNoFilter, draft: e.target.value }) - } - placeholder={t("orders.drawNo")} - aria-label={t("orders.drawNo")} - className="h-8 border-0 bg-transparent px-0 text-base shadow-none focus-visible:ring-0" - /> -
- -
- - setQueryNumberDraft(e.target.value)} - placeholder={t("orders.number")} - aria-label={t("orders.number")} - className="h-8 border-0 bg-transparent px-0 text-base shadow-none focus-visible:ring-0" - /> -
- - - - - {dateLabel} - - } - /> - -

- {t("orders.dateRangeHint", { tz: scheduleTzLabel })} -

- { - if (!range?.from && !range?.to) { - setFromDate(""); - setToDate(""); - return; - } - setFromDate(range?.from ? formatSchedulePickerYmd(range.from) : ""); - setToDate(range?.to ? formatSchedulePickerYmd(range.to) : ""); - }} - /> -
- -
- - -
-
-
-
- - - - - {t("orders.status")} - {queryStatuses.length ? ( - - {t(`ticketStatus.${queryStatuses[0]}`, { defaultValue: queryStatuses[0] })} - - ) : null} - - - - - - } - /> - -
- - {STATUS_OPTIONS.map((status) => { - const checked = queryStatuses[0] === status; - return ( - - ); - })} -
-
-
-
+
{filterFields}
) : null}
- {loading ? ( -
- {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
- ) : error ? ( -
-

{error}

- -
- ) : items.length === 0 ? ( -
-

{t("orders.empty")}

- - {t("orders.submitBet")} - -
- ) : ( - <> -
- {orderGroups.map((group) => { - const cur = group.currency_code ?? activeCurrency; - const st = ticketStatusDisplay( - group.status, - group.win_amount, - group.jackpot_win_amount, - t, - creditMode, - ); - const totalWin = group.win_amount + group.jackpot_win_amount; - return ( -
+ {/* PC 顶栏:统计 + 筛选 + 操作 */} +
+
+
+

+ {drawNoFilter ? t("orders.filteredIssue") : t("orders.totalRecords")} +

+

+ {drawNoFilter || total} +

+
+
+ {filterFields} +
+
+ {hasActiveFilters ? ( +
- ); - })} + {t("actions.clear")} + + ) : null} + + {t("orders.betNow")} + +
-
- {page < lastPage ? ( +
+ + {loading ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ) : error ? ( +
+

{error}

- ) : lastPage > 1 ? ( -

- {t("orders.noMore")} -

- ) : null} - - )} +
+ ) : items.length === 0 ? ( +
+

{t("orders.empty")}

+ + {t("orders.submitBet")} + +
+ ) : ( + <> + {/* 移动端:卡片分组(默认折叠) */} +
+ {orderGroups.map((group) => { + const cur = group.currency_code ?? activeCurrency; + const st = ticketStatusDisplay( + group.status, + group.win_amount, + group.jackpot_win_amount, + t, + creditMode, + ); + const totalWin = group.win_amount + group.jackpot_win_amount; + const expanded = expandedGroupKeys.has(group.key); + return ( +
+ + + {expanded ? ( +
+

+ {t("orders.betItems")} +

+
+ {group.items.map((row, index) => { + const lineSt = ticketStatusDisplay( + row.status, + row.win_amount, + row.jackpot_win_amount, + t, + creditMode, + ); + const lineCur = row.currency_code ?? cur; + return ( + + + {index + 1} + +
+

+ {playLabel(row.play_code, t)} · {row.original_number ?? row.play_code} +

+

+ + {providerLabel(row.provider_name, row.provider_code)} + + {" · "} + {t("orders.deduction")}{" "} + + {formatMinorAsCurrency(row.actual_deduct_amount, lineCur)} + +

+
+
+ + +
+ + ); + })} +
+
+ ) : null} +
+ ); + })} +
+ + {/* PC:后台风格表格(默认折叠) */} +
+
+ + + + {t("orders.drawNo")} + + + {t("orders.betItems")} + + + {t("hall.providers.title")} + + + {t("orders.stake")} + + + {t("orders.deduction")} + + + {t("orders.status")} + + + + + + {orderGroups.map((group) => { + const cur = group.currency_code ?? activeCurrency; + const st = ticketStatusDisplay( + group.status, + group.win_amount, + group.jackpot_win_amount, + t, + creditMode, + ); + const totalWin = group.win_amount + group.jackpot_win_amount; + const expanded = expandedGroupKeys.has(group.key); + return ( + + toggleGroupExpanded(group.key)} + aria-expanded={expanded} + > + +

+ {group.draw_no ?? "—"} +

+ +
+ +

+ {t("orders.betItems")} · {group.items.length} + {totalWin > 0 && group.status === "settled_win" ? ( + + {t("orders.win", { + amount: formatMinorAsCurrency(totalWin, cur), + })} + + ) : null} + {group.status === "partial_failed" ? ( + + {t("orders.partialFailedHint")} + + ) : null} +

+
+ + +

+ {formatMinorAsCurrency(group.total_bet_amount, cur)} +

+
+ +

+ {formatMinorAsCurrency(group.actual_deduct_amount, cur)} +

+
+ +
+ +
+
+ + + +
+ {expanded + ? group.items.map((row, index) => { + const lineSt = ticketStatusDisplay( + row.status, + row.win_amount, + row.jackpot_win_amount, + t, + creditMode, + ); + const lineCur = row.currency_code ?? cur; + return ( + + + + #{index + 1} + + + + + {playLabel(row.play_code, t)} ·{" "} + {row.original_number ?? row.play_code} + + + + + {providerLabel(row.provider_name, row.provider_code)} + + + + + {formatMinorAsCurrency(row.total_bet_amount, lineCur)} + + + + + {formatMinorAsCurrency(row.actual_deduct_amount, lineCur)} + + + + + + + + + + + + + + ); + }) + : null} +
+ ); + })} +
+
+
+ +
+
+ {page < lastPage ? ( + + ) : lastPage > 1 ? ( +

+ {t("orders.noMore")} +

+ ) : null} +
+ + { + void fetchPage(nextPage, false); + }} + /> + + )} +
); diff --git a/src/features/results/draw-result-detail-screen.tsx b/src/features/results/draw-result-detail-screen.tsx index 4a06c04..71a2246 100644 --- a/src/features/results/draw-result-detail-screen.tsx +++ b/src/features/results/draw-result-detail-screen.tsx @@ -27,7 +27,6 @@ import { formatMinorAsCurrency } from "@/lib/money"; import { isCreditFundingPlayer } from "@/lib/player-funding-mode"; import { norm4d } from "@/lib/norm-4d"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; -import { resultsPrizeLabelKey, RESULTS_TOP_PRIZE_KEYS } from "@/lib/results-prize-labels"; import { cn } from "@/lib/utils"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import type { DrawResultDetailPayload } from "@/types/api/draw-results"; @@ -120,9 +119,9 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) backHref="/results" backLabel={t("results.title")} > -
- - +
+ +
); @@ -135,9 +134,9 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) backHref="/results" backLabel={t("results.title")} > -
+

{error ?? t("results.noData")}

-
@@ -170,33 +169,44 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) backHref="/results" backLabel={t("results.title")} > -
- +
+
+ +
+
+ +
- - -
+ + +
{data.previous_draw_no ? ( {t("results.previous")} ) : ( - )}
- + {data.draw_no} - + {t("results.drawTime", { time: formatPlayerInstant(data.draw_time_iso ?? data.draw_time ?? null), })} @@ -208,98 +218,83 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) prefetch={false} className={cn( buttonVariants({ variant: "outline", size: "sm" }), - "min-w-[5rem] rounded-full border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff]", + "min-w-[5rem] rounded-full border-[#dce7f7] bg-white text-[#0b56b7] hover:bg-[#f1f6ff] lg:min-w-[6rem] lg:rounded-lg", )} > {t("results.next")} ) : ( - )}
- -
-
-

- {t("results.detailTitle")} -

- - {t("results.detail")} - + + {providerResults.length > 1 ? ( +
+ {providerResults.map((row) => { + const code = String(row.provider_code ?? ""); + const active = code === String(activeProviderResult?.provider_code ?? ""); + return ( + + ); + })}
- {providerResults.length > 1 ? ( -
- {providerResults.map((row) => { - const code = String(row.provider_code ?? ""); - const active = code === String(activeProviderResult?.provider_code ?? ""); - return ( - - ); - })} -
- ) : null} -
- {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( -
-

- {t(resultsPrizeLabelKey(tier))} -

-

- {activeResults[tier]} -

-
- ))} -
-
+ ) : null} - {showMyPayout && myTotals ? ( -
-

- {t(creditMode ? "results.creditMyWin" : "results.myPayout")} -

-

- {t("results.regular", { - amount: formatMinorAsCurrency(myTotals.win, currency), - })} - {myTotals.jackpot > 0 ? ( - <> - {" "} - ·{" "} - {t("results.jackpot", { - amount: formatMinorAsCurrency(myTotals.jackpot, currency), + {(showMyPayout && myTotals) || showHitOnly ? ( +

+ {showMyPayout && myTotals ? ( +
+

+ {t(creditMode ? "results.creditMyWin" : "results.myPayout")} +

+

+ {t("results.regular", { + amount: formatMinorAsCurrency(myTotals.win, currency), })} - - ) : null} -

-
- ) : null} + {myTotals.jackpot > 0 ? ( + <> + {" "} + ·{" "} + {t("results.jackpot", { + amount: formatMinorAsCurrency(myTotals.jackpot, currency), + })} + + ) : null} +

+
+ ) : null} - {showHitOnly ? ( -
- {t(creditMode ? "results.creditHitPending" : "results.hitPending")} + {showHitOnly ? ( +
+ {t(creditMode ? "results.creditHitPending" : "results.hitPending")} +
+ ) : null}
) : null} diff --git a/src/features/results/draw-results-list-screen.tsx b/src/features/results/draw-results-list-screen.tsx index 37c7a37..b344edd 100644 --- a/src/features/results/draw-results-list-screen.tsx +++ b/src/features/results/draw-results-list-screen.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { CalendarIcon, XIcon } from "lucide-react"; +import { CalendarIcon, ChevronRight, XIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -17,12 +17,23 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { PlayerPanel } from "@/components/layout/player-panel"; +import { PlayerListPagination } from "@/components/layout/player-list-pagination"; import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip"; import { useCurrencyCatalog } from "@/hooks/use-currency-catalog"; +import { useIsMobile } from "@/hooks/use-mobile"; import { formatPlayerInstant } from "@/lib/player-datetime"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; +import { cn } from "@/lib/utils"; import { resultsPrizeLabelKey, RESULTS_TOP_PRIZE_KEYS } from "@/lib/results-prize-labels"; import type { DrawResultListItem } from "@/types/api/draw-results"; @@ -36,6 +47,7 @@ const MONTH_OPTIONS = Array.from({ length: 12 }, (_, value) => ({ export function DrawResultsListScreen() { const { t } = useTranslation("player"); useCurrencyCatalog(); + const isMobile = useIsMobile(); const [items, setItems] = useState(null); const [error, setError] = useState(null); const [date, setDate] = useState(""); @@ -43,6 +55,7 @@ export function DrawResultsListScreen() { const [calendarMonth, setCalendarMonth] = useState(() => new Date()); const [page, setPage] = useState(1); const [lastPage, setLastPage] = useState(1); + const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const loadMoreRef = useRef(null); @@ -71,6 +84,7 @@ export function DrawResultsListScreen() { setItems((current) => (append && current ? [...current, ...res.items] : res.items)); setPage(res.page); setLastPage(res.last_page); + setTotal(res.total); } catch { setError(t("results.loadFailed")); if (!append) { @@ -92,6 +106,8 @@ export function DrawResultsListScreen() { }, [fetchList]); useEffect(() => { + // 仅移动端无限滚动;PC 用分页 + if (!isMobile) return; const target = loadMoreRef.current; if (!target || loading || loadingMore || page >= lastPage) return; @@ -106,241 +122,350 @@ export function DrawResultsListScreen() { observer.observe(target); return () => observer.disconnect(); - }, [fetchList, lastPage, loading, loadingMore, page]); + }, [fetchList, isMobile, lastPage, loading, loadingMore, page]); + + const listRows = items ?? []; return ( -
- +
+
+
+
+
+ +
-
-

- {t("results.businessDate")} -

-
-
- - +
+

+ {t("results.businessDate")} +

+
+ + + + + {date + || t("results.selectBusinessDate", { + defaultValue: "选择日期", + })} + + + } + /> + +
+ + +
+ { + if (!value) return; + setDate(formatBusinessDate(value)); + setCalendarMonth(value); + setDatePickerOpen(false); + }} + className="rounded-lg" + /> +
+
+
+ {date ? ( + + ) : null} + +
+
+
+
+ + {loading ? ( +
+ + + +
+ ) : error ? ( +
+

{error}

+ +
+ ) : listRows.length === 0 ? ( +
+ {t("results.empty")} +
+ ) : ( + <> + {/* 移动端:首条高亮卡 */} + {featured ? ( +
+
+
+

+ {t("results.latest")} +

+

+ {featured.draw_no} +

+

+ {t("results.drawTime", { + time: formatPlayerInstant( + featured.draw_time_iso ?? featured.draw_time ?? null, + ), + })} +

+ {(featured.provider_results?.length ?? 0) > 1 ? ( +

+ {featured.provider_results?.map((row) => row.provider_name || row.provider_code).filter(Boolean).join(" / ")} +

+ ) : null} +
+ + {t("results.openDetail", { defaultValue: "查看详情" })} + +
+
+ {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( +
+

+ {t(resultsPrizeLabelKey(tier))} +

+

+ {featured.results[tier]} +

+
+ ))} +
+
+ ) : null} + + {/* 移动端:卡片列表(首条已高亮展示时跳过) */} +
+ {listRows.slice(1).map((row) => ( + +
+
+

+ {row.draw_no} +

+

+ {formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)} +

+ {(row.provider_results?.length ?? 0) > 1 ? ( +

+ {row.provider_results?.map((item) => item.provider_name || item.provider_code).filter(Boolean).join(" / ")} +

+ ) : null} +
+ + {t("results.detail")} + +
+
+ {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( +
+

+ {t(resultsPrizeLabelKey(tier))} +

+

+ {row.results[tier]} +

+
+ ))} +
+ + ))} +
+ + {/* PC:后台风格表格 */} +
+ + + + + {t("results.detailTitle")} + + {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( + + {t(resultsPrizeLabelKey(tier))} + + ))} + + + + + {listRows.map((row, index) => { + const isLatest = page === 1 && index === 0; + const href = `/results/${encodeURIComponent(row.draw_no)}`; + return ( + + + +
+

+ {row.draw_no} +

+ {isLatest ? ( + + {t("results.latest")} + + ) : null} +
+

+ {formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)} +

+ {(row.provider_results?.length ?? 0) > 1 ? ( +

+ {row.provider_results + ?.map((item) => item.provider_name || item.provider_code) + .filter(Boolean) + .join(" / ")} +

+ ) : null} + +
+ {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( + + + {row.results[tier]} + + + ))} + + + + + +
+ ); + })} +
+
+
+ + { + void fetchList(nextPage, false); + }} + /> + +
+
+ {page < lastPage ? ( - } - /> - -
- - -
- { - if (!value) return; - setDate(formatBusinessDate(value)); - setCalendarMonth(value); - setDatePickerOpen(false); - }} - className="rounded-lg" - /> -
- -
- {date ? ( - - ) : null} - -
-
- - {loading ? ( -
- - - -
- ) : error ? ( -
-

{error}

- -
- ) : items && items.length === 0 ? ( -
- {t("results.empty")} -
- ) : ( -
- {featured ? ( -
-
-
-

- {t("results.detailTitle")} -

-

- {featured.draw_no} -

-

- {t("results.drawTime", { - time: formatPlayerInstant( - featured.draw_time_iso ?? featured.draw_time ?? null, - ), - })} -

- {(featured.provider_results?.length ?? 0) > 1 ? ( -

- {featured.provider_results?.map((row) => row.provider_name || row.provider_code).filter(Boolean).join(" / ")} -

- ) : null} -
- - {t("results.openDetail", { defaultValue: "查看详情" })} - -
-
- {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( -
-

- {t(resultsPrizeLabelKey(tier))} -

-

- {featured.results[tier]} -

-
- ))} -
+ ) : listRows.length > 0 ? ( +

+ {t("results.noMore")} +

+ ) : null}
- ) : null} - - {items?.slice(1).map((row) => ( - -
-
-

- {row.draw_no} -

-

- {formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)} -

- {(row.provider_results?.length ?? 0) > 1 ? ( -

- {row.provider_results?.map((item) => item.provider_name || item.provider_code).filter(Boolean).join(" / ")} -

- ) : null} -
- - {t("results.detail")} - -
- -
- {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( -
-

- {t(resultsPrizeLabelKey(tier))} -

-

- {row.results[tier]} -

-
- ))} -
- - ))} -
- {page < lastPage ? ( - - ) : items && items.length > 0 ? ( -

- {t("results.noMore")} -

- ) : null} -
- )} + + )} +
); diff --git a/src/features/results/jackpot-results-strip.tsx b/src/features/results/jackpot-results-strip.tsx index dc4c312..03831f7 100644 --- a/src/features/results/jackpot-results-strip.tsx +++ b/src/features/results/jackpot-results-strip.tsx @@ -5,14 +5,18 @@ import { useTranslation } from "react-i18next"; import { getJackpotSummary } from "@/api/jackpot"; import { formatMinorAsCurrency } from "@/lib/money"; +import { cn } from "@/lib/utils"; type JackpotResultsStripProps = { currencyCode?: string; + /** PC 工具栏内嵌时用更扁的一行布局 */ + compact?: boolean; }; /** 开奖模块顶部:Jackpot 当前池(公开接口) */ export function JackpotResultsStrip({ currencyCode = "NPR", + compact = false, }: JackpotResultsStripProps) { const { t } = useTranslation("player"); const [minor, setMinor] = useState(null); @@ -47,18 +51,46 @@ export function JackpotResultsStrip({ } return ( -
-

- {t("results.jackpotLabel")} -

-

- {formatMinorAsCurrency(minor, currencyCode.toUpperCase())} -

- {gap !== null ? ( -

- {t("results.jackpotGap", { count: gap })} +

+
+

+ {t("results.jackpotLabel")}

- ) : null} +

+ {formatMinorAsCurrency(minor, currencyCode.toUpperCase())} +

+ {gap !== null ? ( +

+ {t("results.jackpotGap", { count: gap })} +

+ ) : null} +
); } diff --git a/src/features/results/twenty-three-results-grid.tsx b/src/features/results/twenty-three-results-grid.tsx index 65d292d..bd9dd63 100644 --- a/src/features/results/twenty-three-results-grid.tsx +++ b/src/features/results/twenty-three-results-grid.tsx @@ -36,7 +36,7 @@ export function TwentyThreeResultsGrid({ const smallCellTone = (raw: string, tone: "red" | "blue") => cn( - "grid min-h-[3.875rem] grid-rows-[auto_1fr] rounded-lg border bg-white px-1.5 py-2 text-center shadow-[0_6px_16px_rgba(15,23,42,0.04)]", + "grid min-h-[3.25rem] grid-rows-[auto_1fr] rounded-lg border bg-white px-1 py-1.5 text-center shadow-[0_6px_16px_rgba(15,23,42,0.04)] lg:min-h-[3.5rem]", tone === "red" ? "border-red-100 text-[#e5002c]" : "border-blue-100 text-[#0b56b7]", isHit(raw) && "border-amber-400 bg-amber-50 text-amber-700 shadow-[0_8px_18px_rgba(245,158,11,0.16)]", ); @@ -72,58 +72,60 @@ export function TwentyThreeResultsGrid({ ]; return ( -
-
+
+
{prizeCards.map((card) => (
-
- +
+
-

{card.label}

-

{card.value}

+

{card.label}

+

{card.value}

))}
-
-

- - {t("results.grid.starter")} -

-
- {Array.from({ length: 10 }).map((_, i) => ( -
- {i + 1} - - {starters[i] ?? "—"} - -
- ))} +
+
+

+ + {t("results.grid.starter")} +

+
+ {Array.from({ length: 10 }).map((_, i) => ( +
+ {i + 1} + + {starters[i] ?? "—"} + +
+ ))} +
-
-
-

- - {t("results.grid.consolation")} -

-
- {Array.from({ length: 10 }).map((_, i) => ( -
- {i + 1} - - {consos[i] ?? "—"} - -
- ))} +
+

+ + {t("results.grid.consolation")} +

+
+ {Array.from({ length: 10 }).map((_, i) => ( +
+ {i + 1} + + {consos[i] ?? "—"} + +
+ ))} +
diff --git a/src/features/wallet/wallet-logs-block.tsx b/src/features/wallet/wallet-logs-block.tsx index c11747d..cbce1aa 100644 --- a/src/features/wallet/wallet-logs-block.tsx +++ b/src/features/wallet/wallet-logs-block.tsx @@ -3,11 +3,13 @@ import { useMemo, type Ref } from "react"; import { useTranslation } from "react-i18next"; +import { PlayerListPagination } from "@/components/layout/player-list-pagination"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { formatLotteryInstantInTimeZone, formatPlayerInstant } from "@/lib/player-datetime"; import { formatMinorAsCurrency } from "@/lib/money"; import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs"; +import { getWalletLogsLastPage } from "@/types/api/wallet-logs"; /** 与 §4.9 筛选一致;接口 `type` 查询参数 */ export const WALLET_FLOW_FILTERS: { value: string; labelKey: string }[] = [ @@ -101,6 +103,8 @@ type WalletLogsBlockProps = { hasMore?: boolean; onLoadMore?: () => void; loadMoreRef?: Ref; + /** PC:翻页(替换无限滚动) */ + onPageChange?: (page: number) => void; filter: string; onFilterChange: (value: string) => void; currency: string; @@ -119,6 +123,7 @@ export function WalletLogsBlock({ hasMore = false, onLoadMore, loadMoreRef, + onPageChange, filter, onFilterChange, currency, @@ -142,29 +147,32 @@ export function WalletLogsBlock({ return ( <> -
-
+
+
-

{resolvedTitle}

+

{resolvedTitle}

-
- {filters.map((f) => ( - - ))} + {/* 移动端:小胶囊;PC:大厅 4D/3D 同款分段控件 */} +
+ {filters.map((f) => { + const active = filter === f.value; + return ( + + ); + })}
@@ -174,30 +182,68 @@ export function WalletLogsBlock({ {logs ? ( <> -

+ {/* PC 分页底栏已含总数,避免重复 */} +

{t("wallet.totalRecords", { total: logs.total })}

-
    +
    {logs.items.length === 0 ? ( -
  • +
    {t(creditMode ? "wallet.emptyCreditLogs" : "wallet.emptyLogs", { defaultValue: creditMode ? "暂无信用流水" : "暂无流水", })} -
  • +
    ) : ( - logs.items.map((row) => ( - - )) + <> +
    +
    +

    + {t("wallet.logType", { defaultValue: "类型" })} +

    +

    + {t("wallet.logTime", { defaultValue: "时间" })} +

    +

    + {t("wallet.logRef", { defaultValue: "关联单号" })} +

    +

    + {t("wallet.logAmount", { defaultValue: "金额" })} +

    +

    + {creditMode + ? t("wallet.creditAvailableAfter") + : t("wallet.balanceAfter")} +

    +

    + {t("orders.status")} +

    +
    +
      + {logs.items.map((row) => ( + + ))} +
    + {onPageChange ? ( + + ) : null} +
    + )} -
+
{logs.items.length > 0 ? ( - <> +
{hasMore ? (
) : null} ) : null} @@ -258,36 +304,52 @@ export function LogRow({ ? "border-slate-200 bg-slate-50 text-slate-600" : "border-blue-200 bg-blue-50 text-blue-700"; + const timeLabel = creditMode && settlementTimeZone + ? formatLotteryInstantInTimeZone(item.created_at, settlementTimeZone) + : formatPlayerInstant(item.created_at); + + const amountLabel = isCreditSettlementConfirm + ? t("wallet.creditReleasedAmount", { + defaultValue: "释额 {{amount}}", + amount: formatMinorAsCurrency(item.amount_abs, ccy), + }) + : `${isIn ? "+" : "−"}${formatMinorAsCurrency(item.amount_abs, ccy)}`; + + const balanceLabel = item.balance_after != null + ? formatMinorAsCurrency(item.balance_after, ccy) + : creditMode && item.affects_available_credit === false + ? t("wallet.noCreditChange", { defaultValue: "不改变可用信用" }) + : "—"; + + const balanceCaption = creditMode + ? item.affects_available_credit === false + ? t("wallet.creditBillOnly", { defaultValue: "账期记录" }) + : t("wallet.creditAvailableAfter") + : t("wallet.balanceAfter"); + return ( -
  • -
    -
    +
  • +
    +
    - -

    + +

    {logTypeLabel(item.type, t, creditMode, item.biz_type)}

    -

    - {creditMode && settlementTimeZone - ? formatLotteryInstantInTimeZone(item.created_at, settlementTimeZone) - : formatPlayerInstant(item.created_at)} +

    + {timeLabel}

    {item.ref_id ? ( -

    +

    {item.ref_id}

    ) : null}
    -
    +

    - {isCreditSettlementConfirm - ? t("wallet.creditReleasedAmount", { - defaultValue: "释额 {{amount}}", - amount: formatMinorAsCurrency(item.amount_abs, ccy), - }) - : `${isIn ? "+" : "−"}${formatMinorAsCurrency(item.amount_abs, ccy)}`} + {amountLabel}

    {txnStatusLabel(item.status, t)} @@ -295,20 +357,28 @@ export function LogRow({
    -
    +

    {timeLabel}

    +

    + {item.ref_id || "—"} +

    +

    + {amountLabel} +

    +

    + {balanceLabel} +

    +
    + + {txnStatusLabel(item.status, t)} + +
    + +
    - {creditMode - ? item.affects_available_credit === false - ? t("wallet.creditBillOnly", { defaultValue: "账期记录" }) - : t("wallet.creditAvailableAfter") - : t("wallet.balanceAfter")} + {balanceCaption} - {item.balance_after != null - ? formatMinorAsCurrency(item.balance_after, ccy) - : creditMode && item.affects_available_credit === false - ? t("wallet.noCreditChange", { defaultValue: "不改变可用信用" }) - : "—"} + {balanceLabel}
  • diff --git a/src/features/wallet/wallet-screen.tsx b/src/features/wallet/wallet-screen.tsx index b4337eb..e67869d 100644 --- a/src/features/wallet/wallet-screen.tsx +++ b/src/features/wallet/wallet-screen.tsx @@ -17,6 +17,7 @@ import { import { PlayerMoneyDisplay } from "@/components/player-money-display"; import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; +import { useIsMobile } from "@/hooks/use-mobile"; import { isCreditFundingPlayer } from "@/lib/player-funding-mode"; import { formatMinorAsCurrency } from "@/lib/money"; import { formatLotteryInstantInTimeZone, formatPlayerInstant } from "@/lib/player-datetime"; @@ -102,22 +103,22 @@ function CreditSettlementBillsBlock({ const netDirection = netPending >= 0 ? "receivable" : "payable"; return ( -
    -
    -
    -

    +
    +
    +
    +

    {t("wallet.settlementTitle", { defaultValue: "我的账期账单" })}

    -

    +

    {t("wallet.settlementHint", { defaultValue: "只显示未结清账单,中奖超出授信的部分在这里结算。" })}

    - + {t("wallet.settlementPendingCount", { defaultValue: "{{count}}笔", count: pendingCount })}
    -
    +

    {t("wallet.pendingReceivable", { defaultValue: "待收" })} @@ -134,27 +135,26 @@ function CreditSettlementBillsBlock({ {formatMinorAsCurrency(summary?.pending_payable ?? 0, currency)}

    -
    - -
    - - {netDirection === "receivable" - ? t("wallet.netReceivable", { defaultValue: "净待收" }) - : t("wallet.netPayable", { defaultValue: "净待付" })} - - - {formatMinorAsCurrency(Math.abs(netPending), currency)} - +
    + + {netDirection === "receivable" + ? t("wallet.netReceivable", { defaultValue: "净待收" }) + : t("wallet.netPayable", { defaultValue: "净待付" })} + + + {formatMinorAsCurrency(Math.abs(netPending), currency)} + +
    {data === null ? ( - + ) : data.items.length === 0 ? ( -

    +

    {t("wallet.noSettlementBills", { defaultValue: "暂无待结算账单" })}

    ) : ( -
      +
        {data.items.map((item) => { const receivable = item.direction === "receivable"; return ( @@ -218,6 +218,7 @@ export function WalletScreen() { const fundingModeView = resolveFundingModeView(balance, profile); const fundingModeKnown = fundingModeView !== "unknown"; const isCreditPlayer = fundingModeView === "credit"; + const isMobile = useIsMobile(); useEffect(() => { if (actionDeepLinkHandledRef.current || loading) return; @@ -414,7 +415,22 @@ export function WalletScreen() { }); }, [hasMore, loadLogs, loadingMore, logs, t]); + const handleLogsPageChange = useCallback((nextPage: number) => { + if (!logs || nextPage === logs.page || logsLoading) return; + setError(null); + setLogsLoading(true); + void loadLogs(nextPage, false) + .catch((e) => { + setError(formatWalletClientError(e, t)); + }) + .finally(() => { + setLogsLoading(false); + }); + }, [loadLogs, logs, logsLoading, t]); + useEffect(() => { + // 仅移动端无限滚动;PC 用分页 + if (!isMobile) return; const target = loadMoreRef.current; if (!target || loading || logsLoading || loadingMore || !hasMore) return; @@ -429,13 +445,13 @@ export function WalletScreen() { observer.observe(target); return () => observer.disconnect(); - }, [hasMore, loadMore, loading, loadingMore, logsLoading]); + }, [hasMore, isMobile, loadMore, loading, loadingMore, logsLoading]); return ( -
        +
        {error ? ( -
        +

        {error}