feat: 优化开奖结果查询与下注弹窗视觉交互,新增中奖查询页
This commit is contained in:
BIN
public/entry/image4.png
Normal file
BIN
public/entry/image4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/entry/image5.png
Normal file
BIN
public/entry/image5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 731 KiB |
BIN
public/entry/image6.png
Normal file
BIN
public/entry/image6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 KiB |
5
src/app/(player)/(main)/results/check/page.tsx
Normal file
5
src/app/(player)/(main)/results/check/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { CheckWinningScreen } from "@/features/results/check-winning-screen";
|
||||||
|
|
||||||
|
export default function CheckWinningPage() {
|
||||||
|
return <CheckWinningScreen />;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
|
||||||
import { Providers } from "@/components/providers";
|
import { Providers } from "@/components/providers";
|
||||||
import { DEFAULT_LANGUAGE } from "@/i18n";
|
import { DEFAULT_LANGUAGE } from "@/i18n/language";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { ThemeProvider } from "next-themes";
|
|
||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { ErrorProvider } from "@/components/error-provider";
|
import { ErrorProvider } from "@/components/error-provider";
|
||||||
import { IframeBridge } from "@/components/iframe-bridge";
|
import { IframeBridge } from "@/components/iframe-bridge";
|
||||||
@@ -16,7 +14,7 @@ type ProvidersProps = {
|
|||||||
|
|
||||||
export function Providers({ children }: ProvidersProps): ReactNode {
|
export function Providers({ children }: ProvidersProps): ReactNode {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<>
|
||||||
<ErrorProvider>
|
<ErrorProvider>
|
||||||
{/* iframe 通信桥接 - 支持主站嵌入 */}
|
{/* iframe 通信桥接 - 支持主站嵌入 */}
|
||||||
<IframeBridge>
|
<IframeBridge>
|
||||||
@@ -26,6 +24,6 @@ export function Providers({ children }: ProvidersProps): ReactNode {
|
|||||||
</IframeBridge>
|
</IframeBridge>
|
||||||
</ErrorProvider>
|
</ErrorProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</ThemeProvider>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
WalletCards,
|
WalletCards,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
@@ -65,11 +67,17 @@ function SubmittingPanel() {
|
|||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="max-w-[340px] overflow-hidden rounded-2xl border border-white/70 bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
|
className="max-w-[340px] overflow-hidden rounded-2xl border border-white/70 bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
|
||||||
>
|
>
|
||||||
<div className="px-8 py-10 text-center">
|
<div className="px-8 py-8 text-center">
|
||||||
<div className="mx-auto flex size-20 rotate-[-8deg] items-center justify-center rounded-2xl bg-[#e5002c] text-4xl font-black italic text-white shadow-[0_16px_28px_rgba(229,0,44,0.28)]">
|
<Image
|
||||||
N
|
src="/entry/image6.png"
|
||||||
</div>
|
alt=""
|
||||||
<div className="mx-auto mt-8 h-2 w-44 overflow-hidden rounded-full bg-[#dbe3f1]">
|
width={150}
|
||||||
|
height={119}
|
||||||
|
className="mx-auto h-[118px] w-[150px] object-contain"
|
||||||
|
priority
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="mx-auto mt-2 h-2 w-44 overflow-hidden rounded-full bg-[#dbe3f1]">
|
||||||
<div className="h-full w-2/3 rounded-full bg-[#0755c7] shadow-[0_0_14px_rgba(7,85,199,0.45)]" />
|
<div className="h-full w-2/3 rounded-full bg-[#0755c7] shadow-[0_0_14px_rgba(7,85,199,0.45)]" />
|
||||||
</div>
|
</div>
|
||||||
<DialogHeader className="mt-8 items-center gap-2">
|
<DialogHeader className="mt-8 items-center gap-2">
|
||||||
@@ -105,6 +113,12 @@ export function HallBetPreviewDialog({
|
|||||||
const summary = data?.summary;
|
const summary = data?.summary;
|
||||||
const lines = data?.lines ?? [];
|
const lines = data?.lines ?? [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && !placing && !data) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
}, [data, onOpenChange, open, placing]);
|
||||||
|
|
||||||
if (placing) {
|
if (placing) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={() => {}}>
|
<Dialog open={open} onOpenChange={() => {}}>
|
||||||
@@ -113,13 +127,17 @@ export function HallBetPreviewDialog({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="max-h-[min(92vh,760px)] gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_18px_60px_rgba(15,23,42,0.18)] ring-[#e8eef7] sm:max-w-lg"
|
className="flex max-h-[calc(100dvh-24px)] flex-col gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_18px_60px_rgba(15,23,42,0.18)] ring-[#e8eef7] sm:max-h-[min(92vh,760px)] sm:max-w-lg"
|
||||||
>
|
>
|
||||||
<div className="relative px-4 pb-3 pt-5 sm:px-5">
|
<div className="relative shrink-0 px-4 pb-3 pt-5 sm:px-5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
@@ -154,8 +172,7 @@ export function HallBetPreviewDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="overflow-y-auto border-y border-[#e8eef7] px-4 sm:px-5"
|
className="min-h-0 flex-1 overflow-y-auto border-y border-[#e8eef7] px-4 sm:px-5"
|
||||||
style={{ maxHeight: "min(58vh, 470px)" }}
|
|
||||||
>
|
>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{!data ? (
|
{!data ? (
|
||||||
@@ -274,7 +291,7 @@ export function HallBetPreviewDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 border-t border-[#e8eef7] bg-white p-4 sm:p-5">
|
<div className="grid shrink-0 grid-cols-2 gap-3 border-t border-[#e8eef7] bg-white p-4 pb-[calc(1rem+env(safe-area-inset-bottom))] sm:p-5">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { CheckCircle2, ClipboardList, Ticket } from "lucide-react";
|
import { CheckCircle2, ClipboardList, Ticket, XIcon } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -38,8 +38,19 @@ export function HallBetResultDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-h-[min(92vh,760px)] gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_18px_60px_rgba(15,23,42,0.18)] ring-[#e8eef7] sm:max-w-lg">
|
<DialogContent
|
||||||
<div className="px-4 pb-3 pt-7 text-center sm:px-5">
|
showCloseButton={false}
|
||||||
|
className="flex max-h-[calc(100dvh-24px)] flex-col gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_18px_60px_rgba(15,23,42,0.18)] ring-[#e8eef7] sm:max-h-[min(92vh,760px)] sm:max-w-lg"
|
||||||
|
>
|
||||||
|
<div className="relative shrink-0 px-4 pb-3 pt-7 text-center sm:px-5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="absolute right-3 top-3 inline-flex size-9 items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||||
|
aria-label={t("actions.close", { defaultValue: "关闭" })}
|
||||||
|
>
|
||||||
|
<XIcon className="size-5" />
|
||||||
|
</button>
|
||||||
<div className="mx-auto flex size-16 items-center justify-center rounded-full border-4 border-emerald-100 bg-white text-emerald-600 shadow-[0_10px_24px_rgba(22,163,74,0.12)]">
|
<div className="mx-auto flex size-16 items-center justify-center rounded-full border-4 border-emerald-100 bg-white text-emerald-600 shadow-[0_10px_24px_rgba(22,163,74,0.12)]">
|
||||||
<CheckCircle2 className="size-11" strokeWidth={2.5} />
|
<CheckCircle2 className="size-11" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
@@ -57,8 +68,7 @@ export function HallBetResultDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="overflow-y-auto border-y border-[#e8eef7] px-4 sm:px-5"
|
className="min-h-0 flex-1 overflow-y-auto border-y border-[#e8eef7] px-4 sm:px-5"
|
||||||
style={{ maxHeight: "min(58vh, 470px)" }}
|
|
||||||
>
|
>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{!data ? (
|
{!data ? (
|
||||||
@@ -190,7 +200,7 @@ export function HallBetResultDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 border-t border-[#e8eef7] bg-white p-4 sm:p-5">
|
<div className="grid shrink-0 grid-cols-2 gap-3 border-t border-[#e8eef7] bg-white p-4 pb-[calc(1rem+env(safe-area-inset-bottom))] sm:p-5">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Wallet } from "lucide-react";
|
import { Wallet } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -89,9 +90,15 @@ export function HallWalletStrip() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative overflow-hidden rounded-xl bg-[#e5002c] px-4 py-4 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
|
"relative overflow-hidden rounded-xl bg-[#e5002c] px-4 py-4 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]",
|
||||||
"before:absolute before:inset-y-0 before:right-0 before:w-44 before:bg-[radial-gradient(circle_at_70%_70%,rgba(255,255,255,0.22),transparent_38%),linear-gradient(135deg,transparent,rgba(255,255,255,0.13))] before:content-['']",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Image
|
||||||
|
src="/entry/image5.png"
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
className="pointer-events-none object-cover object-center"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
<div className="relative flex items-center gap-3">
|
<div className="relative flex items-center gap-3">
|
||||||
<div className="flex size-13 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
|
<div className="flex size-13 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
|
||||||
<Wallet className="size-7" aria-hidden />
|
<Wallet className="size-7" aria-hidden />
|
||||||
|
|||||||
@@ -231,16 +231,15 @@ export function EntryGate() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-dvh flex-col bg-white">
|
<div className="relative flex min-h-dvh flex-col bg-white">
|
||||||
<div className="relative h-[45vh] min-h-[320px] bg-red-600">
|
<div className={cn("relative h-[45vh] min-h-[320px]", phase === "success" ? "bg-white" : "bg-red-600")}>
|
||||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src="/entry/image1.png"
|
src={phase === "success" ? "/entry/image4.png" : "/entry/image1.png"}
|
||||||
alt={t("header.backgroundAlt")}
|
alt={t("header.backgroundAlt")}
|
||||||
fill
|
fill
|
||||||
className="object-cover object-center"
|
className="object-cover object-center"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-red-600/20 to-red-600/80" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute left-0 right-0 top-0 z-20 flex items-center px-4 py-3">
|
<div className="absolute left-0 right-0 top-0 z-20 flex items-center px-4 py-3">
|
||||||
@@ -438,9 +437,18 @@ export function EntryGate() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 py-4 text-xs text-gray-500">
|
<div className="relative min-h-[150px] overflow-hidden px-4 pb-8 pt-16">
|
||||||
<ShieldCheck className="size-4 text-red-500" aria-hidden />
|
<Image
|
||||||
<span>{t("footer.secure")}</span>
|
src="/entry/image2.png"
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
className="pointer-events-none object-cover object-bottom"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="relative z-10 flex items-center justify-center gap-2 text-xs font-medium text-gray-600">
|
||||||
|
<ShieldCheck className="size-4 text-red-500" aria-hidden />
|
||||||
|
<span>{t("footer.secure")}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
302
src/features/results/check-winning-screen.tsx
Normal file
302
src/features/results/check-winning-screen.tsx
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { BriefcaseBusiness, CheckCircle2, Clock3, RefreshCw, XIcon } from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { getDrawResults } from "@/api/draw";
|
||||||
|
import { getTicketDrawMyMatch, getTicketItems } from "@/api/ticket-items";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||||
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
|
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||||
|
import { playLabel } from "@/lib/play-labels";
|
||||||
|
import type { DrawResultListItem } from "@/types/api/draw-results";
|
||||||
|
import type { TicketDrawMyMatchPayload, TicketItemListRow } from "@/types/api/ticket-items";
|
||||||
|
|
||||||
|
type WinningCheckResult = {
|
||||||
|
draw: DrawResultListItem;
|
||||||
|
match: TicketDrawMyMatchPayload;
|
||||||
|
tickets: TicketItemListRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CheckWinningScreen() {
|
||||||
|
const { t } = useTranslation("player");
|
||||||
|
const [ticketNo, setTicketNo] = useState("");
|
||||||
|
const [latestDraw, setLatestDraw] = useState<DrawResultListItem | null>(null);
|
||||||
|
const [recent, setRecent] = useState<string[]>([]);
|
||||||
|
const [result, setResult] = useState<WinningCheckResult | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const res = await getDrawResults({ page: 1, size: 1 });
|
||||||
|
setLatestDraw(res.items[0] ?? null);
|
||||||
|
} catch {
|
||||||
|
setLatestDraw(null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const normalizedTicketNo = ticketNo.trim();
|
||||||
|
|
||||||
|
const runCheck = useCallback(async () => {
|
||||||
|
if (!latestDraw || normalizedTicketNo === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const [match, tickets] = await Promise.all([
|
||||||
|
getTicketDrawMyMatch(latestDraw.draw_no),
|
||||||
|
getTicketItems({
|
||||||
|
draw_no: latestDraw.draw_no,
|
||||||
|
number: normalizedTicketNo,
|
||||||
|
per_page: 10,
|
||||||
|
page: 1,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const next = {
|
||||||
|
draw: latestDraw,
|
||||||
|
match,
|
||||||
|
tickets: tickets.items,
|
||||||
|
};
|
||||||
|
setResult(next);
|
||||||
|
setRecent((current) => [normalizedTicketNo, ...current.filter((x) => x !== normalizedTicketNo)].slice(0, 5));
|
||||||
|
} catch {
|
||||||
|
setError(t("results.check.loadFailed", { defaultValue: "查询失败,请稍后重试。" }));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [latestDraw, normalizedTicketNo, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlayerPanel title={t("results.check.title", { defaultValue: "查我的中奖" })} backHref="/results" backLabel={t("results.title")}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<section className="overflow-hidden rounded-2xl border border-red-100 bg-white shadow-[0_12px_32px_rgba(15,23,42,0.06)]">
|
||||||
|
<div className="bg-gradient-to-b from-red-50 to-white px-5 pb-5 pt-8 text-center">
|
||||||
|
<div className="mx-auto flex size-24 items-center justify-center rounded-full bg-white text-[#e5002c] shadow-[0_18px_40px_rgba(229,0,44,0.14)]">
|
||||||
|
<BriefcaseBusiness className="size-12" />
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-5 text-lg font-black text-slate-950">
|
||||||
|
{t("results.check.enterTicket", { defaultValue: "输入你的票号或号码" })}
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mt-2 max-w-xs text-sm leading-relaxed text-slate-500">
|
||||||
|
{t("results.check.description", { defaultValue: "系统会按最新已发布期号查询你的注单和中奖情况。" })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 px-4 pb-4">
|
||||||
|
<label className="block space-y-1.5">
|
||||||
|
<span className="text-xs font-black text-slate-700">
|
||||||
|
{t("results.check.ticketNumber", { defaultValue: "票号 / 号码" })}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
value={ticketNo}
|
||||||
|
placeholder={t("results.check.placeholder", { defaultValue: "请输入票号或号码" })}
|
||||||
|
onChange={(e) => setTicketNo(e.target.value)}
|
||||||
|
className="h-12 rounded-xl border-[#dce7f7] bg-white font-mono text-base font-bold"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{latestDraw ? (
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
{t("results.check.latestDraw", { drawNo: latestDraw.draw_no, defaultValue: "最新期号 {{drawNo}}" })}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{error ? <p className="text-sm font-semibold text-[#e5002c]">{error}</p> : null}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!latestDraw || normalizedTicketNo === "" || loading}
|
||||||
|
onClick={() => void runCheck()}
|
||||||
|
className="h-12 w-full rounded-xl bg-[#e5002c] text-base font-black text-white hover:bg-[#d10028]"
|
||||||
|
>
|
||||||
|
{loading ? t("actions.loading", { defaultValue: "查询中..." }) : t("results.check.submit", { defaultValue: "立即查询" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-2xl border border-[#dfe8f6] bg-white p-4 shadow-[0_10px_26px_rgba(15,23,42,0.05)]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-black text-slate-950">
|
||||||
|
{t("results.check.recent", { defaultValue: "最近查询" })}
|
||||||
|
</h3>
|
||||||
|
{recent.length > 0 ? (
|
||||||
|
<button type="button" className="text-sm font-bold text-[#0b56b7]" onClick={() => setRecent([])}>
|
||||||
|
{t("actions.clear", { defaultValue: "清空" })}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 divide-y divide-[#edf2f9]">
|
||||||
|
{recent.length === 0 ? (
|
||||||
|
<p className="py-4 text-sm text-slate-500">
|
||||||
|
{t("results.check.noRecent", { defaultValue: "暂无查询记录。" })}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
recent.map((row) => (
|
||||||
|
<button
|
||||||
|
key={row}
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center justify-between py-3 text-left"
|
||||||
|
onClick={() => setTicketNo(row)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2 font-mono text-sm font-black text-slate-800">
|
||||||
|
<Clock3 className="size-4 text-slate-400" />
|
||||||
|
{row}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
{latestDraw?.business_date ?? "—"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WinningResultDialog
|
||||||
|
open={result !== null}
|
||||||
|
data={result}
|
||||||
|
query={normalizedTicketNo}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setResult(null);
|
||||||
|
}}
|
||||||
|
onCheckAnother={() => {
|
||||||
|
setResult(null);
|
||||||
|
setTicketNo("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PlayerPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WinningResultDialog({
|
||||||
|
open,
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
onOpenChange,
|
||||||
|
onCheckAnother,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
data: WinningCheckResult | null;
|
||||||
|
query: string;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onCheckAnother: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation("player");
|
||||||
|
const totalWin = (data?.match.total_win_minor ?? 0) + (data?.match.total_jackpot_win_minor ?? 0);
|
||||||
|
const isWon = totalWin > 0 || (data?.match.winning_ticket_count ?? 0) > 0;
|
||||||
|
const firstTicket = useMemo(() => data?.tickets[0] ?? null, [data]);
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
className="max-h-[calc(100dvh-24px)] overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.28)] sm:max-w-md"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="absolute right-3 top-3 z-10 inline-flex size-9 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100"
|
||||||
|
aria-label={t("actions.close", { defaultValue: "关闭" })}
|
||||||
|
>
|
||||||
|
<XIcon className="size-5" />
|
||||||
|
</button>
|
||||||
|
<div className="max-h-[calc(100dvh-24px)] overflow-y-auto px-5 pb-5 pt-8">
|
||||||
|
<DialogHeader className="items-center text-center">
|
||||||
|
<div className="flex size-16 items-center justify-center rounded-full border-4 border-emerald-100 bg-white text-emerald-600">
|
||||||
|
<CheckCircle2 className="size-11" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="mt-3 text-xl font-black text-slate-950">
|
||||||
|
{isWon
|
||||||
|
? t("results.check.winTitle", { defaultValue: "恭喜,你中奖了" })
|
||||||
|
: t("results.check.noWinTitle", { defaultValue: "未查询到中奖" })}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-sm text-slate-500">
|
||||||
|
{t("results.check.ticketNumber", { defaultValue: "票号 / 号码" })}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-3 w-fit rounded-xl bg-emerald-50 px-8 py-2 font-mono text-lg font-black text-[#0a8f3e]">
|
||||||
|
{query}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 grid grid-cols-2 overflow-hidden rounded-xl border border-emerald-100 bg-emerald-50 text-center">
|
||||||
|
<div className="border-r border-emerald-100 px-3 py-4">
|
||||||
|
<p className="text-xs font-medium text-slate-500">
|
||||||
|
{t("results.check.match", { defaultValue: "匹配" })}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-lg font-black text-[#0a8f3e]">
|
||||||
|
{firstTicket ? playLabel(firstTicket.play_code, t) : isWon ? t("orders.hit", { defaultValue: "命中" }) : "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-4">
|
||||||
|
<p className="text-xs font-medium text-slate-500">
|
||||||
|
{t("results.check.amount", { defaultValue: "中奖金额" })}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 font-mono text-lg font-black text-[#0a8f3e]">
|
||||||
|
{formatMinorAsCurrency(totalWin, firstTicket?.currency_code ?? "NPR")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 rounded-xl border border-[#e8eef7] bg-white p-4 text-sm">
|
||||||
|
<p className="font-black text-slate-950">
|
||||||
|
{t("results.check.drawInfo", { defaultValue: "开奖信息" })}
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-4 text-slate-500">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs">{t("results.check.issueNo", { defaultValue: "期号" })}</p>
|
||||||
|
<p className="mt-1 font-mono font-black text-slate-900">{data.draw.draw_no}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs">{t("results.businessDate")}</p>
|
||||||
|
<p className="mt-1 font-semibold text-slate-900">{data.draw.business_date}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<p className="text-xs">{t("results.drawTime", { time: "" }).replace(":", "").trim()}</p>
|
||||||
|
<p className="mt-1 font-semibold text-slate-900">
|
||||||
|
{formatLotteryInstant(data.draw.draw_time_iso ?? data.draw.draw_time ?? null)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 grid gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="h-12 rounded-xl bg-[#07459f] text-base font-black text-white hover:bg-[#063b88]"
|
||||||
|
render={<Link href={`/orders?draw_no=${encodeURIComponent(data.draw.draw_no)}&number=${encodeURIComponent(query)}`} />}
|
||||||
|
>
|
||||||
|
{t("results.check.viewBetDetails", { defaultValue: "查看注单详情" })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-12 rounded-xl border-[#ff3650] text-base font-black text-[#e5002c] hover:bg-[#fff5f6]"
|
||||||
|
onClick={onCheckAnother}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-5" />
|
||||||
|
{t("results.check.checkAnother", { defaultValue: "查询另一张票" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -271,7 +271,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
{t("results.hitHint")}
|
{t("results.hitHint")}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href={`/orders?draw_no=${encodeURIComponent(data.draw_no)}&status=settled_win`}
|
href="/results/check"
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "default", size: "sm" }),
|
buttonVariants({ variant: "default", size: "sm" }),
|
||||||
"mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028] sm:w-auto",
|
"mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028] sm:w-auto",
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export function DrawResultsListScreen() {
|
|||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<TwentyThreeResultsGrid numbers={featured.results} />
|
<TwentyThreeResultsGrid numbers={featured.results} />
|
||||||
<Link
|
<Link
|
||||||
href={`/orders?draw_no=${encodeURIComponent(featured.draw_no)}&status=settled_win`}
|
href="/results/check"
|
||||||
className="mt-4 inline-flex h-10 w-full items-center justify-center rounded-xl bg-[#e5002c] px-4 text-sm font-bold text-white transition-colors hover:bg-[#d10028]"
|
className="mt-4 inline-flex h-10 w-full items-center justify-center rounded-xl bg-[#e5002c] px-4 text-sm font-bold text-white transition-colors hover:bg-[#d10028]"
|
||||||
>
|
>
|
||||||
{t("results.viewMyWinning")}
|
{t("results.viewMyWinning")}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { DrawResultsNumbers } from "@/types/api/draw-results";
|
import type { DrawResultsNumbers } from "@/types/api/draw-results";
|
||||||
|
import { Trophy } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { norm4d } from "@/lib/norm-4d";
|
import { norm4d } from "@/lib/norm-4d";
|
||||||
@@ -22,65 +23,105 @@ export function TwentyThreeResultsGrid({
|
|||||||
const consos = numbers.consolation ?? [];
|
const consos = numbers.consolation ?? [];
|
||||||
const hits = highlighted4d ?? null;
|
const hits = highlighted4d ?? null;
|
||||||
|
|
||||||
const cellBase =
|
const isHit = (raw: string): boolean => {
|
||||||
"flex min-h-[2.75rem] items-center justify-center rounded-md border font-mono text-base font-semibold tracking-wide tabular-nums";
|
|
||||||
|
|
||||||
const cellTone = (raw: string) => {
|
|
||||||
const v = (raw || "").trim();
|
const v = (raw || "").trim();
|
||||||
const isHit =
|
return (
|
||||||
hits !== null &&
|
hits !== null &&
|
||||||
hits.size > 0 &&
|
hits.size > 0 &&
|
||||||
v !== "" &&
|
v !== "" &&
|
||||||
v !== "—" &&
|
v !== "—" &&
|
||||||
hits.has(norm4d(v));
|
hits.has(norm4d(v))
|
||||||
return cn(
|
|
||||||
cellBase,
|
|
||||||
isHit
|
|
||||||
? "border-amber-500/80 bg-gradient-to-br from-amber-300 via-amber-400 to-amber-500 text-amber-950 shadow-[inset_0_1px_0_rgba(255,255,255,0.35)]"
|
|
||||||
: "border-border bg-card",
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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)]",
|
||||||
|
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)]",
|
||||||
|
);
|
||||||
|
|
||||||
|
const prizeCards = [
|
||||||
|
{
|
||||||
|
key: "1st" as const,
|
||||||
|
label: t("results.grid.first"),
|
||||||
|
value: numbers["1st"] || "—",
|
||||||
|
tone: "red",
|
||||||
|
border: "border-[#ffb8c3]",
|
||||||
|
text: "text-[#e5002c]",
|
||||||
|
wash: "from-[#fff4f6] to-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2nd" as const,
|
||||||
|
label: t("results.grid.second"),
|
||||||
|
value: numbers["2nd"] || "—",
|
||||||
|
tone: "blue",
|
||||||
|
border: "border-[#b9ccf6]",
|
||||||
|
text: "text-[#0b56b7]",
|
||||||
|
wash: "from-[#f3f7ff] to-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "3rd" as const,
|
||||||
|
label: t("results.grid.third"),
|
||||||
|
value: numbers["3rd"] || "—",
|
||||||
|
tone: "green",
|
||||||
|
border: "border-[#bde7cc]",
|
||||||
|
text: "text-[#0a8f3e]",
|
||||||
|
wash: "from-[#f0fff5] to-white",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{(["1st", "2nd", "3rd"] as const).map((key) => (
|
{prizeCards.map((card) => (
|
||||||
<div key={key} className="flex flex-col gap-1.5 text-center">
|
<div
|
||||||
<span className="text-xs font-medium uppercase text-muted-foreground">
|
key={card.key}
|
||||||
{key === "1st"
|
className={cn(
|
||||||
? t("results.grid.first")
|
"relative overflow-hidden rounded-xl border bg-gradient-to-b px-2 py-4 text-center shadow-[0_10px_24px_rgba(15,23,42,0.06)]",
|
||||||
: key === "2nd"
|
card.border,
|
||||||
? t("results.grid.second")
|
card.wash,
|
||||||
: t("results.grid.third")}
|
isHit(card.value) && "ring-2 ring-amber-300",
|
||||||
</span>
|
)}
|
||||||
<div className={cellTone(numbers[key] || "")}>
|
>
|
||||||
{numbers[key] || "—"}
|
<div className={cn("mx-auto flex size-9 items-center justify-center rounded-full text-white", card.tone === "red" ? "bg-[#e5002c]" : card.tone === "blue" ? "bg-[#0b56b7]" : "bg-[#0a8f3e]")}>
|
||||||
|
<Trophy className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
|
<p className={cn("mt-3 text-xs font-black", card.text)}>{card.label}</p>
|
||||||
|
<p className={cn("mt-2 font-mono text-3xl font-black tabular-nums", card.text)}>{card.value}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="rounded-xl border border-red-100 bg-white p-3 shadow-[0_8px_22px_rgba(15,23,42,0.05)]">
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="mb-3 flex items-center gap-2 text-sm font-black text-[#e5002c]">
|
||||||
{t("results.grid.starter")} (Starter)
|
<Trophy className="size-4" />
|
||||||
|
{t("results.grid.starter")}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<div className="grid grid-cols-5 gap-1.5">
|
||||||
{Array.from({ length: 10 }).map((_, i) => (
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
<div key={`s-${i}`} className={cellTone(starters[i] ?? "—")}>
|
<div key={`s-${i}`} className={smallCellTone(starters[i] ?? "—", "red")}>
|
||||||
{starters[i] ?? "—"}
|
<span className="text-[11px] font-black">{i + 1}</span>
|
||||||
|
<span className="self-center font-mono text-xs font-semibold tabular-nums text-slate-700">
|
||||||
|
{starters[i] ?? "—"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="rounded-xl border border-blue-100 bg-white p-3 shadow-[0_8px_22px_rgba(15,23,42,0.05)]">
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="mb-3 flex items-center gap-2 text-sm font-black text-[#0b56b7]">
|
||||||
{t("results.grid.consolation")} (Consolation)
|
<Trophy className="size-4" />
|
||||||
|
{t("results.grid.consolation")}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<div className="grid grid-cols-5 gap-1.5">
|
||||||
{Array.from({ length: 10 }).map((_, i) => (
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
<div key={`c-${i}`} className={cellTone(consos[i] ?? "—")}>
|
<div key={`c-${i}`} className={smallCellTone(consos[i] ?? "—", "blue")}>
|
||||||
{consos[i] ?? "—"}
|
<span className="text-[11px] font-black">{i + 1}</span>
|
||||||
|
<span className="self-center font-mono text-xs font-semibold tabular-nums text-slate-700">
|
||||||
|
{consos[i] ?? "—"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Wallet } from "lucide-react";
|
import { Wallet } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -157,6 +158,13 @@ export function WalletScreen() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section className="relative overflow-hidden rounded-xl bg-[#e5002c] px-4 py-5 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]">
|
<section className="relative overflow-hidden rounded-xl bg-[#e5002c] px-4 py-5 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]">
|
||||||
|
<Image
|
||||||
|
src="/entry/image5.png"
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
className="pointer-events-none object-cover object-center"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
<div className="relative flex items-center gap-3">
|
<div className="relative flex items-center gap-3">
|
||||||
<div className="flex size-13 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
|
<div className="flex size-13 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
|
||||||
<Wallet className="size-7" aria-hidden />
|
<Wallet className="size-7" aria-hidden />
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ import zhCommon from "./locales/zh/common.json";
|
|||||||
import zhEntry from "./locales/zh/entry.json";
|
import zhEntry from "./locales/zh/entry.json";
|
||||||
import zhLayout from "./locales/zh/layout.json";
|
import zhLayout from "./locales/zh/layout.json";
|
||||||
import zhPlayer from "./locales/zh/player.json";
|
import zhPlayer from "./locales/zh/player.json";
|
||||||
|
import {
|
||||||
/** 对齐后端与产品:尼泊尔语 / 英语 / 中文(简体) */
|
DEFAULT_LANGUAGE,
|
||||||
export const SUPPORTED_LANGUAGES = [
|
normalizeLanguage,
|
||||||
{ code: "en" as const, flag: "🇺🇸" },
|
type AppLanguage,
|
||||||
{ code: "ne" as const, flag: "🇳🇵" },
|
} from "@/i18n/language";
|
||||||
{ code: "zh" as const, flag: "🇨🇳" },
|
export {
|
||||||
];
|
DEFAULT_LANGUAGE,
|
||||||
|
normalizeLanguage,
|
||||||
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]["code"];
|
SUPPORTED_LANGUAGES,
|
||||||
|
type AppLanguage,
|
||||||
export const DEFAULT_LANGUAGE: AppLanguage = "en";
|
} from "@/i18n/language";
|
||||||
|
|
||||||
const namespaces = ["common", "entry", "layout", "player"] as const;
|
const namespaces = ["common", "entry", "layout", "player"] as const;
|
||||||
|
|
||||||
@@ -54,13 +54,6 @@ const resources = {
|
|||||||
Record<(typeof namespaces)[number], Record<string, unknown>>
|
Record<(typeof namespaces)[number], Record<string, unknown>>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function normalizeLanguage(lang: string | undefined): AppLanguage {
|
|
||||||
const base = lang?.split("-")[0]?.toLowerCase();
|
|
||||||
if (base === "ne") return "ne";
|
|
||||||
if (base === "zh") return "zh";
|
|
||||||
return "en";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function syncDocumentLanguage(lang: AppLanguage): void {
|
export function syncDocumentLanguage(lang: AppLanguage): void {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
|
|
||||||
|
|||||||
17
src/i18n/language.ts
Normal file
17
src/i18n/language.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/** 对齐后端与产品:尼泊尔语 / 英语 / 中文(简体) */
|
||||||
|
export const SUPPORTED_LANGUAGES = [
|
||||||
|
{ code: "en" as const, flag: "🇺🇸" },
|
||||||
|
{ code: "ne" as const, flag: "🇳🇵" },
|
||||||
|
{ code: "zh" as const, flag: "🇨🇳" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]["code"];
|
||||||
|
|
||||||
|
export const DEFAULT_LANGUAGE: AppLanguage = "zh";
|
||||||
|
|
||||||
|
export function normalizeLanguage(lang: string | undefined): AppLanguage {
|
||||||
|
const base = lang?.split("-")[0]?.toLowerCase();
|
||||||
|
if (base === "ne") return "ne";
|
||||||
|
if (base === "zh") return "zh";
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user