@zat-design/sisyphus-react 4.6.0-beta.2 → 4.6.0-beta.3

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.
@@ -15,6 +15,7 @@ import {
15
15
  getMenuRoute,
16
16
  hasRelationKey,
17
17
  findExistingTab,
18
+ getComparableTabUrl,
18
19
  normalizeTabUrl,
19
20
  isSafeExternalUrl,
20
21
  isSafeHttpUrl,
@@ -30,6 +31,7 @@ const useTabsState = (options) => {
30
31
  const finalConfig = { ...DEFAULT_TABS_CONFIG, ...config };
31
32
  const urlSyncEnabled = finalConfig.urlSync === true || typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null;
32
33
  const urlSyncIdentity = typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null ? finalConfig.urlSync.identity || "url" : "url";
34
+ const urlSyncBasename = typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null ? finalConfig.urlSync.basename : void 0;
33
35
  const [state, setState] = useState(() => ({
34
36
  tabsList: [],
35
37
  activeKey: "",
@@ -71,8 +73,11 @@ const useTabsState = (options) => {
71
73
  const normalizedUrl = normalizeTabUrl(tab.url);
72
74
  if (!normalizedUrl)
73
75
  return;
76
+ const comparableUrl = getComparableTabUrl(normalizedUrl, urlSyncBasename);
77
+ if (!comparableUrl)
78
+ return;
74
79
  const relationKey = tab.relationKey ?? ((_a2 = tab.menuItem) == null ? void 0 : _a2.relationKey);
75
- const identity = urlSyncIdentity === "relationKey" && hasRelationKey(relationKey) ? `relationKey:${typeof relationKey}:${String(relationKey)}` : `url:${normalizedUrl}`;
80
+ const identity = urlSyncIdentity === "relationKey" && hasRelationKey(relationKey) ? `relationKey:${typeof relationKey}:${String(relationKey)}` : `url:${comparableUrl}`;
76
81
  const retainedTab = retainedTabByIdentity.get(identity);
77
82
  if (retainedTab) {
78
83
  retainedTabByIdentity.set(identity, { ...retainedTab, url: normalizedUrl });
@@ -93,7 +98,7 @@ const useTabsState = (options) => {
93
98
  setState(restoredState);
94
99
  }
95
100
  setIsInitialized(true);
96
- }, [restoreFromCache, urlSyncEnabled, urlSyncIdentity]);
101
+ }, [restoreFromCache, urlSyncEnabled, urlSyncIdentity, urlSyncBasename]);
97
102
  const lastActiveKeyRef = useRef("");
98
103
  const shouldSkipSaveRef = useRef(false);
99
104
  const initialTabsAppliedRef = useRef(false);
@@ -132,7 +137,8 @@ const useTabsState = (options) => {
132
137
  if (urlSyncEnabled || !forceNew) {
133
138
  const existingTab = findExistingTab(state.tabsList, menuItem, {
134
139
  urlSync: urlSyncEnabled,
135
- identity: urlSyncIdentity
140
+ identity: urlSyncIdentity,
141
+ basename: urlSyncBasename
136
142
  });
137
143
  if (existingTab) {
138
144
  return true;
@@ -143,7 +149,7 @@ const useTabsState = (options) => {
143
149
  }
144
150
  return true;
145
151
  },
146
- [state.tabsList, finalConfig, urlSyncEnabled, urlSyncIdentity]
152
+ [state.tabsList, finalConfig, urlSyncEnabled, urlSyncIdentity, urlSyncBasename]
147
153
  );
148
154
  const addTab = useCallback(
149
155
  (menuItem, options2) => {
@@ -170,11 +176,12 @@ const useTabsState = (options) => {
170
176
  const resolvedMenuItem = normalizedUrl ? { ...menuItem, url: normalizedUrl, redirectUrl: void 0 } : menuItem;
171
177
  const existingTab = urlSyncEnabled || !forceNew ? findExistingTab(prevState.tabsList, resolvedMenuItem, {
172
178
  urlSync: urlSyncEnabled,
173
- identity: urlSyncIdentity
179
+ identity: urlSyncIdentity,
180
+ basename: urlSyncBasename
174
181
  }) : void 0;
175
182
  if (existingTab) {
176
183
  const shouldFixClosable = resolvedMenuItem.closable === false && existingTab.closable !== false;
177
- const shouldUpdateUrl = urlSyncIdentity === "relationKey" && normalizedUrl !== void 0 && existingTab.url !== normalizedUrl;
184
+ const shouldUpdateUrl = urlSyncIdentity === "relationKey" && normalizedUrl !== void 0 && getComparableTabUrl(existingTab.url, urlSyncBasename) !== getComparableTabUrl(normalizedUrl, urlSyncBasename);
178
185
  const correctedTab = shouldFixClosable || shouldUpdateUrl ? {
179
186
  ...existingTab,
180
187
  ...shouldFixClosable ? { closable: false } : {},
@@ -216,7 +223,7 @@ const useTabsState = (options) => {
216
223
  };
217
224
  });
218
225
  },
219
- [finalConfig, urlSyncEnabled, urlSyncIdentity]
226
+ [finalConfig, urlSyncEnabled, urlSyncIdentity, urlSyncBasename]
220
227
  );
221
228
  useEffect(() => {
222
229
  const initialTabs = finalConfig.initialTabs;
@@ -3,6 +3,8 @@ import * as ReactRouterDom from "react-router-dom";
3
3
  import {
4
4
  canOpenAsTab,
5
5
  flattenMenuData,
6
+ getBrowserTabUrl,
7
+ getComparableTabUrl,
6
8
  getCurrentTabUrl,
7
9
  getMenuRoute,
8
10
  getTabUrlPathname,
@@ -92,6 +94,7 @@ const useTabsUrlSync = ({
92
94
  const navigator = navigationContext == null ? void 0 : navigationContext.navigator;
93
95
  const enabled = config.urlSync === true || typeof config.urlSync === "object" && config.urlSync !== null;
94
96
  const historyMode = typeof config.urlSync === "object" ? config.urlSync.history || "replace" : "replace";
97
+ const basename = typeof config.urlSync === "object" ? config.urlSync.basename : void 0;
95
98
  const bootstrappedRef = useRef(false);
96
99
  const writingUrlRef = useRef(false);
97
100
  const preserveUrlInteractionRef = useRef(null);
@@ -156,11 +159,14 @@ const useTabsUrlSync = ({
156
159
  if (!enabled || !isInitialized || typeof window === "undefined")
157
160
  return;
158
161
  const currentUrl = normalizeTabUrl(locationChange.url);
159
- if (!currentUrl)
162
+ const comparableCurrentUrl = getComparableTabUrl(currentUrl, basename);
163
+ if (!currentUrl || !comparableCurrentUrl)
160
164
  return;
161
165
  if (handledLocationSequenceRef.current === locationChange.sequence)
162
166
  return;
163
- const exactTab = state.tabsList.find((tab) => normalizeTabUrl(tab.url) === currentUrl);
167
+ const exactTab = state.tabsList.find(
168
+ (tab) => getComparableTabUrl(tab.url, basename) === comparableCurrentUrl
169
+ );
164
170
  if (exactTab) {
165
171
  handledLocationSequenceRef.current = locationChange.sequence;
166
172
  bootstrappedRef.current = true;
@@ -170,16 +176,16 @@ const useTabsUrlSync = ({
170
176
  }
171
177
  return;
172
178
  }
173
- const currentPathname = getTabUrlPathname(currentUrl);
179
+ const currentPathname = getTabUrlPathname(comparableCurrentUrl);
174
180
  const sourceMenus = getSourceMenus(dataSource);
175
181
  const activeTab = state.tabsList.find((tab) => tab.id === state.activeKey);
176
182
  const activeCode = ((_a = activeTab == null ? void 0 : activeTab.menuItem) == null ? void 0 : _a.code) || (activeTab == null ? void 0 : activeTab.code);
177
183
  const activeSourceMenu = activeCode ? sourceMenus.find((item) => item.code === activeCode) : void 0;
178
184
  const activeMenuItem = activeSourceMenu || (activeTab == null ? void 0 : activeTab.menuItem);
179
- const activeBasePathname = activeMenuItem ? getTabUrlPathname(getMenuRoute(activeMenuItem)) : getTabUrlPathname(activeTab == null ? void 0 : activeTab.url);
185
+ const activeBasePathname = activeMenuItem ? getTabUrlPathname(getComparableTabUrl(getMenuRoute(activeMenuItem), basename)) : getTabUrlPathname(getComparableTabUrl(activeTab == null ? void 0 : activeTab.url, basename));
180
186
  const targetMenu = (_b = sourceMenus.map((item) => ({
181
187
  item,
182
- pathname: getTabUrlPathname(getMenuRoute(item))
188
+ pathname: getTabUrlPathname(getComparableTabUrl(getMenuRoute(item), basename))
183
189
  })).filter(
184
190
  (entry) => !!entry.pathname && isSameOrNestedPath(entry.pathname, currentPathname)
185
191
  ).sort((left, right) => right.pathname.length - left.pathname.length)[0]) == null ? void 0 : _b.item;
@@ -223,7 +229,8 @@ const useTabsUrlSync = ({
223
229
  addTab,
224
230
  switchTab,
225
231
  updateTabUrl,
226
- interactionSequence
232
+ interactionSequence,
233
+ basename
227
234
  ]);
228
235
  useEffect(() => {
229
236
  if (!enabled || !isInitialized || !bootstrappedRef.current || typeof window === "undefined") {
@@ -231,13 +238,14 @@ const useTabsUrlSync = ({
231
238
  }
232
239
  const activeTab = state.tabsList.find((tab) => tab.id === state.activeKey);
233
240
  const targetUrl = normalizeTabUrl(activeTab == null ? void 0 : activeTab.url);
241
+ const browserTargetUrl = getBrowserTabUrl(targetUrl, basename);
234
242
  const commitActiveTab = () => {
235
243
  committedActiveKeyRef.current = state.activeKey;
236
244
  committedTabIdsRef.current = new Set(state.tabsList.map((tab) => tab.id));
237
245
  };
238
246
  if (preserveUrlInteractionRef.current !== null) {
239
247
  if (preserveUrlInteractionRef.current === interactionSequence) {
240
- if (targetUrl === getCurrentTabUrl()) {
248
+ if (browserTargetUrl === getCurrentTabUrl()) {
241
249
  preserveUrlInteractionRef.current = null;
242
250
  }
243
251
  commitActiveTab();
@@ -245,7 +253,7 @@ const useTabsUrlSync = ({
245
253
  }
246
254
  preserveUrlInteractionRef.current = null;
247
255
  }
248
- if (!targetUrl || targetUrl === getCurrentTabUrl()) {
256
+ if (!targetUrl || !browserTargetUrl || browserTargetUrl === getCurrentTabUrl()) {
249
257
  const pendingNavigation = pendingDataNavigationRef.current;
250
258
  navigationAttemptRef.current += 1;
251
259
  pendingDataNavigationRef.current = false;
@@ -286,30 +294,34 @@ const useTabsUrlSync = ({
286
294
  pendingDataNavigationRef.current = false;
287
295
  pendingDataNavigationUrlRef.current = null;
288
296
  const currentUrl = getCurrentTabUrl();
289
- if (targetUrl === currentUrl || committedDataNavigationUrl === currentUrl) {
297
+ if (browserTargetUrl === currentUrl || committedDataNavigationUrl === currentUrl) {
290
298
  commitActiveTab();
291
299
  return;
292
300
  }
293
301
  rollbackTabActivation(previousActiveKey, attemptedActiveKey, removeAttemptedTab);
294
302
  };
295
303
  pendingDataNavigationRef.current = true;
296
- pendingDataNavigationUrlRef.current = targetUrl;
297
- void dataRouterContext.router.navigate(getDataRouterUrl(targetUrl, dataRouterContext.basename), {
304
+ pendingDataNavigationUrlRef.current = browserTargetUrl;
305
+ void dataRouterContext.router.navigate(getDataRouterUrl(browserTargetUrl, dataRouterContext.basename), {
298
306
  replace: historyMode === "replace",
299
307
  state: routerState
300
308
  }).then(settleNavigation);
301
309
  } else if (navigator) {
302
310
  if (historyMode === "push") {
303
- navigator.push(targetUrl, routerState);
311
+ navigator.push(browserTargetUrl, routerState);
304
312
  } else {
305
- navigator.replace(targetUrl, routerState);
313
+ navigator.replace(browserTargetUrl, routerState);
306
314
  }
307
315
  commitActiveTab();
308
316
  } else if (historyMode === "push") {
309
- window.history.pushState(createPushHistoryState(window.history.state), "", targetUrl);
317
+ window.history.pushState(
318
+ createPushHistoryState(window.history.state),
319
+ "",
320
+ browserTargetUrl
321
+ );
310
322
  commitActiveTab();
311
323
  } else {
312
- window.history.replaceState(window.history.state, "", targetUrl);
324
+ window.history.replaceState(window.history.state, "", browserTargetUrl);
313
325
  commitActiveTab();
314
326
  }
315
327
  } finally {
@@ -324,7 +336,8 @@ const useTabsUrlSync = ({
324
336
  interactionSequence,
325
337
  navigator,
326
338
  dataRouterContext,
327
- rollbackTabActivation
339
+ rollbackTabActivation,
340
+ basename
328
341
  ]);
329
342
  };
330
343
  export {
@@ -14,10 +14,15 @@ export declare const normalizeTabUrl: (value?: string) => string | undefined;
14
14
  export declare const isSafeExternalUrl: (value?: string) => boolean;
15
15
  export declare const isSafeHttpUrl: (value?: string) => boolean;
16
16
  export declare const getCurrentTabUrl: () => string;
17
+ /** 将浏览器地址和 Tab 内部地址转为同一身份,仅按完整路径段移除 basename。 */
18
+ export declare const getComparableTabUrl: (value?: string, basename?: string) => string | undefined;
19
+ /** 将 Tab 内部地址转为浏览器地址,已带 basename 时不重复添加。 */
20
+ export declare const getBrowserTabUrl: (value?: string, basename?: string) => string | undefined;
17
21
  export declare const getTabUrlPathname: (value?: string) => string | undefined;
18
22
  export interface FindExistingTabOptions {
19
23
  urlSync?: boolean;
20
24
  identity?: TabsUrlSyncConfig['identity'];
25
+ basename?: TabsUrlSyncConfig['basename'];
21
26
  }
22
27
  export declare const findExistingTab: (tabsList: TabItem[], menuItem: MenusType, options?: FindExistingTabOptions) => TabItem | undefined;
23
28
  /**
@@ -29,6 +29,49 @@ const isSafeExternalUrl = (value) => {
29
29
  };
30
30
  const isSafeHttpUrl = (value) => !!resolveHttpUrl(value);
31
31
  const getCurrentTabUrl = () => `${window.location.pathname}${window.location.search}${window.location.hash}`;
32
+ const normalizeTabsBasename = (basename) => {
33
+ const normalized = String(basename ?? "").trim();
34
+ if (!normalized || normalized === "/")
35
+ return "";
36
+ return `/${normalized.replace(/^\/+|\/+$/g, "")}`;
37
+ };
38
+ const splitNormalizedTabUrl = (url) => {
39
+ const pathnameEnd = [url.indexOf("?"), url.indexOf("#")].filter((index) => index >= 0).reduce((current, index) => Math.min(current, index), url.length);
40
+ return {
41
+ pathname: url.slice(0, pathnameEnd),
42
+ suffix: url.slice(pathnameEnd)
43
+ };
44
+ };
45
+ const hasPathBasename = (pathname, basename) => {
46
+ const lowerPathname = pathname.toLowerCase();
47
+ const lowerBasename = basename.toLowerCase();
48
+ return lowerPathname === lowerBasename || lowerPathname.startsWith(`${lowerBasename}/`);
49
+ };
50
+ const getComparableTabUrl = (value, basename) => {
51
+ const normalizedUrl = normalizeTabUrl(value);
52
+ if (!normalizedUrl)
53
+ return void 0;
54
+ const normalizedBasename = normalizeTabsBasename(basename);
55
+ if (!normalizedBasename)
56
+ return normalizedUrl;
57
+ const { pathname, suffix } = splitNormalizedTabUrl(normalizedUrl);
58
+ if (!hasPathBasename(pathname, normalizedBasename))
59
+ return normalizedUrl;
60
+ const barePathname = pathname.slice(normalizedBasename.length) || "/";
61
+ return `${barePathname}${suffix}`;
62
+ };
63
+ const getBrowserTabUrl = (value, basename) => {
64
+ const normalizedUrl = normalizeTabUrl(value);
65
+ if (!normalizedUrl)
66
+ return void 0;
67
+ const normalizedBasename = normalizeTabsBasename(basename);
68
+ if (!normalizedBasename)
69
+ return normalizedUrl;
70
+ const { pathname, suffix } = splitNormalizedTabUrl(normalizedUrl);
71
+ if (hasPathBasename(pathname, normalizedBasename))
72
+ return normalizedUrl;
73
+ return `${normalizedBasename}${pathname}${suffix}`;
74
+ };
32
75
  const getTabUrlPathname = (value) => {
33
76
  const normalized = normalizeTabUrl(value);
34
77
  if (!normalized || typeof window === "undefined")
@@ -45,10 +88,10 @@ const findExistingTab = (tabsList, menuItem, options = {}) => {
45
88
  }
46
89
  );
47
90
  }
48
- const targetUrl = normalizeTabUrl(getMenuRoute(menuItem));
91
+ const targetUrl = getComparableTabUrl(getMenuRoute(menuItem), options.basename);
49
92
  if (!targetUrl)
50
93
  return void 0;
51
- return tabsList.find((tab) => normalizeTabUrl(tab.url) === targetUrl);
94
+ return tabsList.find((tab) => getComparableTabUrl(tab.url, options.basename) === targetUrl);
52
95
  }
53
96
  if (hasRelationKey(menuItem.relationKey)) {
54
97
  return tabsList.find(
@@ -232,6 +275,8 @@ export {
232
275
  findExistingTab,
233
276
  flattenMenuData,
234
277
  generateTabId,
278
+ getBrowserTabUrl,
279
+ getComparableTabUrl,
235
280
  getCurrentTabUrl,
236
281
  getMenuRoute,
237
282
  getPinnedBoundary,
@@ -373,6 +373,11 @@ export interface TabsUrlSyncConfig {
373
373
  * @default 'url'
374
374
  */
375
375
  identity?: 'url' | 'relationKey';
376
+ /**
377
+ * @description 浏览器地址中的显式基础路径;Tab 内部 URL 可以不带该前缀
378
+ * @example '/wjs'
379
+ */
380
+ basename?: string;
376
381
  }
377
382
  /**
378
383
  * @description 添加标签页的业务参数
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zat-design/sisyphus-react",
3
- "version": "4.6.0-beta.2",
3
+ "version": "4.6.0-beta.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",