From 01baf9c18b8cd3ef535872fd6db5c41f6be1153d Mon Sep 17 00:00:00 2001 From: kang Date: Fri, 15 May 2026 16:52:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=B3=A8=E5=8D=95?= =?UTF-8?q?=E4=B8=8E=E9=92=B1=E5=8C=85=E6=B5=81=E6=B0=B4=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 注单列表与钱包流水支持 10 条分页、滚动触底自动加载和手动加载更多 - 新增钱包流水无更多数据提示与分页末页计算工具 - 精简钱包首页快捷入口与页面标题眉标展示 - 将下注表格草稿合计文案调整为投注金额并同步多语言翻译 --- src/components/layout/player-panel.tsx | 6 -- src/features/hall/hall-betting-grid.tsx | 2 +- .../orders/ticket-orders-list-screen.tsx | 45 +++++++-- src/features/wallet/wallet-logs-block.tsx | 32 +++++- src/features/wallet/wallet-logs-screen.tsx | 58 +++++++++-- src/features/wallet/wallet-screen.tsx | 98 ++++++++++++------- src/i18n/locales/en/player.json | 2 +- src/i18n/locales/ne/player.json | 2 +- src/i18n/locales/zh/player.json | 2 +- src/types/api/wallet-logs.ts | 5 + 10 files changed, 187 insertions(+), 65 deletions(-) diff --git a/src/components/layout/player-panel.tsx b/src/components/layout/player-panel.tsx index 5b2ab65..ce6e1f3 100644 --- a/src/components/layout/player-panel.tsx +++ b/src/components/layout/player-panel.tsx @@ -22,7 +22,6 @@ type PlayerPanelProps = { export function PlayerPanel({ title, subtitle, - eyebrow, children, backHref = "/hall", backLabel, @@ -49,11 +48,6 @@ export function PlayerPanel({ {resolvedBackLabel}
- {eyebrow ? ( -

- {eyebrow} -

- ) : null}

{title}

diff --git a/src/features/hall/hall-betting-grid.tsx b/src/features/hall/hall-betting-grid.tsx index a71b785..b5e2597 100644 --- a/src/features/hall/hall-betting-grid.tsx +++ b/src/features/hall/hall-betting-grid.tsx @@ -1007,7 +1007,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
- {t("hall.table.draftTotal", { defaultValue: "草稿合计" })} + {t("hall.table.draftTotal", { defaultValue: "投注金额" })} {formatMinorAsCurrency(debouncedSummary.actual, currencyCode)} diff --git a/src/features/orders/ticket-orders-list-screen.tsx b/src/features/orders/ticket-orders-list-screen.tsx index 4e1ef36..45e37bf 100644 --- a/src/features/orders/ticket-orders-list-screen.tsx +++ b/src/features/orders/ticket-orders-list-screen.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { getTicketItems } from "@/api/ticket-items"; @@ -15,6 +15,8 @@ import { formatLotteryInstant } from "@/lib/player-datetime"; import { playLabel } from "@/lib/play-labels"; import type { TicketItemListRow } from "@/types/api/ticket-items"; +const ORDERS_PAGE_SIZE = 10; + export function TicketOrdersListScreen() { const searchParams = useSearchParams(); const { t } = useTranslation("player"); @@ -30,6 +32,7 @@ export function TicketOrdersListScreen() { const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); + const loadMoreRef = useRef(null); const fetchPage = useCallback( async (nextPage: number, append: boolean) => { @@ -39,7 +42,7 @@ export function TicketOrdersListScreen() { try { const res = await getTicketItems({ page: nextPage, - per_page: 20, + per_page: ORDERS_PAGE_SIZE, draw_no: drawNoFilter || undefined, }); setItems((prev) => (append ? [...prev, ...res.items] : res.items)); @@ -63,10 +66,27 @@ export function TicketOrdersListScreen() { }); }, [fetchPage]); - const loadMore = () => { + const loadMore = useCallback(() => { if (page >= lastPage || loadingMore) return; void fetchPage(page + 1, true); - }; + }, [fetchPage, lastPage, loadingMore, page]); + + useEffect(() => { + const target = loadMoreRef.current; + if (!target || loading || loadingMore || page >= lastPage) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + loadMore(); + } + }, + { rootMargin: "160px" }, + ); + + observer.observe(target); + return () => observer.disconnect(); + }, [lastPage, loadMore, loading, loadingMore, page]); return ( @@ -186,17 +206,24 @@ export function TicketOrdersListScreen() { ); })}
+
{page < lastPage ? ( - ) : null} + ) : ( +

+ {t("orders.noMore", { defaultValue: "没有更多注单" })} +

+ )} )}
diff --git a/src/features/wallet/wallet-logs-block.tsx b/src/features/wallet/wallet-logs-block.tsx index 468ab83..8f73f08 100644 --- a/src/features/wallet/wallet-logs-block.tsx +++ b/src/features/wallet/wallet-logs-block.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, type Ref } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; @@ -37,6 +37,10 @@ function txnStatusLabel( type WalletLogsBlockProps = { logs: WalletLogsData | null; logsLoading: boolean; + loadingMore?: boolean; + hasMore?: boolean; + onLoadMore?: () => void; + loadMoreRef?: Ref; filter: string; onFilterChange: (value: string) => void; currency: string; @@ -48,6 +52,10 @@ type WalletLogsBlockProps = { export function WalletLogsBlock({ logs, logsLoading, + loadingMore = false, + hasMore = false, + onLoadMore, + loadMoreRef, filter, onFilterChange, currency, @@ -132,6 +140,28 @@ export function WalletLogsBlock({ )) )} + {logs.items.length > 0 ? ( + <> +
+ {hasMore ? ( + + ) : ( +

+ {t("wallet.noMoreLogs", { defaultValue: "没有更多流水" })} +

+ )} + + ) : null} ) : null} diff --git a/src/features/wallet/wallet-logs-screen.tsx b/src/features/wallet/wallet-logs-screen.tsx index 59061cd..b04ee31 100644 --- a/src/features/wallet/wallet-logs-screen.tsx +++ b/src/features/wallet/wallet-logs-screen.tsx @@ -9,7 +9,9 @@ import { PlayerPanel } from "@/components/layout/player-panel"; import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block"; import { formatWalletClientError } from "@/lib/wallet-api-error"; import { usePlayerSessionStore } from "@/stores/player-session-store"; -import type { WalletLogsData } from "@/types/api/wallet-logs"; +import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs"; + +const WALLET_LOGS_PAGE_SIZE = 10; export function WalletLogsScreen() { const profile = usePlayerSessionStore((s) => s.profile); @@ -18,7 +20,9 @@ export function WalletLogsScreen() { const [filter, setFilter] = useState(""); const [loading, setLoading] = useState(true); const [logsLoading, setLogsLoading] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); + const loadMoreRef = useRef(null); const currency = useMemo( () => (profile?.default_currency ?? "NPR").toUpperCase(), @@ -27,35 +31,69 @@ export function WalletLogsScreen() { const fetchPassRef = useRef(true); - const load = useCallback(async () => { + const load = useCallback(async (targetPage = 1, append = false) => { setError(null); - if (fetchPassRef.current) { + if (append) { + setLoadingMore(true); + } else if (fetchPassRef.current) { setLoading(true); fetchPassRef.current = false; } else { setLogsLoading(true); } try { - const L = await getWalletLogs({ - page: 1, - size: 50, + const nextLogs = await getWalletLogs({ + page: targetPage, + size: WALLET_LOGS_PAGE_SIZE, type: filter || undefined, }); - setLogs(L); + setLogs((current) => + append && current + ? { ...nextLogs, items: [...current.items, ...nextLogs.items] } + : nextLogs, + ); } catch (e) { setError(formatWalletClientError(e, t)); + if (!append) { + setLogs(null); + } } finally { setLoading(false); setLogsLoading(false); + setLoadingMore(false); } }, [filter, t]); useEffect(() => { queueMicrotask(() => { - void load(); + void load(1, false); }); }, [load]); + const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false; + + const loadMore = useCallback(() => { + if (!logs || !hasMore || loadingMore) return; + void load(logs.page + 1, true); + }, [hasMore, load, loadingMore, logs]); + + useEffect(() => { + const target = loadMoreRef.current; + if (!target || loading || logsLoading || loadingMore || !hasMore) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + loadMore(); + } + }, + { rootMargin: "160px" }, + ); + + observer.observe(target); + return () => observer.disconnect(); + }, [hasMore, loadMore, loading, loadingMore, logsLoading]); + return ( s.profile); @@ -28,7 +29,9 @@ export function WalletScreen() { const [filter, setFilter] = useState(""); const [loading, setLoading] = useState(true); const [logsLoading, setLogsLoading] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); + const loadMoreRef = useRef(null); const currency = useMemo(() => { return ( @@ -40,6 +43,20 @@ export function WalletScreen() { const fetchPassRef = useRef(true); + const loadLogs = useCallback(async (targetPage = 1, append = false) => { + const nextLogs = await getWalletLogs({ + page: targetPage, + size: WALLET_LOGS_PAGE_SIZE, + type: filter || undefined, + }); + setLogs((current) => + append && current + ? { ...nextLogs, items: [...current.items, ...nextLogs.items] } + : nextLogs, + ); + return nextLogs; + }, [filter]); + useEffect(() => { let cancelled = false; @@ -55,13 +72,9 @@ export function WalletScreen() { const b = await getWalletBalance(); if (cancelled) return; setBalance(b); - const L = await getWalletLogs({ - page: 1, - size: 50, - type: filter || undefined, - }); + const nextLogs = await loadLogs(1, false); if (cancelled) return; - setLogs(L); + setLogs(nextLogs); } catch (e) { if (!cancelled) { setError(formatWalletClientError(e, t)); @@ -77,7 +90,7 @@ export function WalletScreen() { return () => { cancelled = true; }; - }, [filter, t]); + }, [loadLogs, t]); const refreshAll = useCallback(async () => { setError(null); @@ -85,19 +98,47 @@ export function WalletScreen() { try { const b = await getWalletBalance(); setBalance(b); - const L = await getWalletLogs({ - page: 1, - size: 50, - type: filter || undefined, - }); - setLogs(L); + await loadLogs(1, false); } catch (e) { setError(formatWalletClientError(e, t)); } finally { setLogsLoading(false); setLoading(false); } - }, [filter, t]); + }, [loadLogs, t]); + + const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false; + + const loadMore = useCallback(() => { + if (!logs || !hasMore || loadingMore) return; + + setError(null); + setLoadingMore(true); + void loadLogs(logs.page + 1, true) + .catch((e) => { + setError(formatWalletClientError(e, t)); + }) + .finally(() => { + setLoadingMore(false); + }); + }, [hasMore, loadLogs, loadingMore, logs, t]); + + useEffect(() => { + const target = loadMoreRef.current; + if (!target || loading || logsLoading || loadingMore || !hasMore) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + loadMore(); + } + }, + { rootMargin: "160px" }, + ); + + observer.observe(target); + return () => observer.disconnect(); + }, [hasMore, loadMore, loading, loadingMore, logsLoading]); return ( @@ -159,30 +200,13 @@ export function WalletScreen() { />
-
- - {t("wallet.inPage")} - - - {t("wallet.outPage")} - - - {t("wallet.logs")} - -
- | null): number { + if (!logs || logs.per_page <= 0) return 1; + return Math.max(1, Math.ceil(logs.total / logs.per_page)); +} + export type GetWalletLogsParams = { page?: number; /** 每页条数(PRD 示例 `size`) */