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:
congsh
2026-04-17 20:23:39 +08:00
parent 0bcf39bb4b
commit 802712c662
17 changed files with 394 additions and 180 deletions
@@ -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;