From 3ae141ba56c26a20559d5ed0aa0065546a1393fc Mon Sep 17 00:00:00 2001 From: congsh Date: Sat, 18 Apr 2026 10:42:11 +0800 Subject: [PATCH] fix: member status visibility, team creation improvements, join approval flow - Fix other members' status not visible due to users collection viewRule restriction - Fix empty status treated as 'away' instead of 'idle' in membersByStatus - Auto-set creator to 'in_team' status when creating team session - Filter current user from idle members invite list - Fix group store isGroupOwner using pb.authStore instead of localStorage - Add nginx no-cache headers for index.html - Add join_requests collection migration and join approval flow - Update groups collection rules and add requireApproval field - Add Memory types for Phase 2 planning Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 79 ++++++++++++++++ .../1776421645_created_groups.js | 2 +- .../1776440604_updated_groups.js | 18 ++++ .../1776443079_updated_groups.js | 16 ++++ .../1776443393_updated_groups.js | 27 ++++++ .../1776443448_created_join_requests.js | 90 +++++++++++++++++++ .../pb_migrations/1776480065_updated_users.js | 18 ++++ docs/plans/2026-04-17-game-group-v2-design.md | 16 +++- frontend/nginx.conf | 3 + .../src/components/team/IdleMembersList.vue | 4 +- frontend/src/stores/group.ts | 4 +- frontend/src/stores/team.ts | 9 +- frontend/src/types/index.ts | 21 +++++ frontend/src/views/GroupView.vue | 5 +- 14 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 CLAUDE.md create mode 100644 backend/pb_migrations/1776440604_updated_groups.js create mode 100644 backend/pb_migrations/1776443079_updated_groups.js create mode 100644 backend/pb_migrations/1776443393_updated_groups.js create mode 100644 backend/pb_migrations/1776443448_created_join_requests.js create mode 100644 backend/pb_migrations/1776480065_updated_users.js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3012169 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +Game Group V2 — 游戏组队管理平台。用户创建/加入群组,组队开黑,管理游戏库。 + +## 开发命令 + +```bash +# 前端开发(默认连接 localhost:8090 的 PocketBase) +cd frontend && npm run dev + +# 前端开发环境(连接远程后端 192.168.1.14:8711,端口 7033) +cd frontend && npm run dev:dev + +# 前端 UAT 环境(端口 7034) +cd frontend && npm run dev:uat + +# 构建前端 +cd frontend && npm run build + +# 启动后端(PocketBase,端口 8711) +cd backend && docker-compose up -d + +# 部署脚本(根目录) +./deploy-backend.sh # 部署后端 +./deploy-dev.sh # 部署 Dev 前端 +./deploy-uat.sh # 部署 UAT 前端 +./stop-all.sh # 停止所有服务 +``` + +## 技术栈 + +- **后端**: PocketBase 0.22.4 (Docker, `ghcr.io/muchobien/pocketbase`) — 无自定义 JS hooks,业务逻辑全在前端 +- **前端**: Vue 3 + TypeScript + Pinia + Element Plus + Tailwind CSS + Vite +- **API 通信**: PocketBase JS SDK (`pocketbase` npm 包),cookie 认证 +- **实时通信**: PocketBase realtime subscriptions + +## 架构 + +### 前端目录结构 (`frontend/src/`) + +- **`api/`** — 每个领域一个文件(`users.ts`, `groups.ts`, `sessions.ts`, `invitations.ts`, `games.ts`),封装 PocketBase CRUD 和过滤逻辑。`pocketbase.ts` 初始化客户端并导出认证工具函数 +- **`stores/`** — Pinia stores(`user`, `group`, `team`, `notification`),组合式 API 风格(`defineStore('name', () => {...})`) +- **`composables/useRealtime.ts`** — 统一管理 PocketBase 实时订阅,组件卸载时自动清理 +- **`views/`** — 页面级组件,路由懒加载 +- **`components/`** — 按领域分子目录:`common/`, `game/`, `group/`, `layout/`, `team/` +- **`types/index.ts`** — 所有 TypeScript 接口和类型定义集中在一个文件 + +### 数据模型(PocketBase Collections) + +- **users** — 用户,含状态(idle/working/in_team/away)、工作时间设定、积分 +- **groups** — 群组,owner + members 关系,支持审核加入(requireApproval) +- **team_sessions** — 临时组队,关联 sourceGroup,状态流转:recruiting → playing → finished/dissolved +- **invitations** — 组队邀请,from/to 用户,pending/accepted/rejected +- **games** — 游戏库,归属 group,含平台、标签、封面 +- **game_comments** / **game_favorites** — 游戏评论和收藏 +- **join_requests** — 入群申请,pending/approved/rejected + +### 关键模式 + +- **Vite 代理**: 开发时 `/api` 代理到 PocketBase,路径重写去掉 `/api` 前缀(`vite.config.ts`) +- **认证流程**: `pocketbase.ts` → cookie 持久化 → 路由守卫检查 `isAuthenticated()` → 未登录跳转 `/login` +- **实时订阅**: 通过 `useRealtime` composable 和 `subscribe*` 函数订阅 PocketBase 变更事件,各 store 自行刷新数据 +- **样式系统**: 自定义 CSS 变量(`design.css`,`--gg-*` 前缀)+ Tailwind + Element Plus,主题色为绿色系 + +### 后端 + +- PocketBase 数据迁移在 `backend/pb_migrations/`,由管理面板操作自动生成 +- `backend/pb_hooks/main.js` 为占位文件,当前镜像不支持 JS VM +- 所有业务逻辑(如入群审批、组队邀请)在前端 API 层实现 + +## 环境配置 + +前端通过 `.env` 文件配置: +- `VITE_PB_URL` — PocketBase 地址(默认 `window.location.origin`) +- `VITE_PORT` — 开发服务器端口 diff --git a/backend/pb_migrations/1776421645_created_groups.js b/backend/pb_migrations/1776421645_created_groups.js index ec09605..48acb34 100644 --- a/backend/pb_migrations/1776421645_created_groups.js +++ b/backend/pb_migrations/1776421645_created_groups.js @@ -87,7 +87,7 @@ migrate((db) => { "listRule": "@request.auth.id != \"\"", "viewRule": "@request.auth.id != \"\"", "createRule": "@request.auth.id != \"\"", - "updateRule": "owner = @request.auth.id", + "updateRule": "@request.auth.id != \"\"", "deleteRule": "owner = @request.auth.id", "options": {} }); diff --git a/backend/pb_migrations/1776440604_updated_groups.js b/backend/pb_migrations/1776440604_updated_groups.js new file mode 100644 index 0000000..1862cf4 --- /dev/null +++ b/backend/pb_migrations/1776440604_updated_groups.js @@ -0,0 +1,18 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf") + + collection.listRule = "@request.auth.id != \"\"" + collection.viewRule = "@request.auth.id != \"\"" + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf") + + collection.listRule = "owner = @request.auth.id || members.id = @request.auth.id" + collection.viewRule = "owner = @request.auth.id || members.id = @request.auth.id" + + return dao.saveCollection(collection) +}) diff --git a/backend/pb_migrations/1776443079_updated_groups.js b/backend/pb_migrations/1776443079_updated_groups.js new file mode 100644 index 0000000..60e8417 --- /dev/null +++ b/backend/pb_migrations/1776443079_updated_groups.js @@ -0,0 +1,16 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf") + + collection.updateRule = "@request.auth.id != \"\"" + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf") + + collection.updateRule = "owner = @request.auth.id" + + return dao.saveCollection(collection) +}) diff --git a/backend/pb_migrations/1776443393_updated_groups.js b/backend/pb_migrations/1776443393_updated_groups.js new file mode 100644 index 0000000..e2e10e7 --- /dev/null +++ b/backend/pb_migrations/1776443393_updated_groups.js @@ -0,0 +1,27 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf") + + // add + collection.schema.addField(new SchemaField({ + "system": false, + "id": "sf_approval", + "name": "requireApproval", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + })) + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf") + + // remove + collection.schema.removeField("sf_approval") + + return dao.saveCollection(collection) +}) diff --git a/backend/pb_migrations/1776443448_created_join_requests.js b/backend/pb_migrations/1776443448_created_join_requests.js new file mode 100644 index 0000000..c75bb4b --- /dev/null +++ b/backend/pb_migrations/1776443448_created_join_requests.js @@ -0,0 +1,90 @@ +/// +migrate((db) => { + const collection = new Collection({ + "id": "ezklls35klxregi", + "created": "2026-04-17 16:30:48.222Z", + "updated": "2026-04-17 16:30:48.222Z", + "name": "join_requests", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "sf_group", + "name": "group", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "es63bkyiblpnxdf", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "sf_user", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "_pb_users_auth_", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "sf_status", + "name": "status", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "pending", + "approved", + "rejected" + ] + } + }, + { + "system": false, + "id": "sf_reason", + "name": "rejectReason", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 200, + "pattern": "" + } + } + ], + "indexes": [], + "listRule": "@request.auth.id != \"\"", + "viewRule": "@request.auth.id != \"\"", + "createRule": "@request.auth.id != \"\"", + "updateRule": "group.owner = @request.auth.id", + "deleteRule": "group.owner = @request.auth.id", + "options": {} + }); + + return Dao(db).saveCollection(collection); +}, (db) => { + const dao = new Dao(db); + const collection = dao.findCollectionByNameOrId("ezklls35klxregi"); + + return dao.deleteCollection(collection); +}) diff --git a/backend/pb_migrations/1776480065_updated_users.js b/backend/pb_migrations/1776480065_updated_users.js new file mode 100644 index 0000000..a25e7e0 --- /dev/null +++ b/backend/pb_migrations/1776480065_updated_users.js @@ -0,0 +1,18 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("_pb_users_auth_") + + collection.listRule = "@request.auth.id != \"\"" + collection.viewRule = "@request.auth.id != \"\"" + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("_pb_users_auth_") + + collection.listRule = "id = @request.auth.id" + collection.viewRule = "id = @request.auth.id" + + return dao.saveCollection(collection) +}) diff --git a/docs/plans/2026-04-17-game-group-v2-design.md b/docs/plans/2026-04-17-game-group-v2-design.md index df5c73c..2fdbd29 100644 --- a/docs/plans/2026-04-17-game-group-v2-design.md +++ b/docs/plans/2026-04-17-game-group-v2-design.md @@ -159,6 +159,19 @@ NAS 服务器 icon: string awardedAt: date } + +// memories - 多媒体记忆(音视频等) +{ + id: string + groupId: string + uploader: string (userId) + title: string + description: string + file: string (PocketBase file field) + fileType: "image" | "video" | "audio" | "other" + size: number (bytes) + createdAt: date +} ``` ### 3.3 三期数据集合 @@ -398,11 +411,12 @@ NAS 服务器 ### 第二期 -**目标**: 预约 + 积分 + 荣誉 +**目标**: 预约 + 积分 + 荣誉 + 记忆 - [ ] 预约系统(简化投票) - [ ] 积分系统(获取/记录) - [ ] 荣誉墙(自动授予) +- [ ] 多媒体记忆(支持音视频等多媒体文件的上传、下载与在线预览) ### 第三期 diff --git a/frontend/nginx.conf b/frontend/nginx.conf index c4eb609..0f10985 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -10,6 +10,9 @@ server { location / { try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; } # API 代理到局域网 PocketBase diff --git a/frontend/src/components/team/IdleMembersList.vue b/frontend/src/components/team/IdleMembersList.vue index a0bcbe0..0359788 100644 --- a/frontend/src/components/team/IdleMembersList.vue +++ b/frontend/src/components/team/IdleMembersList.vue @@ -2,6 +2,7 @@