f99c44f716
- 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>
171 lines
4.1 KiB
TypeScript
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,
|
|
}
|
|
})
|