feat(bulletin): add group bulletin board (信息公示板)

- Add bulletin_posts and bulletin_reads PocketBase collections with migrations
- Add BulletinPost/BulletinRead types and priority levels (low/normal/high/urgent)
- Add bulletin API (CRUD, read tracking, realtime subscription)
- Add bulletin Pinia store with pinned/normal sections and expiration filtering
- Add BulletinPinned header component for pinned announcements
- Add BulletinBoard tab component with sectioned list layout
- Add BulletinPostCard with priority tags, unread dots, and hover actions
- Add CreateBulletinDialog with title, content, priority, pinned, expiresAt
- Integrate bulletin tab into GroupView with Bell icon and real-time updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wjl
2026-04-21 12:46:35 +08:00
parent 3c2b68bbc3
commit f99c44f716
10 changed files with 1478 additions and 2 deletions
+113
View File
@@ -0,0 +1,113 @@
import { pb } from './pocketbase'
import type { BulletinPost, BulletinRead } from '@/types'
export async function createBulletin(data: {
group: string
title: string
content: string
priority?: 'low' | 'normal' | 'high' | 'urgent'
pinned?: boolean
expiresAt?: string
}): Promise<BulletinPost> {
const user = pb.authStore.model
if (!user) throw new Error('未登录')
const post = await pb.collection('bulletin_posts').create({
group: data.group,
creator: user.id,
title: data.title,
content: data.content,
priority: data.priority || 'normal',
pinned: data.pinned || false,
expiresAt: data.expiresAt || '',
})
return post as unknown as BulletinPost
}
export async function listBulletins(groupId: string): Promise<BulletinPost[]> {
const result = await pb.collection('bulletin_posts').getFullList({
filter: `group="${groupId}"`,
sort: '-pinned,-created',
expand: 'creator',
$autoCancel: false,
})
return result as unknown as BulletinPost[]
}
export async function getBulletin(postId: string): Promise<BulletinPost> {
const result = await pb.collection('bulletin_posts').getOne(postId, {
expand: 'creator',
})
return result as unknown as BulletinPost
}
export async function updateBulletin(
postId: string,
data: {
title?: string
content?: string
priority?: 'low' | 'normal' | 'high' | 'urgent'
pinned?: boolean
expiresAt?: string
}
): Promise<BulletinPost> {
const payload: Record<string, any> = {}
if (data.title !== undefined) payload.title = data.title
if (data.content !== undefined) payload.content = data.content
if (data.priority !== undefined) payload.priority = data.priority
if (data.pinned !== undefined) payload.pinned = data.pinned
if (data.expiresAt !== undefined) payload.expiresAt = data.expiresAt
const result = await pb.collection('bulletin_posts').update(postId, payload)
return result as unknown as BulletinPost
}
export async function deleteBulletin(postId: string): Promise<void> {
await pb.collection('bulletin_posts').delete(postId)
}
// 已读追踪
export async function markBulletinAsRead(postId: string): Promise<BulletinRead | null> {
const user = pb.authStore.model
if (!user) return null
// 检查是否已存在
const existing = await pb.collection('bulletin_reads').getList(1, 1, {
filter: `post="${postId}" && user="${user.id}"`,
$autoCancel: false,
})
if (existing.items.length > 0) return existing.items[0] as unknown as BulletinRead
const result = await pb.collection('bulletin_reads').create({
post: postId,
user: user.id,
})
return result as unknown as BulletinRead
}
export async function listMyReads(): Promise<BulletinRead[]> {
const user = pb.authStore.model
if (!user) return []
const result = await pb.collection('bulletin_reads').getFullList({
filter: `user="${user.id}"`,
$autoCancel: false,
})
return result as unknown as BulletinRead[]
}
// 实时订阅
export async function subscribeBulletins(
groupId: string,
callback: (data: any) => void
): Promise<() => void> {
await pb.collection('bulletin_posts').subscribe('*', (data) => {
if (data.record?.group === groupId) {
callback(data)
}
})
return () => {
pb.collection('bulletin_posts').unsubscribe('*')
}
}