import { useEffect, useState } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { NotificationItem } from "../../api/notifications"; import { fetchNotifications, markAllNotificationsRead, markNotificationRead } from "../../api/notifications"; import { useAuth } from "../../auth/AuthContext"; import { resolvePageMeta } from "../../features/navigation/schema"; import { resolveNotificationTargetLink, formatNotificationRelativeTime } from "../../lib/notificationLinks"; import { resolveErrorMessage } from "../../lib/resolveErrorMessage"; import { isWecomWebView } from "../../lib/runtimeContext"; import { GlobalSearchDialog } from "../GlobalSearchDialog"; import { AppBrand } from "../shell/AppBrand"; import { Button } from "../ui/button"; import { Dialog, DialogClose, DialogContent, DialogTrigger } from "../ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { NavLinks } from "./NavLinks"; import { toast } from "sonner"; function MenuIcon() { return ( ); } function BellIcon() { return ( ); } export function Topbar() { const { me, logout } = useAuth(); const location = useLocation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const [notificationsOpen, setNotificationsOpen] = useState(false); const wecomEntry = isWecomWebView(); const pageMeta = resolvePageMeta(location.pathname); const unreadQuery = useQuery({ enabled: Boolean(me), queryKey: ["notifications", "unread-count"], queryFn: () => fetchNotifications({ unreadOnly: true, page: 1, pageSize: 1 }), refetchInterval: 30_000, }); const dropdownQuery = useQuery({ enabled: Boolean(me) && notificationsOpen, queryKey: ["notifications", "dropdown"], queryFn: () => fetchNotifications({ page: 1, pageSize: 5 }), }); const markReadMutation = useMutation({ mutationFn: (notificationId: string) => markNotificationRead(notificationId), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["notifications"] }); }, }); const markAllReadMutation = useMutation({ mutationFn: () => markAllNotificationsRead(), onSuccess: async () => { toast.success("已全部标为已读"); await queryClient.invalidateQueries({ queryKey: ["notifications"] }); }, onError: (error) => { toast.error(resolveErrorMessage(error)); }, }); const unreadCount = unreadQuery.data?.unreadCount ?? 0; const unreadBadge = unreadCount > 99 ? "99+" : String(unreadCount); const handleNotificationClick = async (item: NotificationItem) => { const targetLink = resolveNotificationTargetLink(item) ?? "/notifications"; try { if (!item.readAt) { await markReadMutation.mutateAsync(item.id); } setNotificationsOpen(false); navigate(targetLink); } catch (error) { toast.error(resolveErrorMessage(error)); } }; useEffect(() => { const handleKeydown = (event: KeyboardEvent) => { if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) { return; } if (event.key.toLowerCase() !== "k") { return; } if (!window.matchMedia("(min-width: 768px)").matches) { return; } const target = event.target; if (target instanceof HTMLElement) { const tagName = target.tagName.toLowerCase(); if (target.isContentEditable || tagName === "input" || tagName === "textarea" || tagName === "select") { return; } } event.preventDefault(); setSearchOpen(true); }; window.addEventListener("keydown", handleKeydown); return () => window.removeEventListener("keydown", handleKeydown); }, []); return ( <>
setMobileNavOpen(false)} />
{pageMeta.group}

{pageMeta.title}

{wecomEntry ? ( 当前通过企业微信进入 ) : null}
{me ? `你好,${me.name}` : "欢迎进入海音达工作台"}
通知 {unreadCount}
{dropdownQuery.isLoading ? (
通知加载中...
) : null} {dropdownQuery.isError ? (
{resolveErrorMessage(dropdownQuery.error)}
) : null} {!dropdownQuery.isLoading && !dropdownQuery.isError ? ( dropdownQuery.data?.items?.length ? ( dropdownQuery.data.items.map((item) => ( )) ) : (
暂无通知
) ) : null}
个人设置 { event.preventDefault(); void logout(); }} > 退出登录
); }