diff --git a/src/assets/styles/room.css b/src/assets/styles/room.css index 1afd81f..8630387 100644 --- a/src/assets/styles/room.css +++ b/src/assets/styles/room.css @@ -1616,3 +1616,157 @@ font-size: 18px; } } + +/* ── Settlement Overlay ── */ +.settlement-overlay { + position: absolute; + inset: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(4px); + animation: settlement-fade-in 300ms ease-out; +} + +@keyframes settlement-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.settlement-panel { + width: 380px; + max-width: 90vw; + padding: 28px 24px 20px; + border: 1px solid rgba(220, 191, 118, 0.3); + border-radius: 16px; + background: + linear-gradient(180deg, rgba(14, 55, 40, 0.96), rgba(8, 36, 27, 0.98)), + radial-gradient(circle at 20% 24%, rgba(237, 214, 157, 0.06), transparent 34%); + box-shadow: + inset 0 1px 0 rgba(255, 244, 214, 0.08), + 0 24px 48px rgba(0, 0, 0, 0.4); + animation: settlement-panel-pop 300ms ease-out; +} + +@keyframes settlement-panel-pop { + from { opacity: 0; transform: scale(0.92) translateY(12px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.settlement-title { + margin: 0 0 4px; + color: #e5c472; + font-size: 20px; + font-weight: 800; + text-align: center; + letter-spacing: 1px; + text-shadow: + -1px 0 rgba(0, 0, 0, 0.38), + 0 1px rgba(0, 0, 0, 0.38), + 1px 0 rgba(0, 0, 0, 0.38), + 0 -1px rgba(0, 0, 0, 0.38); +} + +.settlement-round-info { + margin: 0 0 16px; + color: rgba(229, 196, 114, 0.6); + font-size: 13px; + text-align: center; + letter-spacing: 0.5px; +} + +.settlement-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 20px; +} + +.settlement-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.04); + transition: background 150ms; +} + +.settlement-row.is-winner { + background: rgba(229, 196, 114, 0.1); + border: 1px solid rgba(220, 191, 118, 0.2); +} + +.settlement-row.is-self { + box-shadow: inset 0 0 0 1px rgba(229, 196, 114, 0.18); +} + +.settlement-rank { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.7); + font-size: 12px; + font-weight: 700; + flex-shrink: 0; +} + +.settlement-row.is-winner .settlement-rank { + background: rgba(229, 196, 114, 0.2); + color: #e5c472; +} + +.settlement-name { + flex: 1; + color: rgba(255, 255, 255, 0.88); + font-size: 15px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settlement-winner-badge { + display: inline-block; + margin-left: 6px; + padding: 1px 6px; + border-radius: 4px; + background: rgba(229, 196, 114, 0.22); + color: #e5c472; + font-size: 11px; + font-weight: 800; + vertical-align: middle; +} + +.settlement-score { + font-size: 18px; + font-weight: 800; + color: rgba(255, 255, 255, 0.7); + flex-shrink: 0; + min-width: 48px; + text-align: right; +} + +.settlement-score.is-positive { + color: #5dda6e; +} + +.settlement-score.is-negative { + color: #e85d5d; +} + +.settlement-actions { + display: flex; + justify-content: center; + gap: 12px; +} + +.settlement-btn { + min-width: 160px; +} diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts index 3993b7f..2a81a90 100644 --- a/src/store/gameStore.ts +++ b/src/store/gameStore.ts @@ -53,6 +53,10 @@ export const useGameStore = defineStore('game', { winners: [], scores: {}, + + currentRound: 0, + + totalRounds: 0, }), actions: { diff --git a/src/types/state/gameState.ts b/src/types/state/gameState.ts index 340280e..ed33462 100644 --- a/src/types/state/gameState.ts +++ b/src/types/state/gameState.ts @@ -33,4 +33,10 @@ export interface GameState { // 分数(playerId -> score) scores: Record + + // 当前第几局 + currentRound: number + + // 总局数 + totalRounds: number } diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index ccc5237..c6845f9 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -97,6 +97,7 @@ const dingQuePending = ref(false) const discardPending = ref(false) const claimActionPending = ref(false) const turnActionPending = ref(false) +const nextRoundPending = ref(false) const selectedDiscardTileId = ref(null) let clockTimer: number | null = null let discardPendingTimer: number | null = null @@ -322,6 +323,35 @@ const roomStatusText = computed(() => { return map[status] ?? status ?? '--' }) +const roundText = computed(() => { + if (gameStore.totalRounds > 0) { + return `${gameStore.currentRound}/${gameStore.totalRounds}` + } + return '' +}) + +const showSettlementOverlay = computed(() => { + return gameStore.phase === 'settlement' +}) + +const isLastRound = computed(() => { + return gameStore.currentRound >= gameStore.totalRounds && gameStore.totalRounds > 0 +}) + +const settlementPlayers = computed(() => { + const players = Object.values(gameStore.players) + const winnerSet = new Set(gameStore.winners) + return players + .map((player) => ({ + playerId: player.playerId, + displayName: player.displayName || `玩家${player.seatIndex + 1}`, + score: gameStore.scores[player.playerId] ?? 0, + isWinner: winnerSet.has(player.playerId), + seatIndex: player.seatIndex, + })) + .sort((a, b) => b.score - a.score) +}) + const myReadyState = computed(() => { return Boolean(myPlayer.value?.isReady) }) @@ -1203,6 +1233,14 @@ function handleRoomStateResponse(message: unknown): void { ) as Record } gameStore.winners = readStringArray(payload, 'winners') + const currentRound = readNumber(payload, 'current_round', 'currentRound') + const totalRounds = readNumber(payload, 'total_rounds', 'totalRounds') + if (typeof currentRound === 'number') { + gameStore.currentRound = currentRound + } + if (typeof totalRounds === 'number') { + gameStore.totalRounds = totalRounds + } gameStore.pendingClaim = normalizePendingClaim(payload) if (!gameStore.pendingClaim) { claimActionPending.value = false @@ -1255,6 +1293,9 @@ function handleRoomStateResponse(message: unknown): void { if (phase !== 'waiting') { startGamePending.value = false } + if (phase !== 'settlement') { + nextRoundPending.value = false + } if (currentTurnPlayerId && currentTurnPlayerId !== loggedInUserId.value) { markDiscardCompleted() } @@ -1534,6 +1575,14 @@ function handleRoomInfoResponse(message: unknown): void { ) as Record } gameStore.winners = readStringArray(gameState ?? {}, 'winners') + const infoCurrentRound = readNumber(gameState ?? {}, 'current_round', 'currentRound') + const infoTotalRounds = readNumber(gameState ?? {}, 'total_rounds', 'totalRounds') + if (typeof infoCurrentRound === 'number') { + gameStore.currentRound = infoCurrentRound + } + if (typeof infoTotalRounds === 'number') { + gameStore.totalRounds = infoTotalRounds + } setActiveRoom({ roomId, @@ -2465,6 +2514,19 @@ function startGame(): void { }) } +function nextRound(): void { + if (nextRoundPending.value) { + return + } + + nextRoundPending.value = true + sendWsMessage({ + type: 'next_round', + roomId: gameStore.roomId, + payload: {}, + }) +} + function chooseDingQue(suit: Tile['suit']): void { if (dingQuePending.value || !showDingQueChooser.value) { return @@ -2887,6 +2949,10 @@ onBeforeUnmount(() => { 人数 {{ roomState.playerCount }}/{{ roomState.maxPlayers }} +
+ 局数 + {{ roundText }} +
状态 {{ roomStatusText }} @@ -3062,6 +3128,52 @@ onBeforeUnmount(() => { 等待房主开始游戏
+
+
+

+ {{ isLastRound ? '最终结算' : `第 ${gameStore.currentRound} 局结算` }} +

+

+ {{ gameStore.currentRound }} / {{ gameStore.totalRounds }} 局 +

+
+
+ {{ index + 1 }} + + {{ item.displayName }} + + + + {{ item.score > 0 ? '+' : '' }}{{ item.score }} + +
+
+
+ + +
+
+