chore: add PocketBase migrations and fix plan document
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((db) => {
|
||||
const collection = new Collection({
|
||||
"id": "x5adjlc0txf16r8",
|
||||
"created": "2026-04-17 10:26:43.079Z",
|
||||
"updated": "2026-04-17 10:26:43.079Z",
|
||||
"name": "games",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "name_field",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 100,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "platform_field",
|
||||
"name": "platform",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"PC",
|
||||
"PS5",
|
||||
"Xbox",
|
||||
"Switch",
|
||||
"Mobile"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "tags_field",
|
||||
"name": "tags",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "cover_field",
|
||||
"name": "cover",
|
||||
"type": "url",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"exceptDomains": null,
|
||||
"onlyDomains": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "popular_field",
|
||||
"name": "popularCount",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"noDecimal": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": "@request.auth.id != \"\"",
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
});
|
||||
|
||||
return Dao(db).saveCollection(collection);
|
||||
}, (db) => {
|
||||
const dao = new Dao(db);
|
||||
const collection = dao.findCollectionByNameOrId("x5adjlc0txf16r8");
|
||||
|
||||
return dao.deleteCollection(collection);
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((db) => {
|
||||
const collection = new Collection({
|
||||
"id": "es63bkyiblpnxdf",
|
||||
"created": "2026-04-17 10:27:25.626Z",
|
||||
"updated": "2026-04-17 10:27:25.626Z",
|
||||
"name": "groups",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_name",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 50,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_desc",
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 500,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_owner",
|
||||
"name": "owner",
|
||||
"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_members",
|
||||
"name": "members",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": null,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_max",
|
||||
"name": "maxMembers",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 2,
|
||||
"max": 100,
|
||||
"noDecimal": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "owner = @request.auth.id || members.id = @request.auth.id",
|
||||
"viewRule": "owner = @request.auth.id || members.id = @request.auth.id",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "owner = @request.auth.id",
|
||||
"deleteRule": "owner = @request.auth.id",
|
||||
"options": {}
|
||||
});
|
||||
|
||||
return Dao(db).saveCollection(collection);
|
||||
}, (db) => {
|
||||
const dao = new Dao(db);
|
||||
const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf");
|
||||
|
||||
return dao.deleteCollection(collection);
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((db) => {
|
||||
const collection = new Collection({
|
||||
"id": "sac8t6o9rspld8p",
|
||||
"created": "2026-04-17 10:27:44.146Z",
|
||||
"updated": "2026-04-17 10:27:44.146Z",
|
||||
"name": "team_sessions",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_name",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 100,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_group",
|
||||
"name": "sourceGroup",
|
||||
"type": "relation",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "es63bkyiblpnxdf",
|
||||
"cascadeDelete": true,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_game",
|
||||
"name": "gameName",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 100,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_members",
|
||||
"name": "members",
|
||||
"type": "relation",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": null,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_status",
|
||||
"name": "status",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"recruiting",
|
||||
"playing",
|
||||
"finished",
|
||||
"dissolved"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_dissolved",
|
||||
"name": "dissolvedAt",
|
||||
"type": "date",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "sourceGroup.owner = @request.auth.id || sourceGroup.members.id = @request.auth.id",
|
||||
"viewRule": "sourceGroup.owner = @request.auth.id || sourceGroup.members.id = @request.auth.id",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "sourceGroup.owner = @request.auth.id",
|
||||
"deleteRule": "sourceGroup.owner = @request.auth.id",
|
||||
"options": {}
|
||||
});
|
||||
|
||||
return Dao(db).saveCollection(collection);
|
||||
}, (db) => {
|
||||
const dao = new Dao(db);
|
||||
const collection = dao.findCollectionByNameOrId("sac8t6o9rspld8p");
|
||||
|
||||
return dao.deleteCollection(collection);
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((db) => {
|
||||
const collection = new Collection({
|
||||
"id": "u7crt7qlso24l77",
|
||||
"created": "2026-04-17 11:16:13.166Z",
|
||||
"updated": "2026-04-17 11:16:13.166Z",
|
||||
"name": "invitations",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_from",
|
||||
"name": "from",
|
||||
"type": "relation",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_to",
|
||||
"name": "to",
|
||||
"type": "relation",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_session",
|
||||
"name": "teamSession",
|
||||
"type": "relation",
|
||||
"required": true,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"collectionId": "sac8t6o9rspld8p",
|
||||
"cascadeDelete": true,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_status",
|
||||
"name": "status",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"pending",
|
||||
"accepted",
|
||||
"rejected"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_reason",
|
||||
"name": "rejectReason",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 500,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"system": false,
|
||||
"id": "sf_responded",
|
||||
"name": "respondedAt",
|
||||
"type": "date",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "from = @request.auth.id || to = @request.auth.id",
|
||||
"viewRule": "from = @request.auth.id || to = @request.auth.id",
|
||||
"createRule": "@request.auth.id != \"\"",
|
||||
"updateRule": "to = @request.auth.id",
|
||||
"deleteRule": "from = @request.auth.id",
|
||||
"options": {}
|
||||
});
|
||||
|
||||
return Dao(db).saveCollection(collection);
|
||||
}, (db) => {
|
||||
const dao = new Dao(db);
|
||||
const collection = dao.findCollectionByNameOrId("u7crt7qlso24l77");
|
||||
|
||||
return dao.deleteCollection(collection);
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((db) => {
|
||||
const dao = new Dao(db)
|
||||
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "sf_status",
|
||||
"name": "status",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"idle",
|
||||
"working",
|
||||
"in_team",
|
||||
"away"
|
||||
]
|
||||
}
|
||||
}))
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "sf_note",
|
||||
"name": "statusNote",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 200,
|
||||
"pattern": ""
|
||||
}
|
||||
}))
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "sf_maxgroups",
|
||||
"name": "maxGroups",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 1,
|
||||
"max": 20,
|
||||
"noDecimal": true
|
||||
}
|
||||
}))
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "sf_workdays",
|
||||
"name": "workdays",
|
||||
"type": "json",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSize": 500
|
||||
}
|
||||
}))
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "sf_workstart",
|
||||
"name": "workStartTime",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 5,
|
||||
"pattern": ""
|
||||
}
|
||||
}))
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "sf_nextwork",
|
||||
"name": "nextWorkTime",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"noDecimal": true
|
||||
}
|
||||
}))
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "sf_points",
|
||||
"name": "points",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"noDecimal": true
|
||||
}
|
||||
}))
|
||||
|
||||
// update
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "users_avatar",
|
||||
"name": "avatar",
|
||||
"type": "file",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"mimeTypes": [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/svg+xml",
|
||||
"image/gif"
|
||||
],
|
||||
"thumbs": null,
|
||||
"maxSelect": 1,
|
||||
"maxSize": 5242880,
|
||||
"protected": false
|
||||
}
|
||||
}))
|
||||
|
||||
return dao.saveCollection(collection)
|
||||
}, (db) => {
|
||||
const dao = new Dao(db)
|
||||
const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("sf_status")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("sf_note")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("sf_maxgroups")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("sf_workdays")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("sf_workstart")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("sf_nextwork")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("sf_points")
|
||||
|
||||
// update
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "users_avatar",
|
||||
"name": "avatar",
|
||||
"type": "file",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"mimeTypes": [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
"image/webp"
|
||||
],
|
||||
"thumbs": null,
|
||||
"maxSelect": 1,
|
||||
"maxSize": 5242880,
|
||||
"protected": false
|
||||
}
|
||||
}))
|
||||
|
||||
return dao.saveCollection(collection)
|
||||
})
|
||||
@@ -0,0 +1,881 @@
|
||||
# 一期修复 + UI 美化实施计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 修复后端核心流程断裂和安全问题,补齐缺失组件,全面美化前端 UI 使其达到可用的 MVP 水平。
|
||||
|
||||
**Architecture:** 后端修复 PocketBase hooks 中无效 API 调用和缺失的业务逻辑,补全 migration 和访问控制。前端基于 Element Plus + CSS 变量建立统一设计系统,重构 Layout 为侧边栏布局,美化所有页面。
|
||||
|
||||
**Tech Stack:** PocketBase 0.22.4, Vue 3 + TypeScript, Element Plus, Tailwind CSS (已安装但未启用)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 后端 Critical 修复(5 个 Task)
|
||||
|
||||
### Task 1: 恢复 Migration 文件
|
||||
|
||||
**Files:**
|
||||
- Move: `backend/pb_migrations.bak/go.mod` → `backend/pb_hooks/go.mod`(如需要)
|
||||
- Move: `backend/pb_migrations.bak/main.go` → 确认是否为旧 hook 代码
|
||||
- Create: `backend/pb_migrations/1738717600_init.pb.js`
|
||||
|
||||
**Step 1: 清理 pb_migrations.bak**
|
||||
|
||||
`pb_migrations.bak/` 里的 `go.mod` 和 `main.go` 是错误放置的文件。确认 `pb_hooks/` 目录已有完整的 `go.mod` 和 `main.go`,则 `.bak` 内容可忽略。
|
||||
|
||||
**Step 2: 创建正式 migration 文件**
|
||||
|
||||
创建 `backend/pb_migrations/1738717600_init.pb.js`,基于 PocketBase 0.22.4 的 JS migration API。注意:PocketBase 0.22.x 使用 `migrate()` 函数和 `app.save(collection)` 而非旧版 API。实际集合已在 `pb_data/data.db` 中存在,此 migration 作为可重建备份。
|
||||
|
||||
但由于当前数据库已包含集合定义,**不建议用 JS migration 重建**。更好的方案:
|
||||
|
||||
1. 确认 `pb_data/data.db` 中集合定义完整
|
||||
2. 在 README 中记录"数据库通过管理后台创建,首次需手动建表"
|
||||
3. 清空 `pb_migrations.bak/` 目录
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add -A backend/
|
||||
git commit -m "fix: clean up migration directory structure"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 修复后端 Hooks — 删除无效 API + 修复 Invitation 逻辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/pb_hooks/main.go` (全文重写)
|
||||
|
||||
**Step 1: 重写 main.go**
|
||||
|
||||
删除所有 `app.Subscriptions().*` 调用(PocketBase 0.22.x 无此 API)。修复 invitation 创建逻辑:将 `group` 字段引用改为 `teamSession`。添加 invitation accept hook。
|
||||
|
||||
```go
|
||||
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)
|
||||
}
|
||||
// 保护 owner 字段不被篡改
|
||||
originalOwner, _ := app.Dao().FindRecordById("groups", e.Record.Id)
|
||||
if originalOwner.GetString("owner") != authRecord.Id {
|
||||
return apis.NewForbiddenError("只有群组所有者可以更新群组", nil)
|
||||
}
|
||||
e.Record.Set("owner", originalOwner.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
|
||||
})
|
||||
|
||||
// 接受邀请:自动加入群组成员
|
||||
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)
|
||||
}
|
||||
// 将用户加入临时小组 members
|
||||
members := teamSession.GetStringSlice("members")
|
||||
for _, m := range members {
|
||||
if m == authRecord.Id {
|
||||
goto afterJoin
|
||||
}
|
||||
}
|
||||
members = append(members, authRecord.Id)
|
||||
teamSession.Set("members", members)
|
||||
if err := app.Dao().SaveRecord(teamSession); err != nil {
|
||||
return apis.NewBadRequestError("加入临时小组失败", nil)
|
||||
}
|
||||
afterJoin:
|
||||
|
||||
// 更新用户状态为 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
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 移除 README 中的默认凭据**
|
||||
|
||||
修改 `backend/README.md`,删除 `(admin/admin)` 部分。
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add backend/pb_hooks/main.go backend/README.md
|
||||
git commit -m "fix: rewrite hooks - remove invalid API calls, fix invitation accept flow, add access control"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 修复 Invitation 字段不一致
|
||||
|
||||
**Files:**
|
||||
- Verify: `backend/pb_data/data.db` 中 `invitations` 集合的字段名
|
||||
|
||||
**Step 1: 确认数据库 schema**
|
||||
|
||||
通过 PocketBase 管理后台确认 `invitations` 集合中:
|
||||
- `from` 字段存在且为 relation → users
|
||||
- `to` 字段存在且为 relation → users
|
||||
- `teamSession` 字段存在且为 relation → team_sessions
|
||||
- `status` 为 select (pending/accepted/rejected)
|
||||
- `rejectReason` 为 text
|
||||
|
||||
**Step 2: 确认 team_sessions 集合**
|
||||
|
||||
确认字段名是 `team_sessions`(PocketBase 会将 camelCase 转为 snake_case)。前端 API 中使用 `teamSessions`,需要确认 PocketBase 客户端会自动转换。
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
如有字段名修复:
|
||||
```bash
|
||||
git add backend/
|
||||
git commit -m "fix: verify and align collection field names"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 修复前端 API 层与后端对齐
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/api/invitations.ts`
|
||||
- Modify: `frontend/src/api/sessions.ts`
|
||||
|
||||
**Step 1: 修复 invitations.ts**
|
||||
|
||||
移除客户端权限校验(后端已处理)。移除 `respondInvitation` 中直接操作 `teamSession.members` 的代码(后端 hook 已处理)。
|
||||
|
||||
```typescript
|
||||
// 响应邀请
|
||||
export async function respondInvitation(
|
||||
invitationId: string,
|
||||
response: 'accepted' | 'rejected',
|
||||
rejectReason?: string
|
||||
) {
|
||||
const updateData: Record<string, unknown> = {
|
||||
status: response,
|
||||
respondedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
if (response === 'rejected' && rejectReason) {
|
||||
updateData.rejectReason = rejectReason
|
||||
}
|
||||
|
||||
await pb.collection('invitations').update(invitationId, updateData)
|
||||
|
||||
if (response === 'accepted') {
|
||||
// 后端 hook 会自动处理:加入 team members + 更新用户状态
|
||||
// 前端只需刷新本地状态
|
||||
}
|
||||
|
||||
return updateData
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 修复 sessions.ts 中的 endGame**
|
||||
|
||||
`endGame` 函数直接更新所有成员状态,但后端无对应权限。改为仅解散小组,状态恢复由后端处理或由各用户自行操作。
|
||||
|
||||
```typescript
|
||||
export async function endGame(sessionId: string) {
|
||||
return updateTeamStatus(sessionId, 'dissolved')
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/api/invitations.ts frontend/src/api/sessions.ts
|
||||
git commit -m "fix: align frontend API with backend hooks, remove client-side permission checks"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 修复前端组件 Bug
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/Home.vue`
|
||||
- Modify: `frontend/src/views/GroupView.vue`
|
||||
- Modify: `frontend/src/components/team/IdleMembersList.vue`
|
||||
|
||||
**Step 1: 修复 Home.vue 中的组件名**
|
||||
|
||||
```html
|
||||
<!-- 修复前 -->
|
||||
<idle-membersList />
|
||||
|
||||
<!-- 修复后 -->
|
||||
<IdleMembersList />
|
||||
```
|
||||
|
||||
**Step 2: 修复 GroupView.vue 中的组件名**
|
||||
|
||||
```html
|
||||
<!-- 修复前 -->
|
||||
<idle-membersList status="idle" />
|
||||
<idle-membersList />
|
||||
|
||||
<!-- 修复后 -->
|
||||
<IdleMembersList status="idle" />
|
||||
<IdleMembersList />
|
||||
```
|
||||
|
||||
**Step 3: 修复 IdleMembersList.vue 中 inviteMember 的空 teamSession**
|
||||
|
||||
当前 `inviteMember` 传入 `teamSession: ''`,需要先检查或创建 team session。暂时改为:如果没有活跃 team session,提示用户先在 Home 页面创建。
|
||||
|
||||
```typescript
|
||||
async function inviteMember(userId: string, username: string) {
|
||||
// 检查是否有活跃的 team session
|
||||
const { getActiveTeamSession } = await import('@/api/sessions')
|
||||
const session = await getActiveTeamSession()
|
||||
|
||||
if (!session) {
|
||||
ElMessage.warning('请先在首页创建临时小组')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await sendInvitation({
|
||||
to: userId,
|
||||
teamSession: session.id
|
||||
})
|
||||
ElMessage.success(`已邀请 ${username}`)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '邀请失败')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Home.vue frontend/src/views/GroupView.vue frontend/src/components/team/IdleMembersList.vue
|
||||
git commit -m "fix: component naming, invitation flow requires active team session"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 补齐缺失组件(3 个 Task)
|
||||
|
||||
### Task 6: 创建 Notification Store 和 NotificationPanel 组件
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/stores/notification.ts`
|
||||
- Create: `frontend/src/components/common/NotificationPanel.vue`
|
||||
|
||||
**Step 1: 创建 notification store**
|
||||
|
||||
```typescript
|
||||
// src/stores/notification.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Invitation } from '@/types'
|
||||
import { getPendingInvitations } from '@/api/invitations'
|
||||
|
||||
export const useNotificationStore = defineStore('notification', () => {
|
||||
const pendingInvitations = ref<Invitation[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const unreadCount = computed(() => pendingInvitations.value.length)
|
||||
|
||||
async function loadPendingInvitations() {
|
||||
try {
|
||||
loading.value = true
|
||||
pendingInvitations.value = await getPendingInvitations()
|
||||
} catch (error) {
|
||||
console.error('加载待处理邀请失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeInvitation(invitationId: string) {
|
||||
pendingInvitations.value = pendingInvitations.value.filter(i => i.id !== invitationId)
|
||||
}
|
||||
|
||||
return {
|
||||
pendingInvitations,
|
||||
loading,
|
||||
unreadCount,
|
||||
loadPendingInvitations,
|
||||
removeInvitation
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Step 2: 创建 NotificationPanel 组件**
|
||||
|
||||
使用 `ElDrawer` 显示待处理邀请列表,内嵌 `InvitationCard` 组件。
|
||||
|
||||
**Step 3: 集成到 Layout.vue**
|
||||
|
||||
在 Layout 中导入 NotificationStore,通知铃铛按钮打开 NotificationPanel。
|
||||
|
||||
**Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/stores/notification.ts frontend/src/components/common/NotificationPanel.vue frontend/src/views/Layout.vue
|
||||
git commit -m "feat: add notification store and panel with invitation handling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 创建 GameSelectDialog 组件
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/components/team/GameSelectDialog.vue`
|
||||
|
||||
**Step 1: 创建游戏选择弹窗**
|
||||
|
||||
使用 `ElDialog` + 搜索框 + 游戏列表。允许用户选择游戏或手动输入游戏名。搜索调用 `searchGames` API,结果以列表形式展示。
|
||||
|
||||
**Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/team/GameSelectDialog.vue
|
||||
git commit -m "feat: add GameSelectDialog for team creation flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 创建 InviteButton 组件
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/components/team/InviteButton.vue`
|
||||
|
||||
**Step 1: 创建邀请按钮组件**
|
||||
|
||||
逻辑:
|
||||
1. 如果有活跃 team session → 直接发送邀请
|
||||
2. 如果没有 → 打开 GameSelectDialog → 创建 team session → 发送邀请
|
||||
|
||||
**Step 2: 更新 IdleMembersList 使用 InviteButton**
|
||||
|
||||
替换当前的直接邀请按钮,改为使用 `InviteButton` 组件。
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/team/InviteButton.vue frontend/src/components/team/IdleMembersList.vue
|
||||
git commit -m "feat: add InviteButton with auto team session creation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: UI 美化 — 设计系统 + Layout(3 个 Task)
|
||||
|
||||
### Task 9: 建立全局设计系统和主题变量
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/assets/design.css`
|
||||
- Modify: `frontend/src/main.ts`(引入 design.css)
|
||||
- Modify: `frontend/tailwind.config.js`(启用 Tailwind 并配置主题色)
|
||||
|
||||
**Step 1: 创建 design.css**
|
||||
|
||||
定义 CSS 自定义属性:主色调、圆角、阴影、间距、渐变等。覆盖 Element Plus 的默认主题变量使其与游戏主题协调。
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* 主色调:深空蓝 + 霓虹紫 */
|
||||
--gg-primary: #6366f1;
|
||||
--gg-primary-light: #818cf8;
|
||||
--gg-primary-dark: #4f46e5;
|
||||
--gg-accent: #a855f7;
|
||||
--gg-bg: #0f172a;
|
||||
--gg-bg-card: #1e293b;
|
||||
--gg-bg-hover: #334155;
|
||||
--gg-text: #f1f5f9;
|
||||
--gg-text-secondary: #94a3b8;
|
||||
--gg-border: #334155;
|
||||
--gg-success: #22c55e;
|
||||
--gg-danger: #ef4444;
|
||||
--gg-warning: #f59e0b;
|
||||
|
||||
--gg-radius-sm: 8px;
|
||||
--gg-radius-md: 12px;
|
||||
--gg-radius-lg: 16px;
|
||||
--gg-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
--gg-gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
|
||||
|
||||
/* 覆盖 Element Plus 变量 */
|
||||
--el-color-primary: #6366f1;
|
||||
--el-color-primary-light-3: #818cf8;
|
||||
--el-color-primary-light-5: #a5b4fc;
|
||||
--el-color-primary-light-7: #c7d2fe;
|
||||
--el-color-primary-light-9: #eef2ff;
|
||||
--el-color-primary-dark-2: #4f46e5;
|
||||
--el-bg-color: #1e293b;
|
||||
--el-bg-color-page: #0f172a;
|
||||
--el-bg-color-overlay: #1e293b;
|
||||
--el-text-color-primary: #f1f5f9;
|
||||
--el-text-color-regular: #e2e8f0;
|
||||
--el-text-color-secondary: #94a3b8;
|
||||
--el-border-color: #334155;
|
||||
--el-fill-color: #334155;
|
||||
--el-fill-color-light: #1e293b;
|
||||
--el-fill-color-lighter: #1e293b;
|
||||
}
|
||||
|
||||
/* 全局暗色模式基础 */
|
||||
body {
|
||||
background: var(--gg-bg);
|
||||
color: var(--gg-text);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* Element Plus 暗色模式覆盖 */
|
||||
.el-card {
|
||||
background: var(--gg-bg-card) !important;
|
||||
border-color: var(--gg-border) !important;
|
||||
color: var(--gg-text) !important;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
background: var(--gg-bg-card) !important;
|
||||
border-color: var(--gg-border) !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background: var(--gg-bg) !important;
|
||||
box-shadow: 0 0 0 1px var(--gg-border) inset !important;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: var(--gg-text) !important;
|
||||
}
|
||||
|
||||
.el-dropdown-menu {
|
||||
background: var(--gg-bg-card) !important;
|
||||
border-color: var(--gg-border) !important;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
color: var(--gg-text) !important;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
background: var(--gg-bg-hover) !important;
|
||||
color: var(--gg-primary) !important;
|
||||
}
|
||||
|
||||
/* 状态颜色 */
|
||||
.status-idle { color: #22c55e; }
|
||||
.status-working { color: #ef4444; }
|
||||
.status-in_team { color: #3b82f6; }
|
||||
.status-away { color: #6b7280; }
|
||||
```
|
||||
|
||||
**Step 2: 更新 tailwind.config.js**
|
||||
|
||||
启用深色主题色系,与 CSS 变量对齐。
|
||||
|
||||
**Step 3: 在 main.ts 中引入**
|
||||
|
||||
```typescript
|
||||
import './assets/design.css'
|
||||
```
|
||||
|
||||
**Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/assets/design.css frontend/src/main.ts frontend/tailwind.config.js
|
||||
git commit -m "feat: establish dark theme design system with Element Plus overrides"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 重构 Layout 为游戏风格侧边栏布局
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `frontend/src/views/Layout.vue`
|
||||
|
||||
**Step 1: 重写 Layout.vue**
|
||||
|
||||
布局结构改为:
|
||||
|
||||
```
|
||||
┌─────────┬──────────────────────────────────┐
|
||||
│ Sidebar │ Header │
|
||||
│ (240px) │──────────────────────────────────│
|
||||
│ │ │
|
||||
│ Logo │ Main Content │
|
||||
│ Nav │ │
|
||||
│ Groups │ │
|
||||
│ Status │ │
|
||||
│ User │ │
|
||||
└─────────┴──────────────────────────────────┘
|
||||
```
|
||||
|
||||
左侧边栏包含:
|
||||
- Logo(渐变文字)
|
||||
- 导航菜单(首页、游戏库)
|
||||
- 群组列表
|
||||
- 底部:用户状态 + 工作时间按钮 + 用户头像
|
||||
|
||||
右侧:顶部通知栏 + 通知面板,下方 router-view
|
||||
|
||||
全部使用深色主题配色。
|
||||
|
||||
**Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Layout.vue
|
||||
git commit -m "feat: redesign Layout with game-style dark sidebar"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: 美化登录/注册页面
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `frontend/src/views/Login.vue`
|
||||
- Rewrite: `frontend/src/views/Register.vue`
|
||||
|
||||
**Step 1: 美化 Login.vue**
|
||||
|
||||
保持深色主题:全屏渐变背景 + 毛玻璃卡片 + 游戏风格图标/标题。表单输入框使用深色风格。登录按钮使用渐变色。
|
||||
|
||||
关键设计元素:
|
||||
- 背景:深空渐变 `#0f172a → #1e1b4b` 带粒子/星星 CSS 效果
|
||||
- 卡片:半透明 `backdrop-filter: blur` 毛玻璃效果
|
||||
- 标题:带发光效果的 "Game Group" logo
|
||||
- 输入框:深色背景 + 紫色聚焦边框
|
||||
- 按钮:渐变色 + hover 发光
|
||||
|
||||
**Step 2: 同样美化 Register.vue**
|
||||
|
||||
与 Login 保持一致的视觉风格。
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Login.vue frontend/src/views/Register.vue
|
||||
git commit -m "feat: beautify login/register with dark gaming theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: UI 美化 — 内容页面(4 个 Task)
|
||||
|
||||
### Task 12: 美化 Home 页面
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `frontend/src/views/Home.vue`
|
||||
|
||||
**Step 1: 重构 Home 布局**
|
||||
|
||||
从三栏改为更现代的卡片式布局:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ 欢迎回来, {username}! [状态指示器] │
|
||||
├────────────────────┬───────────────────┤
|
||||
│ 我的群组卡片 │ 当前临时小组 │
|
||||
│ (网格 2 列) │ (TeamSession) │
|
||||
│ │ │
|
||||
├────────────────────┴───────────────────┤
|
||||
│ 热门游戏 (横向滚动卡片) │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
设计要点:
|
||||
- 群组卡片:深色底 + 悬浮发光边框 + 成员头像叠放
|
||||
- 游戏卡片:封面图 + 渐变遮罩 + 游戏名
|
||||
- 使用 CSS Grid 和 Flexbox
|
||||
|
||||
**Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Home.vue
|
||||
git commit -m "feat: redesign Home with card-based layout"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 13: 美化 GroupView 页面
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `frontend/src/views/GroupView.vue`
|
||||
|
||||
**Step 1: 重构 GroupView**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ {群组名} [群主标记] │
|
||||
│ {描述} · {N} 成员 │
|
||||
├──────────────────────┬─────────────────────┤
|
||||
│ 空闲成员列表 │ 当前临时小组 │
|
||||
│ (卡片列表) │ (TeamSession) │
|
||||
│ - 头像 + 状态 │ - 成员头像列表 │
|
||||
│ - 邀请按钮 │ - 游戏名 │
|
||||
│ │ - 结束按钮 │
|
||||
├──────────────────────┤ │
|
||||
│ 所有成员 │ │
|
||||
│ (按状态分组显示) │ │
|
||||
└──────────────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
设计要点:
|
||||
- 成员按状态分组(空闲/组队中/工作中/离开),每组不同颜色标识
|
||||
- 邀请按钮用紫色渐变 + 悬浮动画
|
||||
- 临时小组面板用发光边框
|
||||
|
||||
**Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/GroupView.vue
|
||||
git commit -m "feat: redesign GroupView with status-grouped members"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 14: 美化组件库
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `frontend/src/components/team/TeamSessionPanel.vue`
|
||||
- Rewrite: `frontend/src/components/team/IdleMembersList.vue`
|
||||
- Rewrite: `frontend/src/components/team/InvitationCard.vue`
|
||||
- Rewrite: `frontend/src/components/team/StatusToggle.vue`
|
||||
- Rewrite: `frontend/src/components/team/WorkScheduleModal.vue`
|
||||
|
||||
**Step 1: 美化 TeamSessionPanel**
|
||||
|
||||
- 深色卡片 + 紫色发光边框
|
||||
- 成员头像使用在线状态指示灯
|
||||
- 游戏结束按钮改为红色渐变 + 确认动画
|
||||
|
||||
**Step 2: 美化 IdleMembersList**
|
||||
|
||||
- 成员卡片使用深色悬浮效果
|
||||
- 邀请按钮使用紫色渐变
|
||||
- 空状态使用游戏风格图标
|
||||
|
||||
**Step 3: 美化 InvitationCard**
|
||||
|
||||
- 使用深色卡片 + 发光效果
|
||||
- 接受按钮绿色渐变,拒绝按钮灰色
|
||||
- 游戏信息区域使用渐变遮罩
|
||||
|
||||
**Step 4: 美化 StatusToggle**
|
||||
|
||||
- 下拉菜单使用深色主题
|
||||
- 当前状态显示带颜色的指示灯(非 emoji,改用 CSS 圆点)
|
||||
|
||||
**Step 5: 美化 WorkScheduleModal**
|
||||
|
||||
- 弹窗深色背景
|
||||
- 复选框和时间选择器使用自定义样式
|
||||
|
||||
**Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/team/
|
||||
git commit -m "feat: beautify all team components with dark gaming theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 15: 美化其他页面
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `frontend/src/views/GamesLibrary.vue`
|
||||
- Rewrite: `frontend/src/views/Profile.vue`
|
||||
- Rewrite: `frontend/src/views/Settings.vue`
|
||||
- Rewrite: `frontend/src/views/NotFound.vue`
|
||||
|
||||
**Step 1: 美化 GamesLibrary**
|
||||
|
||||
- 游戏卡片使用深色底 + 封面图 + 渐变遮罩文字
|
||||
- 搜索框深色风格
|
||||
- 平台筛选标签使用彩色徽章
|
||||
|
||||
**Step 2: 美化 Profile**
|
||||
|
||||
- 深色卡片布局
|
||||
- 头像区域使用渐变背景
|
||||
- 状态显示使用彩色指示灯
|
||||
|
||||
**Step 3: 美化 Settings**
|
||||
|
||||
- 设置项使用深色卡片 + 紫色强调色
|
||||
- 按钮使用渐变色
|
||||
|
||||
**Step 4: 美化 NotFound**
|
||||
|
||||
- 游戏风格的 404 页面
|
||||
- 返回按钮使用渐变色
|
||||
|
||||
**Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/GamesLibrary.vue frontend/src/views/Profile.vue frontend/src/views/Settings.vue frontend/src/views/NotFound.vue
|
||||
git commit -m "feat: beautify remaining pages with dark gaming theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 集成验证(1 个 Task)
|
||||
|
||||
### Task 16: 全流程验证
|
||||
|
||||
**Step 1: 启动后端**
|
||||
|
||||
```bash
|
||||
cd backend && docker-compose up -d
|
||||
```
|
||||
|
||||
**Step 2: 启动前端**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
**Step 3: 验证核心流程**
|
||||
|
||||
1. 注册新用户 → 确认注册页面正常
|
||||
2. 登录 → 确认跳转到首页
|
||||
3. 创建群组 → 确认群组出现在侧边栏
|
||||
4. 切换状态 → 确认状态更新
|
||||
5. 查看空闲成员 → 确认列表正确
|
||||
6. 邀请成员 → 确认邀请流程(创建 team session → 发送邀请)
|
||||
7. 接受邀请 → 确认用户加入 team session + 状态变为 in_team
|
||||
8. 结束游戏 → 确认 team session 解散
|
||||
9. 检查所有页面 UI → 确认深色主题一致性
|
||||
|
||||
**Step 4: 修复验证中发现的问题**
|
||||
|
||||
**Step 5: 最终提交**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: address issues found during integration testing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 执行优先级
|
||||
|
||||
| 优先级 | Phase | 说明 |
|
||||
|--------|-------|------|
|
||||
| P0 | Phase 1 | 后端 Critical 修复,不修则核心流程不通 |
|
||||
| P1 | Phase 2 | 补齐缺失组件,否则前端功能不完整 |
|
||||
| P2 | Phase 3-4 | UI 美化,提升可用性和观感 |
|
||||
| P3 | Phase 5 | 集成验证 |
|
||||
|
||||
**预计工作量:** 16 个 Task,建议分 3-4 个执行批次。
|
||||
Reference in New Issue
Block a user