Skip to content

Commit b0fcea3

Browse files
author
昔梦
committed
ferat:新增自定义节点配置面板完整案例
1 parent 672a4fb commit b0fcea3

File tree

10 files changed

+825
-1
lines changed

10 files changed

+825
-1
lines changed

docs/xflow/api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ group:
5757
| 属性 | 描述 | 类型 | 默认值 |
5858
| -------------- | ------------------------------------------ | --------- | ------ |
5959
| hideEdgeAddBtn | 是否隐藏两个节点之间,连线上的增加节点按钮 | `boolean` | false |
60-
| deletable | 配置边是否可删除 | `boolean` | false |
60+
| hideEdgeDelBtn | 是否隐藏两个节点之间,连线上的删除按钮, 需要配合deletable一起使用 | `boolean` | false |
61+
| deletable | 配置边是否可用快捷键删除,如果需要同时隐藏删除按钮,则需要设置deletable为true | `boolean` | true |
6162

6263
## TControl
6364

docs/xflow/custom-node-setting.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ export const AdvancedSettingWidget = forwardRef<
161161
162162
<code src="./demo/custom-flow/advancedLinkageCase"></code>
163163
164+
### 完整案例
165+
166+
<code src="./demo/custom-flow/fullCase/index.tsx"></code>
167+
164168
这个示例适用于以下场景:
165169
166170
- 需要根据不同配置类型显示不同表单项的场景
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import '../index.less';
2+
3+
const ReadOnlyPanel = (props) => {
4+
const { value } = props;
5+
6+
return <div className="smart-read-only-panel">{value?? '-'}</div>;
7+
};
8+
9+
export default ReadOnlyPanel;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from 'react';
2+
import { Tag, Popover, Typography, Space } from 'antd';
3+
import '../index.less';
4+
5+
const { Text } = Typography;
6+
7+
interface TagItem {
8+
name: string;
9+
code: string;
10+
description: string;
11+
}
12+
13+
interface TagWidgetProps {
14+
tags?: TagItem[];
15+
style?: React.CSSProperties;
16+
className?: string;
17+
}
18+
19+
const getTagData = (data: any, values: any) => {
20+
const tagData =
21+
(values || [])
22+
?.map((val) => data?.tagData?.find((item) => item.code === val))
23+
.filter(Boolean) || [];
24+
return tagData;
25+
};
26+
27+
const TagWidget: React.FC<TagWidgetProps> = ({ data }) => {
28+
const dataKeys = Object.keys(data);
29+
const nodeType = dataKeys?.includes('firstScene')
30+
? 'firstScene'
31+
: 'secondScene';
32+
const firstSceneData = data?.displayName
33+
? [
34+
{
35+
name: data.displayName,
36+
code: data.firstMeasure,
37+
description: data.description,
38+
color: 'blue',
39+
},
40+
]
41+
: getTagData(data, [data?.firstMeasure] || []);
42+
const nodeData =
43+
nodeType === 'firstScene'
44+
? firstSceneData
45+
: data?.tagWidgetData || getTagData(data, data?.secondMeasures || []);
46+
47+
const renderContent = (tag: TagItem) => (
48+
<Space direction="vertical" size="small" style={{ padding: '8px' }}>
49+
<div>
50+
<Text strong style={{ fontSize: '12px' }}>
51+
指标名称:
52+
</Text>{' '}
53+
{tag.name}
54+
</div>
55+
<div>
56+
<Text strong style={{ fontSize: '12px' }}>
57+
指标代码:
58+
</Text>{' '}
59+
{tag.code}
60+
</div>
61+
<div>
62+
<Text strong style={{ fontSize: '12px' }}>
63+
指标描述:
64+
</Text>{' '}
65+
{tag.description}
66+
</div>
67+
</Space>
68+
);
69+
70+
return (
71+
<div>
72+
{Boolean(data?.firstScene || data?.secondScene) && (
73+
<div style={{ marginBottom: '8px' }}>
74+
<Text strong style={{ fontSize: '12px' }}>
75+
分类:
76+
</Text>
77+
<Text style={{ color: '#1890ff' }}>
78+
{nodeType === 'firstScene' ? data?.firstScene : data?.secondScene}
79+
</Text>
80+
</div>
81+
)}
82+
<Space size="small" style={{ width: '100%' }} wrap>
83+
{(nodeData || []).map((tag: any, index: number) => (
84+
<Popover
85+
key={index}
86+
content={renderContent(tag)}
87+
trigger="hover"
88+
getPopupContainer={() =>
89+
document.getElementById('xflow-container') as HTMLElement
90+
}
91+
overlayClassName="tag-popover"
92+
>
93+
<Tag color={tag.color} key={index}>
94+
{tag.name}
95+
</Tag>
96+
</Popover>
97+
))}
98+
</Space>
99+
</div>
100+
);
101+
};
102+
103+
export default TagWidget;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import FormRender, { useForm } from 'form-render';
2+
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
3+
import { firstTagSchema } from '../setting';
4+
import '../index.less';
5+
import { getColorByIndex } from '..';
6+
import ReadOnlyPanel from './ReadOnlyPanel';
7+
8+
export interface AdvancedSettingWidgetProps {
9+
value: any;
10+
onChange: (value: any) => void;
11+
readOnly?: boolean;
12+
}
13+
14+
export interface AdvancedSettingWidgetRef {
15+
validateForm: () => Promise<boolean>;
16+
}
17+
18+
const CustomWidget = forwardRef<
19+
AdvancedSettingWidgetRef,
20+
AdvancedSettingWidgetProps
21+
>(({ value, onChange, readOnly }, ref) => {
22+
const form = useForm();
23+
24+
// 暴露验证方法
25+
useImperativeHandle(ref, () => ({
26+
validateForm: async () => {
27+
return await form
28+
.validateFields()
29+
.then(() => {
30+
return true;
31+
})
32+
.catch((err) => {
33+
return false;
34+
});
35+
},
36+
}));
37+
38+
// 监听表单变化
39+
const watch = {
40+
'#': (formData) => {
41+
onChange(formData);
42+
},
43+
'firstScene':(value)=>{
44+
if (value) {
45+
form.setValueByPath('title',`一级指标_${value}`)
46+
}
47+
}
48+
};
49+
50+
useEffect(() => {
51+
if (value) {
52+
form.setValues(value);
53+
}
54+
}, []);
55+
56+
return (
57+
<div>
58+
<FormRender
59+
form={form}
60+
schema={firstTagSchema}
61+
watch={watch}
62+
readOnly={readOnly}
63+
onMount={async () => {
64+
const res = await new Promise((resolve) => {
65+
setTimeout(() => {
66+
resolve({
67+
data: [
68+
{
69+
measureName: 'perform_jys_1d_all',
70+
displayName: '用户活跃度指标',
71+
description:
72+
'用户活跃度指标反映了平台用户的参与程度和互动频率。该指标通过分析用户登录次数、浏览时长、功能使用频率等维度综合计算得出。活跃度分为高、中、低三个等级,分别对应不同的用户行为特征和参与度水平。',
73+
},
74+
{
75+
displayName: '用户转化率',
76+
measureName: 'perform_amt_1d',
77+
description: '用户转化率指标衡量了从浏览到实际完成交易的用户比例。该指标通过分析用户行为路径,计算各环节的转化漏斗,识别用户流失的关键节点。转化率受多种因素影响,包括产品展示、价格策略、用户体验等。',
78+
},
79+
{
80+
displayName: '平均停留时长',
81+
measureName: 'perform_adr_1d',
82+
description: '用户平均在平台上的停留时长,反映用户粘性和内容吸引力'
83+
}
84+
]
85+
});
86+
}, 100);
87+
});
88+
const enumData = (res?.data || []).map(
89+
(item: any, index: number) => ({
90+
name: item.displayName,
91+
code: item.measureName,
92+
description: item.description,
93+
color: getColorByIndex(index),
94+
}),
95+
);
96+
form.setValueByPath('tagData', enumData);
97+
form.setSchemaByPath('firstMeasure', {
98+
enum: (res?.data || [])?.map((v) => v.measureName) || [],
99+
enumNames:
100+
(res?.data || [])?.map((item) => `${item.displayName}(${item.measureName})`) || [],
101+
});
102+
}}
103+
size="small"
104+
removeHiddenData={false}
105+
widgets={{ ReadOnlyPanel }}
106+
/>
107+
</div>
108+
);
109+
});
110+
111+
export default CustomWidget;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import FormRender, { useForm } from 'form-render';
2+
import React, {
3+
forwardRef,
4+
useEffect,
5+
useImperativeHandle,
6+
} from 'react';
7+
import { secondTagSchema } from '../setting';
8+
import '../index.less';
9+
import { getColorByIndex } from '..';
10+
import ReadOnlyPanel from './ReadOnlyPanel';
11+
12+
export interface AdvancedSettingWidgetProps {
13+
value: any;
14+
onChange: (value: any) => void;
15+
readOnly?: boolean;
16+
}
17+
18+
export interface AdvancedSettingWidgetRef {
19+
validateForm: () => Promise<boolean>;
20+
}
21+
22+
interface MeasureData {
23+
measureName: string;
24+
displayName: string;
25+
}
26+
27+
const secondTagWidget = forwardRef<
28+
AdvancedSettingWidgetRef,
29+
AdvancedSettingWidgetProps
30+
>(({ value, onChange, readOnly }, ref) => {
31+
const form = useForm();
32+
33+
// 暴露验证方法
34+
useImperativeHandle(ref, () => ({
35+
validateForm: async () => {
36+
return await form
37+
.validateFields()
38+
.then(() => {
39+
return true;
40+
})
41+
.catch((err) => {
42+
return false;
43+
});
44+
},
45+
}));
46+
47+
48+
// 监听表单变化
49+
const watch = {
50+
'#': (formData: any) => {
51+
onChange(formData);
52+
},
53+
secondScene: (value: string) => {
54+
if (value) {
55+
form.setValueByPath('title', `二级指标_${value}`);
56+
}
57+
}
58+
};
59+
60+
useEffect(() => {
61+
if (value) {
62+
form.setValues(value);
63+
}
64+
}, []);
65+
66+
return (
67+
<div>
68+
<FormRender
69+
form={form}
70+
schema={secondTagSchema}
71+
watch={watch}
72+
readOnly={readOnly}
73+
onMount={async () => {
74+
const res = await new Promise((resolve) => {
75+
setTimeout(() => {
76+
resolve({
77+
data: [
78+
{
79+
displayName: '用户转化率',
80+
measureName: 'perform_amt_1d',
81+
description: '用户转化率指标衡量了从浏览到实际完成交易的用户比例。该指标通过分析用户行为路径,计算各环节的转化漏斗,识别用户流失的关键节点。转化率受多种因素影响,包括产品展示、价格策略、用户体验等。'
82+
},
83+
{
84+
displayName: '平均停留时长',
85+
measureName: 'perform_adr_1d',
86+
description: '用户平均在平台上的停留时长,反映用户粘性和内容吸引力'
87+
},
88+
{
89+
displayName: '多端访问量',
90+
measureName: 'xz_ipvuv_daycnt_4client',
91+
description: '多端访问量统计了用户在不同设备(PC、移动端、平板等)上的访问情况,用于分析用户使用习惯和平台适配性'
92+
},
93+
{
94+
displayName: '用户留存率',
95+
measureName: 'perform_jys_1d',
96+
description: '用户留存率反映了用户持续使用平台的比率。该指标通过跟踪用户首次使用后的持续活跃情况,计算不同时间窗口(次日、7日、30日等)的留存率,评估产品的用户粘性和长期价值。'
97+
}
98+
]
99+
});
100+
}, 100);
101+
});
102+
const enumData = (res?.data || []).map(
103+
(item: any, index: number) => ({
104+
name: item.displayName,
105+
code: item.measureName,
106+
description: item.description,
107+
color: getColorByIndex(index),
108+
}),
109+
);
110+
console.log('11hmy', res, enumData)
111+
form.setValueByPath('tagData', enumData);
112+
form.setSchemaByPath('secondMeasures', {
113+
enum:
114+
(res?.data || []).map((v: MeasureData) => v.measureName) || [],
115+
enumNames:
116+
(res?.data || []).map(
117+
(item: MeasureData) =>
118+
`${item.displayName}(${item.measureName})`,
119+
) || [],
120+
});
121+
}}
122+
size="small"
123+
widgets={{ ReadOnlyPanel }}
124+
/>
125+
</div>
126+
);
127+
});
128+
129+
export default secondTagWidget;

0 commit comments

Comments
 (0)