AppLayout.tsx
4.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// REQ-USR-003 / REQ-USR-004: 应用外壳(TopBar + NavOverlay + <Outlet/> + 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 (
<div className={styles.app}>
<TopBar
user={user}
tabs={stableTabs}
activeKey={activeKey}
navOverlayOpen={navOverlayOpen}
onToggleNav={handleNavToggle}
onSelectTab={handleSelectTab}
onCloseTab={handleCloseTab}
onLogout={handleLogout}
onLogoHome={handleLogoHome}
/>
<div className={styles.stage}>
<NavOverlay
open={navOverlayOpen}
onClose={() => setNavOverlayOpen(false)}
onNavigate={handleOverlayNavigate}
onPlaceholder={handleOverlayPlaceholder}
/>
<Outlet />
</div>
</div>
);
}