import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Search, Settings } from 'lucide-react'; import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useAuth } from '../auth/AuthProvider'; import { displayNameFromUser, displayRolesFromCodes } from '../../lib/currentUserDisplay'; import { formatRelativeSince } from '../../lib/relativeSince'; import type { CurrentUserMenuNodeDto } from '../../types/authSession'; import { buildGlobalMenuSearchItems, filterGlobalMenuSearchItems, } from '../../lib/globalMenuSearch'; import { cn } from '../ui/utils'; interface HeaderProps { title: string; menus?: CurrentUserMenuNodeDto[]; /** 选中搜索结果后跳转(与侧栏 setCurrentView 一致) */ onNavigate?: (view: string) => void; /** 右上角设置:默认跳转 Support,与侧栏一致 */ onSettingsClick?: () => void; /** 点击右上角姓名/角色区域:进入当前用户编辑(由 Layout/App 处理路由) */ onProfileClick?: () => void; } export function Header({ title, menus, onNavigate, onSettingsClick, onProfileClick }: HeaderProps) { const auth = useAuth(); const displayName = displayNameFromUser(auth.user); /** 第一行:姓名;第二行:接口返回的人类可读角色(如 Partner Admin) */ const primaryName = auth.roleDisplay ? (auth.user?.fullName ?? '').trim() || (auth.user?.nick ?? auth.user?.userName ?? '').trim() || displayName : displayName; const roleLine = auth.roleDisplay ?? displayRolesFromCodes(auth.roleCodes); const initials = primaryName .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((w) => w[0]?.toUpperCase() ?? '') .join('') || 'U'; const currentDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); const searchItems = useMemo(() => buildGlobalMenuSearchItems(menus), [menus]); const [searchQuery, setSearchQuery] = useState(''); const [searchOpen, setSearchOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const searchWrapRef = useRef(null); const searchResults = useMemo( () => filterGlobalMenuSearchItems(searchItems, searchQuery), [searchItems, searchQuery], ); const showResults = searchOpen && searchQuery.trim().length > 0 && searchResults.length > 0; useEffect(() => { setActiveIndex(0); }, [searchQuery, searchResults.length]); useEffect(() => { if (!searchOpen) return; const onDocDown = (e: MouseEvent) => { if (!searchWrapRef.current?.contains(e.target as Node)) { setSearchOpen(false); } }; document.addEventListener('mousedown', onDocDown); return () => document.removeEventListener('mousedown', onDocDown); }, [searchOpen]); const pickResult = (viewKey: string) => { onNavigate?.(viewKey); setSearchQuery(''); setSearchOpen(false); }; const onSearchKeyDown = (e: React.KeyboardEvent) => { if (!searchResults.length) { if (e.key === 'Escape') setSearchOpen(false); return; } if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex((i) => (i + 1) % searchResults.length); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex((i) => (i - 1 + searchResults.length) % searchResults.length); return; } if (e.key === 'Enter') { e.preventDefault(); const item = searchResults[activeIndex]; if (item) pickResult(item.viewKey); return; } if (e.key === 'Escape') { setSearchOpen(false); } }; return (

{title} Overview

{currentDate} | Last updated:{' '} {auth.loading && !auth.user?.lastUpdated ? '…' : formatRelativeSince(auth.user?.lastUpdated ?? null)}

{ setSearchQuery(e.target.value); setSearchOpen(true); }} onFocus={() => setSearchOpen(true)} onKeyDown={onSearchKeyDown} className="flex-1 min-w-0 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 py-2 px-2 h-full" aria-label="Search menus" aria-expanded={showResults} aria-autocomplete="list" role="combobox" />
{showResults && (
    {searchResults.map((item, index) => (
  • ))}
)}
); }