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:
@@ -0,0 +1,170 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { BulletinPost, BulletinRead } from '@/types'
|
||||
import {
|
||||
listBulletins,
|
||||
listMyReads,
|
||||
markBulletinAsRead,
|
||||
createBulletin,
|
||||
updateBulletin,
|
||||
deleteBulletin,
|
||||
} from '@/api/bulletins'
|
||||
|
||||
export const useBulletinStore = defineStore('bulletin', () => {
|
||||
const posts = ref<BulletinPost[]>([])
|
||||
const reads = ref<BulletinRead[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const now = computed(() => new Date())
|
||||
|
||||
const activePosts = computed(() => {
|
||||
return posts.value.filter((p) => {
|
||||
if (!p.expiresAt) return true
|
||||
return new Date(p.expiresAt) > now.value
|
||||
})
|
||||
})
|
||||
|
||||
const pinnedPosts = computed(() =>
|
||||
activePosts.value
|
||||
.filter((p) => p.pinned)
|
||||
.sort((a, b) => {
|
||||
const prioOrder = { urgent: 0, high: 1, normal: 2, low: 3 }
|
||||
return prioOrder[a.priority] - prioOrder[b.priority]
|
||||
})
|
||||
)
|
||||
|
||||
const normalPosts = computed(() =>
|
||||
activePosts.value
|
||||
.filter((p) => !p.pinned)
|
||||
.sort((a, b) => {
|
||||
const prioOrder = { urgent: 0, high: 1, normal: 2, low: 3 }
|
||||
const prioDiff = prioOrder[a.priority] - prioOrder[b.priority]
|
||||
if (prioDiff !== 0) return prioDiff
|
||||
return new Date(b.created).getTime() - new Date(a.created).getTime()
|
||||
})
|
||||
)
|
||||
|
||||
const readPostIds = computed(() => new Set(reads.value.map((r) => r.post)))
|
||||
|
||||
function isRead(postId: string): boolean {
|
||||
return readPostIds.value.has(postId)
|
||||
}
|
||||
|
||||
function unreadCount(groupId: string): number {
|
||||
return activePosts.value.filter(
|
||||
(p) => p.group === groupId && !isRead(p.id)
|
||||
).length
|
||||
}
|
||||
|
||||
async function loadPosts(groupId: string) {
|
||||
try {
|
||||
loading.value = true
|
||||
const [fetchedPosts, fetchedReads] = await Promise.all([
|
||||
listBulletins(groupId),
|
||||
listMyReads(),
|
||||
])
|
||||
posts.value = fetchedPosts
|
||||
reads.value = fetchedReads
|
||||
} catch (error) {
|
||||
console.error('加载公告列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function markAsRead(postId: string) {
|
||||
if (isRead(postId)) return
|
||||
try {
|
||||
const record = await markBulletinAsRead(postId)
|
||||
if (record) {
|
||||
reads.value.push(record)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('标记已读失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function markAllAsRead(groupId: string) {
|
||||
const unread = activePosts.value.filter(
|
||||
(p) => p.group === groupId && !isRead(p.id)
|
||||
)
|
||||
await Promise.all(unread.map((p) => markAsRead(p.id)))
|
||||
}
|
||||
|
||||
async function create(data: {
|
||||
group: string
|
||||
title: string
|
||||
content: string
|
||||
priority: 'low' | 'normal' | 'high' | 'urgent'
|
||||
pinned: boolean
|
||||
expiresAt?: string
|
||||
}) {
|
||||
try {
|
||||
const post = await createBulletin(data)
|
||||
posts.value.unshift(post)
|
||||
return post
|
||||
} catch (error: any) {
|
||||
console.error('创建公告失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function update(
|
||||
postId: string,
|
||||
data: {
|
||||
title?: string
|
||||
content?: string
|
||||
priority?: 'low' | 'normal' | 'high' | 'urgent'
|
||||
pinned?: boolean
|
||||
expiresAt?: string
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const post = await updateBulletin(postId, data)
|
||||
const index = posts.value.findIndex((p) => p.id === postId)
|
||||
if (index !== -1) {
|
||||
posts.value[index] = { ...posts.value[index], ...post }
|
||||
}
|
||||
return post
|
||||
} catch (error: any) {
|
||||
console.error('更新公告失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(postId: string) {
|
||||
try {
|
||||
await deleteBulletin(postId)
|
||||
posts.value = posts.value.filter((p) => p.id !== postId)
|
||||
reads.value = reads.value.filter((r) => r.post !== postId)
|
||||
} catch (error: any) {
|
||||
console.error('删除公告失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
posts.value = []
|
||||
reads.value = []
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
posts,
|
||||
reads,
|
||||
loading,
|
||||
activePosts,
|
||||
pinnedPosts,
|
||||
normalPosts,
|
||||
readPostIds,
|
||||
isRead,
|
||||
unreadCount,
|
||||
loadPosts,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
clear,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user