import React, { useEffect, useRef, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import axios from 'axios' import { BotIcon, CloudDbIcon, PlusCircleIcon, SearchIcon, TrashIcon } from './Icons' import { useAuth, roleLabel } from '../auth' import { useBranding } from '../branding' import TimeZoneClock from './TimeZoneClock' import FxCalculator from './FxCalculator' import { HOME_PATH, PAGE_LABELS } from '../navigation' const TABS_STORAGE_KEY = 'happy.openTabs' /* 工作台恒定占第一位,后面最多再挂 4 个页面 */ const MAX_TAB_PAGES = 4 const MAX_TABS = MAX_TAB_PAGES + 1 /* 从本地缓存读取上次打开的页面(只保留仍然存在的路由,工作台恒定在首位) */ function readStoredTabs() { try { const raw = JSON.parse(localStorage.getItem(TABS_STORAGE_KEY) || '[]') const list = Array.isArray(raw) ? raw.filter(path => PAGE_LABELS[path] && path !== HOME_PATH) : [] return [HOME_PATH, ...list.slice(0, MAX_TAB_PAGES)] } catch { return [HOME_PATH] } } function GlobalHeader({ onOpenAssistant, onNavigate }) { const { user } = useAuth() const [now, setNow] = useState(new Date()) const [searchOpen, setSearchOpen] = useState(false) const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [searching, setSearching] = useState(false) const [openTabs, setOpenTabs] = useState(readStoredTabs) const searchRef = useRef(null) const tabsRef = useRef(null) const navigate = useNavigate() const location = useLocation() useEffect(() => { const timer = window.setInterval(() => setNow(new Date()), 1000) return () => window.clearInterval(timer) }, []) /* 访问过的页面自动登记成标签:工作台恒在首位,其余按最近使用排序,新页插在第二位 */ useEffect(() => { const path = location.pathname if (!PAGE_LABELS[path]) return setOpenTabs(current => { if (current[0] === path) return current if (current[1] === path) return current const rest = current.filter(item => item !== path && item !== HOME_PATH) return [HOME_PATH, path, ...rest].slice(0, MAX_TABS) }) }, [location.pathname]) useEffect(() => { try { localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(openTabs)) } catch { /* 存储不可用时仅本次会话有效 */ } const active = tabsRef.current?.querySelector('.page-tab.active') active?.scrollIntoView({ block: 'nearest', inline: 'nearest' }) }, [openTabs, location.pathname]) /* 关闭标签:关掉的若是当前页,就退到剩下的最近一个,没有则回工作台 */ const closeTab = (event, path) => { event.stopPropagation() const next = openTabs.filter(item => item !== path) const result = next.length ? next : [HOME_PATH] setOpenTabs(result) if (path === location.pathname) { /* 退到「最近使用过的另一个页面」,全关光了才回工作台 */ navigate(result.find(item => item !== HOME_PATH) || HOME_PATH) } } /* 清空快捷标签:工作台常驻不动,其余一次性摘掉;当前页若被清掉就回工作台 */ const clearTabs = () => { setOpenTabs([HOME_PATH]) if (location.pathname !== HOME_PATH && PAGE_LABELS[location.pathname]) navigate(HOME_PATH) } useEffect(() => { const keyword = query.trim() if (!keyword) { setResults([]); setSearching(false); return } const timer = window.setTimeout(async () => { setSearching(true) try { const response = await axios.get('/api/search', { params: { q: keyword } }) setResults(response.data.results || []) } catch { setResults([]) } finally { setSearching(false) } }, 180) return () => window.clearTimeout(timer) }, [query]) useEffect(() => { const onKeyDown = event => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault() setSearchOpen(true) window.setTimeout(() => searchRef.current?.focus(), 0) } if (event.key === 'Escape') { setSearchOpen(false); searchRef.current?.blur() } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, []) /* 页内所有跳转都走外壳的 guardedNavigate:个人资料有未保存改动时会先弹确认 */ const go = React.useCallback(path => { if (onNavigate) onNavigate(path) else navigate(path) }, [onNavigate, navigate]) const selectResult = result => { go(result.path); setQuery(''); setSearchOpen(false) } const displayName = user?.display_name || user?.username || '未登录' const brand = useBranding() const positionText = user?.position || roleLabel(user?.role) const avatarText = (displayName || '?').slice(0, 1).toUpperCase() return (
{brand.name}{brand.subtitle}
setSearchOpen(true)} onChange={event => setQuery(event.target.value)} placeholder="搜索 everything" autoComplete="off" /> ⌘ K {searchOpen && query.trim() &&
{searching ?
正在检索系统业务数据…
: results.length ? results.map(result => ) :
未找到匹配的数据
}
已检索客户、供应商、产品、订单、商机、待办和活动记录
}
{/* 已打开页面:搜索框后的快速跳转标签,工作台常驻且不可关闭 */} {openTabs.length > 1 && ( )}
) } export default GlobalHeader