Merge remote-tracking branch 'origin/dev'

# Conflicts:
#	src/views/ChengduGamePage.vue
This commit is contained in:
2026-03-24 09:13:16 +08:00
4 changed files with 521 additions and 73 deletions

View File

@@ -7,6 +7,11 @@ export interface RoomItem {
owner_id: string
max_players: number
player_count: number
players?: Array<{
index: number
player_id: string
ready: boolean
}>
status: string
created_at: string
updated_at: string
@@ -58,8 +63,8 @@ export async function joinRoom(
auth: AuthSession,
input: { roomId: string },
onAuthUpdated?: (next: AuthSession) => void,
): Promise<void> {
await authedRequest<Record<string, never> | RoomItem>({
): Promise<RoomItem> {
return authedRequest<RoomItem>({
method: 'POST',
path: ROOM_JOIN_PATH,
auth,

139
src/state/active-room.ts Normal file
View 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,
})
}

View File

@@ -3,6 +3,19 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import deskImage from '../assets/images/desk/desk_01.png'
import { readStoredAuth } from '../utils/auth-storage'
import type { AuthSession } from '../api/authed-request'
import { getUserInfo } from '../api/user'
import {
DEFAULT_MAX_PLAYERS,
activeRoomState,
destroyActiveRoomState,
mergeActiveRoomState,
resetActiveRoomState,
type GameState,
type RoomPlayerState,
type RoomState,
} from '../state/active-room'
import { readStoredAuth, writeStoredAuth } from '../utils/auth-storage'
const router = useRouter()
const route = useRoute()
@@ -20,35 +33,24 @@ const chosenMissingSuit = ref<'wan' | 'tiao' | 'tong'>('tiao')
const overlayState = ref<'missing' | 'action' | 'settlement' | null>('missing')
let clockTimer: number | undefined
const leaveRoomPending = ref(false)
const lastLeaveRoomRequestId = ref('')
const leaveHallAfterAck = ref(false)
const WS_BASE_URL = import.meta.env.VITE_GAME_WS_URL ?? 'ws://127.0.0.1:8080/ws'
const DEFAULT_MAX_PLAYERS = 4
type SeatKey = 'top' | 'right' | 'bottom' | 'left'
type TileSuit = 'wan' | 'tong' | 'tiao' | 'honor'
type MissingSuit = 'wan' | 'tiao' | 'tong'
type TileDirection = 'bottom' | 'top' | 'left' | 'right'
interface RoomPlayerState {
index: number
playerId: string
ready: boolean
}
interface RoomState {
id: string
name: string
ownerId: string
maxPlayers: number
status: string
players: RoomPlayerState[]
currentTurnIndex: number | null
}
interface ActionEventLike {
type?: unknown
status?: unknown
requestId?: unknown
request_id?: unknown
roomId?: unknown
room_id?: unknown
payload?: unknown
data?: unknown
}
@@ -98,7 +100,8 @@ const roomName = computed(() => {
})
const currentUserId = computed(() => {
const candidate = auth.value?.user?.id
const user = auth.value?.user as Record<string, unknown> | undefined
const candidate = user?.id ?? user?.userID ?? user?.user_id
if (typeof candidate === 'string') {
return candidate
}
@@ -116,20 +119,12 @@ const loggedInUserName = computed(() => {
return auth.value.user.nickname ?? auth.value.user.username ?? ''
})
const roomState = ref<RoomState>({
id: roomId.value,
name: roomName.value,
ownerId: '',
maxPlayers: DEFAULT_MAX_PLAYERS,
status: 'waiting',
players: [],
currentTurnIndex: null,
})
const roomState = activeRoomState
const isRoomFull = computed(() => {
return (
roomState.value.maxPlayers > 0 &&
roomState.value.players.length === roomState.value.maxPlayers
roomState.value.playerCount === roomState.value.maxPlayers
)
})
@@ -152,6 +147,16 @@ const seatViews = computed<SeatRenderItem[]>(() => {
}
const players = [...roomState.value.players].sort((a, b) => a.index - b.index)
const hasSelf = players.some((player) => player.playerId === currentUserId.value)
if (currentUserId.value && roomState.value.id && !hasSelf) {
// Fallback before WS full player list arrives: keep current player at bottom.
players.unshift({
index: 0,
playerId: currentUserId.value,
ready: false,
})
}
const me = players.find((player) => player.playerId === currentUserId.value) ?? null
const anchorIndex = me?.index ?? players[0]?.index ?? 0
const clockwiseSeatByDelta: SeatKey[] = ['bottom', 'left', 'top', 'right']
@@ -533,8 +538,16 @@ function missingSuitLabel(suit: MissingSuit): string {
}
function backHall(): void {
sendLeaveRoom()
void router.push('/hall')
if (leaveRoomPending.value) {
return
}
leaveHallAfterAck.value = true
const sent = sendLeaveRoom()
if (!sent) {
leaveHallAfterAck.value = false
pushWsMessage('[client] 退出房间失败:未发送请求')
}
}
function pushWsMessage(text: string): void {
@@ -580,6 +593,56 @@ function toStringOrEmpty(value: unknown): string {
return ''
}
function toSession(source: NonNullable<typeof auth.value>): AuthSession {
return {
token: source.token,
tokenType: source.tokenType,
refreshToken: source.refreshToken,
expiresIn: source.expiresIn,
}
}
function syncAuth(next: AuthSession): void {
if (!auth.value) {
return
}
auth.value = {
...auth.value,
token: next.token,
tokenType: next.tokenType ?? auth.value.tokenType,
refreshToken: next.refreshToken ?? auth.value.refreshToken,
expiresIn: next.expiresIn,
}
writeStoredAuth(auth.value)
}
async function ensureCurrentUserId(): Promise<void> {
if (currentUserId.value || !auth.value) {
return
}
try {
const userInfo = await getUserInfo(toSession(auth.value), syncAuth)
const payload = userInfo as Record<string, unknown>
const resolvedId = toStringOrEmpty(payload.userID ?? payload.user_id ?? payload.id)
if (!resolvedId) {
return
}
auth.value = {
...auth.value,
user: {
...(auth.value.user ?? {}),
id: resolvedId,
},
}
writeStoredAuth(auth.value)
} catch {
wsError.value = '获取当前用户ID失败部分操作可能不可用'
}
}
function toFiniteNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
@@ -591,6 +654,36 @@ function toFiniteNumber(value: unknown): number | null {
return null
}
function toBoolean(value: unknown): boolean {
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'number') {
return value !== 0
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
return normalized === '1' || normalized === 'true' || normalized === 'yes'
}
return false
}
function normalizeScores(value: unknown): Record<string, number> {
const record = toRecord(value)
if (!record) {
return {}
}
const scores: Record<string, number> = {}
for (const [key, score] of Object.entries(record)) {
const parsed = toFiniteNumber(score)
if (parsed !== null) {
scores[key] = parsed
}
}
return scores
}
function normalizePlayer(input: unknown, fallbackIndex: number): RoomPlayerState | null {
const player = toRecord(input)
if (!player) {
@@ -611,7 +704,13 @@ function normalizePlayer(input: unknown, fallbackIndex: number): RoomPlayerState
}
function extractCurrentTurnIndex(value: Record<string, unknown>): number | null {
const game = toRecord(value.game)
const gameState = toRecord(game?.state)
const keys = [
gameState?.currentTurn,
gameState?.current_turn,
gameState?.currentTurnIndex,
gameState?.current_turn_index,
value.currentTurnIndex,
value.current_turn_index,
value.currentPlayerIndex,
@@ -630,13 +729,70 @@ function extractCurrentTurnIndex(value: Record<string, unknown>): number | null
return null
}
function normalizeRoom(input: unknown): RoomState | null {
const room = toRecord(input)
if (!room) {
function normalizeGame(input: unknown): GameState | null {
const game = toRecord(input)
if (!game) {
return null
}
const id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id)
const rule = toRecord(game.rule)
const rawState = toRecord(game.state)
const playersRaw =
(Array.isArray(rawState?.players) ? rawState?.players : null) ??
(Array.isArray(rawState?.playerStates) ? rawState?.playerStates : null) ??
[]
const normalizedPlayers = playersRaw
.map((item, index) => normalizePlayer(item, index))
.filter((item): item is RoomPlayerState => Boolean(item))
return {
rule: rule
? {
name: toStringOrEmpty(rule.name),
isBloodFlow: toBoolean(rule.isBloodFlow ?? rule.is_blood_flow),
hasHongZhong: toBoolean(rule.hasHongZhong ?? rule.has_hong_zhong),
}
: null,
state: rawState
? {
phase: toStringOrEmpty(rawState.phase),
dealerIndex: toFiniteNumber(rawState.dealerIndex ?? rawState.dealer_index) ?? 0,
currentTurn: toFiniteNumber(rawState.currentTurn ?? rawState.current_turn) ?? 0,
needDraw: toBoolean(rawState.needDraw ?? rawState.need_draw),
players: normalizedPlayers,
wall: Array.isArray(rawState.wall) ? rawState.wall.map((item) => toStringOrEmpty(item)).filter(Boolean) : [],
lastDiscardTile: toStringOrEmpty(rawState.lastDiscardTile ?? rawState.last_discard_tile) || null,
lastDiscardBy: toStringOrEmpty(rawState.lastDiscardBy ?? rawState.last_discard_by),
pendingClaim: toRecord(rawState.pendingClaim ?? rawState.pending_claim),
winners: Array.isArray(rawState.winners)
? rawState.winners.map((item) => toStringOrEmpty(item)).filter(Boolean)
: [],
scores: normalizeScores(rawState.scores),
lastDrawPlayerId: toStringOrEmpty(rawState.lastDrawPlayerID ?? rawState.last_draw_player_id),
lastDrawFromGang: toBoolean(rawState.lastDrawFromGang ?? rawState.last_draw_from_gang),
lastDrawIsLastTile: toBoolean(rawState.lastDrawIsLastTile ?? rawState.last_draw_is_last_tile),
huWay: toStringOrEmpty(rawState.huWay ?? rawState.hu_way),
}
: null,
}
}
function normalizeRoom(input: unknown): RoomState | null {
const source = toRecord(input)
if (!source) {
return null
}
let room = source
let id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id)
if (!id) {
const nestedRoom = toRecord(room.data)
if (nestedRoom) {
room = nestedRoom
id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id)
}
}
if (!id) {
return null
}
@@ -648,19 +804,38 @@ function normalizeRoom(input: unknown): RoomState | null {
(Array.isArray(room.playerList) ? room.playerList : null) ??
(Array.isArray(room.player_list) ? room.player_list : null) ??
[]
const playerIdsRaw =
(Array.isArray(room.player_ids) ? room.player_ids : null) ??
(Array.isArray(room.playerIds) ? room.playerIds : null) ??
[]
const players = playersRaw
.map((item, index) => normalizePlayer(item, index))
.filter((item): item is RoomPlayerState => Boolean(item))
.sort((a, b) => a.index - b.index)
const playersFromIds = playerIdsRaw
.map((item, index) => ({
index,
playerId: toStringOrEmpty(item),
ready: false,
}))
.filter((item) => Boolean(item.playerId))
const resolvedPlayers = players.length > 0 ? players : playersFromIds
const parsedPlayerCount = toFiniteNumber(room.player_count ?? room.playerCount)
const game = normalizeGame(room.game)
return {
id,
name: toStringOrEmpty(room.name) || roomState.value.name,
gameType: toStringOrEmpty(room.gameType ?? room.game_type) || roomState.value.gameType || 'chengdu',
ownerId: toStringOrEmpty(room.ownerId ?? room.owner_id),
maxPlayers,
playerCount: parsedPlayerCount ?? resolvedPlayers.length,
status: toStringOrEmpty(room.status) || roomState.value.status || 'waiting',
players,
createdAt: toStringOrEmpty(room.createdAt ?? room.created_at) || roomState.value.createdAt,
updatedAt: toStringOrEmpty(room.updatedAt ?? room.updated_at) || roomState.value.updatedAt,
game: game ?? roomState.value.game,
players: resolvedPlayers,
currentTurnIndex: extractCurrentTurnIndex(room),
}
}
@@ -669,14 +844,7 @@ function mergeRoomState(next: RoomState): void {
if (roomId.value && next.id !== roomId.value) {
return
}
roomState.value = {
...roomState.value,
...next,
players: next.players.length > 0 ? next.players : roomState.value.players,
currentTurnIndex:
next.currentTurnIndex !== null ? next.currentTurnIndex : roomState.value.currentTurnIndex,
}
mergeActiveRoomState(next)
}
function consumeGameEvent(raw: string): void {
@@ -692,12 +860,58 @@ function consumeGameEvent(raw: string): void {
return
}
const candidates: unknown[] = [event, event.payload, event.data]
const payload = toRecord(event.payload)
if (payload) {
candidates.push(payload.room, payload.state, payload.roomState)
const data = toRecord(event.data)
const eventType = toStringOrEmpty(event.type)
const eventStatus = toStringOrEmpty(event.status)
const eventRoomId = toStringOrEmpty(event.roomId ?? event.room_id ?? payload?.roomId ?? payload?.room_id)
const eventRequestId = toStringOrEmpty(
event.requestId ?? event.request_id ?? payload?.requestId ?? payload?.request_id ?? data?.requestId ?? data?.request_id,
)
const payloadPlayerIds = Array.isArray(payload?.player_ids)
? payload.player_ids.map((item) => toStringOrEmpty(item)).filter(Boolean)
: Array.isArray(payload?.playerIds)
? payload.playerIds.map((item) => toStringOrEmpty(item)).filter(Boolean)
: null
const leaveByRequestIdMatched = Boolean(eventRequestId && eventRequestId === lastLeaveRoomRequestId.value)
const leaveByPlayerUpdateMatched =
leaveRoomPending.value &&
eventType === 'room_player_update' &&
eventStatus === 'ok' &&
eventRoomId === (roomState.value.id || roomId.value) &&
Array.isArray(payloadPlayerIds) &&
Boolean(currentUserId.value) &&
!payloadPlayerIds.includes(currentUserId.value)
if (leaveByRequestIdMatched || leaveByPlayerUpdateMatched) {
leaveRoomPending.value = false
lastLeaveRoomRequestId.value = ''
if (event.status === 'error') {
leaveHallAfterAck.value = false
wsError.value = '退出房间失败,请稍后重试'
pushWsMessage(`[client] 退出房间失败 requestId=${eventRequestId}`)
} else {
if (leaveByPlayerUpdateMatched) {
pushWsMessage('[client] 已确认退出房间 player_update')
} else {
pushWsMessage(`[client] 已确认退出房间 requestId=${eventRequestId}`)
}
if (leaveHallAfterAck.value) {
leaveHallAfterAck.value = false
void router.push('/hall')
}
}
}
const candidates: unknown[] = [event.payload, event.data]
if (payload) {
candidates.push(payload.room, payload.state, payload.roomState, payload.data)
}
if (data) {
candidates.push(data.room, data.state, data.roomState, data.data)
}
candidates.push(event)
for (const candidate of candidates) {
const normalized = normalizeRoom(candidate)
if (normalized) {
@@ -725,6 +939,7 @@ function sendStartGame(): void {
}
const sender = currentUserId.value
if (!sender) {
return
}
@@ -749,18 +964,26 @@ function sendStartGame(): void {
pushWsMessage(`[client] 请求开始游戏 requestId=${requestId}`)
}
function sendLeaveRoom(): void {
function sendLeaveRoom(): boolean {
if (!ws.value || ws.value.readyState !== WebSocket.OPEN) {
return
wsError.value = 'WebSocket 未连接,无法退出房间'
return false
}
const sender = currentUserId.value
const targetRoomId = roomState.value.id || roomId.value
if (!sender || !targetRoomId) {
return
if (!sender) {
wsError.value = '缺少当前用户ID无法退出房间'
return false
}
if (!targetRoomId) {
wsError.value = '缺少房间ID无法退出房间'
return false
}
const requestId = createRequestId('leave-room')
leaveRoomPending.value = true
lastLeaveRoomRequestId.value = requestId
const message = {
type: 'leave_room',
sender,
@@ -775,6 +998,8 @@ function sendLeaveRoom(): void {
logWsSend(message)
ws.value.send(JSON.stringify(message))
pushWsMessage(`[client] 请求离开房间 requestId=${requestId}`)
pushWsMessage(`[client] 请求退出房间 requestId=${requestId}`)
return true
}
function connectWs(): void {
@@ -823,6 +1048,13 @@ function connectWs(): void {
socket.onclose = () => {
wsStatus.value = 'disconnected'
startGamePending.value = false
if (leaveRoomPending.value) {
leaveRoomPending.value = false
lastLeaveRoomRequestId.value = ''
leaveHallAfterAck.value = false
wsError.value = '连接已断开,未收到退出房间确认'
pushWsMessage('[client] 连接断开:退出房间请求未确认')
}
pushWsMessage('WebSocket 已断开')
}
}
@@ -850,27 +1082,29 @@ function startNextRound(): void {
watch(
roomId,
(nextRoomId) => {
roomState.value = {
id: nextRoomId,
name: roomName.value,
ownerId: '',
maxPlayers: DEFAULT_MAX_PLAYERS,
status: 'waiting',
players: [],
currentTurnIndex: null,
const currentRoom = roomState.value
if (!nextRoomId) {
destroyActiveRoomState()
} else if (currentRoom.id !== nextRoomId) {
resetActiveRoomState({
id: nextRoomId,
name: roomName.value,
})
} else if (!currentRoom.name && roomName.value) {
roomState.value = { ...currentRoom, name: roomName.value }
}
overlayState.value = 'missing'
startGamePending.value = false
lastStartRequestId.value = ''
leaveRoomPending.value = false
lastLeaveRoomRequestId.value = ''
leaveHallAfterAck.value = false
},
{ immediate: true },
)
watch(roomName, (next) => {
roomState.value = {
...roomState.value,
name: next || roomState.value.name,
}
roomState.value = { ...roomState.value, name: next || roomState.value.name }
})
watch(
@@ -908,7 +1142,8 @@ watch(
{ immediate: true },
)
onMounted(() => {
onMounted(async () => {
await ensureCurrentUserId()
connectWs()
clockTimer = window.setInterval(() => {
now.value = new Date()
@@ -917,6 +1152,7 @@ onMounted(() => {
onBeforeUnmount(() => {
disconnectWs()
destroyActiveRoomState()
if (clockTimer !== undefined) {
window.clearInterval(clockTimer)
}
@@ -937,6 +1173,10 @@ onBeforeUnmount(() => {
<strong>{{ roomState.name || roomName || '成都麻将' }}</strong>
</div>
</div>
<div class="header-actions">
<button class="ghost-btn" type="button" :disabled="leaveRoomPending" @click="backHall">
{{ leaveRoomPending ? '退出中...' : '返回大厅' }}
</button>
<div class="topbar-center">
<div class="title-stack">
@@ -952,6 +1192,25 @@ onBeforeUnmount(() => {
</div>
<div class="status-chip clock-chip">{{ formattedClock }}</div>
<button class="header-btn ghost-btn" type="button" @click="connectWs">重连</button>
<section class="table-panel game-table-panel">
<div class="room-brief">
<span class="room-brief-title">当前房间</span>
<span class="room-brief-item">
<em>房间名</em>
<strong>{{ roomState.name || roomName || '未命名房间' }}</strong>
</span>
<span class="room-brief-item room-brief-id">
<em>room_id</em>
<strong>{{ roomId || '未选择房间' }}</strong>
</span>
<span class="room-brief-item">
<em>状态</em>
<strong>{{ roomStatusText }}</strong>
</span>
<span class="room-brief-item">
<em>人数</em>
<strong>{{ roomState.playerCount }}/{{ roomState.maxPlayers }}</strong>
</span>
<button
class="header-btn primary-btn"
type="button"

View File

@@ -4,6 +4,8 @@ import { useRouter } from 'vue-router'
import { AuthExpiredError, type AuthSession } from '../api/authed-request'
import { createRoom, joinRoom, listRooms, type RoomItem } from '../api/mahjong'
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 { 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
}
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 {
return {
token: source.token,
@@ -220,6 +232,18 @@ async function submitCreateRoom(): Promise<void> {
)
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
createRoomForm.value.name = ''
showCreateModal.value = false
@@ -251,18 +275,27 @@ async function handleJoinRoom(room?: { roomId?: string; roomName?: string }): Pr
return
}
const targetRoomName =
room?.roomName ?? rooms.value.find((item) => item.room_id === targetRoomId)?.name ?? ''
roomSubmitting.value = true
try {
await joinRoom(session, { roomId: targetRoomId }, syncAuth)
quickJoinRoomId.value = targetRoomId
successMessage.value = `已加入房间:${targetRoomId}`
const joinedRoom = await joinRoom(session, { roomId: targetRoomId }, syncAuth)
hydrateActiveRoomFromSelection({
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 router.push({
path: `/game/chengdu/${targetRoomId}`,
query: targetRoomName ? { roomName: targetRoomName } : undefined,
path: `/game/chengdu/${joinedRoom.room_id}`,
query: joinedRoom.name ? { roomName: joinedRoom.name } : undefined,
})
} catch (error) {
if (error instanceof AuthExpiredError) {
@@ -298,6 +331,18 @@ async function enterCreatedRoom(): Promise<void> {
}
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({
path: `/game/chengdu/${createdRoom.value.room_id}`,
query: { roomName: createdRoom.value.name },