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:
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user