- 将多个组件中的背景图片样式替换为 SmartBackground 组件 - 在 CenterModal 中新增 isShowClose 属性控制关闭按钮显示 - 新增 DesktopAutoSettingModal 组件实现自动设置功能 - 新增 DesktopProceduresModal 和 DesktopWithdrawTopupModal 组件 - 配置 shadcn/ui 并集成 Geist 字体和动画库 - 更新 CSS 样式添加暗色主题变量和输入框样式 - 修复 utils 导入路径错误问题
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
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 Bet:150</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>
|
||
)
|
||
}
|