You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Ruby on Rails中实现文档嵌套结构的树视图?

当然可以实现!在Rails里搭建这种层级树视图其实挺常见的,结合你已经在用的ancestry gem,再配合前端的渲染逻辑或者树形组件,完全能搞定。下面给你几个实用的实现思路,从简单到进阶都有:

1. 后端先把数据层级理清楚

既然Section已经用了ancestry,可以直接利用它提供的方法来整理树形结构数据,比如roots获取根章节、children获取子章节,还有arrange方法把数据转成嵌套哈希,方便前端遍历。

比如在DocumentsController的show方法里:

def show
  @document = Document.find(params[:id])
  # 把当前文档的Section整理成树形结构,按position排序(如果有这个字段的话)
  @sections_tree = @document.sections.arrange(order: :position)
  # 单独拿出不属于任何Section的Table和TextField(根级元素)
  @root_tables = @document.tables.where(section_id: nil)
  @root_text_fields = @document.text_fields.where(section_id: nil)
end

2. 前端实现:两种常见方案

方案一:纯ERB递归渲染(无交互,仅展示结构)

如果不需要折叠/展开的交互,只是想展示层级关系,可以用递归的局部模板来渲染。

首先创建app/views/sections/_section_node.html.erb局部模板:

<li>
  <span>*Section: <%= section.title %></span>
  <!-- 渲染当前Section下的Table -->
  <% if section.tables.any? %>
    <ul>
      <% section.tables.each do |table| %>
        <li>📊 Table: <%= table.name %></li>
      <% end %>
    </ul>
  <% end %>
  <!-- 渲染当前Section下的TextField -->
  <% if section.text_fields.any? %>
    <ul>
      <% section.text_fields.each do |tf| %>
        <li>📝 Text Field: <%= tf.content.truncate(20) %></li>
      <% end %>
    </ul>
  <% end %>
  <!-- 递归渲染子Section -->
  <% if section.children.any? %>
    <ul class="child-sections">
      <% section.children.each do |child_section| %>
        <%= render partial: 'sections/section_node', locals: { section: child_section } %>
      <% end %>
    </ul>
  <% end %>
</li>

然后在文档的show.html.erb里调用:

<h1><%= @document.title %></h1>
<ul class="document-tree">
  <!-- 先渲染根级的Table和TextField -->
  <% if @root_tables.any? %>
    <li>
      <span>Root Tables</span>
      <ul>
        <% @root_tables.each do |table| %>
          <li>📊 Table: <%= table.name %></li>
        <% end %>
      </ul>
    </li>
  <% end %>
  <% if @root_text_fields.any? %>
    <li>
      <span>Root Text Fields</span>
      <ul>
        <% @root_text_fields.each do |tf| %>
          <li>📝 Text Field: <%= tf.content.truncate(20) %></li>
        <% end %>
      </ul>
    </li>
  <% end %>
  <!-- 渲染根Section节点 -->
  <% @sections_tree.each do |root_section, _children| %>
    <%= render partial: 'sections/section_node', locals: { section: root_section } %>
  <% end %>
</ul>

再加一点CSS美化层级:

.document-tree {
  list-style: none;
  padding-left: 1rem;
}
.child-sections {
  list-style: none;
  padding-left: 1.5rem;
  border-left: 1px solid #ddd;
  margin-top: 0.5rem;
}
.document-tree li {
  margin: 0.3rem 0;
}

方案二:带折叠/展开交互(原生JS实现)

如果需要用户可以点击折叠/展开节点,只需要给局部模板加一点JS交互即可:

修改_section_node.html.erb:

<li>
  <span class="section-toggle">📂 <%= section.title %></span>
  <!-- 把内容包裹在可隐藏的容器里 -->
  <div class="section-content" style="display: none;">
    <% if section.tables.any? %>
      <ul>
        <% section.tables.each do |table| %>
          <li>📊 Table: <%= table.name %></li>
        <% end %>
      </ul>
    <% end %>
    <% if section.text_fields.any? %>
      <ul>
        <% section.text_fields.each do |tf| %>
          <li>📝 Text Field: <%= tf.content.truncate(20) %></li>
        <% end %>
      </ul>
    <% end %>
    <% if section.children.any? %>
      <ul class="child-sections">
        <% section.children.each do |child_section| %>
          <%= render partial: 'sections/section_node', locals: { section: child_section } %>
        <% end %>
      </ul>
    <% end %>
  </div>
</li>

然后在show.html.erb底部加一段原生JS:

document.addEventListener('DOMContentLoaded', function() {
  // 给所有折叠按钮绑定点击事件
  document.querySelectorAll('.section-toggle').forEach(toggleBtn => {
    toggleBtn.addEventListener('click', function() {
      const content = this.nextElementSibling;
      // 切换显示/隐藏状态
      content.style.display = content.style.display === 'none' ? 'block' : 'none';
      // 切换图标
      this.textContent = content.style.display === 'block' 
        ? '📁 ' + this.textContent.slice(2) 
        : '📂 ' + this.textContent.slice(2);
    });
  });
});

3. 进阶:用专业树形组件(复杂交互需求)

如果需要拖拽排序、节点编辑等更复杂的功能,可以用成熟的前端树形组件,比如jstree、Vue Tree Component等。以jstree为例,步骤大概是:

  1. 后端提供JSON格式的树形数据接口
  2. 前端引入jstree的CSS和JS
  3. 初始化组件并加载数据

比如后端的tree_data接口:

def tree_data
  document = Document.find(params[:id])
  nodes = []
  # 添加根级Table
  document.tables.where(section_id: nil).each do |table|
    nodes << { id: "table_#{table.id}", text: "📊 Table: #{table.name}" }
  end
  # 添加根级TextField
  document.text_fields.where(section_id: nil).each do |tf|
    nodes << { id: "tf_#{tf.id}", text: "📝 Text Field: #{tf.content.truncate(20)}" }
  end
  # 递归构建Section树形结构
  document.sections.roots.each do |root_section|
    nodes << build_section_node(root_section)
  end
  render json: nodes
end

private
def build_section_node(section)
  node = { id: "section_#{section.id}", text: "📂 Section: #{section.title}" }
  children = []
  # 添加当前Section下的Table和TextField
  section.tables.each { |t| children << { id: "table_#{t.id}", text: "📊 Table: #{t.name}" } }
  section.text_fields.each { |tf| children << { id: "tf_#{tf.id}", text: "📝 Text Field: #{tf.content.truncate(20)}" } }
  # 递归添加子Section
  section.children.each { |child| children << build_section_node(child) }
  node[:children] = children unless children.empty?
  node
end

前端初始化jstree:

<div id="document-tree-container"></div>

<script>
$(function() {
  $('#document-tree-container').jstree({
    core: {
      data: {
        url: '<%= tree_data_document_path(@document) %>',
        dataType: 'json'
      }
    }
  });
});
</script>

内容的提问来源于stack exchange,提问作者Shyam Kirubha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 12:08:47