树形选择器组件封装

1. 组件概述与设计理念

1.1 组件定位

IndustryBulletFrame是一个高度可配置的Flutter树形结构选择器组件,专门用于处理行业分类、组织架构等具有层级关系的数据选择场景。组件支持单选/多选、搜索过滤、展开折叠、标签展示等完整功能。

1.2 设计特点

  • 参数驱动配置:通过14个构造参数实现全方位自定义
  • 树形算法核心:自动将扁平数据转换为树形结构
  • 状态内聚管理:内部统一管理选中、展开等状态
  • 智能选中合并:多选模式下自动合并完整子树
  • 响应式UI:基于状态变化自动重建界面

2. 核心数据结构处理

2.1 组件类定义与参数说明

复制代码
class IndustryBulletFrame extends StatefulWidget {
  /// 初始选中的值
  final dynamic initialSelectedValue;

  /// 成功回调
  final Function(dynamic selectedValue) onSuccess;

  /// 选中值改变回调
  final Function(dynamic selectedValue)? onSelectedChange;

  ///是否显示顶部标题:true-显示,false-不显示
  final bool isShowTopTitle;

  ///是否显示底部标签
  final bool isShowBottomLabel;

  ///上滑弹框顶部title
  final String? topTitle;

  ///是否显示底部按钮:true-显示,false-不显示
  final bool isShowBottomButton;
  
  IndustryBulletFrame({
    Key? key,
    this.isMultiSelect = false,
    this.isBottomSheetBarrier = false,
    this.isShowTopTitle = false,
    this.isShowBottomLabel = false,
    this.isShowBottomButton = true,
    this.topTitle = '行业',
    required this.options,
    this.onSelectedChange,
    required this.onSuccess,
    this.initialSelectedValue,
  }) : super(key: key);
}

参数说明

  • options:必填参数,扁平行业数据列表,后端只需提供包含父子关系的列表即可
  • isMultiSelect:核心模式开关,控制单选/多选行为
  • initialSelectedValue:支持传入初始选中值,组件会自动处理回显
  • 回调函数:onSuccess为确定回调,onSelectedChange为实时变化回调

2.2 扁平数据转树形结构

核心算法_buildIndustryTreeFromFlat()方法

复制代码
List<Map<String, dynamic>> _buildIndustryTreeFromFlat(
  List<Map<String, dynamic>> flatList,
) {
  // 1. 创建节点映射表用于快速查找
  final Map<int, Map<String, dynamic>> nodeMap = {};
  for (final item in flatList) {
    final rawId = item['id'];
    final id = rawId is int
        ? rawId
        : (rawId is String ? int.tryParse(rawId) : null);
    if (id == null) continue;

    // 拷贝并确保children字段存在
    final copied = Map<String, dynamic>.from(item);
    copied['children'] = (copied['children'] is List)
        ? List.from(copied['children'])
        : <Map<String, dynamic>>[];
    nodeMap[id] = copied;
  }

  // 2. 组装父子关系
  final List<Map<String, dynamic>> roots = [];
  nodeMap.forEach((id, node) {
    final parentRaw = node['industryParentId'];
    final parentId = parentRaw is int
        ? parentRaw
        : (parentRaw is String ? int.tryParse(parentRaw) : null);

    if (parentId == null || parentId == 0 || !nodeMap.containsKey(parentId)) {
      // 无父节点,作为根节点
      roots.add(node);
    } else {
      // 添加到父节点的children列表
      final parent = nodeMap[parentId]!;
      (parent['children'] as List).add(node);
    }
  });
  
  // 3. 递归排序确保显示顺序一致
  _sortTreeNodes(roots);
  
  return roots;
}

算法解析

  1. 建立索引:遍历所有节点,建立ID到节点的映射表,时间复杂度O(n)
  2. 构建关系 :再次遍历,根据industryParentId找到父节点并建立父子关系
  3. 排序处理 :对每个层级的子节点按industryLevelid排序,保证UI展示稳定

数据格式要求

复制代码
// 每个节点必须包含的字段
{
  "id": 1,                    // 唯一标识,支持int或可转int的String
  "industryName": "互联网",    // 行业名称
  "industryParentId": 0,      // 父级ID,0或null表示根节点
  "industryLevel": 1          // 层级(用于排序)
}

3. 选中状态管理机制

3.1 核心状态变量

复制代码
class _IndustryBulletFrameState extends State<IndustryBulletFrame> {
  // 展开的值集合 - 控制哪些父节点处于展开状态
  Set<int> _expandedValues = {};
  
