两级树形组件
两级树形组件
两级树形组件
源码
import React, {useState, useCallback, useEffect, useRef} from ‘react’; import {View, Text, TouchableOpacity, StyleSheet, TextInput} from ‘react-native’; import {scaleSizeW} from ‘../../../Utils/AdapterUtil’;
const TreeView = ({ data = [], onSelectionChange, selectedIds = [], config = { idKey: ‘id’, nameKey: ‘name’, childrenKey: ‘slope_devices’ }, showSearch = true, defaultExpandAll = true, debounceTime = 300 // 默认防抖时间300ms }) => { // 配置解构 const {idKey = ‘id’, nameKey = ‘name’, childrenKey = ‘slope_devices’} = config;
// 状态管理 const [expandedNodes, setExpandedNodes] = useState({}); const [selectedItems, setSelectedItems] = useState({}); const [searchText, setSearchText] = useState(‘’); const [displaySearchText, setDisplaySearchText] = useState(‘’); // 用于实际搜索的文本
// 防抖定时器引用 const debounceTimer = useRef(null);
// 初始化展开状态(默认展开所有一级节点) useEffect(() => { const initialExpandedNodes = {}; if (defaultExpandAll) { data.forEach(item => { initialExpandedNodes[String(item[idKey])] = true; }); } setExpandedNodes(initialExpandedNodes); }, [data, idKey, defaultExpandAll]);
// 初始化选中状态(回显功能) useEffect(() => { if (!Array.isArray(selectedIds)) {return;}
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
const initialSelectedItems = {};
const selectedIdsSet = new Set(selectedIds.map(String));
data.forEach(parentItem => {
const parentId = String(parentItem[idKey]);
const children = parentItem[childrenKey] || [];
let childrenSelected = {};
let hasSelectedChild = false;
children.forEach(childItem => {
const childId = String(childItem[idKey]);
if (selectedIdsSet.has(childId)) {
childrenSelected[childId] = true;
hasSelectedChild = true;
}
});
const allChildrenSelected = children.length > 0 &&
children.every(child => selectedIdsSet.has(String(child[idKey])));
if (allChildrenSelected || hasSelectedChild) {
initialSelectedItems[parentId] = {
selfSelected: allChildrenSelected,
someChildrenSelected: hasSelectedChild && !allChildrenSelected,
childrenSelected
};
}
});
setSelectedItems(initialSelectedItems); }, [data, selectedIds, idKey, childrenKey]);
// 防抖处理搜索文本变化 const handleSearchTextChange = useCallback((text) => { setSearchText(text);
1
2
3
4
5
6
7
8
9
// 清除之前的定时器
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// 设置新的定时器
debounceTimer.current = setTimeout(() => {
setDisplaySearchText(text);
}, debounceTime); }, [debounceTime]);
// 组件卸载时清除定时器 useEffect(() => { return () => { if (debounceTimer.current) { clearTimeout(debounceTimer.current); } }; }, []);
// 全选/取消全选 const toggleSelectAll = useCallback(() => { setSelectedItems(prev => { const allSelected = Object.keys(prev).length === 0 || !Object.values(prev).every(item => item.selfSelected);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const newSelectedItems = {};
if (allSelected) {
data.forEach(parentItem => {
const parentId = String(parentItem[idKey]);
const children = parentItem[childrenKey] || [];
newSelectedItems[parentId] = {
selfSelected: true,
childrenSelected: children.reduce((acc, child) => {
acc[String(child[idKey])] = true;
return acc;
}, {})
};
});
}
onSelectionChange?.(getSelectedItems(newSelectedItems));
return newSelectedItems;
}); }, [data, idKey, childrenKey, onSelectionChange, getSelectedItems]);
// 过滤数据(搜索功能)- 使用防抖后的displaySearchText const filteredData = useCallback(() => { if (!displaySearchText.trim()) {return data;}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const searchLower = displaySearchText.toLowerCase();
return data.filter(parentItem => {
const parentName = String(parentItem[nameKey]).toLowerCase();
const isParentMatch = parentName.includes(searchLower);
const children = parentItem[childrenKey] || [];
const hasMatchingChildren = children.some(child =>
String(child[nameKey]).toLowerCase().includes(searchLower)
);
if (isParentMatch && !expandedNodes[String(parentItem[idKey])]) {
setExpandedNodes(prev => ({
...prev,
[String(parentItem[idKey])]: true
}));
}
return isParentMatch || hasMatchingChildren;
}); }, [data, displaySearchText, expandedNodes, nameKey, idKey, childrenKey]);
// 获取选中项 const getSelectedItems = useCallback((selectionMap) => { const selected = []; const processedIds = new Set();
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
Object.keys(selectionMap || {}).forEach(parentId => {
const parentItem = data.find(item => String(item[idKey]) === parentId);
if (!parentItem || !parentItem[childrenKey]) {return;}
if (selectionMap[parentId]?.selfSelected) {
parentItem[childrenKey].forEach(childItem => {
const childId = String(childItem[idKey]);
if (!processedIds.has(childId)) {
processedIds.add(childId);
selected.push(childItem);
}
});
return;
}
const childrenSelected = selectionMap[parentId]?.childrenSelected || {};
Object.keys(childrenSelected).forEach(childId => {
if (childrenSelected[childId] && !processedIds.has(childId)) {
const childItem = parentItem[childrenKey].find(
child => String(child[idKey]) === childId
);
if (childItem) {
processedIds.add(childId);
selected.push(childItem);
}
}
});
});
return selected; }, [data, idKey, childrenKey]);
// 切换展开/收起状态 const toggleExpand = useCallback((parentId) => { setExpandedNodes(prev => ({ …prev, [parentId]: !prev[parentId] })); }, []);
// 切换选择状态 const toggleSelection = useCallback((parentId, childId = null) => { setSelectedItems(prev => { const newSelectedItems = {…prev};
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
if (childId === null) {
// 处理父节点点击
if (newSelectedItems[parentId]?.selfSelected) {
delete newSelectedItems[parentId];
} else {
const parentItem = data.find(item => String(item[idKey]) === parentId);
if (parentItem && parentItem[childrenKey]) {
newSelectedItems[parentId] = {
selfSelected: true,
childrenSelected: parentItem[childrenKey].reduce((acc, child) => {
acc[String(child[idKey])] = true;
return acc;
}, {})
};
}
}
} else {
// 处理子节点点击
if (!newSelectedItems[parentId]) {
newSelectedItems[parentId] = {selfSelected: false, childrenSelected: {}};
}
newSelectedItems[parentId].childrenSelected[childId] =
!newSelectedItems[parentId].childrenSelected[childId];
const parentItem = data.find(item => String(item[idKey]) === parentId);
if (parentItem && parentItem[childrenKey]) {
const allChildrenSelected = parentItem[childrenKey].every(
child => newSelectedItems[parentId].childrenSelected[String(child[idKey])]
);
const someChildrenSelected = parentItem[childrenKey].some(
child => newSelectedItems[parentId].childrenSelected[String(child[idKey])]
);
newSelectedItems[parentId].selfSelected = allChildrenSelected;
newSelectedItems[parentId].someChildrenSelected = someChildrenSelected && !allChildrenSelected;
if (!someChildrenSelected) {
delete newSelectedItems[parentId];
}
}
}
onSelectionChange?.(getSelectedItems(newSelectedItems));
return newSelectedItems;
}); }, [data, getSelectedItems, idKey, childrenKey, onSelectionChange]);
// 检查父节点是否被选中 const isParentSelected = useCallback((parentId) => { return selectedItems[parentId]?.selfSelected; }, [selectedItems]);
// 检查父节点是否是部分选中状态 const isParentSomeSelected = useCallback((parentId) => { return selectedItems[parentId]?.someChildrenSelected; }, [selectedItems]);
// 检查子节点是否被选中 const isChildSelected = useCallback((parentId, childId) => { return selectedItems[parentId]?.childrenSelected?.[childId]; }, [selectedItems]);
// 计算是否所有项目都已选中 const isAllSelected = useCallback(() => { if (data.length === 0) {return false;} return data.every(parentItem => { const parentId = String(parentItem[idKey]); return selectedItems[parentId]?.selfSelected; }); }, [data, selectedItems, idKey]);
// 处理头部点击事件 const handleHeaderPress = useCallback((parentId) => { toggleExpand(parentId); }, [toggleExpand]);
// 渲染树节点 const renderTree = () => { return filteredData().map(parentItem => { const parentId = String(parentItem[idKey]); const children = parentItem[childrenKey] || [];
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
// 如果有搜索词,过滤子节点
const parentName = String(parentItem[nameKey]).toLowerCase();
const isParentMatch = displaySearchText.trim() && parentName.includes(displaySearchText.toLowerCase());
const filteredChildren = isParentMatch
? children // 父节点匹配,显示所有子节点
: displaySearchText.trim()
? children.filter(child =>
String(child[nameKey]).toLowerCase().includes(displaySearchText.toLowerCase()))
: children; // 没有搜索词,显示所有子节点
return (
<View key={parentId} style={styles.parentItem}>
{/* 父节点头部 */}
<TouchableOpacity
style={styles.parentHeader}
onPress={() => handleHeaderPress(parentId)}
activeOpacity={0.7}
>
<View style={styles.headerLeft}>
{/* 父节点复选框 */}
<TouchableOpacity
style={[
styles.checkbox,
isParentSelected(parentId) && styles.checkboxSelected,
isParentSomeSelected(parentId) && styles.checkboxSomeSelected
]}
onPress={(e) => {
e.stopPropagation();
toggleSelection(parentId);
}}
>
<Text style={styles.checkboxText}>
{isParentSelected(parentId) ? '✓' :
isParentSomeSelected(parentId) ? '-' : ''}
</Text>
</TouchableOpacity>
{/* 父节点标题 */}
<Text
style={styles.parentText}
numberOfLines={1}
ellipsizeMode="tail"
>
{parentItem[nameKey]}
</Text>
</View>
{/* 展开/收起箭头 */}
<View style={styles.headerRight}>
{children.length > 0 && (
<Text style={styles.expandIcon}>
{expandedNodes[parentId] ? '▼' : '▶'}
</Text>
)}
</View>
</TouchableOpacity>
{/* 子节点列表 */}
{expandedNodes[parentId] && filteredChildren.length > 0 && (
<View style={styles.childrenContainer}>
{filteredChildren.map(childItem => {
const childId = String(childItem[idKey]);
return (
<View key={childId} style={styles.childItem}>
{/* 子节点复选框 */}
<TouchableOpacity
style={[
styles.checkbox,
isChildSelected(parentId, childId) && styles.checkboxSelected
]}
onPress={() => toggleSelection(parentId, childId)}
>
<Text style={styles.checkboxText}>
{isChildSelected(parentId, childId) ? '✓' : ''}
</Text>
</TouchableOpacity>
{/* 子节点标题 */}
<Text
style={styles.childText}
numberOfLines={1}
ellipsizeMode="tail"
>
{childItem[nameKey]}
</Text>
</View>
);
})}
</View>
)}
</View>
);
}); };
return ( <View style={styles.container}> {/* 搜索框和全选按钮 /} {showSearch && ( <View style={styles.searchContainer}> {/ 全选按钮 */} <TouchableOpacity style={styles.selectAllContainer} onPress={toggleSelectAll} > <View style={[ styles.checkbox, styles.checkbox2, isAllSelected() && styles.checkboxSelected, !isAllSelected() && data.some(parentItem => { const parentId = String(parentItem[idKey]); return selectedItems[parentId]?.someChildrenSelected; }) && styles.checkboxSomeSelected ]}> <Text style={styles.checkboxText}> {isAllSelected() ? ‘✓’ : data.some(parentItem => { const parentId = String(parentItem[idKey]); return selectedItems[parentId]?.someChildrenSelected; }) ? ‘-‘ : ‘’} </Text> </View> <Text style={styles.selectAllText}>全选</Text> </TouchableOpacity>
1
2
3
4
5
6
7
8
9
10
11
12
13
{/* 搜索框 */}
<TextInput
style={styles.searchInput}
placeholder="请输入关键字搜索"
value={searchText}
onChangeText={handleSearchTextChange}
/>
</View>
)}
{/* 树形列表 */}
{renderTree()}
</View> ); };
// 组件样式 const styles = StyleSheet.create({ container: { paddingVertical: scaleSizeW(24), }, searchContainer: { flexDirection: ‘row’, alignItems: ‘center’, marginBottom: 10, }, selectAllContainer: { flexDirection: ‘row’, alignItems: ‘center’, marginRight: 10, }, selectAllText: { marginLeft: scaleSizeW(4), fontSize: scaleSizeW(28), color: ‘#2E3033’, }, searchInput: { flex: 1, height: scaleSizeW(64), borderWidth: 1, borderColor: ‘#E5E6EB’, borderRadius: scaleSizeW(8), paddingHorizontal: scaleSizeW(20), backgroundColor: ‘#fff’, marginLeft: scaleSizeW(24), }, parentItem: { marginBottom: 10, borderRadius: scaleSizeW(8), overflow: ‘hidden’, }, parentHeader: { flexDirection: ‘row’, alignItems: ‘center’, justifyContent: ‘space-between’, padding: scaleSizeW(18), backgroundColor: ‘#F2F3F5’, }, headerLeft: { flexDirection: ‘row’, alignItems: ‘center’, flex: 1, marginRight: 10, overflow: ‘hidden’, }, headerRight: { marginLeft: 10, }, expandIcon: { fontSize: 14, color: ‘#666’, minWidth: 20, textAlign: ‘right’, }, parentText: { color: ‘#2E3033’, fontSize: scaleSizeW(28), marginLeft: scaleSizeW(8), flexShrink: 1, }, checkbox: { width: scaleSizeW(28), height: scaleSizeW(28), borderWidth: 1, borderColor: ‘#999’, borderRadius: 4, justifyContent: ‘center’, alignItems: ‘center’, flexShrink: 0, }, checkbox2: { width: scaleSizeW(32), height: scaleSizeW(32), }, checkboxSelected: { backgroundColor: ‘#3A86FF’, borderColor: ‘#3A86FF’, }, checkboxSomeSelected: { backgroundColor: ‘#3A86FF’, borderColor: ‘#3A86FF’, }, checkboxText: { color: ‘white’, fontWeight: ‘bold’, fontSize: scaleSizeW(18), }, childrenContainer: { paddingLeft: 0, paddingTop: 5, paddingBottom: 5, }, childItem: { flexDirection: ‘row’, alignItems: ‘center’, paddingVertical: scaleSizeW(12), paddingLeft: scaleSizeW(56), }, childText: { color: ‘#2E3033’, marginLeft: scaleSizeW(8), fontSize: scaleSizeW(28), flexShrink: 1, flex: 1, }, });
export default TreeView;
使用
<TreeView data={options} selectedIds={selectedIds}
config={{ idKey: ‘id’, nameKey: ‘name’, childrenKey: ‘slope_devices’ }}
onSelectionChange={handleSelectionChange} />