feat: 优化大厅下注表格与开奖结果筛选加载体验
- 精简大厅下注表格布局,缩小列宽与输入框,优化移动端可读性 - 调整默认草稿行与行激活逻辑,简化草稿合计展示 - 新增开奖结果日期选择器、清除日期与加载更多功能 - 支持开奖结果分页滚动加载与无更多数据提示 - 新增 react-day-picker 与 date-fns 依赖 - 补充下注表格相关多语言文案
This commit is contained in:
38
package-lock.json
generated
38
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"axios": "^1.16.0",
|
"axios": "^1.16.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"i18next": "^26.1.0",
|
"i18next": "^26.1.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"i18next-http-backend": "^4.0.0",
|
"i18next-http-backend": "^4.0.0",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pusher-js": "^8.5.0",
|
"pusher-js": "^8.5.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
|
"react-day-picker": "^10.0.0",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-i18next": "^17.0.7",
|
"react-i18next": "^17.0.7",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -556,6 +558,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@date-fns/tz": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@date-fns/tz/-/tz-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@dotenvx/dotenvx": {
|
"node_modules/@dotenvx/dotenvx": {
|
||||||
"version": "1.65.0",
|
"version": "1.65.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@dotenvx/dotenvx/-/dotenvx-1.65.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@dotenvx/dotenvx/-/dotenvx-1.65.0.tgz",
|
||||||
@@ -4209,6 +4217,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -8532,6 +8550,26 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-day-picker": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/react-day-picker/-/react-day-picker-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrEXo5wFPsq5LTcayelM3BPueD00v7zbdipAY+EIdPcseVykYwkOWx4Ujn/EtbBvpnp8ZPUHol17HXH6kVbZoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@date-fns/tz": "^1.4.1",
|
||||||
|
"date-fns": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/gpbl"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"axios": "^1.16.0",
|
"axios": "^1.16.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"i18next": "^26.1.0",
|
"i18next": "^26.1.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"i18next-http-backend": "^4.0.0",
|
"i18next-http-backend": "^4.0.0",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pusher-js": "^8.5.0",
|
"pusher-js": "^8.5.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
|
"react-day-picker": "^10.0.0",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-i18next": "^17.0.7",
|
"react-i18next": "^17.0.7",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
|||||||
221
src/components/ui/calendar.tsx
Normal file
221
src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
DayPicker,
|
||||||
|
getDefaultClassNames,
|
||||||
|
type DayButton,
|
||||||
|
type Locale,
|
||||||
|
} from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
locale,
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn(
|
||||||
|
"group/calendar bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
|
||||||
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
captionLayout={captionLayout}
|
||||||
|
locale={locale}
|
||||||
|
formatters={{
|
||||||
|
formatMonthDropdown: (date) =>
|
||||||
|
date.toLocaleString(locale?.code, { month: "short" }),
|
||||||
|
...formatters,
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
|
months: cn(
|
||||||
|
"relative flex flex-col gap-4 md:flex-row",
|
||||||
|
defaultClassNames.months
|
||||||
|
),
|
||||||
|
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
||||||
|
nav: cn(
|
||||||
|
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
||||||
|
defaultClassNames.nav
|
||||||
|
),
|
||||||
|
button_previous: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||||
|
defaultClassNames.button_previous
|
||||||
|
),
|
||||||
|
button_next: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||||
|
defaultClassNames.button_next
|
||||||
|
),
|
||||||
|
month_caption: cn(
|
||||||
|
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
|
||||||
|
defaultClassNames.month_caption
|
||||||
|
),
|
||||||
|
dropdowns: cn(
|
||||||
|
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||||
|
defaultClassNames.dropdowns
|
||||||
|
),
|
||||||
|
dropdown_root: cn(
|
||||||
|
"relative rounded-(--cell-radius)",
|
||||||
|
defaultClassNames.dropdown_root
|
||||||
|
),
|
||||||
|
dropdown: cn(
|
||||||
|
"absolute inset-0 bg-popover opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
|
caption_label: cn(
|
||||||
|
"font-medium select-none",
|
||||||
|
captionLayout === "label"
|
||||||
|
? "text-sm"
|
||||||
|
: "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
||||||
|
defaultClassNames.caption_label
|
||||||
|
),
|
||||||
|
table: "w-full border-collapse",
|
||||||
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
|
weekday: cn(
|
||||||
|
"flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none",
|
||||||
|
defaultClassNames.weekday
|
||||||
|
),
|
||||||
|
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
||||||
|
week_number_header: cn(
|
||||||
|
"w-(--cell-size) select-none",
|
||||||
|
defaultClassNames.week_number_header
|
||||||
|
),
|
||||||
|
week_number: cn(
|
||||||
|
"text-[0.8rem] text-muted-foreground select-none",
|
||||||
|
defaultClassNames.week_number
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
"group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
|
||||||
|
props.showWeekNumber
|
||||||
|
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
|
||||||
|
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
|
||||||
|
defaultClassNames.day
|
||||||
|
),
|
||||||
|
range_start: cn(
|
||||||
|
"relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
|
||||||
|
defaultClassNames.range_start
|
||||||
|
),
|
||||||
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
|
range_end: cn(
|
||||||
|
"relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted",
|
||||||
|
defaultClassNames.range_end
|
||||||
|
),
|
||||||
|
today: cn(
|
||||||
|
"rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none",
|
||||||
|
defaultClassNames.today
|
||||||
|
),
|
||||||
|
outside: cn(
|
||||||
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
|
defaultClassNames.outside
|
||||||
|
),
|
||||||
|
disabled: cn(
|
||||||
|
"text-muted-foreground opacity-50",
|
||||||
|
defaultClassNames.disabled
|
||||||
|
),
|
||||||
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="calendar"
|
||||||
|
ref={rootRef}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
|
if (orientation === "left") {
|
||||||
|
return (
|
||||||
|
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === "right") {
|
||||||
|
return (
|
||||||
|
<ChevronRightIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
DayButton: ({ ...props }) => (
|
||||||
|
<CalendarDayButton locale={locale} {...props} />
|
||||||
|
),
|
||||||
|
WeekNumber: ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<td {...props}>
|
||||||
|
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...components,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarDayButton({
|
||||||
|
className,
|
||||||
|
day,
|
||||||
|
modifiers,
|
||||||
|
locale,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (modifiers.focused) ref.current?.focus()
|
||||||
|
}, [modifiers.focused])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
data-day={day.date.toLocaleDateString(locale?.code)}
|
||||||
|
data-selected-single={
|
||||||
|
modifiers.selected &&
|
||||||
|
!modifiers.range_start &&
|
||||||
|
!modifiers.range_end &&
|
||||||
|
!modifiers.range_middle
|
||||||
|
}
|
||||||
|
data-range-start={modifiers.range_start}
|
||||||
|
data-range-end={modifiers.range_end}
|
||||||
|
data-range-middle={modifiers.range_middle}
|
||||||
|
className={cn(
|
||||||
|
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
|
||||||
|
defaultClassNames.day,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar, CalendarDayButton }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronRight, CirclePlus, Ticket, Trash2, Star } from "lucide-react";
|
import { CirclePlus, Ticket, Trash2, Star } from "lucide-react";
|
||||||
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";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -138,13 +138,6 @@ function parseRebateRate(rate: string | undefined): number {
|
|||||||
return n > 1 ? n / 100 : n;
|
return n > 1 ? n / 100 : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
function amountToDisplay(minor: number): string {
|
|
||||||
return (minor / 100).toLocaleString(undefined, {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeNumberForPlay(number: string, playCode: string): string {
|
function normalizeNumberForPlay(number: string, playCode: string): string {
|
||||||
if (playCode.startsWith("pos_2")) return number.slice(-2);
|
if (playCode.startsWith("pos_2")) return number.slice(-2);
|
||||||
if (playCode.startsWith("pos_3")) return number.slice(-3);
|
if (playCode.startsWith("pos_3")) return number.slice(-3);
|
||||||
@@ -328,19 +321,6 @@ function cellRiskState(
|
|||||||
return "open";
|
return "open";
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableCellClass(status: CellRiskState, disabled: boolean): string {
|
|
||||||
if (disabled) {
|
|
||||||
return "opacity-50";
|
|
||||||
}
|
|
||||||
if (status === "sold_out") {
|
|
||||||
return "bg-slate-100 text-slate-400 line-through";
|
|
||||||
}
|
|
||||||
if (status === "warning") {
|
|
||||||
return "bg-amber-50 text-amber-700";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function quickFillKeys(category: HallCategory): { favorites: string; history: string } {
|
function quickFillKeys(category: HallCategory): { favorites: string; history: string } {
|
||||||
return {
|
return {
|
||||||
favorites: `lottery.hall.quickfill.favorites.${category}`,
|
favorites: `lottery.hall.quickfill.favorites.${category}`,
|
||||||
@@ -353,12 +333,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
|
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
|
||||||
const [rows, setRows] = useState<DraftRow[]>(() => [
|
const [rows, setRows] = useState<DraftRow[]>(() => [newDraftRow()]);
|
||||||
{ ...newDraftRow(), number: "23", amounts: {} },
|
|
||||||
{ ...newDraftRow(), number: "75", amounts: {} },
|
|
||||||
{ ...newDraftRow(), number: "08", amounts: {} },
|
|
||||||
{ ...newDraftRow(), number: "46", amounts: {} },
|
|
||||||
]);
|
|
||||||
const [activeRowId, setActiveRowId] = useState<string | null>(null);
|
const [activeRowId, setActiveRowId] = useState<string | null>(null);
|
||||||
const [catalogState, setCatalogState] = useState<
|
const [catalogState, setCatalogState] = useState<
|
||||||
| { kind: "loading" }
|
| { kind: "loading" }
|
||||||
@@ -478,14 +453,15 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
: row,
|
: row,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
setActiveRowId(rowId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
setRows((current) => {
|
setRows((current) => {
|
||||||
if (current.length >= MAX_ROWS) return current;
|
if (current.length >= MAX_ROWS) return current;
|
||||||
const next = [...current, newDraftRow()];
|
const row = newDraftRow();
|
||||||
setActiveRowId(next[next.length - 1].id);
|
setActiveRowId(row.id);
|
||||||
return next;
|
return [...current, row];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -887,61 +863,57 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{categoryPlays.length > 0 ? (
|
{categoryPlays.length === 0 ? (
|
||||||
<p className="flex items-start justify-center gap-2 rounded-lg border border-[#c8daf6] bg-[#f0f6ff] px-3 py-2.5 text-left text-[11px] font-medium leading-snug text-[#0b3f96]">
|
|
||||||
<ChevronRight className="mt-0.5 size-4 shrink-0 text-[#1d57b7]" aria-hidden />
|
|
||||||
<span>{t("hall.table.scrollHint")}</span>
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2.5 text-center text-xs text-amber-950"
|
className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2.5 text-center text-xs text-amber-950"
|
||||||
role="status"
|
role="status"
|
||||||
>
|
>
|
||||||
{t("hall.table.noPlaysInCategory")}
|
{t("hall.table.noPlaysInCategory")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-hidden rounded-xl border border-[#e6edf8] bg-white shadow-[0_8px_28px_rgba(15,23,42,0.05)] transition-opacity",
|
"overflow-hidden rounded-xl border border-[#e6edf8] bg-white shadow-[0_8px_24px_rgba(15,23,42,0.05)] transition-opacity",
|
||||||
tableDisabled && "opacity-55",
|
tableDisabled && "opacity-55",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table
|
<table
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full border-collapse text-sm",
|
"w-full border-collapse text-[11px]",
|
||||||
activeCategory === "D4" ? "min-w-[1240px]" : "min-w-[740px]",
|
activeCategory === "D4" ? "min-w-[760px]" : "min-w-[460px]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-[#e8eef7] bg-[#f5f8fd] text-[11px] font-semibold text-[#32518d]">
|
<tr className="border-b border-[#edf2f8] bg-[#f8fafd] text-[#58709d]">
|
||||||
<th className="sticky left-0 z-20 w-12 bg-[#f5f8fd] px-2 py-3 text-center">
|
<th className="w-8 px-1.5 py-2 text-center font-bold">
|
||||||
{t("hall.table.no", { defaultValue: "No." })}
|
{t("hall.table.no", { defaultValue: "No." })}
|
||||||
</th>
|
</th>
|
||||||
<th className="sticky left-12 z-20 w-24 bg-[#f5f8fd] px-2 py-3 text-center">
|
<th className="w-20 px-1.5 py-2 text-center font-bold">
|
||||||
{t("hall.table.number", { defaultValue: "Number" })}
|
<span className="block">{t("hall.table.number", { defaultValue: "Number" })}</span>
|
||||||
<span className="block text-[10px] font-normal text-[#6b7896]">
|
<span className="block text-[9px] font-medium text-[#9aa8bd]">({numberPlaceholder})</span>
|
||||||
({numberPlaceholder})
|
|
||||||
</span>
|
|
||||||
</th>
|
</th>
|
||||||
{categoryPlays.map((play) => (
|
{categoryPlays.map((play) => (
|
||||||
<th key={play.play_code} className="min-w-24 px-2 py-3 text-center">
|
<th key={play.play_code} className="min-w-16 px-1 py-2 text-center font-bold">
|
||||||
{pickDisplayName(play)}
|
<span className="block truncate">{pickDisplayName(play)}</span>
|
||||||
|
<span className="block text-[9px] font-medium text-[#9aa8bd]">
|
||||||
|
{t("hall.table.amountPlaceholder", { defaultValue: "金额" })}
|
||||||
|
</span>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
<th className="w-10 px-2 py-3" aria-label={t("hall.table.delete")} />
|
<th className="w-7 px-1 py-2" aria-label={t("hall.table.delete")} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row, index) => {
|
{rows.map((row, index) => {
|
||||||
const rowKey = row.id;
|
const rowKey = row.id;
|
||||||
return (
|
return (
|
||||||
<tr key={rowKey} className="border-b border-[#eef2f8] last:border-b-0">
|
<tr key={rowKey} className="border-b border-[#f0f3f8] last:border-b-0">
|
||||||
<td className="sticky left-0 z-10 bg-white px-2 py-3 text-center font-semibold text-[#17408d]">
|
<td className="px-1.5 py-2 text-center font-black text-[#17408d]">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</td>
|
</td>
|
||||||
<td className="sticky left-12 z-10 bg-white px-2 py-3">
|
<td className="px-1.5 py-2">
|
||||||
<Input
|
<Input
|
||||||
value={row.number}
|
value={row.number}
|
||||||
disabled={tableDisabled}
|
disabled={tableDisabled}
|
||||||
@@ -950,16 +922,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
onFocus={() => setActiveRowId(row.id)}
|
onFocus={() => setActiveRowId(row.id)}
|
||||||
onClick={() => setActiveRowId(row.id)}
|
onClick={() => setActiveRowId(row.id)}
|
||||||
onChange={(event) => updateRowNumber(row.id, event.target.value)}
|
onChange={(event) => updateRowNumber(row.id, event.target.value)}
|
||||||
className="h-10 rounded-lg border-[#e2e8f0] bg-white text-center font-mono text-sm font-semibold text-slate-900 shadow-sm"
|
className="h-8 rounded-md border-[#e1e8f3] bg-white px-1 text-center font-mono text-sm font-black tracking-[0.1em] text-slate-950 shadow-sm focus-visible:ring-[#1d57b7]"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{categoryPlays.map((play) => {
|
{categoryPlays.map((play) => {
|
||||||
const amountText = row.amounts[play.play_code] ?? "";
|
const amountText = row.amounts[play.play_code] ?? "";
|
||||||
const amountMinor = parseDecimalInputToMinor(amountText);
|
|
||||||
const rebate = amountMinor != null
|
|
||||||
? Math.round(amountMinor * parseRebateRate(play.odds?.rebate_rate))
|
|
||||||
: 0;
|
|
||||||
const actual = amountMinor != null ? Math.max(0, amountMinor - rebate) : 0;
|
|
||||||
const status = cellRiskState(
|
const status = cellRiskState(
|
||||||
play,
|
play,
|
||||||
row.number,
|
row.number,
|
||||||
@@ -967,54 +934,57 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
alertRows,
|
alertRows,
|
||||||
);
|
);
|
||||||
const disabled = tableDisabled || status === "sold_out" || (play.config !== null && !play.config.is_enabled);
|
const disabled = tableDisabled || status === "sold_out" || (play.config !== null && !play.config.is_enabled);
|
||||||
|
const hasAmount = amountText.trim().length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={`${rowKey}-${play.play_code}`}
|
key={`${rowKey}-${play.play_code}`}
|
||||||
className={cn("px-1.5 py-3", tableCellClass(status, disabled))}
|
className={cn(
|
||||||
|
"px-1 py-2 align-top",
|
||||||
|
status === "warning" && "bg-amber-50/70",
|
||||||
|
status === "sold_out" && "bg-slate-100 text-slate-400",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<Input
|
||||||
<Input
|
value={amountText}
|
||||||
value={amountText}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
inputMode="decimal"
|
||||||
inputMode="decimal"
|
placeholder={
|
||||||
placeholder={
|
status === "sold_out"
|
||||||
status === "sold_out"
|
? t("hall.table.soldOut", { defaultValue: "售罄" })
|
||||||
? t("hall.table.soldOut", { defaultValue: "售罄" })
|
: "-"
|
||||||
: "-"
|
}
|
||||||
}
|
onFocus={() => setActiveRowId(row.id)}
|
||||||
onFocus={() => setActiveRowId(row.id)}
|
onClick={() => setActiveRowId(row.id)}
|
||||||
onClick={() => setActiveRowId(row.id)}
|
onChange={(event) => updateAmount(row.id, play.play_code, event.target.value)}
|
||||||
onChange={(event) => updateAmount(row.id, play.play_code, event.target.value)}
|
className={cn(
|
||||||
className="h-10 rounded-lg border-[#e2e8f0] bg-white text-center text-xs tabular-nums shadow-sm"
|
"h-8 rounded-md border-[#e1e8f3] bg-white px-1 text-center text-xs font-bold tabular-nums shadow-sm focus-visible:ring-[#1d57b7]",
|
||||||
/>
|
hasAmount && "border-[#9bbcff] bg-[#f5f9ff] text-[#0b3f96]",
|
||||||
{status === "sold_out" ? (
|
status === "warning" && "border-amber-200 bg-amber-50 text-amber-800",
|
||||||
<p className="text-center text-[10px] font-semibold text-slate-500">
|
status === "sold_out" && "border-slate-200 bg-slate-100 text-slate-400",
|
||||||
{t("hall.table.soldOut", { defaultValue: "售罄" })}
|
)}
|
||||||
</p>
|
/>
|
||||||
) : status === "warning" ? (
|
{status === "sold_out" ? (
|
||||||
<p className="text-center text-[10px] font-semibold text-amber-600">
|
<p className="mt-0.5 text-center text-[9px] font-bold text-slate-500">
|
||||||
{t("hall.table.warning", { defaultValue: "接近售罄" })}
|
{t("hall.table.soldOut", { defaultValue: "售罄" })}
|
||||||
</p>
|
</p>
|
||||||
) : activeCategory !== "D4" && amountMinor !== null ? (
|
) : status === "warning" ? (
|
||||||
<p className="text-center text-[10px] text-slate-500">
|
<p className="mt-0.5 text-center text-[9px] font-bold text-amber-700">
|
||||||
{t("hall.table.actual", { defaultValue: "实扣" })}{" "}
|
{t("hall.table.warning", { defaultValue: "接近售罄" })}
|
||||||
{amountToDisplay(actual)}
|
</p>
|
||||||
</p>
|
) : null}
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<td className="px-1 py-3 text-center">
|
<td className="px-1 py-2 text-center align-middle">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={tableDisabled || rows.length <= 1}
|
disabled={tableDisabled || rows.length <= 1}
|
||||||
onClick={() => removeRow(row.id)}
|
onClick={() => removeRow(row.id)}
|
||||||
className="inline-flex size-8 items-center justify-center rounded-full text-[#ff4d4f] hover:bg-red-50 disabled:text-slate-300 disabled:hover:bg-transparent"
|
className="inline-flex size-6 items-center justify-center rounded-full text-[#e5002c] hover:bg-red-50 disabled:text-slate-300 disabled:hover:bg-transparent"
|
||||||
aria-label={t("actions.deleteRow", { row: index + 1 })}
|
aria-label={t("actions.deleteRow", { row: index + 1 })}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4" aria-hidden />
|
<Trash2 className="size-3.5" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1023,42 +993,25 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={tableDisabled || rows.length >= MAX_ROWS}
|
disabled={tableDisabled || rows.length >= MAX_ROWS}
|
||||||
onClick={addRow}
|
onClick={addRow}
|
||||||
className="flex h-11 w-full items-center justify-center gap-1.5 border-t border-[#edf2f9] text-sm font-semibold text-[#1d57b7] hover:bg-[#f7faff] disabled:text-slate-300"
|
className="flex h-10 w-full items-center justify-center gap-1.5 border-t border-[#edf2f9] bg-white text-xs font-bold text-[#1d57b7] hover:bg-[#f7faff] disabled:text-slate-300"
|
||||||
>
|
>
|
||||||
<CirclePlus className="size-4" aria-hidden />
|
<CirclePlus className="size-4" aria-hidden />
|
||||||
{t("hall.table.addRow", { defaultValue: "添加一行" })}
|
{t("hall.table.addRow", { defaultValue: "添加一行" })}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 rounded-xl border border-[#e9eef7] bg-[#f8fbff] px-4 py-3 text-sm shadow-[0_6px_20px_rgba(15,23,42,0.04)] md:grid-cols-3">
|
<div className="flex items-center justify-between rounded-xl border border-[#e9eef7] bg-[#f8fbff] px-4 py-3 text-sm shadow-[0_6px_20px_rgba(15,23,42,0.04)]">
|
||||||
<div>
|
<span className="text-xs font-medium text-slate-500">
|
||||||
<p className="text-xs font-medium text-slate-500">
|
{t("hall.table.draftTotal", { defaultValue: "草稿合计" })}
|
||||||
{t("hall.summary.expectedDeduct", { defaultValue: "预计扣款" })}
|
</span>
|
||||||
</p>
|
<span className="text-base font-black tabular-nums text-[#0b3f96]">
|
||||||
<p className="mt-1 text-lg font-bold tabular-nums text-[#0b3f96]">
|
{formatMinorAsCurrency(debouncedSummary.actual, currencyCode)}
|
||||||
{formatMinorAsCurrency(debouncedSummary.actual, currencyCode)}
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-500">
|
|
||||||
{t("hall.summary.rebate", { defaultValue: "回水" })}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-lg font-bold tabular-nums text-emerald-600">
|
|
||||||
{formatMinorAsCurrency(debouncedSummary.rebate, currencyCode)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-500">
|
|
||||||
{t("hall.summary.total", { defaultValue: "合计" })}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-lg font-black tabular-nums text-[#d81435]">
|
|
||||||
{formatMinorAsCurrency(debouncedSummary.bet, currencyCode)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sealedBetUi ? (
|
{sealedBetUi ? (
|
||||||
|
|||||||
@@ -1,49 +1,104 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { CalendarIcon, XIcon } from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { getDrawResults } from "@/api/draw";
|
import { getDrawResults } from "@/api/draw";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||||
import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
|
import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
|
||||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||||
import type { DrawResultListItem } from "@/types/api/draw-results";
|
import type { DrawResultListItem } from "@/types/api/draw-results";
|
||||||
|
|
||||||
|
const RESULTS_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
const MONTH_OPTIONS = Array.from({ length: 12 }, (_, value) => ({
|
||||||
|
value,
|
||||||
|
label: `${value + 1}月`,
|
||||||
|
}));
|
||||||
|
|
||||||
export function DrawResultsListScreen() {
|
export function DrawResultsListScreen() {
|
||||||
const { t } = useTranslation("player");
|
const { t } = useTranslation("player");
|
||||||
const [items, setItems] = useState<DrawResultListItem[] | null>(null);
|
const [items, setItems] = useState<DrawResultListItem[] | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [date, setDate] = useState("");
|
const [date, setDate] = useState("");
|
||||||
|
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
||||||
|
const [calendarMonth, setCalendarMonth] = useState(() => new Date());
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [lastPage, setLastPage] = useState(1);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const selectedDate = useMemo(() => parseBusinessDate(date), [date]);
|
||||||
|
const businessDate = /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined;
|
||||||
|
const quickYears = useMemo(() => buildYearOptions(calendarMonth), [calendarMonth]);
|
||||||
|
|
||||||
const fetchList = useCallback(async () => {
|
const fetchList = useCallback(async (targetPage = 1, append = false) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
if (append) {
|
||||||
|
setLoadingMore(true);
|
||||||
|
} else {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getDrawResults({
|
const res = await getDrawResults({
|
||||||
page: 1,
|
page: targetPage,
|
||||||
size: 30,
|
size: RESULTS_PAGE_SIZE,
|
||||||
business_date: /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined,
|
business_date: businessDate,
|
||||||
});
|
});
|
||||||
setItems(res.items);
|
setItems((current) => (append && current ? [...current, ...res.items] : res.items));
|
||||||
|
setPage(res.page);
|
||||||
|
setLastPage(res.last_page);
|
||||||
} catch {
|
} catch {
|
||||||
setError(t("results.loadFailed"));
|
setError(t("results.loadFailed"));
|
||||||
setItems(null);
|
if (!append) {
|
||||||
|
setItems(null);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (append) {
|
||||||
|
setLoadingMore(false);
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [date, t]);
|
}, [businessDate, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
void fetchList();
|
void fetchList(1, false);
|
||||||
});
|
});
|
||||||
}, [fetchList]);
|
}, [fetchList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = loadMoreRef.current;
|
||||||
|
if (!target || loading || loadingMore || page >= lastPage) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry?.isIntersecting) {
|
||||||
|
void fetchList(page + 1, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: "160px" },
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(target);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [fetchList, lastPage, loading, loadingMore, page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlayerPanel title={t("results.title")} subtitle={t("results.subtitle")} eyebrow={t("brand.name")}>
|
<PlayerPanel title={t("results.title")} subtitle={t("results.subtitle")} eyebrow={t("brand.name")}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -54,17 +109,88 @@ export function DrawResultsListScreen() {
|
|||||||
{t("results.businessDate")}
|
{t("results.businessDate")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Popover open={datePickerOpen} onOpenChange={setDatePickerOpen}>
|
||||||
type="date"
|
<PopoverTrigger
|
||||||
value={date}
|
render={
|
||||||
onChange={(e) => setDate(e.target.value)}
|
<Button
|
||||||
className="h-10 rounded-lg border-[#dce7f7] bg-white text-sm"
|
type="button"
|
||||||
/>
|
variant="outline"
|
||||||
|
className="h-10 flex-1 justify-start rounded-lg border-[#dce7f7] bg-white px-3 text-left text-sm font-semibold text-[#32518d] hover:bg-[#f8fbff]"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 size-4 text-[#7890b8]" />
|
||||||
|
{date || t("results.selectBusinessDate", { defaultValue: "选择日期" })}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PopoverContent align="start" className="w-auto border-[#dce7f7] p-2 shadow-[0_16px_40px_rgba(15,23,42,0.14)]">
|
||||||
|
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||||
|
<Select
|
||||||
|
value={String(calendarMonth.getMonth())}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setCalendarMonth((current) => new Date(current.getFullYear(), Number(value), 1));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 w-full border-[#dce7f7] bg-white text-xs font-semibold text-[#32518d]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-64 min-w-24">
|
||||||
|
{MONTH_OPTIONS.map((month) => (
|
||||||
|
<SelectItem key={month.value} value={String(month.value)}>
|
||||||
|
{month.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={String(calendarMonth.getFullYear())}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setCalendarMonth((current) => new Date(Number(value), current.getMonth(), 1));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 w-full border-[#dce7f7] bg-white text-xs font-semibold text-[#32518d]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-64 min-w-24">
|
||||||
|
{quickYears.map((year) => (
|
||||||
|
<SelectItem key={year} value={String(year)}>
|
||||||
|
{year}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
month={calendarMonth}
|
||||||
|
onMonthChange={setCalendarMonth}
|
||||||
|
selected={selectedDate}
|
||||||
|
onSelect={(value) => {
|
||||||
|
if (!value) return;
|
||||||
|
setDate(formatBusinessDate(value));
|
||||||
|
setCalendarMonth(value);
|
||||||
|
setDatePickerOpen(false);
|
||||||
|
}}
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{date ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="h-10 w-10 rounded-lg border-[#dce7f7] bg-white text-[#7890b8] hover:bg-[#f8fbff] hover:text-[#32518d]"
|
||||||
|
aria-label={t("actions.clear", { defaultValue: "清除" })}
|
||||||
|
onClick={() => setDate("")}
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-10 rounded-lg bg-[#07459f] px-4 text-white hover:bg-[#063b88]"
|
className="h-10 rounded-lg bg-[#07459f] px-4 text-white hover:bg-[#063b88]"
|
||||||
onClick={() => void fetchList()}
|
onClick={() => void fetchList(1, false)}
|
||||||
>
|
>
|
||||||
{t("actions.apply")}
|
{t("actions.apply")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -84,7 +210,7 @@ export function DrawResultsListScreen() {
|
|||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
|
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
|
||||||
onClick={() => void fetchList()}
|
onClick={() => void fetchList(1, false)}
|
||||||
>
|
>
|
||||||
{t("actions.retry")}
|
{t("actions.retry")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -131,9 +257,52 @@ export function DrawResultsListScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
<div ref={loadMoreRef} className="min-h-1" />
|
||||||
|
{page < lastPage ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-10 w-full rounded-xl border-[#dce7f7] bg-white text-sm font-bold text-[#32518d] hover:bg-[#f8fbff]"
|
||||||
|
disabled={loadingMore}
|
||||||
|
onClick={() => void fetchList(page + 1, true)}
|
||||||
|
>
|
||||||
|
{loadingMore
|
||||||
|
? t("actions.loading", { defaultValue: "加载中..." })
|
||||||
|
: t("actions.loadMore", { defaultValue: "加载更多" })}
|
||||||
|
</Button>
|
||||||
|
) : items && items.length > 0 ? (
|
||||||
|
<p className="py-2 text-center text-xs text-slate-400">
|
||||||
|
{t("results.noMore", { defaultValue: "没有更多开奖结果" })}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PlayerPanel>
|
</PlayerPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildYearOptions(month: Date): number[] {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const visibleYear = month.getFullYear();
|
||||||
|
const start = Math.min(currentYear - 10, visibleYear - 2);
|
||||||
|
const end = Math.max(currentYear + 2, visibleYear + 2);
|
||||||
|
|
||||||
|
return Array.from({ length: end - start + 1 }, (_, index) => start + index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBusinessDate(value: string): Date | undefined {
|
||||||
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||||
|
if (!match) return undefined;
|
||||||
|
|
||||||
|
const [, year, month, day] = match;
|
||||||
|
return new Date(Number(year), Number(month) - 1, Number(day));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBusinessDate(value: Date): string {
|
||||||
|
const year = value.getFullYear();
|
||||||
|
const month = String(value.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(value.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,6 +127,11 @@
|
|||||||
"sealedHint": "Closed: this table is locked. Please wait for the next issue.",
|
"sealedHint": "Closed: this table is locked. Please wait for the next issue.",
|
||||||
"previewing": "Previewing...",
|
"previewing": "Previewing...",
|
||||||
"submitBet": "Submit Bet",
|
"submitBet": "Submit Bet",
|
||||||
|
"amountPlaceholder": "Amount",
|
||||||
|
"filledPlayCount": "{{count}} plays filled",
|
||||||
|
"tapToFill": "Tap to enter amounts",
|
||||||
|
"rowActual": "Actual",
|
||||||
|
"editRow": "Edit row {{row}}",
|
||||||
"scrollHint": "This table is wide: swipe or scroll horizontally, then enter your stake in each play column on the right (e.g. Big/Small, position plays).",
|
"scrollHint": "This table is wide: swipe or scroll horizontally, then enter your stake in each play column on the right (e.g. Big/Small, position plays).",
|
||||||
"noPlaysInCategory": "No open play types in this tab, so there are no amount fields. Try 2D / 3D / 4D, or ask an admin to enable plays for this category."
|
"noPlaysInCategory": "No open play types in this tab, so there are no amount fields. Try 2D / 3D / 4D, or ask an admin to enable plays for this category."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,6 +127,11 @@
|
|||||||
"sealedHint": "बन्द: यो तालिका लक छ। कृपया अर्को इश्यू पर्खनुहोस्।",
|
"sealedHint": "बन्द: यो तालिका लक छ। कृपया अर्को इश्यू पर्खनुहोस्।",
|
||||||
"previewing": "पूर्वावलोकन...",
|
"previewing": "पूर्वावलोकन...",
|
||||||
"submitBet": "बेट पेश गर्नुहोस्",
|
"submitBet": "बेट पेश गर्नुहोस्",
|
||||||
|
"amountPlaceholder": "रकम",
|
||||||
|
"filledPlayCount": "{{count}} प्ले भरियो",
|
||||||
|
"tapToFill": "रकम लेख्न ट्याप गर्नुहोस्",
|
||||||
|
"rowActual": "वास्तविक",
|
||||||
|
"editRow": "पंक्ति {{row}} सम्पादन गर्नुहोस्",
|
||||||
"scrollHint": "तालिका फराकिलो छ: दायाँतिर स्क्रोल गर्नुहोस्, दायाँका प्रत्येक खेल स्तम्भमा बेट रकम लेख्नुहोस्।",
|
"scrollHint": "तालिका फराकिलो छ: दायाँतिर स्क्रोल गर्नुहोस्, दायाँका प्रत्येक खेल स्तम्भमा बेट रकम लेख्नुहोस्।",
|
||||||
"noPlaysInCategory": "यस ट्याबमा खुला खेल प्रकार छैन। २D / ३D / ४D प्रयास गर्नुहोस् वा व्यवस्थापकले खेल खोल्नुपर्छ।"
|
"noPlaysInCategory": "यस ट्याबमा खुला खेल प्रकार छैन। २D / ३D / ४D प्रयास गर्नुहोस् वा व्यवस्थापकले खेल खोल्नुपर्छ।"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,6 +127,11 @@
|
|||||||
"sealedHint": "已封盘:当前表格不可编辑,请等待下一期。",
|
"sealedHint": "已封盘:当前表格不可编辑,请等待下一期。",
|
||||||
"previewing": "预览中...",
|
"previewing": "预览中...",
|
||||||
"submitBet": "提交下注",
|
"submitBet": "提交下注",
|
||||||
|
"amountPlaceholder": "金额",
|
||||||
|
"filledPlayCount": "已填写 {{count}} 个玩法",
|
||||||
|
"tapToFill": "点击填写玩法金额",
|
||||||
|
"rowActual": "实扣",
|
||||||
|
"editRow": "编辑第 {{row}} 行",
|
||||||
"scrollHint": "表格较宽:请向右滑动,在右侧各玩法列(如 Big / Small、位置玩法等)输入下注金额。",
|
"scrollHint": "表格较宽:请向右滑动,在右侧各玩法列(如 Big / Small、位置玩法等)输入下注金额。",
|
||||||
"noPlaysInCategory": "当前分类没有已开放的玩法,无法填写金额。请尝试切换 2D / 3D / 4D,或在后台开放对应玩法。"
|
"noPlaysInCategory": "当前分类没有已开放的玩法,无法填写金额。请尝试切换 2D / 3D / 4D,或在后台开放对应玩法。"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user