@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
@@ -168,8 +168,9 @@ const ProModalSelect = (props, ref) => {
168
168
  transformParams,
169
169
  transformResponse
170
170
  }, {
171
- ready: (initParams || defaultOne) && nextMode === 'input' || open,
172
- ...useRequest?.options
171
+ ...useRequest?.options,
172
+ ready: useRequest?.options?.ready ?? ((initParams || defaultOne) && nextMode === 'input' ? true : undefined),
173
+ manual: useRequest?.options?.manual ?? ((initParams || defaultOne) && nextMode === 'input' ? undefined : true)
173
174
  });
174
175
  const defaultOptionRender = rowData => rowData?.[labelKey];
175
176
  let handleFormat = onFormat || defaultOptionRender;
@@ -190,6 +191,16 @@ const ProModalSelect = (props, ref) => {
190
191
  if (typeof data === 'string' || typeof data === 'number') {
191
192
  return data;
192
193
  }
194
+ // lodash isObject 对数组也为 true,必须先判数组,否则多选展示永远走对象臂
195
+ if (_isArray(data)) {
196
+ return data.map(item => {
197
+ return handleFormat({
198
+ ...item,
199
+ value: item[valueKey],
200
+ label: item[labelKey]
201
+ });
202
+ }).join('、');
203
+ }
193
204
  if (_isObject(data)) {
194
205
  return handleFormat({
195
206
  ...data,
@@ -197,13 +208,7 @@ const ProModalSelect = (props, ref) => {
197
208
  label: data[labelKey]
198
209
  });
199
210
  }
200
- return _isArray(data) ? data?.map(item => {
201
- return handleFormat({
202
- ...item,
203
- value: item[valueKey],
204
- label: item[labelKey]
205
- });
206
- })?.join('、') : '';
211
+ return '';
207
212
  };
208
213
  const viewText = useMemo(() => {
209
214
  let nextValue = labelInValue ? value : _value;
@@ -334,6 +339,18 @@ const ProModalSelect = (props, ref) => {
334
339
  d: Date.now()
335
340
  });
336
341
  };
342
+ const handleReset = () => {
343
+ form.resetFields();
344
+ formColumns?.forEach(column => {
345
+ if (column?.name !== undefined && Object.prototype.hasOwnProperty.call(column, 'initialValue')) {
346
+ form.setFieldValue(column.name, column.initialValue);
347
+ }
348
+ });
349
+ onSearch({
350
+ ...form.getFieldsValue(),
351
+ d: Date.now()
352
+ });
353
+ };
337
354
  const _rowSelection = {
338
355
  type,
339
356
  // 多选时开启,保证翻页/筛选后不丢弃不在当前页的已选 key
@@ -501,9 +518,6 @@ const ProModalSelect = (props, ref) => {
501
518
  });
502
519
  useEffect(() => {
503
520
  if (onOff && !open) {
504
- if (useRequest?.options?.manual !== true) {
505
- onReset();
506
- }
507
521
  const nextState = {
508
522
  open: true
509
523
  };
@@ -531,6 +545,11 @@ const ProModalSelect = (props, ref) => {
531
545
  setState(nextState);
532
546
  }
533
547
  }, [onOff, open, JSON.stringify(value)]);
548
+ useEffect(() => {
549
+ if (open && useRequest?.options?.ready !== false && useRequest?.options?.manual === undefined) {
550
+ onSearch(form.getFieldsValue(), true);
551
+ }
552
+ }, [open, useRequest?.options?.manual, useRequest?.options?.ready]);
534
553
  useEffect(() => {
535
554
  hideTooltipIfOpen(fieldRef.current, open);
536
555
  }, [open]);
@@ -741,6 +760,7 @@ const ProModalSelect = (props, ref) => {
741
760
  form: form,
742
761
  columns: formColumns,
743
762
  onFinish: handleSearch,
763
+ onCancel: handleReset,
744
764
  confirmLoading: loading,
745
765
  footer: formColumns?.length > 1
746
766
  }), /*#__PURE__*/_jsx(ProTable, {
@@ -18,7 +18,7 @@ const useControlled = props => {
18
18
  const internalValue = [props.value?.[0] && dayjs(props.value[0], props.format), props.value?.[1] && dayjs(props.value[1], props.format)];
19
19
  const handleChange = (value, other) => {
20
20
  if (!value) {
21
- props?.onChange(value, other);
21
+ props?.onChange?.(value, other);
22
22
  } else {
23
23
  const [start, end] = value;
24
24
  if (props?.onChange) {
@@ -173,8 +173,8 @@ const Render = props => {
173
173
 
174
174
  // 防抖记忆化:handlerRef 始终指向最新 handler,debounce 仅按 debounceWait 重建,避免每次渲染重置定时器导致防抖失效
175
175
  const debounceWait = lastComponentProps?.debounceWait;
176
- const handlerRef = useRef(() => undefined);
177
- const debouncedHandleChange = useMemo(() => _debounce((...args) => handlerRef.current(...args), debounceWait), [debounceWait]);
176
+ const handlerRef = useRef(undefined);
177
+ const debouncedHandleChange = useMemo(() => _debounce((...args) => handlerRef.current?.(...args), debounceWait), [debounceWait]);
178
178
  useEffect(() => () => debouncedHandleChange.cancel(), [debouncedHandleChange]);
179
179
  const itemClassName = classNames({
180
180
  [className]: className,
@@ -49,14 +49,15 @@ function setValueByPath(obj, path, value) {
49
49
 
50
50
  // 创建之后namepath就不可变的警告
51
51
  // selector 模式(dependencies 为函数)下跳过:函数引用每次渲染不同,无法稳定 stringify
52
- const useWatchWarning = process.env.NODE_ENV !== 'production' ? dependencies => {
52
+ // warning production 下本身为空操作;始终走同一实现,避免 Jest production 空函数不可达
53
+ const useWatchWarning = dependencies => {
53
54
  const isSelector = typeof dependencies === 'function';
54
55
  const depsStr = isSelector ? '__selector__' : JSON.stringify(dependencies);
55
56
  const depsStrRef = useRef(depsStr);
56
57
  if (!isSelector) {
57
58
  warning(depsStrRef.current === depsStr, '`useWatch` is not support dynamic `namePath`. Please provide static instead.');
58
59
  }
59
- } : () => {};
60
+ };
60
61
 
61
62
  /**
62
63
  * selector 函数:接收所有表单值,返回派生值
@@ -151,8 +152,8 @@ function useWatch(dependencies, form, wait) {
151
152
  if (!formInstance) return undefined;
152
153
 
153
154
  // 创建更新状态的函数
154
- const updateState = () => {
155
- const values = formInstance.getFieldsValue(true);
155
+ const updateState = watchValues => {
156
+ const values = watchValues ?? formInstance.getFieldsValue(true);
156
157
 
157
158
  // selector 模式:直接调用 selector 计算派生值
158
159
  if (isSelector && selectorRef.current) {
@@ -190,10 +191,8 @@ function useWatch(dependencies, form, wait) {
190
191
  const {
191
192
  registerWatch
192
193
  } = getInternalHooks('RC_FORM_INTERNAL_HOOKS');
193
- const cancelWatch = registerWatch(values => {
194
- if (values) {
195
- updateState();
196
- }
194
+ const cancelWatch = registerWatch((values, allValues) => {
195
+ updateState(allValues ?? values);
197
196
  });
198
197
 
199
198
  // 清理订阅
@@ -129,12 +129,12 @@ export const getAllNamePath = (object, currentPath = []) => {
129
129
 
130
130
  // 拆开组合key (组合key只可能存在于最外层)
131
131
  if (key.includes('-')) {
132
- resultKeys.concat(key.split('-').map(keyStr => keyStr.split('_')));
132
+ resultKeys.push(...key.split('-').map(keyStr => keyStr.split('_')));
133
133
  }
134
134
 
135
135
  // 深度获取对象key
136
136
  if (_isObject(value)) {
137
- resultKeys.concat(getAllNamePath(value, [...currentPath, key]));
137
+ resultKeys.push(...getAllNamePath(value, [...currentPath, key]));
138
138
  }
139
139
  resultKeys.push([...currentPath, key]);
140
140
  });
@@ -49,6 +49,7 @@ const OpenMenu = props => {
49
49
  // 特殊处理:如果layoutSelectedPath是'##EMPTY##',表示有意清空选中状态
50
50
  const preferredPath = query.get('activeMenu') || (layoutSelectedPath === '##EMPTY##' ? null : layoutSelectedPath) || window.location.pathname;
51
51
  const selectKeyIdPath = preferredPath ? getIdsByPathName(menus, preferredPath)?.map(item => String(item)) || [] : [];
52
+ const selectedRootKey = selectKeyIdPath[0];
52
53
  const itemsTransform = useMemo(() => {
53
54
  const result = _cloneDeep(menus);
54
55
  if (!Array.isArray(result) || !result.length) {
@@ -66,7 +67,7 @@ const OpenMenu = props => {
66
67
  children: [/*#__PURE__*/_jsx(_Fragment, {
67
68
  children: item.imgUrl && isFirstMenu ? /*#__PURE__*/_jsx("img", {
68
69
  className: "pro-layout-icon",
69
- src: selectKeyIdPath[0] === String(item.id) ? item.imgActiveUrl || item.imgUrl : item.imgUrl,
70
+ src: selectedRootKey === String(item.id) ? item.imgActiveUrl || item.imgUrl : item.imgUrl,
70
71
  alt: "icon"
71
72
  }) : item.icon && isFirstMenu ? /*#__PURE__*/_jsx(ProIcon, {
72
73
  type: item.icon || '',
@@ -102,7 +103,7 @@ const OpenMenu = props => {
102
103
  };
103
104
  menuDeep(result);
104
105
  return result;
105
- }, [dataSource]);
106
+ }, [dataSource, selectedRootKey]);
106
107
  useDeepCompareEffect(() => {
107
108
  if (Array.isArray(selectKeyIdPath)) {
108
109
  const newState = {
@@ -140,14 +141,13 @@ const OpenMenu = props => {
140
141
  });
141
142
  },
142
143
  onClick: ({
143
- item,
144
144
  keyPath,
145
- key,
146
- domEvent
145
+ key
147
146
  }) => {
148
147
  // 查找完整的菜单项数据
149
148
  const menuItem = findMenuItemByKey(menus, key);
150
149
  const menuKeyPath = menuItem?.keyIdPath ? menuItem.keyIdPath.map(id => String(id)) : keyPath;
150
+ const menuRouter = menuItem?.redirectUrl || menuItem?.router || menuItem?.url;
151
151
 
152
152
  // 调用用户传入的onMenuClick回调,获取返回值(是否应该激活菜单)
153
153
  let shouldActivate = true;
@@ -168,13 +168,13 @@ const OpenMenu = props => {
168
168
  if (menuItem && canOpenAsTab(menuItem) && shouldActivate) {
169
169
  setState({
170
170
  selectedKeys: keyPath,
171
- router: item?.props?.router
171
+ router: menuRouter
172
172
  });
173
173
  layoutOnSelected?.({
174
- selectedPath: item?.props?.router
174
+ selectedPath: menuRouter
175
175
  });
176
176
  if (layoutTarget) {
177
- window.open(item?.props?.router, layoutTarget);
177
+ window.open(menuRouter, layoutTarget);
178
178
  } else {
179
179
  setTimeout(() => {
180
180
  // 路由变更,且不再demo文档中
@@ -157,7 +157,7 @@ const Menu = props => {
157
157
  onMenuClick: onMenuClick,
158
158
  style: {
159
159
  display: collapsed ? 'none' : 'block',
160
- height: `calc(100vh - ${headerHeight + (notice ? 32 : 0) + 48 || 0}px)`
160
+ height: `calc(100vh - ${(headerHeight ?? 0) + (notice ? 32 : 0) + 48}px)`
161
161
  }
162
162
  }), sideMenuFooterRender || /*#__PURE__*/_jsx("div", {
163
163
  className: "pro-layout-menu-collapsed",
@@ -31,11 +31,12 @@ const ProFooter = props => {
31
31
  'pro-footer': true,
32
32
  [`${className}`]: className
33
33
  });
34
+ const sideWidth = size?.width ?? 0;
34
35
  return /*#__PURE__*/_jsx("div", {
35
36
  className: cls,
36
37
  style: {
37
38
  zIndex,
38
- width: `calc(100% - ${size?.width + 1}px)`,
39
+ width: `calc(100% - ${sideWidth + 1}px)`,
39
40
  ...props.style
40
41
  },
41
42
  children: /*#__PURE__*/_jsx(Space, {
@@ -46,7 +46,9 @@ export const DescribeValue = ({
46
46
  children: [link ? /*#__PURE__*/_jsx("a", {
47
47
  onClick: () => {
48
48
  const newWindow = window.open(link);
49
- newWindow.opener = null;
49
+ if (newWindow) {
50
+ newWindow.opener = null;
51
+ }
50
52
  },
51
53
  children: value
52
54
  }) : value, copyable && _isString(value) ? /*#__PURE__*/_jsx(Copy, {
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { CloseOutlined } from '@ant-design/icons';
3
- import ProTooltip from "../../../../ProTooltip";
3
+ import { Tooltip } from 'antd';
4
4
  import { TabContextMenu } from "./TabContextMenu";
5
5
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  const MAX_TAB_TITLE_LENGTH = 12;
@@ -70,12 +70,13 @@ const TabItemComponent = ({
70
70
  }) : tab.icon
71
71
  }), /*#__PURE__*/_jsx("div", {
72
72
  className: "pro-layout-tab-title",
73
- children: isTitleOverflow ? /*#__PURE__*/_jsx(ProTooltip, {
74
- text: displayTitle,
73
+ children: isTitleOverflow ? /*#__PURE__*/_jsx(Tooltip, {
75
74
  title: tabTitle,
76
- line: 1,
77
75
  placement: "top",
78
- scrollFollowParent: false
76
+ children: /*#__PURE__*/_jsx("span", {
77
+ className: "pro-tooltip",
78
+ children: displayTitle
79
+ })
79
80
  }) : tabTitle
80
81
  }), tab.closable && /*#__PURE__*/_jsx("span", {
81
82
  className: "pro-layout-tab-close",
@@ -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
- // 如果 forceNew = false(默认),检查是否已存在相同的标签页
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
- handleExternalOpen(menuItem);
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, menuItem) : undefined;
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 = menuItem.closable === false && existingTab.closable !== false ? {
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(menuItem, existingIds);
188
- const newTab = createTabFromMenu(menuItem, prevState.newTabIndex);
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 ? menuItem.code || tabId : prevState.activeComponent
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
- // 监听 URL 变化并同步 Tabs (仅在初始化和 location 变化时)
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 {};