  // 叶子节点选中状态 - 只记录叶子节点的true/false状态
  Map<int, bool> _leafSelected = {};
  
  // 当前选中的值 - 用于回调和标签展示(已智能合并)
  List<Map<String, dynamic>> _selectedValues = [];
  
  // 树形结构数据 - 转换后的完整树
  List<Map<String, dynamic>> _treeOptions = [];
  
  // 展示的列表 - 考虑搜索过滤后的结果
  List<Map<String, dynamic>> _displayOptions = [];
}

状态设计理念

  • _leafSelected:作为唯一的状态源,只记录叶子节点的最终选中状态
  • _selectedValues:派生的展示状态,用于UI标签和回调
  • 分离设计保证状态一致性,避免冗余和冲突

3.2 多选模式的三态逻辑

核心方法_computeNodeState()- 计算节点的选中状态

复制代码
bool? _computeNodeState(Map<String, dynamic> node) {
  final children = _childrenOf(node);

  // 叶子节点:直接看自己的状态
  if (children.isEmpty) {
    final id = _getItemId(node);
    if (id == null) return false;
    return _leafSelected[id] == true;  // true:选中, false:未选
  }

  // 父节点:递归计算子节点状态
  bool? aggregateState;
  for (final child in children) {
    final childState = _computeNodeState(child);
    
    if (aggregateState == null) {
      aggregateState = childState;
    } else if (aggregateState != childState) {
      return null;  // 子节点状态不一致,返回半选状态
    }
  }
  
  // 返回聚合状态,全部为null时当作未选
  return aggregateState ?? false;
}

三态说明

  • true:全选 - 该节点下所有叶子节点都被选中
  • false:未选 - 该节点下所有叶子节点都未选中
  • null:半选 - 该节点下部分叶子节点被选中

3.3 智能选中合并算法

核心方法collectWithMerge()- 在_rebuildSelectedValues()中调用

复制代码
void collectWithMerge(List<Map<String, dynamic>> nodes) {
  for (final node in nodes) {
    final children = _childrenOf(node);

    if (children.isEmpty) {
      // 叶子节点:如果被选中,直接添加
      final id = _getItemId(node);
      if (id != null && _leafSelected[id] == true) {
        _selectedValues.add(node);
      }
    } else {
      // 父节点:检查是否所有后代叶子都被选中
      if (_areAllDescendantLeavesSelected(node)) {
        // 所有后代都被选中,合并到当前节点
        _selectedValues.add(node);
        // 跳过所有后代,不再递归
      } else {
        // 没有全部选中,递归检查子节点
        collectWithMerge(children);
      }
    }
  }
}

合并示例

复制代码
原始选中状态:
- 互联网 (id:1)
  - 电子商务 (id:11) ✓
  - 社交网络 (id:12) ✓
  - 在线游戏 (id:13) ✓
- 金融 (id:2)
  - 银行 (id:21) ✓
  - 证券 (id:22) ✗
  - 保险 (id:23) ✓

智能合并结果:
_selectedValues = [
  {id:1, name:"互联网"},      // 所有子节点全选,合并为父节点
  {id:21, name:"银行"},      // 单个叶子节点
  {id:23, name:"保险"}       // 单个叶子节点
]
// 注意:id:22未选中,id:2未全选所以不合并

3.4 单选/多选模式差异化处理

核心方法_onNodeToggle()- 处理节点点击

复制代码
void _onNodeToggle(Map<String, dynamic> node, bool selected) {
  if (widget.isMultiSelect) {
    // 多选模式:只更新这个节点子树的叶子,保留其它已选
    _setDescendantLeaves(node, selected);
    _rebuildSelectedValues();
  } else {
    // 单选模式:先清空所有
    _leafSelected.clear();
    _selectedValues.clear();

    if (selected) {
      final children = _childrenOf(node);
      
      if (children.isEmpty) {
        // 点击的是叶子节点(子级)
        final id = _getItemId(node);
        if (id != null) {
          _leafSelected[id] = true;
          _selectedValues.add(Map<String, dynamic>.from(node));
        }
      } else {
        // 点击的是父节点
        _setDescendantLeaves(node, true);  // UI显示:勾选所有子级
        _selectedValues.add(Map<String, dynamic>.from(node));  // 回调:只传父级
      }
    }
  }

  _notifySelectedChange();
  setState(() {});
}

模式对比

