@zat-design/sisyphus-react 4.5.6-beta.3 → 4.5.6
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/ProLayout/components/ProCollapse/index.js +3 -1
- package/es/ProLayout/components/TabsManager/components/TabContextMenu.d.ts +1 -1
- package/es/ProLayout/components/TabsManager/components/TabContextMenu.js +16 -1
- package/es/ProLayout/components/TabsManager/components/TabItem.d.ts +6 -0
- package/es/ProLayout/components/TabsManager/components/TabItem.js +26 -3
- package/es/ProLayout/components/TabsManager/hooks/useTabsState.js +56 -12
- package/es/ProLayout/components/TabsManager/index.js +59 -29
- package/es/ProLayout/components/TabsManager/propTypes.d.ts +10 -0
- package/es/ProLayout/components/TabsManager/style/index.less +12 -0
- package/es/ProLayout/components/TabsManager/utils/index.d.ts +32 -0
- package/es/ProLayout/components/TabsManager/utils/index.js +106 -1
- package/es/ProLayout/propTypes.d.ts +21 -0
- package/es/ProLayout/style/index.less +17 -1
- package/es/locale/en_US.d.ts +2 -0
- package/es/locale/en_US.js +3 -1
- package/es/locale/zh_CN.d.ts +2 -0
- package/es/locale/zh_CN.js +3 -1
- package/package.json +2 -1
|
@@ -5,6 +5,8 @@ import classNames from 'classnames';
|
|
|
5
5
|
import foldSvg from "../../../assets/fold.svg";
|
|
6
6
|
import ProIcon from "../../../ProIcon/index";
|
|
7
7
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
const OPEN_ACTIVE_KEYS = ['1'];
|
|
9
|
+
const CLOSED_ACTIVE_KEYS = [];
|
|
8
10
|
const ProCollapse = props => {
|
|
9
11
|
const {
|
|
10
12
|
title,
|
|
@@ -34,7 +36,7 @@ const ProCollapse = props => {
|
|
|
34
36
|
...collapseProps
|
|
35
37
|
};
|
|
36
38
|
if (open !== undefined && !('activeKey' in finalCollapseProps)) {
|
|
37
|
-
finalCollapseProps.activeKey = open ?
|
|
39
|
+
finalCollapseProps.activeKey = open ? OPEN_ACTIVE_KEYS : CLOSED_ACTIVE_KEYS;
|
|
38
40
|
}
|
|
39
41
|
const cls = classNames({
|
|
40
42
|
'pro-collapse': true,
|
|
@@ -2,6 +2,6 @@ import { TabContextMenuProps } from '../propTypes';
|
|
|
2
2
|
/**
|
|
3
3
|
* 右键菜单组件 - 兼容Antd 4.x和5.x
|
|
4
4
|
*/
|
|
5
|
-
declare function TabContextMenu({ tabId, children, closable, onClose, onCloseOthers, onCloseRight, onCloseAll, menuItems: customMenuItems, tabMenuClick, tab, tabs, }: TabContextMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
declare function TabContextMenu({ tabId, children, closable, onClose, onCloseOthers, onCloseRight, onCloseAll, pinned, pinDisabled, onTogglePin, menuItems: customMenuItems, tabMenuClick, tab, tabs, }: TabContextMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
export default TabContextMenu;
|
|
7
7
|
export { TabContextMenu };
|
|
@@ -14,6 +14,9 @@ function TabContextMenu({
|
|
|
14
14
|
onCloseOthers,
|
|
15
15
|
onCloseRight,
|
|
16
16
|
onCloseAll,
|
|
17
|
+
pinned = false,
|
|
18
|
+
pinDisabled = false,
|
|
19
|
+
onTogglePin,
|
|
17
20
|
menuItems: customMenuItems,
|
|
18
21
|
tabMenuClick,
|
|
19
22
|
tab,
|
|
@@ -49,8 +52,14 @@ function TabContextMenu({
|
|
|
49
52
|
case 'closeAll':
|
|
50
53
|
onCloseAll();
|
|
51
54
|
break;
|
|
55
|
+
case 'togglePin':
|
|
56
|
+
if (pinDisabled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
onTogglePin?.(tabId);
|
|
60
|
+
break;
|
|
52
61
|
}
|
|
53
|
-
}, [tabId, closable, onClose, onCloseOthers, onCloseRight, onCloseAll, tabMenuClick, tab, tabs]);
|
|
62
|
+
}, [tabId, closable, onClose, onCloseOthers, onCloseRight, onCloseAll, pinDisabled, onTogglePin, tabMenuClick, tab, tabs]);
|
|
54
63
|
|
|
55
64
|
// 默认右键菜单配置 - 统一使用items方式(antd 4.20+都支持)
|
|
56
65
|
const defaultMenuItems = [{
|
|
@@ -66,6 +75,12 @@ function TabContextMenu({
|
|
|
66
75
|
}, {
|
|
67
76
|
key: 'closeAll',
|
|
68
77
|
label: locale.ProLayout.tabMenuCloseAll
|
|
78
|
+
}, {
|
|
79
|
+
type: 'divider'
|
|
80
|
+
}, {
|
|
81
|
+
key: 'togglePin',
|
|
82
|
+
label: pinned ? locale.ProLayout.tabMenuUnpin : locale.ProLayout.tabMenuPin,
|
|
83
|
+
disabled: pinDisabled
|
|
69
84
|
}];
|
|
70
85
|
|
|
71
86
|
// 合并自定义菜单项和默认菜单项
|
|
@@ -8,6 +8,12 @@ interface TabItemComponentProps {
|
|
|
8
8
|
onCloseOthers: () => void;
|
|
9
9
|
onCloseRight: () => void;
|
|
10
10
|
onCloseAll: () => void;
|
|
11
|
+
/** 当前标签是否已固定 */
|
|
12
|
+
pinned?: boolean;
|
|
13
|
+
/** 是否禁用固定/取消固定(config.fixed 声明的强制固定不可取消) */
|
|
14
|
+
pinDisabled?: boolean;
|
|
15
|
+
/** 切换固定状态回调 */
|
|
16
|
+
onTogglePin?: () => void;
|
|
11
17
|
/** 所有标签页列表 */
|
|
12
18
|
tabsList?: TabItem[];
|
|
13
19
|
/** 自定义菜单项 */
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { CloseOutlined } from '@ant-design/icons';
|
|
3
|
+
import ProTooltip from "../../../../ProTooltip";
|
|
3
4
|
import { TabContextMenu } from "./TabContextMenu";
|
|
4
5
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
6
|
+
const MAX_TAB_TITLE_LENGTH = 12;
|
|
7
|
+
const getTabDisplayTitle = title => {
|
|
8
|
+
const text = title || '';
|
|
9
|
+
const chars = Array.from(text);
|
|
10
|
+
if (chars.length <= MAX_TAB_TITLE_LENGTH) return text;
|
|
11
|
+
return `${chars.slice(0, MAX_TAB_TITLE_LENGTH).join('')}...`;
|
|
12
|
+
};
|
|
5
13
|
/**
|
|
6
14
|
* 单个标签页组件
|
|
7
15
|
*/
|
|
@@ -13,10 +21,16 @@ const TabItemComponent = ({
|
|
|
13
21
|
onCloseOthers,
|
|
14
22
|
onCloseRight,
|
|
15
23
|
onCloseAll,
|
|
24
|
+
pinned = false,
|
|
25
|
+
pinDisabled = false,
|
|
26
|
+
onTogglePin,
|
|
16
27
|
tabsList = [],
|
|
17
28
|
menuItems,
|
|
18
29
|
tabMenuClick
|
|
19
30
|
}) => {
|
|
31
|
+
const tabTitle = tab.title || tab.name || '';
|
|
32
|
+
const displayTitle = getTabDisplayTitle(tabTitle);
|
|
33
|
+
const isTitleOverflow = displayTitle !== tabTitle;
|
|
20
34
|
const handleCloseClick = e => {
|
|
21
35
|
e.stopPropagation();
|
|
22
36
|
onClose();
|
|
@@ -36,6 +50,9 @@ const TabItemComponent = ({
|
|
|
36
50
|
onCloseOthers: () => onCloseOthers(),
|
|
37
51
|
onCloseRight: () => onCloseRight(),
|
|
38
52
|
onCloseAll: onCloseAll,
|
|
53
|
+
pinned: pinned,
|
|
54
|
+
pinDisabled: pinDisabled,
|
|
55
|
+
onTogglePin: onTogglePin ? () => onTogglePin() : undefined,
|
|
39
56
|
tab: tab,
|
|
40
57
|
tabs: tabsList,
|
|
41
58
|
menuItems: menuItems,
|
|
@@ -51,10 +68,16 @@ const TabItemComponent = ({
|
|
|
51
68
|
children: typeof tab.icon === 'string' ? /*#__PURE__*/_jsx("i", {
|
|
52
69
|
className: `iconfont ${tab.icon}`
|
|
53
70
|
}) : tab.icon
|
|
54
|
-
}), /*#__PURE__*/_jsx("
|
|
71
|
+
}), /*#__PURE__*/_jsx("div", {
|
|
55
72
|
className: "pro-layout-tab-title",
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
children: isTitleOverflow ? /*#__PURE__*/_jsx(ProTooltip, {
|
|
74
|
+
text: displayTitle,
|
|
75
|
+
title: tabTitle,
|
|
76
|
+
line: 1,
|
|
77
|
+
width: "12em",
|
|
78
|
+
placement: "top",
|
|
79
|
+
scrollFollowParent: false
|
|
80
|
+
}) : tabTitle
|
|
58
81
|
}), tab.closable && /*#__PURE__*/_jsx("span", {
|
|
59
82
|
className: "pro-layout-tab-close",
|
|
60
83
|
onClick: handleCloseClick,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { message } from 'antd';
|
|
3
|
-
import { arrayMove } from '@dnd-kit/sortable';
|
|
4
3
|
import { DEFAULT_TABS_CONFIG } from "../propTypes";
|
|
5
4
|
import { useTabsCache } from "./useTabsCache";
|
|
6
5
|
import locale, { formatMessage } from "../../../../locale";
|
|
7
|
-
import { createTabFromMenu, generateTabId, shouldOpenExternal, handleExternalOpen, checkTabLimit, getRightTabs, flattenMenuData, canOpenAsTab, getMenuRoute } from "../utils";
|
|
6
|
+
import { createTabFromMenu, generateTabId, shouldOpenExternal, handleExternalOpen, checkTabLimit, getRightTabs, flattenMenuData, canOpenAsTab, getMenuRoute, findExistingTab, derivePinned, pinTab as pinTabInList, unpinTab as unpinTabInList, reorderWithPinConstraint } from "../utils";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* 标签页状态管理Hook
|
|
@@ -59,6 +58,13 @@ export const useTabsState = options => {
|
|
|
59
58
|
restoredState.activeTabInfo = restoredState.activeTab;
|
|
60
59
|
delete restoredState.activeTab;
|
|
61
60
|
}
|
|
61
|
+
|
|
62
|
+
// 兼容旧缓存:无 pinned 字段的标签用 closable===false 推导出 pinned,
|
|
63
|
+
// 保证刷新后固定标签仍落在左侧固定区
|
|
64
|
+
restoredState.tabsList = restoredState.tabsList.map(tab => tab.pinned === undefined ? {
|
|
65
|
+
...tab,
|
|
66
|
+
pinned: derivePinned(tab)
|
|
67
|
+
} : tab);
|
|
62
68
|
setState(restoredState);
|
|
63
69
|
}
|
|
64
70
|
setIsInitialized(true);
|
|
@@ -120,8 +126,7 @@ export const useTabsState = options => {
|
|
|
120
126
|
|
|
121
127
|
// 如果 forceNew = false(默认),检查是否已存在相同的标签页
|
|
122
128
|
if (!forceNew) {
|
|
123
|
-
const
|
|
124
|
-
const existingTab = state.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuRoute);
|
|
129
|
+
const existingTab = findExistingTab(state.tabsList, menuItem);
|
|
125
130
|
if (existingTab) {
|
|
126
131
|
// 如果已存在,可以添加(会切换到已存在的标签页)
|
|
127
132
|
return true;
|
|
@@ -152,8 +157,7 @@ export const useTabsState = options => {
|
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
// 非强制新建时,查找已存在的标签页并切换
|
|
155
|
-
const
|
|
156
|
-
const existingTab = !forceNew ? prevState.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuRoute) : undefined;
|
|
160
|
+
const existingTab = !forceNew ? findExistingTab(prevState.tabsList, menuItem) : undefined;
|
|
157
161
|
if (existingTab) {
|
|
158
162
|
// 若来源菜单明确标记 closable: false 而已有 tab 未继承,则修正
|
|
159
163
|
const correctedTab = menuItem.closable === false && existingTab.closable !== false ? {
|
|
@@ -217,6 +221,9 @@ export const useTabsState = options => {
|
|
|
217
221
|
}),
|
|
218
222
|
...(updates.closable !== undefined && {
|
|
219
223
|
closable: updates.closable
|
|
224
|
+
}),
|
|
225
|
+
...(updates.pinned !== undefined && {
|
|
226
|
+
pinned: updates.pinned
|
|
220
227
|
})
|
|
221
228
|
} : target.menuItem;
|
|
222
229
|
const nextTab = {
|
|
@@ -231,6 +238,9 @@ export const useTabsState = options => {
|
|
|
231
238
|
...(updates.closable !== undefined && {
|
|
232
239
|
closable: updates.closable
|
|
233
240
|
}),
|
|
241
|
+
...(updates.pinned !== undefined && {
|
|
242
|
+
pinned: updates.pinned
|
|
243
|
+
}),
|
|
234
244
|
menuItem: nextMenuItem
|
|
235
245
|
};
|
|
236
246
|
const newTabsList = [...prevState.tabsList];
|
|
@@ -384,15 +394,47 @@ export const useTabsState = options => {
|
|
|
384
394
|
|
|
385
395
|
/**
|
|
386
396
|
* 重排标签页顺序(拖拽结束后调用)
|
|
397
|
+
* 应用固定分区约束:普通标签不可拖入固定区、固定标签不可拖出固定区
|
|
387
398
|
*/
|
|
388
399
|
const reorderTabs = useCallback((activeId, overId) => {
|
|
389
400
|
setState(prevState => {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
401
|
+
const nextTabsList = reorderWithPinConstraint(prevState.tabsList, activeId, overId);
|
|
402
|
+
if (nextTabsList === prevState.tabsList) return prevState;
|
|
403
|
+
return {
|
|
404
|
+
...prevState,
|
|
405
|
+
tabsList: nextTabsList
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}, []);
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 固定标签页:设为 pinned/不可关闭并移动到固定区末尾
|
|
412
|
+
*/
|
|
413
|
+
const pinTab = useCallback(tabId => {
|
|
414
|
+
setState(prevState => {
|
|
415
|
+
const nextTabsList = pinTabInList(prevState.tabsList, tabId);
|
|
416
|
+
if (nextTabsList === prevState.tabsList) return prevState;
|
|
417
|
+
const nextActiveTabInfo = prevState.activeKey === tabId ? nextTabsList.find(tab => tab.id === tabId) : prevState.activeTabInfo;
|
|
418
|
+
return {
|
|
419
|
+
...prevState,
|
|
420
|
+
tabsList: nextTabsList,
|
|
421
|
+
activeTabInfo: nextActiveTabInfo
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
}, []);
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 取消固定标签页:设为可关闭并移动到普通区最前
|
|
428
|
+
*/
|
|
429
|
+
const unpinTab = useCallback(tabId => {
|
|
430
|
+
setState(prevState => {
|
|
431
|
+
const nextTabsList = unpinTabInList(prevState.tabsList, tabId);
|
|
432
|
+
if (nextTabsList === prevState.tabsList) return prevState;
|
|
433
|
+
const nextActiveTabInfo = prevState.activeKey === tabId ? nextTabsList.find(tab => tab.id === tabId) : prevState.activeTabInfo;
|
|
393
434
|
return {
|
|
394
435
|
...prevState,
|
|
395
|
-
tabsList:
|
|
436
|
+
tabsList: nextTabsList,
|
|
437
|
+
activeTabInfo: nextActiveTabInfo
|
|
396
438
|
};
|
|
397
439
|
});
|
|
398
440
|
}, []);
|
|
@@ -419,7 +461,7 @@ export const useTabsState = options => {
|
|
|
419
461
|
const flatMenus = flattenMenuData(dataSource.menus || []);
|
|
420
462
|
const targetMenu = flatMenus.find(item => getMenuRoute(item) === currentPath);
|
|
421
463
|
if (!targetMenu || !canOpenAsTab(targetMenu)) return;
|
|
422
|
-
const existingTab = state.tabsList
|
|
464
|
+
const existingTab = findExistingTab(state.tabsList, targetMenu);
|
|
423
465
|
if (existingTab) {
|
|
424
466
|
if (state.activeKey !== existingTab.id) switchTab(existingTab.id);
|
|
425
467
|
return;
|
|
@@ -438,6 +480,8 @@ export const useTabsState = options => {
|
|
|
438
480
|
closeRightTabs,
|
|
439
481
|
closeAllTabs,
|
|
440
482
|
resetTabs,
|
|
441
|
-
reorderTabs
|
|
483
|
+
reorderTabs,
|
|
484
|
+
pinTab,
|
|
485
|
+
unpinTab
|
|
442
486
|
};
|
|
443
487
|
};
|
|
@@ -2,7 +2,7 @@ import { useCallback, useMemo, forwardRef, useImperativeHandle, useState, useEff
|
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { useTabsState } from "./hooks/useTabsState";
|
|
4
4
|
import { useIframeRoute } from "./hooks/useIframeRoute";
|
|
5
|
-
import { flattenMenuData } from "./utils";
|
|
5
|
+
import { flattenMenuData, derivePinned } from "./utils";
|
|
6
6
|
import { TabItemComponent } from "./components/TabItem";
|
|
7
7
|
import { TabsHeader } from "./components/TabsHeader";
|
|
8
8
|
import { TabsContext } from "./components/TabsContext";
|
|
@@ -45,7 +45,9 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
45
45
|
closeOtherTabs,
|
|
46
46
|
closeRightTabs,
|
|
47
47
|
closeAllTabs,
|
|
48
|
-
reorderTabs
|
|
48
|
+
reorderTabs,
|
|
49
|
+
pinTab,
|
|
50
|
+
unpinTab
|
|
49
51
|
} = useTabsState({
|
|
50
52
|
config,
|
|
51
53
|
dataSource,
|
|
@@ -165,26 +167,30 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
165
167
|
// menus 为空说明异步数据尚未返回,等 dataSource 变化后重跑,避免用 code 创建占位 tab
|
|
166
168
|
if (sourceMenus.length === 0) return;
|
|
167
169
|
config.fixed.forEach(code => {
|
|
168
|
-
// 已存在(如缓存恢复):仅校正 closable,不调 addTab 以保留缓存中的 activeKey
|
|
170
|
+
// 已存在(如缓存恢复):仅校正 closable/pinned,不调 addTab 以保留缓存中的 activeKey
|
|
169
171
|
const existing = state.tabsList.find(tab => tab.menuItem?.code === code || tab.code === code);
|
|
170
172
|
if (existing) {
|
|
171
|
-
if (existing.closable !== false) {
|
|
173
|
+
if (existing.closable !== false || existing.pinned !== true) {
|
|
172
174
|
updateTab(existing.id, {
|
|
173
|
-
closable: false
|
|
175
|
+
closable: false,
|
|
176
|
+
pinned: true
|
|
174
177
|
});
|
|
175
178
|
}
|
|
176
179
|
return;
|
|
177
180
|
}
|
|
178
181
|
const matchedMenu = sourceMenus.find(item => item.code === code);
|
|
182
|
+
// pinned:true 使声明式固定标签与用户手动固定统一走 pinned 分区逻辑
|
|
179
183
|
const menuItem = matchedMenu ? {
|
|
180
184
|
...matchedMenu,
|
|
181
|
-
closable: false
|
|
185
|
+
closable: false,
|
|
186
|
+
pinned: true
|
|
182
187
|
} : {
|
|
183
188
|
id: Date.now(),
|
|
184
189
|
code,
|
|
185
190
|
name: code,
|
|
186
191
|
url: `/${code}`,
|
|
187
|
-
closable: false
|
|
192
|
+
closable: false,
|
|
193
|
+
pinned: true
|
|
188
194
|
};
|
|
189
195
|
// silent: 已有 active(如 iframe 路由命中)时不抢 active;无 active 时仍激活以保留默认行为
|
|
190
196
|
addTabRef.current(menuItem, {
|
|
@@ -201,13 +207,25 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
201
207
|
originalOnMenuClick?.(params);
|
|
202
208
|
}, [addTab, originalOnMenuClick]);
|
|
203
209
|
|
|
210
|
+
// 切换固定状态:已固定→取消固定,未固定→固定
|
|
211
|
+
const handleTogglePin = useCallback(tabId => {
|
|
212
|
+
const target = state.tabsList.find(tab => tab.id === tabId);
|
|
213
|
+
if (!target) return;
|
|
214
|
+
if (derivePinned(target)) {
|
|
215
|
+
unpinTab(tabId);
|
|
216
|
+
} else {
|
|
217
|
+
pinTab(tabId);
|
|
218
|
+
}
|
|
219
|
+
}, [state.tabsList, pinTab, unpinTab]);
|
|
220
|
+
|
|
204
221
|
// 创建标签页实例 API
|
|
205
222
|
const tabsInstance = useMemo(() => ({
|
|
206
223
|
addTab: (params, options) => {
|
|
207
224
|
const {
|
|
208
225
|
code,
|
|
209
226
|
name,
|
|
210
|
-
extra
|
|
227
|
+
extra,
|
|
228
|
+
relationKey
|
|
211
229
|
} = params;
|
|
212
230
|
const sourceMenus = Array.isArray(dataSource?.menus) ? flattenMenuData(dataSource.menus) : [];
|
|
213
231
|
const matchedMenu = sourceMenus.find(item => item.code === code);
|
|
@@ -215,13 +233,15 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
215
233
|
...matchedMenu,
|
|
216
234
|
// 允许外部覆盖展示名称和附加数据,但保留菜单原始 closable/url 等配置
|
|
217
235
|
name: name || matchedMenu.name,
|
|
218
|
-
extra: extra ?? matchedMenu.extra
|
|
236
|
+
extra: extra ?? matchedMenu.extra,
|
|
237
|
+
relationKey: relationKey ?? matchedMenu.relationKey
|
|
219
238
|
} : {
|
|
220
239
|
id: Date.now(),
|
|
221
240
|
code,
|
|
222
241
|
name,
|
|
223
242
|
url: `/${code}`,
|
|
224
|
-
extra
|
|
243
|
+
extra,
|
|
244
|
+
relationKey
|
|
225
245
|
};
|
|
226
246
|
addTab(menuItem, options);
|
|
227
247
|
},
|
|
@@ -248,25 +268,35 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
248
268
|
// 构建 Tabs items
|
|
249
269
|
const tabsItems = useMemo(() => {
|
|
250
270
|
if (state.tabsList.length === 0) return [];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
|
|
272
|
+
// config.fixed 声明的 code 集合:这些标签是强制固定,不允许用户取消固定
|
|
273
|
+
const forcedFixedCodes = new Set(config?.fixed ?? []);
|
|
274
|
+
return state.tabsList.map(tab => {
|
|
275
|
+
const pinned = derivePinned(tab);
|
|
276
|
+
const pinDisabled = forcedFixedCodes.has(tab.menuItem?.code ?? tab.code);
|
|
277
|
+
return {
|
|
278
|
+
key: tab.id,
|
|
279
|
+
label: /*#__PURE__*/_jsx(TabItemComponent, {
|
|
280
|
+
tab: tab,
|
|
281
|
+
active: tab.id === state.activeKey,
|
|
282
|
+
onClick: () => switchTab(tab.id),
|
|
283
|
+
onClose: () => removeTab(tab.id),
|
|
284
|
+
onCloseOthers: () => closeOtherTabs(tab.id),
|
|
285
|
+
onCloseRight: () => closeRightTabs(tab.id),
|
|
286
|
+
onCloseAll: closeAllTabs,
|
|
287
|
+
pinned: pinned,
|
|
288
|
+
pinDisabled: pinDisabled,
|
|
289
|
+
onTogglePin: () => handleTogglePin(tab.id),
|
|
290
|
+
tabsList: state.tabsList,
|
|
291
|
+
menuItems: config?.menuItems,
|
|
292
|
+
tabMenuClick: config?.tabMenuClick
|
|
293
|
+
}),
|
|
294
|
+
closable: false,
|
|
295
|
+
// 关闭 antd 内置 ×,由 TabItemComponent 自定义按钮处理
|
|
296
|
+
children: null
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
}, [state.tabsList, state.activeKey, config?.menuItems, config?.tabMenuClick, config?.fixed, switchTab, removeTab, closeOtherTabs, closeRightTabs, closeAllTabs, handleTogglePin]);
|
|
270
300
|
const activeComponent = config?.activeComponent;
|
|
271
301
|
const emptyComponent = config?.empty;
|
|
272
302
|
|
|
@@ -29,6 +29,12 @@ export interface TabContextMenuProps {
|
|
|
29
29
|
onCloseOthers: (tabId: string) => void;
|
|
30
30
|
onCloseRight: (tabId: string) => void;
|
|
31
31
|
onCloseAll: () => void;
|
|
32
|
+
/** 当前标签页是否已固定(决定菜单显示「固定标签」还是「取消固定」) */
|
|
33
|
+
pinned?: boolean;
|
|
34
|
+
/** 是否禁用固定/取消固定项(如 config.fixed 声明的强制固定标签不可取消) */
|
|
35
|
+
pinDisabled?: boolean;
|
|
36
|
+
/** 切换固定状态回调 */
|
|
37
|
+
onTogglePin?: (tabId: string) => void;
|
|
32
38
|
/** 自定义菜单项,会与默认菜单项合并 */
|
|
33
39
|
menuItems?: MenuProps['items'];
|
|
34
40
|
/** 自定义菜单项点击回调 */
|
|
@@ -77,6 +83,10 @@ export interface UseTabsStateReturn {
|
|
|
77
83
|
resetTabs: () => void;
|
|
78
84
|
/** 重排标签页顺序 */
|
|
79
85
|
reorderTabs: (activeId: string, overId: string) => void;
|
|
86
|
+
/** 固定标签页(设为不可关闭并移动到固定区末尾) */
|
|
87
|
+
pinTab: (tabId: string) => void;
|
|
88
|
+
/** 取消固定标签页(设为可关闭并移动到普通区最前) */
|
|
89
|
+
unpinTab: (tabId: string) => void;
|
|
80
90
|
}
|
|
81
91
|
export interface TabsCacheManager {
|
|
82
92
|
save: (state: TabsState) => void;
|
|
@@ -228,6 +228,18 @@
|
|
|
228
228
|
white-space: nowrap;
|
|
229
229
|
text-overflow: ellipsis;
|
|
230
230
|
transition: color 0.2s;
|
|
231
|
+
|
|
232
|
+
.pro-tooltip {
|
|
233
|
+
color: inherit;
|
|
234
|
+
font-weight: inherit;
|
|
235
|
+
font-size: inherit;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.singleLine {
|
|
239
|
+
color: inherit;
|
|
240
|
+
font-weight: inherit;
|
|
241
|
+
font-size: inherit;
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
.pro-layout-tab-close {
|
|
@@ -4,6 +4,8 @@ import { TabItem, MenusType } from '../../../propTypes';
|
|
|
4
4
|
* 约定优先使用 redirectUrl,其次回退 url
|
|
5
5
|
*/
|
|
6
6
|
export declare const getMenuRoute: (menuItem: MenusType) => string;
|
|
7
|
+
export declare const hasRelationKey: (relationKey: MenusType["relationKey"]) => relationKey is string | number;
|
|
8
|
+
export declare const findExistingTab: (tabsList: TabItem[], menuItem: MenusType) => TabItem | undefined;
|
|
7
9
|
/**
|
|
8
10
|
* 根据菜单项生成TabItem
|
|
9
11
|
*/
|
|
@@ -45,3 +47,33 @@ export declare const isLeafMenuItem: (menuItem: MenusType) => boolean;
|
|
|
45
47
|
* 规则:只有叶子节点(无 children)才可打开
|
|
46
48
|
*/
|
|
47
49
|
export declare const canOpenAsTab: (menuItem: MenusType) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* 推导标签页是否处于固定状态。
|
|
52
|
+
* pinned 为本质状态;未显式设置 pinned 的旧缓存标签,用 closable===false 推导。
|
|
53
|
+
*/
|
|
54
|
+
export declare const derivePinned: (tab: TabItem) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 分区边界:第一个「非固定标签」的下标,等于固定标签的数量。
|
|
57
|
+
* 约定固定区始终排在普通区左侧,故边界即两区分界点。
|
|
58
|
+
*/
|
|
59
|
+
export declare const getPinnedBoundary: (tabs: TabItem[]) => number;
|
|
60
|
+
/**
|
|
61
|
+
* 固定标签:将目标标签设为 pinned=true / closable=false,并移动到固定区末尾。
|
|
62
|
+
* - 已固定则幂等(顺序不变)
|
|
63
|
+
* - tabId 不存在时返回原数组引用
|
|
64
|
+
*/
|
|
65
|
+
export declare const pinTab: (tabs: TabItem[], tabId: string) => TabItem[];
|
|
66
|
+
/**
|
|
67
|
+
* 取消固定:将目标标签设为 pinned=false / closable=true,并移动到普通区最前
|
|
68
|
+
* (即剩余固定区之后的第一位)。
|
|
69
|
+
* - 已是普通标签则幂等(顺序不变)
|
|
70
|
+
* - tabId 不存在时返回原数组引用
|
|
71
|
+
*/
|
|
72
|
+
export declare const unpinTab: (tabs: TabItem[], tabId: string) => TabItem[];
|
|
73
|
+
/**
|
|
74
|
+
* 约束重排:在普通 arrayMove 基础上,钳制落点以维持「固定区在左、普通区在右」不变量。
|
|
75
|
+
* - 被拖动的普通标签:落点钳制到 >= boundary(不进固定区)
|
|
76
|
+
* - 被拖动的固定标签:落点钳制到 <= boundary-1(不出固定区)
|
|
77
|
+
* - 钳制后落点等于原位、或 id 相同/无效时,返回原数组引用(避免无效更新)
|
|
78
|
+
*/
|
|
79
|
+
export declare const reorderWithPinConstraint: (tabs: TabItem[], activeId: string, overId: string) => TabItem[];
|
|
@@ -5,12 +5,22 @@
|
|
|
5
5
|
export const getMenuRoute = menuItem => {
|
|
6
6
|
return menuItem.redirectUrl || menuItem.url || '';
|
|
7
7
|
};
|
|
8
|
+
export const hasRelationKey = relationKey => relationKey !== undefined && relationKey !== null && relationKey !== '';
|
|
9
|
+
export const findExistingTab = (tabsList, menuItem) => {
|
|
10
|
+
if (hasRelationKey(menuItem.relationKey)) {
|
|
11
|
+
return tabsList.find(tab => tab.relationKey === menuItem.relationKey || tab.menuItem?.relationKey === menuItem.relationKey);
|
|
12
|
+
}
|
|
13
|
+
const menuRoute = getMenuRoute(menuItem);
|
|
14
|
+
return tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuRoute);
|
|
15
|
+
};
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* 根据菜单项生成TabItem
|
|
11
19
|
*/
|
|
12
20
|
export const createTabFromMenu = (menuItem, index = 0) => {
|
|
13
21
|
const route = getMenuRoute(menuItem);
|
|
22
|
+
// pinned 优先取菜单显式声明,否则由 closable===false 推导(声明式固定标签)
|
|
23
|
+
const pinned = menuItem.pinned ?? menuItem.closable === false;
|
|
14
24
|
const result = {
|
|
15
25
|
id: String(menuItem.id || menuItem.code || menuItem.url || index),
|
|
16
26
|
code: menuItem.code,
|
|
@@ -18,8 +28,10 @@ export const createTabFromMenu = (menuItem, index = 0) => {
|
|
|
18
28
|
title: menuItem.name,
|
|
19
29
|
url: route,
|
|
20
30
|
closable: menuItem.closable !== false,
|
|
31
|
+
pinned,
|
|
21
32
|
menuItem,
|
|
22
|
-
icon: menuItem.icon || menuItem.imgUrl
|
|
33
|
+
icon: menuItem.icon || menuItem.imgUrl,
|
|
34
|
+
relationKey: menuItem.relationKey
|
|
23
35
|
};
|
|
24
36
|
return result;
|
|
25
37
|
};
|
|
@@ -115,4 +127,97 @@ export const canOpenAsTab = menuItem => {
|
|
|
115
127
|
if (!menuItem) return false;
|
|
116
128
|
if (!(menuItem.url || menuItem.redirectUrl)) return false;
|
|
117
129
|
return isLeafMenuItem(menuItem);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 推导标签页是否处于固定状态。
|
|
134
|
+
* pinned 为本质状态;未显式设置 pinned 的旧缓存标签,用 closable===false 推导。
|
|
135
|
+
*/
|
|
136
|
+
export const derivePinned = tab => tab.pinned ?? tab.closable === false;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 分区边界:第一个「非固定标签」的下标,等于固定标签的数量。
|
|
140
|
+
* 约定固定区始终排在普通区左侧,故边界即两区分界点。
|
|
141
|
+
*/
|
|
142
|
+
export const getPinnedBoundary = tabs => {
|
|
143
|
+
const index = tabs.findIndex(tab => !derivePinned(tab));
|
|
144
|
+
return index === -1 ? tabs.length : index;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 不依赖外部库的数组移动(immutable)。
|
|
149
|
+
* from/to 越界时返回原数组的浅拷贝。
|
|
150
|
+
*/
|
|
151
|
+
const moveItem = (list, from, to) => {
|
|
152
|
+
const result = [...list];
|
|
153
|
+
if (from < 0 || from >= result.length || to < 0 || to >= result.length) return result;
|
|
154
|
+
const [removed] = result.splice(from, 1);
|
|
155
|
+
result.splice(to, 0, removed);
|
|
156
|
+
return result;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 固定标签:将目标标签设为 pinned=true / closable=false,并移动到固定区末尾。
|
|
161
|
+
* - 已固定则幂等(顺序不变)
|
|
162
|
+
* - tabId 不存在时返回原数组引用
|
|
163
|
+
*/
|
|
164
|
+
export const pinTab = (tabs, tabId) => {
|
|
165
|
+
const index = tabs.findIndex(tab => tab.id === tabId);
|
|
166
|
+
if (index === -1) return tabs;
|
|
167
|
+
if (derivePinned(tabs[index])) return tabs;
|
|
168
|
+
const pinnedTab = {
|
|
169
|
+
...tabs[index],
|
|
170
|
+
pinned: true,
|
|
171
|
+
closable: false
|
|
172
|
+
};
|
|
173
|
+
// 移除目标后计算固定区末尾位置(= 剩余标签中的固定数量)
|
|
174
|
+
const rest = tabs.filter((_, i) => i !== index);
|
|
175
|
+
const insertAt = getPinnedBoundary(rest);
|
|
176
|
+
return [...rest.slice(0, insertAt), pinnedTab, ...rest.slice(insertAt)];
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 取消固定:将目标标签设为 pinned=false / closable=true,并移动到普通区最前
|
|
181
|
+
* (即剩余固定区之后的第一位)。
|
|
182
|
+
* - 已是普通标签则幂等(顺序不变)
|
|
183
|
+
* - tabId 不存在时返回原数组引用
|
|
184
|
+
*/
|
|
185
|
+
export const unpinTab = (tabs, tabId) => {
|
|
186
|
+
const index = tabs.findIndex(tab => tab.id === tabId);
|
|
187
|
+
if (index === -1) return tabs;
|
|
188
|
+
if (!derivePinned(tabs[index])) return tabs;
|
|
189
|
+
const normalTab = {
|
|
190
|
+
...tabs[index],
|
|
191
|
+
pinned: false,
|
|
192
|
+
closable: true
|
|
193
|
+
};
|
|
194
|
+
const rest = tabs.filter((_, i) => i !== index);
|
|
195
|
+
const insertAt = getPinnedBoundary(rest);
|
|
196
|
+
return [...rest.slice(0, insertAt), normalTab, ...rest.slice(insertAt)];
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 约束重排:在普通 arrayMove 基础上,钳制落点以维持「固定区在左、普通区在右」不变量。
|
|
201
|
+
* - 被拖动的普通标签:落点钳制到 >= boundary(不进固定区)
|
|
202
|
+
* - 被拖动的固定标签:落点钳制到 <= boundary-1(不出固定区)
|
|
203
|
+
* - 钳制后落点等于原位、或 id 相同/无效时,返回原数组引用(避免无效更新)
|
|
204
|
+
*/
|
|
205
|
+
export const reorderWithPinConstraint = (tabs, activeId, overId) => {
|
|
206
|
+
if (activeId === overId) return tabs;
|
|
207
|
+
const activeIndex = tabs.findIndex(tab => tab.id === activeId);
|
|
208
|
+
const overIndex = tabs.findIndex(tab => tab.id === overId);
|
|
209
|
+
if (activeIndex === -1 || overIndex === -1) return tabs;
|
|
210
|
+
const boundary = getPinnedBoundary(tabs);
|
|
211
|
+
const activePinned = derivePinned(tabs[activeIndex]);
|
|
212
|
+
let targetIndex = overIndex;
|
|
213
|
+
if (activePinned) {
|
|
214
|
+
// 固定标签只能落在固定区内:[0, boundary-1]
|
|
215
|
+
const maxIndex = boundary - 1;
|
|
216
|
+
if (targetIndex > maxIndex) targetIndex = maxIndex;
|
|
217
|
+
} else {
|
|
218
|
+
// 普通标签只能落在普通区内:[boundary, length-1]
|
|
219
|
+
if (targetIndex < boundary) targetIndex = boundary;
|
|
220
|
+
}
|
|
221
|
+
if (targetIndex === activeIndex) return tabs;
|
|
222
|
+
return moveItem(tabs, activeIndex, targetIndex);
|
|
118
223
|
};
|