Finereport折叠树功能实现与优化指南

1. Finereport报表中的折叠树功能解析

Finereport作为企业级报表工具,其折叠树功能在数据可视化领域具有独特优势。折叠树(Collapsible Tree)本质上是一种允许用户通过点击节点来展开或折叠子节点的树形结构展示方式。这种交互式设计特别适合展示具有层级关系的数据,比如组织架构、产品分类或地域分布等场景。

1.1 折叠树的核心实现原理

在Finereport中实现折叠树主要依赖以下几个技术组件:

  1. 数据集准备:需要确保数据源包含明确的层级关系字段,通常包含ID、父ID、显示文本等关键列。例如部门数据表可能包含"部门ID"、"上级部门ID"、"部门名称"三个核心字段。

  2. 单元格属性设置:

    // 典型折叠树单元格属性配置示例 { "displayAsTree": true, "treeOption": { "idColumn": "dept_id", "parentColumn": "parent_id", "textColumn": "dept_name", "expandLevel": 2 // 默认展开到第二级 } }
  3. 前端渲染机制:Finereport使用混合渲染技术(HTML+Canvas)来优化大量节点时的性能表现。当节点数量超过500时,会自动启用虚拟滚动技术。

重要提示:在配置父子关系字段时,务必确保没有循环引用的情况,否则会导致渲染异常。建议在SQL查询阶段就通过WITH RECURSIVE语句检查数据完整性。

1.2 基础折叠树的实现步骤

下面以一个部门组织结构报表为例,演示具体实现流程:

  1. 数据准备阶段:

    • 创建数据集SQL(以MySQL为例):
    SELECT dept_id, parent_id, dept_name, manager, employee_count FROM department ORDER BY parent_id, dept_id
  2. 模板设计阶段:

    • 在单元格A1插入树形控件
    • 右键单元格 → 单元格元素 → 插入树形结构
    • 配置树属性:
      • 标识列 → dept_id
      • 父标识列 → parent_id
      • 显示文本列 → dept_name
  3. 样式优化技巧:

    • 通过条件属性设置不同层级节点的缩进:
    // 根据层级设置缩进 function getIndent(level) { return level * 15 + "px"; }
    • 为叶子节点添加特殊图标:
    .fr-tree-leaf:before { content: "📄"; margin-right: 5px; }

2. 多级折叠树的进阶实现

当数据具有多层级嵌套关系时(如集团-分公司-部门-小组四级结构),需要采用多级折叠树方案。Finereport提供了两种实现方式:

2.1 单数据集多级树实现

适用于层级固定的场景(如已知最多4级):

  1. 数据结构要求:

    字段名类型说明
    level1_idString一级节点ID
    level1_nameString一级节点名称
    level2_idString二级节点ID
    level2_nameString二级节点名称
    .........
  2. 配置要点:

    • 使用"多级树"控件替代基础树控件
    • 为每个层级单独设置标识列和显示列
    • 建议设置expandToLevel: 3控制默认展开层级

2.2 动态多级树实现

更灵活的解决方案是使用递归公用表表达式(CTE)准备数据:

WITH RECURSIVE dept_tree AS ( -- 基础查询(根节点) SELECT dept_id, parent_id, dept_name, 1 AS level, CAST(dept_id AS CHAR(200)) AS path FROM department WHERE parent_id IS NULL UNION ALL -- 递归查询 SELECT d.dept_id, d.parent_id, d.dept_name, dt.level + 1, CONCAT(dt.path, ',', d.dept_id) FROM department d JOIN dept_tree dt ON d.parent_id = dt.dept_id ) SELECT * FROM dept_tree ORDER BY path;

2.3 性能优化方案

当处理大型组织架构(节点数>1000)时,建议:

  1. 分页加载技术:

    // 前端分页配置 treeConfig: { lazyLoad: true, pageSize: 50, loadFn: function(node, resolve) { // 异步加载子节点 FR.ajax({ url: 'tree/load', data: { parentId: node.id, page: node.pageIndex } }).then(resolve); } }
  2. 后端优化技巧:

    • 为parent_id字段创建索引
    • 使用内存缓存高频访问的树结构
    • 实现预计算路径枚举(如MPTT算法)

