- 移除 useGameBoardVm 数据层实施说明文档 - 移除核心玩法与前端规则摘要文档 - 移除游戏模块数据与界面分层第一阶段实施稿文档 - 清理与数据层重构相关的技术方案说明 - 删除关于 PC 和 Mobile 界面分离的设计规划 - 移除 view-model hooks 架构设计相关内容
288 lines
13 KiB
TypeScript
288 lines
13 KiB
TypeScript
import { History } from 'lucide-react'
|
|
import { useCallback, useRef } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import historyBg from '@/assets/system/history-bg.png'
|
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
|
import { SmartImage } from '@/components/smart-image'
|
|
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
|
import { FLOWER_IMAGE_BY_ID } from '@/features/game/shared'
|
|
import { useGameHistoryVm } from '@/hooks/use-game-history-vm.ts'
|
|
|
|
function HistoryRewardNumber({
|
|
className,
|
|
number,
|
|
}: {
|
|
className?: string
|
|
number: number
|
|
}) {
|
|
const image = FLOWER_IMAGE_BY_ID[number]
|
|
const label = String(number).padStart(2, '0')
|
|
|
|
if (!image?.rewardUrl) {
|
|
return (
|
|
<span
|
|
className={`inline-flex h-design-38 min-w-design-38 shrink-0 items-center justify-center px-design-5 text-design-15 font-bold text-[#D5FBFF] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(75,233,255,0.12),0_0_calc(var(--design-unit)*10)_rgba(75,233,255,0.1)] align-middle ${className ?? ''}`}
|
|
>
|
|
{label}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<span
|
|
className={`inline-flex h-design-38 w-design-38 shrink-0 items-center justify-center p-design-1 align-middle ${className ?? ''}`}
|
|
title={label}
|
|
>
|
|
<SmartImage
|
|
src={image.rewardUrl}
|
|
alt={label}
|
|
showSkeleton={false}
|
|
className="h-full w-full overflow-visible"
|
|
imgClassName="object-contain"
|
|
/>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function HistoryEmptyState({ label }: { label: string }) {
|
|
return (
|
|
<div className="flex min-h-full w-full flex-1 items-center justify-center px-design-10 py-design-10">
|
|
<div className="flex w-fit max-w-full flex-col items-center rounded-[calc(var(--design-unit)*8)] border border-[rgba(94,212,230,0.14)] bg-[rgba(5,23,33,0.3)] px-design-14 py-design-12 text-center">
|
|
<div className="mb-design-8 flex h-design-42 w-design-42 items-center justify-center rounded-full border border-[#56EFFF]/18 bg-[#061D29]/55 shadow-[0_0_calc(var(--design-unit)*10)_rgba(86,239,255,0.08)]">
|
|
<History
|
|
aria-hidden="true"
|
|
className="h-design-20 w-design-20 text-[#A8EAF1]"
|
|
strokeWidth={1.8}
|
|
/>
|
|
</div>
|
|
<div className="whitespace-nowrap text-design-16 font-semibold text-[#9CCFD4]">
|
|
{label}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function DesktopGameHistory() {
|
|
const { t } = useTranslation()
|
|
const {
|
|
emptyText,
|
|
endText,
|
|
fetchNextPage,
|
|
hasNextPage,
|
|
isEmpty,
|
|
isFetchingNextPage,
|
|
isInitialLoading,
|
|
items,
|
|
loadingText,
|
|
} = useGameHistoryVm()
|
|
const parentRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
const handleScroll = useCallback(() => {
|
|
const element = parentRef.current
|
|
|
|
if (!element || !hasNextPage || isFetchingNextPage) {
|
|
return
|
|
}
|
|
|
|
const distanceToBottom =
|
|
element.scrollHeight - element.scrollTop - element.clientHeight
|
|
|
|
if (distanceToBottom <= 120) {
|
|
void fetchNextPage()
|
|
}
|
|
}, [fetchNextPage, hasNextPage, isFetchingNextPage])
|
|
|
|
return (
|
|
<SmartBackground
|
|
src={historyBg}
|
|
size="100% 100%"
|
|
className="desktop-game-history-glow flex h-full min-h-0 w-full flex-col items-center overflow-hidden bg-center bg-no-repeat py-2"
|
|
>
|
|
<div
|
|
className={
|
|
'relative z-20 flex h-design-50 shrink-0 items-center justify-center text-design-28 font-bold text-[#D5FBFF]'
|
|
}
|
|
>
|
|
{t('gameDesktop.history.title')}
|
|
</div>
|
|
<div
|
|
ref={parentRef}
|
|
onScroll={handleScroll}
|
|
className={
|
|
'history-scroll-hidden z-10 flex min-h-0 flex-1 w-full flex-col gap-design-6 overflow-y-auto overflow-x-hidden px-design-14 py-design-14'
|
|
}
|
|
>
|
|
{isInitialLoading ? (
|
|
<DataLoadingIndicator
|
|
label={loadingText}
|
|
className="min-h-full flex-1"
|
|
/>
|
|
) : isEmpty ? (
|
|
<HistoryEmptyState label={emptyText} />
|
|
) : (
|
|
<>
|
|
{items.map((item) => {
|
|
const isWin = item.resultState === 'win'
|
|
const statusLabel =
|
|
item.resultState === 'pending'
|
|
? t('gameDesktop.history.pending')
|
|
: isWin
|
|
? t('gameDesktop.history.win')
|
|
: t('gameDesktop.history.lost')
|
|
const statusColor =
|
|
item.resultState === 'pending'
|
|
? '#D5FBFF'
|
|
: isWin
|
|
? '#FFE375'
|
|
: '#8DFF98'
|
|
const statusTextShadow =
|
|
item.resultState === 'pending'
|
|
? '0 0 calc(var(--design-unit)*10) rgba(213,251,255,0.85), 0 0 calc(var(--design-unit)*22) rgba(213,251,255,0.32)'
|
|
: isWin
|
|
? '0 0 calc(var(--design-unit)*10) #FFE375, 0 0 calc(var(--design-unit)*22) rgba(255,227,117,0.48)'
|
|
: '0 0 calc(var(--design-unit)*10) #8DFF98, 0 0 calc(var(--design-unit)*22) rgba(141,255,152,0.48)'
|
|
const statusBorderColor =
|
|
item.resultState === 'pending'
|
|
? 'rgba(143,241,255,0.34)'
|
|
: isWin
|
|
? 'rgba(255,227,117,0.5)'
|
|
: 'rgba(141,255,152,0.36)'
|
|
const statusBg =
|
|
item.resultState === 'pending'
|
|
? 'linear-gradient(180deg,rgba(28,106,126,0.38),rgba(5,25,36,0.9))'
|
|
: isWin
|
|
? 'linear-gradient(180deg,rgba(127,92,14,0.5),rgba(29,22,8,0.92))'
|
|
: 'linear-gradient(180deg,rgba(31,111,54,0.38),rgba(6,28,20,0.92))'
|
|
|
|
return (
|
|
<div key={item.id} className="w-full pb-design-4 last:pb-0">
|
|
<div
|
|
className={
|
|
'relative isolate flex w-full flex-col overflow-hidden rounded-[calc(var(--design-unit)*8)] border bg-[linear-gradient(180deg,rgba(6,33,45,0.95),rgba(3,14,23,0.92))] text-[#FFE375] shadow-[0_0_calc(var(--design-unit)*10)_rgba(63,226,255,0.1),inset_0_1px_0_rgba(218,255,255,0.1)] transition-colors duration-200'
|
|
}
|
|
style={{
|
|
borderColor: statusBorderColor,
|
|
}}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-x-design-8 top-0 h-px opacity-70"
|
|
style={{
|
|
background: `linear-gradient(90deg, transparent, ${statusColor}, transparent)`,
|
|
}}
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute -right-design-24 -top-design-28 h-design-72 w-design-72 rounded-full blur-[24px]"
|
|
style={{
|
|
background: statusColor,
|
|
opacity: isWin ? 0.14 : 0.08,
|
|
}}
|
|
/>
|
|
<div
|
|
className="relative z-10 grid min-h-design-32 w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-design-8 border-b px-design-9 py-design-4"
|
|
style={{
|
|
background: statusBg,
|
|
borderColor: statusBorderColor,
|
|
}}
|
|
>
|
|
<span className="min-w-0 truncate text-design-12 font-medium text-[#8DBCC2]">
|
|
{item.createdAtLabel}
|
|
</span>
|
|
<span
|
|
className="inline-flex min-w-design-72 items-center justify-center gap-design-5 text-design-15 font-bold"
|
|
style={{
|
|
borderColor: statusBorderColor,
|
|
color: statusColor,
|
|
textShadow: statusTextShadow,
|
|
}}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className="h-design-6 w-design-6 rounded-full shadow-[0_0_calc(var(--design-unit)*8)_currentColor]"
|
|
style={{ backgroundColor: statusColor }}
|
|
/>
|
|
{statusLabel}
|
|
</span>
|
|
<span className="min-w-0 truncate text-right text-design-12 font-medium text-[#C0E7EB]">
|
|
{t('gameDesktop.history.roundId')}: {item.periodNo}
|
|
</span>
|
|
</div>
|
|
<div
|
|
className={
|
|
'relative z-10 flex w-full flex-col gap-design-5 px-design-8 py-design-7 text-design-13'
|
|
}
|
|
>
|
|
<div className="flex min-h-design-34 items-start justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(94,212,230,0.12)] bg-[rgba(5,23,33,0.52)] px-design-7 py-design-4">
|
|
<span className={'shrink-0 pt-design-5 text-[#84A2A2]'}>
|
|
{t('gameDesktop.history.numbers')}
|
|
</span>
|
|
{item.numbers.length === 0 ? (
|
|
<span className="pt-design-5 text-right">
|
|
{item.numbersLabel}
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex min-w-0 flex-1 flex-wrap items-center justify-end gap-design-3 align-middle">
|
|
{item.numbers.map((number) => (
|
|
<HistoryRewardNumber
|
|
className="!h-design-28 !min-w-design-28 !w-design-28 !p-0"
|
|
key={`${item.id}-${number}`}
|
|
number={number}
|
|
/>
|
|
))}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-[1fr_1fr] gap-design-5">
|
|
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,227,117,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(84,57,8,0.14))] px-design-7 py-design-4">
|
|
<span className="shrink-0 text-[#84A2A2]">
|
|
{t('gameDesktop.history.payout')}
|
|
</span>
|
|
<span className="min-w-0 truncate text-right text-design-14 font-bold text-[#FFE375] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(255,227,117,0.2)]">
|
|
{item.winAmountLabel}
|
|
</span>
|
|
</div>
|
|
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,117,117,0.16)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(88,20,28,0.14))] px-design-7 py-design-4">
|
|
<span className={'shrink-0 text-[#84A2A2]'}>
|
|
{t('gameDesktop.history.winningResult')}
|
|
</span>
|
|
<span className="flex min-w-0 items-center justify-end">
|
|
{item.resultNumber === null ? (
|
|
<span
|
|
className={
|
|
'text-right font-bold text-[#FF7575] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(255,117,117,0.24)]'
|
|
}
|
|
>
|
|
{item.resultNumberLabel}
|
|
</span>
|
|
) : (
|
|
<HistoryRewardNumber
|
|
className="!h-design-28 !min-w-design-28 !w-design-28 !p-0 text-[#FF7575]"
|
|
number={item.resultNumber}
|
|
/>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
<div className="flex min-h-[calc(var(--design-unit)*40)] items-center justify-center text-design-16 text-[#84A2A2]">
|
|
{isFetchingNextPage ? (
|
|
<DataLoadingIndicator compact label={loadingText} />
|
|
) : hasNextPage ? (
|
|
''
|
|
) : (
|
|
endText
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</SmartBackground>
|
|
)
|
|
}
|