@zat-design/sisyphus-react 4.3.0-beta.6 → 4.3.0-beta.7

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.
@@ -66,7 +66,6 @@ const RenderField = ({
66
66
  const {
67
67
  mode,
68
68
  cellName,
69
- cellNamePath,
70
69
  _isEditing,
71
70
  form,
72
71
  setState,
@@ -89,11 +88,7 @@ const RenderField = ({
89
88
  let _disabled = false;
90
89
  let _desensitization = desensitization || [];
91
90
  let _component = component || editRender;
92
- const isCell = mode === 'cell';
93
91
  const isSingleMode = mode === 'single';
94
- if (isCell) {
95
- record['is-view'] = !_isEqual(cellNamePath, cellName);
96
- }
97
92
 
98
93
  // type类型 首字母转大写
99
94
  type = type?.replace?.(type[0], type[0].toUpperCase()) || 'Input';
@@ -512,12 +507,6 @@ const RenderField = ({
512
507
  }
513
508
  }
514
509
  }
515
- if (isCell) {
516
- await form.validateFields([cellName]);
517
- setState({
518
- cellNamePath: []
519
- });
520
- }
521
510
  // 单行编辑时需要 强制更新视图,仅对当前行打标记
522
511
  compatStartTransition(() => {
523
512
  setState({
@@ -526,7 +515,7 @@ const RenderField = ({
526
515
  }
527
516
  });
528
517
  });
529
- }, [onBlur, formatArgs, namePath, index, form, TargetComponent, validateTrigger, debounceValidate, isCell, cellName, setState]);
518
+ }, [onBlur, formatArgs, namePath, index, form, TargetComponent, validateTrigger, debounceValidate, cellName, setState]);
530
519
 
531
520
  // 使用useMemo优化componentProps对象
532
521
  let componentProps = useMemo(() => ({
@@ -556,20 +545,9 @@ const RenderField = ({
556
545
  }
557
546
  }), [cellName, _fieldProps, TargetComponent?.props, namePath, index, _disabled, _onChange, _onblur, confirm, _desensitization, names, originalName, viewEmpty, _valueType, isView, otherProps?.desensitizationKey]);
558
547
  componentProps = _omit(componentProps, ['onFieldChange', 'namePath', 'index', ...OMIT_FORM_ITEM_AND_DOM_KEYS]);
559
-
560
- // 单元格编辑时,设置各个单元格disabled属性
561
- if (isCell) {
562
- record[`${dataIndex}-Disabled`] = _disabled;
563
- }
564
548
  if (['Switch', 'SwitchCheckbox'].includes(type)) {
565
549
  _formItemProps.valuePropName = 'checked';
566
550
  }
567
- useEffect(() => {
568
- if (isCell) {
569
- const editingDom = document.getElementById(cellNamePath.join('_'));
570
- editingDom?.focus?.();
571
- }
572
- }, [cellNamePath, isCell]);
573
551
 
574
552
  // 可编辑表格默认关闭scrollFollowParent
575
553
  if (['Select', 'ProSelect', 'ProEnum'].includes(type)) {
@@ -81,8 +81,6 @@ const ProEditTable = ({
81
81
  return themeConfig?.data?.zauiFormAlign ?? configRequiredAlign ?? 'left';
82
82
  }, [themeConfig?.data?.zauiFormAlign, configRequiredAlign]);
83
83
  const [state, setState] = useSetState({
84
- cellNamePath: [],
85
- // hover模式下正在编辑的单元格路径
86
84
  forceUpdate: {},
87
85
  // 表格内部强制刷新开关
88
86
  editingKeys: [],
@@ -99,7 +97,6 @@ const ProEditTable = ({
99
97
  }
100
98
  });
101
99
  const {
102
- cellNamePath,
103
100
  forceUpdate,
104
101
  editingKeys,
105
102
  virtualKey,
@@ -186,7 +183,6 @@ const ProEditTable = ({
186
183
  isView,
187
184
  viewEmpty,
188
185
  disabled,
189
- cellNamePath,
190
186
  forceUpdate,
191
187
  insertType,
192
188
  emptyBtnText,
@@ -221,7 +217,7 @@ const ProEditTable = ({
221
217
  getIsNew,
222
218
  handlePageChange,
223
219
  ...resetProps
224
- }), [actionDirection, actionProps, actionWidth, cellNamePath, deletePoConfirmMsg, disabled, diffConfig, editingKeys, emptyBtnText, form,
220
+ }), [actionDirection, actionProps, actionWidth, deletePoConfirmMsg, diffConfig, editingKeys, emptyBtnText, form,
225
221
  // forceUpdate 不应该作为依赖项,因为它是触发刷新的信号,而非用于比较的数据
226
222
  getIsNew, handlePageChange, insertType, isView, max, mode, mulDeletePoConfirmMsg, name, namePath, onlyOneLineMsg, originalValues, prefixCls, requiredAlign, resetProps, rowDisabled, selectedRowKeys, selectedRows, shouldUpdateDebounce, tableRef, toolbarProps, value?.length, viewEmpty, virtualKey, page]);
227
223
 
@@ -317,7 +313,7 @@ const ProEditTable = ({
317
313
  };
318
314
  const _columns = useMemo(() => {
319
315
  return transformColumns(columns, config);
320
- }, [disabled, forceUpdate, columns, page, actionProps, editingKeys, cellNamePath, config]);
316
+ }, [disabled, forceUpdate, columns, page, actionProps, editingKeys, config]);
321
317
  const initDataSource = () => {
322
318
  // 检查每一项是否有 rowKey 或通过 rowKey 字段获取的 key
323
319
  const isAllHasKey = value?.every?.(item => item.rowKey || typeof rowKey === 'string' && item[rowKey]);
@@ -194,8 +194,6 @@ export type ProEditTableColumnsProps<K = any> = ProColumnsProps<K, 'ProEditTable
194
194
  * 状态接口
195
195
  */
196
196
  export interface StateProps {
197
- /** 单元格名称路径 */
198
- cellNamePath: NamePath[];
199
197
  /** 表格内部强制刷新开关 */
200
198
  forceUpdate: Record<string, any>;
201
199
  /** 正在编辑行的row-key集合 */
@@ -232,7 +230,7 @@ export type ProEditTableRefType = ProEditTableRefProps;
232
230
  /**
233
231
  * 可编辑表格模式类型
234
232
  */
235
- export type ProEditTableMode = 'single' | 'multiple' | 'cell';
233
+ export type ProEditTableMode = 'single' | 'multiple';
236
234
  export type ProEditTableModeType = ProEditTableMode;
237
235
  /**
238
236
  * 表格插入类型
@@ -76,20 +76,6 @@
76
76
  left: -9999px;
77
77
  }
78
78
 
79
- .is-cell
80
- .@{ant-prefix}-form-item
81
- .@{ant-prefix}-form-item-row
82
- .@{ant-prefix}-form-item-control
83
- .@{ant-prefix}-form-item-control-input {
84
- border: 1px solid transparent;
85
-
86
- &:hover {
87
- border: 1px solid #dee0e3;
88
- border-radius: var(--zaui-border-radius, 8px);
89
- text-indent: 5px;
90
- }
91
- }
92
-
93
79
  .pro-edit-table-drag-wrapper {
94
80
  display: flex;
95
81
  align-items: center;
@@ -185,7 +185,6 @@ export const transformColumns = (columns = [], config) => {
185
185
  form,
186
186
  name,
187
187
  disabled,
188
- cellNamePath,
189
188
  editingKeys,
190
189
  virtualKey,
191
190
  requiredAlign,
@@ -193,7 +192,6 @@ export const transformColumns = (columns = [], config) => {
193
192
  setState,
194
193
  page
195
194
  } = config;
196
- const isCell = mode === 'cell';
197
195
  let nextColumns = columns.map(item => cloneDeepFilterNode(item));
198
196
  const pageNum = tools.calc(page.pageNum, '-', 1);
199
197
  const firstIndex = tools.calc(pageNum, '*', page.pageSize);
@@ -304,25 +302,9 @@ export const transformColumns = (columns = [], config) => {
304
302
  isResponsiveWidth: true
305
303
  });
306
304
  }
307
- let cellClassName;
308
- const isCanEdit = isCell && !record[`${columnName}-Disabled`] && item.isEditable !== false;
309
- // 单元格编辑场景下,需要将所有单元格设置为不可编辑状态,当鼠标hover时,展示输出框。点击可修改
310
- if (isCanEdit) {
311
- record['is-view'] = true;
312
- // 样式处理
313
- cellClassName = classnames({
314
- 'is-cell': !_isEqual(cellNamePath, cellName)
315
- });
316
- }
317
305
 
318
306
  // 单行多行交互有较大差异
319
307
  return /*#__PURE__*/_jsxs("div", {
320
- className: cellClassName,
321
- onClick: () => {
322
- isCanEdit && setState({
323
- cellNamePath: cellName
324
- });
325
- },
326
308
  children: [index === 0 && !item.width ? /*#__PURE__*/_jsx("div", {
327
309
  style: {
328
310
  visibility: 'hidden',
@@ -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
- children?: React.ReactNode | ((form: FormInstance<any>) => React.ReactNode);
79
- trim?: boolean;
80
- normalize?: (value: any, prevValue: any, allValues: import("@rc-component/form/lib/interface").Store) => any;
78
+ status?: "" | "success" | "warning" | "error" | "validating";
81
79
  className?: string;
82
- style?: React.CSSProperties;
83
- preserve?: boolean;
84
- id?: string;
85
80
  hidden?: boolean;
81
+ id?: string;
82
+ style?: React.CSSProperties;
83
+ children?: React.ReactNode | ((form: FormInstance<any>) => React.ReactNode);
86
84
  onReset?: () => void;
87
85
  prefixCls?: string;
88
86
  rootClassName?: string;
89
- layout?: import("antd/es/form/Form").FormItemLayout;
90
- help?: React.ReactNode;
91
- vertical?: boolean;
87
+ colon?: boolean;
92
88
  htmlFor?: string;
93
- trigger?: string;
94
- status?: "" | "warning" | "error" | "success" | "validating";
95
- isView?: boolean;
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;
100
89
  labelAlign?: import("antd/es/form/interface").FormLabelAlign;
101
- colon?: boolean;
102
90
  labelCol?: import("antd").ColProps;
103
- wrapperCol?: import("antd").ColProps;
91
+ vertical?: boolean;
104
92
  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;
105
94
  shouldUpdate?: import("@rc-component/form/lib/Field").ShouldUpdate<any>;
95
+ trigger?: string;
96
+ validateTrigger?: string | false | string[];
106
97
  validateDebounce?: number;
107
98
  valuePropName?: string;
99
+ getValueProps?: ((value: any) => Record<string, unknown>) & ((value: any) => Record<string, unknown>);
108
100
  messageVariables?: Record<string, string>;
109
101
  initialValue?: any;
110
102
  onMetaChange?: (meta: import("@rc-component/form/lib/Field").MetaEvent) => void;
103
+ preserve?: boolean;
111
104
  isListField?: boolean;
112
105
  isList?: boolean;
113
106
  noStyle?: boolean;
114
107
  hasFeedback?: boolean | {
115
108
  icons: import("antd/es/form/FormItem").FeedbackIcons;
116
109
  };
117
- validateStatus?: "" | "warning" | "error" | "success" | "validating";
110
+ validateStatus?: "" | "success" | "warning" | "error" | "validating";
111
+ layout?: import("antd/es/form/Form").FormItemLayout;
112
+ wrapperCol?: import("antd").ColProps;
113
+ help?: React.ReactNode;
118
114
  fieldId?: string;
119
115
  valueType?: import("../../../render/propsType").ProFormValueType;
120
- switchValue?: [any, any];
121
116
  viewRender?: (value: any, record: any, { form, index, namePath, }: {
122
117
  [key: string]: any;
123
118
  form: FormInstance<any>;
124
119
  index?: number;
125
120
  }) => string | React.ReactElement<any, any>;
121
+ desensitization?: [number, number] | ReactiveFunction<any, [number, number]>;
122
+ isView?: boolean;
123
+ switchValue?: [any, any];
126
124
  viewType?: import("../../../render/propsType").ViewType;
125
+ trim?: boolean;
127
126
  upperCase?: boolean;
128
127
  toISOString?: boolean;
129
128
  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, "format" | "clearNotShow" | "valueType" | "switchValue" | "dependNames" | "toISOString" | "toCSTString" | "precision">;
144
+ componentProps: import("lodash").Omit<any, "valueType" | "precision" | "format" | "switchValue" | "dependNames" | "toISOString" | "toCSTString" | "clearNotShow">;
145
145
  formItemTransform: {
146
146
  getValueProps: any;
147
147
  normalize: any;
@@ -1,5 +1,5 @@
1
1
  import { Form } from 'antd';
2
- import { useContext, useLayoutEffect } from 'react';
2
+ import { useContext } from 'react';
3
3
  import { filterInternalFields, getFormFieldPaths, mergeNames, isListForm } from "./index";
4
4
  import { handleScrollToError as handleScrollToErrorProEditTable } from "../../ProEditTable/utils/tools";
5
5
  import { FormsContext } from "../../FormsProvider";
@@ -19,15 +19,17 @@ export const useForm = function (originForm, options) {
19
19
 
20
20
  // formKey全局共享逻辑
21
21
  const forms = useContext(FormsContext);
22
- const [form] = Form.useForm(forms[formKey] || _originForm);
23
-
24
- // 使用 useLayoutEffect 在浏览器绘制前同步注册 form 实例,避免 React 19 StrictMode 警告
25
- useLayoutEffect(() => {
26
- if (formKey && forms[formKey] !== form) {
27
- forms[formKey] = form;
28
- form.formKey = formKey;
29
- }
30
- }, [formKey, form, forms]);
22
+ const [localForm] = Form.useForm(_originForm);
23
+ // 同步注册共享实例,确保同一渲染流程中相同 formKey 复用同一个实例
24
+ if (formKey && !forms[formKey]) {
25
+ forms[formKey] = localForm;
26
+ localForm.formKey = formKey;
27
+ }
28
+ const sharedForm = formKey ? forms[formKey] : undefined;
29
+ const form = sharedForm || localForm;
30
+ if (formKey && form.formKey !== formKey) {
31
+ form.formKey = formKey;
32
+ }
31
33
  const {
32
34
  getFieldsValue,
33
35
  validateFields,
@@ -109,7 +109,7 @@ const FoldMenu = props => {
109
109
  },
110
110
  placement: "rightTop",
111
111
  children: /*#__PURE__*/_jsx("span", {
112
- onClick: () => {
112
+ onClick: e => {
113
113
  // 查找完整的菜单项数据
114
114
  const menuItem = findMenuItemByKey(menus, String(id));
115
115
  const menuKeyPath = menuItem?.keyIdPath ? menuItem.keyIdPath.map(id => String(id)) : [String(id)];
@@ -141,7 +141,7 @@ const FoldMenu = props => {
141
141
  placement: "right",
142
142
  title: name,
143
143
  children: /*#__PURE__*/_jsx("span", {
144
- onClick: () => {
144
+ onClickCapture: e => {
145
145
  // 查找完整的菜单项数据
146
146
  const menuItem = findMenuItemByKey(menus, String(id));
147
147
  const menuKeyPath = menuItem?.keyIdPath ? menuItem.keyIdPath.map(id => String(id)) : [String(id)];
@@ -165,6 +165,10 @@ const FoldMenu = props => {
165
165
  onSelected({
166
166
  selectedPath: toPath
167
167
  });
168
+ } else {
169
+ // Capture 阶段拦截,早于 Link 内部导航处理器
170
+ e.preventDefault();
171
+ e.stopPropagation();
168
172
  }
169
173
  },
170
174
  children: /*#__PURE__*/_jsx(Link, {
@@ -173,7 +177,7 @@ const FoldMenu = props => {
173
177
  })
174
178
  })
175
179
  }, `${id}-${name}`) : /*#__PURE__*/_jsx("span", {
176
- onClick: () => {
180
+ onClickCapture: e => {
177
181
  // 查找完整的菜单项数据
178
182
  const menuItem = findMenuItemByKey(menus, String(id));
179
183
  const menuKeyPath = menuItem?.keyIdPath ? menuItem.keyIdPath.map(id => String(id)) : [String(id)];
@@ -197,6 +201,10 @@ const FoldMenu = props => {
197
201
  onSelected({
198
202
  selectedPath: toPath
199
203
  });
204
+ } else {
205
+ // Capture 阶段拦截,早于 Link 内部导航处理器
206
+ e.preventDefault();
207
+ e.stopPropagation();
200
208
  }
201
209
  },
202
210
  children: /*#__PURE__*/_jsx(Link, {
@@ -7,7 +7,7 @@ import classnames from 'classnames';
7
7
  import { Link as RouterLink } from 'react-router-dom';
8
8
  import { LayoutContext } from "../../../../index";
9
9
  import { getIdsByPathName, findMenuItemByKey } from "../../../../utils";
10
- import { isLeafMenuItem } from "../../../TabsManager/utils";
10
+ import { canOpenAsTab } from "../../../TabsManager/utils";
11
11
  import { Icon } from "../../index";
12
12
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  const Link = RouterLink;
@@ -163,9 +163,9 @@ const OpenMenu = props => {
163
163
  }
164
164
  }
165
165
 
166
- // 只有最后一级菜单(叶子节点)才设置选中状态和路径
166
+ // 可入签节点(叶子 + 根节点特例)才设置选中状态和路径
167
167
  // 并且只有在 shouldActivate 为 true 时才设置
168
- if (menuItem && isLeafMenuItem(menuItem) && shouldActivate) {
168
+ if (menuItem && canOpenAsTab(menuItem) && shouldActivate) {
169
169
  setState({
170
170
  selectedKeys: keyPath,
171
171
  router: item?.props?.router
@@ -4,7 +4,7 @@ import { arrayMove } from '@dnd-kit/sortable';
4
4
  import { DEFAULT_TABS_CONFIG } from "../propTypes";
5
5
  import { useTabsCache } from "./useTabsCache";
6
6
  import locale, { formatMessage } from "../../../../locale";
7
- import { createTabFromMenu, generateTabId, shouldOpenExternal, handleExternalOpen, checkTabLimit, getRightTabs, flattenMenuData, isLeafMenuItem } from "../utils";
7
+ import { createTabFromMenu, generateTabId, shouldOpenExternal, handleExternalOpen, checkTabLimit, getRightTabs, flattenMenuData, canOpenAsTab, getMenuRoute } from "../utils";
8
8
 
9
9
  /**
10
10
  * 标签页状态管理Hook
@@ -107,8 +107,8 @@ export const useTabsState = options => {
107
107
  forceNew = false
108
108
  } = options || {};
109
109
 
110
- // 只有最后一级菜单(叶子节点)才能添加标签页
111
- if (!isLeafMenuItem(menuItem)) {
110
+ // 非叶子节点默认不入签,仅根节点特例可入签
111
+ if (!canOpenAsTab(menuItem)) {
112
112
  return false;
113
113
  }
114
114
 
@@ -119,7 +119,8 @@ export const useTabsState = options => {
119
119
 
120
120
  // 如果 forceNew = false(默认),检查是否已存在相同的标签页
121
121
  if (!forceNew) {
122
- const existingTab = state.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuItem.url);
122
+ const menuRoute = getMenuRoute(menuItem);
123
+ const existingTab = state.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuRoute);
123
124
  if (existingTab) {
124
125
  // 如果已存在,可以添加(会切换到已存在的标签页)
125
126
  return true;
@@ -142,8 +143,8 @@ export const useTabsState = options => {
142
143
  forceNew = false
143
144
  } = options || {};
144
145
  setState(prevState => {
145
- // 只有最后一级菜单(叶子节点)才能添加标签页
146
- if (!isLeafMenuItem(menuItem)) {
146
+ // 非叶子节点默认不入签,仅根节点特例可入签
147
+ if (!canOpenAsTab(menuItem)) {
147
148
  return prevState;
148
149
  }
149
150
 
@@ -157,7 +158,8 @@ export const useTabsState = options => {
157
158
 
158
159
  // 如果 forceNew = false(默认),检查是否已存在相同的标签页
159
160
  if (!forceNew) {
160
- const existingTab = prevState.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuItem.url);
161
+ const menuRoute = getMenuRoute(menuItem);
162
+ const existingTab = prevState.tabsList.find(tab => tab.menuItem?.code === menuItem.code || tab.url === menuRoute);
161
163
  if (existingTab) {
162
164
  // 如果已存在,直接切换到该标签页
163
165
  const newState = {
@@ -375,15 +377,15 @@ export const useTabsState = options => {
375
377
 
376
378
  // 在菜单中查找当前 URL 对应的菜单项
377
379
  const flatMenus = flattenMenuData(dataSource.menus || []);
378
- const targetMenu = flatMenus.find(item => item.url === currentPath);
380
+ const targetMenu = flatMenus.find(item => getMenuRoute(item) === currentPath);
379
381
  if (targetMenu) {
380
- // 只有最后一级菜单(叶子节点)才能添加标签页
381
- if (!isLeafMenuItem(targetMenu)) {
382
+ // 非叶子节点默认不入签,仅根节点特例可入签
383
+ if (!canOpenAsTab(targetMenu)) {
382
384
  return;
383
385
  }
384
386
 
385
387
  // 检查是否已在 Tabs 中
386
- const existingTab = state.tabsList.find(tab => tab.menuItem?.code === targetMenu.code || tab.url === targetMenu.url);
388
+ const existingTab = state.tabsList.find(tab => tab.menuItem?.code === targetMenu.code || tab.url === getMenuRoute(targetMenu));
387
389
  if (existingTab) {
388
390
  // 如果已存在,切换到该 Tab
389
391
  if (state.activeKey !== existingTab.id) {
@@ -1,4 +1,9 @@
1
1
  import { TabItem, MenusType } from '../../../propTypes';
2
+ /**
3
+ * 获取菜单实际用于路由/签页的路径
4
+ * 约定优先使用 redirectUrl,其次回退 url
5
+ */
6
+ export declare const getMenuRoute: (menuItem: MenusType) => string;
2
7
  /**
3
8
  * 根据菜单项生成TabItem
4
9
  */
@@ -39,3 +44,12 @@ export declare const flattenMenuData: (menus?: MenusType[]) => MenusType[];
39
44
  * 只有当菜单项没有子菜单或子菜单为空时,才认为是叶子节点
40
45
  */
41
46
  export declare const isLeafMenuItem: (menuItem: MenusType) => boolean;
47
+ /**
48
+ * 判断菜单项是否为根节点
49
+ */
50
+ export declare const isRootMenuItem: (menuItem: MenusType) => boolean;
51
+ /**
52
+ * 判断菜单项是否可以在 Tabs 模式下打开
53
+ * 规则:叶子节点可打开;有 children 时仅根节点可打开
54
+ */
55
+ export declare const canOpenAsTab: (menuItem: MenusType) => boolean;
@@ -1,14 +1,23 @@
1
+ /**
2
+ * 获取菜单实际用于路由/签页的路径
3
+ * 约定优先使用 redirectUrl,其次回退 url
4
+ */
5
+ export const getMenuRoute = menuItem => {
6
+ return menuItem.redirectUrl || menuItem.url || '';
7
+ };
8
+
1
9
  /**
2
10
  * 根据菜单项生成TabItem
3
11
  */
4
12
  export const createTabFromMenu = (menuItem, index = 0) => {
13
+ const route = getMenuRoute(menuItem);
5
14
  return {
6
15
  id: String(menuItem.id || menuItem.code || menuItem.url || index),
7
16
  code: menuItem.code,
8
17
  name: menuItem.name,
9
18
  title: menuItem.name,
10
- url: menuItem.url,
11
- closable: true,
19
+ url: route,
20
+ closable: menuItem.closable !== false,
12
21
  menuItem,
13
22
  icon: menuItem.icon || menuItem.imgUrl
14
23
  };
@@ -34,16 +43,17 @@ export const generateTabId = (menuItem, existingIds) => {
34
43
  * 检查菜单项是否应该外部跳转
35
44
  */
36
45
  export const shouldOpenExternal = (menuItem, target) => {
37
- return target === '_blank' || menuItem.redirectUrl && menuItem.redirectUrl !== menuItem.url || menuItem.type === 'EXTERNAL';
46
+ const menuTarget = menuItem?.target || target;
47
+ return menuTarget === '_blank' || menuItem.type === 'EXTERNAL';
38
48
  };
39
49
 
40
50
  /**
41
51
  * 处理外部跳转
42
52
  */
43
53
  export const handleExternalOpen = menuItem => {
44
- const url = menuItem.redirectUrl || menuItem.url;
54
+ const url = getMenuRoute(menuItem);
45
55
  if (url) {
46
- window.open(url, '_blank');
56
+ window.open(url, menuItem.target || '_blank');
47
57
  }
48
58
  };
49
59
 
@@ -109,4 +119,28 @@ export const isLeafMenuItem = menuItem => {
109
119
  } = menuItem || {};
110
120
  // 如果 children 不存在、为 null、为 undefined,或者为空数组,则认为是叶子节点
111
121
  return !children || Array.isArray(children) && children.length === 0;
122
+ };
123
+
124
+ /**
125
+ * 判断菜单项是否为根节点
126
+ */
127
+ export const isRootMenuItem = menuItem => {
128
+ if (!menuItem) return false;
129
+ if (menuItem.parentId === null || menuItem.parentId === undefined) {
130
+ return true;
131
+ }
132
+ return Array.isArray(menuItem.keyIdPath) && menuItem.keyIdPath.length === 1;
133
+ };
134
+
135
+ /**
136
+ * 判断菜单项是否可以在 Tabs 模式下打开
137
+ * 规则:叶子节点可打开;有 children 时仅根节点可打开
138
+ */
139
+ export const canOpenAsTab = menuItem => {
140
+ if (!menuItem) return false;
141
+ if (!(menuItem.url || menuItem.redirectUrl)) return false;
142
+ if (isLeafMenuItem(menuItem)) {
143
+ return true;
144
+ }
145
+ return isRootMenuItem(menuItem);
112
146
  };
@@ -11,7 +11,7 @@ import { isTabsMode, validateTabsProps } from "./propTypes";
11
11
  import { useProLayoutTabs } from "./components/TabsManager/hooks/useProLayoutTabs";
12
12
  import { useActiveTab } from "./components/TabsManager/hooks/useActiveTab";
13
13
  import { transformMenus } from "./utils";
14
- import { isLeafMenuItem } from "./components/TabsManager/utils";
14
+ import { canOpenAsTab } from "./components/TabsManager/utils";
15
15
  import headerBg from "../assets/header_bg.png";
16
16
 
17
17
  // 全局上下文
@@ -99,7 +99,7 @@ const ProLayout = props => {
99
99
  });
100
100
  return;
101
101
  }
102
- const targetPath = activeTab?.url || activeTab?.menuItem?.router || activeTab?.menuItem?.url || activeTab?.menuItem?.redirectUrl;
102
+ const targetPath = activeTab?.url || activeTab?.menuItem?.router || activeTab?.menuItem?.redirectUrl || activeTab?.menuItem?.url;
103
103
  if (targetPath && targetPath !== selectedPath) {
104
104
  setState({
105
105
  selectedPath: targetPath
@@ -134,13 +134,16 @@ const ProLayout = props => {
134
134
 
135
135
  // tabs 模式:调用 TabsManager 的 handleMenuClick 来添加标签页
136
136
  tabsManagerRef.current?.handleMenuClick?.(params);
137
+ if (!params.item) {
138
+ return true;
139
+ }
137
140
 
138
- // 非叶子节点,直接允许激活
139
- if (!params.item || !isLeafMenuItem(params.item)) {
141
+ // 根节点可入签;中间层级(有 children)不激活;叶子入签
142
+ if (!canOpenAsTab(params.item)) {
140
143
  return true;
141
144
  }
142
145
 
143
- // 叶子节点:检查是否可以添加标签页(检查 max 限制)
146
+ // 可入签节点:检查是否可以添加标签页(检查 max 限制、_blank 等)
144
147
  const canAdd = tabsManagerRef.current?.canAddTab?.(params.item);
145
148
  if (!canAdd) {
146
149
  // 超过限制,返回 false 阻止菜单激活
@@ -148,13 +151,14 @@ const ProLayout = props => {
148
151
  }
149
152
 
150
153
  // 可以添加标签页,设置选中路径(激活菜单)
151
- const targetPath = params.item?.url || params.item?.redirectUrl || params.item?.router;
154
+ const targetPath = params.item?.redirectUrl || params.item?.url || params.item?.router;
152
155
  if (targetPath && targetPath !== selectedPath) {
153
156
  setState({
154
157
  selectedPath: targetPath
155
158
  });
156
159
  }
157
- return true;
160
+ // tabs 模式下由 Tab 管理内容,阻止菜单默认 Link 跳转
161
+ return false;
158
162
  }, [isTabsLayout, onMenuClick, selectedPath]);
159
163
 
160
164
  /**
@@ -124,6 +124,11 @@ export interface MenusType {
124
124
  * @default -
125
125
  */
126
126
  type?: string;
127
+ /**
128
+ * @description 当前菜单点击后的打开方式
129
+ * @default "_self"
130
+ */
131
+ target?: '_blank' | '_parent' | '_self' | '_top';
127
132
  /**
128
133
  * @description 允许扩展字段
129
134
  */
@@ -0,0 +1 @@
1
+ <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1775025459029" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7119" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.1953125" height="200"><path d="M181.10185262 930.4882664c-18.69801175 0-33.42977859-15.29837325-33.42977858-33.42977858 0-18.69801175 15.29837325-33.42977859 33.42977858-33.4297786h691.25982844c18.69801175 0 33.42977859 15.29837325 33.42977859 33.4297786 0 18.69801175-15.29837325 33.42977859-33.42977859 33.42977858h-691.25982844z m671.42860384-747.92047012L752.80772713 82.84506694c-12.46534117-12.46534117-32.86317217-12.46534117-45.32851335 0l-100.85594218 100.85594218 145.05124269 145.05124269 100.85594217-100.85594218c12.46534117-12.46534117 12.46534117-32.29656575 0-45.32851335z m-275.93732495 31.16335293L240.59552637 549.72875434c-9.06570266 9.06570266-16.9981925 28.89692726-16.9981925 28.89692726l-52.12779033 165.44907369c-3.96624492 12.46534117 7.93248983 23.7974695 20.397831 20.397831l164.31586085-52.12779034s19.83122459-7.36588342 29.46353368-17.56479891L721.6443742 358.7823919 576.59313151 213.73114921z" fill="#2C2C2C" p-id="7120"></path></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zat-design/sisyphus-react",
3
- "version": "4.3.0-beta.6",
3
+ "version": "4.3.0-beta.7",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "es",