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

使用Vue渲染函数动态将标题按层级包裹并分组为列表项

使用Vue渲染函数动态将标题按层级包裹并分组为列表项

嘿,这个需求我懂!你想要做一个<Steps>组件,能自动把同级标题(比如h3和h3)及其后续的所有内容(包括层级更低的标题,比如h4)打包成<li>,外层套<ol>对吧?刚好可以用Vue的渲染函数h()来搞定,我给你一步步拆解实现思路和代码。

核心思路拆解

要实现这个逻辑,关键要解决两个问题:

  1. 确定基准标题层级:找到第一个出现的h2/h3/h4,把它的层级作为“触发新列表项”的标准(比如第一个是h3,那只有后续的h3才会开启新的<li>)
  2. 节点分组:遍历所有插槽节点,把从当前基准标题到下一个基准标题之间的所有内容归为一组,每组对应一个<li>

具体代码实现(Vue 2 选项式写法)

<script>
export default {
  render(h) {
    // 1. 获取组件默认插槽的所有节点
    const slots = this.$slots.default || [];
    if (slots.length === 0) return h('ol');

    // 2. 找到第一个标题节点,确定基准层级
    let baseLevel = null;
    for (const node of slots) {
      if (node.tag && ['h2', 'h3', 'h4'].includes(node.tag)) {
        baseLevel = parseInt(node.tag.slice(1)); // 从h3提取数字3
        break;
      }
    }
    // 如果插槽里没有标题,直接返回包裹所有内容的ol
    if (!baseLevel) return h('ol', slots);

    // 3. 遍历节点,按基准标题分组
    const groups = [];
    let currentGroup = [];
    for (const node of slots) {
      // 遇到基准层级的标题时,先把当前分组存入结果,再新建分组
      if (node.tag && node.tag === `h${baseLevel}`) {
        if (currentGroup.length > 0) {
          groups.push(currentGroup);
          currentGroup = [];
        }
      }
      // 将当前节点加入当前分组
      currentGroup.push(node);
    }
    // 别忘了把最后一个分组加入结果
    if (currentGroup.length > 0) {
      groups.push(currentGroup);
    }

    // 4. 渲染成ol包裹的li列表
    const listItems = groups.map(group => h('li', group));
    return h('ol', listItems);
  }
};
</script>

Vue 3 组合式API写法

如果你用的是Vue 3,也可以用组合式API来实现:

<script setup>
import { h, useSlots, defineRender } from 'vue';

const render = () => {
  // 获取插槽节点
  const slots = useSlots().default?.() || [];
  if (slots.length === 0) return h('ol');

  // 确定基准标题层级
  let baseLevel = null;
  for (const node of slots) {
    if (node.type && ['h2', 'h3', 'h4'].includes(node.type)) {
      baseLevel = parseInt(node.type.slice(1));
      break;
    }
  }
  if (!baseLevel) return h('ol', slots);

  // 节点分组逻辑和Vue2一致
  const groups = [];
  let currentGroup = [];
  for (const node of slots) {
    if (node.type && node.type === `h${baseLevel}`) {
      if (currentGroup.length > 0) {
        groups.push(currentGroup);
        currentGroup = [];
      }
    }
    currentGroup.push(node);
  }
  if (currentGroup.length > 0) groups.push(currentGroup);

  // 渲染最终结构
  const listItems = groups.map(group => h('li', group));
  return h('ol', listItems);
};

// 注册渲染函数
defineRender(render);
</script>

验证你的测试案例

拿你给出的第一个输入来测试:

<Steps>
  <h3>heading a</h3>
  <p>paragraph a</p>
  <div class="code-block"><pre>const x = 1</pre></div>
  <h4>this should not create an li</h4>
  <h3>heading b</h3>
  <p>paragraph b</p>
</Steps>

组件会输出完全符合你要求的结构:

<ol>
  <li>
    <h3>heading a</h3>
    <p>paragraph a</p>
    <div class="code-block"><pre>const x = 1</pre></div>
    <h4>this should not create an li</h4>
  </li>
  <li>
    <h3>heading b</h3>
    <p>paragraph b</p>
  </li>
</ol>

第二个只有单个基准标题的输入,也会把所有内容都包裹在同一个<li>里,完全符合预期。

边界情况说明

  • 如果插槽里没有任何标题节点,组件会直接把所有内容包裹在<ol>里返回
  • 如果插槽开头是非标题内容,这些内容会被放到第一个<li>里,直到遇到第一个基准标题

备注:内容来源于stack exchange,提问作者dbzx10299

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 11:44:35