From d76ecb15d660fab7b62697a7a4be5cb49a837746 Mon Sep 17 00:00:00 2001 From: congsh Date: Fri, 24 Apr 2026 16:33:08 +0800 Subject: [PATCH] feat: game aliases + edit permissions + scoped game select (v0.3.6) - Add aliases field to games (json array), searchable in game library and team session - Add edit functionality inside GameDetailDialog (creator/owner/admin can modify) - Add permission control for delete/edit buttons (only visible to authorized users) - Scope GameSelectDialog to current group only - Support aliases in import/export Co-Authored-By: Claude Opus 4.7 --- .../pb_migrations/1776942666_updated_games.js | 18 ++ .../pb_migrations/1777017167_updated_games.js | 37 ++++ frontend/src/api/games.ts | 32 ++- .../src/components/game/GameDetailDialog.vue | 170 +++++++++++++++- .../src/components/game/GameFormDialog.vue | 183 ++++++++++++++++++ .../src/components/game/ImportGamesDialog.vue | 6 +- .../src/components/team/GameSelectDialog.vue | 3 +- frontend/src/components/team/InviteButton.vue | 1 + .../src/components/team/TeamSessionPanel.vue | 2 +- frontend/src/types/index.ts | 1 + frontend/src/views/Changelog.vue | 14 ++ frontend/src/views/GamesLibrary.vue | 19 +- 12 files changed, 469 insertions(+), 17 deletions(-) create mode 100644 backend/pb_migrations/1776942666_updated_games.js create mode 100644 backend/pb_migrations/1777017167_updated_games.js create mode 100644 frontend/src/components/game/GameFormDialog.vue diff --git a/backend/pb_migrations/1776942666_updated_games.js b/backend/pb_migrations/1776942666_updated_games.js new file mode 100644 index 0000000..18bfced --- /dev/null +++ b/backend/pb_migrations/1776942666_updated_games.js @@ -0,0 +1,18 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("x5adjlc0txf16r8") + + collection.listRule = "@request.auth.id != \"\" && group.members ~ @request.auth.id" + collection.viewRule = "@request.auth.id != \"\" && group.members ~ @request.auth.id" + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("x5adjlc0txf16r8") + + collection.listRule = "group.owner = @request.auth.id || group.members.id = @request.auth.id" + collection.viewRule = "group.owner = @request.auth.id || group.members.id = @request.auth.id" + + return dao.saveCollection(collection) +}) diff --git a/backend/pb_migrations/1777017167_updated_games.js b/backend/pb_migrations/1777017167_updated_games.js new file mode 100644 index 0000000..66fdd8a --- /dev/null +++ b/backend/pb_migrations/1777017167_updated_games.js @@ -0,0 +1,37 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("x5adjlc0txf16r8") + + // add aliases field (json array of strings) + collection.schema.addField(new SchemaField({ + "system": false, + "id": "aliases_field", + "name": "aliases", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 5000 + } + })) + + // update rules: allow group admins to update/delete + collection.updateRule = "group.owner = @request.auth.id || addedBy = @request.auth.id || group.admins ~ @request.auth.id" + collection.deleteRule = "group.owner = @request.auth.id || addedBy = @request.auth.id || group.admins ~ @request.auth.id" + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("x5adjlc0txf16r8") + + // remove aliases field + collection.schema.removeField("aliases_field") + + // restore original rules + collection.updateRule = "group.owner = @request.auth.id || addedBy = @request.auth.id" + collection.deleteRule = "group.owner = @request.auth.id || addedBy = @request.auth.id" + + return dao.saveCollection(collection) +}) diff --git a/frontend/src/api/games.ts b/frontend/src/api/games.ts index 9997810..f991d6c 100644 --- a/frontend/src/api/games.ts +++ b/frontend/src/api/games.ts @@ -17,7 +17,7 @@ export async function getGroupGames(groupId: string, options?: { const { page = 1, limit = 50, platform, search } = options || {} let filter = `group="${groupId}"` if (platform) filter += ` && platform="${platform}"` - if (search) filter += ` && name ~ "${search}"` + if (search) filter += ` && (name ~ "${search}" || aliases ~ "${search}")` const result = await pb.collection('games').getList(page, limit, { filter, @@ -30,6 +30,7 @@ export async function getGroupGames(groupId: string, options?: { // 添加游戏到群组 export async function addGame(groupId: string, data: { name: string + aliases?: string[] platform?: GamePlatform tags?: string[] coverFile?: File @@ -41,12 +42,39 @@ export async function addGame(groupId: string, data: { formData.append('addedBy', user?.id || '') formData.append('popularCount', '0') if (data.platform) formData.append('platform', data.platform) + if (data.aliases && data.aliases.length > 0) formData.append('aliases', JSON.stringify(data.aliases)) 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 }) } +// 更新游戏 +export async function updateGame(gameId: string, data: { + name?: string + aliases?: string[] + platform?: GamePlatform | '' + tags?: string[] + coverFile?: File +}) { + const formData = new FormData() + if (data.name !== undefined) formData.append('name', data.name) + if (data.aliases && data.aliases.length > 0) { + formData.append('aliases', JSON.stringify(data.aliases)) + } else { + formData.append('aliases', '[]') + } + if (data.platform !== undefined) formData.append('platform', data.platform || '') + if (data.tags && data.tags.length > 0) { + formData.append('tags', JSON.stringify(data.tags)) + } else { + formData.append('tags', '[]') + } + if (data.coverFile) formData.append('cover', data.coverFile) + + return pb.collection('games').update(gameId, formData, { $autoCancel: false }) +} + // 删除游戏 export async function deleteGame(gameId: string) { return pb.collection('games').delete(gameId, { $autoCancel: false }) @@ -146,7 +174,7 @@ export function getAllPlatforms(): GamePlatform[] { // 搜索游戏(用于 GameSelectDialog,全局搜索或群组内搜索) export async function searchGames(query: string, groupId?: string, limit = 20): Promise { if (!query.trim()) return [] - let filter = `name ~ "${query}"` + let filter = `(name ~ "${query}" || aliases ~ "${query}")` if (groupId) filter += ` && group="${groupId}"` const result = await pb.collection('games').getList(1, limit, { diff --git a/frontend/src/components/game/GameDetailDialog.vue b/frontend/src/components/game/GameDetailDialog.vue index 2d8a009..8a34c7c 100644 --- a/frontend/src/components/game/GameDetailDialog.vue +++ b/frontend/src/components/game/GameDetailDialog.vue @@ -1,9 +1,12 @@ + + + + diff --git a/frontend/src/components/game/ImportGamesDialog.vue b/frontend/src/components/game/ImportGamesDialog.vue index 7b0a25e..4b0ef4d 100644 --- a/frontend/src/components/game/ImportGamesDialog.vue +++ b/frontend/src/components/game/ImportGamesDialog.vue @@ -52,7 +52,7 @@ function parseCSV(text: string) { const values = lines[i].split(',').map(v => v.trim()) const obj: any = {} headers.forEach((h, idx) => { - if (h === 'tags' && values[idx]) { + if ((h === 'tags' || h === 'aliases') && values[idx]) { obj[h] = values[idx].split(';').map((t: string) => t.trim()) } else { obj[h] = values[idx] || undefined @@ -97,8 +97,8 @@ function reset() {
-

