@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
|
@@ -168,8 +168,9 @@ const ProModalSelect = (props, ref) => {
|
|
|
168
168
|
transformParams,
|
|
169
169
|
transformResponse
|
|
170
170
|
}, {
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
132
|
+
resultKeys.push(...key.split('-').map(keyStr => keyStr.split('_')));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// 深度获取对象key
|
|
136
136
|
if (_isObject(value)) {
|
|
137
|
-
resultKeys.
|
|
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:
|
|
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:
|
|
171
|
+
router: menuRouter
|
|
172
172
|
});
|
|
173
173
|
layoutOnSelected?.({
|
|
174
|
-
selectedPath:
|
|
174
|
+
selectedPath: menuRouter
|
|
175
175
|
});
|
|
176
176
|
if (layoutTarget) {
|
|
177
|
-
window.open(
|
|
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
|
|
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% - ${
|
|
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
|
|
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
|
|
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(
|
|
74
|
-
text: displayTitle,
|
|
73
|
+
children: isTitleOverflow ? /*#__PURE__*/_jsx(Tooltip, {
|
|
75
74
|
title: tabTitle,
|
|
76
|
-
line: 1,
|
|
77
75
|
placement: "top",
|
|
78
|
-
|
|
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
|
-
//
|
|
132
|
-
if (!forceNew) {
|
|
133
|
-
const existingTab = findExistingTab(state.tabsList, menuItem
|
|
157
|
+
// URL 同步模式下完整 URL 始终唯一,forceNew 不能绕过身份匹配。
|
|
158
|
+
if (urlSyncEnabled || !forceNew) {
|
|
159
|
+
const existingTab = findExistingTab(state.tabsList, menuItem, {
|
|
160
|
+
urlSync: urlSyncEnabled
|
|
161
|
+
});
|
|
134
162
|
if (existingTab) {
|
|
135
163
|
// 如果已存在,可以添加(会切换到已存在的标签页)
|
|
136
164
|
return true;
|
|
@@ -143,7 +171,7 @@ export const useTabsState = options => {
|
|
|
143
171
|
return false;
|
|
144
172
|
}
|
|
145
173
|
return true;
|
|
146
|
-
}, [state.tabsList, finalConfig]);
|
|
174
|
+
}, [state.tabsList, finalConfig, urlSyncEnabled]);
|
|
147
175
|
|
|
148
176
|
/**
|
|
149
177
|
* 添加标签页
|
|
@@ -156,15 +184,33 @@ export const useTabsState = options => {
|
|
|
156
184
|
setState(prevState => {
|
|
157
185
|
if (!canOpenAsTab(menuItem)) return prevState;
|
|
158
186
|
if (shouldOpenExternal(menuItem)) {
|
|
159
|
-
|
|
187
|
+
const externalUrl = getMenuRoute(menuItem);
|
|
188
|
+
if (!urlSyncEnabled || isSafeHttpUrl(externalUrl)) {
|
|
189
|
+
handleExternalOpen(menuItem, urlSyncEnabled);
|
|
190
|
+
}
|
|
191
|
+
return prevState;
|
|
192
|
+
}
|
|
193
|
+
const rawUrl = getMenuRoute(menuItem);
|
|
194
|
+
const normalizedUrl = urlSyncEnabled ? normalizeTabUrl(rawUrl) : undefined;
|
|
195
|
+
if (urlSyncEnabled && rawUrl && !normalizedUrl) {
|
|
196
|
+
if (isSafeExternalUrl(rawUrl)) {
|
|
197
|
+
handleExternalOpen(menuItem, true);
|
|
198
|
+
}
|
|
160
199
|
return prevState;
|
|
161
200
|
}
|
|
201
|
+
const resolvedMenuItem = normalizedUrl ? {
|
|
202
|
+
...menuItem,
|
|
203
|
+
url: normalizedUrl,
|
|
204
|
+
redirectUrl: undefined
|
|
205
|
+
} : menuItem;
|
|
162
206
|
|
|
163
207
|
// 非强制新建时,查找已存在的标签页并切换
|
|
164
|
-
const existingTab = !forceNew ? findExistingTab(prevState.tabsList,
|
|
208
|
+
const existingTab = urlSyncEnabled || !forceNew ? findExistingTab(prevState.tabsList, resolvedMenuItem, {
|
|
209
|
+
urlSync: urlSyncEnabled
|
|
210
|
+
}) : undefined;
|
|
165
211
|
if (existingTab) {
|
|
166
212
|
// 若来源菜单明确标记 closable: false 而已有 tab 未继承,则修正
|
|
167
|
-
const correctedTab =
|
|
213
|
+
const correctedTab = resolvedMenuItem.closable === false && existingTab.closable !== false ? {
|
|
168
214
|
...existingTab,
|
|
169
215
|
closable: false
|
|
170
216
|
} : existingTab;
|
|
@@ -184,8 +230,8 @@ export const useTabsState = options => {
|
|
|
184
230
|
return prevState;
|
|
185
231
|
}
|
|
186
232
|
const existingIds = prevState.tabsList.map(tab => tab.id);
|
|
187
|
-
const tabId = generateTabId(
|
|
188
|
-
const newTab = createTabFromMenu(
|
|
233
|
+
const tabId = generateTabId(resolvedMenuItem, existingIds);
|
|
234
|
+
const newTab = createTabFromMenu(resolvedMenuItem, prevState.newTabIndex);
|
|
189
235
|
newTab.id = tabId;
|
|
190
236
|
const pinnedBoundary = getPinnedBoundary(prevState.tabsList);
|
|
191
237
|
const nextTabsList = derivePinned(newTab) ? [...prevState.tabsList.slice(0, pinnedBoundary), newTab, ...prevState.tabsList.slice(pinnedBoundary)] : [...prevState.tabsList, newTab];
|
|
@@ -198,10 +244,10 @@ export const useTabsState = options => {
|
|
|
198
244
|
activeKey: shouldActivate ? tabId : prevState.activeKey,
|
|
199
245
|
activeTabInfo: shouldActivate ? newTab : prevState.activeTabInfo,
|
|
200
246
|
newTabIndex: prevState.newTabIndex + 1,
|
|
201
|
-
activeComponent: shouldActivate ?
|
|
247
|
+
activeComponent: shouldActivate ? resolvedMenuItem.code || tabId : prevState.activeComponent
|
|
202
248
|
};
|
|
203
249
|
});
|
|
204
|
-
}, [finalConfig]);
|
|
250
|
+
}, [finalConfig, urlSyncEnabled]);
|
|
205
251
|
|
|
206
252
|
// 普通初始标签只在当前 TabsManager 挂载周期应用一次。
|
|
207
253
|
// 缓存恢复已有标签时只消费默认值;closeAll 后 ref 保持 true,避免再次补签。
|
|
@@ -344,6 +390,46 @@ export const useTabsState = options => {
|
|
|
344
390
|
});
|
|
345
391
|
}, []);
|
|
346
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Router blocker 拒绝 URL 导航时,将激活状态还原到上一个已提交页签。
|
|
395
|
+
* 首次打开页签就被拦截时,同时移除未提交的新页签。
|
|
396
|
+
*/
|
|
397
|
+
const rollbackTabActivation = useCallback((previousTabId, attemptedTabId, removeAttemptedTab) => {
|
|
398
|
+
setState(prevState => {
|
|
399
|
+
if (prevState.activeKey !== attemptedTabId) return prevState;
|
|
400
|
+
const nextTabsList = removeAttemptedTab ? prevState.tabsList.filter(tab => tab.id !== attemptedTabId) : prevState.tabsList;
|
|
401
|
+
const previousTab = nextTabsList.find(tab => tab.id === previousTabId);
|
|
402
|
+
return {
|
|
403
|
+
...prevState,
|
|
404
|
+
tabsList: nextTabsList,
|
|
405
|
+
activeKey: previousTab?.id || '',
|
|
406
|
+
activeTabInfo: previousTab,
|
|
407
|
+
activeComponent: previousTab ? previousTab.menuItem?.code || previousTab.id : ''
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
}, []);
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 更新页签当前 URL,保持 tab.id 和 menuItem 的菜单基础路由不变。
|
|
414
|
+
*/
|
|
415
|
+
const updateTabUrl = useCallback((tabId, url) => {
|
|
416
|
+
setState(prevState => {
|
|
417
|
+
const targetIndex = prevState.tabsList.findIndex(tab => tab.id === tabId);
|
|
418
|
+
if (targetIndex === -1 || prevState.tabsList[targetIndex].url === url) return prevState;
|
|
419
|
+
const nextTab = {
|
|
420
|
+
...prevState.tabsList[targetIndex],
|
|
421
|
+
url
|
|
422
|
+
};
|
|
423
|
+
const nextTabsList = [...prevState.tabsList];
|
|
424
|
+
nextTabsList[targetIndex] = nextTab;
|
|
425
|
+
return {
|
|
426
|
+
...prevState,
|
|
427
|
+
tabsList: nextTabsList,
|
|
428
|
+
activeTabInfo: prevState.activeKey === tabId ? nextTab : prevState.activeTabInfo
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
}, []);
|
|
432
|
+
|
|
347
433
|
/**
|
|
348
434
|
* 关闭其他标签页
|
|
349
435
|
*/
|
|
@@ -474,9 +560,9 @@ export const useTabsState = options => {
|
|
|
474
560
|
});
|
|
475
561
|
}, []);
|
|
476
562
|
|
|
477
|
-
//
|
|
478
|
-
// 放在最后以确保可以使用 addTab 和 switchTab
|
|
563
|
+
// URL 不同步模式保留旧有的 pathname -> Tabs 初始匹配,但不写回 URL。
|
|
479
564
|
useEffect(() => {
|
|
565
|
+
if (urlSyncEnabled) return;
|
|
480
566
|
if (!dataSource || !('menus' in dataSource)) return;
|
|
481
567
|
const currentPath = window.location.pathname;
|
|
482
568
|
const activeTab = state.tabsList.find(tab => tab.id === state.activeKey);
|
|
@@ -490,7 +576,7 @@ export const useTabsState = options => {
|
|
|
490
576
|
return;
|
|
491
577
|
}
|
|
492
578
|
addTab(targetMenu);
|
|
493
|
-
}, [dataSource, state.tabsList, state.activeKey, addTab, switchTab]);
|
|
579
|
+
}, [urlSyncEnabled, dataSource, state.tabsList, state.activeKey, addTab, switchTab]);
|
|
494
580
|
return {
|
|
495
581
|
state,
|
|
496
582
|
isInitialized,
|
|
@@ -498,7 +584,9 @@ export const useTabsState = options => {
|
|
|
498
584
|
canAddTab,
|
|
499
585
|
removeTab,
|
|
500
586
|
updateTab,
|
|
587
|
+
updateTabUrl,
|
|
501
588
|
switchTab,
|
|
589
|
+
rollbackTabActivation,
|
|
502
590
|
closeOtherTabs,
|
|
503
591
|
closeRightTabs,
|
|
504
592
|
closeAllTabs,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TabsConfig, TabsState } from '../../../propTypes';
|
|
2
|
+
import type { TabsManagerProps, UseTabsStateReturn } from '../propTypes';
|
|
3
|
+
interface UseTabsUrlSyncOptions {
|
|
4
|
+
config: TabsConfig;
|
|
5
|
+
dataSource: TabsManagerProps['dataSource'];
|
|
6
|
+
state: TabsState;
|
|
7
|
+
isInitialized: boolean;
|
|
8
|
+
addTab: UseTabsStateReturn['addTab'];
|
|
9
|
+
switchTab: UseTabsStateReturn['switchTab'];
|
|
10
|
+
updateTabUrl: UseTabsStateReturn['updateTabUrl'];
|
|
11
|
+
rollbackTabActivation: UseTabsStateReturn['rollbackTabActivation'];
|
|
12
|
+
interactionSequence: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 维护浏览器 location 与活动页签 URL 的双向同步。
|
|
16
|
+
* 未开启 config.urlSync 时不安装任何 History 监听或写入逻辑。
|
|
17
|
+
*/
|
|
18
|
+
export declare const useTabsUrlSync: ({ config, dataSource, state, isInitialized, addTab, switchTab, updateTabUrl, rollbackTabActivation, interactionSequence, }: UseTabsUrlSyncOptions) => void;
|
|
19
|
+
export {};
|