3. 折叠树与分页功能的结合

当树形结构数据量极大时,需要结合分页功能提升用户体验。Finereport提供了多种分页方案:

3.1 横向分页实现

适用于宽表场景(如包含多年度数据对比):

  1. 配置步骤:

    • 右键模板 → 模板属性 → 分页设置
    • 选择"横向分页"
    • 设置每页显示列数(如10列)
  2. 与折叠树的联动:

    // 分页切换时保持树形状态 function onPageChange(page) { const expandedNodes = FR.getTreeExpandedNodes('tree1'); loadPageData(page); FR.setTreeExpandedNodes('tree1', expandedNodes); }

3.2 纵向分页实现

更常见的分页方式,特别适合深层次树形结构:

  1. 基础配置:

    • 分页设置选择"纵向分页"
    • 设置每页行数(如50行)
    • 启用"保持树形状态"选项
  2. 高级技巧:

    • 使用书签保持展开状态:
    // 保存状态 localStorage.setItem('treeState', JSON.stringify(FR.getTreeState())); // 恢复状态 const savedState = localStorage.getItem('treeState'); if(savedState) FR.setTreeState(JSON.parse(savedState));

3.3 混合分页方案

对于既需要横向扩展又需要纵向分页的复杂场景:

  1. 数据结构设计:

    SELECT d.dept_id, d.dept_name, s2020.sales AS sales_2020, s2021.sales AS sales_2021 -- 更多年度数据... FROM department d LEFT JOIN sales s2020 ON d.dept_id = s2020.dept_id AND s2020.year = 2020 LEFT JOIN sales s2021 ON d.dept_id = s2021.dept_id AND s2021.year = 2021
  2. 模板配置要点:

    • 对树形列启用纵向分页
    • 对数据列启用横向分页
    • 设置关联滚动:
    FR.bindScroll('vertical', 'treeContainer', 'dataContainer'); FR.bindScroll('horizontal', 'header', 'dataContainer');

4. 实战问题排查与性能优化

4.1 常见问题速查表

问题现象可能原因解决方案
节点无法展开1. 父子关系配置错误
2. 数据未正确排序
1. 检查parent_id字段映射
2. 添加ORDER BY parent_id, id
分页后状态丢失未启用状态保持选项在分页设置中勾选"保持树形状态"
大型树加载慢1. 未使用懒加载
2. 缺少数据库索引
1. 启用lazyLoad配置
2. 为parent_id创建索引
移动端显示异常未启用响应式设计添加CSS媒体查询调整缩进

4.2 性能优化实战记录

案例背景:某集团公司组织架构报表,包含8000+节点,加载时间超过15秒。

优化过程:

  1. 数据层优化:

    • 将递归查询改为预计算路径枚举:
    ALTER TABLE department ADD COLUMN path VARCHAR(1000); UPDATE department SET path = CONCAT(parent_path, ',', dept_id); CREATE INDEX idx_path ON department(path);
  2. 前端优化:

    • 实现虚拟滚动:
    new VirtualScroll({ container: '#tree-container', rowHeight: 32, render: node => `<div class="tree-node">${node.name}</div>` });
  3. 缓存策略:

    // 后端缓存实现示例 @Cacheable(value = "deptTree", key = "#rootOrgId") public List<Department> getEntireTree(String rootOrgId) { // 查询逻辑 }

优化结果:加载时间从15s降至1.2s,内存消耗减少60%。

4.3 移动端适配技巧

  1. 触控优化:

    .fr-tree-node { min-height: 44px; /* 符合苹果人机指南 */ padding: 12px 15px; }
  2. 手势支持:

    Hammer(treeEl).on('swipeleft', () => nextPage()); Hammer(treeEl).on('swiperight', () => prevPage());
  3. 响应式布局:

    @media (max-width: 768px) { .fr-tree { --indent: 10px; /* 缩小缩进 */ font-size: 14px; } }

在实际项目中,我们发现折叠树的展开状态同步是个关键挑战。特别是在分页场景下,推荐使用前端路由的hash来保存状态:

// 状态保存 window.location.hash = FR.getTreeStateString(); // 状态恢复 FR.setTreeStateFromString(window.location.hash.slice(1));