diff --git a/backend/pb_migrations/1776938195_updated_games_cover_to_file.js b/backend/pb_migrations/1776938195_updated_games_cover_to_file.js
new file mode 100644
index 0000000..a7276c1
--- /dev/null
+++ b/backend/pb_migrations/1776938195_updated_games_cover_to_file.js
@@ -0,0 +1,51 @@
+///
+migrate((db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("x5adjlc0txf16r8")
+
+ // remove old url cover field
+ collection.schema.removeField("cover_field")
+
+ // add new file cover field
+ collection.schema.addField(new SchemaField({
+ "system": false,
+ "id": "cover_file",
+ "name": "cover",
+ "type": "file",
+ "required": false,
+ "presentable": false,
+ "unique": false,
+ "options": {
+ "maxSelect": 1,
+ "maxSize": 5242880,
+ "mimeTypes": ["image/jpeg","image/png","image/gif","image/webp","image/svg+xml"],
+ "thumbs": null,
+ "protected": false
+ }
+ }))
+
+ return dao.saveCollection(collection)
+}, (db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("x5adjlc0txf16r8")
+
+ // remove file cover field
+ collection.schema.removeField("cover_file")
+
+ // restore url cover field
+ collection.schema.addField(new SchemaField({
+ "system": false,
+ "id": "cover_field",
+ "name": "cover",
+ "type": "url",
+ "required": false,
+ "presentable": false,
+ "unique": false,
+ "options": {
+ "exceptDomains": null,
+ "onlyDomains": null
+ }
+ }))
+
+ return dao.saveCollection(collection)
+})
diff --git a/frontend/src/api/games.ts b/frontend/src/api/games.ts
index 8f49f30..9997810 100644
--- a/frontend/src/api/games.ts
+++ b/frontend/src/api/games.ts
@@ -1,6 +1,12 @@
import pb from './pocketbase'
import type { Game, GamePlatform, GameComment } from '@/types'
+// 生成游戏封面 URL
+export function getGameCoverUrl(game: Game, thumb?: string): string {
+ if (!game.cover) return ''
+ return pb.files.getUrl(game, game.cover, thumb ? { thumb } : undefined) as string
+}
+
// 获取群组的游戏列表
export async function getGroupGames(groupId: string, options?: {
page?: number
@@ -26,15 +32,19 @@ export async function addGame(groupId: string, data: {
name: string
platform?: GamePlatform
tags?: string[]
- cover?: string
+ coverFile?: File
}) {
const user = pb.authStore.model
- return pb.collection('games').create({
- ...data,
- group: groupId,
- addedBy: user?.id,
- popularCount: 0
- }, { $autoCancel: false })
+ const formData = new FormData()
+ formData.append('name', data.name)
+ formData.append('group', groupId)
+ formData.append('addedBy', user?.id || '')
+ formData.append('popularCount', '0')
+ if (data.platform) formData.append('platform', data.platform)
+ if (data.tags && data.tags.length > 0) formData.append('tags', JSON.stringify(data.tags))
+ if (data.coverFile) formData.append('cover', data.coverFile)
+
+ return pb.collection('games').create(formData, { $autoCancel: false })
}
// 删除游戏
@@ -42,12 +52,11 @@ export async function deleteGame(gameId: string) {
return pb.collection('games').delete(gameId, { $autoCancel: false })
}
-// 导入游戏(批量添加)
+// 导入游戏(批量添加,无封面文件)
export async function importGames(groupId: string, games: Array<{
name: string
platform?: GamePlatform
tags?: string[]
- cover?: string
}>) {
const results = []
for (const game of games) {
diff --git a/frontend/src/components/game/AddGameDialog.vue b/frontend/src/components/game/AddGameDialog.vue
index de98e91..4b3588c 100644
--- a/frontend/src/components/game/AddGameDialog.vue
+++ b/frontend/src/components/game/AddGameDialog.vue
@@ -4,6 +4,8 @@ import { addGame } from '@/api/games'
import type { GamePlatform } from '@/types'
import { ElMessage } from 'element-plus'
import { getAllPlatforms } from '@/api/games'
+import { Plus } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
const props = defineProps<{
modelValue: boolean
@@ -23,10 +25,23 @@ const visible = computed({
const name = ref('')
const platform = ref('')
const tagsInput = ref('')
-const cover = ref('')
+const coverFile = ref(null)
+const coverPreview = ref('')
const loading = ref(false)
const platforms = getAllPlatforms()
+function handleFileChange(uploadFile: UploadFile) {
+ if (uploadFile.raw) {
+ coverFile.value = uploadFile.raw
+ coverPreview.value = URL.createObjectURL(uploadFile.raw)
+ }
+}
+
+function handleFileRemove() {
+ coverFile.value = null
+ coverPreview.value = ''
+}
+
async function handleSubmit() {
if (!name.value.trim()) {
ElMessage.warning('请输入游戏名称')
@@ -39,13 +54,14 @@ async function handleSubmit() {
name: name.value.trim(),
platform: platform.value || undefined,
tags: tags.length > 0 ? tags : undefined,
- cover: cover.value.trim() || undefined
+ coverFile: coverFile.value || undefined
})
ElMessage.success('添加成功')
name.value = ''
platform.value = ''
tagsInput.value = ''
- cover.value = ''
+ coverFile.value = null
+ coverPreview.value = ''
emit('created')
visible.value = false
} catch (error: any) {
@@ -74,8 +90,23 @@ async function handleSubmit() {
-
-
+
+
+
+
![封面预览]()
+
+
+
@@ -89,4 +120,19 @@ async function handleSubmit() {
.form-fields { display: flex; flex-direction: column; gap: 18px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: 13px; font-weight: 500; color: var(--gg-text-secondary); }
+.cover-preview img {
+ width: 100%;
+ max-height: 200px;
+ object-fit: contain;
+ border-radius: 6px;
+}
+.upload-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 20px;
+ color: var(--gg-text-muted);
+ font-size: 13px;
+}
diff --git a/frontend/src/components/game/GameDetailDialog.vue b/frontend/src/components/game/GameDetailDialog.vue
index 66bfff6..2d8a009 100644
--- a/frontend/src/components/game/GameDetailDialog.vue
+++ b/frontend/src/components/game/GameDetailDialog.vue
@@ -1,7 +1,7 @@