Files
gamegroup2/frontend/src/components-mobile/game/AddGameSheetMobile.vue
T
锦麟 王 6ba671d2c3 feat(mobile): stage 7 - games library (group-scoped, detail/comments/favorites/add/import)
- rewrite GamesLibraryMobile.vue for uat model (games bound to group, not global):
  group selector + search + platform filter + 2-col grid + add/import entries
- new GameDetailSheetMobile.vue: cover/name/aliases/tags/platform + favorite/edit/delete
  + quick-team + comments list with rating
- new AddGameSheetMobile.vue: name/aliases/platform/tags/cover-upload (bound to group)
- new ImportGamesSheetMobile.vue: bulk import via text (name | platform | tags per line)
- router: wire GamesLibrary mobile view
- diverges from master: uat games API requires groupId (addGame/importGames/getGroupGames)
  vs master's global getPopularGames/searchGames; mobile rewritten to match uat PC behavior

build verified: vue-tsc + vite build pass
2026-06-18 11:17:38 +08:00

229 lines
5.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- src/components-mobile/game/AddGameSheetMobile.vue -->
<!-- 添加游戏表单绑定到当前群组名称/别名/平台/标签/封面 -->
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { addGame, getAllPlatforms } from '@/api/games'
import type { GamePlatform } from '@/types'
import { showSuccessToast, showFailToast } from 'vant'
const props = defineProps<{
show: boolean
groupId: string
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
'saved': []
}>()
const visible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
const platforms = getAllPlatforms()
const form = ref({
name: '',
aliases: '',
platform: '' as GamePlatform | '',
tags: ''
})
const coverFile = ref<File | null>(null)
const coverPreview = ref('')
const saving = ref(false)
watch(() => props.show, (val) => {
if (val) {
form.value = { name: '', aliases: '', platform: '', tags: '' }
coverFile.value = null
coverPreview.value = ''
}
})
function onFileChange(e: Event) {
const input = e.target as HTMLInputElement
if (input.files?.[0]) {
coverFile.value = input.files[0]
coverPreview.value = URL.createObjectURL(input.files[0])
}
}
function clearCover() {
coverFile.value = null
coverPreview.value = ''
}
async function handleSubmit() {
if (!form.value.name.trim()) {
showFailToast('请输入游戏名称')
return
}
try {
saving.value = true
const aliases = form.value.aliases.split(',').map(t => t.trim()).filter(Boolean)
const tags = form.value.tags.split(',').map(t => t.trim()).filter(Boolean)
await addGame(props.groupId, {
name: form.value.name.trim(),
aliases: aliases.length > 0 ? aliases : undefined,
platform: form.value.platform || undefined,
tags: tags.length > 0 ? tags : undefined,
coverFile: coverFile.value || undefined
})
showSuccessToast('添加成功')
emit('saved')
} catch (e: any) {
showFailToast(e.message || '添加失败')
} finally {
saving.value = false
}
}
</script>
<template>
<van-popup
v-model:show="visible"
position="bottom"
round
closeable
:style="{ height: '80%' }"
>
<div class="add-sheet">
<div class="sheet-title">添加游戏</div>
<van-cell-group inset>
<van-field v-model="form.name" label="名称" placeholder="游戏名称" required />
<van-field v-model="form.aliases" label="别名" placeholder="逗号分隔,如 LOL,英雄联盟" />
<van-field name="platform" label="平台">
<template #input>
<select v-model="form.platform" class="native-select">
<option value="">不指定</option>
<option v-for="p in platforms" :key="p" :value="p">{{ p }}</option>
</select>
</template>
</van-field>
<van-field v-model="form.tags" label="标签" placeholder="逗号分隔,如 MOBA,竞技" />
</van-cell-group>
<!-- 封面上传 -->
<div class="cover-field">
<div class="field-label">封面图</div>
<div class="cover-upload">
<div v-if="coverPreview" class="cover-preview">
<img :src="coverPreview" alt="" />
<button class="cover-clear" @click="clearCover">
<van-icon name="cross" />
</button>
</div>
<label v-else class="cover-picker">
<van-icon name="photograph" size="28" />
<span>上传封面</span>
<input type="file" accept="image/*" class="file-input" @change="onFileChange" />
</label>
</div>
</div>
<div class="sheet-actions">
<van-button type="primary" block round :loading="saving" @click="handleSubmit">
添加游戏
</van-button>
</div>
</div>
</van-popup>
</template>
<style scoped>
.add-sheet {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 16px 0 24px;
}
.sheet-title {
text-align: center;
font-size: 16px;
font-weight: 600;
padding: 8px 0 16px;
}
.native-select {
border: 1px solid var(--gg-border);
border-radius: var(--gg-radius-sm);
padding: 6px 10px;
font-size: 14px;
background: var(--gg-bg-card);
width: 100%;
}
.cover-field {
padding: 16px;
}
.field-label {
font-size: 13px;
font-weight: 500;
color: var(--gg-text-secondary);
margin-bottom: 8px;
}
.cover-upload {
display: flex;
}
.cover-preview {
position: relative;
width: 90px;
height: 120px;
border-radius: var(--gg-radius-sm);
overflow: hidden;
border: 1px solid var(--gg-border);
}
.cover-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-clear {
position: absolute;
top: 2px;
right: 2px;
width: 20px;
height: 20px;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.cover-picker {
width: 90px;
height: 120px;
border: 1px dashed var(--gg-border);
border-radius: var(--gg-radius-sm);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--gg-text-muted);
font-size: 12px;
cursor: pointer;
}
.file-input {
display: none;
}
.sheet-actions {
padding: 20px 16px 0;
margin-top: auto;
}
</style>