feat: complete Phase 1 - game library, lifecycle, realtime sync
- Seed 33 popular games across 5 platforms via admin API script - Add GameDetailDialog with game info and quick-team button - Update GamesLibrary with game card click to open detail dialog - Update Home hot games to open detail dialog instead of navigating - Rewrite invitation accept: frontend auto-joins team + updates status - Add user status reset on team dissolution (endGame) - Add start game / dissolve buttons to TeamSessionPanel lifecycle - Integrate realtime subscriptions in GroupView and Layout - Add notification store realtime invitation listener - Add placeholder images for game covers and avatars - Remove Go hooks, add JS hooks placeholder + Docker mount Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Game } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
game: Game | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'create-team': [gameName: string]
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
function handleCreateTeam() {
|
||||
if (props.game) {
|
||||
emit('create-team', props.game.name)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" width="440px" :show-close="true">
|
||||
<template v-if="game">
|
||||
<div class="game-detail">
|
||||
<div class="detail-cover-wrap">
|
||||
<img
|
||||
:src="game.cover || '/game-placeholder.png'"
|
||||
:alt="game.name"
|
||||
class="detail-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2 class="detail-name">{{ game.name }}</h2>
|
||||
|
||||
<div class="detail-meta">
|
||||
<span v-if="game.platform" class="platform-badge">{{ game.platform }}</span>
|
||||
<span class="popularity">🔥 {{ game.popularCount }} 人游玩</span>
|
||||
</div>
|
||||
|
||||
<div v-if="game.tags?.length" class="detail-tags">
|
||||
<span v-for="tag in game.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" class="team-btn" @click="handleCreateTeam">
|
||||
快速组队
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.detail-cover-wrap {
|
||||
width: 200px;
|
||||
height: 260px;
|
||||
border-radius: var(--gg-radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--gg-bg-elevated);
|
||||
border: 1px solid var(--gg-border);
|
||||
}
|
||||
|
||||
.detail-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--gg-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.platform-badge {
|
||||
padding: 4px 14px;
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: var(--gg-accent);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.popularity {
|
||||
font-size: 14px;
|
||||
color: var(--gg-text-secondary);
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 4px 12px;
|
||||
background: var(--gg-bg-elevated);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--gg-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.team-btn {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -19,6 +19,14 @@ const memberDetails = computed(() => {
|
||||
.filter((m): m is NonNullable<typeof m> => Boolean(m))
|
||||
})
|
||||
|
||||
async function startGame() {
|
||||
try {
|
||||
await teamStore.updateStatus('playing')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function endGame() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
@@ -67,9 +75,15 @@ async function endGame() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="end-game-btn" @click="endGame">
|
||||
<button v-if="session.status === 'recruiting'" class="start-game-btn" @click="startGame">
|
||||
开始游戏
|
||||
</button>
|
||||
<button v-else-if="session.status === 'playing'" class="end-game-btn" @click="endGame">
|
||||
游戏结束
|
||||
</button>
|
||||
<button v-if="session.status === 'recruiting'" class="dissolve-btn" @click="endGame">
|
||||
解散小组
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-session">
|
||||
@@ -177,6 +191,43 @@ async function endGame() {
|
||||
color: var(--gg-text);
|
||||
}
|
||||
|
||||
.start-game-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
border: none;
|
||||
border-radius: var(--gg-radius-sm);
|
||||
background: var(--gg-gradient-green);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.start-game-btn:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 4px 16px rgba(5, 150, 105, 0.3);
|
||||
}
|
||||
|
||||
.dissolve-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--gg-border);
|
||||
border-radius: var(--gg-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--gg-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dissolve-btn:hover {
|
||||
border-color: var(--gg-danger);
|
||||
color: var(--gg-danger);
|
||||
}
|
||||
|
||||
.end-game-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
Reference in New Issue
Block a user