From c76346294abfdf0f4926813f8039d783b1798b10 Mon Sep 17 00:00:00 2001 From: congsh Date: Sat, 18 Apr 2026 00:46:31 +0800 Subject: [PATCH] feat: add group join approval flow with requireApproval setting - New join_requests PocketBase collection for pending join applications - Group requireApproval field (default true) with owner toggle - JoinGroupDialog: apply when approval required, direct join when not - JoinRequestCard component for accept/reject in notifications and group panel - NotificationPanel shows both invitations and join requests - GroupMembersPanel shows pending requests and approval switch for owners Co-Authored-By: Claude Opus 4.7 --- frontend/src/api/groups.ts | 93 ++++++++++- .../components/common/NotificationPanel.vue | 57 +++++-- .../components/group/GroupMembersPanel.vue | 112 ++++++++++++- .../src/components/group/JoinGroupDialog.vue | 52 ++++-- .../src/components/group/JoinRequestCard.vue | 151 ++++++++++++++++++ frontend/src/stores/notification.ts | 26 ++- frontend/src/types/index.ts | 19 +++ 7 files changed, 473 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/group/JoinRequestCard.vue diff --git a/frontend/src/api/groups.ts b/frontend/src/api/groups.ts index 8dc16d1..8068cc9 100644 --- a/frontend/src/api/groups.ts +++ b/frontend/src/api/groups.ts @@ -1,6 +1,6 @@ // src/api/groups.ts import pb from './pocketbase' -import type { Group } from '@/types' +import type { Group, JoinRequest } from '@/types' // 创建群组 export async function createGroup(data: { @@ -14,7 +14,8 @@ export async function createGroup(data: { return pb.collection('groups').create({ ...data, owner: user.id, - members: [user.id] + members: [user.id], + requireApproval: true }) } @@ -23,7 +24,6 @@ export async function getUserGroups(): Promise { 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[]) @@ -36,7 +36,7 @@ export async function getGroup(groupId: string): Promise { }) as unknown as Group } -// 加入群组 +// 直接加入群组(无需审核时调用) export async function joinGroup(groupId: string) { const user = pb.authStore.model if (!user) throw new Error('未登录') @@ -57,6 +57,80 @@ export async function joinGroup(groupId: string) { }) } +// 提交加入申请 +export async function createJoinRequest(groupId: string): Promise { + const user = pb.authStore.model + if (!user) throw new Error('未登录') + + // 检查是否已有待审核的申请 + const existing = await pb.collection('join_requests').getList(1, 1, { + filter: `group="${groupId}" && user="${user.id}" && status="pending"` + }) + if (existing.items.length > 0) { + throw new Error('已提交过申请,请等待审核') + } + + return pb.collection('join_requests').create({ + group: groupId, + user: user.id, + status: 'pending' + }) as unknown as JoinRequest +} + +// 获取群组的待审核申请(群主用) +export async function getGroupJoinRequests(groupId: string): Promise { + const result = await pb.collection('join_requests').getList(1, 50, { + filter: `group="${groupId}" && status="pending"`, + sort: '-created', + expand: 'user' + }) + return result.items as unknown as JoinRequest[] +} + +// 获取我作为群主的所有群组的待审核申请 +export async function getMyGroupsJoinRequests(): Promise { + const user = pb.authStore.model + if (!user) return [] + + const result = await pb.collection('join_requests').getList(1, 50, { + filter: `group.owner="${user.id}" && status="pending"`, + sort: '-created', + expand: 'user,group' + }) + return result.items as unknown as JoinRequest[] +} + +// 审批加入申请 +export async function respondJoinRequest( + requestId: string, + status: 'approved' | 'rejected', + rejectReason?: string +) { + const updateData: Record = { status } + if (status === 'rejected' && rejectReason) { + updateData.rejectReason = rejectReason + } + + const request = await pb.collection('join_requests').update(requestId, updateData) as any + + // 如果同意,将用户加入群组 + if (status === 'approved') { + const group = await pb.collection('groups').getOne(request.group) as any + const members: string[] = group.members || [] + if (!members.includes(request.user)) { + members.push(request.user) + await pb.collection('groups').update(request.group, { members }) + } + } + + return request +} + +// 更新群组审核设置 +export async function updateGroupApproval(groupId: string, requireApproval: boolean) { + return pb.collection('groups').update(groupId, { requireApproval }) +} + // 退出群组 export async function leaveGroup(groupId: string) { const user = pb.authStore.model @@ -97,12 +171,21 @@ export function subscribeGroup(groupId: string, callback: (group: Group) => void }) } +// 订阅加入申请变更 +export function subscribeJoinRequests(groupId: string, callback: (request: JoinRequest) => void) { + return pb.collection('join_requests').subscribe('*', (payload) => { + const record = payload.record as any + if (record.group === groupId) { + callback(record as unknown as JoinRequest) + } + }) +} + // 获取群组成员 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(' || ') }) diff --git a/frontend/src/components/common/NotificationPanel.vue b/frontend/src/components/common/NotificationPanel.vue index 69e6326..59dde6a 100644 --- a/frontend/src/components/common/NotificationPanel.vue +++ b/frontend/src/components/common/NotificationPanel.vue @@ -2,6 +2,7 @@ import { onMounted } from 'vue' import { useNotificationStore } from '@/stores/notification' import InvitationCard from '@/components/team/InvitationCard.vue' +import JoinRequestCard from '@/components/group/JoinRequestCard.vue' const store = useNotificationStore() @@ -12,6 +13,10 @@ onMounted(() => { function onInvitationResponded(id: string, _accepted: boolean) { store.removeInvitation(id) } + +function onJoinRequestResponded(id: string) { + store.removeJoinRequest(id) +} @@ -48,9 +72,20 @@ function onInvitationResponded(id: string, _accepted: boolean) { padding: 48px 0; } -.invitation-list { +.section { + margin-bottom: 20px; +} + +.section-title { + font-size: 13px; + font-weight: 600; + color: var(--gg-text-secondary); + margin: 0 0 10px; +} + +.list { display: flex; flex-direction: column; - gap: 12px; + gap: 10px; } diff --git a/frontend/src/components/group/GroupMembersPanel.vue b/frontend/src/components/group/GroupMembersPanel.vue index a69c8f1..a67c46d 100644 --- a/frontend/src/components/group/GroupMembersPanel.vue +++ b/frontend/src/components/group/GroupMembersPanel.vue @@ -1,8 +1,12 @@