mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-30 11:24:48 +08:00
144 lines
3.6 KiB
TypeScript
144 lines
3.6 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { announcementsAPI } from '@/api'
|
|
import type { UserAnnouncement } from '@/types'
|
|
|
|
const THROTTLE_MS = 20 * 60 * 1000 // 20 minutes
|
|
|
|
export const useAnnouncementStore = defineStore('announcements', () => {
|
|
// State
|
|
const announcements = ref<UserAnnouncement[]>([])
|
|
const loading = ref(false)
|
|
const lastFetchTime = ref(0)
|
|
const popupQueue = ref<UserAnnouncement[]>([])
|
|
const currentPopup = ref<UserAnnouncement | null>(null)
|
|
|
|
// Session-scoped dedup set — not reactive, used as plain lookup only
|
|
let shownPopupIds = new Set<number>()
|
|
|
|
// Getters
|
|
const unreadCount = computed(() =>
|
|
announcements.value.filter((a) => !a.read_at).length
|
|
)
|
|
|
|
// Actions
|
|
async function fetchAnnouncements(force = false) {
|
|
const now = Date.now()
|
|
if (!force && lastFetchTime.value > 0 && now - lastFetchTime.value < THROTTLE_MS) {
|
|
return
|
|
}
|
|
|
|
// Set immediately to prevent concurrent duplicate requests
|
|
lastFetchTime.value = now
|
|
|
|
try {
|
|
loading.value = true
|
|
const all = await announcementsAPI.list(false)
|
|
announcements.value = all.slice(0, 20)
|
|
enqueueNewPopups()
|
|
} catch (err: any) {
|
|
// Revert throttle timestamp on failure so retry is allowed
|
|
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
|
|
|
|
// Mark as read (fire-and-forget, UI already updated)
|
|
markAsRead(id)
|
|
|
|
// Show next popup after a short delay
|
|
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 {
|
|
// State
|
|
announcements,
|
|
loading,
|
|
currentPopup,
|
|
// Getters
|
|
unreadCount,
|
|
// Actions
|
|
fetchAnnouncements,
|
|
dismissPopup,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
reset,
|
|
}
|
|
})
|