@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.
Files changed (51) hide show
  1. package/dist/index.esm.css +1 -1
  2. package/dist/less.esm.css +1 -1
  3. package/es/ProAction/components/CheckModalContent/index.js +2 -2
  4. package/es/ProDownload/index.d.ts +2 -1
  5. package/es/ProDownload/index.js +31 -16
  6. package/es/ProDownload/utils.d.ts +10 -5
  7. package/es/ProDownload/utils.js +7 -5
  8. package/es/ProEditTable/utils/index.js +6 -1
  9. package/es/ProEnum/hooks/useEnumRequest.js +98 -77
  10. package/es/ProForm/components/combination/FormList/components/ActionButton.js +2 -2
  11. package/es/ProForm/components/combination/FormList/components/BlockFields.js +1 -0
  12. package/es/ProForm/components/combination/FormList/components/Empty.js +1 -1
  13. package/es/ProForm/components/combination/FormList/components/ToolbarButton.js +3 -3
  14. package/es/ProForm/components/combination/FormList/style/index.less +6 -0
  15. package/es/ProForm/components/combination/Group/component/FlexibleGroup.js +5 -11
  16. package/es/ProForm/components/combination/Group/hooks/index.js +1 -1
  17. package/es/ProForm/components/combination/Group/index.js +1 -1
  18. package/es/ProForm/components/combination/ProModalSelect/hooks/useRequestList.js +22 -8
  19. package/es/ProForm/components/combination/ProModalSelect/index.js +32 -12
  20. package/es/ProForm/components/combination/ProRangeLimit/index.js +1 -1
  21. package/es/ProForm/components/render/Render.js +2 -2
  22. package/es/ProForm/hooks/useWatch.js +7 -8
  23. package/es/ProForm/utils/index.js +2 -2
  24. package/es/ProLayout/components/Layout/Menu/OpenMenu/index.js +8 -8
  25. package/es/ProLayout/components/Layout/Menu/index.js +1 -1
  26. package/es/ProLayout/components/ProFooter/index.js +2 -1
  27. package/es/ProLayout/components/ProHeader/components/Describe/index.js +3 -1
  28. package/es/ProLayout/components/TabsManager/components/TabItem.js +6 -5
  29. package/es/ProLayout/components/TabsManager/hooks/useTabsState.js +104 -16
  30. package/es/ProLayout/components/TabsManager/hooks/useTabsUrlSync.d.ts +19 -0
  31. package/es/ProLayout/components/TabsManager/hooks/useTabsUrlSync.js +295 -0
  32. package/es/ProLayout/components/TabsManager/index.js +54 -14
  33. package/es/ProLayout/components/TabsManager/propTypes.d.ts +4 -0
  34. package/es/ProLayout/components/TabsManager/style/index.less +1 -1
  35. package/es/ProLayout/components/TabsManager/utils/historyObserver.d.ts +7 -0
  36. package/es/ProLayout/components/TabsManager/utils/historyObserver.js +62 -0
  37. package/es/ProLayout/components/TabsManager/utils/index.d.ts +15 -2
  38. package/es/ProLayout/components/TabsManager/utils/index.js +63 -9
  39. package/es/ProLayout/index.js +6 -3
  40. package/es/ProLayout/propTypes.d.ts +28 -1
  41. package/es/ProSelect/index.js +10 -1
  42. package/es/ProSelect/style/index.less +13 -0
  43. package/es/ProStep/style/index.less +1 -1
  44. package/es/ProTable/hooks/useAntdTable.js +11 -10
  45. package/es/ProTabs/index.js +3 -3
  46. package/es/ProTooltip/index.js +2 -5
  47. package/es/ProTree/components/ProTreeSelect/index.js +2 -2
  48. package/es/ProTreeModal/index.js +0 -3
  49. package/es/ProUpload/components/DragRender.js +0 -14
  50. package/package.json +2 -1
  51. 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
- removeTab(targetKey);
294
+ handleRemoveTab(targetKey);
255
295
  }
256
- }, [removeTab]);
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: () => switchTab(tab.id),
273
- onClose: () => removeTab(tab.id),
274
- onCloseOthers: () => closeOtherTabs(tab.id),
275
- onCloseRight: () => closeRightTabs(tab.id),
276
- onCloseAll: closeAllTabs,
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, switchTab, removeTab, closeOtherTabs, closeRightTabs, closeAllTabs, handleTogglePin]);
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
  /** 关闭右侧标签页 */
@@ -171,7 +171,7 @@
171
171
  padding: 4px 0 5px !important;
172
172
  margin-right: 8px !important;
173
173
  margin-left: 0;
174
- background: #F5F5F5;
174
+ background: #f5f5f5;
175
175
  transition: none;
176
176
  border-color: #e7e8ee;
177
177
  &.@{ant-prefix}-tabs-tab-active {
@@ -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
- export declare const findExistingTab: (tabsList: TabItem[], menuItem: MenusType) => TabItem | undefined;
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 当前标签页列表