文章

两级树形组件

两级树形组件

两级树形组件

两级树形组件

源码

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} />

本文由作者按照 CC BY 4.0 进行授权