From 6ac42cf35e9344cb8aeb9519293232da7d07fe49 Mon Sep 17 00:00:00 2001 From: JiaJun <2394389886@qq.com> Date: Mon, 18 May 2026 18:01:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=B7=BB=E5=8A=A0=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E6=A1=8C=E9=9D=A2=E7=BB=84=E4=BB=B6=E7=9A=84=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=E6=8F=90=E7=A4=BA=E5=92=8C=E5=8A=A8=E7=94=BB=E6=95=88?= =?UTF-8?q?=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在动物游戏中添加余额不足和选择限制的警告提示 - 为警告状态添加震动动画和视觉反馈效果 - 实现倒计时警告状态的动画和样式变化 - 添加胜利和失败状态的历史记录显示 - 为筹码和操作按钮添加禁用状态的视觉效果 - 实现用户信息和流程按钮的交互功能 - 添加自动设置弹窗的打开功能 - 优化游戏阶段切换时的选择清空逻辑 - 添加剩余时间变化的回调函数支持 - 为历史记录添加中奖状态的颜色标识 --- src/assets/game/chip-lock.webp | Bin 0 -> 16300 bytes src/assets/lottie/down5.json | 769 ++++++++++++++++++ .../components/desktop/desktop-animal.tsx | 172 +++- .../components/desktop/desktop-control.tsx | 75 +- .../components/desktop/desktop-countdown.tsx | 10 +- .../desktop/desktop-game-history.tsx | 32 +- .../components/desktop/desktop-header.tsx | 26 +- .../components/desktop/desktop-status.tsx | 52 +- .../game/hooks/use-game-control-vm.ts | 21 +- .../game/hooks/use-game-history-vm.ts | 5 +- src/locales/en-US/common.ts | 4 + src/locales/id-ID/common.ts | 4 + src/locales/ms-MY/common.ts | 4 + src/locales/zh-CN/common.ts | 4 + src/store/game/game-round-store.ts | 38 +- 15 files changed, 1125 insertions(+), 91 deletions(-) create mode 100644 src/assets/game/chip-lock.webp create mode 100644 src/assets/lottie/down5.json diff --git a/src/assets/game/chip-lock.webp b/src/assets/game/chip-lock.webp new file mode 100644 index 0000000000000000000000000000000000000000..34e14090dadbd6cdbbc3600be6b55cc8e2e6e1b9 GIT binary patch literal 16300 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, -) { - return Object.entries(selectionByCell) - .map(([cellId, value]) => ({ - 字花: String(cellId).padStart(2, '0'), - 筹码: value.amount, - })) - .sort((left, right) => Number(left.字花) - Number(right.字花)) +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 @@ -72,6 +83,7 @@ export function DesktopAnimal({ }: DesktopAnimalProps) { const { t } = useTranslation() const authStatus = useAuthStore((state) => state.status) + const currentUser = useAuthStore((state) => state.currentUser) const markSoundPlaybackUnlocked = useAudioStore( (state) => state.markSoundPlaybackUnlocked, ) @@ -87,6 +99,7 @@ export function DesktopAnimal({ (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, @@ -97,10 +110,15 @@ export function DesktopAnimal({ const [marqueeId, setMarqueeId] = useState(() => 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>( (accumulator, selection) => { @@ -150,30 +168,48 @@ export function DesktopAnimal({ } if (isSelectedCell(animalId)) { - const nextSelectionByCell = { ...selectionByCell } - delete nextSelectionByCell[animalId] - console.log('已选', formatSelectedLog(nextSelectionByCell)) removeSelectionsForCell(animalId) return } if (selectedCellCount >= maxSelectionCount) { + setCellWarning({ + cellId: animalId, + type: 'limit', + }) + return + } + + if (totalBetAmount + (activeChip?.amount ?? 0) > balance) { + setCellWarning({ + cellId: animalId, + type: 'balance', + }) return } - console.log( - '已选', - formatSelectedLog({ - ...selectionByCell, - [animalId]: { - amount: activeChip?.amount ?? 0, - count: 1, - }, - }), - ) 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) @@ -208,13 +244,41 @@ export function DesktopAnimal({ const hasPlacedSelection = Boolean(selectionMeta) const isActive = item.id === activeId || hasPlacedSelection const isMarqueeActive = showStandbyState && item.id === marqueeId + const warningType = + cellWarning?.cellId === item.id ? cellWarning.type : null + const showCellWarning = warningType !== null + const warningLabel = + warningType === 'balance' + ? t('gameDesktop.animal.insufficientBalanceRecharge') + : t('gameDesktop.animal.selectionLimitReached') return ( - + ) })} diff --git a/src/features/game/components/desktop/desktop-control.tsx b/src/features/game/components/desktop/desktop-control.tsx index a86c2e6..ae1e838 100644 --- a/src/features/game/components/desktop/desktop-control.tsx +++ b/src/features/game/components/desktop/desktop-control.tsx @@ -5,6 +5,7 @@ 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 chipLock from '@/assets/game/chip-lock.webp' import confirmBg from '@/assets/game/confirm-bg.webp' import confirmRedBg from '@/assets/game/confirm-red-bg.png' import controlBg from '@/assets/game/control-bg.png' @@ -20,6 +21,8 @@ import { cn } from '@/lib/utils' export function DesktopControl() { const { t } = useTranslation() const { + acceptingBets, + actionsEnabled, canClear, chips, confirmLabel, @@ -29,6 +32,7 @@ export function DesktopControl() { onChipSelect, onConfirm, onClearSelections, + onOpenAutoSetting, onRepeatSelections, selectedChipAmountLabel, selectedChipId, @@ -40,11 +44,19 @@ export function DesktopControl() { const [confirmClicked, setConfirmClicked] = useState(false) const handleChipClick = (chipId: string) => { + if (!acceptingBets) { + return + } + onChipSelect(chipId) } const handleActionClick = useCallback( (id: string) => { + if (!actionsEnabled) { + return + } + if (id === 'clear' && canClear) { onClearSelections() } @@ -53,6 +65,10 @@ export function DesktopControl() { onRepeatSelections() } + if (id === 'auto-spin') { + onOpenAutoSetting() + } + setClickedId(id) setTimeout(() => { setClickedId(null) @@ -62,7 +78,13 @@ export function DesktopControl() { }, 180) }, 200) }, - [canClear, onClearSelections, onRepeatSelections], + [ + actionsEnabled, + canClear, + onClearSelections, + onOpenAutoSetting, + onRepeatSelections, + ], ) const handleConfirmClick = useCallback(() => { @@ -117,6 +139,7 @@ export function DesktopControl() { > {chips.map((chip) => { const isSelected = chip.id === selectedChipId + const showLockedState = !acceptingBets return ( handleChipClick(chip.id)} - whileTap={{ scale: 0.94 }} + disabled={showLockedState} + whileTap={showLockedState ? undefined : { scale: 0.94 }} transition={{ layout: { type: 'spring', @@ -134,12 +158,20 @@ export function DesktopControl() { duration: 0.26, }} className={ - 'relative flex h-design-70 w-design-70 shrink-0 cursor-pointer items-center justify-center rounded-full' + 'relative flex h-design-70 w-design-70 shrink-0 items-center justify-center rounded-full' + } + style={ + showLockedState + ? { + WebkitFilter: 'grayscale(100%)', + filter: 'grayscale(100%)', + } + : undefined } > + {showLockedState && ( + chip-locked + )} {ACTION_OPTIONS.map(({ id, labelKey, Icon, bg }) => { const isClicked = clickedId === id const isHiding = hidingId === id - const showBg = isClicked || isHiding + const showBg = actionsEnabled && (isClicked || isHiding) return ( handleActionClick(id)} - whileHover={{ y: -1, scale: 1.01 }} - whileTap={{ scale: 0.96 }} + whileHover={actionsEnabled ? { y: -1, scale: 1.01 } : undefined} + whileTap={actionsEnabled ? { scale: 0.96 } : undefined} className={cn( - 'relative flex h-full flex-1 cursor-pointer items-center justify-center overflow-hidden', + 'relative flex h-full flex-1 items-center justify-center overflow-hidden', + actionsEnabled ? 'cursor-pointer' : 'cursor-not-allowed', { '-translate-x-1.5': id === 'auto-spin' }, )} > diff --git a/src/features/game/components/desktop/desktop-countdown.tsx b/src/features/game/components/desktop/desktop-countdown.tsx index 687bb7b..490e83d 100644 --- a/src/features/game/components/desktop/desktop-countdown.tsx +++ b/src/features/game/components/desktop/desktop-countdown.tsx @@ -19,6 +19,7 @@ interface DesktopCountdownProps { initialMs?: number initialSeconds?: number onComplete?: () => void + onRemainingMsChange?: (remainingMs: number) => void } export function DesktopCountdown({ @@ -26,6 +27,7 @@ export function DesktopCountdown({ initialMs, initialSeconds, onComplete, + onRemainingMsChange, }: DesktopCountdownProps) { const initialCountdownMs = useMemo(() => { if (typeof initialMs === 'number') { @@ -43,7 +45,8 @@ export function DesktopCountdown({ useEffect(() => { setRemainingMs(initialCountdownMs) - }, [initialCountdownMs]) + onRemainingMsChange?.(initialCountdownMs) + }, [initialCountdownMs, onRemainingMsChange]) useEffect(() => { if (initialCountdownMs <= 0) { @@ -58,6 +61,7 @@ export function DesktopCountdown({ const nextRemainingMs = Math.max(0, initialCountdownMs - elapsedMs) setRemainingMs(nextRemainingMs) + onRemainingMsChange?.(nextRemainingMs) if (nextRemainingMs === 0) { window.clearInterval(timer) @@ -68,12 +72,12 @@ export function DesktopCountdown({ return () => { window.clearInterval(timer) } - }, [initialCountdownMs, onComplete]) + }, [initialCountdownMs, onComplete, onRemainingMsChange]) return (
diff --git a/src/features/game/components/desktop/desktop-game-history.tsx b/src/features/game/components/desktop/desktop-game-history.tsx index 2d53b68..7406444 100644 --- a/src/features/game/components/desktop/desktop-game-history.tsx +++ b/src/features/game/components/desktop/desktop-game-history.tsx @@ -80,23 +80,23 @@ export function DesktopGameHistory() { } >
- {item.statusLabel} + {item.isWin + ? t('gameDesktop.history.win') + : t('gameDesktop.history.lost')}
-
- - {t('gameDesktop.history.orderNo')}:{' '} - - {item.orderNo} -
{t('gameDesktop.history.roundId')}:{' '} @@ -109,12 +109,6 @@ export function DesktopGameHistory() { {item.numbersLabel}
-
- - {t('gameDesktop.history.settledAt')}:{' '} - - {item.createdAtLabel} -
{t('gameDesktop.history.totalPoolAmount')}:{' '} @@ -131,12 +125,6 @@ export function DesktopGameHistory() { {item.resultNumberLabel}
-
- - {t('gameDesktop.history.payout')}:{' '} - - {item.winAmountLabel} -
diff --git a/src/features/game/components/desktop/desktop-header.tsx b/src/features/game/components/desktop/desktop-header.tsx index faa6b82..007e701 100644 --- a/src/features/game/components/desktop/desktop-header.tsx +++ b/src/features/game/components/desktop/desktop-header.tsx @@ -173,6 +173,12 @@ export function DesktopHeader() { const connection = useGameSessionStore((state) => state.connection) const setModalOpen = useModalStore((state) => state.setModalOpen) const { currentLanguageLabel, currentLanguageOption } = useAppLanguage() + const handleOpenUserInfo = () => { + setModalOpen('desktopUserInfo', true) + } + const handleOpenProcedures = () => { + setModalOpen('desktopProcedures', true) + } const serverClockOffsetMs = useMemo(() => { if ( @@ -368,7 +374,11 @@ export function DesktopHeader() { 'flex items-center justify-center gap-design-30 pl-design-30 pr-design-10' } > -
+
+ -
+
+ ) : (
0 + const countdownClassName = useMemo( + () => + showWarningCountdown + ? 'text-[#FF5A5A] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(255,90,90,0.85),0_0_calc(var(--design-unit)*22)_rgba(255,90,90,0.32)]' + : 'text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(75,255,254,0.85),0_0_calc(var(--design-unit)*22)_rgba(75,255,254,0.32)]', + [showWarningCountdown], + ) return (
@@ -39,18 +51,38 @@ export function DesktopStatusLine() { {t('gameDesktop.status.limit')}: {limitLabel}
- - { - console.log('countdown finished') +
+ - +
+ +
+ +
{t('gameDesktop.status.roundId')}:{roundId} diff --git a/src/features/game/hooks/use-game-control-vm.ts b/src/features/game/hooks/use-game-control-vm.ts index 9860023..89dfba4 100644 --- a/src/features/game/hooks/use-game-control-vm.ts +++ b/src/features/game/hooks/use-game-control-vm.ts @@ -4,7 +4,11 @@ import { CHIP_IMAGE_MAP, CHIP_IMAGE_OPTIONS } from '@/constants' import { placeGameBet } from '@/features/game' import { notify } from '@/lib/notify' import { useAuthStore, useModalStore } from '@/store' -import { selectSelectionTotal, useGameRoundStore } from '@/store/game' +import { + selectSelectionTotal, + useGameRoundStore, + useGameSessionStore, +} from '@/store/game' type ConfirmState = 'idle' | 'ready' | 'insufficient' | 'submitting' @@ -71,6 +75,12 @@ export function useGameControlVm() { ) const selectChip = useGameRoundStore((state) => state.selectChip) const totalBetAmount = useGameRoundStore(selectSelectionTotal) + const connectionStatus = useGameSessionStore( + (state) => state.connection.status, + ) + const shouldConnectRealtime = useGameSessionStore( + (state) => state.shouldConnectRealtime, + ) const authStatus = useAuthStore((state) => state.status) const currentUser = useAuthStore((state) => state.currentUser) const setCurrentUser = useAuthStore((state) => state.setCurrentUser) @@ -99,6 +109,8 @@ export function useGameControlVm() { chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null const balance = parseBalance(currentUser?.coin) const hasSelections = selections.length > 0 + const hasEnteredGame = + shouldConnectRealtime && connectionStatus === 'connected' const hasInsufficientBalance = hasSelections && totalBetAmount > balance const confirmState: ConfirmState = isSubmitting ? 'submitting' @@ -235,7 +247,13 @@ export function useGameControlVm() { notify.success(t('commonUi.toast.repeatSelectionsRestored')) }, [restoreRecentSuccessfulSelections, round.phase, t]) + const handleOpenAutoSetting = useCallback(() => { + setModalOpen('desktopAutoSetting', true) + }, [setModalOpen]) + return { + acceptingBets: round.phase === 'betting', + actionsEnabled: hasEnteredGame, canClear: selections.length > 0, confirmLabel: confirmState === 'idle' @@ -250,6 +268,7 @@ export function useGameControlVm() { onChipSelect: selectChip, onConfirm: handleConfirm, onClearSelections: clearSelections, + onOpenAutoSetting: handleOpenAutoSetting, onRepeatSelections: handleRepeatSelections, maxSelectionCountLabel: maxSelectionCount, selectedChipAmountLabel: selectedChip?.valueLabel ?? '--', diff --git a/src/features/game/hooks/use-game-history-vm.ts b/src/features/game/hooks/use-game-history-vm.ts index 626437c..72ca48d 100644 --- a/src/features/game/hooks/use-game-history-vm.ts +++ b/src/features/game/hooks/use-game-history-vm.ts @@ -69,14 +69,17 @@ export function useGameHistoryVm() { i18n.resolvedLanguage ?? 'en-US', ), id: entry.order_no, + isWin: + entry.result_number !== null && + entry.numbers.includes(entry.result_number), numbersLabel: formatNumbers(entry.numbers), + numbers: entry.numbers, orderNo: entry.order_no, periodNo: entry.period_no, resultNumberLabel: entry.result_number === null ? '--' : String(entry.result_number).padStart(2, '0'), - statusLabel: entry.status, winAmountLabel: entry.win_amount, })), ), diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index a6c633a..7233a6f 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -362,12 +362,16 @@ export default { announcement: 'Announcement', }, animal: { + insufficientBalanceRecharge: 'Insufficient balance, please top up', loading: 'Loading', + selectionLimitReached: 'Selection limit exceeded', tapToEnter: 'Tap To Enter', getStart: 'Get Start', }, history: { title: 'History', + win: 'WIN', + lost: 'LOST', orderNo: 'Order No.', roundId: 'Round ID', numbers: 'Bet Numbers', diff --git a/src/locales/id-ID/common.ts b/src/locales/id-ID/common.ts index 1da1745..80c334a 100644 --- a/src/locales/id-ID/common.ts +++ b/src/locales/id-ID/common.ts @@ -361,12 +361,16 @@ export default { announcement: 'Pengumuman', }, animal: { + insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang', loading: 'Memuat', + selectionLimitReached: 'Melebihi pilihan yang diizinkan', tapToEnter: 'Ketuk Untuk Masuk', getStart: 'Mulai', }, history: { title: 'Riwayat', + win: 'WIN', + lost: 'LOST', orderNo: 'No. Order', roundId: 'ID Ronde', numbers: 'Nomor Taruhan', diff --git a/src/locales/ms-MY/common.ts b/src/locales/ms-MY/common.ts index 8aeae9d..007de9f 100644 --- a/src/locales/ms-MY/common.ts +++ b/src/locales/ms-MY/common.ts @@ -365,12 +365,16 @@ export default { announcement: 'Pengumuman', }, animal: { + insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai', loading: 'Memuatkan', + selectionLimitReached: 'Melebihi pilihan aksara yang dibenarkan', tapToEnter: 'Ketik Untuk Masuk', getStart: 'Mula', }, history: { title: 'Sejarah', + win: 'WIN', + lost: 'LOST', orderNo: 'No. Pesanan', roundId: 'ID Pusingan', numbers: 'Nombor Pertaruhan', diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 2c89542..68155a8 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -352,12 +352,16 @@ export default { announcement: '公告栏', }, animal: { + insufficientBalanceRecharge: '余额不足,请充值', loading: '加载中', + selectionLimitReached: '超过可选择字花', tapToEnter: '点击进入', getStart: '开始游戏', }, history: { title: '历史记录', + win: 'WIN', + lost: 'LOST', orderNo: '订单号', roundId: '期号', numbers: '下注号码', diff --git a/src/store/game/game-round-store.ts b/src/store/game/game-round-store.ts index b85bff8..a53a58e 100644 --- a/src/store/game/game-round-store.ts +++ b/src/store/game/game-round-store.ts @@ -203,20 +203,38 @@ export const useGameRoundStore = create()((set) => ({ }) }, setPhase: (phase) => { - set((state) => ({ - round: { - ...state.round, - phase, - }, - })) + set((state) => { + const nextState: Partial = { + round: { + ...state.round, + phase, + }, + } + + if (phase === 'settled') { + nextState.selections = [] + } + + return nextState + }) }, syncRound: (round) => { - set((state) => ({ - round: { + set((state) => { + const nextRound = { ...state.round, ...round, - }, - })) + } + + const nextState: Partial = { + round: nextRound, + } + + if (nextRound.phase === 'settled') { + nextState.selections = [] + } + + return nextState + }) }, upsertSelections: (selections) => { set({ selections })