@zat-design/sisyphus-react 4.3.6 → 4.4.0
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/ProEditTable/components/RenderField/ListChangedWrapper.js +0 -2
- package/es/ProForm/components/combination/Group/utils/index.d.ts +19 -19
- package/es/ProForm/utils/index.d.ts +0 -6
- package/es/ProForm/utils/index.js +0 -47
- package/es/ProLayout/components/TabsManager/hooks/useIframeRoute.d.ts +25 -0
- package/es/ProLayout/components/TabsManager/hooks/useIframeRoute.js +63 -0
- package/es/ProLayout/components/TabsManager/hooks/useTabsState.js +11 -6
- package/es/ProLayout/components/TabsManager/index.js +56 -5
- package/es/ProLayout/components/TabsManager/propTypes.d.ts +4 -0
- package/es/ProLayout/components/TabsManager/utils/index.d.ts +0 -8
- package/es/ProLayout/components/TabsManager/utils/index.js +0 -26
- package/es/ProLayout/index.js +15 -1
- package/es/ProLayout/propTypes.d.ts +56 -0
- package/es/ProLayout/propTypes.js +14 -0
- package/es/ProLayout/style/index.less +5 -0
- package/es/utils/index.d.ts +0 -1
- package/es/utils/index.js +1 -22
- package/package.json +1 -1
|
@@ -76,34 +76,35 @@ export declare const useFormItemProps: (column: FlexibleGroupColumnType, context
|
|
|
76
76
|
show?: boolean | ReactiveFunction<any, boolean>;
|
|
77
77
|
component?: React.ReactNode | ReactiveFunction<any, React.ReactNode>;
|
|
78
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;
|
|
81
|
-
prefixCls?: string;
|
|
82
|
-
trigger?: string;
|
|
83
|
-
id?: string;
|
|
84
79
|
className?: string;
|
|
85
|
-
|
|
80
|
+
hidden?: boolean;
|
|
81
|
+
id?: string;
|
|
86
82
|
style?: React.CSSProperties;
|
|
83
|
+
onReset?: () => void;
|
|
84
|
+
validateTrigger?: string | false | string[];
|
|
85
|
+
preserve?: boolean;
|
|
86
|
+
prefixCls?: string;
|
|
87
|
+
rootClassName?: string;
|
|
88
|
+
status?: "" | "warning" | "error" | "success" | "validating";
|
|
87
89
|
isView?: boolean;
|
|
88
|
-
|
|
90
|
+
valueType?: import("../../../render/propsType").ProFormValueType;
|
|
91
|
+
viewType?: import("../../../render/propsType").ViewType;
|
|
92
|
+
trim?: boolean;
|
|
93
|
+
normalize?: (value: any, prevValue: any, allValues: import("@rc-component/form/lib/interface").Store) => any;
|
|
94
|
+
getValueProps?: ((value: any) => Record<string, unknown>) & ((value: any) => Record<string, unknown>);
|
|
95
|
+
desensitization?: [number, number] | ReactiveFunction<any, [number, number]>;
|
|
89
96
|
vertical?: boolean;
|
|
90
|
-
hidden?: boolean;
|
|
91
|
-
layout?: import("antd/es/form/Form").FormItemLayout;
|
|
92
|
-
help?: React.ReactNode;
|
|
93
|
-
preserve?: boolean;
|
|
94
97
|
colon?: boolean;
|
|
95
98
|
htmlFor?: string;
|
|
96
99
|
labelAlign?: import("antd/es/form/interface").FormLabelAlign;
|
|
97
100
|
labelCol?: import("antd").ColProps;
|
|
98
101
|
getValueFromEvent?: (...args: import("@rc-component/form/lib/interface").EventArgs) => any;
|
|
99
102
|
shouldUpdate?: import("@rc-component/form/lib/Field").ShouldUpdate<any>;
|
|
100
|
-
|
|
103
|
+
trigger?: string;
|
|
101
104
|
validateDebounce?: number;
|
|
102
105
|
valuePropName?: string;
|
|
103
|
-
getValueProps?: ((value: any) => Record<string, unknown>) & ((value: any) => Record<string, unknown>);
|
|
104
106
|
messageVariables?: Record<string, string>;
|
|
105
107
|
initialValue?: any;
|
|
106
|
-
onReset?: () => void;
|
|
107
108
|
onMetaChange?: (meta: import("@rc-component/form/lib/Field").MetaEvent) => void;
|
|
108
109
|
isListField?: boolean;
|
|
109
110
|
isList?: boolean;
|
|
@@ -111,22 +112,21 @@ export declare const useFormItemProps: (column: FlexibleGroupColumnType, context
|
|
|
111
112
|
hasFeedback?: boolean | {
|
|
112
113
|
icons: import("antd/es/form/FormItem").FeedbackIcons;
|
|
113
114
|
};
|
|
114
|
-
validateStatus?: "" | "
|
|
115
|
+
validateStatus?: "" | "warning" | "error" | "success" | "validating";
|
|
116
|
+
layout?: import("antd/es/form/Form").FormItemLayout;
|
|
115
117
|
wrapperCol?: import("antd").ColProps;
|
|
118
|
+
help?: React.ReactNode;
|
|
116
119
|
fieldId?: string;
|
|
117
|
-
valueType?: import("../../../render/propsType").ProFormValueType;
|
|
118
120
|
switchValue?: [any, any];
|
|
119
121
|
viewRender?: (value: any, record: any, { form, index, namePath, }: {
|
|
120
122
|
[key: string]: any;
|
|
121
123
|
form: FormInstance<any>;
|
|
122
124
|
index?: number;
|
|
123
125
|
}) => string | React.ReactElement<any, any>;
|
|
124
|
-
viewType?: import("../../../render/propsType").ViewType;
|
|
125
126
|
upperCase?: boolean;
|
|
126
127
|
toISOString?: boolean;
|
|
127
128
|
toCSTString?: boolean;
|
|
128
129
|
clearNotShow?: boolean;
|
|
129
|
-
desensitization?: [number, number] | ReactiveFunction<any, [number, number]>;
|
|
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, "
|
|
144
|
+
componentProps: import("lodash").Omit<any, "precision" | "valueType" | "format" | "switchValue" | "dependNames" | "toISOString" | "toCSTString" | "clearNotShow">;
|
|
145
145
|
formItemTransform: {
|
|
146
146
|
getValueProps: any;
|
|
147
147
|
normalize: any;
|
|
@@ -25,7 +25,6 @@ export declare const isTrim: (type: string, trim: boolean, configData: any) => a
|
|
|
25
25
|
* @returns {}
|
|
26
26
|
*/
|
|
27
27
|
export declare const isUpperCase: (type: string, upperCase: boolean) => any;
|
|
28
|
-
export declare const weedOutProps: (data: Record<string, any>, keys: string[]) => {}[];
|
|
29
28
|
export declare const diffField: (prevValues: any, curValues: any, names: NamePath[]) => boolean;
|
|
30
29
|
export declare const splitNameStr: (name: string) => string[][];
|
|
31
30
|
export declare const filterInternalFields: (values: any, optimize?: boolean) => any;
|
|
@@ -75,7 +74,6 @@ export declare const view: (obj: any) => boolean;
|
|
|
75
74
|
* @returns 位数
|
|
76
75
|
*/
|
|
77
76
|
export declare const getDecimalDigits: (num: number) => number;
|
|
78
|
-
export declare const deleteForPath: (values: any, namePath: string | number | (string | number)[]) => void;
|
|
79
77
|
/**
|
|
80
78
|
* 获取树状结构option
|
|
81
79
|
* @param treeData 树数据源
|
|
@@ -88,10 +86,6 @@ export declare const findOptionByValue: (treeData: any[], value: string | number
|
|
|
88
86
|
value?: string;
|
|
89
87
|
children?: string;
|
|
90
88
|
}) => any;
|
|
91
|
-
/**
|
|
92
|
-
* 对比依赖列表中的 值是否变更
|
|
93
|
-
*/
|
|
94
|
-
export declare const equalDependencies: (dependencies: any, prevValues: any, currentValues: any) => any;
|
|
95
89
|
/** 解析namePath */
|
|
96
90
|
export declare const parseNamePath: (input: string) => any;
|
|
97
91
|
/**
|
|
@@ -95,21 +95,6 @@ export const isUpperCase = (type, upperCase) => {
|
|
|
95
95
|
return result;
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
// 剔除otherProps 组成对象返回
|
|
99
|
-
export const weedOutProps = (data, keys) => {
|
|
100
|
-
const obj = {};
|
|
101
|
-
const weedOut = {};
|
|
102
|
-
Object.keys(data).forEach(key => {
|
|
103
|
-
const included = keys.includes(key);
|
|
104
|
-
if (included) {
|
|
105
|
-
weedOut[key] = data[key];
|
|
106
|
-
} else {
|
|
107
|
-
obj[key] = data[key];
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
return [obj, weedOut];
|
|
111
|
-
};
|
|
112
|
-
|
|
113
98
|
// 对比字段变化
|
|
114
99
|
export const diffField = (prevValues, curValues, names) => {
|
|
115
100
|
return names.some(name => {
|
|
@@ -331,24 +316,6 @@ export const getDecimalDigits = num => {
|
|
|
331
316
|
const match = num.toString().match(/\.(\d+)/);
|
|
332
317
|
return match ? match[1].length : 0;
|
|
333
318
|
};
|
|
334
|
-
export const deleteForPath = (values, namePath) => {
|
|
335
|
-
if (Array.isArray(namePath)) {
|
|
336
|
-
let current = values;
|
|
337
|
-
namePath.every((nameItem, index) => {
|
|
338
|
-
if (index === namePath.length - 1) {
|
|
339
|
-
delete current[nameItem];
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
if (typeof current[nameItem] !== 'object') {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
current = current[nameItem];
|
|
346
|
-
return true;
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
delete values[namePath];
|
|
351
|
-
};
|
|
352
319
|
|
|
353
320
|
/**
|
|
354
321
|
* 获取树状结构option
|
|
@@ -374,20 +341,6 @@ export const findOptionByValue = (treeData, value, fieldNames) => {
|
|
|
374
341
|
}
|
|
375
342
|
};
|
|
376
343
|
|
|
377
|
-
/**
|
|
378
|
-
* 对比依赖列表中的 值是否变更
|
|
379
|
-
*/
|
|
380
|
-
export const equalDependencies = (dependencies, prevValues, currentValues) => {
|
|
381
|
-
if (dependencies?.length) {
|
|
382
|
-
return dependencies.some(namePath => {
|
|
383
|
-
const prevValue = _get(prevValues, namePath);
|
|
384
|
-
const currentValue = _get(currentValues, namePath);
|
|
385
|
-
return prevValue !== currentValue;
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
return false;
|
|
389
|
-
};
|
|
390
|
-
|
|
391
344
|
/** 解析namePath */
|
|
392
345
|
export const parseNamePath = input => {
|
|
393
346
|
const parts = input.split('_');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IframeConfig } from '../../../propTypes';
|
|
2
|
+
export interface UseIframeRouteOptions {
|
|
3
|
+
/** iframe 嵌入配置 */
|
|
4
|
+
config: IframeConfig | undefined;
|
|
5
|
+
/** 命中路由后调用的 addTab(由外部 tabsInstance.addTab 注入) */
|
|
6
|
+
addTab: (params: {
|
|
7
|
+
code: string;
|
|
8
|
+
name: string;
|
|
9
|
+
extra?: Record<string, any>;
|
|
10
|
+
}) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface UseIframeRouteReturn {
|
|
13
|
+
/** 当前是否处于 iframe 嵌入环境(通过 URL query `?isIframe` 判定,大小写不敏感) */
|
|
14
|
+
isIframe: boolean;
|
|
15
|
+
/** 是否启用纯净渲染(隐藏 Header/Menu/TabsBar);非 iframe 或未配置 iframe 时始终为 false */
|
|
16
|
+
pure: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* iframe 嵌入模式 hook
|
|
20
|
+
* - 通过 URL query `?isIframe`(大小写不敏感)判定是否处于 iframe 嵌入场景
|
|
21
|
+
* - 命中时按 routes 静态路径匹配 window.location.pathname,调用 addTab
|
|
22
|
+
* - query string 自动作为 extra 透传给业务组件
|
|
23
|
+
* - 暴露 pure 标志供外部 layout 决定渲染范围
|
|
24
|
+
*/
|
|
25
|
+
export declare const useIframeRoute: (options: UseIframeRouteOptions) => UseIframeRouteReturn;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
const detectIframe = () => {
|
|
3
|
+
if (typeof window === 'undefined') return false;
|
|
4
|
+
// URL 上带 ?isIframe(大小写不敏感)即视为 iframe 嵌入
|
|
5
|
+
const params = new URLSearchParams(window.location.search);
|
|
6
|
+
for (const key of params.keys()) {
|
|
7
|
+
if (key.toLowerCase() === 'isiframe') return true;
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
};
|
|
11
|
+
const stripSlashes = s => s.replace(/^\/+|\/+$/g, '');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* iframe 嵌入模式 hook
|
|
15
|
+
* - 通过 URL query `?isIframe`(大小写不敏感)判定是否处于 iframe 嵌入场景
|
|
16
|
+
* - 命中时按 routes 静态路径匹配 window.location.pathname,调用 addTab
|
|
17
|
+
* - query string 自动作为 extra 透传给业务组件
|
|
18
|
+
* - 暴露 pure 标志供外部 layout 决定渲染范围
|
|
19
|
+
*/
|
|
20
|
+
export const useIframeRoute = options => {
|
|
21
|
+
const {
|
|
22
|
+
config,
|
|
23
|
+
addTab
|
|
24
|
+
} = options;
|
|
25
|
+
const isIframe = useMemo(detectIframe, []);
|
|
26
|
+
// pure 仅在显式配置 iframe 时才考虑;非 iframe 嵌入(URL 无 ?isIframe)始终为 false
|
|
27
|
+
const pure = isIframe && config != null && config.pure !== false;
|
|
28
|
+
|
|
29
|
+
// 用 ref 持有最新的 addTab/config,避免依赖变化触发副作用重跑
|
|
30
|
+
const addTabRef = useRef(addTab);
|
|
31
|
+
addTabRef.current = addTab;
|
|
32
|
+
const configRef = useRef(config);
|
|
33
|
+
configRef.current = config;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!isIframe) return;
|
|
36
|
+
const cfg = configRef.current;
|
|
37
|
+
if (!cfg?.routes?.length) return;
|
|
38
|
+
const {
|
|
39
|
+
pathname,
|
|
40
|
+
search
|
|
41
|
+
} = window.location;
|
|
42
|
+
const searchParams = {};
|
|
43
|
+
new URLSearchParams(search).forEach((value, key) => {
|
|
44
|
+
searchParams[key] = value;
|
|
45
|
+
});
|
|
46
|
+
const normalizedPath = stripSlashes(pathname);
|
|
47
|
+
for (let i = 0; i < cfg.routes.length; i += 1) {
|
|
48
|
+
const route = cfg.routes[i];
|
|
49
|
+
if (stripSlashes(route.path) === normalizedPath) {
|
|
50
|
+
addTabRef.current({
|
|
51
|
+
code: route.code,
|
|
52
|
+
name: route.name,
|
|
53
|
+
extra: searchParams
|
|
54
|
+
});
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, [isIframe]);
|
|
59
|
+
return {
|
|
60
|
+
isIframe,
|
|
61
|
+
pure
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -13,7 +13,8 @@ export const useTabsState = options => {
|
|
|
13
13
|
const {
|
|
14
14
|
initialState = {},
|
|
15
15
|
config,
|
|
16
|
-
dataSource
|
|
16
|
+
dataSource,
|
|
17
|
+
cacheEnabled = true
|
|
17
18
|
} = options;
|
|
18
19
|
|
|
19
20
|
// 合并默认配置
|
|
@@ -40,7 +41,7 @@ export const useTabsState = options => {
|
|
|
40
41
|
} = useTabsCache({
|
|
41
42
|
cacheKey: finalConfig.cacheKey,
|
|
42
43
|
storage: finalConfig.storage,
|
|
43
|
-
enabled:
|
|
44
|
+
enabled: cacheEnabled
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
// 初始化标记,确保只在初始化后保存缓存
|
|
@@ -140,7 +141,8 @@ export const useTabsState = options => {
|
|
|
140
141
|
*/
|
|
141
142
|
const addTab = useCallback((menuItem, options) => {
|
|
142
143
|
const {
|
|
143
|
-
forceNew = false
|
|
144
|
+
forceNew = false,
|
|
145
|
+
silent = false
|
|
144
146
|
} = options || {};
|
|
145
147
|
setState(prevState => {
|
|
146
148
|
if (!canOpenAsTab(menuItem)) return prevState;
|
|
@@ -177,13 +179,16 @@ export const useTabsState = options => {
|
|
|
177
179
|
const tabId = generateTabId(menuItem, existingIds);
|
|
178
180
|
const newTab = createTabFromMenu(menuItem, prevState.newTabIndex);
|
|
179
181
|
newTab.id = tabId;
|
|
182
|
+
|
|
183
|
+
// silent=true 时,若已有 active 则保留;无 active(首次进入)仍激活以保留默认行为
|
|
184
|
+
const shouldActivate = !silent || prevState.activeKey === '';
|
|
180
185
|
return {
|
|
181
186
|
...prevState,
|
|
182
187
|
tabsList: [...prevState.tabsList, newTab],
|
|
183
|
-
activeKey: tabId,
|
|
184
|
-
activeTabInfo: newTab,
|
|
188
|
+
activeKey: shouldActivate ? tabId : prevState.activeKey,
|
|
189
|
+
activeTabInfo: shouldActivate ? newTab : prevState.activeTabInfo,
|
|
185
190
|
newTabIndex: prevState.newTabIndex + 1,
|
|
186
|
-
activeComponent: menuItem.code || tabId
|
|
191
|
+
activeComponent: shouldActivate ? menuItem.code || tabId : prevState.activeComponent
|
|
187
192
|
};
|
|
188
193
|
});
|
|
189
194
|
}, [finalConfig]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
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 { useIframeRoute } from "./hooks/useIframeRoute";
|
|
4
5
|
import { flattenMenuData } from "./utils";
|
|
5
6
|
import { TabItemComponent } from "./components/TabItem";
|
|
6
7
|
import { TabsHeader } from "./components/TabsHeader";
|
|
@@ -16,8 +17,23 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
16
17
|
dataSource,
|
|
17
18
|
originalOnMenuClick,
|
|
18
19
|
tabsBarContainer,
|
|
19
|
-
onTabsChange
|
|
20
|
+
onTabsChange,
|
|
21
|
+
onIframePureChange
|
|
20
22
|
}, ref) => {
|
|
23
|
+
// iframe 模式检测(在 useTabsState 之前,因为 cacheEnabled 依赖 pure)
|
|
24
|
+
// addTab 由后续 useTabsState 提供,此处先用占位,后面用 ref 透传
|
|
25
|
+
const iframeAddTabRef = useRef(() => {});
|
|
26
|
+
const {
|
|
27
|
+
pure: isIframePure
|
|
28
|
+
} = useIframeRoute({
|
|
29
|
+
config: config?.iframe,
|
|
30
|
+
addTab: params => iframeAddTabRef.current(params)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 通知父级(ProLayout)pure 状态变化
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
onIframePureChange?.(isIframePure);
|
|
36
|
+
}, [isIframePure, onIframePureChange]);
|
|
21
37
|
const {
|
|
22
38
|
state,
|
|
23
39
|
isInitialized,
|
|
@@ -32,9 +48,28 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
32
48
|
reorderTabs
|
|
33
49
|
} = useTabsState({
|
|
34
50
|
config,
|
|
35
|
-
dataSource
|
|
51
|
+
dataSource,
|
|
52
|
+
cacheEnabled: !isIframePure
|
|
36
53
|
});
|
|
37
54
|
|
|
55
|
+
// 桥接 useIframeRoute 的 { code, name, extra } → addTab(menuItem)
|
|
56
|
+
iframeAddTabRef.current = params => {
|
|
57
|
+
const sourceMenus = Array.isArray(dataSource?.menus) ? flattenMenuData(dataSource.menus) : [];
|
|
58
|
+
const matchedMenu = sourceMenus.find(item => item.code === params.code);
|
|
59
|
+
const menuItem = matchedMenu ? {
|
|
60
|
+
...matchedMenu,
|
|
61
|
+
name: params.name || matchedMenu.name,
|
|
62
|
+
extra: params.extra ?? matchedMenu.extra
|
|
63
|
+
} : {
|
|
64
|
+
id: Date.now(),
|
|
65
|
+
code: params.code,
|
|
66
|
+
name: params.name,
|
|
67
|
+
url: `/${params.code}`,
|
|
68
|
+
extra: params.extra
|
|
69
|
+
};
|
|
70
|
+
addTab(menuItem);
|
|
71
|
+
};
|
|
72
|
+
|
|
38
73
|
// 记录已访问过的 Tab ID,用于懒加载
|
|
39
74
|
const [visitedTabIds, setVisitedTabIds] = useState(new Set());
|
|
40
75
|
|
|
@@ -60,12 +95,24 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
60
95
|
const addTabRef = useRef(addTab);
|
|
61
96
|
addTabRef.current = addTab;
|
|
62
97
|
useEffect(() => {
|
|
98
|
+
// iframe pure 模式下不应用 fixed(嵌入页面只显示路由命中的单个 tab)
|
|
99
|
+
if (isIframePure) return;
|
|
63
100
|
if (!isInitialized || !config.fixed || config.fixed.length === 0) return;
|
|
64
101
|
const sourceMenus = Array.isArray(dataSource?.menus) ? flattenMenuData(dataSource.menus) : [];
|
|
65
102
|
|
|
66
103
|
// menus 为空说明异步数据尚未返回,等 dataSource 变化后重跑,避免用 code 创建占位 tab
|
|
67
104
|
if (sourceMenus.length === 0) return;
|
|
68
105
|
config.fixed.forEach(code => {
|
|
106
|
+
// 已存在(如缓存恢复):仅校正 closable,不调 addTab 以保留缓存中的 activeKey
|
|
107
|
+
const existing = state.tabsList.find(tab => tab.menuItem?.code === code || tab.code === code);
|
|
108
|
+
if (existing) {
|
|
109
|
+
if (existing.closable !== false) {
|
|
110
|
+
updateTab(existing.id, {
|
|
111
|
+
closable: false
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
69
116
|
const matchedMenu = sourceMenus.find(item => item.code === code);
|
|
70
117
|
const menuItem = matchedMenu ? {
|
|
71
118
|
...matchedMenu,
|
|
@@ -77,9 +124,12 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
77
124
|
url: `/${code}`,
|
|
78
125
|
closable: false
|
|
79
126
|
};
|
|
80
|
-
|
|
127
|
+
// silent: 已有 active(如 iframe 路由命中)时不抢 active;无 active 时仍激活以保留默认行为
|
|
128
|
+
addTabRef.current(menuItem, {
|
|
129
|
+
silent: true
|
|
130
|
+
});
|
|
81
131
|
});
|
|
82
|
-
}, [isInitialized, config.fixed, dataSource]);
|
|
132
|
+
}, [isInitialized, config.fixed, dataSource, state.tabsList, updateTab, isIframePure]);
|
|
83
133
|
|
|
84
134
|
// 处理菜单点击 - 拦截原有的菜单点击逻辑
|
|
85
135
|
const handleMenuClick = useCallback(params => {
|
|
@@ -191,7 +241,8 @@ const TabsManager = /*#__PURE__*/forwardRef(({
|
|
|
191
241
|
};
|
|
192
242
|
|
|
193
243
|
// TabsHeader 节点:有 tabsBarContainer 时用 Portal 渲染到外部,否则原位 fallback
|
|
194
|
-
|
|
244
|
+
// iframe pure 模式下完全不渲染 tab bar
|
|
245
|
+
const tabsHeaderNode = state.tabsList.length > 0 && !isIframePure ? /*#__PURE__*/_jsx(TabsHeader, {
|
|
195
246
|
activeKey: state.activeKey || undefined,
|
|
196
247
|
tabsItems: tabsItems,
|
|
197
248
|
onTabChange: handleTabChange,
|
|
@@ -18,6 +18,8 @@ export interface TabsManagerProps {
|
|
|
18
18
|
tabsBarContainer?: HTMLElement | null;
|
|
19
19
|
/** Tab 有无变化回调,供 ProLayout 控制 wrapper 动画 */
|
|
20
20
|
onTabsChange?: (hasTabs: boolean) => void;
|
|
21
|
+
/** iframe pure 模式变化回调,供 ProLayout 决定是否隐藏外壳 */
|
|
22
|
+
onIframePureChange?: (pure: boolean) => void;
|
|
21
23
|
}
|
|
22
24
|
export interface TabContextMenuProps {
|
|
23
25
|
tabId: string;
|
|
@@ -47,6 +49,8 @@ export interface UseTabsStateOptions {
|
|
|
47
49
|
config: TabsManagerProps['config'];
|
|
48
50
|
/** 数据源 */
|
|
49
51
|
dataSource?: TabsManagerProps['dataSource'];
|
|
52
|
+
/** 是否启用缓存(读+写),默认 true。iframe pure 模式下传 false */
|
|
53
|
+
cacheEnabled?: boolean;
|
|
50
54
|
}
|
|
51
55
|
export interface UseTabsStateReturn {
|
|
52
56
|
/** 当前状态 */
|
|
@@ -27,10 +27,6 @@ export declare const handleExternalOpen: (menuItem: MenusType) => void;
|
|
|
27
27
|
* @returns 如果超出限制返回 true,否则返回 false
|
|
28
28
|
*/
|
|
29
29
|
export declare const checkTabLimit: (currentTabs: TabItem[], maxTabs?: number | null) => boolean;
|
|
30
|
-
/**
|
|
31
|
-
* 移除最旧的标签页(根据访问时间或创建时间)
|
|
32
|
-
*/
|
|
33
|
-
export declare const removeOldestTab: (tabs: TabItem[]) => TabItem[];
|
|
34
30
|
/**
|
|
35
31
|
* 获取右侧标签页
|
|
36
32
|
*/
|
|
@@ -44,10 +40,6 @@ export declare const flattenMenuData: (menus?: MenusType[]) => MenusType[];
|
|
|
44
40
|
* 只有当菜单项没有子菜单或子菜单为空时,才认为是叶子节点
|
|
45
41
|
*/
|
|
46
42
|
export declare const isLeafMenuItem: (menuItem: MenusType) => boolean;
|
|
47
|
-
/**
|
|
48
|
-
* 判断菜单项是否为根节点
|
|
49
|
-
*/
|
|
50
|
-
export declare const isRootMenuItem: (menuItem: MenusType) => boolean;
|
|
51
43
|
/**
|
|
52
44
|
* 判断菜单项是否可以在 Tabs 模式下打开
|
|
53
45
|
* 规则:只有叶子节点(无 children)才可打开
|
|
@@ -72,21 +72,6 @@ export const checkTabLimit = (currentTabs, maxTabs) => {
|
|
|
72
72
|
return currentTabs.length >= maxTabs;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
* 移除最旧的标签页(根据访问时间或创建时间)
|
|
77
|
-
*/
|
|
78
|
-
export const removeOldestTab = tabs => {
|
|
79
|
-
if (tabs.length === 0) return tabs;
|
|
80
|
-
|
|
81
|
-
// 找到可关闭的最旧标签页
|
|
82
|
-
const closableTabs = tabs.filter(tab => tab.closable);
|
|
83
|
-
if (closableTabs.length === 0) return tabs;
|
|
84
|
-
|
|
85
|
-
// 移除第一个可关闭的标签页
|
|
86
|
-
const oldestTab = closableTabs[0];
|
|
87
|
-
return tabs.filter(tab => tab.id !== oldestTab.id);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
75
|
/**
|
|
91
76
|
* 获取右侧标签页
|
|
92
77
|
*/
|
|
@@ -122,17 +107,6 @@ export const isLeafMenuItem = menuItem => {
|
|
|
122
107
|
return !children || Array.isArray(children) && children.length === 0;
|
|
123
108
|
};
|
|
124
109
|
|
|
125
|
-
/**
|
|
126
|
-
* 判断菜单项是否为根节点
|
|
127
|
-
*/
|
|
128
|
-
export const isRootMenuItem = menuItem => {
|
|
129
|
-
if (!menuItem) return false;
|
|
130
|
-
if (menuItem.parentId === null || menuItem.parentId === undefined) {
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
return Array.isArray(menuItem.keyIdPath) && menuItem.keyIdPath.length === 1;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
110
|
/**
|
|
137
111
|
* 判断菜单项是否可以在 Tabs 模式下打开
|
|
138
112
|
* 规则:只有叶子节点(无 children)才可打开
|
package/es/ProLayout/index.js
CHANGED
|
@@ -55,6 +55,8 @@ const ProLayout = props => {
|
|
|
55
55
|
// Tab 栏 Portal 挂载目标及动画状态
|
|
56
56
|
const [tabsBarEl, setTabsBarEl] = useState(null);
|
|
57
57
|
const [hasTabs, setHasTabs] = useState(false);
|
|
58
|
+
// TabsManager 通过 onIframePureChange 报告的 pure 状态(iframe 嵌入 + 默认纯净渲染)
|
|
59
|
+
const [isIframePure, setIsIframePure] = useState(false);
|
|
58
60
|
const [{
|
|
59
61
|
notice,
|
|
60
62
|
menus,
|
|
@@ -135,6 +137,7 @@ const ProLayout = props => {
|
|
|
135
137
|
originalOnMenuClick: onMenuClick,
|
|
136
138
|
tabsBarContainer: tabsBarEl,
|
|
137
139
|
onTabsChange: setHasTabs,
|
|
140
|
+
onIframePureChange: setIsIframePure,
|
|
138
141
|
children: children
|
|
139
142
|
});
|
|
140
143
|
};
|
|
@@ -233,7 +236,18 @@ const ProLayout = props => {
|
|
|
233
236
|
onMenuClick: handleMenuClick,
|
|
234
237
|
appsOptions
|
|
235
238
|
};
|
|
236
|
-
const layoutDom =
|
|
239
|
+
const layoutDom = isIframePure ?
|
|
240
|
+
/*#__PURE__*/
|
|
241
|
+
/* iframe pure 模式:仅渲染 TabsManager 内容,不渲染 Header/Notice/Menu/tabsBar */
|
|
242
|
+
_jsx("div", {
|
|
243
|
+
className: classNames({
|
|
244
|
+
'pro-layout': true,
|
|
245
|
+
'pro-layout-light': true,
|
|
246
|
+
'pro-layout-iframe-pure': true,
|
|
247
|
+
[`${className}`]: className
|
|
248
|
+
}),
|
|
249
|
+
children: renderContent()
|
|
250
|
+
}) : /*#__PURE__*/_jsxs("div", {
|
|
237
251
|
className: classNames({
|
|
238
252
|
'pro-layout': true,
|
|
239
253
|
'pro-layout-light': true,
|
|
@@ -370,6 +370,12 @@ export interface AddTabOptions {
|
|
|
370
370
|
* @description 是否强制创建新标签页(忽略已存在的相同标签页),默认 false
|
|
371
371
|
*/
|
|
372
372
|
forceNew?: boolean;
|
|
373
|
+
/**
|
|
374
|
+
* @description 是否静默追加:true 时新建 tab 不切换 active;当前已有 active 时保留原 active。
|
|
375
|
+
* 列表为空(activeKey 为空)时仍会激活,以保留首次进入默认激活的行为。
|
|
376
|
+
* 用于 fixed 声明式注入等不应抢用户 active 的场景。默认 false。
|
|
377
|
+
*/
|
|
378
|
+
silent?: boolean;
|
|
373
379
|
}
|
|
374
380
|
/**
|
|
375
381
|
* @description 更新标签页的字段(仅允许更新展示与扩展数据)
|
|
@@ -426,6 +432,51 @@ export interface ProLayoutTabsInstance {
|
|
|
426
432
|
*/
|
|
427
433
|
getTabInfo: () => TabsInfoResult;
|
|
428
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* @description iframe 路由表配置项
|
|
437
|
+
*/
|
|
438
|
+
export interface IframeRoute {
|
|
439
|
+
/**
|
|
440
|
+
* @description 静态路径,与 `window.location.pathname` 完全匹配(忽略首尾斜杠)。
|
|
441
|
+
* 不支持 `:param` 动态片段;动态参数通过 query string 传递,
|
|
442
|
+
* 自动作为 props 透传给业务组件。
|
|
443
|
+
* 业务方需保证 URL query 字段名与业务组件 props 名一致。
|
|
444
|
+
* @example '/wjs/policy/view'
|
|
445
|
+
* 配合 URL: `/wjs/policy/view?taskId=123&policyId=456`
|
|
446
|
+
*/
|
|
447
|
+
path: string;
|
|
448
|
+
/**
|
|
449
|
+
* @description 命中后用于解析 activeComponent 的业务 code
|
|
450
|
+
*/
|
|
451
|
+
code: string;
|
|
452
|
+
/**
|
|
453
|
+
* @description 命中后的标签名称
|
|
454
|
+
*/
|
|
455
|
+
name: string;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* @description iframe 嵌入配置
|
|
459
|
+
*
|
|
460
|
+
* **触发条件**:URL query 上带 `?isIframe`(大小写不敏感,值不限,只看 key 是否存在)。
|
|
461
|
+
* 第三方系统通过在 iframe src 上拼 `?isIframe=1` 来触发组件库的嵌入模式。
|
|
462
|
+
*
|
|
463
|
+
* 未带该 query 时(独立访问),即使配置了 `tabs.iframe`,组件库也完全不读取,
|
|
464
|
+
* 维持完整 Layout 渲染。
|
|
465
|
+
*/
|
|
466
|
+
export interface IframeConfig {
|
|
467
|
+
/**
|
|
468
|
+
* @description 路由表:按 URL pathname 顺序匹配,命中后自动 addTab
|
|
469
|
+
* - URL query 自动合并进 extra,作为业务组件 props 透传
|
|
470
|
+
*/
|
|
471
|
+
routes: IframeRoute[];
|
|
472
|
+
/**
|
|
473
|
+
* @description 嵌入时是否使用纯净渲染(只显示内容区域)
|
|
474
|
+
* - true (默认): 隐藏 Header / Menu / TabsBar,仅渲染当前 tab 内容
|
|
475
|
+
* - false: 完整 layout 渲染
|
|
476
|
+
* @default true
|
|
477
|
+
*/
|
|
478
|
+
pure?: boolean;
|
|
479
|
+
}
|
|
429
480
|
export interface TabsConfig {
|
|
430
481
|
max?: number;
|
|
431
482
|
storage?: TabsStorageStrategy;
|
|
@@ -461,6 +512,11 @@ export interface TabsConfig {
|
|
|
461
512
|
* @example fixed: ['Workspace', 'PolicyInput']
|
|
462
513
|
*/
|
|
463
514
|
fixed?: string[];
|
|
515
|
+
/**
|
|
516
|
+
* @description iframe 嵌入配置,允许第三方系统通过 URL 定位到具体 tab
|
|
517
|
+
* 仅在 URL 带 `?isIframe`(大小写不敏感)时生效
|
|
518
|
+
*/
|
|
519
|
+
iframe?: IframeConfig;
|
|
464
520
|
}
|
|
465
521
|
export type ProLayoutMode = 'normal' | 'tabs';
|
|
466
522
|
export interface ProLayoutTabsProps extends ProLayoutBaseProps {
|
|
@@ -30,6 +30,20 @@
|
|
|
30
30
|
* @description ProLayout 标签页实例接口(类似 FormInstance)
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @description iframe 路由表配置项
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @description iframe 嵌入配置
|
|
39
|
+
*
|
|
40
|
+
* **触发条件**:URL query 上带 `?isIframe`(大小写不敏感,值不限,只看 key 是否存在)。
|
|
41
|
+
* 第三方系统通过在 iframe src 上拼 `?isIframe=1` 来触发组件库的嵌入模式。
|
|
42
|
+
*
|
|
43
|
+
* 未带该 query 时(独立访问),即使配置了 `tabs.iframe`,组件库也完全不读取,
|
|
44
|
+
* 维持完整 Layout 渲染。
|
|
45
|
+
*/
|
|
46
|
+
|
|
33
47
|
export const isTabsMode = props => props.mode === 'tabs';
|
|
34
48
|
export const validateTabsProps = (mode = 'normal', tabs) => {
|
|
35
49
|
if (mode === 'tabs' && !tabs) {
|
package/es/utils/index.d.ts
CHANGED