diff --git a/backend/pb_migrations/1776487854_updated_users.js b/backend/pb_migrations/1776487854_updated_users.js new file mode 100644 index 0000000..d8f7c32 --- /dev/null +++ b/backend/pb_migrations/1776487854_updated_users.js @@ -0,0 +1,36 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("_pb_users_auth_") + + collection.options = { + "allowEmailAuth": true, + "allowOAuth2Auth": true, + "allowUsernameAuth": true, + "exceptEmailDomains": null, + "manageRule": null, + "minPasswordLength": 6, + "onlyEmailDomains": null, + "onlyVerified": false, + "requireEmail": false + } + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("_pb_users_auth_") + + collection.options = { + "allowEmailAuth": true, + "allowOAuth2Auth": true, + "allowUsernameAuth": true, + "exceptEmailDomains": null, + "manageRule": null, + "minPasswordLength": 8, + "onlyEmailDomains": null, + "onlyVerified": false, + "requireEmail": false + } + + return dao.saveCollection(collection) +}) diff --git a/backend/pb_migrations/1776490303_updated_users.js b/backend/pb_migrations/1776490303_updated_users.js new file mode 100644 index 0000000..ff37c5c --- /dev/null +++ b/backend/pb_migrations/1776490303_updated_users.js @@ -0,0 +1,18 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("_pb_users_auth_") + + collection.listRule = "" + collection.viewRule = "" + + return dao.saveCollection(collection) +}, (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) +}) diff --git a/backend/pb_migrations/1776490365_updated_users.js b/backend/pb_migrations/1776490365_updated_users.js new file mode 100644 index 0000000..ff37c5c --- /dev/null +++ b/backend/pb_migrations/1776490365_updated_users.js @@ -0,0 +1,18 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("_pb_users_auth_") + + collection.listRule = "" + collection.viewRule = "" + + return dao.saveCollection(collection) +}, (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) +}) diff --git a/backend/pb_migrations/1776500005_created_notifications.js b/backend/pb_migrations/1776500005_created_notifications.js index 9bbe3fd..7d8bf6c 100644 --- a/backend/pb_migrations/1776500005_created_notifications.js +++ b/backend/pb_migrations/1776500005_created_notifications.js @@ -106,7 +106,7 @@ migrate((db) => { "indexes": [], "listRule": "user = @request.auth.id", "viewRule": "user = @request.auth.id", - "createRule": "@request.auth.id != \"\" && user = @request.auth.id", + "createRule": "@request.auth.id != \"\"", "updateRule": "user = @request.auth.id", "deleteRule": "user = @request.auth.id", "options": {} diff --git a/backend/pb_migrations/1776508472_updated_notifications.js b/backend/pb_migrations/1776508472_updated_notifications.js new file mode 100644 index 0000000..ddbd55b --- /dev/null +++ b/backend/pb_migrations/1776508472_updated_notifications.js @@ -0,0 +1,16 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("s63vtbeeqlv1xzu") + + collection.createRule = "@request.auth.id != \"\"" + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("s63vtbeeqlv1xzu") + + collection.createRule = "@request.auth.id != \"\" && user = @request.auth.id" + + return dao.saveCollection(collection) +}) diff --git a/frontend/src/api/invitations.ts b/frontend/src/api/invitations.ts index 48429bd..e500069 100644 --- a/frontend/src/api/invitations.ts +++ b/frontend/src/api/invitations.ts @@ -111,6 +111,27 @@ export async function respondInvitation( // 更新邀请状态 await pb.collection('invitations').update(invitationId, updateData) + + // 通知邀请发起人 + try { + const invitation = await pb.collection('invitations').getOne(invitationId, { + expand: 'teamSession', + $autoCancel: false, + }) as any + const { createNotification } = await import('./notifications') + await createNotification({ + user: invitation.from, + type: response === 'rejected' ? 'team_invite' : 'team_invite', + title: response === 'rejected' ? '邀请被拒绝' : '邀请已接受', + content: response === 'rejected' + ? (rejectReason || '对方拒绝了组队邀请') + : '对方已接受组队邀请', + relatedId: invitation.teamSession, + relatedType: 'team', + }) + } catch { + // 通知失败不影响主流程 + } } // 订阅邀请变更 diff --git a/frontend/src/api/polls.ts b/frontend/src/api/polls.ts index 31630c2..19b0594 100644 --- a/frontend/src/api/polls.ts +++ b/frontend/src/api/polls.ts @@ -1,5 +1,6 @@ import { pb } from './pocketbase' import { awardPoints, deductPoints } from './points' +import { createNotification } from './notifications' import type { Poll, PollOption, PollVote } from '@/types' export async function createPoll(data: { @@ -33,6 +34,27 @@ export async function createPoll(data: { }) } + // 给同群组其他成员发送通知 + try { + const group = await pb.collection('groups').getOne(data.group) + const typeLabel = data.type === 'rollcall' ? '接龙报名' : '投票' + const otherMembers = (group.members || []).filter((id: string) => id !== user.id) + await Promise.all( + otherMembers.map((memberId: string) => + createNotification({ + user: memberId, + type: 'poll_new', + title: `新${typeLabel}`, + content: `${data.title}`, + relatedId: poll.id, + relatedType: 'poll', + }) + ) + ) + } catch { + // 通知发送失败不影响主流程 + } + return poll as unknown as Poll } diff --git a/frontend/src/components/common/NotificationPanel.vue b/frontend/src/components/common/NotificationPanel.vue index 59dde6a..3b65cd3 100644 --- a/frontend/src/components/common/NotificationPanel.vue +++ b/frontend/src/components/common/NotificationPanel.vue @@ -1,13 +1,17 @@ @@ -76,11 +124,30 @@ function onJoinRequestResponded(id: string) { margin-bottom: 20px; } +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + .section-title { font-size: 13px; font-weight: 600; color: var(--gg-text-secondary); - margin: 0 0 10px; + margin: 0; +} + +.mark-all-btn { + border: none; + background: none; + color: var(--gg-primary); + font-size: 12px; + cursor: pointer; +} + +.mark-all-btn:hover { + opacity: 0.8; } .list { @@ -88,4 +155,39 @@ function onJoinRequestResponded(id: string) { flex-direction: column; gap: 10px; } + +.app-notification { + padding: 12px 14px; + border-radius: var(--gg-radius-sm); + border: 1px solid var(--gg-border); + cursor: pointer; + transition: background 0.2s; +} + +.app-notification:hover { + background: var(--gg-bg-hover); +} + +.app-notification.unread { + border-left: 3px solid var(--gg-primary); + background: rgba(5, 150, 105, 0.04); +} + +.notif-title { + font-size: 14px; + font-weight: 500; + color: var(--gg-text); +} + +.notif-content { + font-size: 13px; + color: var(--gg-text-secondary); + margin-top: 4px; +} + +.notif-time { + font-size: 11px; + color: var(--gg-text-muted); + margin-top: 6px; +} diff --git a/frontend/src/components/poll/CreatePollDialog.vue b/frontend/src/components/poll/CreatePollDialog.vue index 18adbc8..f4b3ac5 100644 --- a/frontend/src/components/poll/CreatePollDialog.vue +++ b/frontend/src/components/poll/CreatePollDialog.vue @@ -1,6 +1,7 @@