@zat-design/sisyphus-react 4.5.9 → 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/ProDownload/index.d.ts +2 -1
- package/es/ProDownload/index.js +31 -16
- package/es/ProDownload/utils.d.ts +10 -5
- package/es/ProDownload/utils.js +7 -5
- package/es/ProEditTable/utils/index.js +6 -1
- package/es/ProEnum/hooks/useEnumRequest.js +98 -77
- 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/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 +0 -3
- package/es/ProUpload/components/DragRender.js +0 -14
- package/package.json +2 -1
- package/es/ProConfigProvider/demo/locale.js +0 -58
|
@@ -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
|
|
|
@@ -71,8 +71,12 @@ export interface UseTabsStateReturn {
|
|
|
71
71
|
removeTab: (tabId: string) => void;
|
|
72
72
|
/** 更新指定 id 的标签页字段(替换语义;找不到时 no-op) */
|
|
73
73
|
updateTab: (tabId: string, updates: UpdateTabParams) => void;
|
|
74
|
+
/** 更新页签当前 URL,保持 tab.id 不变 */
|
|
75
|
+
updateTabUrl: (tabId: string, url: string) => void;
|
|
74
76
|
/** 切换标签页 */
|
|
75
77
|
switchTab: (tabId: string) => void;
|
|
78
|
+
/** URL 导航未提交时回滚本次激活 */
|
|
79
|
+
rollbackTabActivation: (previousTabId: string, attemptedTabId: string, removeAttemptedTab: boolean) => void;
|
|
76
80
|
/** 关闭其他标签页 */
|
|
77
81
|
closeOtherTabs: (currentTabId: string) => void;
|
|
78
82
|
/** 关闭右侧标签页 */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type HistoryLocationChangeSource = 'push' | 'replace' | 'pop' | 'hash';
|
|
2
|
+
type HistoryLocationListener = (source: HistoryLocationChangeSource) => void;
|
|
3
|
+
/**
|
|
4
|
+
* 共享对原生 History 的唯一包装,避免多个 ProLayout 实例互相覆盖和泄漏。
|
|
5
|
+
*/
|
|
6
|
+
export declare const subscribeHistoryLocation: (listener: HistoryLocationListener) => (() => void);
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const listeners = new Set();
|
|
2
|
+
let originalPushState;
|
|
3
|
+
let originalReplaceState;
|
|
4
|
+
let wrappedPushState;
|
|
5
|
+
let wrappedReplaceState;
|
|
6
|
+
let installationSequence = 0;
|
|
7
|
+
let activeInstallation = 0;
|
|
8
|
+
const notify = source => {
|
|
9
|
+
listeners.forEach(listener => listener(source));
|
|
10
|
+
};
|
|
11
|
+
const handlePopState = () => notify('pop');
|
|
12
|
+
const handleHashChange = () => notify('hash');
|
|
13
|
+
const install = () => {
|
|
14
|
+
const browserHistory = window.history;
|
|
15
|
+
const installation = installationSequence + 1;
|
|
16
|
+
installationSequence = installation;
|
|
17
|
+
activeInstallation = installation;
|
|
18
|
+
originalPushState = browserHistory.pushState;
|
|
19
|
+
originalReplaceState = browserHistory.replaceState;
|
|
20
|
+
const installedPushState = originalPushState;
|
|
21
|
+
const installedReplaceState = originalReplaceState;
|
|
22
|
+
wrappedPushState = function pushState(...args) {
|
|
23
|
+
installedPushState.apply(browserHistory, args);
|
|
24
|
+
if (activeInstallation === installation) notify('push');
|
|
25
|
+
};
|
|
26
|
+
wrappedReplaceState = function replaceState(...args) {
|
|
27
|
+
installedReplaceState.apply(browserHistory, args);
|
|
28
|
+
if (activeInstallation === installation) notify('replace');
|
|
29
|
+
};
|
|
30
|
+
browserHistory.pushState = wrappedPushState;
|
|
31
|
+
browserHistory.replaceState = wrappedReplaceState;
|
|
32
|
+
window.addEventListener('popstate', handlePopState);
|
|
33
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
34
|
+
};
|
|
35
|
+
const uninstall = () => {
|
|
36
|
+
const browserHistory = window.history;
|
|
37
|
+
activeInstallation = 0;
|
|
38
|
+
if (wrappedPushState && originalPushState && browserHistory.pushState === wrappedPushState) {
|
|
39
|
+
browserHistory.pushState = originalPushState;
|
|
40
|
+
}
|
|
41
|
+
if (wrappedReplaceState && originalReplaceState && browserHistory.replaceState === wrappedReplaceState) {
|
|
42
|
+
browserHistory.replaceState = originalReplaceState;
|
|
43
|
+
}
|
|
44
|
+
window.removeEventListener('popstate', handlePopState);
|
|
45
|
+
window.removeEventListener('hashchange', handleHashChange);
|
|
46
|
+
originalPushState = undefined;
|
|
47
|
+
originalReplaceState = undefined;
|
|
48
|
+
wrappedPushState = undefined;
|
|
49
|
+
wrappedReplaceState = undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 共享对原生 History 的唯一包装,避免多个 ProLayout 实例互相覆盖和泄漏。
|
|
54
|
+
*/
|
|
55
|
+
export const subscribeHistoryLocation = listener => {
|
|
56
|
+
if (listeners.size === 0) install();
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
return () => {
|
|
59
|
+
listeners.delete(listener);
|
|
60
|
+
if (listeners.size === 0) uninstall();
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -5,7 +5,20 @@ import { AddTabParams, TabItem, MenusType } from '../../../propTypes';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const getMenuRoute: (menuItem: MenusType) => string;
|
|
7
7
|
export declare const hasRelationKey: (relationKey: MenusType['relationKey']) => relationKey is string | number;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* 将同源 URL 规范化为应用内地址,保留 search 和 hash。
|
|
10
|
+
* 返回 undefined 表示 URL 无效或跨域。
|
|
11
|
+
*/
|
|
12
|
+
export declare const normalizeTabUrl: (value?: string) => string | undefined;
|
|
13
|
+
/** 仅允许将跨域 HTTP(S) URL 交给浏览器外部打开。 */
|
|
14
|
+
export declare const isSafeExternalUrl: (value?: string) => boolean;
|
|
15
|
+
export declare const isSafeHttpUrl: (value?: string) => boolean;
|
|
16
|
+
export declare const getCurrentTabUrl: () => string;
|
|
17
|
+
export declare const getTabUrlPathname: (value?: string) => string | undefined;
|
|
18
|
+
export interface FindExistingTabOptions {
|
|
19
|
+
urlSync?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const findExistingTab: (tabsList: TabItem[], menuItem: MenusType, options?: FindExistingTabOptions) => TabItem | undefined;
|
|
9
22
|
/**
|
|
10
23
|
* 根据菜单项生成TabItem
|
|
11
24
|
*/
|
|
@@ -21,7 +34,7 @@ export declare const shouldOpenExternal: (menuItem: MenusType, target?: string)
|
|
|
21
34
|
/**
|
|
22
35
|
* 处理外部跳转
|
|
23
36
|
*/
|
|
24
|
-
export declare const handleExternalOpen: (menuItem: MenusType) => void;
|
|
37
|
+
export declare const handleExternalOpen: (menuItem: MenusType, secure?: boolean) => void;
|
|
25
38
|
/**
|
|
26
39
|
* 检查是否超出最大标签页限制
|
|
27
40
|
* @param currentTabs 当前标签页列表
|