Compare commits

...

5 Commits

Author SHA1 Message Date
ee797ebb14 Merge remote-tracking branch 'origin/dev'
# Conflicts:
#	src/views/ChengduGamePage.vue
2026-03-24 09:13:16 +08:00
1308ca5a2c Update vite.config.ts 2026-03-24 09:03:11 +08:00
632a0267a4 update 2026-03-23 21:13:38 +08:00
fba407c1bf chore(config): 更新开发服务器代理配置
- 将 API 代理目标端口从 8080 更改为 18080
- 将 WebSocket 代理目标端口从 8080 更改为 18080
- 保持了原有的 changeOrigin 和 ws 配置选项
2026-03-20 17:27:13 +08:00
0fa14ca407 feat(game): 实现成都麻将游戏界面和核心功能
- 添加麻将桌面背景和完整的UI布局设计
- 实现玩家座位渲染、牌面显示和游戏状态管理
- 集成定缺选择、碰牌操作和结算功能
- 添加计时器、网络状态和实时消息显示
- 创建麻将牌面图片资源和动态加载机制
- 实现游戏流程控制和玩家交互逻辑
2026-03-18 17:26:20 +08:00
10 changed files with 2107 additions and 142 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

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,
})
}

File diff suppressed because it is too large Load Diff

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 },

View File

@@ -4,15 +4,17 @@ import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
host: '127.0.0.1',
port: 3000,
proxy: {
'/api/v1': {
target: 'http://127.0.0.1:8080',
target: 'http://127.0.0.1:19000',
changeOrigin: true,
},
'/api/v1/ws': {
target: 'ws://127.0.0.1:8080',
target: 'ws://127.0.0.1:19000',
ws: true,
}
}
}
})
})