This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { ChevronDown, KeyRound, LogOut, Menu as MenuIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState, type KeyboardEvent, type Ref } from "react";
|
||||
import { Avatar, Button, Dropdown, Layout, Space, Tooltip, type DropdownOption } from "tdesign-react";
|
||||
|
||||
export interface AppHeaderProps {
|
||||
adminName?: string;
|
||||
adminUsername?: string;
|
||||
onToggleMenu?: () => void;
|
||||
menuButtonRef?: Ref<HTMLButtonElement>;
|
||||
menuOpen?: boolean;
|
||||
menuControls?: string;
|
||||
onLogout?: () => void;
|
||||
onOpenPassword?: () => void;
|
||||
}
|
||||
|
||||
export default function AppHeader({ adminName, adminUsername, onToggleMenu, menuButtonRef, menuOpen = false, menuControls, onLogout, onOpenPassword }: AppHeaderProps) {
|
||||
const displayName = adminName?.trim() || "管理员";
|
||||
const avatarText = [...displayName][0]?.toUpperCase() || "管";
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const userMenuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuItem = (label: string) => <button type="button" role="menuitem" className="tn-user-dropdown-button" onKeyDown={handleMenuKeyDown}>{label}</button>;
|
||||
const userOptions: DropdownOption[] = [
|
||||
...(onOpenPassword ? [{ value: "password", content: menuItem("修改密码"), prefixIcon: <KeyRound size={16} /> }] : []),
|
||||
...(onLogout ? [{ value: "logout", content: menuItem("退出登录"), theme: "error" as const, prefixIcon: <LogOut size={16} /> }] : []),
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!userMenuOpen) return;
|
||||
let disposed = false;
|
||||
const focusMenu = () => {
|
||||
if (disposed) return;
|
||||
const menu = document.querySelector<HTMLElement>(".tn-user-dropdown");
|
||||
if (!menu) return;
|
||||
menu.setAttribute("role", "menu");
|
||||
menu.setAttribute("aria-label", "账号操作");
|
||||
menu.querySelector<HTMLElement>("[role='menuitem']")?.focus();
|
||||
};
|
||||
const frame = window.requestAnimationFrame(focusMenu);
|
||||
const timer = window.setTimeout(focusMenu, 80);
|
||||
return () => { disposed = true; window.cancelAnimationFrame(frame); window.clearTimeout(timer); };
|
||||
}, [userMenuOpen]);
|
||||
|
||||
function handleMenuKeyDown(event: KeyboardEvent<HTMLButtonElement>) {
|
||||
const items = Array.from(document.querySelectorAll<HTMLButtonElement>(".tn-user-dropdown [role='menuitem']"));
|
||||
const index = items.indexOf(event.currentTarget);
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
userMenuButtonRef.current?.click();
|
||||
userMenuButtonRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key) || !items.length) return;
|
||||
event.preventDefault();
|
||||
const nextIndex = event.key === "Home" ? 0
|
||||
: event.key === "End" ? items.length - 1
|
||||
: event.key === "ArrowDown" ? (index + 1) % items.length
|
||||
: (index - 1 + items.length) % items.length;
|
||||
items[nextIndex]?.focus();
|
||||
}
|
||||
|
||||
const handleUserAction = (item: DropdownOption) => {
|
||||
if (item.value === "password") onOpenPassword?.();
|
||||
if (item.value === "logout") onLogout?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout.Header className="tn-header">
|
||||
<Space className="tn-header-leading" align="center" size="small">
|
||||
<Tooltip content="导航菜单" placement="bottom">
|
||||
<Button
|
||||
ref={menuButtonRef}
|
||||
className="tn-header-menu-toggle"
|
||||
shape="square"
|
||||
size="large"
|
||||
variant="text"
|
||||
aria-label="导航菜单"
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls={menuControls}
|
||||
onClick={onToggleMenu}
|
||||
icon={<MenuIcon size={19} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="tn-header-nav-label">主导航</span>
|
||||
</Space>
|
||||
<Space className="tn-header-actions" align="center" size="small">
|
||||
{userOptions.length > 0 && <Dropdown
|
||||
trigger="click"
|
||||
placement="bottom-right"
|
||||
options={userOptions}
|
||||
minColumnWidth={184}
|
||||
maxColumnWidth={240}
|
||||
onClick={handleUserAction}
|
||||
panelTopContent={<div className="tn-user-dropdown-identity"><strong>{displayName}</strong>{adminUsername && <span>{adminUsername}</span>}</div>}
|
||||
popupProps={{ overlayInnerClassName: "tn-user-dropdown", onVisibleChange: setUserMenuOpen }}
|
||||
>
|
||||
<Button ref={userMenuButtonRef} className="tn-user-menu-trigger" variant="text" size="large" aria-label={`${displayName},打开账号菜单`} aria-haspopup="menu" aria-expanded={userMenuOpen} suffix={<ChevronDown size={16} />}>
|
||||
<Avatar className="tn-user-avatar" size="32px" shape="circle">{avatarText}</Avatar>
|
||||
<span className="tn-header-user">{displayName}</span>
|
||||
</Button>
|
||||
</Dropdown>}
|
||||
</Space>
|
||||
</Layout.Header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user