如何在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为例,步骤大概是:
- 后端提供JSON格式的树形数据接口
- 前端引入jstree的CSS和JS
- 初始化组件并加载数据
比如后端的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
相关产品推荐
相关产品推荐

