feat: 添加房间侧边栏和操作按钮组件
This commit is contained in:
134
src/views/game/components/RoomActions.vue
Normal file
134
src/views/game/components/RoomActions.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="room-actions">
|
||||
<!-- 等待中:准备/取消准备按钮 -->
|
||||
<template v-if="store.status === 'waiting' && store.isPlayer">
|
||||
<button
|
||||
class="room-actions__btn"
|
||||
:class="isReady ? 'room-actions__btn--cancel' : 'room-actions__btn--ready'"
|
||||
@click="toggleReady"
|
||||
>
|
||||
{{ isReady ? '取消准备' : '准备' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="store.isOwner"
|
||||
class="room-actions__btn room-actions__btn--start"
|
||||
@click="startGame"
|
||||
>
|
||||
开始游戏
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- 对局中:认输、求和 -->
|
||||
<template v-if="store.isPlaying && store.isPlayer">
|
||||
<button class="room-actions__btn room-actions__btn--danger" @click="resign">
|
||||
认输
|
||||
</button>
|
||||
<button class="room-actions__btn room-actions__btn--warn" @click="requestDraw">
|
||||
求和
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- 离开房间(始终显示) -->
|
||||
<button class="room-actions__btn room-actions__btn--leave" @click="leaveRoom">
|
||||
离开房间
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoomStore } from '@/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { RoomSocket } from '@/common/websocket/roomSocket'
|
||||
|
||||
const store = useRoomStore()
|
||||
const { players, currentUserId } = storeToRefs(store)
|
||||
const router = useRouter()
|
||||
|
||||
/** 通过 inject 获取 WS 连接(父组件 provide) */
|
||||
const socket = inject<RoomSocket>('roomSocket')!
|
||||
|
||||
/** 当前用户是否已准备 */
|
||||
const isReady = computed(() => {
|
||||
const me = players.value.find((p) => p.userId === currentUserId.value)
|
||||
return me?.ready ?? false
|
||||
})
|
||||
|
||||
function toggleReady() {
|
||||
socket.send('ready')
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
socket.send('start')
|
||||
}
|
||||
|
||||
function resign() {
|
||||
socket.send('resign')
|
||||
}
|
||||
|
||||
function requestDraw() {
|
||||
socket.send('draw_request')
|
||||
}
|
||||
|
||||
function leaveRoom() {
|
||||
socket.send('leave')
|
||||
socket.close()
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.room-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.room-actions__btn {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--ready {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--text-inverse);
|
||||
&:hover { background-color: var(--color-primary-hover); }
|
||||
}
|
||||
&--cancel {
|
||||
background-color: var(--bg-surface-hover);
|
||||
color: var(--text-secondary);
|
||||
&:hover { background-color: var(--border-default); }
|
||||
}
|
||||
&--start {
|
||||
background-color: var(--color-success);
|
||||
color: white;
|
||||
&:hover { opacity: 0.9; }
|
||||
}
|
||||
&--danger {
|
||||
background-color: transparent;
|
||||
color: var(--color-danger);
|
||||
border: 1px solid var(--color-danger);
|
||||
&:hover { background-color: var(--color-danger); color: white; }
|
||||
}
|
||||
&--warn {
|
||||
background-color: transparent;
|
||||
color: var(--color-warning);
|
||||
border: 1px solid var(--color-warning);
|
||||
&:hover { background-color: var(--color-warning); color: white; }
|
||||
}
|
||||
&--leave {
|
||||
background-color: transparent;
|
||||
color: var(--text-tertiary);
|
||||
border: 1px solid var(--border-default);
|
||||
&:hover { border-color: var(--text-secondary); color: var(--text-primary); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
212
src/views/game/components/RoomSidePanel.vue
Normal file
212
src/views/game/components/RoomSidePanel.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="room-side-panel">
|
||||
<!-- 对手区域(非当前用户的第一个玩家) -->
|
||||
<div v-if="opponent" class="room-side-panel__player-card">
|
||||
<img :src="getAvatarUrl(opponent.avatar)" class="room-side-panel__avatar" />
|
||||
<div class="room-side-panel__player-info">
|
||||
<div class="room-side-panel__nickname">
|
||||
{{ opponent.nickname }}
|
||||
<span v-if="opponent.userId === store.creatorId" class="room-side-panel__crown">👑</span>
|
||||
</div>
|
||||
<div class="room-side-panel__role">
|
||||
{{ opponentReady ? '✅ 已准备' : '⏳ 未准备' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对局信息 -->
|
||||
<div v-if="store.isPlaying || store.isFinished" class="room-side-panel__game-info">
|
||||
<div class="room-side-panel__info-row">
|
||||
<span>步数</span>
|
||||
<span>{{ store.moves.length }}</span>
|
||||
</div>
|
||||
<div class="room-side-panel__info-row">
|
||||
<span>当前轮次</span>
|
||||
<span :class="{ 'room-side-panel__turn--me': isMyTurn }">
|
||||
{{ isMyTurn ? '轮到你了' : '等待对手' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 我的信息 -->
|
||||
<div v-if="me" class="room-side-panel__player-card room-side-panel__player-card--me">
|
||||
<img :src="getAvatarUrl(me.avatar)" class="room-side-panel__avatar" />
|
||||
<div class="room-side-panel__player-info">
|
||||
<div class="room-side-panel__nickname">
|
||||
{{ me.nickname }}
|
||||
<span v-if="me.userId === store.creatorId" class="room-side-panel__crown">👑</span>
|
||||
</div>
|
||||
<div class="room-side-panel__role">
|
||||
{{ meReady ? '✅ 已准备' : '⏳ 未准备' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 观战提示 -->
|
||||
<div v-if="!store.isPlayer" class="room-side-panel__spectator-note">
|
||||
👀 观战中
|
||||
</div>
|
||||
|
||||
<!-- 对局结果 -->
|
||||
<div v-if="store.isFinished && store.resultType" class="room-side-panel__result">
|
||||
<div v-if="store.resultType === 'draw'" class="room-side-panel__result-text">
|
||||
🤝 平局
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="room-side-panel__result-text"
|
||||
:class="isWinner ? 'room-side-panel__result-text--win' : 'room-side-panel__result-text--lose'"
|
||||
>
|
||||
{{ isWinner ? '🎉 你赢了!' : '💀 你输了' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<RoomActions />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoomStore } from '@/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { getAvatarUrl } from '@/utils'
|
||||
import RoomActions from './RoomActions.vue'
|
||||
|
||||
const store = useRoomStore()
|
||||
const { players, currentUserId, currentTurn, winner } = storeToRefs(store)
|
||||
|
||||
/** 当前玩家自己 */
|
||||
const me = computed(() => players.value.find((p) => p.userId === currentUserId.value) ?? null)
|
||||
|
||||
/** 对手(非当前用户的第一个玩家) */
|
||||
const opponent = computed(() => players.value.find((p) => p.userId !== currentUserId.value) ?? null)
|
||||
|
||||
/** 当前用户是否已准备 */
|
||||
const meReady = computed(() => me.value?.ready ?? false)
|
||||
const opponentReady = computed(() => opponent.value?.ready ?? false)
|
||||
|
||||
/** 当前是否轮到我了 */
|
||||
const isMyTurn = computed(() => currentTurn.value === currentUserId.value)
|
||||
|
||||
/** 当前用户是否赢了 */
|
||||
const isWinner = computed(() => winner.value === currentUserId.value)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.room-side-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 240px;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
background-color: var(--bg-sidebar);
|
||||
border-right: 1px solid var(--border-default);
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.room-side-panel__player-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-light);
|
||||
|
||||
&--me {
|
||||
border-color: var(--color-primary-light);
|
||||
background-color: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
.room-side-panel__avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.room-side-panel__player-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.room-side-panel__nickname {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.room-side-panel__crown {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.room-side-panel__role {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.room-side-panel__game-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.room-side-panel__info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
|
||||
span:first-child {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
span:last-child {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.room-side-panel__turn--me {
|
||||
color: var(--color-primary) !important;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.room-side-panel__spectator-note {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--text-tertiary);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.room-side-panel__result {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.room-side-panel__result-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
|
||||
&--win {
|
||||
color: var(--color-success);
|
||||
}
|
||||
&--lose {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user