feat: 联调充值和提现接口

This commit is contained in:
JiaJun
2026-05-21 13:40:32 +08:00
parent 6ac42cf35e
commit 44c984d59e
51 changed files with 3830 additions and 1478 deletions

View File

@@ -1,17 +1,11 @@
import { TriangleAlert } from 'lucide-react'
import { motion } from 'motion/react'
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import diamondIcon from '@/assets/system/diamond.webp'
import { SmartImage } from '@/components/smart-image'
import { notify } from '@/lib/notify'
import { useAnimalVm } from '@/features/game/hooks/use-animal-vm'
import { cn } from '@/lib/utils'
import { useAudioStore, useAuthStore, useModalStore } from '@/store'
import {
selectSelectionTotal,
useGameRoundStore,
useGameSessionStore,
} from '@/store/game'
const animalModules = import.meta.glob('../../../../assets/animal/*.webp', {
eager: true,
@@ -30,42 +24,6 @@ const animalImageList = Object.entries(animalModules)
.filter((item) => item.id > 0)
.sort((left, right) => left.id - right.id)
function getNextMarqueeId(currentId: number | null) {
if (animalImageList.length === 0) {
return null
}
if (animalImageList.length === 1) {
return animalImageList[0]?.id ?? null
}
let nextId = currentId
while (nextId === currentId) {
nextId =
animalImageList[Math.floor(Math.random() * animalImageList.length)]?.id ??
currentId
}
return nextId
}
function parseBalance(value: string | number | null | undefined) {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : 0
}
if (typeof value !== 'string') {
return 0
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
type CellWarningType = 'balance' | 'limit'
interface DesktopAnimalProps {
activeId?: number | null
className?: string
@@ -82,155 +40,17 @@ export function DesktopAnimal({
onSelect,
}: DesktopAnimalProps) {
const { t } = useTranslation()
const authStatus = useAuthStore((state) => state.status)
const currentUser = useAuthStore((state) => state.currentUser)
const markSoundPlaybackUnlocked = useAudioStore(
(state) => state.markSoundPlaybackUnlocked,
)
const setModalOpen = useModalStore((state) => state.setModalOpen)
const activeChipId = useGameRoundStore((state) => state.activeChipId)
const chips = useGameRoundStore((state) => state.chips)
const clearSelections = useGameRoundStore((state) => state.clearSelections)
const maxSelectionCount = useGameRoundStore(
(state) => state.maxSelectionCount,
)
const placeBet = useGameRoundStore((state) => state.placeBet)
const removeSelectionsForCell = useGameRoundStore(
(state) => state.removeSelectionsForCell,
)
const selections = useGameRoundStore((state) => state.selections)
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
const connection = useGameSessionStore((state) => state.connection)
const requestRealtimeConnection = useGameSessionStore(
(state) => state.requestRealtimeConnection,
)
const shouldConnectRealtime = useGameSessionStore(
(state) => state.shouldConnectRealtime,
)
const [marqueeId, setMarqueeId] = useState<number | null>(() =>
getNextMarqueeId(null),
)
const [cellWarning, setCellWarning] = useState<{
cellId: number
type: CellWarningType
} | null>(null)
const activeChip = useMemo(
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
[activeChipId, chips],
)
const balance = parseBalance(currentUser?.coin)
const selectionByCell = useMemo(() => {
return selections.reduce<Record<number, { amount: number; count: number }>>(
(accumulator, selection) => {
const current = accumulator[selection.cellId] ?? { amount: 0, count: 0 }
accumulator[selection.cellId] = {
amount: current.amount + selection.amount,
count: current.count + 1,
}
return accumulator
},
{},
)
}, [selections])
const isRealtimeConnected = connection.status === 'connected'
const isRealtimeConnecting =
shouldConnectRealtime &&
(connection.status === 'connecting' || connection.status === 'reconnecting')
const showStandbyState = !shouldConnectRealtime || !isRealtimeConnected
const lockInteraction = showStandbyState
const isSelectedCell = (animalId: number) =>
Boolean(selectionByCell[animalId])
const selectedCellCount = Object.keys(selectionByCell).length
const handleStart = () => {
if (authStatus !== 'authenticated') {
notify.warning(t('commonUi.toast.loginRequired'))
setModalOpen('desktopLogin', true)
return
}
clearSelections()
markSoundPlaybackUnlocked()
requestRealtimeConnection()
}
const handleSelect = (animalId: number) => {
if (showStandbyState) {
return
}
if (onSelect) {
onSelect(animalId)
return
}
if (isSelectedCell(animalId)) {
removeSelectionsForCell(animalId)
return
}
if (selectedCellCount >= maxSelectionCount) {
setCellWarning({
cellId: animalId,
type: 'limit',
})
return
}
if (totalBetAmount + (activeChip?.amount ?? 0) > balance) {
setCellWarning({
cellId: animalId,
type: 'balance',
})
return
}
placeBet(animalId)
}
useEffect(() => {
if (cellWarning === null) {
return
}
const timerId = window.setTimeout(() => {
setCellWarning((currentWarning) =>
currentWarning?.cellId === cellWarning.cellId &&
currentWarning.type === cellWarning.type
? null
: currentWarning,
)
}, 1200)
return () => {
window.clearTimeout(timerId)
}
}, [cellWarning])
useEffect(() => {
if (!showStandbyState) {
setMarqueeId(null)
return
}
setMarqueeId((currentId) => getNextMarqueeId(currentId))
let timerId = 0
const loop = () => {
setMarqueeId((currentId) => getNextMarqueeId(currentId))
timerId = window.setTimeout(loop, 180 + Math.floor(Math.random() * 220))
}
timerId = window.setTimeout(loop, 220)
return () => {
window.clearTimeout(timerId)
}
}, [showStandbyState])
const animalIds = useMemo(() => animalImageList.map((item) => item.id), [])
const {
cellWarning,
handleSelect,
handleStart,
isRealtimeConnecting,
lockInteraction,
marqueeId,
selectionByCell,
showStandbyState,
} = useAnimalVm(animalIds, onSelect)
return (
<section