@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.
@@ -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 ? ['1'] : [];
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("span", {
71
+ }), /*#__PURE__*/_jsx("div", {
55
72
  className: "pro-layout-tab-title",
56
- title: tab.title,
57
- children: tab.title
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 menuRoute = getMenuRoute(menuItem);
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 menuRoute = getMenuRoute(menuItem);
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 activeIndex = prevState.tabsList.findIndex(tab => tab.id === activeId);
391
- const overIndex = prevState.tabsList.findIndex(tab => tab.id === overId);
392
- if (activeIndex === -1 || overIndex === -1 || activeIndex === overIndex) return prevState;
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: arrayMove(prevState.tabsList, activeIndex, overIndex)
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.find(tab => tab.menuItem?.code === targetMenu.code || tab.url === getMenuRoute(targetMenu));
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
- return state.tabsList.map(tab => ({
252
- key: tab.id,
253
- label: /*#__PURE__*/_jsx(TabItemComponent, {
254
- tab: tab,
255
- active: tab.id === state.activeKey,
256
- onClick: () => switchTab(tab.id),
257
- onClose: () => removeTab(tab.id),
258
- onCloseOthers: () => closeOtherTabs(tab.id),
259
- onCloseRight: () => closeRightTabs(tab.id),
260
- onCloseAll: closeAllTabs,
261
- tabsList: state.tabsList,
262
- menuItems: config?.menuItems,
263
- tabMenuClick: config?.tabMenuClick
264
- }),
265
- closable: false,
266
- // 关闭 antd 内置 ×,由 TabItemComponent 自定义按钮处理
267
- children: null
268
- }));
269
- }, [state.tabsList, state.activeKey, config?.menuItems, config?.tabMenuClick, switchTab, removeTab, closeOtherTabs, closeRightTabs, closeAllTabs]);
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
  };