feat: complete Phase 1 - game library, lifecycle, realtime sync

- Seed 33 popular games across 5 platforms via admin API script
- Add GameDetailDialog with game info and quick-team button
- Update GamesLibrary with game card click to open detail dialog
- Update Home hot games to open detail dialog instead of navigating
- Rewrite invitation accept: frontend auto-joins team + updates status
- Add user status reset on team dissolution (endGame)
- Add start game / dissolve buttons to TeamSessionPanel lifecycle
- Integrate realtime subscriptions in GroupView and Layout
- Add notification store realtime invitation listener
- Add placeholder images for game covers and avatars
- Remove Go hooks, add JS hooks placeholder + Docker mount

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
congsh
2026-04-17 20:23:39 +08:00
parent 0bcf39bb4b
commit 802712c662
17 changed files with 394 additions and 180 deletions
+1
View File
@@ -10,6 +10,7 @@ services:
volumes:
- ./pb_data:/pb_data
- ./pb_migrations:/pb_migrations
- ./pb_hooks:/pb_hooks
environment:
- GO_ENV=production
restart: unless-stopped
-5
View File
@@ -1,5 +0,0 @@
module gamegroup-hooks
go 1.21
require github.com/pocketbase/pocketbase v0.22.4
-159
View File
@@ -1,159 +0,0 @@
package main
import (
"log"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
// ── Groups ──
app.OnRecordBeforeCreateRequest("groups").Add(func(e *core.RecordCreateEvent) error {
authRecord, _ := e.HttpContext.AuthRecord()
if authRecord == nil {
return apis.NewForbiddenError("需要登录", nil)
}
e.Record.Set("owner", authRecord.Id)
e.Record.Set("members", []string{authRecord.Id})
return nil
})
app.OnRecordBeforeUpdateRequest("groups").Add(func(e *core.RecordUpdateEvent) error {
authRecord, _ := e.HttpContext.AuthRecord()
if authRecord == nil {
return apis.NewForbiddenError("需要登录", nil)
}
original, err := app.Dao().FindRecordById("groups", e.Record.Id)
if err != nil {
return apis.NewNotFoundError("群组不存在", nil)
}
if original.GetString("owner") != authRecord.Id {
return apis.NewForbiddenError("只有群组所有者可以更新群组", nil)
}
// 保护 owner 字段不被篡改
e.Record.Set("owner", original.GetString("owner"))
// 验证 members 数量
members := e.Record.GetStringSlice("members")
maxMembers := e.Record.GetInt("maxMembers")
if maxMembers > 0 && len(members) > maxMembers {
return apis.NewBadRequestError("成员数量超过上限", nil)
}
return nil
})
app.OnRecordBeforeDeleteRequest("groups").Add(func(e *core.RecordDeleteEvent) error {
authRecord, _ := e.HttpContext.AuthRecord()
if authRecord == nil {
return apis.NewForbiddenError("需要登录", nil)
}
isOwner, err := isGroupOwner(app, e.Record.Id, authRecord.Id)
if err != nil || !isOwner {
return apis.NewForbiddenError("只有群组所有者可以删除群组", nil)
}
return nil
})
// ── Team Sessions ──
app.OnRecordBeforeCreateRequest("team_sessions").Add(func(e *core.RecordCreateEvent) error {
authRecord, _ := e.HttpContext.AuthRecord()
if authRecord == nil {
return apis.NewForbiddenError("需要登录", nil)
}
groupId := e.Record.GetString("sourceGroup")
isMember, err := isGroupMember(app, groupId, authRecord.Id)
if err != nil || !isMember {
return apis.NewForbiddenError("只有群组成员可以创建团队会话", nil)
}
return nil
})
// ── Invitations ──
app.OnRecordBeforeCreateRequest("invitations").Add(func(e *core.RecordCreateEvent) error {
authRecord, _ := e.HttpContext.AuthRecord()
if authRecord == nil {
return apis.NewForbiddenError("需要登录", nil)
}
// 强制设置 from 为当前用户
e.Record.Set("from", authRecord.Id)
e.Record.Set("status", "pending")
return nil
})
// 接受邀请:自动加入 team session + 更新用户状态
app.OnRecordBeforeUpdateRequest("invitations").Add(func(e *core.RecordUpdateEvent) error {
authRecord, _ := e.HttpContext.AuthRecord()
if authRecord == nil {
return apis.NewForbiddenError("需要登录", nil)
}
// 只有 recipient 可以更新邀请
if e.Record.GetString("to") != authRecord.Id {
return apis.NewForbiddenError("无权操作此邀请", nil)
}
newStatus := e.Record.GetString("status")
if newStatus == "accepted" {
teamSessionId := e.Record.GetString("teamSession")
teamSession, err := app.Dao().FindRecordById("team_sessions", teamSessionId)
if err != nil {
return apis.NewNotFoundError("临时小组不存在", nil)
}
// 将用户加入 team session members
members := teamSession.GetStringSlice("members")
alreadyIn := false
for _, m := range members {
if m == authRecord.Id {
alreadyIn = true
break
}
}
if !alreadyIn {
members = append(members, authRecord.Id)
teamSession.Set("members", members)
if err := app.Dao().SaveRecord(teamSession); err != nil {
return apis.NewBadRequestError("加入临时小组失败", nil)
}
}
// 更新用户状态为 in_team
user, err := app.Dao().FindRecordById("users", authRecord.Id)
if err == nil {
user.Set("status", "in_team")
app.Dao().SaveRecord(user)
}
}
return nil
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
func isGroupMember(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) {
group, err := app.Dao().FindRecordById("groups", groupId)
if err != nil {
return false, err
}
members := group.GetStringSlice("members")
for _, member := range members {
if member == userId {
return true, nil
}
}
return false, nil
}
func isGroupOwner(app *pocketbase.PocketBase, groupId string, userId string) (bool, error) {
group, err := app.Dao().FindRecordById("groups", groupId)
if err != nil {
return false, err
}
return group.GetString("owner") == userId, nil
}
+5
View File
@@ -0,0 +1,5 @@
// JS hooks placeholder - PocketBase 0.22.4 muchobien image may not support JS VM
// Key logic handled in frontend instead:
// - Groups: frontend sets owner + members on create
// - Invitations: frontend auto-joins team session on accept
// - Team dissolution: frontend resets member statuses on end
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# seed-games.sh - 批量导入热门游戏数据到 PocketBase
set -e
PB_URL="${PB_URL:-http://192.168.1.14:8090}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}"
ADMIN_PASS="${ADMIN_PASS:-admin123456}"
echo "获取管理员 Token..."
TOKEN=$(curl -s -X POST "$PB_URL/api/admins/auth-with-password" \
-H "Content-Type: application/json" \
-d "{\"identity\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASS\"}" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
echo "Token 获取成功,开始导入游戏..."
create_game() {
local name="$1" platform="$2" tags="$3" count="$4"
curl -s -X POST "$PB_URL/api/collections/games/records" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$name\",\"platform\":\"$platform\",\"tags\":$tags,\"popularCount\":$count}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' ✓ {d.get(\"name\",\"ERROR\")}')"
}
# PC 游戏
create_game "League of Legends" "PC" '["MOBA","竞技","5v5"]' 10000
create_game "Counter-Strike 2" "PC" '["FPS","射击","竞技"]' 9500
create_game "Valorant" "PC" '["FPS","射击","战术"]' 9000
create_game "Dota 2" "PC" '["MOBA","竞技","策略"]' 8500
create_game "Apex Legends" "PC" '["FPS","大逃杀","吃鸡"]' 8000
create_game "PUBG" "PC" '["大逃杀","吃鸡","射击"]' 7500
create_game "Overwatch 2" "PC" '["FPS","英雄射击","团队"]' 7000
create_game "Minecraft" "PC" '["沙盒","生存","建造"]' 6500
create_game "Genshin Impact" "PC" '["RPG","开放世界","二次元"]' 6000
create_game "Elden Ring" "PC" '["RPG","魂系","开放世界"]' 5500
create_game "Teamfight Tactics" "PC" '["自走棋","策略","休闲"]' 5000
create_game "Call of Duty: Warzone" "PC" '["FPS","大逃杀","射击"]' 4500
create_game "Fortnite" "PC" '["大逃杀","建造","射击"]' 4000
create_game "Dead by Daylight" "PC" '["恐怖","非对称","多人"]' 3500
create_game "Among Us" "PC" '["社交","推理","休闲"]' 3000
create_game "It Takes Two" "PC" '["合作","双人","冒险"]' 2500
create_game "Baldurs Gate 3" "PC" '["RPG","回合制","合作"]' 2000
# PS5
create_game "Helldivers 2" "PS5" '["射击","合作","PvE"]' 1800
create_game "Final Fantasy XIV" "PS5" '["MMORPG","RPG","多人"]' 1500
create_game "Spider-Man 2" "PS5" '["动作","冒险","开放世界"]' 1200
create_game "God of War Ragnarok" "PS5" '["动作","冒险","单人"]' 1000
# Switch
create_game "Mario Kart 8" "Switch" '["竞速","休闲","派对"]' 2000
create_game "Zelda: TOTK" "Switch" '["冒险","开放世界","解谜"]' 1800
create_game "Super Smash Bros" "Switch" '["格斗","派对","竞技"]' 1500
create_game "Pokemon Scarlet" "Switch" '["RPG","收集","冒险"]' 1200
# Mobile
create_game "Honor of Kings" "Mobile" '["MOBA","竞技","5v5"]' 9000
create_game "PUBG Mobile" "Mobile" '["大逃杀","吃鸡","射击"]' 7000
create_game "Genshin Impact" "Mobile" '["RPG","开放世界","二次元"]' 5500
create_game "Call of Duty Mobile" "Mobile" '["FPS","射击","多人"]' 4000
create_game "Arena of Valor" "Mobile" '["MOBA","竞技","5v5"]' 3000
# Xbox
create_game "Halo Infinite" "Xbox" '["FPS","射击","竞技"]' 3000
create_game "Forza Horizon 5" "Xbox" '["竞速","开放世界","休闲"]' 2500
create_game "Starfield" "Xbox" '["RPG","开放世界","太空"]' 2000
echo "导入完成!"