|----------|----------------|-------------|
| 特性 | 单选模式 | 多选模式 |
| 选择行为 | 互斥,只能选一个 | 叠加,可多选 |
| UI反馈 | 点击父节点视觉上勾选所有子项 | 显示三态复选框 |
| 回调数据 | 返回单个节点 | 返回智能合并列表 |
| 初始值 | Map 或 [Map] | List<Map> |

3.5 初始值初始化

核心方法_initializeSelectedValue()

复制代码
void _initializeSelectedValue() {
  _leafSelected.clear();
  _selectedValues.clear();

  if (widget.initialSelectedValue == null) return;

  // 将initialSelectedValue统一转换为节点列表
  final List<Map<String, dynamic>> initialNodes = [];

  if (widget.isMultiSelect) {
    if (widget.initialSelectedValue is List) {
      for (final e in widget.initialSelectedValue as List) {
        if (e is Map<String, dynamic>) initialNodes.add(e);
      }
    }
  } else {
    // 单选:支持[ { ... } ] 或直接 Map
    if (widget.initialSelectedValue is List &&
        (widget.initialSelectedValue as List).isNotEmpty) {
      final first = (widget.initialSelectedValue as List).first;
      if (first is Map<String, dynamic>) initialNodes.add(first);
    } else if (widget.initialSelectedValue is Map<String, dynamic>) {
      initialNodes.add(widget.initialSelectedValue as Map<String, dynamic>);
    }
  }

  // 根据初始节点,设置对应子树下所有叶子的选中状态
  for (final initialNode in initialNodes) {
    final nodeId = _getItemId(initialNode);
    if (nodeId == null) continue;

    final nodeInTree = _findNodeById(nodeId, _treeOptions);
    if (nodeInTree == null) continue;

    // 设置该节点所有后代叶子为选中
    _setDescendantLeaves(nodeInTree, true);

    if (!widget.isMultiSelect) {
      _selectedValues.clear();
      _selectedValues.add(Map<String, dynamic>.from(nodeInTree));
    }
  }

  // 多选模式:使用智能合并逻辑
  if (widget.isMultiSelect) {
    _rebuildSelectedValues();
  }
}

4. 搜索过滤功能实现

4.1 树形搜索算法

核心方法searchOptions()- 支持关键词搜索并保持树形结构

复制代码
List<Map<String, dynamic>> searchOptions(
  String keyword,
  List<Map<String, dynamic>> options,
) {
  _expandedValues.clear();  // 清空原有展开状态
  
  // 匹配的返回结果
  List<Map<String, dynamic>> results = [];

  // 核心递归搜索函数
  Map<String, dynamic>? searchSubtree(Map<String, dynamic> node) {
    // 判断当前节点是否匹配关键字
    final industryName = _getItemName(node);
    final isMatch = industryName.toLowerCase().contains(keyword.toLowerCase());
    final children = _castToMapList(node['children']);

    // 优先递归子节点
    List<Map<String, dynamic>>? filteredChildren;
    if (children.isNotEmpty) {
      final matchedChildren = <Map<String, dynamic>>[];
      for (final child in children) {
        final matchedChild = searchSubtree(child);
        if (matchedChild != null) {
          // 如果匹配到子节点,将其添加到结果中
          final childId = _getItemId(child);
          if (childId != null) {
            _expandedValues.add(childId);  // 自动展开匹配路径
          }
          matchedChildren.add(matchedChild);
        }
      }
      if (matchedChildren.isNotEmpty) {
        filteredChildren = matchedChildren;
      }
    }

    // 当前节点匹配 或 子节点匹配,都需要保留
    if (isMatch || filteredChildren != null) {
      final newNode = Map<String, dynamic>.from(node);
      if (filteredChildren != null) {
        newNode['children'] = filteredChildren;
      }
      // 自动展开匹配节点
      final newNodeId = _getItemId(newNode);
      if (newNodeId != null) {
        _expandedValues.add(newNodeId);
      }
      return newNode;
    }
    
    return null;  // 不匹配且无匹配子节点
  }

  // 遍历所有根节点
  for (final root in options) {
    final matchedTree = searchSubtree(root);
    if (matchedTree != null) {
      results.add(matchedTree);
    }
  }
  
  return results;
}

搜索特性

  1. 深度优先遍历:递归搜索整棵树
  2. 路径保持:保留匹配节点到根节点的完整路径
  3. 自动展开:匹配节点及其祖先自动展开,确保可见性
  4. 防抖优化:300ms防抖避免频繁重建

4.2 防抖搜索实现

