feat: 新增首页和图片资源

This commit is contained in:
JiaJun
2026-04-24 18:02:42 +08:00
parent bd92f10b83
commit 9127a06d4a
179 changed files with 3424 additions and 101 deletions

1
src/store/auth/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './auth-store'

View File

@@ -0,0 +1,171 @@
import { create } from 'zustand'
import type {
BetSelection,
Chip,
GameBootstrapSnapshot,
GameCell,
HistoryEntry,
RoundPhase,
RoundSnapshot,
TrendEntry,
} from '@/features/game/shared'
import {
buildGameCellViewModels,
createMockGameBootstrapSnapshot,
DEFAULT_ACTIVE_CHIP_ID,
getChipById,
getRecentWinningCellIds,
getSelectionTotal,
groupSelectionsByCell,
} from '@/features/game/shared'
type GameRoundSlice = Pick<
GameBootstrapSnapshot,
'cells' | 'chips' | 'history' | 'round' | 'selections' | 'trends'
>
export interface GameRoundStoreState extends GameRoundSlice {
activeChipId: string
clearSelections: () => void
hydrateRound: (snapshot: GameRoundSlice) => void
placeBet: (cellId: number) => void
removeSelectionsForCell: (cellId: number) => void
selectChip: (chipId: string) => void
setPhase: (phase: RoundPhase) => void
syncRound: (round: Partial<RoundSnapshot>) => void
upsertSelections: (selections: BetSelection[]) => void
}
function createInitialRoundState(): GameRoundSlice & { activeChipId: string } {
const snapshot = createMockGameBootstrapSnapshot()
return {
activeChipId:
snapshot.chips.find((chip) => chip.isDefault)?.id ??
DEFAULT_ACTIVE_CHIP_ID,
cells: snapshot.cells,
chips: snapshot.chips,
history: snapshot.history,
round: snapshot.round,
selections: snapshot.selections,
trends: snapshot.trends,
}
}
export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
...createInitialRoundState(),
clearSelections: () => {
set({ selections: [] })
},
hydrateRound: (snapshot) => {
set((state) => ({
activeChipId: 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,
round: snapshot.round,
selections: snapshot.selections,
trends: snapshot.trends,
}))
},
placeBet: (cellId) => {
set((state) => {
const activeChip =
getChipById(state.chips, state.activeChipId) ??
state.chips.find((chip) => chip.isDefault) ??
state.chips[0]
if (!activeChip || state.round.phase !== 'betting') {
return state
}
return {
selections: [
...state.selections,
{
amount: activeChip.amount,
cellId,
chipId: activeChip.id,
id: `bet-${cellId}-${state.selections.length + 1}-${Date.now()}`,
placedAt: new Date().toISOString(),
source: 'local',
},
],
}
})
},
removeSelectionsForCell: (cellId) => {
set((state) => ({
selections: state.selections.filter(
(selection) => selection.cellId !== cellId,
),
}))
},
selectChip: (chipId) => {
set((state) => {
if (!getChipById(state.chips, chipId)) {
return state
}
return { activeChipId: chipId }
})
},
setPhase: (phase) => {
set((state) => ({
round: {
...state.round,
phase,
},
}))
},
syncRound: (round) => {
set((state) => ({
round: {
...state.round,
...round,
},
}))
},
upsertSelections: (selections) => {
set({ selections })
},
}))
export const selectActiveChip = (state: GameRoundStoreState): Chip | null =>
getChipById(state.chips, state.activeChipId) ??
state.chips.find((chip) => chip.isDefault) ??
state.chips[0] ??
null
export const selectBoardCells = (state: GameRoundStoreState) =>
buildGameCellViewModels({
cells: state.cells,
round: state.round,
selections: state.selections,
trends: state.trends,
})
export const selectCanPlaceBets = (state: GameRoundStoreState) =>
state.round.phase === 'betting'
export const selectRecentResults = (state: GameRoundStoreState) =>
getRecentWinningCellIds(state.history)
export const selectSelectionTotal = (state: GameRoundStoreState) =>
getSelectionTotal(state.selections)
export const selectSelectionsByCell = (state: GameRoundStoreState) =>
groupSelectionsByCell(state.selections)
export type GameRoundStore = typeof useGameRoundStore
export type GameRoundStoreData = Pick<
GameRoundStoreState,
'cells' | 'chips' | 'history' | 'round' | 'selections' | 'trends'
>
export type { BetSelection, GameCell, HistoryEntry, RoundSnapshot, TrendEntry }

