@@ -1,9 +1,15 @@
<!-- src / views / GamesLibrary . vue -- >
< script setup lang = "ts" >
import { ref , onMounted } from 'vue'
import { getGames , getAllPlatforms } from '@/api/games '
import { ref , onMounted , computed } from 'vue'
import { useGroupStore } from '@/stores/group '
import { getGroupGames , deleteGame , exportGames , getAllPlatforms } from '@/api/games'
import type { Game , GamePlatform } from '@/types'
import GameDetailDialog from '@/components/game/GameDetailDialog.vue'
import AddGameDialog from '@/components/game/AddGameDialog.vue'
import ImportGamesDialog from '@/components/game/ImportGamesDialog.vue'
import { ElMessage , ElMessageBox } from 'element-plus'
const groupStore = useGroupStore ( )
const games = ref < Game [ ] > ( [ ] )
const loading = ref ( false )
@@ -12,16 +18,23 @@ const selectedPlatform = ref<GamePlatform | ''>('')
const platforms = getAllPlatforms ( )
const selectedGame = ref < Game | null > ( null )
const showDetail = ref ( false )
const showAddGame = ref ( false )
const showImport = ref ( false )
const currentGroupId = computed ( ( ) => groupStore . currentGroupId )
onMounted ( async ( ) => {
await loadGames ( )
if ( currentGroupId . value ) {
await loadGames ( )
}
} )
async function loadGames ( ) {
if ( ! currentGroupId . value ) return
try {
loading . value = true
const result = await getGames ( {
limit : 50 ,
const result = await getGroupGames ( currentGroupId . value , {
limit : 100 ,
search : searchQuery . value || undefined ,
platform : selectedPlatform . value || undefined
} )
@@ -33,174 +46,179 @@ async function loadGames() {
}
}
function handleSearch ( ) {
loadGames ( )
}
function openGameDetail ( game : Game ) {
selectedGame . value = game
showDetail . value = true
}
function handleCreateTeam ( _gameName : string ) {
// TODO: 导航到群组页面触发组队流程
async function handleDeleteGame ( game : Game ) {
try {
await ElMessageBox . confirm ( ` 确定要删除「 ${ game . name } 」吗? ` , '确认' , {
confirmButtonText : '确定' ,
cancelButtonText : '取消' ,
type : 'warning'
} )
await deleteGame ( game . id )
ElMessage . success ( '删除成功' )
await loadGames ( )
} catch { /* 用户取消 */ }
}
function handleExport ( ) {
if ( ! currentGroupId . value ) return
exportGames ( currentGroupId . value ) . then ( data => {
const json = JSON . stringify ( data . map ( g => ( {
name : g . name ,
platform : g . platform ,
tags : g . tags ,
cover : g . cover
} ) ) , null , 2 )
const blob = new Blob ( [ json ] , { type : 'application/json' } )
const url = URL . createObjectURL ( blob )
const a = document . createElement ( 'a' )
a . href = url
a . download = ` games-export- ${ currentGroupId . value } .json `
a . click ( )
URL . revokeObjectURL ( url )
ElMessage . success ( ` 已导出 ${ data . length } 个游戏 ` )
} )
}
async function handleGameAdded ( ) {
showAddGame . value = false
await loadGames ( )
}
async function handleImportComplete ( ) {
showImport . value = false
await 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 "
<!-- 无群组提示 -- >
< div v-if = "!currentGroupId" class="no-group" >
< p class = "no-group-text" > 请先选择一个群组 < / p >
< p class = "no-group-hint" > 在左侧选择或创建群组后 , 即可管理游戏库 < / p >
< / div >
< template v-else >
< div class = "page-header" >
< h1 > 游戏库 < / h1 >
< div class = "toolbar " >
< el-input
v-model = "searchQuery "
placeholder = "搜索游戏... "
clearable
style = "width: 240px "
@input ="loadGames"
/ >
< / 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" >
< div v-for = "game in games" :key="game.id" class="game-card" @click="openGameDetail(game)" >
< 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 >
< el-select v-model = "selectedPlatform" placeholder="平台" clearable style="width: 120px" @change="loadGames" >
< el -option v-for = "p in platforms" :key="p" :label="p" :value="p" / >
< / el-select >
< button class = "tool-btn primary" @click ="showAddGame = true" > 添加游戏 < / button >
< button class = "tool-btn" @click ="showImport = true" > 导入 < / button >
< button class = "tool-btn" @click ="handleExport" > 导出 < / button >
< / div >
< / div >
< / div >
< GameDetailDialog
v-model = "showDetail"
:game = "selectedGame"
@ create -team = " handleCreateTeam "
/ >
< div v-if = "loading" class="loading" > 加载中... < / div >
< div v-else-if = "games.length === 0" class="empty" >
< p class = "empty-text" > 暂无游戏 < / p >
< p class = "empty-hint" > 点击 「 添加游戏 」 或 「 导入 」 开始管理游戏库 < / p >
< / div >
< div v-else class = "games-grid" >
< div v-for = "game in games" :key="game.id" class="game-card" @click="openGameDetail(game)" >
< img : src = "game.cover || '/game-placeholder.svg'" :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 >
< button class = "delete-btn" @click.stop ="handleDeleteGame(game)" title = "删除" > ✕ < / button >
< / div >
< / div >
< GameDetailDialog v-model = "showDetail" :game="selectedGame" :group-id="currentGroupId" @deleted="loadGames" / >
< AddGameDialog v-model = "showAddGame" :group-id="currentGroupId" @created="handleGameAdded" / >
< ImportGamesDialog v-model = "showImport" :group-id="currentGroupId" @imported="handleImportComplete" / >
< / template >
< / div >
< / template >
< style scoped >
. games - library {
width : 100 % ;
}
. games - library { width : 100 % ; }
. page - header {
margin - bottom : 28 px ;
. no - group {
text - align : center ;
padding : 120 px 32 px ;
}
. no - group - text { font - size : 18 px ; font - weight : 600 ; color : var ( -- gg - text - secondary ) ; margin : 0 0 8 px ; }
. no - group - hint { font - size : 14 px ; color : var ( -- gg - text - muted ) ; margin : 0 ; }
. page - header { margin - bottom : 24 px ; }
. page - header h1 {
font - size : 28 px ;
font - weight : 700 ;
margin : 0 0 16 px ;
background : var ( -- gg - gradient ) ;
- webkit - background - clip : text ;
- webkit - text - fill - color : transparent ;
background - clip : text ;
font - size : 28 px ; font - weight : 700 ; margin : 0 0 16 px ;
background : var ( -- gg - gradient ) ; - webkit - background - clip : text ; - webkit - text - fill - color : transparent ; background - clip : text ;
}
. filters {
display : flex ;
gap : 12 px ;
}
. toolbar { display : flex ; gap : 10 px ; align - items : center ; flex - wrap : wrap ; }
. filters . el - input {
width : 300 px ;
. tool - btn {
padding : 8 px 18 px ; border : 1 px solid var ( -- gg - border ) ; border - radius : var ( -- gg - radius - sm ) ;
background : var ( -- gg - bg - card ) ; color : var ( -- gg - text - secondary ) ; font - size : 13 px ; font - weight : 500 ;
cursor : pointer ; transition : all 0.2 s ; white - space : nowrap ;
}
. tool - btn : hover { border - color : var ( -- gg - primary ) ; color : var ( -- gg - primary ) ; }
. tool - btn . primary {
background : var ( -- gg - gradient - green ) ; border - color : transparent ; color : white ;
}
. tool - btn . primary : hover { opacity : 0.9 ; }
. loading {
text - align : center ;
padding : 80 px 32 px ;
color : var ( -- gg - text - muted ) ;
font - size : 16 px ;
}
. loading { text - align : center ; padding : 80 px 32 px ; color : var ( -- gg - text - muted ) ; }
. empty {
text - align : center ;
padding : 80 px 32 px ;
color : var ( -- gg - text - muted ) ;
font - size : 15 px ;
}
. empty { text - align : center ; padding : 80 px 32 px ; }
. empty - text { font - size : 16 px ; font - weight : 500 ; color : var ( -- gg - text - secondary ) ; margin : 0 0 8 px ; }
. empty - hint { font - size : 14 px ; color : var ( -- gg - text - muted ) ; margin : 0 ; }
. games - grid {
display : grid ;
grid - template - columns : repeat ( auto - fill , minmax ( 200 px , 1 fr ) ) ;
gap : 16 px ;
display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 200 px , 1 fr ) ) ; gap : 16 px ;
}
. game - card {
border - radius : var ( -- gg - radius - md ) ;
overflow : hidden ;
cursor : pointer ;
background : var ( -- gg - bg - card ) ;
border : 1 px solid var ( -- gg - border ) ;
position : relative ; border - radius : var ( -- gg - radius - md ) ; overflow : hidden ; cursor : pointer ;
background : var ( -- gg - bg - card ) ; border : 1 px solid var ( -- gg - border ) ;
transition : transform 0.2 s , box - shadow 0.2 s , border - color 0.2 s ;
}
. game - card : hover { transform : translateY ( - 4 px ) ; border - color : var ( -- gg - primary ) ; box - shadow : 0 4 px 20 px rgba ( 5 , 150 , 105 , 0.15 ) ; }
. game - card : hover {
transform : translateY ( - 4 px ) ;
border - color : var ( -- gg - primary ) ;
box - shadow : 0 4 px 20 px rgba ( 5 , 150 , 105 , 0.15 ) ;
}
. game - cover { width : 100 % ; aspect - ratio : 3 / 4 ; object - fit : cover ; }
. game - cover {
width : 100 % ;
aspect - ratio : 3 / 4 ;
object - fit : cover ;
}
. game - info {
padding : 14 px ;
}
. game - info { padding : 14 px ; }
. game - name {
font - size : 16 px ;
font - weight : 700 ;
margin : 0 0 4 px ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
color : var ( -- gg - text ) ;
}
. game - platform {
font - size : 13 px ;
color : var ( -- gg - text - secondary ) ;
margin : 0 0 8 px ;
}
. game - tags {
display : flex ;
flex - wrap : wrap ;
gap : 4 px ;
font - size : 16 px ; font - weight : 700 ; margin : 0 0 4 px ; color : var ( -- gg - text ) ;
overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ;
}
. game - platform { font - size : 13 px ; color : var ( -- gg - text - secondary ) ; margin : 0 0 8 px ; }
. game - tags { display : flex ; flex - wrap : wrap ; gap : 4 px ; }
. tag {
padding : 3 px 10 px ;
background : var ( -- gg - bg - elevated ) ;
border - radius : 6 px ;
font - size : 11 px ;
color : var ( -- gg - primary ) ;
font - weight : 500 ;
padding : 3 px 10 px ; background : var ( -- gg - bg - elevated ) ; border - radius : 6 px ;
font - size : 11 px ; color : var ( -- gg - primary ) ; font - weight : 500 ;
}
. delete - btn {
position : absolute ; top : 8 px ; right : 8 px ; width : 28 px ; height : 28 px ;
border : none ; border - radius : 50 % ; background : rgba ( 0 , 0 , 0 , 0.5 ) ; color : white ;
font - size : 12 px ; cursor : pointer ; opacity : 0 ; transition : opacity 0.2 s ;
display : flex ; align - items : center ; justify - content : center ;
}
. game - card : hover . delete - btn { opacity : 1 ; }
. delete - btn : hover { background : var ( -- gg - danger ) ; }
< / style >