| import { defineStore } from 'pinia' |
| import { ref, computed } from 'vue' |
| import { announcementsAPI } from '@/api' |
| import type { UserAnnouncement } from '@/types' |
|
|
| const THROTTLE_MS = 20 * 60 * 1000 |
|
|
| export const useAnnouncementStore = defineStore('announcements', () => { |
| |
| const announcements = ref<UserAnnouncement[]>([]) |
| const loading = ref(false) |
| const lastFetchTime = ref(0) |
| const popupQueue = ref<UserAnnouncement[]>([]) |
| const currentPopup = ref<UserAnnouncement | null>(null) |
|
|
| |
| let shownPopupIds = new Set<number>() |
|
|
| |
| const unreadCount = computed(() => |
| announcements.value.filter((a) => !a.read_at).length |
| ) |
|
|
| |
| async function fetchAnnouncements(force = false) { |
| const now = Date.now() |
| if (!force && lastFetchTime.value > 0 && now - lastFetchTime.value < THROTTLE_MS) { |
| return |
| } |
|
|
| |
| lastFetchTime.value = now |
|
|
| try { |
| loading.value = true |
| const all = await announcementsAPI.list(false) |
| announcements.value = all.slice(0, 20) |
| enqueueNewPopups() |
| } catch (err: any) { |
| |
| lastFetchTime.value = 0 |
| console.error('Failed to fetch announcements:', err) |
| } finally { |
| loading.value = false |
| } |
| } |
|
|
| function enqueueNewPopups() { |
| const newPopups = announcements.value.filter( |
| (a) => a.notify_mode === 'popup' && !a.read_at && !shownPopupIds.has(a.id) |
| ) |
| if (newPopups.length === 0) return |
|
|
| for (const p of newPopups) { |
| if (!popupQueue.value.some((q) => q.id === p.id)) { |
| popupQueue.value.push(p) |
| } |
| } |
|
|
| if (!currentPopup.value) { |
| showNextPopup() |
| } |
| } |
|
|
| function showNextPopup() { |
| if (popupQueue.value.length === 0) { |
| currentPopup.value = null |
| return |
| } |
| currentPopup.value = popupQueue.value.shift()! |
| shownPopupIds.add(currentPopup.value.id) |
| } |
|
|
| async function dismissPopup() { |
| if (!currentPopup.value) return |
| const id = currentPopup.value.id |
| currentPopup.value = null |
|
|
| |
| markAsRead(id) |
|
|
| |
| if (popupQueue.value.length > 0) { |
| setTimeout(() => showNextPopup(), 300) |
| } |
| } |
|
|
| async function markAsRead(id: number) { |
| try { |
| await announcementsAPI.markRead(id) |
| const ann = announcements.value.find((a) => a.id === id) |
| if (ann) { |
| ann.read_at = new Date().toISOString() |
| } |
| } catch (err: any) { |
| console.error('Failed to mark announcement as read:', err) |
| } |
| } |
|
|
| async function markAllAsRead() { |
| const unread = announcements.value.filter((a) => !a.read_at) |
| if (unread.length === 0) return |
|
|
| try { |
| loading.value = true |
| await Promise.all(unread.map((a) => announcementsAPI.markRead(a.id))) |
| announcements.value.forEach((a) => { |
| if (!a.read_at) { |
| a.read_at = new Date().toISOString() |
| } |
| }) |
| } catch (err: any) { |
| console.error('Failed to mark all as read:', err) |
| throw err |
| } finally { |
| loading.value = false |
| } |
| } |
|
|
| function reset() { |
| announcements.value = [] |
| lastFetchTime.value = 0 |
| shownPopupIds = new Set() |
| popupQueue.value = [] |
| currentPopup.value = null |
| loading.value = false |
| } |
|
|
| return { |
| |
| announcements, |
| loading, |
| currentPopup, |
| |
| unreadCount, |
| |
| fetchAnnouncements, |
| dismissPopup, |
| markAsRead, |
| markAllAsRead, |
| reset, |
| } |
| }) |
|
|