View File

@@ -0,0 +1,131 @@
import { create } from 'zustand'
import type {
AnnouncementState,
ConnectionState,
ConnectionStatus,
DashboardState,
GameBootstrapSnapshot,
} from '@/features/game/shared'
import {
createMockGameBootstrapSnapshot,
getUnreadAnnouncementCount,
getVisibleAnnouncements,
} from '@/features/game/shared'
type GameSessionSlice = Pick<
GameBootstrapSnapshot,
'announcements' | 'connection' | 'dashboard'
>
export interface GameSessionStoreState extends GameSessionSlice {
dismissAnnouncement: (announcementId: string) => void
hydrateSession: (snapshot: GameSessionSlice) => void
markAnnouncementRead: (announcementId: string) => void
setConnectionLatency: (latencyMs: number | null) => void
setConnectionStatus: (status: ConnectionStatus) => void
syncConnection: (patch: Partial<ConnectionState>) => void
syncDashboard: (patch: Partial<DashboardState>) => void
}
function createInitialSessionState(): GameSessionSlice {
const snapshot = createMockGameBootstrapSnapshot()
return {
announcements: snapshot.announcements,
connection: snapshot.connection,
dashboard: snapshot.dashboard,
}
}
export const useGameSessionStore = create<GameSessionStoreState>()((set) => ({
...createInitialSessionState(),
dismissAnnouncement: (announcementId) => {
set((state) => ({
announcements: {
...state.announcements,
activeAnnouncementId:
state.announcements.activeAnnouncementId === announcementId
? (state.announcements.items.find(
(item) => item.id !== announcementId,
)?.id ?? null)
: state.announcements.activeAnnouncementId,
items: state.announcements.items.filter(
(item) => item.id !== announcementId,
),
},
}))
},
hydrateSession: (snapshot) => {
set(snapshot)
},
markAnnouncementRead: (announcementId) => {
set((state) => ({
announcements: {
...state.announcements,
items: state.announcements.items.map((item) =>
item.id === announcementId ? { ...item, isRead: true } : item,
),
},
}))
},
setConnectionLatency: (latencyMs) => {
set((state) => ({
connection: {
...state.connection,
latencyMs,
},
}))
},
setConnectionStatus: (status) => {
set((state) => ({
connection: {
...state.connection,
connectedAt:
status === 'connected'
? (state.connection.connectedAt ?? new Date().toISOString())
: state.connection.connectedAt,
status,
},
}))
},
syncConnection: (patch) => {
set((state) => ({
connection: {
...state.connection,
...patch,
},
}))
},
syncDashboard: (patch) => {
set((state) => ({
dashboard: {
...state.dashboard,
...patch,
},
}))
},
}))
export const selectActiveAnnouncement = (state: GameSessionStoreState) =>
state.announcements.items.find(
(item) => item.id === state.announcements.activeAnnouncementId,
) ?? null
export const selectIsConnectionHealthy = (state: GameSessionStoreState) =>
state.connection.status === 'connected' &&
(state.connection.latencyMs === null || state.connection.latencyMs < 150)
export const selectUnreadAnnouncementCount = (state: GameSessionStoreState) =>
getUnreadAnnouncementCount(state.announcements)
export const selectVisibleAnnouncements = (state: GameSessionStoreState) =>
getVisibleAnnouncements(state.announcements)
export type GameSessionStore = typeof useGameSessionStore
export type GameSessionStoreData = Pick<
GameSessionStoreState,
'announcements' | 'connection' | 'dashboard'
>
export type { AnnouncementState, ConnectionState, DashboardState }

2
src/store/game/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './game-round-store'
export * from './game-session-store'

2
src/store/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './auth'
export * from './game'