# 树形搜索
AssetTree.tsx
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { SearchOutlined } from '@ant-design/icons';
import { Empty, Input, Spin, Typography } from 'antd';
import Tree, { DataNode } from 'antd/lib/tree';
import { useTranslation } from 'react-i18next';
import {
filterTreePath,
getAllExpendKeyFromFirstChild,
getEntireTreeNodeKeys,
omitRootKey,
getAllLeafNodeByKey
} from './util';
interface IProps {
loading: boolean;
title?: string;
checkable?: boolean;
dataList?: Array<DataNode>;
defaultSelectedValue?: Array<string>;
defaultCheckedValue?: Array<string>;
defaultExpandValue?: string[];
onSelect?: (keys: string[]) => void;
// onCheck?: (key: string) => void;
treeWrapProps?: any;
rootKey?: string;
[key: string]: any;
}
/**
* 资产树筛选组件
* 默认展开一级资产的所有节点
* 筛选后,高亮资产名,并收缩非关键路径节点
*/
const AssetTree = ({
loading = false,
title,
checkable = false,
defaultSelectedValue = [],
defaultCheckedValue = [],
defaultExpandValue,
dataList = [],
onSelect,
// onCheck,
treeWrapProps = {},
rootKey = '',
...props
}: IProps) => {
const { t } = useTranslation();
const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
const [expandedKeys, setExpandedKeys] = useState<Array<string>>(defaultSelectedValue);
const [treeData, setTreeData] = useState<Array<DataNode>>(dataList);
const [selectedKeys, setSelectedKeys] = useState<Array<string>>(defaultSelectedValue);
const [halfCheckedKeys, setHalfChecked] = useState<Array<string>>(
selectedKeys.length && !selectedKeys.includes(rootKey) ? [rootKey] : []
);
const [entireTreeNodeKeys, setEntireTreeNodeKeys] = useState<string[]>([]);
useEffect(() => {
const keys = getEntireTreeNodeKeys(dataList);
setEntireTreeNodeKeys(keys);
if (defaultExpandValue.length) {
setExpandedKeys(defaultExpandValue);
setSelectedKeys(defaultSelectedValue);
if (defaultSelectedValue.length === keys.length) {
setHalfChecked([]);
setSelectedKeys([...defaultSelectedValue, rootKey]);
} else if (defaultSelectedValue.length > 0) {
setHalfChecked([rootKey]);
} else {
setHalfChecked([]);
}
} else if (dataList.length) {
const expandedKeys = getAllExpendKeyFromFirstChild(dataList, true);
setExpandedKeys(expandedKeys);
}
setTreeData(dataList);
}, [dataList, defaultExpandValue]);
// 重置为原始数据
// const resetTreeData = () => {
// setTreeData(dataList);
// };
const onSearch = (e: { target: { value: any } }) => {
const { value } = e.target;
const [taggedTreeData, expandedPath] = filterTreePath(value, dataList);
if (!value.length) {
// 搜索被清空
if (!selectedKeys.length) {
// 默认展开一级所有节点
const expandedKeys = getAllExpendKeyFromFirstChild(dataList, true);
setExpandedKeys(expandedKeys);
} else {
// 展开选择节点
setExpandedKeys(getAllLeafNodeByKey(taggedTreeData, selectedKeys[0], false));
}
} else {
setExpandedKeys(expandedPath);
}
setTreeData(taggedTreeData);
setAutoExpandParent(true);
};
const handleExpand = expandedKeys => {
setAutoExpandParent(false);
setExpandedKeys(expandedKeys);
};
// const handleSelect = (selectedKeys) => {
// if (selectedKeys.length) {
// const key = selectedKeys[selectedKeys.length - 1];
// setSelectedKeys([key]);
// onSelect(key);
// }
// };
const handleCheck = ({ checked, halfChecked }, info) => {
const {
checked: checkedValue,
node: { key },
} = info;
if (key === rootKey) {
if (checkedValue) {
// 全选状态
setSelectedKeys(entireTreeNodeKeys);
onSelect(omitRootKey(entireTreeNodeKeys, rootKey));
setHalfChecked([]);
return;
}
// 取消全选
setSelectedKeys([]);
onSelect([]);
setHalfChecked([]);
return;
}
const getChildrenKeys = node => {
const keys = [];
let stacks = [node];
while (stacks.length) {
const { key, isLeaf, children } = stacks.shift();
keys.push(key);
if (!isLeaf) {
stacks = [...stacks, ...(children || [])];
}
}
// remove root node key
return keys.slice(1);
};
const childKeys = getChildrenKeys(info.node);
if (checkedValue) {
// 子孙节点也需要全选上
const temp = [...checked, ...childKeys];
if (checked.length + 1 === entireTreeNodeKeys.length) {
temp.push(rootKey);
setHalfChecked([]);
} else if (checked.length) {
setHalfChecked([rootKey]);
}
setSelectedKeys(temp);
onSelect(omitRootKey(temp, rootKey));
return;
}
// 子孙节点需要取消选中
checked = checked.filter(key => !childKeys.includes(key));
const omitRootList = omitRootKey(checked, rootKey);
setSelectedKeys(omitRootList);
onSelect(omitRootList);
if (omitRootList.length) {
setHalfChecked([rootKey]);
return;
}
setHalfChecked([]);
// console.log(checked, halfChecked);
// const checkedKeysTemp = [...checked];
// const newArrIncludeRoot = checked.includes(rootKey);
// const oldArrIncludeRoot = selectedKeys.includes(rootKey);
// // const newHalfIncludeRoot = halfChecked.includes(rootKey);
// // const oldHalfIncludeRoot = halfCheckedKeys.includes(rootKey);
// // checked 有变化,新增root
// if (newArrIncludeRoot && !oldArrIncludeRoot) {
// // 全选状态
// setSelectedKeys(entireTreeNodeKeys);
// onSelect(omitRootKey(entireTreeNodeKeys, rootKey));
// setHalfChecked([]);
// return;
// }
// if (!newArrIncludeRoot && oldArrIncludeRoot) {
// // 取消全选
// setSelectedKeys([]);
// onSelect([]);
// setHalfChecked([]);
// return;
// }
// if (!newArrIncludeRoot && !oldArrIncludeRoot) {
// const temp = [...checked];
// if (checked.length + 1 === entireTreeNodeKeys.length) {
// temp.push(rootKey);
// setHalfChecked([]);
// } else if (checked.length) {
// setHalfChecked([rootKey]);
// }
// setSelectedKeys(temp);
// onSelect(omitRootKey(temp, rootKey));
// return;
// }
// if (newArrIncludeRoot && oldArrIncludeRoot) {
// const omitRootList = omitRootKey(checked, rootKey);
// setSelectedKeys(omitRootList);
// onSelect(omitRootList);
// setHalfChecked([rootKey]);
// }
};
return (
<Spin spinning={loading}>
<Input
allowClear
// maxLength={20}
style={{ marginBottom: 15 }}
placeholder={t('assetTree.searchPlaceholder')}
onChange={onSearch}
suffix={<SearchOutlined />}
/>
{!loading && treeData.length ? (
<div {...treeWrapProps} id="debugTree">
<Tree
checkStrictly
checkable={checkable}
onExpand={handleExpand}
autoExpandParent={autoExpandParent}
expandedKeys={expandedKeys}
checkedKeys={{
checked: selectedKeys,
halfChecked: halfCheckedKeys,
}}
defaultCheckedKeys={defaultCheckedValue}
// onSelect={handleSelect}
onCheck={handleCheck}
treeData={treeData}
/>
</div>
) : (
<Empty />
)}
</Spin>
);
};
export default AssetTree;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
util.ts
import React from 'react';
import { DataNode } from 'antd/lib/tree';
import styles from './index.module.less';
// 过滤资产节点,只显示有关键字的叶子节点和其关键路径上的节点
export const filterTreePath = (searchKey: string, dataList: Array<DataNode>) => {
const result = [];
const path = [];
if (!searchKey.length) {
return [dataList, path];
}
dataList.forEach(item => {
const newItem = {
...item,
};
const index = (newItem.title as string).indexOf(searchKey);
if (index > -1) {
// 记录key
path.push(item.key);
const beforeStr = (newItem.title as string).substr(0, index);
const afterStr = (newItem.title as string).substr(index + searchKey.length);
newItem.title = (
<span>
{beforeStr}
<span className={styles['tree-highlight-value']}>{searchKey}</span>
{afterStr}
</span>
);
// 叶子节点保存
if (!newItem.children) {
result.push(newItem);
}
}
const prePathLen = path.length;
// 继续遍历子节点
if (newItem.children) {
const [children, subPath] = filterTreePath(searchKey, newItem.children);
newItem.children = children;
path.push(...subPath);
}
const afterPathLen = path.length;
// 子节点遍历完,对比前后路径长度变化,如果增加,将当前节点加入结果集
if (afterPathLen > prePathLen) {
result.push(newItem);
if (index === -1) {
// 补齐path
path.push(newItem.key);
}
} else if (index > -1 && newItem.children) {
// 非叶节点命中
result.push(newItem);
}
});
return [result, path];
};
export const getAllExpendKeyFromFirstChild = (dataList: Array<DataNode>, isFirst = false) => {
const keys = [];
if (isFirst) {
const firstChild = dataList[0];
keys.push(
firstChild.key,
...getAllExpendKeyFromFirstChild(firstChild.children ? firstChild.children : [])
);
} else if (dataList.length) {
dataList.forEach(item => {
keys.push(item.key, ...getAllExpendKeyFromFirstChild(item.children ? item.children : []));
});
}
return keys;
};
export const getEntireTreeNodeKeys = (dataList, filterValidNode = true) => {
const result = [];
dataList.forEach(item => {
if (filterValidNode && item.disabled) {
// 需要过滤节点,且节点不可用
} else {
result.push(item.key);
}
if (item.children?.length) {
result.push(...getEntireTreeNodeKeys(item.children));
}
});
return result;
};
export const omitRootKey = (list, rootKey) => {
const newList = (list || []).filter(item => {
return item !== rootKey;
});
return newList;
};
// 找到树中,某个节点的所有叶子节点
export const getAllLeafNodeByKey = (tree, key, inPath) => {
const result = [];
if (!Array.isArray(tree) || !tree.length) {
return result;
}
for (let i = 0; i < tree.length; i++) {
const item = tree[i];
if (item.key === key) {
result.push(item.key, ...getAllLeafNodeByKey(item.children, key, true));
break;
}
if (inPath) {
if (item.isLeaf) {
result.push(item.key);
} else {
result.push(...getAllLeafNodeByKey(item.children, key, true));
}
} else {
const subResult = getAllLeafNodeByKey(item.children, key, false);
if (subResult.length) {
// 补齐父节点
result.push(item.key, ...subResult);
} else {
result.push(...subResult);
}
}
}
return [...result];
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# 表单封装
import { useImperativeHandle, forwardRef } from "react";
import {
Form,
Input,
Button,
Space,
type FormProps,
type FormItemProps,
type FormInstance,
} from "antd";
export interface formItem {
itemProps: FormItemProps;
component?: () => React.ReactNode;
}
type IProps = {
[k in keyof FormProps]: FormProps[k];
} & {
formItems: formItem[];
onFinish: ((values: any) => void) | undefined;
onReset?: () => void;
};
const MyForm = forwardRef(
(
{ formItems, onFinish, onReset, ...formProps }: IProps,
ref?: React.ForwardedRef<FormInstance>
) => {
const [antForm] = Form.useForm();
useImperativeHandle(ref, () => antForm);
const handleReset = () => {
antForm.resetFields();
onReset?.();
};
return (
<Form
form={antForm}
layout="vertical"
onFinish={onFinish}
style={{ display: "flex", columnGap: 20, flexWrap: "wrap" }}
{...formProps}
>
{formItems.map((item, index) => (
<Form.Item style={{ minWidth: 200 }} {...item.itemProps} key={index}>
{item.component ? item.component() : <Input />}
</Form.Item>
))}
{formItems.length && (
<Form.Item style={{ margin: 0, display: "flex", alignItems: "center" }}>
<Space>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button htmlType="button" onClick={handleReset}>
Reset
</Button>
</Space>
</Form.Item>
)}
</Form>
);
}
);
export default MyForm;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
使用
import { FormInstance } from "antd";
import { useState, useRef } from "react";
import MyForm, { type formItem } from "@/components/MyForm";
const Test = () => {
const ref = useRef<FormInstance<any>>(null);
const [formItems, setformItems] = useState<formItem[]>([
{
itemProps: { label: "Devices Name", name: "name" },
},
{
itemProps: { label: "Devices ID", name: "id" },
},
{
itemProps: { label: "Product ID", name: "productId" },
},
]);
const onFinish = () => {};
return <MyForm ref={ref} formItems={formItems} onFinish={onFinish} />;
};
export default Test;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Promise弹窗(mobile)
import { Dialog } from 'antd-mobile';
import type { DialogConfirmProps } from 'antd-mobile/es/components/dialog';
type DialogProps = Omit<DialogConfirmProps, 'onConfirm' | 'onCancel'> & {
onConfirm: () => any;
onCancel: () => any;
confirmResultType?: 'resolve' | 'reject';
cancelResultType?: 'resolve' | 'reject';
};
export default function PromiseDialog(dialogProps: DialogProps) {
const {
cancelResultType = 'resolve',
confirmResultType = 'resolve',
onConfirm = () => '点击confirm',
onCancel = () => '点击cancel',
} = dialogProps;
return new Promise((resolve, reject) => {
const newDialogProps = {
...dialogProps,
onConfirm: () => {
confirmResultType === 'resolve' ? resolve(onConfirm()) : reject(onConfirm());
},
onCancel: () => {
cancelResultType === 'resolve' ? resolve(onCancel()) : reject(onCancel());
},
};
Dialog.confirm(newDialogProps);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
使用
await PromiseDialog({
title: modalItem.name,
content: RecommendedContent(modalItem),
cancelText: '取消',
confirmText: '下一步',
cancelResultType: 'reject', // 设为 reject 直接抛出错误
});
// ... 下一步的操作
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8