import { computed, ref } from "vue"; import { useRoute, useRouter } from "vue-router"; import { listGeneralNotifications, markAllGeneralNotificationsRead, markGeneralNotificationRead, } from "../api/notifications"; import { useStudyStore } from "../store/study"; import type { GeneralNotificationFeed, GeneralNotificationItem } from "../types/notifications"; export const PROJECT_NOTIFICATIONS_CHANGED_EVENT = "ctms:project-notifications-changed"; const POLL_INTERVAL_MS = 60_000; export const notifyProjectNotificationsChanged = () => { if (typeof window !== "undefined") window.dispatchEvent(new Event(PROJECT_NOTIFICATIONS_CHANGED_EVENT)); }; const formatNotificationTime = (value: string) => { const date = new Date(value); if (Number.isNaN(date.getTime())) return ""; const elapsed = Date.now() - date.getTime(); if (elapsed < 60_000) return "刚刚"; if (elapsed < 3_600_000) return `${Math.max(1, Math.floor(elapsed / 60_000))} 分钟前`; if (elapsed < 86_400_000) return `${Math.max(1, Math.floor(elapsed / 3_600_000))} 小时前`; return `${date.getMonth() + 1}-${String(date.getDate()).padStart(2, "0")}`; }; const notificationTone = (item: GeneralNotificationItem): "danger" | "warning" | "info" => { if (item.priority === "URGENT") return "danger"; if (item.priority === "HIGH") return "warning"; return "info"; }; export const useProjectNotifications = () => { const study = useStudyStore(); const route = useRoute(); const router = useRouter(); const headerRemindersLoading = ref(false); const feed = ref({ unread_count: 0, total_count: 0, items: [] }); let requestId = 0; let pollTimer: number | undefined; const loadHeaderReminders = async () => { const studyId = study.currentStudy?.id; const currentRequest = ++requestId; if (!studyId || route.path.startsWith("/admin")) { feed.value = { unread_count: 0, total_count: 0, items: [] }; return; } headerRemindersLoading.value = true; try { const { data } = await listGeneralNotifications(studyId, { limit: 10 }); if (currentRequest === requestId && study.currentStudy?.id === studyId) feed.value = data; } catch { // 短时网络异常时保留当前内存中的提醒,避免角标在轮询失败时闪烁消失。 } finally { if (currentRequest === requestId) headerRemindersLoading.value = false; } }; const headerReminderItems = computed(() => feed.value.items.map((item) => ({ key: item.id, command: `notification:${item.id}`, title: item.title, description: item.message, timeLabel: formatNotificationTime(item.created_at), tone: notificationTone(item), isRead: Boolean(item.read_at), }))); const headerReminderTotal = computed(() => feed.value.unread_count); const headerReminderBadgeValue = computed(() => headerReminderTotal.value > 99 ? "99+" : headerReminderTotal.value); const handleReminderCommand = async (command: string) => { if (command === "notification:center") { await router.push("/project/notifications"); return; } if (command === "notification:read-all") { const studyId = study.currentStudy?.id; if (!studyId || feed.value.unread_count <= 0) return; await markAllGeneralNotificationsRead(studyId); await loadHeaderReminders(); return; } if (!command.startsWith("notification:")) return; const notificationId = command.slice("notification:".length); const item = feed.value.items.find((candidate) => candidate.id === notificationId); const studyId = study.currentStudy?.id; if (!item || !studyId) return; if (!item.read_at) { item.read_at = new Date().toISOString(); feed.value.unread_count = Math.max(0, feed.value.unread_count - 1); const response = await markGeneralNotificationRead(studyId, item.id).catch(() => { item.read_at = null; feed.value.unread_count += 1; return null; }); if (response?.data.resolved_at) { feed.value.items = feed.value.items.filter((candidate) => candidate.id !== item.id); feed.value.total_count = Math.max(0, feed.value.total_count - 1); } } if (item.action_path?.startsWith("/")) await router.push(item.action_path); }; const handleReminderVisibilityChange = (visible: boolean) => { if (visible) void loadHeaderReminders(); }; const handleWindowFocus = () => { void loadHeaderReminders(); }; const startHeaderNotificationPolling = () => { if (pollTimer !== undefined || typeof window === "undefined") return; pollTimer = window.setInterval(() => { void loadHeaderReminders(); }, POLL_INTERVAL_MS); window.addEventListener("focus", handleWindowFocus); window.addEventListener(PROJECT_NOTIFICATIONS_CHANGED_EVENT, handleWindowFocus); }; const stopHeaderNotificationPolling = () => { if (pollTimer !== undefined) window.clearInterval(pollTimer); pollTimer = undefined; if (typeof window !== "undefined") { window.removeEventListener("focus", handleWindowFocus); window.removeEventListener(PROJECT_NOTIFICATIONS_CHANGED_EVENT, handleWindowFocus); } }; return { headerRemindersLoading, headerReminderItems, headerReminderTotal, headerReminderBadgeValue, loadHeaderReminders, handleReminderCommand, handleReminderVisibilityChange, startHeaderNotificationPolling, stopHeaderNotificationPolling, }; };