@zat-design/sisyphus-react 4.5.10-beta.2 → 4.6.0-beta.2

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.
@@ -13,6 +13,7 @@ import {
13
13
  flattenMenuData,
14
14
  canOpenAsTab,
15
15
  getMenuRoute,
16
+ hasRelationKey,
16
17
  findExistingTab,
17
18
  normalizeTabUrl,
18
19
  isSafeExternalUrl,
@@ -28,6 +29,7 @@ const useTabsState = (options) => {
28
29
  const { initialState = {}, config, dataSource, cacheEnabled = true } = options;
29
30
  const finalConfig = { ...DEFAULT_TABS_CONFIG, ...config };
30
31
  const urlSyncEnabled = finalConfig.urlSync === true || typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null;
32
+ const urlSyncIdentity = typeof finalConfig.urlSync === "object" && finalConfig.urlSync !== null ? finalConfig.urlSync.identity || "url" : "url";
31
33
  const [state, setState] = useState(() => ({
32
34
  tabsList: [],
33
35
  activeKey: "",
@@ -62,22 +64,26 @@ const useTabsState = (options) => {
62
64
  }));
63
65
  restoredState.activeKey = restoredState.activeKey === void 0 || restoredState.activeKey === null ? "" : String(restoredState.activeKey);
64
66
  if (urlSyncEnabled) {
65
- const retainedTabByUrl = /* @__PURE__ */ new Map();
67
+ const retainedTabByIdentity = /* @__PURE__ */ new Map();
66
68
  const retainedIdByOriginalId = /* @__PURE__ */ new Map();
67
69
  restoredState.tabsList.forEach((tab) => {
70
+ var _a2;
68
71
  const normalizedUrl = normalizeTabUrl(tab.url);
69
72
  if (!normalizedUrl)
70
73
  return;
71
- const retainedTab = retainedTabByUrl.get(normalizedUrl);
74
+ const relationKey = tab.relationKey ?? ((_a2 = tab.menuItem) == null ? void 0 : _a2.relationKey);
75
+ const identity = urlSyncIdentity === "relationKey" && hasRelationKey(relationKey) ? `relationKey:${typeof relationKey}:${String(relationKey)}` : `url:${normalizedUrl}`;
76
+ const retainedTab = retainedTabByIdentity.get(identity);
72
77
  if (retainedTab) {
78
+ retainedTabByIdentity.set(identity, { ...retainedTab, url: normalizedUrl });
73
79
  retainedIdByOriginalId.set(tab.id, retainedTab.id);
74
80
  return;
75
81
  }
76
82
  const normalizedTab = { ...tab, url: normalizedUrl };
77
- retainedTabByUrl.set(normalizedUrl, normalizedTab);
83
+ retainedTabByIdentity.set(identity, normalizedTab);
78
84
  retainedIdByOriginalId.set(tab.id, normalizedTab.id);
79
85
  });
80
- restoredState.tabsList = Array.from(retainedTabByUrl.values());
86
+ restoredState.tabsList = Array.from(retainedTabByIdentity.values());
81
87
  restoredState.activeKey = retainedIdByOriginalId.get(restoredState.activeKey) || ((_a = restoredState.tabsList[0]) == null ? void 0 : _a.id) || "";
82
88
  }
83
89
  restoredState.activeTabInfo = restoredState.tabsList.find(
@@ -87,7 +93,7 @@ const useTabsState = (options) => {
87
93
  setState(restoredState);
88
94
  }
89
95
  setIsInitialized(true);
90
- }, [restoreFromCache, urlSyncEnabled]);
96
+ }, [restoreFromCache, urlSyncEnabled, urlSyncIdentity]);
91
97
  const lastActiveKeyRef = useRef("");
92
98
  const shouldSkipSaveRef = useRef(false);
93
99
  const initialTabsAppliedRef = useRef(false);
@@ -125,7 +131,8 @@ const useTabsState = (options) => {
125
131
  }
