164 lines
5.9 KiB
JavaScript
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>
|
|
);
|
|
}
|