2026-05-02 13:02:57 +08:00

164 lines
5.9 KiB
JavaScript

import ExpandMore from "@mui/icons-material/ExpandMore";
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
import Logout from "@mui/icons-material/Logout";
import Menu from "@mui/icons-material/Menu";
import MenuOpen from "@mui/icons-material/MenuOpen";
import NotificationsNoneOutlined from "@mui/icons-material/NotificationsNoneOutlined";
import Person from "@mui/icons-material/Person";
import Refresh from "@mui/icons-material/Refresh";
import Search from "@mui/icons-material/Search";
import MuiMenu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { lazy, Suspense, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
import { useAuth } from "@/app/auth/AuthProvider.jsx";
import { PERMISSIONS } from "@/app/permissions";
import { listNotifications } from "@/features/notifications/api";
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
import { IconButton } from "@/shared/ui/IconButton.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
const SearchDialog = lazy(() => import("@/features/search/components/SearchDialog.jsx").then((module) => ({ default: module.SearchDialog })));
export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSidebar }) {
const [searchOpen, setSearchOpen] = useState(false);
const [searchMounted, setSearchMounted] = useState(false);
const [userMenuAnchor, setUserMenuAnchor] = useState(null);
const navigate = useNavigate();
const { can, logout, user } = useAuth();
const appScope = useAppScope();
const { showToast } = useToast();
const ToggleIcon = isSidebarCollapsed ? MenuOpen : Menu;
const userMenuOpen = Boolean(userMenuAnchor);
const canViewNotifications = can(PERMISSIONS.notificationView);
const { data: notificationState } = useAdminQuery(() => listNotifications(), {
enabled: canViewNotifications,
initialData: { items: [], unread: 0 },
queryKey: ["notifications", "unread"],
refetchInterval: 30000
});
const unread = notificationState?.unread || 0;
useEffect(() => {
if (searchOpen) {
setSearchMounted(true);
return undefined;
}
const timeout = window.setTimeout(() => setSearchMounted(false), 280);
return () => window.clearTimeout(timeout);
}, [searchOpen]);
const openSearch = () => {
setSearchMounted(true);
setSearchOpen(true);
};
const closeUserMenu = () => {
setUserMenuAnchor(null);
};
const handleLogout = async () => {
closeUserMenu();
await logout();
navigate("/login", { replace: true });
};
const handleSearchSelect = (result) => {
if (result?.target) {
navigate(result.target);
}
};
const openNotifications = () => {
navigate("/notifications");
};
const refresh = () => {
onRefresh();
showToast("已刷新当前页面", "success");
};
return (
<header className="header">
<div className="brand">
<div className="brand-mark">
<Inventory2Outlined fontSize="small" />
</div>
<div className="brand-text">
<span className="brand-title">HYApp 管理平台</span>
<span className="brand-meta">Admin Platform</span>
</div>
</div>
<div className="header-tools">
<div className="header-left-tools">
<IconButton label={isSidebarCollapsed ? "展开菜单" : "收起菜单"} onClick={onToggleSidebar}>
<ToggleIcon fontSize="small" />
</IconButton>
<div className="header-location">{activeLabel}</div>
</div>
<div className="header-right-tools">
<TextField
className="app-switch"
disabled={appScope.loading || !appScope.apps.length}
select
size="small"
value={appScope.appCode}
onChange={(event) => appScope.setAppCode(event.target.value)}
>
{appScope.apps.length ? (
appScope.apps.map((app) => (
<MenuItem key={app.appCode} value={app.appCode}>
{app.appName || app.appCode}
</MenuItem>
))
) : (
<MenuItem value={appScope.appCode}>{appScope.appCode}</MenuItem>
)}
</TextField>
<button className="search-trigger" type="button" onClick={openSearch}>
<Search fontSize="small" />
<span>搜索用户角色...</span>
</button>
<IconButton label="刷新" onClick={refresh}>
<Refresh fontSize="small" />
</IconButton>
{canViewNotifications ? (
<IconButton label="通知" notice={unread ? String(unread) : undefined} onClick={openNotifications}>
<NotificationsNoneOutlined fontSize="small" />
</IconButton>
) : null}
<button className="user-pill" type="button" onClick={(event) => setUserMenuAnchor(event.currentTarget)}>
<Person fontSize="small" />
<span>{user?.name || user?.account || "admin"}</span>
<ExpandMore fontSize="inherit" />
</button>
<MuiMenu
anchorEl={userMenuAnchor}
className="user-menu"
onClose={closeUserMenu}
open={userMenuOpen}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
<MenuItem className="user-menu__item" onClick={handleLogout}>
<Logout fontSize="small" />
退出
</MenuItem>
</MuiMenu>
</div>
</div>
{searchMounted ? (
<Suspense fallback={null}>
<SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} onSelect={handleSearchSelect} />
</Suspense>
) : null}
</header>
);
}