mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-25 17:14:45 +08:00
feat: announcement支持强制弹窗通知
This commit is contained in:
143
frontend/src/stores/announcements.ts
Normal file
143
frontend/src/stores/announcements.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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,
|
||||
}
|
||||
})
|
||||
@@ -8,6 +8,7 @@ export { useAppStore } from './app'
|
||||
export { useAdminSettingsStore } from './adminSettings'
|
||||
export { useSubscriptionStore } from './subscriptions'
|
||||
export { useOnboardingStore } from './onboarding'
|
||||
export { useAnnouncementStore } from './announcements'
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { User, LoginRequest, RegisterRequest, AuthResponse } from '@/types'
|
||||
|
||||
Reference in New Issue
Block a user