- 添加 AuthCoinAmount 类型和 AuthWithdrawAccount 接口 - 在 AuthUser 和相关 DTO 中增加余额和提款账户字段 - 将获取用户资料的请求方法从 POST 改为 GET - 实现提款账户信息的默认值设置和状态管理 - 添加 JackpotPoolTicker 组件并在状态栏中显示 - 更新多语言文件中的游戏规则说明 - 实现提款表单的预填充和状态重置功能
181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import broadcast from '@/assets/system/broadcast.webp'
|
|
import { JackpotPoolTicker } from '@/components/jackpot-pool-ticker.tsx'
|
|
import { SmartImage } from '@/components/smart-image.tsx'
|
|
import { cn } from '@/lib/utils.ts'
|
|
import { useGameSessionStore } from '@/store/game'
|
|
import type { JackpotBroadcastItem } from '@/type'
|
|
|
|
const winAmountFormatter = new Intl.NumberFormat('en-US', {
|
|
maximumFractionDigits: 6,
|
|
})
|
|
const MESSAGE_BROADCAST_ANIMATION_MS = 28_000
|
|
|
|
function formatWinAmount(value: string) {
|
|
const amount = Number(value)
|
|
|
|
return Number.isFinite(amount) ? winAmountFormatter.format(amount) : value
|
|
}
|
|
|
|
type MessageBroadcastProps = {
|
|
className?: string
|
|
showJackpotPool?: boolean
|
|
}
|
|
|
|
export function MessageBroadcast({
|
|
className,
|
|
showJackpotPool = true,
|
|
}: MessageBroadcastProps) {
|
|
const { t } = useTranslation()
|
|
const prefersReducedMotion = useReducedMotion()
|
|
const jackpotBroadcasts = useGameSessionStore(
|
|
(state) => state.jackpotBroadcasts,
|
|
)
|
|
const shownBroadcastIdsRef = useRef(new Set<string>())
|
|
const queuedBroadcastIdsRef = useRef(new Set<string>())
|
|
const [queue, setQueue] = useState<JackpotBroadcastItem[]>([])
|
|
const [activeBroadcast, setActiveBroadcast] =
|
|
useState<JackpotBroadcastItem | null>(null)
|
|
const activeBroadcastLabel = useMemo(() => {
|
|
if (!activeBroadcast) {
|
|
return ''
|
|
}
|
|
|
|
return t('game.jackpotBroadcast.ariaMessage', {
|
|
amount: formatWinAmount(activeBroadcast.totalWin),
|
|
nickname: activeBroadcast.nickname,
|
|
streak: activeBroadcast.currentStreak,
|
|
})
|
|
}, [activeBroadcast, t])
|
|
|
|
useEffect(() => {
|
|
const nextBroadcasts = jackpotBroadcasts.filter((item) => {
|
|
if (
|
|
shownBroadcastIdsRef.current.has(item.id) ||
|
|
queuedBroadcastIdsRef.current.has(item.id) ||
|
|
activeBroadcast?.id === item.id
|
|
) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
if (nextBroadcasts.length === 0) {
|
|
return
|
|
}
|
|
|
|
for (const item of nextBroadcasts) {
|
|
queuedBroadcastIdsRef.current.add(item.id)
|
|
}
|
|
|
|
setQueue((currentQueue) => [...currentQueue, ...nextBroadcasts])
|
|
}, [activeBroadcast?.id, jackpotBroadcasts])
|
|
|
|
useEffect(() => {
|
|
if (activeBroadcast || queue.length === 0) {
|
|
return
|
|
}
|
|
|
|
const [nextBroadcast, ...remainingQueue] = queue
|
|
|
|
queuedBroadcastIdsRef.current.delete(nextBroadcast.id)
|
|
shownBroadcastIdsRef.current.add(nextBroadcast.id)
|
|
setActiveBroadcast(nextBroadcast)
|
|
setQueue(remainingQueue)
|
|
}, [activeBroadcast, queue])
|
|
|
|
useEffect(() => {
|
|
if (!activeBroadcast) {
|
|
return
|
|
}
|
|
|
|
const timerId = window.setTimeout(() => {
|
|
setActiveBroadcast((currentBroadcast) =>
|
|
currentBroadcast?.id === activeBroadcast.id ? null : currentBroadcast,
|
|
)
|
|
}, MESSAGE_BROADCAST_ANIMATION_MS)
|
|
|
|
return () => {
|
|
window.clearTimeout(timerId)
|
|
}
|
|
}, [activeBroadcast])
|
|
|
|
return (
|
|
<section
|
|
aria-label={t('game.jackpotBroadcast.ariaLabel')}
|
|
className={cn(
|
|
'common-neon-inset flex w-full min-w-0 items-center overflow-hidden',
|
|
'h-design-24 gap-design-4 !rounded-[3px] !px-design-5 !py-0 text-design-8',
|
|
'md:h-design-65 md:gap-design-12 md:!rounded-[5px] md:!px-design-20 md:text-design-16',
|
|
className,
|
|
)}
|
|
>
|
|
<div className="flex min-w-0 flex-1 items-center gap-design-4 md:gap-design-10">
|
|
<SmartImage
|
|
className="h-design-12 w-design-12 shrink-0 md:h-design-24 md:w-design-24"
|
|
alt={t('game.jackpotBroadcast.iconAlt')}
|
|
src={broadcast}
|
|
/>
|
|
<div className="relative h-design-16 min-w-0 flex-1 overflow-hidden md:h-design-28">
|
|
<AnimatePresence>
|
|
{activeBroadcast ? (
|
|
<motion.div
|
|
aria-label={activeBroadcastLabel}
|
|
className="absolute inset-0 flex min-w-max items-center whitespace-nowrap font-semibold"
|
|
key={activeBroadcast.id}
|
|
initial={
|
|
prefersReducedMotion
|
|
? { opacity: 0 }
|
|
: { opacity: 1, x: '108%' }
|
|
}
|
|
animate={
|
|
prefersReducedMotion
|
|
? { opacity: 1 }
|
|
: { opacity: 1, x: '-108%' }
|
|
}
|
|
exit={{ opacity: 0 }}
|
|
transition={{
|
|
duration: prefersReducedMotion
|
|
? 0.18
|
|
: MESSAGE_BROADCAST_ANIMATION_MS / 1000,
|
|
ease: prefersReducedMotion ? [0.16, 1, 0.3, 1] : 'linear',
|
|
}}
|
|
>
|
|
<span className="text-[#D5F7FF]">
|
|
{t('game.jackpotBroadcast.prefix')}
|
|
</span>
|
|
<span className="ml-design-4 text-[#4BFFFE] md:ml-design-8">
|
|
{activeBroadcast.nickname}
|
|
</span>
|
|
<span className="mx-design-4 text-[#D5F7FF] md:mx-design-8">
|
|
{t('game.jackpotBroadcast.separator')}
|
|
</span>
|
|
<span className="text-[#78FF7F]">
|
|
{t('game.jackpotBroadcast.streak', {
|
|
count: activeBroadcast.currentStreak,
|
|
})}
|
|
</span>
|
|
<span className="mx-design-4 text-[#D5F7FF] md:mx-design-8">
|
|
{t('game.jackpotBroadcast.winAction')}
|
|
</span>
|
|
<span className="text-[#FFB72B]">
|
|
{formatWinAmount(activeBroadcast.totalWin)}
|
|
</span>
|
|
</motion.div>
|
|
) : null}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
{showJackpotPool ? (
|
|
<JackpotPoolTicker
|
|
variant="inline"
|
|
className="!w-fit max-w-[54%] shrink-0 md:max-w-[calc(var(--design-unit)*320)]"
|
|
/>
|
|
) : null}
|
|
</section>
|
|
)
|
|
}
|