126
132
  if (urlSyncEnabled || !forceNew) {
127
133
  const existingTab = findExistingTab(state.tabsList, menuItem, {
128
- urlSync: urlSyncEnabled
134
+ urlSync: urlSyncEnabled,
135
+ identity: urlSyncIdentity
129
136
  });
130
137
  if (existingTab) {
131
138
  return true;
@@ -136,7 +143,7 @@ const useTabsState = (options) => {
136
143
  }
137
144
  return true;
138
145
  },
139
- [state.tabsList, finalConfig, urlSyncEnabled]
146
+ [state.tabsList, finalConfig, urlSyncEnabled, urlSyncIdentity]
140
147
  );
141
148
  const addTab = useCallback(
142
149
  (menuItem, options2) => {
@@ -161,9 +168,18 @@ const useTabsState = (options) => {
161
168
  return prevState;
162
169
  }
163
170
  const resolvedMenuItem = normalizedUrl ? { ...menuItem, url: normalizedUrl, redirectUrl: void 0 } : menuItem;
164
- const existingTab = urlSyncEnabled || !forceNew ? findExistingTab(prevState.tabsList, resolvedMenuItem, { urlSync: urlSyncEnabled }) : void 0;
171
+ const existingTab = urlSyncEnabled || !forceNew ? findExistingTab(prevState.tabsList, resolvedMenuItem, {
172
+ urlSync: urlSyncEnabled,
173
+ identity: urlSyncIdentity
174
+ }) : void 0;
165
175
  if (existingTab) {
166
- const correctedTab = resolvedMenuItem.closable === false && existingTab.closable !== false ? { ...existingTab, closable: false } : existingTab;
176
+ const shouldFixClosable = resolvedMenuItem.closable === false && existingTab.closable !== false;
177
+ const shouldUpdateUrl = urlSyncIdentity === "relationKey" && normalizedUrl !== void 0 && existingTab.url !== normalizedUrl;
178
+ const correctedTab = shouldFixClosable || shouldUpdateUrl ? {
179
+ ...existingTab,
180
+ ...shouldFixClosable ? { closable: false } : {},
181
+ ...shouldUpdateUrl ? { url: normalizedUrl } : {}
182
+ } : existingTab;
167
183
  const updatedTabsList = correctedTab !== existingTab ? prevState.tabsList.map((t) => t.id === existingTab.id ? correctedTab : t) : prevState.tabsList;
168
184
  return {
169
185
  ...prevState,
@@ -200,7 +216,7 @@ const useTabsState = (options) => {
200
216
  };
201
217
  });
202
218
  },
203
- [finalConfig, urlSyncEnabled]
219
+ [finalConfig, urlSyncEnabled, urlSyncIdentity]
204
220
  );
