// REQ-USR-003 / REQ-USR-004: 应用外壳(TopBar + NavOverlay + + AppFooter)。 // 持有标签栈 / overlay 开关本地态(D3);据路由反向同步激活标签;登录态复用 Redux authSlice。 import { useCallback, useEffect, useMemo, useState } from 'react'; import { Outlet, useLocation, useNavigate, matchPath } from 'react-router-dom'; import { App as AntdApp } from 'antd'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { clearCredentials } from '../../store/slices/authSlice'; import TopBar from './TopBar'; import NavOverlay from './NavOverlay'; import { useTabStack, BIZ_TABS } from './useTabStack'; import type { BizTabKey } from './useTabStack'; import { LOGOUT_SUCCESS_TEXT, FEATURE_WIP_TEXT } from './shellMessages'; import styles from './AppLayout.module.css'; /** 据当前路径推导应激活/应打开的业务标签 key(路由 → 标签反向同步) */ function deriveBizTab(pathname: string): BizTabKey | null { if (matchPath('/usr/users/new', pathname) || matchPath('/usr/users/:id', pathname)) { return 'userdetail'; } if (matchPath('/usr/users', pathname)) { return 'userlist'; } return null; } export default function AppLayout() { const user = useAppSelector((s) => s.auth.user); const dispatch = useAppDispatch(); const navigate = useNavigate(); const location = useLocation(); const { message } = AntdApp.useApp(); const { tabs, activeKey, openTab, closeTab, setActive } = useTabStack(); const [navOverlayOpen, setNavOverlayOpen] = useState(false); // 路由 → 标签反向同步:进入业务路由时确保对应标签打开并激活;回主页激活 home useEffect(() => { const biz = deriveBizTab(location.pathname); if (biz) { openTab(biz); } else if (matchPath('/', location.pathname)) { setActive('home'); } // openTab/setActive 为稳定回调;仅在路径变化时同步 // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]); const handleSelectTab = useCallback( (key: string) => { const tab = tabs.find((t) => t.key === key); if (tab) { setActive(key); navigate(tab.routePath); } }, [tabs, setActive, navigate], ); const handleCloseTab = useCallback( (key: string) => { closeTab(key); // 关闭联动后跳转到对应路由(BR5/BR6) if (key === 'userlist') { navigate('/'); } else if (key === 'userdetail') { navigate(BIZ_TABS.userlist.routePath); } }, [closeTab, navigate], ); const handleLogout = useCallback(() => { dispatch(clearCredentials()); message.success(LOGOUT_SUCCESS_TEXT); navigate('/login', { replace: true }); }, [dispatch, message, navigate]); const handleLogoHome = useCallback(() => { setActive('home'); navigate('/'); }, [setActive, navigate]); const handleNavToggle = useCallback(() => { setNavOverlayOpen((v) => !v); }, []); const handleOverlayNavigate = useCallback( (routePath: string) => { setNavOverlayOpen(false); if (routePath === BIZ_TABS.userlist.routePath) { openTab('userlist'); } navigate(routePath); }, [openTab, navigate], ); const handleOverlayPlaceholder = useCallback(() => { setNavOverlayOpen(false); message.info(FEATURE_WIP_TEXT); }, [message]); const stableTabs = useMemo(() => tabs, [tabs]); return (
setNavOverlayOpen(false)} onNavigate={handleOverlayNavigate} onPlaceholder={handleOverlayPlaceholder} />
); }