feat(game): 更新马来西亚手机号验证和游戏界面优化
- 修改认证模块手机号验证规则适配马来西亚号码格式 - 添加新的投注限制提示文本支持多语言 - 重命名结算阶段标签为Drawing统一显示 - 新增桌面版动物游戏遮罩组件分离功能 - 添加回合开始投注提醒弹窗组件 - 优化开奖动画流程和视觉效果 - 添加奖励动画显示和投注汇总展示 - 新增多种投注限制和状态提示信息
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useGameRoundStore } from '@/store/game'
|
||||
|
||||
const BETTING_START_ALERT_DURATION_MS = 2000
|
||||
|
||||
interface RoundBettingStartAlertProps {
|
||||
className?: string
|
||||
placement?: 'absolute' | 'fixed'
|
||||
}
|
||||
|
||||
export function RoundBettingStartAlert({
|
||||
className,
|
||||
placement = 'absolute',
|
||||
}: RoundBettingStartAlertProps) {
|
||||
const { t } = useTranslation()
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const roundId = useGameRoundStore((state) => state.round.id)
|
||||
const roundPhase = useGameRoundStore((state) => state.round.phase)
|
||||
const lastShownRoundIdRef = useRef<string | null>(null)
|
||||
const [visibleRoundId, setVisibleRoundId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (roundPhase !== 'betting' || !roundId) {
|
||||
setVisibleRoundId(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (lastShownRoundIdRef.current === roundId) {
|
||||
return
|
||||
}
|
||||
|
||||
lastShownRoundIdRef.current = roundId
|
||||
setVisibleRoundId(roundId)
|
||||
|
||||
const timerId = window.setTimeout(() => {
|
||||
setVisibleRoundId((currentRoundId) =>
|
||||
currentRoundId === roundId ? null : currentRoundId,
|
||||
)
|
||||
}, BETTING_START_ALERT_DURATION_MS)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timerId)
|
||||
}
|
||||
}, [roundId, roundPhase])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visibleRoundId ? (
|
||||
<motion.div
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
'pointer-events-none left-1/2 top-1/2 z-50 w-[min(calc(var(--design-unit)*560),88vw)] -translate-x-1/2 -translate-y-1/2',
|
||||
placement === 'fixed' ? 'fixed' : 'absolute',
|
||||
className,
|
||||
)}
|
||||
initial={
|
||||
prefersReducedMotion
|
||||
? { opacity: 0 }
|
||||
: { opacity: 0, scale: 0.82, y: 'calc(var(--design-unit)*18)' }
|
||||
}
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? { opacity: 1 }
|
||||
: { opacity: 1, scale: 1, y: 0 }
|
||||
}
|
||||
exit={
|
||||
prefersReducedMotion
|
||||
? { opacity: 0 }
|
||||
: { opacity: 0, scale: 0.92, y: 'calc(var(--design-unit)*-12)' }
|
||||
}
|
||||
transition={{ duration: 0.28, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
<motion.div
|
||||
className="relative overflow-hidden rounded-[calc(var(--design-unit)*22)] border border-[rgba(126,255,248,0.76)] bg-[linear-gradient(180deg,rgba(7,33,43,0.96),rgba(4,16,24,0.98))] px-design-36 py-design-28 text-center shadow-[0_0_calc(var(--design-unit)*20)_rgba(70,245,255,0.4),0_0_calc(var(--design-unit)*52)_rgba(18,206,232,0.28),inset_0_0_calc(var(--design-unit)*22)_rgba(126,255,248,0.16)]"
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
boxShadow: [
|
||||
'0 0 calc(var(--design-unit)*14) rgba(70,245,255,0.32), 0 0 calc(var(--design-unit)*34) rgba(18,206,232,0.2), inset 0 0 calc(var(--design-unit)*18) rgba(126,255,248,0.14)',
|
||||
'0 0 calc(var(--design-unit)*24) rgba(87,250,255,0.54), 0 0 calc(var(--design-unit)*58) rgba(25,223,255,0.34), inset 0 0 calc(var(--design-unit)*24) rgba(146,255,251,0.22)',
|
||||
'0 0 calc(var(--design-unit)*18) rgba(70,245,255,0.38), 0 0 calc(var(--design-unit)*46) rgba(18,206,232,0.26), inset 0 0 calc(var(--design-unit)*20) rgba(126,255,248,0.16)',
|
||||
],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 0.72,
|
||||
ease: 'easeInOut',
|
||||
}
|
||||
}
|
||||
>
|
||||
<span className="pointer-events-none absolute inset-[1px] rounded-[calc(var(--design-unit)*22)] border border-[rgba(229,255,255,0.12)]" />
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-design-20 top-design-10 h-design-24 rounded-full bg-[linear-gradient(180deg,rgba(255,255,255,0.18),rgba(255,255,255,0))]"
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: { opacity: [0.54, 0.9, 0.58], x: ['-7%', '7%', '-2%'] }
|
||||
}
|
||||
transition={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: { duration: 1.1, ease: 'easeInOut' }
|
||||
}
|
||||
/>
|
||||
<div className="relative z-10 flex flex-col items-center gap-design-28">
|
||||
<motion.div
|
||||
className="text-design-22 font-semibold leading-tight tracking-[0.18em] text-[#78FF7F]"
|
||||
animate={
|
||||
prefersReducedMotion ? undefined : { opacity: [0.78, 1, 0.9] }
|
||||
}
|
||||
transition={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: { duration: 0.72, ease: 'easeOut' }
|
||||
}
|
||||
>
|
||||
{t('game.roundBettingStart.title', {
|
||||
roundId: visibleRoundId,
|
||||
})}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="relative flex items-center justify-center px-design-12 text-design-46 font-bold leading-tight tracking-[0.14em] text-[#F2FFFF] [text-shadow:0_0_calc(var(--design-unit)*16)_rgba(92,249,255,0.66)]"
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
opacity: [0.88, 1, 0.92],
|
||||
scale: [0.98, 1.07, 0.99],
|
||||
textShadow: [
|
||||
'0 0 calc(var(--design-unit)*12) rgba(92,249,255,0.52), 0 0 calc(var(--design-unit)*22) rgba(120,255,127,0.22)',
|
||||
'0 0 calc(var(--design-unit)*22) rgba(111,255,255,0.9), 0 0 calc(var(--design-unit)*42) rgba(120,255,127,0.44)',
|
||||
'0 0 calc(var(--design-unit)*15) rgba(92,249,255,0.62), 0 0 calc(var(--design-unit)*28) rgba(120,255,127,0.28)',
|
||||
],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 1.05,
|
||||
ease: 'easeInOut',
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
}
|
||||
}
|
||||
>
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-x-0 top-1/2 h-design-30 -translate-y-1/2 rounded-full bg-[rgba(120,255,248,0.24)] blur-[calc(var(--design-unit)*10)]"
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
opacity: [0.28, 0.72, 0.34],
|
||||
scaleX: [0.82, 1.12, 0.9],
|
||||
}
|
||||
}
|
||||
transition={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
duration: 1.05,
|
||||
ease: 'easeInOut',
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span className="relative z-10">
|
||||
{t('game.roundBettingStart.action')}
|
||||
</span>
|
||||
</motion.div>
|
||||
<div className="mt-design-8 flex items-center gap-design-7">
|
||||
{[0, 1, 2, 3, 4].map((index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
className="h-design-5 w-design-22 rounded-full bg-[rgba(130,255,248,0.88)] shadow-[0_0_calc(var(--design-unit)*10)_rgba(91,248,255,0.52)]"
|
||||
animate={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: { opacity: [0.36, 1, 0.42], y: [0, -2, 0] }
|
||||
}
|
||||
transition={
|
||||
prefersReducedMotion
|
||||
? undefined
|
||||
: {
|
||||
delay: index * 0.06,
|
||||
duration: 1.1,
|
||||
ease: 'easeInOut',
|
||||
repeat: 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user