复制代码
String _searchKeyword = '';
Timer? _searchDebounce;  // 防抖计时器

// 在TextField的onChanged中
onChanged: (value) {
  _searchDebounce?.cancel();  // 取消之前的计时器
  _searchDebounce = Timer(const Duration(milliseconds: 300), () {
    if (!mounted) return;
    setState(() {
      _searchKeyword = value;
      _displayOptions = value.isEmpty
          ? _treeOptions
          : searchOptions(value, _treeOptions);
    });
  });
},

5. UI渲染与交互实现

5.1 层级缩进渲染

核心组件_levelSelection()- 渲染单个树节点

复制代码
Widget _levelSelection({
  required int depth,
  required String industryName,
  required int id,
  bool hasChildren = false,
  bool isExpanded = false,
  VoidCallback? onTap,
  String searchKeyword = '',
  required Map<String, dynamic> item,
}) {
  // 动态计算缩进:每层增加12逻辑像素
  double getIndentLeft() => (depth + 1) * 12.w;
  
  return Container(
    width: double.infinity,
    color: depth >= 1 ? Color(0xFFF4F4F4) : Color(0xFFFFFFFF),
    child: Column(
      children: [
        // 分隔线
        Container(
          width: double.infinity,
          height: 1.h,
          color: Color(0xFF000000).withOpacity(0.15),
        ),
        Container(
          padding: EdgeInsets.only(left: getIndentLeft(), right: 16.w),
          child: Row(
            children: [
              // 层级指示线(从第二层开始显示)
              if (depth >= 1) ...[
                Image.asset(
                  'assets/icon/vector1.png',
                  width: 10.sp,
                  height: 10.sp,
                ),
              ],
              // 复选框
              Checkbox(
                tristate: widget.isMultiSelect,  // 多选支持三态
                value: widget.isMultiSelect
                    ? _computeNodeState(item)  // 多选:true/false/null
                    : _computeNodeState(item) == true,  // 单选:true/false
                onChanged: (_) {
                  if (widget.isMultiSelect) {
                    final state = _computeNodeState(item);
                    // true -> 本次点击全取消;false/null -> 本次点击全选
                    final bool target = (state == true) ? false : true;
                    _onNodeToggle(item, target);
                  } else {
                    // 单选模式:检查是否点击了已选中的节点
                    final currentSelectedId = _selectedValues.isNotEmpty
                        ? _getItemId(_selectedValues.first)
                        : null;
                    final clickedId = _getItemId(item);
                    final bool shouldSelect = (currentSelectedId != clickedId);
                    _onNodeToggle(item, shouldSelect);
                  }
                },
              ),
              // 行业名称和展开箭头
              Expanded(
                child: GestureDetector(
                  onTap: onTap,  // 点击整行可展开/收起
                  child: Container(
                    padding: EdgeInsets.symmetric(vertical: 13.h),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        _buildHighlightedText(industryName, searchKeyword),
                        if (hasChildren) ...[
                          AnimatedSwitcher(
                            duration: Duration(milliseconds: 100),
                            child: Transform.rotate(
                              angle: isExpanded ? 0 : pi,  // 旋转箭头
                              child: Image.asset(
                                'assets/icon/down.png',
                                width: 16.sp,
                                height: 16.sp,
                              ),
                            ),
                          ),
                        ],
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

5.2 关键词高亮显示

核心方法_buildHighlightedText()- 实现搜索关键词高亮

复制代码
Widget _buildHighlightedText(String text, String searchKeyword) {
  if (searchKeyword.trim().isEmpty) {
    return Text(
      text,
      style: TextStyle(
        fontSize: 12.sp,
        fontWeight: FontWeight.w800,
        color: Color(0xFF1D2129).withOpacity(0.65),
      ),
    );
  }

  final lowerText = text.toLowerCase();
  final lowerKeyword = searchKeyword.toLowerCase();

  if (!lowerText.contains(lowerKeyword)) {
    return Text(
      text,
      style: TextStyle(
        fontSize: 12.sp,
        fontWeight: FontWeight.w800,
        color: Color(0xFF1D2129).withOpacity(0.65),
      ),
    );
  }

  // 构建高亮文本片段
  final spans = <TextSpan>[];
  int start = 0;

  while (true) {
    final index = lowerText.indexOf(lowerKeyword, start);
    if (index == -1) {
      if (start < text.length) {
        spans.add(TextSpan(
          text: text.substring(start),
          style: TextStyle(/* 基础样式 */),
        ));
      }
      break;
    }

    // 非匹配部分
    if (index > start) {
      spans.add(TextSpan(
        text: text.substring(start, index),
        style: TextStyle(/* 基础样式 */),
      ));
    }

    // 匹配部分(高亮)
    spans.add(TextSpan(
      text: text.substring(index, index + searchKeyword.length),
      style: TextStyle(
        fontSize: 12.sp,
        fontWeight: FontWeight.w800,
        color: Color(0xFF026CE7),  // 高亮蓝色
      ),
    ));

    start = index + searchKeyword.length;
  }

  return RichText(text: TextSpan(children: spans));
}

6. 已选标签展示与删除

6.1 标签生成与展示

核心方法_buildSelectedTags()- 构建标签列表

复制代码
List<Widget> _buildSelectedTags() {
  return _selectedValues.map((node) {
    final id = _getItemId(node);
    final name = _getItemName(node);
    if (id == null) return SizedBox.shrink();
    
    return Container(
      margin: EdgeInsets.only(right: 7.w),
      padding: EdgeInsets.symmetric(horizontal: 7.w, vertical: 4.h),
      decoration: BoxDecoration(
        color: Color(0xFFF3F3F3),
        borderRadius: BorderRadius.circular(4.r),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Flexible(
            child: Text(
              name,
              style: TextStyle(
                fontSize: 13.sp,
                fontWeight: FontWeight.w400,
                color: Color(0xFF000000),
              ),
              overflow: TextOverflow.ellipsis,
            ),
          ),
          SizedBox(width: 4.w),
          GestureDetector(
            onTap: () => _onTagRemove(node),
            child: Icon(Icons.close, size: 16.sp, color: Color(0xFFBDBDBD)),
          ),
        ],
      ),
    );
  }).toList();
}

UI位置:标签显示在底部操作区上方

复制代码
if (widget.isShowBottomLabel) 
  Row(
    children: [
      Text('已选(${_selectedValues.length})'),
      SizedBox(width: 10.w),
      Expanded(
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,  // 横向滚动
          child: Row(children: _buildSelectedTags()),
        ),
      ),
    ],
  ),

6.2 标签删除逻辑

核心方法_onTagRemove()- 处理标签删除

复制代码
void _onTagRemove(Map<String, dynamic> node) {
  // 检查这个节点是否所有后代叶子都被选中(即是否是被合并的父节点)
  if (_areAllDescendantLeavesSelected(node)) {
    // 如果是合并的父节点,取消所有后代叶子的选中
    _setDescendantLeaves(node, false);
  } else {
    // 如果是叶子节点或未完全合并的节点
    final id = _getItemId(node);
    if (id != null) {
      final children = _childrenOf(node);
      if (children.isEmpty) {
        // 叶子节点:直接取消
        _leafSelected[id] = false;
      } else {
        // 父节点但未完全合并:取消所有后代叶子
        _setDescendantLeaves(node, false);
      }
    }
  }

  _rebuildSelectedValues();  // 重新构建选中列表
  _notifySelectedChange();
  setState(() {});
}

删除行为一致性:标签删除与点击复选框取消选中保持逻辑一致,确保用户体验统一。

7. 组件配置参数详解

回调数据处理

核心方法_getSelectedValueForCallback()- 准备回调数据

复制代码
dynamic _getSelectedValueForCallback() {
  if (widget.isMultiSelect) {
    // 多选:返回清理后的列表(移除children字段)
    return _selectedValues.map((node) {
      return {
        'id': node['id'],
        'industryName': node['industryName'],
        'industryLevel': node['industryLevel'],
        'industryParentId': node['industryParentId'],
        // 明确不包含children字段
      };
    }).toList();
  } else {
    // 单选:返回第一个元素(如果没有则返回空Map)
    if (_selectedValues.isEmpty) {
      return <String, dynamic>{};
    }
    final node = _selectedValues.first;
    return {
      'id': node['id'],
      'industryName': node['industryName'],
      'industryLevel': node['industryLevel'],
      'industryParentId': node['industryParentId'],
    };
  }
}

回调触发时机

  1. 实时变化onSelectedChange在每次选中状态变化时触发
  2. 最终确认onSuccess在点击"确定"按钮时触发

8. 生命周期与性能优化

8.1 完整的生命周期管理

复制代码
@override
void initState() {
  super.initState();
  // 初始构建树形数据和选中状态
  _treeOptions = _buildIndustryTreeFromFlat(widget.options);
  _displayOptions = _treeOptions;
  _initializeSelectedValue();
  
  // 首帧之后再通知父组件,避免build期间setState
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _notifySelectedChange();
  });
}

@override
void didUpdateWidget(covariant IndustryBulletFrame oldWidget) {
  super.didUpdateWidget(oldWidget);

  // 监听父组件数据变化
  final bool optionsChanged = !listEquals(oldWidget.options, widget.options);
  final bool initialValueChanged = 
      oldWidget.initialSelectedValue != widget.initialSelectedValue;

  if (optionsChanged || initialValueChanged) {
    _treeOptions = _buildIndustryTreeFromFlat(widget.options);
    _displayOptions = _treeOptions;
    _initializeSelectedValue();
    setState(() {});
  }
}

@override
void dispose() {
  _searchDebounce?.cancel();  // 清理防抖计时器
  super.dispose();
}

8.2 性能优化措施

  1. 防抖搜索:300ms延迟避免频繁搜索重建
  2. 映射表优化 :使用Map实现O(1)节点查找
  3. 智能合并:减少选中列表数据量
  4. 局部更新 :通过setState精确控制重建范围
  5. 资源清理 :在dispose中取消计时器

9. 使用示例

9.1 基本使用(单选模式)

复制代码
IndustryBulletFrame(
  options: industryList,  // 扁平行业列表
  initialSelectedValue: {
    'id': 101,
    'industryName': '互联网',
    'industryParentId': 0
  },
  isMultiSelect: false,
  isShowTopTitle: true,
  isShowBottomLabel: true,
  onSuccess: (selectedValue) {
    print('选中结果: $selectedValue');
    // 单选返回Map: {id: 101, industryName: '互联网', ...}
  },
  onSelectedChange: (selectedValue) {
    print('选中变化: $selectedValue');
  },
)

9.2 多选模式使用

复制代码
IndustryBulletFrame(
  options: industryList,
  initialSelectedValue: [
    {'id': 101, 'industryName': '互联网'},
    {'id': 201, 'industryName': '金融'},
  ],
  isMultiSelect: true,
  isShowBottomLabel: true,
  onSuccess: (selectedList) {
    print('选中结果: $selectedList');
    // 多选返回List: [{id: 101, ...}, {id: 201, ...}]
  },
)

9.3 数据格式示例

复制代码
// 后端返回的扁平数据
List<Map<String, dynamic>> industryList = [
  {'id': 1, 'industryName': '互联网', 'industryParentId': 0, 'industryLevel': 1},
  {'id': 2, 'industryName': '金融', 'industryParentId': 0, 'industryLevel': 1},
  {'id': 11, 'industryName': '电子商务', 'industryParentId': 1, 'industryLevel': 2},
  {'id': 12, 'industryName': '社交网络', 'industryParentId': 1, 'industryLevel': 2},
  {'id': 21, 'industryName': '银行', 'industryParentId': 2, 'industryLevel': 2},
  {'id': 22, 'industryName': '证券', 'industryParentId': 2, 'industryLevel': 2},
];

// 组件内部转换后的树形结构
List<Map<String, dynamic>> treeData = [
  {
    'id': 1,
    'industryName': '互联网',
    'industryParentId': 0,
    'industryLevel': 1,
    'children': [
      {'id': 11, 'industryName': '电子商务', 'industryParentId': 1, 'industryLevel': 2},
      {'id': 12, 'industryName': '社交网络', 'industryParentId': 1, 'industryLevel': 2},
    ]
  },
  // ... 其他节点
];

相关推荐
CHU7290352 小时前
一番赏爬塔闯关小程序前端功能玩法设计解析
前端·小程序
ℋᙚᵐⁱᒻᵉ鲸落2 小时前
Vue3 分页加载避坑指南:如何解决“向下滚动时出现重复数据”的问题?
前端·vue.js
smchaopiao2 小时前
理解HTML中的段落标签:功能与应用
前端·css·html
云原生指北2 小时前
AI Agent 的代码执行沙箱:从容器到微虚拟机的隔离之道
前端
Fairy要carry3 小时前
面试-Agent Loop
前端·chrome
Surmon5 小时前
基于 Cloudflare 生态的 AI Agent 实现
前端·人工智能·架构
六月June June9 小时前
自定义调色盘组件
前端·javascript·调色盘
SY_FC10 小时前
实现一个父组件引入了子组件,跳转到其他页面,其他页面返回回来重新加载子组件函数
java·前端·javascript
糟糕好吃10 小时前
我让 AI 操作网页之后,开始不想点按钮了
前端·javascript·后端