Files
36-character-flower/src/features/game/components/desktop/desktop-control.tsx
JiaJun 7622d4121f feat(game): 重构游戏组件并新增自动设置模态框
- 将多个组件中的背景图片样式替换为 SmartBackground 组件
- 在 CenterModal 中新增 isShowClose 属性控制关闭按钮显示
- 新增 DesktopAutoSettingModal 组件实现自动设置功能
- 新增 DesktopProceduresModal 和 DesktopWithdrawTopupModal 组件
- 配置 shadcn/ui 并集成 Geist 字体和动画库
- 更新 CSS 样式添加暗色主题变量和输入框样式
- 修复 utils 导入路径错误问题
2026-05-08 17:56:06 +08:00

361 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { motion } from 'motion/react'
import { useCallback, useState } from 'react'
import add from '@/assets/game/add.webp'
import arrow from '@/assets/game/arrow.webp'
import chipBg from '@/assets/game/chip-bg.webp'
import chipLineBg from '@/assets/game/chip-line-bg.webp'
import confirmBg from '@/assets/game/confirm-bg.png'
import controlBg from '@/assets/game/control-bg.png'
import leftBottomBg from '@/assets/game/left-bg.webp'
import reduce from '@/assets/game/reduce.webp'
import totalBg from '@/assets/game/total-bg.webp'
import { SmartBackground } from '@/components/smart-background.tsx'
import { SmartImage } from '@/components/smart-image.tsx'
import { ACTION_OPTIONS, CHIP_OPTIONS } from '@/constants'
import { cn } from '@/lib/utils'
export function DesktopControl() {
const [chips, setChips] = useState(CHIP_OPTIONS)
const [selectedChipId, setSelectedChipId] = useState(
CHIP_OPTIONS[CHIP_OPTIONS.length - 1]?.id ?? '',
)
const [clickedId, setClickedId] = useState<string | null>(null)
const [hidingId, setHidingId] = useState<string | null>(null)
const [confirmClicked, setConfirmClicked] = useState(false)
const selectedChip =
chips.find((chip) => chip.id === selectedChipId) ?? CHIP_OPTIONS[0]
const handleChipClick = (chipId: string) => {
setSelectedChipId(chipId)
setChips((current) => {
const next = [...current]
const index = next.findIndex((chip) => chip.id === chipId)
if (index === -1 || index === next.length - 1) {
return next
}
const [selected] = next.splice(index, 1)
next.push(selected)
return next
})
}
const handleActionClick = useCallback((id: string) => {
setClickedId(id)
setTimeout(() => {
setClickedId(null)
setHidingId(id)
setTimeout(() => {
setHidingId(null)
}, 180)
}, 200)
}, [])
const handleConfirmClick = useCallback(() => {
setConfirmClicked(true)
setTimeout(() => {
setConfirmClicked(false)
}, 200)
}, [])
return (
<div
className={
'flex h-design-100 w-full items-center gap-design-12 overflow-hidden'
}
>
<SmartBackground
src={leftBottomBg}
size="100% 100%"
className={
'flex flex-col items-center gap-1 justify-center h-full w-design-110 shrink-0 bg-center bg-no-repeat'
}
>
<div className={'flex flex-col items-center justify-center'}>
<div>TREBD</div>
<div>MAP</div>
</div>
<SmartImage
src={arrow}
alt={`animal`}
className={'w-design-20 h-design-10'}
/>
</SmartBackground>
<SmartBackground
src={chipBg}
size="100% 100%"
className={
'desktop-control-chip relative z-10 flex h-full w-design-710 shrink-0 items-center gap-design-10 pl-design-20 pr-design-40 bg-center bg-no-repeat'
}
>
<SmartBackground
src={chipLineBg}
size="100% 100%"
className={
'flex min-w-0 flex-1 items-center gap-design-10 overflow-visible'
}
>
{chips.map((chip) => {
const isSelected = chip.id === selectedChipId
return (
<motion.button
key={chip.id}
layout
type="button"
onClick={() => handleChipClick(chip.id)}
whileTap={{ scale: 0.94 }}
transition={{
layout: {
type: 'spring',
stiffness: 420,
damping: 32,
},
duration: 0.18,
}}
className={
'relative flex h-design-70 w-design-70 shrink-0 cursor-pointer items-center justify-center rounded-full'
}
>
<motion.span
animate={
isSelected
? {
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)*-4)] left-1/2 h-design-16 w-design-46 -translate-x-1/2 rounded-full bg-[rgba(0,0,0,0.48)] blur-[6px]'
}
/>
<motion.span
animate={
isSelected
? {
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 8px rgba(245, 200, 107, 0.44), 0 0 18px rgba(245, 200, 107, 0.22), inset 0 0 8px rgba(255, 214, 110, 0.18)',
'0 0 0 1px rgba(245, 200, 107, 0.96), 0 0 14px rgba(245, 200, 107, 0.78), 0 0 28px rgba(245, 200, 107, 0.42), inset 0 0 12px rgba(255, 214, 110, 0.32)',
'0 0 0 1px rgba(245, 200, 107, 0.68), 0 0 8px rgba(245, 200, 107, 0.44), 0 0 18px rgba(245, 200, 107, 0.22), inset 0 0 8px 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
animate={
isSelected
? {
y: [-1, -3, -1],
scale: [1.02, 1.06, 1.02],
filter: [
'drop-shadow(0 8px 10px rgba(0,0,0,0.18))',
'drop-shadow(0 10px 14px rgba(245, 200, 107, 0.22))',
'drop-shadow(0 8px 10px rgba(0,0,0,0.18))',
],
}
: {
y: 0,
scale: 1,
filter:
'drop-shadow(0 6px 10px rgba(0,0,0,0.34)) drop-shadow(0 2px 4px rgba(255,255,255,0.08))',
}
}
transition={{ type: 'spring', stiffness: 380, damping: 24 }}
className={'relative z-[1]'}
>
<motion.img
src={chip.src}
alt={`chip-${chip.value}`}
draggable={false}
className={'h-design-70 w-design-70 object-contain'}
/>
</motion.div>
</motion.button>
)
})}
</SmartBackground>
<div
className={
'flex h-design-50 shrink-0 items-center rounded-md bg-[#091118] box-border px-design-2 py-design-3'
}
>
<SmartImage
src={add}
alt={`add`}
className={'w-design-40 h-design-40'}
/>
<div
className={'w-design-80 h-full flex items-center justify-center'}
>
{selectedChip.value}
</div>
<SmartImage
src={reduce}
alt={`reduce`}
className={'w-design-40 h-design-40'}
/>
</div>
</SmartBackground>
<SmartBackground
src={totalBg}
size="100% 100%"
className={
'desktop-control-total relative flex flex-col items-center justify-center z-10 h-full w-design-435 shrink-0 bg-center bg-no-repeat'
}
>
<div>SELECTED:3/5</div>
<div>Total Bet150</div>
</SmartBackground>
<SmartBackground
src={controlBg}
size="100% 100%"
className={cn(
'desktop-control-actions relative z-10 flex h-full w-design-385 shrink-0 items-center bg-center bg-no-repeat pl-design-15',
)}
>
{ACTION_OPTIONS.map(({ id, label, Icon, bg }) => {
const isClicked = clickedId === id
const isHiding = hidingId === id
const showBg = isClicked || isHiding
return (
<motion.button
key={id}
type="button"
onClick={() => handleActionClick(id)}
whileHover={{ y: -1, scale: 1.01 }}
whileTap={{ scale: 0.96 }}
className={cn(
'relative flex h-full flex-1 cursor-pointer items-center justify-center overflow-hidden',
{ '-translate-x-1.5': id === 'auto-spin' },
)}
>
{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={cn(
'pointer-events-none absolute bg-center inset-0 bg-no-repeat h-design-100',
)}
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={20}
strokeWidth={2.2}
className={showBg ? 'text-[#D9FEFF]' : 'text-[#37D5CB]'}
/>
<div className={'mt-design-6 text-design-14 leading-none'}>
{label}
</div>
</motion.div>
</motion.button>
)
})}
</SmartBackground>
<SmartBackground
as={motion.button}
src={confirmBg}
size="100% 100%"
type="button"
onClick={handleConfirmClick}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.96 }}
className={cn(
'relative z-10 h-full w-design-260 shrink-0 cursor-pointer bg-center bg-no-repeat flex items-center justify-center text-design-32 font-bold',
)}
>
{confirmClicked && (
<SmartBackground
as={motion.span}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="pointer-events-none absolute inset-0 bg-center bg-no-repeat"
src={confirmBg}
size="100% 100%"
/>
)}
<motion.span
animate={confirmClicked ? { opacity: 0, y: 2 } : { opacity: 1, y: 0 }}
transition={{ duration: 0.15 }}
className="relative"
>
confirm
</motion.span>
</SmartBackground>
</div>
)
}