Compare commits
5 Commits
5210b8309f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ee797ebb14 | |||
| 1308ca5a2c | |||
| 632a0267a4 | |||
| fba407c1bf | |||
| 0fa14ca407 |
BIN
pictures/微信图片_20260318170012_3_20.png
Normal file
BIN
pictures/微信图片_20260318170012_3_20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
BIN
pictures/微信图片_20260318170013_4_20.png
Normal file
BIN
pictures/微信图片_20260318170013_4_20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
BIN
pictures/微信图片_20260318170016_5_20.png
Normal file
BIN
pictures/微信图片_20260318170016_5_20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
BIN
pictures/微信图片_20260318170019_6_20.png
Normal file
BIN
pictures/微信图片_20260318170019_6_20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
BIN
pictures/微信图片_20260318170025_7_20.png
Normal file
BIN
pictures/微信图片_20260318170025_7_20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
@@ -7,6 +7,11 @@ export interface RoomItem {
|
|||||||
owner_id: string
|
owner_id: string
|
||||||
max_players: number
|
max_players: number
|
||||||
player_count: number
|
player_count: number
|
||||||
|
players?: Array<{
|
||||||
|
index: number
|
||||||
|
player_id: string
|
||||||
|
ready: boolean
|
||||||
|
}>
|
||||||
status: string
|
status: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@@ -58,8 +63,8 @@ export async function joinRoom(
|
|||||||
auth: AuthSession,
|
auth: AuthSession,
|
||||||
input: { roomId: string },
|
input: { roomId: string },
|
||||||
onAuthUpdated?: (next: AuthSession) => void,
|
onAuthUpdated?: (next: AuthSession) => void,
|
||||||
): Promise<void> {
|
): Promise<RoomItem> {
|
||||||
await authedRequest<Record<string, never> | RoomItem>({
|
return authedRequest<RoomItem>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: ROOM_JOIN_PATH,
|
path: ROOM_JOIN_PATH,
|
||||||
auth,
|
auth,
|
||||||
|
|||||||
139
src/state/active-room.ts
Normal file
139
src/state/active-room.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const DEFAULT_MAX_PLAYERS = 4
|
||||||
|
export type RoomStatus = 'waiting' | 'playing' | 'finished'
|
||||||
|
|
||||||
|
export interface RoomPlayerState {
|
||||||
|
index: number
|
||||||
|
playerId: string
|
||||||
|
ready: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleState {
|
||||||
|
name: string
|
||||||
|
isBloodFlow: boolean
|
||||||
|
hasHongZhong: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GamePlayerState {
|
||||||
|
playerId: string
|
||||||
|
index: number
|
||||||
|
ready: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EngineState {
|
||||||
|
phase: string
|
||||||
|
dealerIndex: number
|
||||||
|
currentTurn: number
|
||||||
|
needDraw: boolean
|
||||||
|
players: GamePlayerState[]
|
||||||
|
wall: string[]
|
||||||
|
lastDiscardTile: string | null
|
||||||
|
lastDiscardBy: string
|
||||||
|
pendingClaim: Record<string, unknown> | null
|
||||||
|
winners: string[]
|
||||||
|
scores: Record<string, number>
|
||||||
|
lastDrawPlayerId: string
|
||||||
|
lastDrawFromGang: boolean
|
||||||
|
lastDrawIsLastTile: boolean
|
||||||
|
huWay: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameState {
|
||||||
|
rule: RuleState | null
|
||||||
|
state: EngineState | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoomState {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
gameType: string
|
||||||
|
ownerId: string
|
||||||
|
maxPlayers: number
|
||||||
|
playerCount: number
|
||||||
|
status: RoomStatus | string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
game: GameState | null
|
||||||
|
players: RoomPlayerState[]
|
||||||
|
currentTurnIndex: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInitialRoomState(): RoomState {
|
||||||
|
return {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
gameType: 'chengdu',
|
||||||
|
ownerId: '',
|
||||||
|
maxPlayers: DEFAULT_MAX_PLAYERS,
|
||||||
|
playerCount: 0,
|
||||||
|
status: 'waiting',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
game: null,
|
||||||
|
players: [],
|
||||||
|
currentTurnIndex: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const activeRoomState = ref<RoomState>(createInitialRoomState())
|
||||||
|
|
||||||
|
export function destroyActiveRoomState(): void {
|
||||||
|
activeRoomState.value = createInitialRoomState()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetActiveRoomState(seed?: Partial<RoomState>): void {
|
||||||
|
destroyActiveRoomState()
|
||||||
|
if (!seed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeRoomState.value = {
|
||||||
|
...activeRoomState.value,
|
||||||
|
...seed,
|
||||||
|
players: seed.players ?? [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeActiveRoomState(next: RoomState): void {
|
||||||
|
if (activeRoomState.value.id && next.id && next.id !== activeRoomState.value.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeRoomState.value = {
|
||||||
|
...activeRoomState.value,
|
||||||
|
...next,
|
||||||
|
game: next.game ?? activeRoomState.value.game,
|
||||||
|
players: next.players.length > 0 ? next.players : activeRoomState.value.players,
|
||||||
|
currentTurnIndex:
|
||||||
|
next.currentTurnIndex !== null ? next.currentTurnIndex : activeRoomState.value.currentTurnIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hydrateActiveRoomFromSelection(input: {
|
||||||
|
roomId: string
|
||||||
|
roomName?: string
|
||||||
|
gameType?: string
|
||||||
|
ownerId?: string
|
||||||
|
maxPlayers?: number
|
||||||
|
playerCount?: number
|
||||||
|
status?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
players?: RoomPlayerState[]
|
||||||
|
currentTurnIndex?: number | null
|
||||||
|
}): void {
|
||||||
|
resetActiveRoomState({
|
||||||
|
id: input.roomId,
|
||||||
|
name: input.roomName ?? '',
|
||||||
|
gameType: input.gameType ?? 'chengdu',
|
||||||
|
ownerId: input.ownerId ?? '',
|
||||||
|
maxPlayers: input.maxPlayers ?? DEFAULT_MAX_PLAYERS,
|
||||||
|
playerCount: input.playerCount ?? 0,
|
||||||
|
status: input.status ?? 'waiting',
|
||||||
|
createdAt: input.createdAt ?? '',
|
||||||
|
updatedAt: input.updatedAt ?? '',
|
||||||
|
players: input.players ?? [],
|
||||||
|
currentTurnIndex: input.currentTurnIndex ?? null,
|
||||||
|
})
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ import { useRouter } from 'vue-router'
|
|||||||
import { AuthExpiredError, type AuthSession } from '../api/authed-request'
|
import { AuthExpiredError, type AuthSession } from '../api/authed-request'
|
||||||
import { createRoom, joinRoom, listRooms, type RoomItem } from '../api/mahjong'
|
import { createRoom, joinRoom, listRooms, type RoomItem } from '../api/mahjong'
|
||||||
import { getUserInfo, type UserInfo } from '../api/user'
|
import { getUserInfo, type UserInfo } from '../api/user'
|
||||||
|
import { hydrateActiveRoomFromSelection } from '../state/active-room'
|
||||||
|
import type { RoomPlayerState } from '../state/active-room'
|
||||||
import type { StoredAuth } from '../types/session'
|
import type { StoredAuth } from '../types/session'
|
||||||
import { clearAuth, readStoredAuth, writeStoredAuth } from '../utils/auth-storage'
|
import { clearAuth, readStoredAuth, writeStoredAuth } from '../utils/auth-storage'
|
||||||
|
|
||||||
@@ -121,6 +123,16 @@ function isMyRoom(room: RoomItem): boolean {
|
|||||||
return Boolean(currentUserId.value) && room.owner_id === currentUserId.value
|
return Boolean(currentUserId.value) && room.owner_id === currentUserId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapRoomPlayers(room: RoomItem): RoomPlayerState[] {
|
||||||
|
return (room.players ?? [])
|
||||||
|
.map((item, fallbackIndex) => ({
|
||||||
|
index: Number.isFinite(item.index) ? item.index : fallbackIndex,
|
||||||
|
playerId: item.player_id,
|
||||||
|
ready: Boolean(item.ready),
|
||||||
|
}))
|
||||||
|
.filter((item) => Boolean(item.playerId))
|
||||||
|
}
|
||||||
|
|
||||||
function toSession(source: StoredAuth): AuthSession {
|
function toSession(source: StoredAuth): AuthSession {
|
||||||
return {
|
return {
|
||||||
token: source.token,
|
token: source.token,
|
||||||
@@ -220,6 +232,18 @@ async function submitCreateRoom(): Promise<void> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
createdRoom.value = room
|
createdRoom.value = room
|
||||||
|
hydrateActiveRoomFromSelection({
|
||||||
|
roomId: room.room_id,
|
||||||
|
roomName: room.name,
|
||||||
|
gameType: room.game_type,
|
||||||
|
ownerId: room.owner_id,
|
||||||
|
maxPlayers: room.max_players,
|
||||||
|
playerCount: room.player_count,
|
||||||
|
status: room.status,
|
||||||
|
createdAt: room.created_at,
|
||||||
|
updatedAt: room.updated_at,
|
||||||
|
players: mapRoomPlayers(room),
|
||||||
|
})
|
||||||
quickJoinRoomId.value = room.room_id
|
quickJoinRoomId.value = room.room_id
|
||||||
createRoomForm.value.name = ''
|
createRoomForm.value.name = ''
|
||||||
showCreateModal.value = false
|
showCreateModal.value = false
|
||||||
@@ -251,18 +275,27 @@ async function handleJoinRoom(room?: { roomId?: string; roomName?: string }): Pr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetRoomName =
|
|
||||||
room?.roomName ?? rooms.value.find((item) => item.room_id === targetRoomId)?.name ?? ''
|
|
||||||
|
|
||||||
roomSubmitting.value = true
|
roomSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
await joinRoom(session, { roomId: targetRoomId }, syncAuth)
|
const joinedRoom = await joinRoom(session, { roomId: targetRoomId }, syncAuth)
|
||||||
quickJoinRoomId.value = targetRoomId
|
hydrateActiveRoomFromSelection({
|
||||||
successMessage.value = `已加入房间:${targetRoomId}`
|
roomId: joinedRoom.room_id,
|
||||||
|
roomName: joinedRoom.name,
|
||||||
|
gameType: joinedRoom.game_type,
|
||||||
|
ownerId: joinedRoom.owner_id,
|
||||||
|
maxPlayers: joinedRoom.max_players,
|
||||||
|
playerCount: joinedRoom.player_count,
|
||||||
|
status: joinedRoom.status,
|
||||||
|
createdAt: joinedRoom.created_at,
|
||||||
|
updatedAt: joinedRoom.updated_at,
|
||||||
|
players: mapRoomPlayers(joinedRoom),
|
||||||
|
})
|
||||||
|
quickJoinRoomId.value = joinedRoom.room_id
|
||||||
|
successMessage.value = `已加入房间:${joinedRoom.room_id}`
|
||||||
await refreshRooms()
|
await refreshRooms()
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/game/chengdu/${targetRoomId}`,
|
path: `/game/chengdu/${joinedRoom.room_id}`,
|
||||||
query: targetRoomName ? { roomName: targetRoomName } : undefined,
|
query: joinedRoom.name ? { roomName: joinedRoom.name } : undefined,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AuthExpiredError) {
|
if (error instanceof AuthExpiredError) {
|
||||||
@@ -298,6 +331,18 @@ async function enterCreatedRoom(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showCreatedModal.value = false
|
showCreatedModal.value = false
|
||||||
|
hydrateActiveRoomFromSelection({
|
||||||
|
roomId: createdRoom.value.room_id,
|
||||||
|
roomName: createdRoom.value.name,
|
||||||
|
gameType: createdRoom.value.game_type,
|
||||||
|
ownerId: createdRoom.value.owner_id,
|
||||||
|
maxPlayers: createdRoom.value.max_players,
|
||||||
|
playerCount: createdRoom.value.player_count,
|
||||||
|
status: createdRoom.value.status,
|
||||||
|
createdAt: createdRoom.value.created_at,
|
||||||
|
updatedAt: createdRoom.value.updated_at,
|
||||||
|
players: mapRoomPlayers(createdRoom.value),
|
||||||
|
})
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/game/chengdu/${createdRoom.value.room_id}`,
|
path: `/game/chengdu/${createdRoom.value.room_id}`,
|
||||||
query: { roomName: createdRoom.value.name },
|
query: { roomName: createdRoom.value.name },
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
server: {
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/v1': {
|
'/api/v1': {
|
||||||
target: 'http://127.0.0.1:8080',
|
target: 'http://127.0.0.1:19000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/api/v1/ws': {
|
'/api/v1/ws': {
|
||||||
target: 'ws://127.0.0.1:8080',
|
target: 'ws://127.0.0.1:19000',
|
||||||
ws: true,
|
ws: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user