feat: add GameGroup2 project with frontend and backend

- Add .gitignore for Node.js and PocketBase projects
- Add frontend (Vue 3 + Vite + TypeScript)
- Add backend (PocketBase)
- Add deployment scripts and Docker compose configs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
congsh
2026-04-17 15:45:54 +08:00
parent 2db391901c
commit 2ce8985747
56 changed files with 3981 additions and 783 deletions
+98
View File
@@ -0,0 +1,98 @@
// src/api/games.ts
import pb from './pocketbase'
import type { Game, GamePlatform } from '@/types'
// 获取游戏列表
export async function getGames(options?: {
page?: number
limit?: number
platform?: GamePlatform
search?: string
}): Promise<{ items: Game[], total: number }> {
const { page = 1, limit = 20, platform, search } = options || {}
let filter = ''
if (platform) {
filter = `platform="${platform}"`
}
if (search) {
const searchFilter = `name ~ "${search}"`
filter = filter ? `${filter} && ${searchFilter}` : searchFilter
}
const result = await pb.collection('games').getList(page, limit, {
filter,
sort: '-popularCount'
})
return {
items: result.items as unknown as Game[],
total: result.totalItems
}
}
// 获取热门游戏
export async function getPopularGames(limit = 10): Promise<Game[]> {
const result = await pb.collection('games').getList(1, limit, {
sort: '-popularCount'
})
return result.items as unknown as Game[]
}
// 搜索游戏
export async function searchGames(query: string, limit = 20): Promise<Game[]> {
if (!query.trim()) return []
const result = await pb.collection('games').getList(1, limit, {
filter: `name ~ "${query}"`,
sort: '-popularCount'
})
return result.items as unknown as Game[]
}
// 添加游戏(需要管理员权限)
export async function addGame(data: {
name: string
platform: GamePlatform
tags?: string[]
cover?: string
}) {
return pb.collection('games').create(data)
}
// 更新游戏热度
export async function incrementGamePopularity(gameId: string) {
const game = await pb.collection('games').getOne(gameId)
return pb.collection('games').update(gameId, {
popularCount: (game.popularCount || 0) + 1
})
}
// 获取游戏详情
export async function getGame(gameId: string): Promise<Game> {
return pb.collection('games').getOne(gameId) as unknown as Game
}
// 按平台获取游戏
export async function getGamesByPlatform(platform: GamePlatform): Promise<Game[]> {
const result = await pb.collection('games').getList(1, 50, {
filter: `platform="${platform}"`,
sort: '-popularCount'
})
return result.items as unknown as Game[]
}
// 获取所有平台
export function getAllPlatforms(): GamePlatform[] {
return ['PC', 'PS5', 'Xbox', 'Switch', 'Mobile']
}
// 订阅游戏变更
export function subscribeGames(callback: (game: Game) => void) {
return pb.collection('games').subscribe('*', (payload) => {
callback(payload.record as unknown as Game)
})
}
+111
View File
@@ -0,0 +1,111 @@
// src/api/groups.ts
import pb from './pocketbase'
import type { Group } from '@/types'
// 创建群组
export async function createGroup(data: {
name: string
description: string
maxMembers: number
}) {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
return pb.collection('groups').create({
...data,
owner: user.id,
members: [user.id]
})
}
// 获取用户的群组列表
export async function getUserGroups(): Promise<Group[]> {
const user = pb.authStore.model
if (!user) return []
// 通过 members 字段过滤
return pb.collection('groups').getList(1, 50, {
filter: `members ~ "${user.id}"`
}).then(res => res.items as unknown as Group[])
}
// 获取群组详情
export async function getGroup(groupId: string): Promise<Group> {
return pb.collection('groups').getOne(groupId, {
expand: 'members'
}) as unknown as Group
}
// 加入群组
export async function joinGroup(groupId: string) {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
const group = await pb.collection('groups').getOne(groupId)
const members = group.members as string[]
if (members.length >= group.maxMembers) {
throw new Error('群组已满')
}
if (members.includes(user.id)) {
throw new Error('已是群组成员')
}
return pb.collection('groups').update(groupId, {
members: [...members, user.id]
})
}
// 退出群组
export async function leaveGroup(groupId: string) {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
const group = await pb.collection('groups').getOne(groupId)
if (group.owner === user.id) {
throw new Error('群主不能退出群组,请先转让群主或解散群组')
}
const members = group.members as string[]
return pb.collection('groups').update(groupId, {
members: members.filter(id => id !== user.id)
})
}
// 解散群组
export async function dissolveGroup(groupId: string) {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
const group = await pb.collection('groups').getOne(groupId)
if (group.owner !== user.id) {
throw new Error('只有群主可以解散群组')
}
return pb.collection('groups').delete(groupId)
}
// 订阅群组变更
export function subscribeGroup(groupId: string, callback: (group: Group) => void) {
return pb.collection('groups').subscribe('*', (payload) => {
if (payload.record.id === groupId) {
callback(payload.record as unknown as Group)
}
})
}
// 获取群组成员
export async function getGroupMembers(groupId: string) {
const group = await getGroup(groupId)
const members = group.members as string[]
// 批量获取用户信息
const users = await pb.collection('users').getList(1, 50, {
filter: members.map(id => `id="${id}"`).join(' || ')
})
return users.items
}
+116
View File
@@ -0,0 +1,116 @@
// src/api/invitations.ts
import pb from './pocketbase'
import type { Invitation, InviteStatus } from '@/types'
import { joinTeamSession } from './sessions'
import { updateUserStatus } from './users'
// 发送邀请
export async function sendInvitation(data: {
to: string
teamSession: string
}) {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
// 检查是否已有待处理邀请
const existing = await pb.collection('invitations').getList(1, 1, {
filter: `from="${user.id}" && to="${data.to}" && teamSession="${data.teamSession}" && status="pending"`
})
if (existing.items.length > 0) {
throw new Error('已有待处理的邀请')
}
return pb.collection('invitations').create({
...data,
from: user.id,
status: 'pending'
})
}
// 批量发送邀请
export async function sendBulkInvitations(recipients: string[], teamSessionId: string) {
const promises = recipients.map(to =>
sendInvitation({ to, teamSession: teamSessionId })
)
return Promise.allSettled(promises)
}
// 获取用户的待处理邀请
export async function getPendingInvitations(): Promise<Invitation[]> {
const user = pb.authStore.model
if (!user) return []
const result = await pb.collection('invitations').getList(1, 50, {
filter: `to="${user.id}" && status="pending"`,
sort: '-created',
expand: 'from,teamSession'
})
return result.items as unknown as Invitation[]
}
// 获取我发送的邀请
export async function getMySentInvitations(): Promise<Invitation[]> {
const user = pb.authStore.model
if (!user) return []
const result = await pb.collection('invitations').getList(1, 50, {
filter: `from="${user.id}"`,
sort: '-created',
expand: 'to,teamSession'
})
return result.items as unknown as Invitation[]
}
// 响应邀请
export async function respondInvitation(
invitationId: string,
response: 'accepted' | 'rejected',
rejectReason?: string
) {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
const invitation = await pb.collection('invitations').getOne(invitationId)
if (invitation.to !== user.id) {
throw new Error('无权操作此邀请')
}
const updateData: Partial<Invitation> = {
status: response as InviteStatus,
respondedAt: new Date().toISOString()
}
if (response === 'rejected' && rejectReason) {
updateData.rejectReason = rejectReason
}
// 更新邀请状态
await pb.collection('invitations').update(invitationId, updateData)
// 如果接受,加入临时小组
if (response === 'accepted') {
await joinTeamSession(invitation.teamSession)
// 更新用户状态
await updateUserStatus('in_team')
}
return updateData
}
// 订阅邀请变更
export function subscribeInvitations(callback: (invitation: Invitation) => void) {
const user = pb.authStore.model
if (!user) return () => {}
return pb.collection('invitations').subscribe('*', (payload) => {
const invite = payload.record as unknown as Invitation
if (invite.to === user.id || invite.from === user.id) {
callback(invite)
}
})
}
+32
View File
@@ -0,0 +1,32 @@
// src/api/pocketbase.ts
import PocketBase from 'pocketbase'
const pbUrl = import.meta.env.VITE_PB_URL || '/api'
export const pb = new PocketBase(pbUrl)
// 认证状态持久化
pb.authStore.loadFromCookie(document.cookie)
// 保存认证状态到 cookie
pb.authStore.onChange(() => {
document.cookie = pb.authStore.exportToCookie({ httpOnly: false })
})
// 获取当前用户
export function getCurrentUser() {
return pb.authStore.model
}
// 检查是否已登录
export function isAuthenticated(): boolean {
return pb.authStore.isValid
}
// 登出
export function logout() {
pb.authStore.clear()
window.location.href = '/login'
}
export default pb
+92
View File
@@ -0,0 +1,92 @@
// src/api/sessions.ts
import pb from './pocketbase'
import type { TeamSession, TeamStatus } from '@/types'
// 创建临时小组
export async function createTeamSession(data: {
sourceGroup: string
name: string
gameName: string
members: string[]
}): Promise<TeamSession> {
return pb.collection('teamSessions').create({
...data,
status: 'recruiting'
}) as unknown as TeamSession
}
// 获取用户的活跃临时小组
export async function getActiveTeamSession(): Promise<TeamSession | null> {
const user = pb.authStore.model
if (!user) return null
const result = await pb.collection('teamSessions').getList(1, 1, {
filter: `members ~ "${user.id}" && status != "dissolved" && status != "finished"`,
sort: '-created'
})
return (result.items[0] as unknown as TeamSession) || null
}
// 获取群组的临时小组列表
export async function getGroupTeamSessions(groupId: string): Promise<TeamSession[]> {
const result = await pb.collection('teamSessions').getList(1, 20, {
filter: `sourceGroup="${groupId}"`,
sort: '-created'
})
return result.items as unknown as TeamSession[]
}
// 更新临时小组状态
export async function updateTeamStatus(sessionId: string, status: TeamStatus): Promise<TeamSession> {
const updateData: Partial<TeamSession> = { status }
if (status === 'dissolved') {
updateData.dissolvedAt = new Date().toISOString()
}
return pb.collection('teamSessions').update(sessionId, updateData) as unknown as TeamSession
}
// 结束游戏(解散临时小组)
export async function endGame(sessionId: string) {
const session = await pb.collection('teamSessions').getOne(sessionId)
// 将所有成员状态恢复为 idle
const members = session.members as string[]
const updatePromises = members.map(userId =>
pb.collection('users').update(userId, { status: 'idle' })
)
await Promise.all(updatePromises)
// 解散临时小组
return updateTeamStatus(sessionId, 'dissolved')
}
// 加入临时小组
export async function joinTeamSession(sessionId: string) {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
const session = await pb.collection('teamSessions').getOne(sessionId) as { members: string[] }
const members = session.members as string[]
if (members.includes(user.id)) {
throw new Error('已在小组中')
}
return pb.collection('teamSessions').update(sessionId, {
members: [...members, user.id]
})
}
// 订阅临时小组变更
export function subscribeTeamSession(sessionId: string, callback: (session: TeamSession) => void) {
return pb.collection('teamSessions').subscribe('*', (payload) => {
if (payload.record.id === sessionId) {
callback(payload.record as unknown as TeamSession)
}
})
}
+79
View File
@@ -0,0 +1,79 @@
// src/api/users.ts
import pb, { getCurrentUser } from './pocketbase'
import type { User, UserStatus, WorkSchedule } from '@/types'
// 更新用户状态
export async function updateUserStatus(status: UserStatus, note?: string) {
const user = getCurrentUser()
if (!user) throw new Error('未登录')
return pb.collection('users').update(user.id, {
status,
statusNote: note || ''
})
}
// 更新工作时间设置
export async function updateWorkSchedule(schedule: WorkSchedule) {
const user = getCurrentUser()
if (!user) throw new Error('未登录')
// 计算下次工作时间
const nextWorkTime = calculateNextWorkTime(schedule.workdays, schedule.workStartTime)
return pb.collection('users').update(user.id, {
workdays: schedule.workdays,
workStartTime: schedule.workStartTime,
nextWorkTime
})
}
// 计算下次工作时间戳
function calculateNextWorkTime(workdays: number[], startTime: string): number {
const now = new Date()
const [hours, minutes] = startTime.split(':').map(Number)
for (let i = 0; i < 7; i++) {
const checkDate = new Date(now)
checkDate.setDate(now.getDate() + i)
checkDate.setHours(hours, minutes, 0, 0)
const dayOfWeek = checkDate.getDay() || 7 // 转换为 1-7 (周一到周日)
if (workdays.includes(dayOfWeek)) {
// 如果是今天,检查时间是否已过
if (i === 0 && checkDate <= now) {
continue
}
return Math.floor(checkDate.getTime() / 1000)
}
}
// 默认返回下周
const nextWeek = new Date(now)
nextWeek.setDate(now.getDate() + 7)
nextWeek.setHours(hours, minutes, 0, 0)
return Math.floor(nextWeek.getTime() / 1000)
}
// 获取用户信息
export async function getUser(userId: string): Promise<User> {
return pb.collection('users').getOne(userId)
}
// 更新用户资料
export async function updateProfile(data: Partial<Pick<User, 'username' | 'avatar'>>) {
const user = getCurrentUser()
if (!user) throw new Error('未登录')
return pb.collection('users').update(user.id, data)
}
// 订阅用户状态变更
export function subscribeUserStatus(userId: string, callback: (user: User) => void) {
return pb.collection('users').subscribe('*', (payload) => {
if (payload.record.id === userId) {
callback(payload.record as unknown as User)
}
})
}