docs(game): 添加游戏模块数据

- 新增 useGameBoardVm 数据层实施说明文档
- 添加 36字花核心玩法与前端规则摘要
- 创建游戏模块数据与界面分层第一阶段实施稿
- 定义四层架构:api/dto、store、view-model hooks、ui层
- 规范 PC 与 Mobile 共享业务逻辑的改造方案
- 明确各层职责边界和组件改造顺序
This commit is contained in:
JiaJun
2026-05-09 17:52:30 +08:00
parent 7622d4121f
commit 6aaf90a6ac
28 changed files with 2635 additions and 258 deletions

View File

@@ -0,0 +1,42 @@
import { useMemo } from 'react'
import { CHIP_OPTIONS } from '@/constants'
import { selectSelectionTotal, useGameRoundStore } from '@/store/game'
const CHIP_IMAGE_MAP = new Map(
CHIP_OPTIONS.map((chip) => [chip.value, chip.src] as const),
)
export function useGameControlVm() {
const chips = useGameRoundStore((state) => state.chips)
const activeChipId = useGameRoundStore((state) => state.activeChipId)
const selections = useGameRoundStore((state) => state.selections)
const clearSelections = useGameRoundStore((state) => state.clearSelections)
const selectChip = useGameRoundStore((state) => state.selectChip)
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
const chipItems = useMemo(
() =>
chips.map((chip) => ({
amount: chip.amount,
id: chip.id,
isSelected: chip.id === activeChipId,
src: CHIP_IMAGE_MAP.get(chip.amount) ?? CHIP_OPTIONS[0]?.src ?? '',
valueLabel: String(chip.amount),
})),
[activeChipId, chips],
)
const selectedChip =
chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null
return {
canClear: selections.length > 0,
onChipSelect: selectChip,
onClearSelections: clearSelections,
selectedChipAmountLabel: selectedChip?.valueLabel ?? '--',
selectedChipId: activeChipId,
selectedCountLabel: `${selections.length}/5`,
totalBetAmountLabel: String(totalBetAmount),
chips: chipItems,
}
}

View File

@@ -0,0 +1,43 @@
import { useMemo } from 'react'
import { useGameRoundStore } from '@/store/game'
function formatSettledTime(iso: string) {
const date = new Date(iso)
if (Number.isNaN(date.getTime())) {
return '--'
}
return date.toLocaleString('zh-CN', {
hour12: false,
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
export function useGameHistoryVm() {
const history = useGameRoundStore((state) => state.history)
const items = useMemo(
() =>
history.map((entry) => ({
id: entry.roundId,
payoutMultiplierLabel: `${entry.payoutMultiplier}x`,
roundId: entry.roundId,
settledAtLabel: formatSettledTime(entry.settledAt),
statusLabel: 'settled',
totalPoolAmountLabel: entry.totalPoolAmount.toFixed(2),
winningCellIdLabel: String(entry.winningCellId),
})),
[history],
)
return {
emptyText: 'No history yet',
isEmpty: items.length === 0,
items,
}
}

View File

@@ -0,0 +1,59 @@
import { useMemo } from 'react'
import { getRoundCountdownMs } from '@/features/game/shared/selectors'
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
const PHASE_META = {
betting: {
description: '(Menerima Taruhan)',
label: 'OPEN',
toneClassName: 'text-[#78FF7F]',
},
locked: {
description: '(Taruhan Ditutup)',
label: 'LOCKED',
toneClassName: 'text-[#FFE375]',
},
revealing: {
description: '(Mengundi Hasil)',
label: 'DRAWING',
toneClassName: 'text-[#57E8FF]',
},
settled: {
description: '(Putaran Selesai)',
label: 'SETTLED',
toneClassName: 'text-[#FF9C6B]',
},
waiting: {
description: '(Menunggu Putaran Berikutnya)',
label: 'WAITING',
toneClassName: 'text-[#A7B6C7]',
},
} as const
export function useGameStatusVm() {
const cells = useGameRoundStore((state) => state.cells)
const round = useGameRoundStore((state) => state.round)
const trends = useGameRoundStore((state) => state.trends)
const dashboard = useGameSessionStore((state) => state.dashboard)
return useMemo(() => {
const oddsValue = cells[0]?.odds ?? '--'
const featuredTrend = trends.find(
(entry) => entry.cellId === dashboard.featuredCellId,
)
const phaseMeta = PHASE_META[round.phase]
return {
acceptingBets: round.phase === 'betting',
countdownMs: getRoundCountdownMs(round),
limitLabel: `${dashboard.tableLimitMin}-${dashboard.tableLimitMax}`,
oddsLabel: `1:${oddsValue}`,
phase: round.phase,
phaseDescription: phaseMeta.description,
phaseLabel: phaseMeta.label,
phaseToneClassName: phaseMeta.toneClassName,
roundId: round.id,
streakLabel: featuredTrend ? `X${featuredTrend.currentStreak}` : '--',
}
}, [cells, dashboard, round, trends])
}