feat: add GameGroup2 project with frontend and backend

- Add .gitignore for Node.js and PocketBase projects
- Add frontend (Vue 3 + Vite + TypeScript)
- Add backend (PocketBase)
- Add deployment scripts and Docker compose configs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
congsh
2026-04-17 15:45:54 +08:00
parent 2db391901c
commit 2ce8985747
56 changed files with 3981 additions and 783 deletions
+178
View File
@@ -0,0 +1,178 @@
<!-- src/views/GamesLibrary.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getGames, getAllPlatforms } from '@/api/games'
import type { Game, GamePlatform } from '@/types'
import { ElCard, ElInput, ElSelect, ElOption } from 'element-plus'
const games = ref<Game[]>([])
const loading = ref(false)
const searchQuery = ref('')
const selectedPlatform = ref<GamePlatform | ''>('')
const platforms = getAllPlatforms()
onMounted(async () => {
await loadGames()
})
async function loadGames() {
try {
loading.value = true
const result = await getGames({
limit: 50,
search: searchQuery.value || undefined,
platform: selectedPlatform.value || undefined
})
games.value = result.items
} catch (error) {
console.error('加载游戏失败:', error)
} finally {
loading.value = false
}
}
function handleSearch() {
loadGames()
}
</script>
<template>
<div class="games-library">
<div class="page-header">
<h1>游戏库</h1>
<div class="filters">
<el-input
v-model="searchQuery"
placeholder="搜索游戏..."
clearable
@input="handleSearch"
/>
<el-select v-model="selectedPlatform" placeholder="选择平台" clearable @change="loadGames">
<el-option
v-for="platform in platforms"
:key="platform"
:label="platform"
:value="platform"
/>
</el-select>
</div>
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="games.length === 0" class="empty">
<p>暂无游戏</p>
</div>
<div v-else class="games-grid">
<el-card v-for="game in games" :key="game.id" class="game-card">
<img
:src="game.cover || '/game-placeholder.png'"
:alt="game.name"
class="game-cover"
/>
<div class="game-info">
<h3 class="game-name">{{ game.name }}</h3>
<p class="game-platform">{{ game.platform }}</p>
<div class="game-tags">
<span v-for="tag in game.tags" :key="tag" class="tag">{{ tag }}</span>
</div>
</div>
</el-card>
</div>
</div>
</template>
<style scoped>
.games-library {
width: 100%;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 28px;
font-weight: 700;
margin: 0 0 16px;
}
.filters {
display: flex;
gap: 12px;
}
.filters .el-input {
width: 300px;
}
.loading {
text-align: center;
padding: 60px;
color: var(--el-text-color-secondary);
}
.empty {
text-align: center;
padding: 60px;
color: var(--el-text-color-secondary);
}
.games-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.game-card {
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.game-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.game-cover {
width: 100%;
aspect-ratio: 3/4;
object-fit: cover;
}
.game-info {
padding: 12px;
}
.game-name {
font-size: 16px;
font-weight: 600;
margin: 0 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.game-platform {
font-size: 13px;
color: var(--el-text-color-secondary);
margin: 0 0 8px;
}
.game-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.tag {
padding: 2px 8px;
background: var(--el-fill-color-light);
border-radius: 4px;
font-size: 11px;
color: var(--el-text-color-secondary);
}
</style>