JSON 格式: [{"name":"LOL","platform":"PC","tags":["MOBA"]}]

-

CSV 格式: name,platform,tags,cover(tags 用分号分隔)

+

JSON 格式: [{"name":"英雄联盟","aliases":["LOL","撸啊撸"],"platform":"PC","tags":["MOBA"]}]

+

CSV 格式: name,aliases,platform,tags,cover(aliases/tags 用分号分隔)

diff --git a/frontend/src/components/team/GameSelectDialog.vue b/frontend/src/components/team/GameSelectDialog.vue index 68e7b79..29f6c1d 100644 --- a/frontend/src/components/team/GameSelectDialog.vue +++ b/frontend/src/components/team/GameSelectDialog.vue @@ -5,6 +5,7 @@ import type { Game } from '@/types' const props = defineProps<{ modelValue: boolean + groupId?: string }>() const emit = defineEmits<{ @@ -37,7 +38,7 @@ async function handleSearch() { } loading.value = true try { - games.value = await searchGames(searchQuery.value) + games.value = await searchGames(searchQuery.value, props.groupId) } catch (error) { console.error('搜索游戏失败:', error) } finally { diff --git a/frontend/src/components/team/InviteButton.vue b/frontend/src/components/team/InviteButton.vue index 4afe258..ccb3f95 100644 --- a/frontend/src/components/team/InviteButton.vue +++ b/frontend/src/components/team/InviteButton.vue @@ -73,6 +73,7 @@ async function sendInvite(teamSessionId: string) { diff --git a/frontend/src/components/team/TeamSessionPanel.vue b/frontend/src/components/team/TeamSessionPanel.vue index f1d4b5b..a2cbc10 100644 --- a/frontend/src/components/team/TeamSessionPanel.vue +++ b/frontend/src/components/team/TeamSessionPanel.vue @@ -168,7 +168,7 @@ async function handleGameSelected(gameName: string) {
- +