refactor(game): 优化游戏组件并实现国际化支持

- 在AppBootResourceGate组件中集成react-i18next实现资源加载文本的国际化
- 修改DesktopAnimal组件中的loading dots key以提高渲染性能
- 在DesktopControl组件中添加useRef和useEffect钩子管理定时器清理逻辑
- 将DesktopTitle组件重构为MessageBroadcast组件并增强其响应式设计
- 更新DesktopSupportModal组件中的客户服务文本为国际化格式
- 在AuthSession模块中实现本地存储数据清理时保留关键偏好设置
- 调整多个游戏组件的样式类以改进移动端适配效果
- 移除未使用的桌面提取功能相关代码文件
- 更新GitNexus索引统计数据反映最新的代码变更
This commit is contained in:
JiaJun
2026-06-04 18:01:40 +08:00
parent bfb4b76611
commit a6b34660ad
48 changed files with 3185 additions and 1216 deletions

View File

@@ -0,0 +1,550 @@
import { motion } from 'motion/react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import reduce from '@/assets/game/add.webp'
import chipBg from '@/assets/game/chip-bg.webp'
import chipLineBg from '@/assets/game/chip-line-bg.webp'
import chipLock from '@/assets/game/chip-lock.webp'
import confirmRedBg from '@/assets/game/confirm-red-bg.png'
import controlBg from '@/assets/game/control-bg.png'
import leftBottomBg from '@/assets/game/left-bg.webp'
import mobileAddReduceBg from '@/assets/game/mobile-add-reduce-bg.webp'
import mobileConfirmBg from '@/assets/game/mobile-contro-comfirm.webp'
import mobileTotalBg from '@/assets/game/mobile-control-number.webp'
import add from '@/assets/game/reduce.webp'
import diamond from '@/assets/system/diamond.webp'
import { SmartBackground } from '@/components/smart-background.tsx'
import { SmartImage } from '@/components/smart-image.tsx'
import { ACTION_OPTIONS } from '@/constants'
import { useGameControlVm } from '@/hooks/use-game-control-vm.ts'
import { cn } from '@/lib/utils'
import { useModalStore } from '@/store'
export function MobileControl() {
const { t } = useTranslation()
const setModalOpen = useModalStore((state) => state.setModalOpen)
const {
acceptingBets,
actionsEnabled,
canClear,
canDecreaseBetQuantity,
chips,
confirmLabel,
confirmState,
isConfirmClickable,
maxSelectionCountLabel,
onChipSelect,
onConfirm,
onClearSelections,
onDecreaseBetQuantity,
onIncreaseBetQuantity,
onOpenAutoSetting,
onRepeatSelections,
selectedBetQuantityLabel,
selectedChipId,
selectedCountLabel,
totalBetAmountLabel,
} = useGameControlVm()
const [clickedId, setClickedId] = useState<string | null>(null)
const [hidingId, setHidingId] = useState<string | null>(null)
const [confirmClicked, setConfirmClicked] = useState(false)
const clickResetTimerRef = useRef<number | null>(null)
const hideResetTimerRef = useRef<number | null>(null)
const confirmResetTimerRef = useRef<number | null>(null)
const isConfirmWarning =
confirmState === 'insufficient' || confirmState === 'limit'
useEffect(() => {
return () => {
if (clickResetTimerRef.current !== null) {
window.clearTimeout(clickResetTimerRef.current)
}
if (hideResetTimerRef.current !== null) {
window.clearTimeout(hideResetTimerRef.current)
}
if (confirmResetTimerRef.current !== null) {
window.clearTimeout(confirmResetTimerRef.current)
}
}
}, [])
const handleChipClick = (chipId: string) => {
if (!acceptingBets) {
return
}
onChipSelect(chipId)
}
const handleActionClick = useCallback(
(id: string) => {
if (!actionsEnabled) {
return
}
if (id === 'clear' && canClear) {
onClearSelections()
}
if (id === 'repeat') {
onRepeatSelections()
}
if (id === 'auto-spin') {
onOpenAutoSetting()
}
setClickedId(id)
if (clickResetTimerRef.current !== null) {
window.clearTimeout(clickResetTimerRef.current)
}
clickResetTimerRef.current = window.setTimeout(() => {
setClickedId(null)
setHidingId(id)
if (hideResetTimerRef.current !== null) {
window.clearTimeout(hideResetTimerRef.current)
}
hideResetTimerRef.current = window.setTimeout(() => {
setHidingId(null)
}, 180)
}, 200)
},
[
actionsEnabled,
canClear,
onClearSelections,
onOpenAutoSetting,
onRepeatSelections,
],
)
const handleConfirmClick = useCallback(() => {
if (!isConfirmClickable) {
void onConfirm()
return
}
setConfirmClicked(true)
if (confirmResetTimerRef.current !== null) {
window.clearTimeout(confirmResetTimerRef.current)
}
confirmResetTimerRef.current = window.setTimeout(() => {
setConfirmClicked(false)
}, 200)
void onConfirm()
}, [isConfirmClickable, onConfirm])
return (
<div className="flex w-full flex-col gap-design-3 overflow-visible text-[#D5FBFF]">
<div className="flex h-design-42 w-full items-center gap-design-3">
<SmartBackground
src={chipBg}
size="100% 100%"
className="relative z-10 flex h-full min-w-0 flex-1 items-center bg-center bg-no-repeat px-design-8"
>
<SmartBackground
src={chipLineBg}
size="100% 100%"
className="flex h-design-32 min-w-0 flex-1 items-center justify-between gap-design-2 overflow-visible px-design-4"
>
{chips.map((chip) => {
const isSelected = chip.id === selectedChipId
const showLockedState = !acceptingBets
return (
<motion.button
key={chip.id}
layout
type="button"
onClick={() => handleChipClick(chip.id)}
disabled={showLockedState}
whileTap={showLockedState ? undefined : { scale: 0.94 }}
transition={{
layout: {
type: 'spring',
stiffness: 360,
damping: 26,
},
duration: 0.26,
}}
className="relative flex h-design-32 w-design-32 shrink-0 items-center justify-center rounded-full"
style={
showLockedState
? {
WebkitFilter: 'grayscale(100%)',
filter: 'grayscale(100%)',
}
: undefined
}
>
<motion.span
animate={
isSelected && !showLockedState
? {
opacity: [0.22, 0.5, 0.22],
scaleX: [0.82, 1.02, 0.82],
scaleY: [0.42, 0.56, 0.42],
}
: {
opacity: 0.32,
scaleX: 0.88,
scaleY: 0.48,
}
}
transition={
isSelected
? {
duration: 1.5,
repeat: Number.POSITIVE_INFINITY,
ease: 'easeInOut',
}
: { duration: 0.2, ease: 'easeOut' }
}
className="pointer-events-none absolute bottom-[calc(var(--design-unit)*-2)] left-1/2 h-design-8 w-design-24 -translate-x-1/2 rounded-full bg-[rgba(0,0,0,0.48)] blur-[3px]"
/>
<motion.span
animate={
isSelected && !showLockedState
? {
opacity: [0.72, 1, 0.72],
scale: [0.96, 1.04, 0.96],
boxShadow: [
'0 0 0 1px rgba(245, 200, 107, 0.68), 0 0 6px rgba(245, 200, 107, 0.44), 0 0 12px rgba(245, 200, 107, 0.22), inset 0 0 6px rgba(255, 214, 110, 0.18)',
'0 0 0 1px rgba(245, 200, 107, 0.96), 0 0 10px rgba(245, 200, 107, 0.78), 0 0 18px rgba(245, 200, 107, 0.42), inset 0 0 8px rgba(255, 214, 110, 0.32)',
'0 0 0 1px rgba(245, 200, 107, 0.68), 0 0 6px rgba(245, 200, 107, 0.44), 0 0 12px rgba(245, 200, 107, 0.22), inset 0 0 6px rgba(255, 214, 110, 0.18)',
],
}
: {
opacity: 0,
scale: 0.92,
boxShadow: '0 0 0 0 rgba(0,0,0,0)',
}
}
transition={
isSelected
? {
duration: 1.6,
repeat: Number.POSITIVE_INFINITY,
ease: 'easeInOut',
}
: { duration: 0.22, ease: 'easeOut' }
}
className="pointer-events-none absolute inset-0 rounded-full"
/>
<motion.div
layout
animate={
isSelected && !showLockedState
? {
y: [-1, -3, -1],
scale: [1.04, 1.1, 1.04],
filter: [
'drop-shadow(0 4px 6px rgba(0,0,0,0.22))',
'drop-shadow(0 7px 10px rgba(245, 200, 107, 0.28))',
'drop-shadow(0 4px 6px rgba(0,0,0,0.22))',
],
}
: {
y: 0,
scale: 1,
filter: showLockedState
? 'none'
: 'drop-shadow(0 3px 5px rgba(0,0,0,0.34))',
}
}
transition={{ type: 'spring', stiffness: 380, damping: 24 }}
className="relative z-10"
>
<motion.img
src={chip.src}
alt={`chip-${chip.amount}`}
draggable={false}
className="h-design-32 w-design-32 object-contain"
/>
{showLockedState && (
<img
src={chipLock}
alt="chip-locked"
draggable={false}
className="pointer-events-none absolute left-1/2 top-1/2 z-40 h-design-25 w-design-25 -translate-x-1/2 -translate-y-1/2 object-contain"
/>
)}
<span className="pointer-events-none absolute inset-x-0 top-1/2 z-30 -translate-y-1/2 text-center text-design-7 font-black leading-none tracking-[0.03em] text-white [text-shadow:0_1px_0_rgba(255,255,255,0.6),0_2px_3px_rgba(0,0,0,0.72),0_0_6px_rgba(255,255,255,0.22)]">
{chip.valueLabel}
</span>
</motion.div>
</motion.button>
)
})}
</SmartBackground>
</SmartBackground>
<SmartBackground
src={mobileAddReduceBg}
size="100% 100%"
className="relative z-20 -ml-design-6 flex h-full w-design-103 shrink-0 items-center justify-center gap-design-6 bg-center bg-no-repeat px-design-9 text-design-13 font-bold"
>
<button
type="button"
disabled={!acceptingBets || !canDecreaseBetQuantity}
onClick={onDecreaseBetQuantity}
className={cn(
'flex h-design-24 w-design-24 shrink-0 items-center justify-center',
acceptingBets && canDecreaseBetQuantity
? 'cursor-pointer'
: 'cursor-not-allowed opacity-50',
)}
>
<SmartImage
src={add}
alt="decrease"
className="h-design-22 w-design-22"
/>
</button>
<div className="flex h-design-24 w-design-34 items-center justify-center rounded-sm bg-[#091118]">
{selectedBetQuantityLabel}
</div>
<button
type="button"
disabled={!acceptingBets}
onClick={onIncreaseBetQuantity}
className={cn(
'flex h-design-24 w-design-24 shrink-0 items-center justify-center',
acceptingBets
? 'cursor-pointer'
: 'cursor-not-allowed opacity-50',
)}
>
<SmartImage
src={reduce}
alt="increase"
className="h-design-22 w-design-22"
/>
</button>
</SmartBackground>
</div>
<div className="flex h-design-40 w-full items-center gap-design-3">
<SmartBackground
as="button"
type="button"
src={leftBottomBg}
size="100% 100%"
onClick={() => setModalOpen('desktopPeriodHistory', true)}
aria-label={t('gameDesktop.history.title')}
className="relative z-20 flex h-full w-design-60 shrink-0 items-center justify-center bg-center bg-no-repeat px-design-6 text-center !text-design-8 font-bold leading-[1.15] text-[#D5FBFF] transition-[filter] duration-200 hover:brightness-125 focus-visible:ring-2 focus-visible:ring-[#4FEAFF]"
>
{t('gameDesktop.history.title')}
</SmartBackground>
<SmartBackground
src={mobileTotalBg}
size="100% 100%"
className="relative z-10 flex h-full min-w-0 flex-[0.9] items-center justify-center gap-design-14 bg-center bg-no-repeat px-design-8 text-design-9 font-bold"
>
<div className="flex flex-col items-center gap-design-3 leading-none">
<div>
{t('gameDesktop.control.selected')}:
<span className="text-[#FF5A5A]">{selectedCountLabel}</span>/
{maxSelectionCountLabel}
</div>
<div className="flex items-center gap-design-4">
<span>{t('gameDesktop.control.totalBet')}:</span>
<SmartImage
className="h-design-13 w-design-13"
src={diamond}
alt="diamond"
/>
<span>{totalBetAmountLabel}</span>
</div>
</div>
</SmartBackground>
<SmartBackground
src={controlBg}
size="100% 100%"
className={cn(
'desktop-control-actions !ml-[calc(var(--design-unit)*-12)] relative z-20 flex h-full min-w-0 flex-1 items-center overflow-hidden bg-center bg-no-repeat pl-design-4',
)}
style={
!actionsEnabled
? {
WebkitFilter: 'grayscale(100%)',
filter: 'grayscale(100%)',
}
: undefined
}
>
{ACTION_OPTIONS.map(({ id, labelKey, Icon, bg }) => {
const isClicked = clickedId === id
const isHiding = hidingId === id
const showBg = actionsEnabled && (isClicked || isHiding)
return (
<motion.button
key={id}
type="button"
disabled={!actionsEnabled}
onClick={() => handleActionClick(id)}
whileHover={actionsEnabled ? { y: -1, scale: 1.01 } : undefined}
whileTap={actionsEnabled ? { scale: 0.96 } : undefined}
className={cn(
'relative flex h-full min-w-0 flex-1 items-center justify-center overflow-hidden',
actionsEnabled ? 'cursor-pointer' : 'cursor-not-allowed',
id === 'auto-spin' &&
'-translate-x-[calc(var(--design-unit)*1.5)]',
)}
>
{showBg && (
<SmartBackground
as={motion.span}
initial={{ opacity: 0 }}
animate={isClicked ? { opacity: 1 } : { opacity: 0 }}
transition={{
duration: isClicked ? 0.15 : 0.18,
ease: 'easeOut',
}}
src={bg}
className="pointer-events-none absolute inset-0 h-full bg-center bg-no-repeat"
size={
id === 'clear'
? '110% 90%'
: id === 'repeat'
? '98% 80%'
: '96% 80%'
}
/>
)}
<motion.div
animate={
showBg
? {
y: -1,
scale: 1.01,
}
: {
y: 0,
scale: 1,
}
}
transition={{
duration: showBg ? 0.22 : 0.18,
ease: 'easeOut',
}}
className="relative z-10 flex flex-col items-center justify-center"
>
<Icon
size={14}
strokeWidth={2.2}
className={showBg ? 'text-[#D9FEFF]' : 'text-[#37D5CB]'}
/>
<div className="mt-design-4 text-design-8 leading-none">
{t(labelKey)}
</div>
</motion.div>
</motion.button>
)
})}
</SmartBackground>
</div>
<SmartBackground
as={motion.button}
src={isConfirmWarning ? confirmRedBg : mobileConfirmBg}
size="100% 100%"
type="button"
onClick={handleConfirmClick}
whileHover={isConfirmClickable ? { scale: 1.005 } : undefined}
whileTap={isConfirmClickable ? { scale: 0.97 } : undefined}
animate={
confirmState === 'ready'
? {
scale: [1, 1.018, 1],
filter: [
'drop-shadow(0 0 0 rgba(0,0,0,0))',
'drop-shadow(0 0 12px rgba(245,200,107,0.72))',
'drop-shadow(0 0 0 rgba(0,0,0,0))',
],
}
: confirmState === 'idle'
? { scale: 1, filter: 'grayscale(.95)' }
: { scale: 1, filter: 'none' }
}
transition={
confirmState === 'ready'
? {
duration: 1.2,
repeat: Number.POSITIVE_INFINITY,
ease: 'easeInOut',
}
: { duration: 0.2, ease: 'easeOut' }
}
style={
confirmState === 'idle'
? {
WebkitFilter: 'grayscale(.95)',
filter: 'grayscale(.95)',
}
: undefined
}
className={cn(
'relative z-10 flex h-design-39 w-full shrink-0 items-center justify-center overflow-hidden bg-center bg-no-repeat text-design-16 font-black tracking-[0.04em]',
isConfirmClickable ? 'cursor-pointer' : 'cursor-not-allowed',
)}
>
{confirmClicked && (
<SmartBackground
as={motion.span}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
className="pointer-events-none absolute inset-0 bg-center bg-no-repeat"
src={isConfirmWarning ? confirmRedBg : mobileConfirmBg}
size="100% 100%"
/>
)}
<motion.span
animate={
confirmState === 'ready'
? {
opacity: confirmClicked ? 0 : 1,
y: confirmClicked ? 2 : [0, -1, 0],
textShadow: [
'0 0 8px rgba(255,238,173,0.72), 0 0 16px rgba(255,214,96,0.4)',
'0 0 12px rgba(255,252,220,0.95), 0 0 24px rgba(255,214,96,0.8)',
'0 0 8px rgba(255,238,173,0.72), 0 0 16px rgba(255,214,96,0.4)',
],
}
: {
opacity: confirmClicked ? 0 : 1,
y: confirmClicked ? 2 : 0,
textShadow: isConfirmWarning
? '0 0 8px rgba(255,206,206,0.55)'
: '0 0 8px rgba(210,255,255,0.38)',
}
}
transition={
confirmState === 'ready'
? {
duration: 1.2,
repeat: Number.POSITIVE_INFINITY,
ease: 'easeInOut',
}
: { duration: 0.15 }
}
className={cn('relative z-10', isConfirmWarning && 'text-[#FFF1F1]')}
>
{confirmLabel}
</motion.span>
</SmartBackground>
</div>
)
}