205
221
  useEffect(() => {
206
222
  const initialTabs = finalConfig.initialTabs;
@@ -257,21 +273,22 @@ const useTabsState = (options) => {
257
273
  (tabId) => {
258
274
  setState((prevState) => {
259
275
  var _a;
260
- const tabToRemove = prevState.tabsList.find((tab) => tab.id === tabId);
276
+ const targetTabId = tabId ?? prevState.activeKey;
277
+ const tabToRemove = prevState.tabsList.find((tab) => tab.id === targetTabId);
261
278
  if (!tabToRemove || !tabToRemove.closable) {
262
279
  return prevState;
263
280
  }
264
- const newTabs = prevState.tabsList.filter((tab) => tab.id !== tabId);
281
+ const newTabs = prevState.tabsList.filter((tab) => tab.id !== targetTabId);
265
282
  let newActiveKey = prevState.activeKey;
266
283
  let newActiveTabInfo = prevState.activeTabInfo;
267
284
  let newActiveComponent = prevState.activeComponent;
268
- if (prevState.activeKey === tabId && newTabs.length > 0) {
269
- const currentIndex = prevState.tabsList.findIndex((tab) => tab.id === tabId);
285
+ if (prevState.activeKey === targetTabId && newTabs.length > 0) {
286
+ const currentIndex = prevState.tabsList.findIndex((tab) => tab.id === targetTabId);
270
287
  const newActiveIndex = Math.min(currentIndex, newTabs.length - 1);
271
288
  newActiveTabInfo = newTabs[newActiveIndex];
272
289
  newActiveKey = newActiveTabInfo.id;
273
290
  newActiveComponent = ((_a = newActiveTabInfo.menuItem) == null ? void 0 : _a.code) || newActiveTabInfo.id;
274
- } else if (prevState.activeKey === tabId) {
291
+ } else if (prevState.activeKey === targetTabId) {
275
292
  newActiveKey = "";
276
293
  newActiveTabInfo = void 0;
277
294
  newActiveComponent = "";
@@ -13,7 +13,7 @@ import { createPortal } from "react-dom";
13
13
  import { useTabsState } from "./hooks/useTabsState";
14
14
  import { useTabsUrlSync } from "./hooks/useTabsUrlSync";
15
15
  import { useIframeRoute } from "./hooks/useIframeRoute";
16
- import { flattenMenuData, resolveTabMenuItem, derivePinned } from "./utils";
16
+ import { flattenMenuData, resolveTabMenuItem, derivePinned, normalizeTabUrl } from "./utils";
17
17
  import { TabItemComponent } from "./components/TabItem";
18
18
  import { TabsHeader } from "./components/TabsHeader";
19
19
  import { TabsContext } from "./components/TabsContext";
@@ -234,6 +234,13 @@ const TabsManager = forwardRef(
234
234
  removeTab(tabId);
235
235
  },
236
236
  updateTab,
237
+ updateTabUrl: (tabId, url) => {
238
+ const normalizedUrl = normalizeTabUrl(url);
239
+ if (!normalizedUrl)
240
+ return;
241
+ markTabInteraction();
242
+ updateTabUrl(tabId, normalizedUrl);
243
+ },
237
244
  getTabInfo: () => ({
238
245
  tabsList: state.tabsList,
239
246
  activeTabInfo: state.tabsList.find((tab) => tab.id === state.activeKey),
@@ -248,6 +255,7 @@ const TabsManager = forwardRef(
248
255
  markTabInteraction,
249
256
  removeTab,
250
257
  updateTab,
258
+ updateTabUrl,
251
259
  state.tabsList,
252
260
  state.activeKey,
253
261
  state.activeComponent,
@@ -67,8 +67,8 @@ export interface UseTabsStateReturn {
67
67
  addTab: (menuItem: MenusType, options?: AddTabOptions) => void;
68
68
  /** 检查是否可以添加标签页 */
69
69
  canAddTab: (menuItem: MenusType, options?: AddTabOptions) => boolean;
70
- /** 移除标签页 */
71
- removeTab: (tabId: string) => void;
70
+ /** 移除指定标签页;不传 tabId 时移除当前激活标签页 */
71
+ removeTab: (tabId?: string) => void;
72
72
  /** 更新指定 id 的标签页字段(替换语义;找不到时 no-op) */
73
73
  updateTab: (tabId: string, updates: UpdateTabParams) => void;
74
74
  /** 更新页签当前 URL,保持 tab.id 不变 */
@@ -1,4 +1,4 @@
1
- import { AddTabParams, TabItem, MenusType } from '../../../propTypes';
1
+ import { AddTabParams, TabItem, MenusType, TabsUrlSyncConfig } from '../../../propTypes';
2
2
  /**
3
3
  * 获取菜单实际用于路由/签页的路径
4
4
  * 约定优先使用 redirectUrl,其次回退 url
@@ -17,6 +17,7 @@ export declare const getCurrentTabUrl: () => string;
17
17
  export declare const getTabUrlPathname: (value?: string) => string | undefined;
18
18
  export interface FindExistingTabOptions {
19
19
  urlSync?: boolean;
20
+ identity?: TabsUrlSyncConfig['identity'];
20
21
  }
21
22
  export declare const findExistingTab: (tabsList: TabItem[], menuItem: MenusType, options?: FindExistingTabOptions) => TabItem | undefined;
22
23
  /**
@@ -37,6 +37,14 @@ const getTabUrlPathname = (value) => {
37
37
  };
38
38
  const findExistingTab = (tabsList, menuItem, options = {}) => {
39
39
  if (options.urlSync) {
40
+ if (options.identity === "relationKey" && hasRelationKey(menuItem.relationKey)) {
41
+ return tabsList.find(
42
+ (tab) => {
43
+ var _a;
44
+ return tab.relationKey === menuItem.relationKey || ((_a = tab.menuItem) == null ? void 0 : _a.relationKey) === menuItem.relationKey;
45
+ }
46
+ );
47
+ }
40
48
  const targetUrl = normalizeTabUrl(getMenuRoute(menuItem));
41
49
  if (!targetUrl)
42
50
  return void 0;
@@ -368,6 +368,11 @@ export interface TabsUrlSyncConfig {
368
368
  * @default 'replace'
369
369
  */
370
370
  history?: 'replace' | 'push';
371
+ /**
372
+ * @description 页签复用身份。url 保持既有完整 URL 唯一语义;relationKey 在显式传入时优先按业务关联键复用
373
+ * @default 'url'
374
+ */
375
+ identity?: 'url' | 'relationKey';
371
376
  }
372
377
  /**
373
378
  * @description 添加标签页的业务参数
@@ -459,15 +464,25 @@ export interface ProLayoutTabsInstance {
459
464
  */
460
465
  addTab: (params: AddTabParams, options?: AddTabOptions) => void;
461
466
  /**
462
- * @description 删除标签页
467
+ * @description 删除指定标签页;不传 tabId 时删除当前激活标签页
468
+ * @param tabId 目标标签页 id,省略时使用当前激活标签页
469
+ * @example
470
+ * ```tsx
471
+ * layoutTabs.removeTab();
472
+ * layoutTabs.removeTab('tab-id');
473
+ * ```
463
474
  */
464
- removeTab: (tabId: string) => void;
475
+ removeTab: (tabId?: string) => void;
465
476
  /**
466
477
  * @description 更新已存在标签页的展示信息(不会卸载/重挂渲染组件)
467
478
  * @param tabId 目标标签页 id
468
479
  * @param updates 仅更新传入的字段,extra 为替换式覆盖
469
480
  */
470
481
  updateTab: (tabId: string, updates: UpdateTabParams) => void;
482
+ /**
483
+ * @description 就地更新页签完整 URL,保持 tab id 与已挂载组件不变
484
+ */
485
+ updateTabUrl: (tabId: string, url: string) => void;
471
486
  /**
472
487
  * @description 获取标签页信息
473
488
  */
@@ -14,6 +14,7 @@ import React, {
14
14
  import isFunction from "lodash/isFunction";
15
15
  import omit from "lodash/omit";
16
16
  import isNumber from "lodash/isNumber";
17
+ import isString from "lodash/isString";
17
18
  import { useProConfig } from "../ProConfigProvider";
18
19
  import Container from "../ProForm/components/Container";
19
20
  import AdaptiveTooltip from "./components/AdaptiveTooltip";
@@ -190,9 +191,25 @@ const ProSelect = (props, ref) => {
190
191
  const option = findOption(v);
191
192
  if (!option)
192
193
  return "-";
193
- return selectProps.showCodeName ? `${option[code]}-${option[label]}` : option == null ? void 0 : option[label];
194
+ const optionLabel = option[label];
195
+ if (!selectProps.showCodeName)
196
+ return optionLabel;
197
+ if (isString(optionLabel) || isNumber(optionLabel)) {
198
+ return `${option[code]}-${optionLabel}`;
199
+ }
200
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
201
+ option[code],
202
+ "-",
203
+ optionLabel
204
+ ] });
194
205
  });
195
- return labelList == null ? void 0 : labelList.join("、");
206
+ if (labelList.every((item) => isString(item) || isNumber(item))) {
207
+ return labelList.join("、");
208
+ }
209
+ return labelList.map((item, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
210
+ index > 0 ? "、" : null,
211
+ item
212
+ ] }, `${String(value2[index] ?? "empty")}-${index}`));
196
213
  };
197
214
  const defaultFilterOption = useCallback(defaultFilterOptionFn, []);
198
215
  const mergedShowSearch = useMemo(() => {
@@ -282,13 +299,11 @@ const ProSelect = (props, ref) => {
282
299
  };
283
300
  const isReactElement = (element) => isValidElement(element);
284
301
  const selectOptionRender = (option) => {
285
- var _a2, _b2, _c2;
302
+ var _a2, _b2;
286
303
  const currentRecord = ((_a2 = option == null ? void 0 : option.data) == null ? void 0 : _a2.record) || (option == null ? void 0 : option.record);
287
- const text = ((_b2 = option == null ? void 0 : option.data) == null ? void 0 : _b2.searchText) ?? ((_c2 = option == null ? void 0 : option.data) == null ? void 0 : _c2.title) ?? (option == null ? void 0 : option.label) ?? "";
288
- if (!currentRecord) {
289
- return /* @__PURE__ */ jsx(TooltipOption, { title: String(text), children: String(text) });
290
- }
291
- return /* @__PURE__ */ jsx(TooltipOption, { title: String(text), children: OptionRender ? /* @__PURE__ */ jsx("span", { children: OptionRender(currentRecord) }) : /* @__PURE__ */ jsx("span", { children: String(text) }) });
304
+ const optionLabel = ((_b2 = option == null ? void 0 : option.data) == null ? void 0 : _b2.label) ?? (option == null ? void 0 : option.label) ?? "";
305
+ const content = currentRecord && OptionRender ? OptionRender(currentRecord) : optionLabel;
306
+ return /* @__PURE__ */ jsx(TooltipOption, { children: /* @__PURE__ */ jsx("span", { children: content }) });
292
307
  };
293
308
  const mode = selectProps.mode;
294
309
  const handleChange = (value2, rawOption) => {
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import type { DataOptionType } from '../propsType';
2
3
  /**
3
4
  * 返回当前组件可以显示的数据源
@@ -27,7 +28,7 @@ export declare const getOptionText: ({ item, optionRender, showCodeName, code, l
27
28
  showCodeName?: boolean;
28
29
  code: string;
29
30
  label: string;
30
- }) => string;
31
+ }) => ReactNode;
31
32
  /** 构建单个 antd Select option 对象 */
32
33
  export declare const buildOptionItem: (item: DataOptionType, index: number, config: {
33
34
  optionRender?: (item: DataOptionType) => any;
@@ -36,7 +37,7 @@ export declare const buildOptionItem: (item: DataOptionType, index: number, conf
36
37
  label: string;
37
38
  }) => {
38
39
  key: string;
39
- label: string;
40
+ label: ReactNode;
40
41
  value: any;
41
42
  disabled: boolean;
42
43
  title: string;
@@ -2,6 +2,7 @@ import difference from "lodash/difference";
2
2
  import isNumber from "lodash/isNumber";
3
3
  import isObject from "lodash/isObject";
4
4
  import isString from "lodash/isString";
5
+ import { createElement, Fragment } from "react";
5
6
  const findSelectNameValues = ({
6
7
  list = [],
7
8
  selectName,
@@ -130,22 +131,30 @@ const getOptionText = ({
130
131
  code,
131
132
  label
132
133
  }) => {
134
+ const itemLabel = item[label];
133
135
  if (optionRender) {
134
136
  const rendered = optionRender(item);
135
- return isString(rendered) ? rendered : String(item[label] ?? "");
137
+ return isString(rendered) ? rendered : isObject(itemLabel) ? itemLabel : String(itemLabel ?? "");
136
138
  }
137
- const text = showCodeName ? `${item[code]}-${item[label]}` : item[label];
138
- return isString(text) ? text : String(text ?? "");
139
+ if (!showCodeName) {
140
+ return isObject(itemLabel) ? itemLabel : String(itemLabel ?? "");
141
+ }
142
+ if (isString(itemLabel) || isNumber(itemLabel)) {
143
+ return `${item[code]}-${itemLabel}`;
144
+ }
145
+ return createElement(Fragment, null, item[code], "-", itemLabel);
139
146
  };
140
147
  const buildOptionItem = (item, index, config) => {
141
- const text = getOptionText({ item, ...config });
148
+ const optionLabel = getOptionText({ item, ...config });
149
+ const isTextLabel = isString(optionLabel) || isNumber(optionLabel);
150
+ const searchText = isTextLabel ? String(optionLabel) : isString(item[config.label]) || isNumber(item[config.label]) ? String(item[config.label]) : item[config.code] != null ? String(item[config.code]) : "";
142
151
  return {
143
152
  key: item[config.code] != null ? `${item[config.code]}-${index}` : `option-${index}`,
144
- label: text,
153
+ label: optionLabel,
145
154
  value: item[config.code] != null ? item[config.code] : void 0,
146
155
  disabled: !!item.disabled,
147
- title: text,
148
- searchText: text,
156
+ title: isTextLabel ? String(optionLabel) : void 0,
157
+ searchText,
149
158
  record: item
150
159
  };
151
160
  };
@@ -156,9 +165,10 @@ const buildSelectOptions = (list, config) => {
156
165
  return list.map((item, index) => {
157
166
  const groupOptions = item.options;
158
167
  if (Array.isArray(groupOptions)) {
168
+ const groupLabel = item[config.label];
159
169
  return {
160
- key: item[config.label] ?? `group-${index}`,
161
- label: item[config.label],
170
+ key: isString(groupLabel) || isNumber(groupLabel) ? String(groupLabel) : item[config.code] != null ? String(item[config.code]) : `group-${index}`,
171
+ label: groupLabel,
162
172
  options: groupOptions.map(
163
173
  (opt, optIndex) => buildOptionItem(opt, optIndex, config)
164
174
  )
@@ -14,7 +14,8 @@ const RenderTabs = (props) => {
14
14
  formTableProps,
15
15
  transformParams
16
16
  } = props;
17
- const { form, onSearch } = formTableProps || {};
17
+ const { form, onSearch, loading } = formTableProps || {};
18
+ const requestLoading = Boolean(loading);
18
19
  const [activeKey, setActiveKey] = useState(props.activeKey);
19
20
  const [options] = useEnum(code || "__PRO_TABLE_NO_CODE__");
20
21
  const fieldValue = Form.useWatch(name, form);
@@ -48,6 +49,8 @@ const RenderTabs = (props) => {
48
49
  return null;
49
50
  }
50
51
  const handleChange = (key) => {
52
+ if (requestLoading)
53
+ return;
51
54
  const fieldsValues = (form == null ? void 0 : form.getFieldsValue()) || {};
52
55
  setActiveKey(key);
53
56
  form.setFieldValue(name, key);
@@ -65,7 +68,7 @@ const RenderTabs = (props) => {
65
68
  onChange: handleChange,
66
69
  type: "card",
67
70
  ...tabsProps,
68
- items: tabItems,
71
+ items: requestLoading ? tabItems.map((item) => ({ ...item, disabled: true })) : tabItems,
69
72
  activeKey
70
73
  }
71
74
  );
@@ -76,6 +79,7 @@ const RenderTabs = (props) => {
76
79
  className: "pro-table-segmented",
77
80
  options: tabItems.map((item) => ({ label: item.label, value: item.key })),
78
81
  ...segmentedProps,
82
+ disabled: requestLoading || (segmentedProps == null ? void 0 : segmentedProps.disabled),
79
83
  onChange: (value) => handleChange(value),
80
84
  value: activeKey
81
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zat-design/sisyphus-react",
3
- "version": "4.5.10-beta.2",
3
+ "version": "4.6.0-beta.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",