Files
gamegroup2/frontend/src/stores/bulletin.ts
T
congsh f99c44f716 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>
2026-04-21 12:46:35 +08:00

171 lines
4.1 KiB
TypeScript

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,
}
})