273 lines
9.3 KiB
JavaScript
273 lines
9.3 KiB
JavaScript
import ExpandMore from "@mui/icons-material/ExpandMore";
|
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
|
import LockReset from "@mui/icons-material/LockReset";
|
|
import Logout from "@mui/icons-material/Logout";
|
|
import Menu from "@mui/icons-material/Menu";
|
|
import MenuOpen from "@mui/icons-material/MenuOpen";
|
|
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 { useTimeZone } from "@/app/timezone/TimeZoneProvider.jsx";
|
|
import { changePassword } from "@/features/auth/api";
|
|
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
|
|
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 })));
|
|
const emptyPasswordForm = { oldPassword: "", newPassword: "", confirmPassword: "" };
|
|
|
|
export function Header({ activeLabel, isSidebarCollapsed, menuItems = [], onRefresh, onToggleSidebar }) {
|
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
const [searchMounted, setSearchMounted] = useState(false);
|
|
const [userMenuAnchor, setUserMenuAnchor] = useState(null);
|
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
|
const [passwordForm, setPasswordForm] = useState(emptyPasswordForm);
|
|
const [passwordError, setPasswordError] = useState("");
|
|
const [changingPassword, setChangingPassword] = useState(false);
|
|
const navigate = useNavigate();
|
|
const { logout, user } = useAuth();
|
|
const appScope = useAppScope();
|
|
const { showToast } = useToast();
|
|
const { setTimeZone, timeZone, timeZoneOptions } = useTimeZone();
|
|
const ToggleIcon = isSidebarCollapsed ? MenuOpen : Menu;
|
|
const userMenuOpen = Boolean(userMenuAnchor);
|
|
|
|
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 openPasswordDialog = () => {
|
|
closeUserMenu();
|
|
setPasswordForm(emptyPasswordForm);
|
|
setPasswordError("");
|
|
setPasswordDialogOpen(true);
|
|
};
|
|
|
|
const closePasswordDialog = () => {
|
|
if (changingPassword) {
|
|
return;
|
|
}
|
|
setPasswordDialogOpen(false);
|
|
setPasswordError("");
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
closeUserMenu();
|
|
await logout();
|
|
navigate("/login", { replace: true });
|
|
};
|
|
|
|
const handlePasswordFieldChange = (field) => (event) => {
|
|
setPasswordForm((current) => ({ ...current, [field]: event.target.value }));
|
|
if (passwordError) {
|
|
setPasswordError("");
|
|
}
|
|
};
|
|
|
|
const handlePasswordSubmit = async (event) => {
|
|
event.preventDefault();
|
|
const { oldPassword, newPassword, confirmPassword } = passwordForm;
|
|
|
|
if (!oldPassword || !newPassword || !confirmPassword) {
|
|
setPasswordError("请填写原密码、新密码和确认密码");
|
|
return;
|
|
}
|
|
if (newPassword.length < 6) {
|
|
setPasswordError("新密码至少 6 位");
|
|
return;
|
|
}
|
|
if (newPassword.length > 72) {
|
|
setPasswordError("新密码不能超过 72 位");
|
|
return;
|
|
}
|
|
if (newPassword !== confirmPassword) {
|
|
setPasswordError("两次输入的新密码不一致");
|
|
return;
|
|
}
|
|
|
|
setChangingPassword(true);
|
|
try {
|
|
await changePassword({ oldPassword, newPassword });
|
|
showToast("密码已修改,请重新登录", "success");
|
|
await logout();
|
|
navigate("/login", { replace: true });
|
|
} catch (err) {
|
|
setPasswordError(err.message || "修改密码失败");
|
|
} finally {
|
|
setChangingPassword(false);
|
|
}
|
|
};
|
|
|
|
const handleSearchSelect = (result) => {
|
|
if (result?.target) {
|
|
navigate(result.target);
|
|
}
|
|
};
|
|
|
|
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>
|
|
<TextField
|
|
className="timezone-switch"
|
|
select
|
|
size="small"
|
|
slotProps={{ htmlInput: { "aria-label": "显示时区" } }}
|
|
value={timeZone}
|
|
onChange={(event) => setTimeZone(event.target.value)}
|
|
>
|
|
{timeZoneOptions.map((option) => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<button className="search-trigger" type="button" onClick={openSearch}>
|
|
<Search fontSize="small" />
|
|
<span>搜索菜单、用户、角色...</span>
|
|
</button>
|
|
<IconButton label="刷新" onClick={refresh}>
|
|
<Refresh fontSize="small" />
|
|
</IconButton>
|
|
<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={openPasswordDialog}>
|
|
<LockReset fontSize="small" />
|
|
修改密码
|
|
</MenuItem>
|
|
<MenuItem className="user-menu__item" onClick={handleLogout}>
|
|
<Logout fontSize="small" />
|
|
退出
|
|
</MenuItem>
|
|
</MuiMenu>
|
|
</div>
|
|
</div>
|
|
|
|
{searchMounted ? (
|
|
<Suspense fallback={null}>
|
|
<SearchDialog menuItems={menuItems} open={searchOpen} onClose={() => setSearchOpen(false)} onSelect={handleSearchSelect} />
|
|
</Suspense>
|
|
) : null}
|
|
<AdminFormDialog
|
|
loading={changingPassword}
|
|
onClose={closePasswordDialog}
|
|
onSubmit={handlePasswordSubmit}
|
|
open={passwordDialogOpen}
|
|
size="narrow"
|
|
submitDisabled={
|
|
changingPassword ||
|
|
!passwordForm.oldPassword ||
|
|
!passwordForm.newPassword ||
|
|
!passwordForm.confirmPassword
|
|
}
|
|
submitLabel="确认修改"
|
|
title="修改密码"
|
|
>
|
|
<TextField
|
|
autoComplete="current-password"
|
|
autoFocus
|
|
disabled={changingPassword}
|
|
error={Boolean(passwordError)}
|
|
helperText={passwordError || "修改成功后将自动退出登录"}
|
|
label="原密码"
|
|
onChange={handlePasswordFieldChange("oldPassword")}
|
|
required
|
|
type="password"
|
|
value={passwordForm.oldPassword}
|
|
/>
|
|
<TextField
|
|
autoComplete="new-password"
|
|
disabled={changingPassword}
|
|
label="新密码"
|
|
onChange={handlePasswordFieldChange("newPassword")}
|
|
required
|
|
type="password"
|
|
value={passwordForm.newPassword}
|
|
/>
|
|
<TextField
|
|
autoComplete="new-password"
|
|
disabled={changingPassword}
|
|
label="确认新密码"
|
|
onChange={handlePasswordFieldChange("confirmPassword")}
|
|
required
|
|
type="password"
|
|
value={passwordForm.confirmPassword}
|
|
/>
|
|
</AdminFormDialog>
|
|
</header>
|
|
);
|
|
}
|