diff --git a/src/app/globals.css b/src/app/globals.css index 6747975..54e02ec 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -121,11 +121,14 @@ * { @apply border-border outline-ring/50; } - body { - @apply bg-background text-foreground; - } html { @apply font-sans; + -webkit-text-size-adjust: 100%; + } + body { + @apply bg-background text-foreground; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; } html:lang(ne) { font-family: @@ -134,6 +137,9 @@ system-ui, sans-serif; } + button, a, [role="button"] { + touch-action: manipulation; + } } /* 玩家端 Toast:顶部居中、紧凑尺寸(位置见 components/ui/sonner.tsx) */ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0eede12..0cc0d2c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -28,6 +28,8 @@ export const metadata: Metadata = { export const viewport = { width: "device-width", initialScale: 1, + maximumScale: 1, + userScalable: false, }; export default function RootLayout({ @@ -39,9 +41,9 @@ export default function RootLayout({ -
{t("hall.preview.totalBet")}
@@ -319,7 +319,7 @@ export function HallBetPreviewDialog({
{t("hall.result.orderNo")}:{" "} {data.order_no} @@ -232,7 +232,7 @@ export function HallBetResultDialog({ {item.number} - + {item.ticket_no} @@ -287,7 +287,7 @@ export function HallBetResultDialog({
+
{t("hall.table.soldOut")}
{t("hall.table.warning")}
{t("draw.issueNo")}
{display.draw_no}
{t("orders.stake")}
{formatMinorAsCurrency(group.total_bet_amount, cur)}
{t("orders.deduction")}
{formatMinorAsCurrency(group.actual_deduct_amount, cur)}
@@ -241,7 +241,7 @@ export function TicketOrdersListScreen() {
{activeCurrency} {profile?.locale ? ( diff --git a/src/features/results/draw-result-detail-screen.tsx b/src/features/results/draw-result-detail-screen.tsx index ef6c358..fd2d5a5 100644 --- a/src/features/results/draw-result-detail-screen.tsx +++ b/src/features/results/draw-result-detail-screen.tsx @@ -224,7 +224,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) key={tier} className="rounded-lg border border-[#edf2f8] bg-white py-2 shadow-[0_4px_12px_rgba(15,23,42,0.03)]" > - + {t(resultsPrizeLabelKey(tier))} @@ -276,7 +276,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) href="/results/check" className={cn( buttonVariants({ variant: "default", size: "sm" }), - "mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028] sm:w-auto", + "mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028]", )} > {t("results.viewMyWinning")} diff --git a/src/features/results/draw-results-list-screen.tsx b/src/features/results/draw-results-list-screen.tsx index 85470b9..f491811 100644 --- a/src/features/results/draw-results-list-screen.tsx +++ b/src/features/results/draw-results-list-screen.tsx @@ -295,7 +295,7 @@ export function DrawResultsListScreen() { {RESULTS_TOP_PRIZE_KEYS.map((tier) => ( - + {t(resultsPrizeLabelKey(tier))} diff --git a/src/features/rules/play-rules-screen.tsx b/src/features/rules/play-rules-screen.tsx index 97d4b17..2344e1d 100644 --- a/src/features/rules/play-rules-screen.tsx +++ b/src/features/rules/play-rules-screen.tsx @@ -41,7 +41,7 @@ export function PlayRulesScreen() { > - + {loading ? ( diff --git a/src/hooks/use-pull-to-refresh.ts b/src/hooks/use-pull-to-refresh.ts new file mode 100644 index 0000000..26cb10a --- /dev/null +++ b/src/hooks/use-pull-to-refresh.ts @@ -0,0 +1,120 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; + +interface PullToRefreshOptions { + onRefresh: () => void | Promise; + threshold?: number; + enabled?: boolean; +} + +interface PullToRefreshResult { + pullDistance: number; + isRefreshing: boolean; + pullPercent: number; +} + +/** + * Pull-to-refresh hook for mobile H5 pages. + * Attaches touch listeners to the document body and triggers `onRefresh` + * when the user pulls down past `threshold` while scrolled to top. + */ +export function usePullToRefresh({ + onRefresh, + threshold = 70, + enabled = true, +}: PullToRefreshOptions): PullToRefreshResult { + const [pullDistance, setPullDistance] = useState(0); + const [isRefreshing, setIsRefreshing] = useState(false); + const startYRef = useRef(0); + const pullingRef = useRef(false); + const pullDistanceRef = useRef(0); + const onRefreshRef = useRef(onRefresh); + + onRefreshRef.current = onRefresh; + + const handleTouchStart = useCallback( + (e: TouchEvent) => { + if (!enabled || isRefreshing) return; + + // Check if the scroll container is scrolled down + const scrollContainer = document.getElementById("player-scroll-container"); + const currentScrollY = scrollContainer ? scrollContainer.scrollTop : window.scrollY; + + if (currentScrollY > 0) return; + startYRef.current = e.touches[0].clientY; + pullingRef.current = true; + }, + [enabled, isRefreshing], + ); + + const handleTouchMove = useCallback( + (e: TouchEvent) => { + if (!pullingRef.current || isRefreshing) return; + const delta = e.touches[0].clientY - startYRef.current; + if (delta <= 0) { + // User is scrolling down — abort pull-to-refresh entirely + pullingRef.current = false; + if (pullDistanceRef.current !== 0) { + pullDistanceRef.current = 0; + setPullDistance(0); + } + return; + } + // Apply rubber-band resistance + const resisted = Math.min(delta * 0.4, threshold * 1.5); + if (resisted !== pullDistanceRef.current) { + pullDistanceRef.current = resisted; + setPullDistance(resisted); + } + }, + [isRefreshing, threshold], + ); + + const handleTouchEnd = useCallback(async () => { + if (!pullingRef.current) { + if (pullDistanceRef.current !== 0) { + pullDistanceRef.current = 0; + setPullDistance(0); + } + return; + } + pullingRef.current = false; + + if (pullDistanceRef.current >= threshold) { + setIsRefreshing(true); + pullDistanceRef.current = threshold * 0.5; + setPullDistance(threshold * 0.5); + try { + await onRefreshRef.current(); + } finally { + setIsRefreshing(false); + pullDistanceRef.current = 0; + setPullDistance(0); + } + } else { + pullDistanceRef.current = 0; + setPullDistance(0); + } + }, [threshold]); + + useEffect(() => { + if (!enabled) return; + + document.addEventListener("touchstart", handleTouchStart, { passive: true }); + document.addEventListener("touchmove", handleTouchMove, { passive: true }); + document.addEventListener("touchend", handleTouchEnd, { passive: true }); + + return () => { + document.removeEventListener("touchstart", handleTouchStart); + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + }; + }, [enabled, handleTouchStart, handleTouchMove, handleTouchEnd]); + + return { + pullDistance, + isRefreshing, + pullPercent: Math.min(pullDistance / threshold, 1), + }; +}
{t(resultsPrizeLabelKey(tier))}
@@ -276,7 +276,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) href="/results/check" className={cn( buttonVariants({ variant: "default", size: "sm" }), - "mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028] sm:w-auto", + "mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028]", )} > {t("results.viewMyWinning")} diff --git a/src/features/results/draw-results-list-screen.tsx b/src/features/results/draw-results-list-screen.tsx index 85470b9..f491811 100644 --- a/src/features/results/draw-results-list-screen.tsx +++ b/src/features/results/draw-results-list-screen.tsx @@ -295,7 +295,7 @@ export function DrawResultsListScreen() {
diff --git a/src/features/rules/play-rules-screen.tsx b/src/features/rules/play-rules-screen.tsx index 97d4b17..2344e1d 100644 --- a/src/features/rules/play-rules-screen.tsx +++ b/src/features/rules/play-rules-screen.tsx @@ -41,7 +41,7 @@ export function PlayRulesScreen() { >