diff --git a/app/(account)/notifications/page.tsx b/app/(account)/notifications/page.tsx index 9cc0584..aac9616 100644 --- a/app/(account)/notifications/page.tsx +++ b/app/(account)/notifications/page.tsx @@ -4,10 +4,14 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { requestWithAuth } from "@/lib/api/client" +import { listNotifications, markAllNotificationsAsRead } from "@/lib/api/notifications" +import { toApiError } from "@/lib/errors" +import { notifyInfo } from "@/lib/toast" import type { Notification } from "@/lib/types" -import { useNotificationStore } from "@/store/notifications" import { Bell, CheckCheck, MessageSquare, ShoppingBag } from "lucide-react" import Link from "next/link" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" const typeIcons: Record = { order: ShoppingBag, @@ -55,12 +59,129 @@ function NotificationItem({ notification }: { notification: Notification }) { } export default function NotificationsPage() { - const notifications = useNotificationStore((state) => state.notifications) - const markAllAsRead = useNotificationStore((state) => state.markAllAsRead) - const unreadCount = notifications.filter((notification) => !notification.read).length - const orderNotifs = notifications.filter((notification) => notification.type === "order") - const communityNotifs = notifications.filter((notification) => notification.type === "community") - const systemNotifs = notifications.filter((notification) => notification.type === "system") + const mountedRef = useRef(true) + const markAllPendingRef = useRef(false) + + const [notifications, setNotifications] = useState([]) + const [loading, setLoading] = useState(true) + const [loadingError, setLoadingError] = useState(null) + const [markingAll, setMarkingAll] = useState(false) + + const loadNotifications = useCallback(async () => { + setLoading(true) + setLoadingError(null) + + try { + const res = await requestWithAuth(() => listNotifications({ offset: 0, limit: 50 }), { + onUnauthorized: () => loadNotifications(), + }) + + if (!mountedRef.current) return + + if (res === null) { + setLoading(false) + return + } + + setNotifications(res) + setLoading(false) + } catch (err: unknown) { + if (!mountedRef.current) return + setLoading(false) + setLoadingError(toApiError(err).msg) + } + }, []) + + useEffect(() => { + mountedRef.current = true + void loadNotifications() + return () => { + mountedRef.current = false + } + }, [loadNotifications]) + + const markAllAsRead = useCallback(async () => { + if (markAllPendingRef.current) return + + markAllPendingRef.current = true + setMarkingAll(true) + + try { + const res = await requestWithAuth(() => markAllNotificationsAsRead(), { + onUnauthorized: () => markAllAsRead(), + }) + + if (!mountedRef.current) return + + if (res === null) { + return + } + + setNotifications((prev) => prev.map((n) => (n.read ? n : { ...n, read: true }))) + } catch (err: unknown) { + if (!mountedRef.current) return + notifyInfo(toApiError(err).msg) + } finally { + markAllPendingRef.current = false + if (mountedRef.current) setMarkingAll(false) + } + }, []) + + const unreadCount = useMemo( + () => notifications.filter((notification) => !notification.read).length, + [notifications], + ) + const orderNotifs = useMemo( + () => notifications.filter((notification) => notification.type === "order"), + [notifications], + ) + const communityNotifs = useMemo( + () => notifications.filter((notification) => notification.type === "community"), + [notifications], + ) + const systemNotifs = useMemo( + () => notifications.filter((notification) => notification.type === "system"), + [notifications], + ) + + const renderTab = useCallback( + (items: Notification[], emptyText: string) => { + if (loading) { + return ( + + + 加载中... + + + ) + } + + if (loadingError) { + return ( + + + {loadingError} + + + ) + } + + if (items.length === 0) { + return ( + + + {emptyText} + + + ) + } + + return items.map((notification) => ( + + )) + }, + [loading, loadingError], + ) return (
@@ -69,7 +190,7 @@ export default function NotificationsPage() {

通知中心

{unreadCount > 0 && {unreadCount} 条未读}
- @@ -84,53 +205,19 @@ export default function NotificationsPage() { - {notifications.length === 0 ? ( - - - 暂无通知 - - - ) : ( - notifications.map((notification) => ( - - )) - )} + {renderTab(notifications, "暂无通知")} - {orderNotifs.length === 0 ? ( - - - 暂无订单通知 - - - ) : ( - orderNotifs.map((n) => ) - )} + {renderTab(orderNotifs, "暂无订单通知")} - {communityNotifs.length === 0 ? ( - - - 暂无社区通知 - - - ) : ( - communityNotifs.map((n) => ) - )} + {renderTab(communityNotifs, "暂无社区通知")} - {systemNotifs.length === 0 ? ( - - - 暂无系统通知 - - - ) : ( - systemNotifs.map((n) => ) - )} + {renderTab(systemNotifs, "暂无系统通知")} diff --git a/lib/api/notifications.ts b/lib/api/notifications.ts index e5a6a5c..321e02b 100644 --- a/lib/api/notifications.ts +++ b/lib/api/notifications.ts @@ -1,33 +1,44 @@ -import { allow, deny } from "@/lib/decision" import type { Notification } from "@/lib/types" -import { useAuthStore } from "@/store/auth" -import { useNotificationStore } from "@/store/notifications" -export function listNotifications() { - return useNotificationStore.getState().notifications -} +import { httpJson } from "./http" -function isNotificationEnabled(type: Notification["type"]) { - const prefs = useAuthStore.getState().notificationPrefs - if (type === "order") return prefs.order - if (type === "community") return prefs.community - return prefs.system -} - -export function addNotification(input: { - type: Notification["type"] - title: string - content: string - link?: string -}) { - if (!isNotificationEnabled(input.type)) { - return deny(400, "该类通知已关闭") +type Paginated = { + items: T[] + meta: { + total: number + offset: number + limit: number } - - useNotificationStore.getState().addNotification(input) - return allow() } -export function markNotificationAsRead(notificationId: string) { - useNotificationStore.getState().markAsRead(notificationId) +export async function listNotifications(input?: { + offset?: number + limit?: number +}): Promise { + const offset = input?.offset ?? 0 + const limit = input?.limit ?? 50 + + const searchParams = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + }) + + const res = await httpJson>(`/api/v1/notifications?${searchParams}`, { + cache: "no-store", + }) + return res.items +} + +export async function markNotificationAsRead(notificationId: string): Promise { + await httpJson(`/api/v1/notifications/${encodeURIComponent(notificationId)}/read`, { + method: "PUT", + cache: "no-store", + }) +} + +export async function markAllNotificationsAsRead(): Promise { + await httpJson("/api/v1/notifications/read-all", { + method: "PUT", + cache: "no-store", + }) }