142 lines
3.6 KiB
TypeScript
142 lines
3.6 KiB
TypeScript
|
|
export type NotificationType = "success" | "error" | "info" | "warning";
|
|||
|
|
|
|||
|
|
export interface AppNotification {
|
|||
|
|
id: string;
|
|||
|
|
type: NotificationType;
|
|||
|
|
title: string;
|
|||
|
|
message?: string;
|
|||
|
|
href?: string;
|
|||
|
|
read: boolean;
|
|||
|
|
createdAt: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const STORAGE_KEY_PREFIX = "haixun-notifications";
|
|||
|
|
const MAX_ITEMS = 80;
|
|||
|
|
|
|||
|
|
type Listener = () => void;
|
|||
|
|
const listeners = new Set<Listener>();
|
|||
|
|
|
|||
|
|
let scopedUserId: string | null = null;
|
|||
|
|
let cachedNotifications: AppNotification[] = [];
|
|||
|
|
let cachedUnreadCount = 0;
|
|||
|
|
let initialized = false;
|
|||
|
|
|
|||
|
|
function storageKey() {
|
|||
|
|
return scopedUserId ? `${STORAGE_KEY_PREFIX}-${scopedUserId}` : `${STORAGE_KEY_PREFIX}-anonymous`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function emit() {
|
|||
|
|
listeners.forEach((fn) => fn());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 切換登入使用者時重載通知,避免看到別人的 local 紀錄。 */
|
|||
|
|
export function setNotificationScope(userId: string) {
|
|||
|
|
if (scopedUserId === userId) return;
|
|||
|
|
scopedUserId = userId;
|
|||
|
|
initialized = false;
|
|||
|
|
cachedNotifications = [];
|
|||
|
|
cachedUnreadCount = 0;
|
|||
|
|
ensureInitialized();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function load(): AppNotification[] {
|
|||
|
|
if (typeof window === "undefined") return [];
|
|||
|
|
try {
|
|||
|
|
const raw = localStorage.getItem(storageKey());
|
|||
|
|
if (!raw) return [];
|
|||
|
|
return JSON.parse(raw) as AppNotification[];
|
|||
|
|
} catch {
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function refreshUnreadCount() {
|
|||
|
|
cachedUnreadCount = cachedNotifications.filter((n) => !n.read).length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ensureInitialized() {
|
|||
|
|
if (initialized || typeof window === "undefined") return;
|
|||
|
|
cachedNotifications = load();
|
|||
|
|
refreshUnreadCount();
|
|||
|
|
initialized = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createNotificationId(): string {
|
|||
|
|
const c = typeof globalThis !== "undefined" ? globalThis.crypto : undefined;
|
|||
|
|
if (c && typeof c.randomUUID === "function") {
|
|||
|
|
return c.randomUUID();
|
|||
|
|
}
|
|||
|
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function save(items: AppNotification[]) {
|
|||
|
|
cachedNotifications = items.slice(0, MAX_ITEMS);
|
|||
|
|
refreshUnreadCount();
|
|||
|
|
initialized = true;
|
|||
|
|
if (typeof window !== "undefined") {
|
|||
|
|
try {
|
|||
|
|
localStorage.setItem(storageKey(), JSON.stringify(cachedNotifications));
|
|||
|
|
} catch {
|
|||
|
|
// localStorage 已滿或不可寫入(例如 Safari 隱私模式),略過持久化
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
emit();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function subscribe(listener: Listener): () => void {
|
|||
|
|
listeners.add(listener);
|
|||
|
|
return () => listeners.delete(listener);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getNotifications(): AppNotification[] {
|
|||
|
|
ensureInitialized();
|
|||
|
|
return cachedNotifications;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getUnreadCount(): number {
|
|||
|
|
ensureInitialized();
|
|||
|
|
return cachedUnreadCount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 寫入通知中心 — 僅用於 AI 產出、背景任務完成/失敗等可稍後查看的訊息。
|
|||
|
|
* 儲存、刪除、表單驗證等直接操作請用頁面內 InlineAlert(useActionFeedback)。
|
|||
|
|
*/
|
|||
|
|
export function notify(params: {
|
|||
|
|
type: NotificationType;
|
|||
|
|
title: string;
|
|||
|
|
message?: string;
|
|||
|
|
href?: string;
|
|||
|
|
}) {
|
|||
|
|
ensureInitialized();
|
|||
|
|
const item: AppNotification = {
|
|||
|
|
id: createNotificationId(),
|
|||
|
|
type: params.type,
|
|||
|
|
title: params.title,
|
|||
|
|
message: params.message,
|
|||
|
|
href: params.href,
|
|||
|
|
read: false,
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
};
|
|||
|
|
save([item, ...cachedNotifications]);
|
|||
|
|
return item.id;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function markRead(id: string) {
|
|||
|
|
ensureInitialized();
|
|||
|
|
save(cachedNotifications.map((n) => (n.id === id ? { ...n, read: true } : n)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function markAllRead() {
|
|||
|
|
ensureInitialized();
|
|||
|
|
save(cachedNotifications.map((n) => ({ ...n, read: true })));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function clearAll() {
|
|||
|
|
save([]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function removeNotification(id: string) {
|
|||
|
|
ensureInitialized();
|
|||
|
|
save(cachedNotifications.filter((n) => n.id !== id));
|
|||
|
|
}
|