@zat-design/sisyphus-react 4.3.0-beta.8 → 4.3.0-beta.9

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.
@@ -75,58 +75,58 @@ export declare const useFormItemProps: (column: FlexibleGroupColumnType, context
75
75
  confirm?: boolean | import("antd").ModalFuncProps | import("../../../render/propsType").FunctionArgs<any, boolean | import("antd").ModalFuncProps>;
76
76
  show?: boolean | ReactiveFunction<any, boolean>;
77
77
  component?: React.ReactNode | ReactiveFunction<any, React.ReactNode>;
78
- status?: "" | "success" | "warning" | "error" | "validating";
78
+ id?: string;
79
79
  className?: string;
80
+ children?: React.ReactNode | ((form: FormInstance<any>) => React.ReactNode);
80
81
  hidden?: boolean;
81
- id?: string;
82
82
  style?: React.CSSProperties;
83
- children?: React.ReactNode | ((form: FormInstance<any>) => React.ReactNode);
84
83
  onReset?: () => void;
85
84
  prefixCls?: string;
86
85
  rootClassName?: string;
87
- colon?: boolean;
86
+ layout?: import("antd/es/form/Form").FormItemLayout;
87
+ help?: React.ReactNode;
88
+ vertical?: boolean;
89
+ preserve?: boolean;
90
+ trim?: boolean;
91
+ normalize?: (value: any, prevValue: any, allValues: import("@rc-component/form/lib/interface").Store) => any;
88
92
  htmlFor?: string;
93
+ trigger?: string;
94
+ isView?: boolean;
95
+ status?: "" | "warning" | "error" | "success" | "validating";
96
+ getValueProps?: ((value: any) => Record<string, unknown>) & ((value: any) => Record<string, unknown>);
97
+ desensitization?: [number, number] | ReactiveFunction<any, [number, number]>;
98
+ validateTrigger?: string | false | string[];
99
+ clearNotShow?: boolean;
89
100
  labelAlign?: import("antd/es/form/interface").FormLabelAlign;
101
+ colon?: boolean;
90
102
  labelCol?: import("antd").ColProps;
91
- vertical?: boolean;
103
+ wrapperCol?: import("antd").ColProps;
92
104
  getValueFromEvent?: (...args: import("@rc-component/form/lib/interface").EventArgs) => any;
93
- normalize?: (value: any, prevValue: any, allValues: import("@rc-component/form/lib/interface").Store) => any;
94
105
  shouldUpdate?: import("@rc-component/form/lib/Field").ShouldUpdate<any>;
95
- trigger?: string;
96
- validateTrigger?: string | false | string[];
97
106
  validateDebounce?: number;
98
107
  valuePropName?: string;
99
- getValueProps?: ((value: any) => Record<string, unknown>) & ((value: any) => Record<string, unknown>);
100
108
  messageVariables?: Record<string, string>;
101
109
  initialValue?: any;
102
110
  onMetaChange?: (meta: import("@rc-component/form/lib/Field").MetaEvent) => void;
103
- preserve?: boolean;
104
111
  isListField?: boolean;
105
112
  isList?: boolean;
106
113
  noStyle?: boolean;
107
114
  hasFeedback?: boolean | {
108
115
  icons: import("antd/es/form/FormItem").FeedbackIcons;
109
116
  };
110
- validateStatus?: "" | "success" | "warning" | "error" | "validating";
111
- layout?: import("antd/es/form/Form").FormItemLayout;
112
- wrapperCol?: import("antd").ColProps;
113
- help?: React.ReactNode;
117
+ validateStatus?: "" | "warning" | "error" | "success" | "validating";
114
118
  fieldId?: string;
115
119
  valueType?: import("../../../render/propsType").ProFormValueType;
120
+ switchValue?: [any, any];
116
121
  viewRender?: (value: any, record: any, { form, index, namePath, }: {
117
122
  [key: string]: any;
118
123
  form: FormInstance<any>;
119
124
  index?: number;
120
125
  }) => string | React.ReactElement<any, any>;
121
- desensitization?: [number, number] | ReactiveFunction<any, [number, number]>;
122
- isView?: boolean;
123
- switchValue?: [any, any];
124
126
  viewType?: import("../../../render/propsType").ViewType;
125
- trim?: boolean;
126
127
  upperCase?: boolean;
127
128
  toISOString?: boolean;
128
129
  toCSTString?: boolean;
129
- clearNotShow?: boolean;
130
130
  name: any;
131
131
  dependencies: any[];
132
132
  tooltip: string | {
@@ -141,7 +141,7 @@ export declare const useFormItemProps: (column: FlexibleGroupColumnType, context
141
141
  * 创建组件属性
142
142
  */
143
143
  export declare const createComponentProps: (column: FlexibleGroupColumnType, formItemProps: any) => {
144
- componentProps: import("lodash").Omit<any, "valueType" | "precision" | "format" | "switchValue" | "dependNames" | "toISOString" | "toCSTString" | "clearNotShow">;
144
+ componentProps: import("lodash").Omit<any, "format" | "clearNotShow" | "valueType" | "switchValue" | "dependNames" | "toISOString" | "toCSTString" | "precision">;
145
145
  formItemTransform: {
146
146
  getValueProps: any;
147
147
  normalize: any;
@@ -143,49 +143,41 @@ export const useTabsState = options => {
143
143
  forceNew = false
144
144
  } = options || {};
145
145
  setState(prevState => {
146
- // 非叶子节点默认不入签,仅根节点特例可入签
147
- if (!canOpenAsTab(menuItem)) {
148
- return prevState;
149
- }
150
-
151
- // 检查是否需要外部跳转
146
+ if (!canOpenAsTab(menuItem)) return prevState;
152
147
  if (shouldOpenExternal(menuItem)) {
153
148
  handleExternalOpen(menuItem);
154
149
  return prevState;
155
150
  }
156
- const existingIds = prevState.tabsList.map(tab => tab.id);
157
- const tabId = generateTabId(menuItem, existingIds);
158
151
 
159
- // 如果 forceNew = false(默认),检查是否已存在相同的标签页
160
- if (!forceNew) {
161
- const menuRoute = getMenuRoute(menuItem);
162
- const existingTab = prevState.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuRoute);
163
- if (existingTab) {
164
- // 如果已存在,直接切换到该标签页
165
- const newState = {
166
- ...prevState,
167
- activeKey: existingTab.id,
168
- activeTabInfo: existingTab,
169
- activeComponent: existingTab.menuItem?.code || existingTab.id
170
- };
171
- return newState;
172
- }
152
+ // 非强制新建时,查找已存在的标签页并切换
153
+ const menuRoute = getMenuRoute(menuItem);
154
+ const existingTab = !forceNew ? prevState.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuRoute) : undefined;
155
+ if (existingTab) {
156
+ // 若来源菜单明确标记 closable: false 而已有 tab 未继承,则修正
157
+ const correctedTab = menuItem.closable === false && existingTab.closable !== false ? {
158
+ ...existingTab,
159
+ closable: false
160
+ } : existingTab;
161
+ const updatedTabsList = correctedTab !== existingTab ? prevState.tabsList.map(t => t.id === existingTab.id ? correctedTab : t) : prevState.tabsList;
162
+ return {
163
+ ...prevState,
164
+ tabsList: updatedTabsList,
165
+ activeKey: existingTab.id,
166
+ activeTabInfo: correctedTab,
167
+ activeComponent: existingTab.menuItem?.code || existingTab.id
168
+ };
173
169
  }
174
-
175
- // 检查是否超出限制
176
170
  if (checkTabLimit(prevState.tabsList, finalConfig.max)) {
177
- // 达到最大值时,不添加新标签页,显示提示信息
178
- const messageText = formatMessage(locale.ProLayout.tabMaxLimitMessage, {
171
+ message.info(formatMessage(locale.ProLayout.tabMaxLimitMessage, {
179
172
  max: finalConfig.max
180
- });
181
- message.info(messageText);
173
+ }));
182
174
  return prevState;
183
175
  }
184
-
185
- // 创建新标签页
176
+ const existingIds = prevState.tabsList.map(tab => tab.id);
177
+ const tabId = generateTabId(menuItem, existingIds);
186
178
  const newTab = createTabFromMenu(menuItem, prevState.newTabIndex);
187
179
  newTab.id = tabId;
188
- const newState = {
180
+ return {
189
181
  ...prevState,
190
182
  tabsList: [...prevState.tabsList, newTab],
191
183
  activeKey: tabId,
@@ -193,7 +185,6 @@ export const useTabsState = options => {
193
185
  newTabIndex: prevState.newTabIndex + 1,
194
186
  activeComponent: menuItem.code || tabId
195
187
  };
196
- return newState;
197
188
  });
198
189
  }, [finalConfig]);
199
190
 
@@ -319,17 +310,18 @@ export const useTabsState = options => {
319
310
  shouldSkipSaveRef.current = true;
320
311
  clearCache();
321
312
 
322
- // 如果关闭后没有标签页了,直接调用 onTabChange
323
- if (newTabs.length === 0 && finalConfig.onTabChange) {
313
+ // 直接调用 onTabChange(useEffect 因 shouldSkipSaveRef 会跳过)
314
+ if (finalConfig.onTabChange) {
315
+ const firstTab = newTabs.length > 0 ? newTabs[0] : undefined;
324
316
  setTimeout(() => {
325
- finalConfig.onTabChange('', undefined, []);
317
+ finalConfig.onTabChange(firstTab?.id ?? '', firstTab, newTabs);
326
318
  }, 0);
327
319
  }
328
320
  return {
329
321
  ...prevState,
330
322
  tabsList: newTabs,
331
323
  activeKey: newActiveKey,
332
- activeTabInfo: undefined,
324
+ activeTabInfo: newTabs.length > 0 ? newTabs[0] : undefined,
333
325
  activeComponent: newActiveComponent
334
326
  };
335
327
  });
@@ -365,40 +357,23 @@ export const useTabsState = options => {
365
357
  // 监听 URL 变化并同步 Tabs (仅在初始化和 location 变化时)
366
358
  // 放在最后以确保可以使用 addTab 和 switchTab
367
359
  useEffect(() => {
368
- // 确保数据源存在
369
360
  if (!dataSource || !('menus' in dataSource)) return;
370
361
  const currentPath = window.location.pathname;
371
-
372
- // 如果当前没有 activeKey 或者 activeKey 对应的 URL 与当前 URL 不一致
373
362
  const activeTab = state.tabsList.find(tab => tab.id === state.activeKey);
374
-
375
- // 如果当前已经在正确的 Tab 上,无需处理
376
363
  if (activeTab && activeTab.url === currentPath) return;
377
-
378
- // 在菜单中查找当前 URL 对应的菜单项
379
364
  const flatMenus = flattenMenuData(dataSource.menus || []);
380
365
  const targetMenu = flatMenus.find(item => getMenuRoute(item) === currentPath);
381
- if (targetMenu) {
382
- // 非叶子节点默认不入签,仅根节点特例可入签
383
- if (!canOpenAsTab(targetMenu)) {
384
- return;
385
- }
386
-
387
- // 检查是否已在 Tabs 中
388
- const existingTab = state.tabsList.find(tab => tab.menuItem?.code === targetMenu.code || tab.url === getMenuRoute(targetMenu));
389
- if (existingTab) {
390
- // 如果已存在,切换到该 Tab
391
- if (state.activeKey !== existingTab.id) {
392
- switchTab(existingTab.id);
393
- }
394
- } else {
395
- // 如果不存在,添加新 Tab
396
- addTab(targetMenu);
397
- }
366
+ if (!targetMenu || !canOpenAsTab(targetMenu)) return;
367
+ const existingTab = state.tabsList.find(tab => tab.menuItem?.code === targetMenu.code || tab.url === getMenuRoute(targetMenu));
368
+ if (existingTab) {
369
+ if (state.activeKey !== existingTab.id) switchTab(existingTab.id);
370
+ return;
398
371
  }
372
+ addTab(targetMenu);
399
373
  }, [dataSource, state.tabsList, state.activeKey, addTab, switchTab]);
400
374
  return {
401
375
  state,
376
+ isInitialized,
402
377
  addTab,
403
378
  canAddTab,
404
379
  removeTab,
@@ -1,6 +1,7 @@
1
- import { useCallback, useMemo, forwardRef, useImperativeHandle, useState, useEffect } from 'react';
1
+ import { useCallback, useMemo, forwardRef, useImperativeHandle, useState, useEffect, useRef } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { useTabsState } from "./hooks/useTabsState";
4
+ import { flattenMenuData } from "./utils";
4
5
  import { TabItemComponent } from "./components/TabItem";
5
6
  import { TabsHeader } from "./components/TabsHeader";
6
7
  import { TabsContext } from "./components/TabsContext";
@@ -19,6 +20,7 @@ const TabsManager = /*#__PURE__*/forwardRef(({
19
20
  }, ref) => {
20
21
  const {
21
22
  state,
23
+ isInitialized,
22
24
  addTab,
23
25
  canAddTab,
24
26
  removeTab,
@@ -52,6 +54,29 @@ const TabsManager = /*#__PURE__*/forwardRef(({
52
54
  onTabsChange?.(state.tabsList.length > 0);
53
55
  }, [state.tabsList.length, onTabsChange]);
54
56
 
57
+ // 处理声明式固定标签页:config.fixed 中的 code 在初始化完成后自动添加,强制 closable=false
58
+ // 用 ref 持有 addTab,避免 addTab 引用变化引起无限循环
59
+ const addTabRef = useRef(addTab);
60
+ addTabRef.current = addTab;
61
+ useEffect(() => {
62
+ if (!isInitialized || !config.fixed || config.fixed.length === 0) return;
63
+ const sourceMenus = Array.isArray(dataSource?.menus) ? flattenMenuData(dataSource.menus) : [];
64
+ config.fixed.forEach(code => {
65
+ const matchedMenu = sourceMenus.find(item => item.code === code);
66
+ const menuItem = matchedMenu ? {
67
+ ...matchedMenu,
68
+ closable: false
69
+ } : {
70
+ id: Date.now(),
71
+ code,
72
+ name: code,
73
+ url: `/${code}`,
74
+ closable: false
75
+ };
76
+ addTabRef.current(menuItem);
77
+ });
78
+ }, [isInitialized, config.fixed, dataSource]);
79
+
55
80
  // 处理菜单点击 - 拦截原有的菜单点击逻辑
56
81
  const handleMenuClick = useCallback(params => {
57
82
  if (params.item) {
@@ -68,7 +93,14 @@ const TabsManager = /*#__PURE__*/forwardRef(({
68
93
  name,
69
94
  extra
70
95
  } = params;
71
- const menuItem = {
96
+ const sourceMenus = Array.isArray(dataSource?.menus) ? flattenMenuData(dataSource.menus) : [];
97
+ const matchedMenu = sourceMenus.find(item => item.code === code);
98
+ const menuItem = matchedMenu ? {
99
+ ...matchedMenu,
100
+ // 允许外部覆盖展示名称和附加数据,但保留菜单原始 closable/url 等配置
101
+ name: name || matchedMenu.name,
102
+ extra: extra ?? matchedMenu.extra
103
+ } : {
72
104
  id: Date.now(),
73
105
  code,
74
106
  name,
@@ -83,7 +115,7 @@ const TabsManager = /*#__PURE__*/forwardRef(({
83
115
  activeTabInfo: state.tabsList.find(tab => tab.id === state.activeKey),
84
116
  activeComponent: state.activeComponent
85
117
  })
86
- }), [addTab, removeTab, state.tabsList, state.activeKey, state.activeComponent]);
118
+ }), [addTab, removeTab, state.tabsList, state.activeKey, state.activeComponent, dataSource]);
87
119
  const handleTabChange = useCallback(activeKey => {
88
120
  switchTab(activeKey);
89
121
  }, [switchTab]);
@@ -164,8 +196,9 @@ const TabsManager = /*#__PURE__*/forwardRef(({
164
196
  }) : null;
165
197
  useImperativeHandle(ref, () => ({
166
198
  handleMenuClick,
167
- canAddTab
168
- }), [handleMenuClick, canAddTab]);
199
+ canAddTab,
200
+ getTabInfo: tabsInstance.getTabInfo
201
+ }), [handleMenuClick, canAddTab, tabsInstance]);
169
202
  return /*#__PURE__*/_jsxs(TabsContext.Provider, {
170
203
  value: tabsInstance,
171
204
  children: [tabsBarContainer ? /*#__PURE__*/createPortal(tabsHeaderNode, tabsBarContainer) : tabsHeaderNode, /*#__PURE__*/_jsx("div", {
@@ -51,6 +51,8 @@ export interface UseTabsStateOptions {
51
51
  export interface UseTabsStateReturn {
52
52
  /** 当前状态 */
53
53
  state: TabsState;
54
+ /** 缓存是否已恢复完成 */
55
+ isInitialized: boolean;
54
56
  /** 添加标签页 */
55
57
  addTab: (menuItem: MenusType, options?: AddTabOptions) => void;
56
58
  /** 检查是否可以添加标签页 */
@@ -50,6 +50,6 @@ export declare const isLeafMenuItem: (menuItem: MenusType) => boolean;
50
50
  export declare const isRootMenuItem: (menuItem: MenusType) => boolean;
51
51
  /**
52
52
  * 判断菜单项是否可以在 Tabs 模式下打开
53
- * 规则:叶子节点可打开;有 children 时仅根节点可打开
53
+ * 规则:只有叶子节点(无 children)才可打开
54
54
  */
55
55
  export declare const canOpenAsTab: (menuItem: MenusType) => boolean;
@@ -11,7 +11,7 @@ export const getMenuRoute = menuItem => {
11
11
  */
12
12
  export const createTabFromMenu = (menuItem, index = 0) => {
13
13
  const route = getMenuRoute(menuItem);
14
- return {
14
+ const result = {
15
15
  id: String(menuItem.id || menuItem.code || menuItem.url || index),
16
16
  code: menuItem.code,
17
17
  name: menuItem.name,
@@ -21,6 +21,7 @@ export const createTabFromMenu = (menuItem, index = 0) => {
21
21
  menuItem,
22
22
  icon: menuItem.icon || menuItem.imgUrl
23
23
  };
24
+ return result;
24
25
  };
25
26
 
26
27
  /**
@@ -134,13 +135,10 @@ export const isRootMenuItem = menuItem => {
134
135
 
135
136
  /**
136
137
  * 判断菜单项是否可以在 Tabs 模式下打开
137
- * 规则:叶子节点可打开;有 children 时仅根节点可打开
138
+ * 规则:只有叶子节点(无 children)才可打开
138
139
  */
139
140
  export const canOpenAsTab = menuItem => {
140
141
  if (!menuItem) return false;
141
142
  if (!(menuItem.url || menuItem.redirectUrl)) return false;
142
- if (isLeafMenuItem(menuItem)) {
143
- return true;
144
- }
145
- return isRootMenuItem(menuItem);
143
+ return isLeafMenuItem(menuItem);
146
144
  };
@@ -82,9 +82,22 @@ const ProLayout = props => {
82
82
  menus: menuData
83
83
  });
84
84
  }, [dataSource]);
85
- const menuDataSource = useMemo(() => Array.isArray(menus) ? {
86
- menus
87
- } : menus, [menus]);
85
+ const menuDataSource = useMemo(() => {
86
+ // menus 已通过 useDeepCompareEffect 填充,直接使用
87
+ if (Array.isArray(menus) && menus.length > 0) {
88
+ return {
89
+ menus
90
+ };
91
+ }
92
+ // menus state 尚未填充(初始为 []),直接用 dataSource 原始 menus,
93
+ // 确保 tabsInstance 第一次创建时就能查到完整菜单数据(含 closable 等属性)
94
+ if (!Array.isArray(dataSource) && Array.isArray(dataSource?.menus)) {
95
+ return dataSource;
96
+ }
97
+ return {
98
+ menus: menus || []
99
+ };
100
+ }, [menus, dataSource]);
88
101
  const enhancedTabsConfig = useMemo(() => {
89
102
  if (!isTabsLayout || !tabs) {
90
103
  return tabs;
@@ -138,9 +151,9 @@ const ProLayout = props => {
138
151
  return true;
139
152
  }
140
153
 
141
- // 根节点可入签;中间层级(有 children)不激活;叶子入签
154
+ // 只有叶子节点(无 children)可入签;有 children 的节点不激活
142
155
  if (!canOpenAsTab(params.item)) {
143
- return true;
156
+ return false;
144
157
  }
145
158
 
146
159
  // 可入签节点:检查是否可以添加标签页(检查 max 限制、_blank 等)
@@ -429,6 +429,11 @@ export interface TabsConfig {
429
429
  tab: TabItem;
430
430
  tabs: TabItem[];
431
431
  }) => void;
432
+ /**
433
+ * @description 声明式固定标签页,传入菜单 code 数组,这些 tab 会在初始化时自动添加且不可关闭
434
+ * @example fixed: ['Workspace', 'PolicyInput']
435
+ */
436
+ fixed?: string[];
432
437
  }
433
438
  export type ProLayoutMode = 'normal' | 'tabs';
434
439
  export interface ProLayoutTabsProps extends ProLayoutBaseProps {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zat-design/sisyphus-react",
3
- "version": "4.3.0-beta.8",
3
+ "version": "4.3.0-beta.9",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "es",