feat(client): 通知中心与未读角标(F16)
- Topbar 增加通知铃铛与未读数 - 新增 /notifications 列表页,支持筛选与标记已读 - 为 workflow/reimbursement/auth 通知提供跳转入口
这个提交包含在:
@@ -1,5 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchNotifications } from "../../api/notifications";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
@@ -37,11 +39,42 @@ function MenuIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
function BellIcon() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15 17H9m9-2V11a6 6 0 10-12 0v4l-2 2h16l-2-2z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Topbar() {
|
||||
const { me, logout } = useAuth();
|
||||
const roleCodes = me?.roleCodes ?? [];
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
|
||||
const unreadQuery = useQuery({
|
||||
enabled: Boolean(me),
|
||||
queryKey: ["notifications", "unread-count"],
|
||||
queryFn: () => fetchNotifications({ unreadOnly: true, page: 1, pageSize: 1 }),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
const unreadCount = unreadQuery.data?.unreadCount ?? 0;
|
||||
const unreadBadge = unreadCount > 99 ? "99+" : String(unreadCount);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 border-b border-slate-200 bg-white/95 px-4 py-3 backdrop-blur sm:px-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
@@ -89,6 +122,24 @@ export function Topbar() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
asChild
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
aria-label="通知"
|
||||
type="button"
|
||||
data-testid="notifications-bell"
|
||||
>
|
||||
<Link className="relative" to="/notifications">
|
||||
<BellIcon />
|
||||
{unreadCount > 0 ? (
|
||||
<span className="absolute -right-2 -top-2 min-w-5 rounded-full bg-red-500 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white">
|
||||
{unreadBadge}
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="hidden text-right sm:block">
|
||||
<p className="text-xs uppercase tracking-wide text-secondary">角色</p>
|
||||
<p className="text-sm font-medium text-slate-900">{roleCodes.join(" / ") || "-"}</p>
|
||||
|
||||
在新工单中引用
屏蔽一个用户