@zat-design/sisyphus-react 4.5.9-beta.1 → 4.5.10-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.css +1 -1
- package/dist/less.esm.css +1 -1
- package/es/ProAction/components/CheckModalContent/index.js +2 -2
- package/es/ProConfigProvider/index.js +12 -3
- package/es/ProDownload/index.d.ts +2 -1
- package/es/ProDownload/index.js +48 -35
- package/es/ProDownload/utils.d.ts +10 -5
- package/es/ProDownload/utils.js +28 -13
- package/es/ProEditTable/utils/index.js +6 -1
- package/es/ProEnum/hooks/useEnumRequest.js +98 -77
- package/es/ProEnum/index.js +3 -4
- package/es/ProForm/components/base/InputNumber/index.js +1 -1
- package/es/ProForm/components/combination/FormList/components/ActionButton.js +2 -2
- package/es/ProForm/components/combination/FormList/components/BlockFields.js +1 -0
- package/es/ProForm/components/combination/FormList/components/Empty.js +1 -1
- package/es/ProForm/components/combination/FormList/components/ToolbarButton.js +3 -3
- package/es/ProForm/components/combination/FormList/style/index.less +6 -0
- package/es/ProForm/components/combination/Group/component/FlexibleGroup.js +5 -11
- package/es/ProForm/components/combination/Group/hooks/index.js +1 -1
- package/es/ProForm/components/combination/Group/index.js +1 -1
- package/es/ProForm/components/combination/ProModalSelect/hooks/useRequestList.js +22 -8
- package/es/ProForm/components/combination/ProModalSelect/index.js +32 -12
- package/es/ProForm/components/combination/ProRangeLimit/index.js +1 -1
- package/es/ProForm/components/render/Render.js +2 -2
- package/es/ProForm/hooks/useWatch.js +7 -8
- package/es/ProForm/utils/index.js +2 -2
- package/es/ProLayout/components/Layout/Menu/OpenMenu/index.js +8 -8
- package/es/ProLayout/components/Layout/Menu/index.js +1 -1
- package/es/ProLayout/components/ProFooter/index.js +2 -1
- package/es/ProLayout/components/ProHeader/components/Describe/index.js +3 -1
- package/es/ProLayout/components/TabsManager/components/TabItem.js +6 -5
- package/es/ProLayout/components/TabsManager/hooks/useTabsState.js +104 -16
- package/es/ProLayout/components/TabsManager/hooks/useTabsUrlSync.d.ts +19 -0
- package/es/ProLayout/components/TabsManager/hooks/useTabsUrlSync.js +295 -0
- package/es/ProLayout/components/TabsManager/index.js +54 -14
- package/es/ProLayout/components/TabsManager/propTypes.d.ts +4 -0
- package/es/ProLayout/components/TabsManager/style/index.less +1 -1
- package/es/ProLayout/components/TabsManager/utils/historyObserver.d.ts +7 -0
- package/es/ProLayout/components/TabsManager/utils/historyObserver.js +62 -0
- package/es/ProLayout/components/TabsManager/utils/index.d.ts +15 -2
- package/es/ProLayout/components/TabsManager/utils/index.js +63 -9
- package/es/ProLayout/images/logoImg.png +0 -0
- package/es/ProLayout/index.js +6 -3
- package/es/ProLayout/propTypes.d.ts +28 -1
- package/es/ProSelect/index.js +10 -1
- package/es/ProSelect/style/index.less +13 -0
- package/es/ProStep/style/index.less +1 -1
- package/es/ProTable/hooks/useAntdTable.js +11 -10
- package/es/ProTabs/index.js +3 -3
- package/es/ProTooltip/index.js +2 -5
- package/es/ProTree/components/ProTreeSelect/index.js +2 -2
- package/es/ProTreeModal/index.js +1 -4
- package/es/ProUpload/components/DragRender.js +0 -14
- package/package.json +5 -1
- package/es/ProConfigProvider/demo/locale.js +0 -58
|
@@ -3,7 +3,7 @@ import { message } from 'antd';
|
|
|
3
3
|
import { DEFAULT_TABS_CONFIG } from "../propTypes";
|
|
4
4
|
import { useTabsCache } from "./useTabsCache";
|
|
5
5
|
import locale, { formatMessage } from "../../../../locale";
|
|
6
|
-
import { createTabFromMenu, generateTabId, shouldOpenExternal, handleExternalOpen, checkTabLimit, getRightTabs, flattenMenuData, canOpenAsTab, getMenuRoute, findExistingTab, resolveTabMenuItem, derivePinned, getPinnedBoundary, pinTab as pinTabInList, unpinTab as unpinTabInList, reorderWithPinConstraint } from "../utils";
|
|
6
|
+
import { createTabFromMenu, generateTabId, shouldOpenExternal, handleExternalOpen, checkTabLimit, getRightTabs, flattenMenuData, canOpenAsTab, getMenuRoute, findExistingTab, normalizeTabUrl, isSafeExternalUrl, isSafeHttpUrl, resolveTabMenuItem, derivePinned, getPinnedBoundary, pinTab as pinTabInList, unpinTab as unpinTabInList, reorderWithPinConstraint } from "../utils";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 标签页状态管理Hook
|
|
@@ -21,6 +21,7 @@ export const useTabsState = options => {
|
|
|
21
21
|
...DEFAULT_TABS_CONFIG,
|
|
22
22
|
...config
|
|
23
23
|
};
|
|
24
|
+
const urlSyncEnabled = finalConfig.urlSync === true || typeof finalConfig.urlSync === 'object' && finalConfig.urlSync !== null;
|
|
24
25
|
|
|
25
26
|
// 初始化状态
|
|
26
27
|
const [state, setState] = useState(() => ({
|
|
@@ -47,7 +48,10 @@ export const useTabsState = options => {
|
|
|
47
48
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
48
49
|
|
|
49
50
|
// 从缓存恢复状态
|
|
51
|
+
const hasRestoredCacheRef = useRef(false);
|
|
50
52
|
useEffect(() => {
|
|
53
|
+
if (hasRestoredCacheRef.current) return;
|
|
54
|
+
hasRestoredCacheRef.current = true;
|
|
51
55
|
const cachedState = restoreFromCache();
|
|
52
56
|
if (cachedState && cachedState.tabsList && cachedState.tabsList.length > 0) {
|
|
53
57
|
// 兼容旧缓存:如果缓存中有 activeTab 但没有 activeTabInfo,进行迁移
|
|
@@ -67,11 +71,33 @@ export const useTabsState = options => {
|
|
|
67
71
|
pinned: tab.pinned === undefined ? derivePinned(tab) : tab.pinned
|
|
68
72
|
}));
|
|
69
73
|
restoredState.activeKey = restoredState.activeKey === undefined || restoredState.activeKey === null ? '' : String(restoredState.activeKey);
|
|
74
|
+
if (urlSyncEnabled) {
|
|
75
|
+
const retainedTabByUrl = new Map();
|
|
76
|
+
const retainedIdByOriginalId = new Map();
|
|
77
|
+
restoredState.tabsList.forEach(tab => {
|
|
78
|
+
const normalizedUrl = normalizeTabUrl(tab.url);
|
|
79
|
+
if (!normalizedUrl) return;
|
|
80
|
+
const retainedTab = retainedTabByUrl.get(normalizedUrl);
|
|
81
|
+
if (retainedTab) {
|
|
82
|
+
retainedIdByOriginalId.set(tab.id, retainedTab.id);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const normalizedTab = {
|
|
86
|
+
...tab,
|
|
87
|
+
url: normalizedUrl
|
|
88
|
+
};
|
|
89
|
+
retainedTabByUrl.set(normalizedUrl, normalizedTab);
|
|
90
|
+
retainedIdByOriginalId.set(tab.id, normalizedTab.id);
|
|
91
|
+
});
|
|
92
|
+
restoredState.tabsList = Array.from(retainedTabByUrl.values());
|
|
93
|
+
restoredState.activeKey = retainedIdByOriginalId.get(restoredState.activeKey) || restoredState.tabsList[0]?.id || '';
|
|
94
|
+
}
|
|
70
95
|
restoredState.activeTabInfo = restoredState.tabsList.find(tab => tab.id === restoredState.activeKey);
|
|
96
|
+
restoredState.activeComponent = restoredState.activeTabInfo ? restoredState.activeTabInfo.menuItem?.code || restoredState.activeTabInfo.id : '';
|
|
71
97
|
setState(restoredState);
|
|
72
98
|
}
|
|
73
99
|
setIsInitialized(true);
|
|
74
|
-
}, [restoreFromCache]);
|
|
100
|
+
}, [restoreFromCache, urlSyncEnabled]);
|
|
75
101
|
const lastActiveKeyRef = useRef('');
|
|
76
102
|
const shouldSkipSaveRef = useRef(false);
|
|
77
103
|
const initialTabsAppliedRef = useRef(false);
|
|
@@ -128,9 +154,11 @@ export const useTabsState = options => {
|
|
|
128
154
|
return false;
|
|
129
155
|
}
|
|
130
156
|
|
|
131
|
-
//
|
|
132
|
-
if (!forceNew) {
|
|
133
|
-
const existingTab = findExistingTab(state.tabsList, menuItem
|
|
157
|
+
// URL 同步模式下完整 URL 始终唯一,forceNew 不能绕过身份匹配。
|
|
158
|
+
if (urlSyncEnabled || !forceNew) {
|
|
159
|
+
const existingTab = findExistingTab(state.tabsList, menuItem, {
|
|
160
|
+
urlSync: urlSyncEnabled
|
|
161
|
+
});
|
|
134
162
|
if (existingTab) {
|
|
135
163
|
// 如果已存在,可以添加(会切换到已存在的标签页)
|
|
136
164
|
return true;
|
|
@@ -143,7 +171,7 @@ export const useTabsState = options => {
|
|
|
143
171
|
return false;
|
|
144
172
|
}
|
|
145
173
|
return true;
|
|
146
|
-
}, [state.tabsList, finalConfig]);
|
|
174
|
+
}, [state.tabsList, finalConfig, urlSyncEnabled]);
|
|
147
175
|
|
|
148
176
|
/**
|
|
149
177
|
* 添加标签页
|
|
@@ -156,15 +184,33 @@ export const useTabsState = options => {
|
|
|
156
184
|
setState(prevState => {
|
|
157
185
|
if (!canOpenAsTab(menuItem)) return prevState;
|
|
158
186
|
if (shouldOpenExternal(menuItem)) {
|
|
159
|
-
|
|
187
|
+
const externalUrl = getMenuRoute(menuItem);
|
|
188
|
+
if (!urlSyncEnabled || isSafeHttpUrl(externalUrl)) {
|
|
189
|
+
handleExternalOpen(menuItem, urlSyncEnabled);
|
|
190
|
+
}
|
|
191
|
+
return prevState;
|
|
192
|
+
}
|
|
193
|
+
const rawUrl = getMenuRoute(menuItem);
|
|
194
|
+
const normalizedUrl = urlSyncEnabled ? normalizeTabUrl(rawUrl) : undefined;
|
|
195
|
+
if (urlSyncEnabled && rawUrl && !normalizedUrl) {
|
|
196
|
+
if (isSafeExternalUrl(rawUrl)) {
|
|
197
|
+
handleExternalOpen(menuItem, true);
|
|
198
|
+
}
|
|
160
199
|
return prevState;
|
|
161
200
|
}
|
|
201
|
+
const resolvedMenuItem = normalizedUrl ? {
|
|
202
|
+
...menuItem,
|
|
203
|
+
url: normalizedUrl,
|
|
204
|
+
redirectUrl: undefined
|
|
205
|
+
} : menuItem;
|
|
162
206
|
|
|
163
207
|
// 非强制新建时,查找已存在的标签页并切换
|
|
164
|
-
const existingTab = !forceNew ? findExistingTab(prevState.tabsList,
|
|
208
|
+
const existingTab = urlSyncEnabled || !forceNew ? findExistingTab(prevState.tabsList, resolvedMenuItem, {
|
|
209
|
+
urlSync: urlSyncEnabled
|
|
210
|
+
}) : undefined;
|
|
165
211
|
if (existingTab) {
|
|
166
212
|
// 若来源菜单明确标记 closable: false 而已有 tab 未继承,则修正
|
|
167
|
-
const correctedTab =
|
|
213
|
+
const correctedTab = resolvedMenuItem.closable === false && existingTab.closable !== false ? {
|
|
168
214
|
...existingTab,
|
|
169
215
|
closable: false
|
|
170
216
|
} : existingTab;
|
|
@@ -184,8 +230,8 @@ export const useTabsState = options => {
|
|
|
184
230
|
return prevState;
|
|
185
231
|
}
|
|
186
232
|
const existingIds = prevState.tabsList.map(tab => tab.id);
|
|
187
|
-
const tabId = generateTabId(
|
|
188
|
-
const newTab = createTabFromMenu(
|
|
233
|
+
const tabId = generateTabId(resolvedMenuItem, existingIds);
|
|
234
|
+
const newTab = createTabFromMenu(resolvedMenuItem, prevState.newTabIndex);
|
|
189
235
|
newTab.id = tabId;
|
|
190
236
|
const pinnedBoundary = getPinnedBoundary(prevState.tabsList);
|
|
191
237
|
const nextTabsList = derivePinned(newTab) ? [...prevState.tabsList.slice(0, pinnedBoundary), newTab, ...prevState.tabsList.slice(pinnedBoundary)] : [...prevState.tabsList, newTab];
|
|
@@ -198,10 +244,10 @@ export const useTabsState = options => {
|
|
|
198
244
|
activeKey: shouldActivate ? tabId : prevState.activeKey,
|
|
199
245
|
activeTabInfo: shouldActivate ? newTab : prevState.activeTabInfo,
|
|
200
246
|
newTabIndex: prevState.newTabIndex + 1,
|
|
201
|
-
activeComponent: shouldActivate ?
|
|
247
|
+
activeComponent: shouldActivate ? resolvedMenuItem.code || tabId : prevState.activeComponent
|
|
202
248
|
};
|
|
203
249
|
});
|
|
204
|
-
}, [finalConfig]);
|
|
250
|
+
}, [finalConfig, urlSyncEnabled]);
|
|
205
251
|
|
|
206
252
|
// 普通初始标签只在当前 TabsManager 挂载周期应用一次。
|
|
207
253
|
// 缓存恢复已有标签时只消费默认值;closeAll 后 ref 保持 true,避免再次补签。
|
|
@@ -344,6 +390,46 @@ export const useTabsState = options => {
|
|
|
344
390
|
});
|
|
345
391
|
}, []);
|
|
346
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Router blocker 拒绝 URL 导航时,将激活状态还原到上一个已提交页签。
|
|
395
|
+
* 首次打开页签就被拦截时,同时移除未提交的新页签。
|
|
396
|
+
*/
|
|
397
|
+
const rollbackTabActivation = useCallback((previousTabId, attemptedTabId, removeAttemptedTab) => {
|
|
398
|
+
setState(prevState => {
|
|
399
|
+
if (prevState.activeKey !== attemptedTabId) return prevState;
|
|
400
|
+
const nextTabsList = removeAttemptedTab ? prevState.tabsList.filter(tab => tab.id !== attemptedTabId) : prevState.tabsList;
|
|
401
|
+
const previousTab = nextTabsList.find(tab => tab.id === previousTabId);
|
|
402
|
+
return {
|
|
403
|
+
...prevState,
|
|
404
|
+
tabsList: nextTabsList,
|
|
405
|
+
activeKey: previousTab?.id || '',
|
|
406
|
+
activeTabInfo: previousTab,
|
|
407
|
+
activeComponent: previousTab ? previousTab.menuItem?.code || previousTab.id : ''
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
}, []);
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 更新页签当前 URL,保持 tab.id 和 menuItem 的菜单基础路由不变。
|
|
414
|
+
*/
|
|
415
|
+
const updateTabUrl = useCallback((tabId, url) => {
|
|
416
|
+
setState(prevState => {
|
|
417
|
+
const targetIndex = prevState.tabsList.findIndex(tab => tab.id === tabId);
|
|
418
|
+
if (targetIndex === -1 || prevState.tabsList[targetIndex].url === url) return prevState;
|
|
419
|
+
const nextTab = {
|
|
420
|
+
...prevState.tabsList[targetIndex],
|
|
421
|
+
url
|
|
422
|
+
};
|
|
423
|
+
const nextTabsList = [...prevState.tabsList];
|
|
424
|
+
nextTabsList[targetIndex] = nextTab;
|
|
425
|
+
return {
|
|
426
|
+
...prevState,
|
|
427
|
+
tabsList: nextTabsList,
|
|
428
|
+
activeTabInfo: prevState.activeKey === tabId ? nextTab : prevState.activeTabInfo
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
}, []);
|
|
432
|
+
|
|
347
433
|
/**
|
|
348
434
|
* 关闭其他标签页
|
|
349
435
|
*/
|
|
@@ -474,9 +560,9 @@ export const useTabsState = options => {
|
|
|
474
560
|
});
|
|
475
561
|
}, []);
|
|
476
562
|
|
|
477
|
-
//
|
|
478
|
-
// 放在最后以确保可以使用 addTab 和 switchTab
|
|
563
|
+
// URL 不同步模式保留旧有的 pathname -> Tabs 初始匹配,但不写回 URL。
|
|
479
564
|
useEffect(() => {
|
|
565
|
+
if (urlSyncEnabled) return;
|
|
480
566
|
if (!dataSource || !('menus' in dataSource)) return;
|
|
481
567
|
const currentPath = window.location.pathname;
|
|
482
568
|
const activeTab = state.tabsList.find(tab => tab.id === state.activeKey);
|
|
@@ -490,7 +576,7 @@ export const useTabsState = options => {
|
|
|
490
576
|
return;
|
|
491
577
|
}
|
|
492
578
|
addTab(targetMenu);
|
|
493
|
-
}, [dataSource, state.tabsList, state.activeKey, addTab, switchTab]);
|
|
579
|
+
}, [urlSyncEnabled, dataSource, state.tabsList, state.activeKey, addTab, switchTab]);
|
|
494
580
|
return {
|
|
495
581
|
state,
|
|
496
582
|
isInitialized,
|
|
@@ -498,7 +584,9 @@ export const useTabsState = options => {
|
|
|
498
584
|
canAddTab,
|
|
499
585
|
removeTab,
|
|
500
586
|
updateTab,
|
|
587
|
+
updateTabUrl,
|
|
501
588
|
switchTab,
|
|
589
|
+
rollbackTabActivation,
|
|
502
590
|
closeOtherTabs,
|
|
503
591
|
closeRightTabs,
|
|
504
592
|
closeAllTabs,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TabsConfig, TabsState } from '../../../propTypes';
|
|
2
|
+
import type { TabsManagerProps, UseTabsStateReturn } from '../propTypes';
|
|
3
|
+
interface UseTabsUrlSyncOptions {
|
|
4
|
+
config: TabsConfig;
|
|
5
|
+
dataSource: TabsManagerProps['dataSource'];
|
|
6
|
+
state: TabsState;
|
|
7
|
+
isInitialized: boolean;
|
|
8
|
+
addTab: UseTabsStateReturn['addTab'];
|
|
9
|
+
switchTab: UseTabsStateReturn['switchTab'];
|
|
10
|
+
updateTabUrl: UseTabsStateReturn['updateTabUrl'];
|
|
11
|
+
rollbackTabActivation: UseTabsStateReturn['rollbackTabActivation'];
|
|
12
|
+
interactionSequence: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 维护浏览器 location 与活动页签 URL 的双向同步。
|
|
16
|
+
* 未开启 config.urlSync 时不安装任何 History 监听或写入逻辑。
|
|
17
|
+
*/
|
|
18
|
+
export declare const useTabsUrlSync: ({ config, dataSource, state, isInitialized, addTab, switchTab, updateTabUrl, rollbackTabActivation, interactionSequence, }: UseTabsUrlSyncOptions) => void;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import * as ReactRouterDom from 'react-router-dom';
|
|
3
|
+
import { canOpenAsTab, flattenMenuData, getCurrentTabUrl, getMenuRoute, getTabUrlPathname, normalizeTabUrl } from "../utils";
|
|
4
|
+
import { subscribeHistoryLocation } from "../utils/historyObserver";
|
|
5
|
+
const getOptionalRouterContext = exportName => {
|
|
6
|
+
const descriptor = Object.getOwnPropertyDescriptor(ReactRouterDom, exportName);
|
|
7
|
+
return descriptor ? Reflect.get(ReactRouterDom, exportName) : undefined;
|
|
8
|
+
};
|
|
9
|
+
const OptionalNavigationContext = getOptionalRouterContext('UNSAFE_NavigationContext') || /*#__PURE__*/createContext(null);
|
|
10
|
+
|
|
11
|
+
// Data Router 从 react-router-dom 6.4 起提供;保留 6.0-6.3 peer 兼容。
|
|
12
|
+
const OptionalDataRouterContext = getOptionalRouterContext('UNSAFE_DataRouterContext') || /*#__PURE__*/createContext(null);
|
|
13
|
+
const getDataRouterUrl = (url, basename) => {
|
|
14
|
+
if (!basename || basename === '/') return url;
|
|
15
|
+
const pathname = getTabUrlPathname(url);
|
|
16
|
+
if (!pathname) return url;
|
|
17
|
+
const normalizedBasename = `/${basename.replace(/^\/+|\/+$/g, '')}`;
|
|
18
|
+
const lowerPathname = pathname.toLowerCase();
|
|
19
|
+
const lowerBasename = normalizedBasename.toLowerCase();
|
|
20
|
+
const suffix = url.slice(pathname.length);
|
|
21
|
+
if (lowerPathname === lowerBasename) return `/${suffix}`;
|
|
22
|
+
if (lowerPathname.startsWith(`${lowerBasename}/`)) {
|
|
23
|
+
return `${pathname.slice(normalizedBasename.length)}${suffix}`;
|
|
24
|
+
}
|
|
25
|
+
return url;
|
|
26
|
+
};
|
|
27
|
+
const isCurrentDataRouterNavigation = (currentUrl, dataRouterContext) => {
|
|
28
|
+
const navigationLocation = dataRouterContext?.router.state?.navigation?.location;
|
|
29
|
+
if (!navigationLocation) return false;
|
|
30
|
+
const navigationUrl = `${navigationLocation.pathname}${navigationLocation.search ?? ''}${navigationLocation.hash ?? ''}`;
|
|
31
|
+
return navigationUrl === currentUrl || navigationUrl === getDataRouterUrl(currentUrl, dataRouterContext?.basename);
|
|
32
|
+
};
|
|
33
|
+
const isSameOrNestedPath = (basePathname, currentPathname) => {
|
|
34
|
+
if (!basePathname || !currentPathname) return false;
|
|
35
|
+
const normalizedBase = basePathname === '/' ? '/' : basePathname.replace(/\/$/, '');
|
|
36
|
+
return currentPathname === normalizedBase || normalizedBase !== '/' && currentPathname.startsWith(`${normalizedBase}/`);
|
|
37
|
+
};
|
|
38
|
+
const getSourceMenus = dataSource => {
|
|
39
|
+
if (!dataSource || !('menus' in dataSource) || !Array.isArray(dataSource.menus)) return [];
|
|
40
|
+
return flattenMenuData(dataSource.menus);
|
|
41
|
+
};
|
|
42
|
+
const createPushHistoryState = currentState => {
|
|
43
|
+
if (!currentState || typeof currentState !== 'object' || Array.isArray(currentState)) {
|
|
44
|
+
return currentState;
|
|
45
|
+
}
|
|
46
|
+
const nextState = {
|
|
47
|
+
...currentState
|
|
48
|
+
};
|
|
49
|
+
const currentIndex = nextState.idx;
|
|
50
|
+
const currentKey = nextState.key;
|
|
51
|
+
const isReactRouterState = 'usr' in nextState && typeof currentIndex === 'number' && typeof currentKey === 'string';
|
|
52
|
+
if (isReactRouterState) {
|
|
53
|
+
nextState.idx = currentIndex + 1;
|
|
54
|
+
nextState.key = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
55
|
+
}
|
|
56
|
+
return nextState;
|
|
57
|
+
};
|
|
58
|
+
const getRouterLocationState = currentState => {
|
|
59
|
+
if (!currentState || typeof currentState !== 'object' || Array.isArray(currentState)) {
|
|
60
|
+
return currentState;
|
|
61
|
+
}
|
|
62
|
+
const state = currentState;
|
|
63
|
+
if (typeof state.idx !== 'number') return currentState;
|
|
64
|
+
return 'usr' in state ? state.usr : null;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* 维护浏览器 location 与活动页签 URL 的双向同步。
|
|
68
|
+
* 未开启 config.urlSync 时不安装任何 History 监听或写入逻辑。
|
|
69
|
+
*/
|
|
70
|
+
export const useTabsUrlSync = ({
|
|
71
|
+
config,
|
|
72
|
+
dataSource,
|
|
73
|
+
state,
|
|
74
|
+
isInitialized,
|
|
75
|
+
addTab,
|
|
76
|
+
switchTab,
|
|
77
|
+
updateTabUrl,
|
|
78
|
+
rollbackTabActivation,
|
|
79
|
+
interactionSequence
|
|
80
|
+
}) => {
|
|
81
|
+
const navigationContext = useContext(OptionalNavigationContext);
|
|
82
|
+
const dataRouterContext = useContext(OptionalDataRouterContext);
|
|
83
|
+
const navigator = navigationContext?.navigator;
|
|
84
|
+
const enabled = config.urlSync === true || typeof config.urlSync === 'object' && config.urlSync !== null;
|
|
85
|
+
const historyMode = typeof config.urlSync === 'object' ? config.urlSync.history || 'replace' : 'replace';
|
|
86
|
+
const bootstrappedRef = useRef(false);
|
|
87
|
+
const writingUrlRef = useRef(false);
|
|
88
|
+
const preserveUrlInteractionRef = useRef(null);
|
|
89
|
+
const locationSequenceRef = useRef(0);
|
|
90
|
+
const handledLocationSequenceRef = useRef(undefined);
|
|
91
|
+
const committedActiveKeyRef = useRef(state.activeKey);
|
|
92
|
+
const committedTabIdsRef = useRef(new Set(state.tabsList.map(tab => tab.id)));
|
|
93
|
+
const navigationAttemptRef = useRef(0);
|
|
94
|
+
const pendingDataNavigationRef = useRef(false);
|
|
95
|
+
const pendingDataNavigationUrlRef = useRef(null);
|
|
96
|
+
const ignoredHistoryUrlRef = useRef(null);
|
|
97
|
+
const [locationChange, setLocationChange] = useState(() => ({
|
|
98
|
+
url: typeof window === 'undefined' ? '' : getCurrentTabUrl(),
|
|
99
|
+
source: 'initial',
|
|
100
|
+
sequence: 0
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// History API 不会为 pushState/replaceState 触发 popstate,在挂载期间补齐通知。
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!enabled || typeof window === 'undefined') return undefined;
|
|
106
|
+
return subscribeHistoryLocation(source => {
|
|
107
|
+
if (writingUrlRef.current) return;
|
|
108
|
+
const currentUrl = getCurrentTabUrl();
|
|
109
|
+
if (ignoredHistoryUrlRef.current === currentUrl) {
|
|
110
|
+
ignoredHistoryUrlRef.current = null;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const pendingDataNavigation = pendingDataNavigationRef.current;
|
|
114
|
+
const pendingDataNavigationUrl = pendingDataNavigationUrlRef.current;
|
|
115
|
+
const isPendingDataNavigationCommit = pendingDataNavigation && (pendingDataNavigationUrl === currentUrl || isCurrentDataRouterNavigation(currentUrl, dataRouterContext));
|
|
116
|
+
if (!isPendingDataNavigationCommit) {
|
|
117
|
+
navigationAttemptRef.current += 1;
|
|
118
|
+
}
|
|
119
|
+
pendingDataNavigationRef.current = false;
|
|
120
|
+
pendingDataNavigationUrlRef.current = isPendingDataNavigationCommit ? currentUrl : null;
|
|
121
|
+
if (pendingDataNavigation && !isPendingDataNavigationCommit && dataRouterContext) {
|
|
122
|
+
ignoredHistoryUrlRef.current = currentUrl;
|
|
123
|
+
const cancellation = dataRouterContext.router.navigate(getDataRouterUrl(currentUrl, dataRouterContext.basename), {
|
|
124
|
+
replace: true,
|
|
125
|
+
state: getRouterLocationState(window.history.state)
|
|
126
|
+
});
|
|
127
|
+
void cancellation.finally(() => {
|
|
128
|
+
if (ignoredHistoryUrlRef.current === currentUrl) {
|
|
129
|
+
ignoredHistoryUrlRef.current = null;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
preserveUrlInteractionRef.current = null;
|
|
134
|
+
locationSequenceRef.current += 1;
|
|
135
|
+
setLocationChange({
|
|
136
|
+
url: currentUrl,
|
|
137
|
+
source,
|
|
138
|
+
sequence: locationSequenceRef.current
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}, [enabled, dataRouterContext]);
|
|
142
|
+
|
|
143
|
+
// URL -> Tabs:精确 URL 优先;同 pathname 视为当前页签内导航;
|
|
144
|
+
// 其次才按菜单 pathname 创建新页签。
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!enabled || !isInitialized || typeof window === 'undefined') return;
|
|
147
|
+
const currentUrl = normalizeTabUrl(locationChange.url);
|
|
148
|
+
if (!currentUrl) return;
|
|
149
|
+
if (handledLocationSequenceRef.current === locationChange.sequence) return;
|
|
150
|
+
const exactTab = state.tabsList.find(tab => normalizeTabUrl(tab.url) === currentUrl);
|
|
151
|
+
if (exactTab) {
|
|
152
|
+
handledLocationSequenceRef.current = locationChange.sequence;
|
|
153
|
+
bootstrappedRef.current = true;
|
|
154
|
+
preserveUrlInteractionRef.current = null;
|
|
155
|
+
if (state.activeKey !== exactTab.id) {
|
|
156
|
+
switchTab(exactTab.id);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const currentPathname = getTabUrlPathname(currentUrl);
|
|
161
|
+
const sourceMenus = getSourceMenus(dataSource);
|
|
162
|
+
const activeTab = state.tabsList.find(tab => tab.id === state.activeKey);
|
|
163
|
+
const activeCode = activeTab?.menuItem?.code || activeTab?.code;
|
|
164
|
+
const activeSourceMenu = activeCode ? sourceMenus.find(item => item.code === activeCode) : undefined;
|
|
165
|
+
const activeMenuItem = activeSourceMenu || activeTab?.menuItem;
|
|
166
|
+
const activeBasePathname = activeMenuItem ? getTabUrlPathname(getMenuRoute(activeMenuItem)) : getTabUrlPathname(activeTab?.url);
|
|
167
|
+
const targetMenu = sourceMenus.map(item => ({
|
|
168
|
+
item,
|
|
169
|
+
pathname: getTabUrlPathname(getMenuRoute(item))
|
|
170
|
+
})).filter(entry => !!entry.pathname && isSameOrNestedPath(entry.pathname, currentPathname)).sort((left, right) => right.pathname.length - left.pathname.length)[0]?.item;
|
|
171
|
+
const targetBelongsToActiveMenu = !targetMenu || !!activeCode && targetMenu.code === activeCode;
|
|
172
|
+
if (activeTab && locationChange.source !== 'initial' && targetBelongsToActiveMenu && isSameOrNestedPath(activeBasePathname, currentPathname)) {
|
|
173
|
+
handledLocationSequenceRef.current = locationChange.sequence;
|
|
174
|
+
bootstrappedRef.current = true;
|
|
175
|
+
preserveUrlInteractionRef.current = null;
|
|
176
|
+
updateTabUrl(activeTab.id, currentUrl);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (targetMenu && canOpenAsTab(targetMenu)) {
|
|
180
|
+
handledLocationSequenceRef.current = locationChange.sequence;
|
|
181
|
+
bootstrappedRef.current = true;
|
|
182
|
+
preserveUrlInteractionRef.current = interactionSequence;
|
|
183
|
+
addTab({
|
|
184
|
+
...targetMenu,
|
|
185
|
+
url: currentUrl,
|
|
186
|
+
redirectUrl: undefined
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!bootstrappedRef.current) {
|
|
191
|
+
bootstrappedRef.current = true;
|
|
192
|
+
preserveUrlInteractionRef.current = interactionSequence;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (activeTab && locationChange.source !== 'initial') {
|
|
196
|
+
handledLocationSequenceRef.current = locationChange.sequence;
|
|
197
|
+
preserveUrlInteractionRef.current = null;
|
|
198
|
+
navigationAttemptRef.current += 1;
|
|
199
|
+
updateTabUrl(activeTab.id, currentUrl);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (locationChange.source !== 'initial') {
|
|
203
|
+
handledLocationSequenceRef.current = locationChange.sequence;
|
|
204
|
+
}
|
|
205
|
+
}, [enabled, isInitialized, locationChange, state.tabsList, state.activeKey, dataSource, addTab, switchTab, updateTabUrl, interactionSequence]);
|
|
206
|
+
|
|
207
|
+
// Tabs -> URL:默认用 replace 避免切签堆积浏览器历史。
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!enabled || !isInitialized || !bootstrappedRef.current || typeof window === 'undefined') {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const activeTab = state.tabsList.find(tab => tab.id === state.activeKey);
|
|
213
|
+
const targetUrl = normalizeTabUrl(activeTab?.url);
|
|
214
|
+
const commitActiveTab = () => {
|
|
215
|
+
committedActiveKeyRef.current = state.activeKey;
|
|
216
|
+
committedTabIdsRef.current = new Set(state.tabsList.map(tab => tab.id));
|
|
217
|
+
};
|
|
218
|
+
if (preserveUrlInteractionRef.current !== null) {
|
|
219
|
+
if (preserveUrlInteractionRef.current === interactionSequence) {
|
|
220
|
+
if (targetUrl === getCurrentTabUrl()) {
|
|
221
|
+
preserveUrlInteractionRef.current = null;
|
|
222
|
+
}
|
|
223
|
+
commitActiveTab();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
preserveUrlInteractionRef.current = null;
|
|
227
|
+
}
|
|
228
|
+
if (!targetUrl || targetUrl === getCurrentTabUrl()) {
|
|
229
|
+
const pendingNavigation = pendingDataNavigationRef.current;
|
|
230
|
+
navigationAttemptRef.current += 1;
|
|
231
|
+
pendingDataNavigationRef.current = false;
|
|
232
|
+
pendingDataNavigationUrlRef.current = null;
|
|
233
|
+
if (pendingNavigation && dataRouterContext) {
|
|
234
|
+
const cancellationUrl = getCurrentTabUrl();
|
|
235
|
+
ignoredHistoryUrlRef.current = cancellationUrl;
|
|
236
|
+
const cancellation = dataRouterContext.router.navigate(getDataRouterUrl(cancellationUrl, dataRouterContext.basename), {
|
|
237
|
+
replace: true,
|
|
238
|
+
state: getRouterLocationState(window.history.state)
|
|
239
|
+
});
|
|
240
|
+
void cancellation.finally(() => {
|
|
241
|
+
if (ignoredHistoryUrlRef.current === cancellationUrl) {
|
|
242
|
+
ignoredHistoryUrlRef.current = null;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
commitActiveTab();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
handledLocationSequenceRef.current = locationChange.sequence;
|
|
250
|
+
const previousActiveKey = committedActiveKeyRef.current;
|
|
251
|
+
const attemptedActiveKey = state.activeKey;
|
|
252
|
+
const removeAttemptedTab = !committedTabIdsRef.current.has(attemptedActiveKey);
|
|
253
|
+
const navigationAttempt = navigationAttemptRef.current + 1;
|
|
254
|
+
navigationAttemptRef.current = navigationAttempt;
|
|
255
|
+
writingUrlRef.current = true;
|
|
256
|
+
try {
|
|
257
|
+
const routerState = getRouterLocationState(window.history.state);
|
|
258
|
+
if (dataRouterContext) {
|
|
259
|
+
const settleNavigation = () => {
|
|
260
|
+
if (navigationAttemptRef.current !== navigationAttempt) return;
|
|
261
|
+
const committedDataNavigationUrl = pendingDataNavigationUrlRef.current;
|
|
262
|
+
pendingDataNavigationRef.current = false;
|
|
263
|
+
pendingDataNavigationUrlRef.current = null;
|
|
264
|
+
const currentUrl = getCurrentTabUrl();
|
|
265
|
+
if (targetUrl === currentUrl || committedDataNavigationUrl === currentUrl) {
|
|
266
|
+
commitActiveTab();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
rollbackTabActivation(previousActiveKey, attemptedActiveKey, removeAttemptedTab);
|
|
270
|
+
};
|
|
271
|
+
pendingDataNavigationRef.current = true;
|
|
272
|
+
pendingDataNavigationUrlRef.current = targetUrl;
|
|
273
|
+
void dataRouterContext.router.navigate(getDataRouterUrl(targetUrl, dataRouterContext.basename), {
|
|
274
|
+
replace: historyMode === 'replace',
|
|
275
|
+
state: routerState
|
|
276
|
+
}).then(settleNavigation);
|
|
277
|
+
} else if (navigator) {
|
|
278
|
+
if (historyMode === 'push') {
|
|
279
|
+
navigator.push(targetUrl, routerState);
|
|
280
|
+
} else {
|
|
281
|
+
navigator.replace(targetUrl, routerState);
|
|
282
|
+
}
|
|
283
|
+
commitActiveTab();
|
|
284
|
+
} else if (historyMode === 'push') {
|
|
285
|
+
window.history.pushState(createPushHistoryState(window.history.state), '', targetUrl);
|
|
286
|
+
commitActiveTab();
|
|
287
|
+
} else {
|
|
288
|
+
window.history.replaceState(window.history.state, '', targetUrl);
|
|
289
|
+
commitActiveTab();
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
writingUrlRef.current = false;
|
|
293
|
+
}
|
|
294
|
+
}, [enabled, historyMode, isInitialized, state.tabsList, state.activeKey, interactionSequence, navigator, dataRouterContext, rollbackTabActivation]);
|
|
295
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, forwardRef, useImperativeHandle, useState, useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { useTabsState } from "./hooks/useTabsState";
|
|
4
|
+
import { useTabsUrlSync } from "./hooks/useTabsUrlSync";
|
|
4
5
|
import { useIframeRoute } from "./hooks/useIframeRoute";
|
|
5
6
|
import { flattenMenuData, resolveTabMenuItem, derivePinned } from "./utils";
|
|
6
7
|
import { TabItemComponent } from "./components/TabItem";
|
|
@@ -22,12 +23,12 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
22
23
|
}, ref) => {
|
|
23
24
|
// iframe 模式检测(在 useTabsState 之前,因为 cacheEnabled 依赖 pure)
|
|
24
25
|
// addTab 由后续 useTabsState 提供,此处先用占位,后面用 ref 透传
|
|
25
|
-
const iframeAddTabRef = useRef(
|
|
26
|
+
const iframeAddTabRef = useRef(undefined);
|
|
26
27
|
const {
|
|
27
28
|
pure: isIframePure
|
|
28
29
|
} = useIframeRoute({
|
|
29
30
|
config: config?.iframe,
|
|
30
|
-
addTab: params => iframeAddTabRef.current(params)
|
|
31
|
+
addTab: params => iframeAddTabRef.current?.(params)
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
// 通知父级(ProLayout)pure 状态变化
|
|
@@ -46,7 +47,9 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
46
47
|
canAddTab,
|
|
47
48
|
removeTab,
|
|
48
49
|
updateTab,
|
|
50
|
+
updateTabUrl,
|
|
49
51
|
switchTab,
|
|
52
|
+
rollbackTabActivation,
|
|
50
53
|
closeOtherTabs,
|
|
51
54
|
closeRightTabs,
|
|
52
55
|
closeAllTabs,
|
|
@@ -58,6 +61,21 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
58
61
|
dataSource,
|
|
59
62
|
cacheEnabled: !isIframePure
|
|
60
63
|
});
|
|
64
|
+
const tabInteractionSequenceRef = useRef(0);
|
|
65
|
+
const markTabInteraction = useCallback(() => {
|
|
66
|
+
tabInteractionSequenceRef.current += 1;
|
|
67
|
+
}, []);
|
|
68
|
+
useTabsUrlSync({
|
|
69
|
+
config,
|
|
70
|
+
dataSource,
|
|
71
|
+
state,
|
|
72
|
+
isInitialized,
|
|
73
|
+
addTab,
|
|
74
|
+
switchTab,
|
|
75
|
+
updateTabUrl,
|
|
76
|
+
rollbackTabActivation,
|
|
77
|
+
interactionSequence: tabInteractionSequenceRef.current
|
|
78
|
+
});
|
|
61
79
|
|
|
62
80
|
// 桥接 useIframeRoute 的 { code, name, extra } → addTab(menuItem)
|
|
63
81
|
iframeAddTabRef.current = params => {
|
|
@@ -212,10 +230,11 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
212
230
|
// 处理菜单点击 - 拦截原有的菜单点击逻辑
|
|
213
231
|
const handleMenuClick = useCallback(params => {
|
|
214
232
|
if (params.item) {
|
|
233
|
+
markTabInteraction();
|
|
215
234
|
addTab(params.item);
|
|
216
235
|
}
|
|
217
236
|
originalOnMenuClick?.(params);
|
|
218
|
-
}, [addTab, originalOnMenuClick]);
|
|
237
|
+
}, [addTab, markTabInteraction, originalOnMenuClick]);
|
|
219
238
|
|
|
220
239
|
// 切换固定状态:已固定→取消固定,未固定→固定
|
|
221
240
|
const handleTogglePin = useCallback(tabId => {
|
|
@@ -231,11 +250,15 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
231
250
|
// 创建标签页实例 API
|
|
232
251
|
const tabsInstance = useMemo(() => ({
|
|
233
252
|
addTab: (params, options) => {
|
|
253
|
+
markTabInteraction();
|
|
234
254
|
const sourceMenus = Array.isArray(dataSource?.menus) ? dataSource.menus : [];
|
|
235
255
|
const menuItem = resolveTabMenuItem(params, sourceMenus);
|
|
236
256
|
addTab(menuItem, options);
|
|
237
257
|
},
|
|
238
|
-
removeTab
|
|
258
|
+
removeTab: tabId => {
|
|
259
|
+
markTabInteraction();
|
|
260
|
+
removeTab(tabId);
|
|
261
|
+
},
|
|
239
262
|
updateTab,
|
|
240
263
|
getTabInfo: () => ({
|
|
241
264
|
tabsList: state.tabsList,
|
|
@@ -245,15 +268,32 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
245
268
|
pushView,
|
|
246
269
|
back,
|
|
247
270
|
getViewStack
|
|
248
|
-
}), [addTab, removeTab, updateTab, state.tabsList, state.activeKey, state.activeComponent, dataSource, pushView, back, getViewStack]);
|
|
271
|
+
}), [addTab, markTabInteraction, removeTab, updateTab, state.tabsList, state.activeKey, state.activeComponent, dataSource, pushView, back, getViewStack]);
|
|
249
272
|
const handleTabChange = useCallback(activeKey => {
|
|
273
|
+
markTabInteraction();
|
|
250
274
|
switchTab(activeKey);
|
|
251
|
-
}, [switchTab]);
|
|
275
|
+
}, [markTabInteraction, switchTab]);
|
|
276
|
+
const handleRemoveTab = useCallback(tabId => {
|
|
277
|
+
markTabInteraction();
|
|
278
|
+
removeTab(tabId);
|
|
279
|
+
}, [markTabInteraction, removeTab]);
|
|
280
|
+
const handleCloseOtherTabs = useCallback(tabId => {
|
|
281
|
+
markTabInteraction();
|
|
282
|
+
closeOtherTabs(tabId);
|
|
283
|
+
}, [closeOtherTabs, markTabInteraction]);
|
|
284
|
+
const handleCloseRightTabs = useCallback(tabId => {
|
|
285
|
+
markTabInteraction();
|
|
286
|
+
closeRightTabs(tabId);
|
|
287
|
+
}, [closeRightTabs, markTabInteraction]);
|
|
288
|
+
const handleCloseAllTabs = useCallback(() => {
|
|
289
|
+
markTabInteraction();
|
|
290
|
+
closeAllTabs();
|
|
291
|
+
}, [closeAllTabs, markTabInteraction]);
|
|
252
292
|
const handleTabEdit = useCallback((targetKey, action) => {
|
|
253
293
|
if (action === 'remove') {
|
|
254
|
-
|
|
294
|
+
handleRemoveTab(targetKey);
|
|
255
295
|
}
|
|
256
|
-
}, [
|
|
296
|
+
}, [handleRemoveTab]);
|
|
257
297
|
|
|
258
298
|
// 构建 Tabs items
|
|
259
299
|
const tabsItems = useMemo(() => {
|
|
@@ -269,11 +309,11 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
269
309
|
label: /*#__PURE__*/_jsx(TabItemComponent, {
|
|
270
310
|
tab: tab,
|
|
271
311
|
active: tab.id === state.activeKey,
|
|
272
|
-
onClick: () =>
|
|
273
|
-
onClose: () =>
|
|
274
|
-
onCloseOthers: () =>
|
|
275
|
-
onCloseRight: () =>
|
|
276
|
-
onCloseAll:
|
|
312
|
+
onClick: () => handleTabChange(tab.id),
|
|
313
|
+
onClose: () => handleRemoveTab(tab.id),
|
|
314
|
+
onCloseOthers: () => handleCloseOtherTabs(tab.id),
|
|
315
|
+
onCloseRight: () => handleCloseRightTabs(tab.id),
|
|
316
|
+
onCloseAll: handleCloseAllTabs,
|
|
277
317
|
pinned: pinned,
|
|
278
318
|
pinDisabled: pinDisabled,
|
|
279
319
|
onTogglePin: () => handleTogglePin(tab.id),
|
|
@@ -286,7 +326,7 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
286
326
|
children: null
|
|
287
327
|
};
|
|
288
328
|
});
|
|
289
|
-
}, [state.tabsList, state.activeKey, config?.menuItems, config?.tabMenuClick, config?.fixed,
|
|
329
|
+
}, [state.tabsList, state.activeKey, config?.menuItems, config?.tabMenuClick, config?.fixed, handleTabChange, handleRemoveTab, handleCloseOtherTabs, handleCloseRightTabs, handleCloseAllTabs, handleTogglePin]);
|
|
290
330
|
const activeComponent = config?.activeComponent;
|
|
291
331
|
const emptyComponent = config?.empty;
|
|
292
332
|
|