feat(game): 优化界面组件

- 在国际化文件中添加钱包流水相关翻译项
- 在用户个人资料页面添加复制邀请链接功能
- 优化桌面端动物组件的视觉效果和动画参数
- 添加虚拟滚动功能到财务记录标签页提升性能
- 为桌面端控制面板添加投注数量调节按钮
- 更新消息模态框为通知列表和详情展示
- 在头部余额显示旁添加充值图标入口
This commit is contained in:
JiaJun
2026-05-28 11:34:02 +08:00
parent 2b2b86a73d
commit dbfe5701aa
130 changed files with 1357 additions and 597 deletions

View File

@@ -24,10 +24,16 @@ export interface GameAutoHostingStoreState {
balanceAfterBet: number | null
completedRounds: number
isHosting: boolean
lastIsJackpot: boolean | null
lastSingleWinAmount: number | null
lastSubmittedRoundId: string | null
rules: AutoHostingStopRules
selections: BetSelection[]
markRoundSubmitted: (roundId: string, balanceAfterBet: number | null) => void
recordBetWin: (input: {
isJackpot: boolean
singleWinAmount: number | null
}) => void
startHosting: (input: StartAutoHostingInput) => void
stopHosting: () => void
}
@@ -49,6 +55,8 @@ export const useGameAutoHostingStore = create<GameAutoHostingStoreState>()(
balanceAfterBet: null,
completedRounds: 0,
isHosting: false,
lastIsJackpot: null,
lastSingleWinAmount: null,
lastSubmittedRoundId: null,
rules: DEFAULT_AUTO_HOSTING_RULES,
selections: [],
@@ -65,11 +73,25 @@ export const useGameAutoHostingStore = create<GameAutoHostingStoreState>()(
}
})
},
recordBetWin: ({ isJackpot, singleWinAmount }) => {
set((state) => {
if (!state.isHosting) {
return state
}
return {
lastIsJackpot: isJackpot,
lastSingleWinAmount: singleWinAmount,
}
})
},
startHosting: ({ balanceAfterBet, rules, selections }) => {
set({
balanceAfterBet,
completedRounds: 0,
isHosting: true,
lastIsJackpot: null,
lastSingleWinAmount: null,
lastSubmittedRoundId: null,
rules,
selections: selections.map((selection) => ({
@@ -82,6 +104,8 @@ export const useGameAutoHostingStore = create<GameAutoHostingStoreState>()(
balanceAfterBet: null,
completedRounds: 0,
isHosting: false,
lastIsJackpot: null,
lastSingleWinAmount: null,
lastSubmittedRoundId: null,
selections: [],
})

View File

@@ -31,6 +31,8 @@ type GameRoundSlice = Pick<
| 'trends'
>
const MIN_BET_QUANTITY = 1
export type RevealAnimationPhase = 'idle' | 'spinning' | 'stopping' | 'result'
export type RewardAnimationType = 'none' | 'small' | 'big'
@@ -102,8 +104,49 @@ function resolveRecentActiveChipId(
DEFAULT_ACTIVE_CHIP_ID)
}
function normalizeBetQuantity(quantity: number) {
if (!Number.isFinite(quantity)) {
return MIN_BET_QUANTITY
}
return Math.max(MIN_BET_QUANTITY, Math.floor(quantity))
}
function getSelectionBetAmount(chip: Chip, quantity: number) {
return chip.amount * normalizeBetQuantity(quantity)
}
function syncSelectionsBetAmount(
selections: BetSelection[],
chipId: string,
amount: number,
) {
return selections.map((selection) => ({
...selection,
amount,
chipId,
}))
}
function resolveSelectionQuantity(
selections: BetSelection[],
chips: Chip[],
activeChipId: string,
) {
const chip = getChipById(chips, activeChipId)
const firstSelection = selections[0]
if (!chip || !firstSelection || chip.amount <= 0) {
return MIN_BET_QUANTITY
}
return normalizeBetQuantity(firstSelection.amount / chip.amount)
}
export interface GameRoundStoreState extends GameRoundSlice {
activeChipId: string
activeBetQuantity: number
adjustBetQuantity: (delta: number) => void
clearSelections: () => void
clearRewardAnimation: () => void
finishRevealAnimation: () => void
@@ -135,6 +178,7 @@ export interface GameRoundStoreState extends GameRoundSlice {
function createInitialRoundState(): GameRoundSlice & {
activeChipId: string
activeBetQuantity: number
recentSuccessfulSelections: BetSelection[]
revealAnimation: RevealAnimationState
} {
@@ -142,6 +186,7 @@ function createInitialRoundState(): GameRoundSlice & {
return {
activeChipId: DEFAULT_ACTIVE_CHIP_ID,
activeBetQuantity: MIN_BET_QUANTITY,
cells: snapshot.cells,
chips: snapshot.chips,
history: snapshot.history,
@@ -156,6 +201,30 @@ function createInitialRoundState(): GameRoundSlice & {
export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
...createInitialRoundState(),
adjustBetQuantity: (delta) => {
set((state) => {
const activeChip =
getChipById(state.chips, state.activeChipId) ??
state.chips.find((chip) => chip.isDefault) ??
state.chips[0]
const nextQuantity = normalizeBetQuantity(state.activeBetQuantity + delta)
if (!activeChip) {
return {
activeBetQuantity: nextQuantity,
}
}
return {
activeBetQuantity: nextQuantity,
selections: syncSelectionsBetAmount(
state.selections,
activeChip.id,
getSelectionBetAmount(activeChip, nextQuantity),
),
}
})
},
clearSelections: () => {
set({ selections: [] })
},
@@ -211,24 +280,29 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
})
},
hydrateRound: (snapshot) => {
set((state) => ({
activeChipId: getChipById(snapshot.chips, state.activeChipId)
set((state) => {
const nextActiveChipId = getChipById(snapshot.chips, state.activeChipId)
? state.activeChipId
: (snapshot.chips.find((chip) => chip.isDefault)?.id ??
snapshot.chips[0]?.id ??
DEFAULT_ACTIVE_CHIP_ID),
cells: snapshot.cells,
chips: snapshot.chips,
history: snapshot.history,
maxSelectionCount: snapshot.maxSelectionCount,
revealAnimation:
snapshot.round.phase === 'betting' || snapshot.round.phase === 'waiting'
? createIdleRevealAnimationPreservingReward(state.revealAnimation)
: state.revealAnimation,
round: snapshot.round,
selections: snapshot.selections,
trends: snapshot.trends,
}))
DEFAULT_ACTIVE_CHIP_ID)
return {
activeChipId: nextActiveChipId,
cells: snapshot.cells,
chips: snapshot.chips,
history: snapshot.history,
maxSelectionCount: snapshot.maxSelectionCount,
revealAnimation:
snapshot.round.phase === 'betting' ||
snapshot.round.phase === 'waiting'
? createIdleRevealAnimationPreservingReward(state.revealAnimation)
: state.revealAnimation,
round: snapshot.round,
selections: snapshot.selections,
trends: snapshot.trends,
}
})
},
placeBet: (cellId) => {
set((state) => {
@@ -256,7 +330,7 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
selections: [
...state.selections,
{
amount: activeChip.amount,
amount: getSelectionBetAmount(activeChip, state.activeBetQuantity),
cellId,
chipId: activeChip.id,
id: `bet-${cellId}-${state.selections.length + 1}-${Date.now()}`,
@@ -337,13 +411,28 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
return false
}
const nextActiveChipId = resolveRecentActiveChipId(
state.chips,
nextSelections,
state.activeChipId,
)
const nextActiveChip = getChipById(state.chips, nextActiveChipId)
const nextBetQuantity = resolveSelectionQuantity(
nextSelections,
state.chips,
nextActiveChipId,
)
set({
activeChipId: resolveRecentActiveChipId(
state.chips,
nextSelections,
state.activeChipId,
),
selections: nextSelections,
activeBetQuantity: nextBetQuantity,
activeChipId: nextActiveChipId,
selections: nextActiveChip
? syncSelectionsBetAmount(
nextSelections,
nextActiveChipId,
getSelectionBetAmount(nextActiveChip, nextBetQuantity),
)
: nextSelections,
})
return true
@@ -357,11 +446,21 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
},
selectChip: (chipId) => {
set((state) => {
if (!getChipById(state.chips, chipId)) {
const nextChip = getChipById(state.chips, chipId)
if (!nextChip) {
return state
}
return { activeChipId: chipId }
return {
activeBetQuantity: MIN_BET_QUANTITY,
activeChipId: chipId,
selections: syncSelectionsBetAmount(
state.selections,
chipId,
getSelectionBetAmount(nextChip, MIN_BET_QUANTITY),
),
}
})
},
setPhase: (phase) => {