前言

如果你经常用Typora,一定对 [TOC]不默生,这是自动生成目录结构的代码。然而把这代码直接从Typora复制到博客时,发现Joe主题并不支持[TOC],没解释代码而是直接显示[TOC]。

针对这一情况,翻了一下Joe主题的后台设置,没找到与“生成文章目录结构”相关的设置选项。这意味着想要实现目录树,要自己解决问题。

经过一翻搜索及测试,找到了以下的解决办法,不必有过多的修改,只需要添加几段代码即可达到预期效果,以下是具体步骤。

方法一:为文章添加目录树结构

更多资料来源,参考:https://www.ydyno.com/archives/1331.html

1.1 网站后台 -> 更换外观 ->点击全局设置

无插件为Typecho的Joe主题添加文章目录

1.2 自定义<head></head>里内容,添加样式

<style type="text/css">
.outline-outside-modal-opened {
    z-index: 10000 !important;
    left: 0;
    width: 300px !important;
}
</style>
<link rel="stylesheet" href="https://fastly.jsdelivr.net/gh/yaohaixiao/autocjs/dist/css/autoc.min.css">

1.3 自定义<body></body>末尾位置内容,添加代码

<script src="https://cdn.jsdelivr.net/gh/yaohaixiao/autocjs/dist/autoc.min.js"></script>
<script>
// 文章导航
if ( $(".joe_detail__article").length > 0 ){
    // 创建 Outline 实例
    let navigation = new AutocJs({
        // 文章正文 DOM 节点的 ID 选择器
        article: '.joe_detail__article',
        // 要收集的标题选择器
        selector: 'h1,h2,h3,h4,h5,h6',
        // 侧边栏导航的标题
        title: '文章导读',
        // 文章导读导航的位置
        // outside - 以侧边栏菜单形式显示(默认值)
        // inside - 在文章正文一开始的地方显示
        position: 'outside',
        // 标题图标链接的 URL 地址
        // (默认)没有设置定制,点击链接页面滚动到标题位置
        // 设置了链接地址,则不会滚动定位
        anchorURL: '',
        // 链接的显示位置
        // front - 在标题最前面(默认值)
        // back - 在标题后面
        anchorAt: 'back',
        // 是否生成文章导读导航
        isGenerateOutline: true,
        // 是否在文章导读导航中显示段落章节编号
        isGenerateOutlineChapterCode: false,
        // 是否在正文的文章标题中显示段落章节编号
        isGenerateHeadingChapterCode: false,
        // 是否在正文的文章标题中创建锚点
        isGenerateHeadingAnchor: false
    });
}
</script>

方法一:效果显示

可以看到,左边会出现三道杠,点开后就会根据标题生成对应的目录树

但经测试,这段代码有些小bug,如你的标题有特殊代码 #,会导致目录树解释错乱。

如:### # 组合测试,会被解释成 # 组成测试,简单来说就是h3会被解释成 h1。

无插件为Typecho的Joe主题添加文章目录
[/warning]

如:### # 组合测试,会被解释成 # 组成测试,简单来说就是h3会被解释成 h1。

无插件为Typecho的Joe主题添加文章目录

方法二:更新后要修改aside.php

方法来源:https://cloud.tencent.com/developer/article/2011100

2.1 修改aside.php文件

  • 文件位置: /usr/themes/Joe/public/aside.php
  • aside.php 的合适位置增加如下代码,用于在侧边栏创建【目录容器】。
<?php //# 仅在文章和页面生效?>
<?php if ($this->is('post') || $this->is('page')) : ?>
    <section class="joe_aside__item catalogue">
        <div class="joe_aside__item-title">
            <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2084" width="18" height="18"><path d="M640 192H224c-17.7 0-32-14.3-32-32s14.3-32 32-32h416c17.7 0 32 14.3 32 32s-14.3 32-32 32zM960 544H224c-17.7 0-32-14.3-32-32s14.3-32 32-32h736c17.7 0 32 14.3 32 32s-14.3 32-32 32zM640 896H224c-17.7 0-32-14.3-32-32s14.3-32 32-32h416c17.7 0 32 14.3 32 32s-14.3 32-32 32zM96 192H64c-17.7 0-32-14.3-32-32s14.3-32 32-32h32c17.7 0 32 14.3 32 32s-14.3 32-32 32zM96 544H64c-17.7 0-32-14.3-32-32s14.3-32 32-32h32c17.7 0 32 14.3 32 32s-14.3 32-32 32zM96 896H64c-17.7 0-32-14.3-32-32s14.3-32 32-32h32c17.7 0 32 14.3 32 32s-14.3 32-32 32z" p-id="2085"></path></svg>
            <span class="text">目录</span>
            <span class="line"></span>
        </div>
        <div class="joe_aside__item-contain">
            <ul class="catalogue-items">
            </ul>
        </div>
    </section>
<?php endif; ?>
<?php //# 添加结束?>

通常添加在最后 </aside>前即可,因为这方法的实现效果是,目录容器会漂浮置顶。

无插件为Typecho的Joe主题添加文章目录

2.2 网站后台 -> 外观 ->全局设置

分别添加 css js,如下图所示,具体位置: 控制台->外观->设置外观->全局设置->自定义css

无插件为Typecho的Joe主题添加文章目录

自定义css

.joe_aside__item.catalogue {
    z-index: 999;
    position: sticky;
    top: 45px;
    margin-bottom: 15px;
    transition: top 0.35s;
    background: var(--background)
}
.joe_aside__item.catalogue .joe_aside__item-contain  {
    padding: 0;
    margin: 0;
    margin-left: 10px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items {
    border-left: 1px solid var(--classC);
    border-bottom: 1px solid var(--background);
    padding: 15px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item {
    margin: 0;
    padding: 0;
    line-height: 26px;
    font-size: 16px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item a {
    position: relative;
    display: block;
    line-height: 26px;
    color: var(--main);
    transition: color 0.5s
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item a: hover {
    color: var(--theme)
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item._active>a, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item.active>a {
    color: var(--theme)
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item._active>a::before, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item.active>a::before {
    content: "";
    position: absolute;
    left: -17px;
    top: 0;
    width: 2px;
    height: 26px;
    background-color: var(--theme);
    transition: height 0.35s
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-2.catalogue-item, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-3 .catalogue-item {
    font-size: 14px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-2 .catalogue-item._active>a::before, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-2 .catalogue-item.active>a::before, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-3 .catalogue-item._active>a::before, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-3 .catalogue-item.active>a::before {
    left: -34px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-2 .catalogue-item .level-3 .catalogue-item, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-3 .catalogue-item .level-3 .catalogue-item {
    font-size: 12px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-2 .catalogue-item .level-3 .catalogue-item._active>a::before, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-2 .catalogue-item .level-3 .catalogue-item.active>a::before, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-3 .catalogue-item .level-3 .catalogue-item._active>a::before, .joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-3 .catalogue-item .level-3 .catalogue-item.active>a::before {
    left: -51px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item .level-3 .catalogue-item {
    font-size: 12px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items .catalogue-item ul {
    padding-left: 17px
}
.joe_aside__item.catalogue .joe_aside__item-contain .catalogue-items ul {
    display: block;
    list-style-type: disc
}

自定义js

// 自定义文章目录侧边栏-start
function get_catalogs(article_content) {
  const titleTag = ["H2", "H3", "H4"];
  let titles = [];
  article_content.childNodes.forEach((e, index) => {
      const id = "header-" + index;
      if(titleTag.includes(e.nodeName)){
          titles.push({
              id: id,
              text: e.textContent,
              level: Number(e.nodeName.substring(1, 2) - 1)
          });
          e.setAttribute("id", id);
      }
  });
  return titles;
}
// 找到目录容器
article_content = document.querySelector('.joe_detail__article');
if (article_content) {
  var catalog = get_catalogs(article_content);
  console.log(catalog, '标题级别');
  if (catalog.length == 0) {
      // 无目录,隐藏
      $('.catalogue').hide();
  } else {
    const arr = []
    function getLv1(catalog, index, arr, lv, lv1Item) { // 获取一级菜单
      const newArr = JSON.parse(JSON.stringify(catalog)).slice(index) // 从哪个位置开始遍历
      if (newArr.length === 0) return
      // for循环可以进行 打破 终结循环
      for(let i = 0; i < newArr.length; i++) {
        newArr[i].children = []
        if (newArr[i].level === lv) {  // 拿到对应等级的 放到一起


          newArr[i].el = `
            <li class="catalogue-item">
              <a 
                href="javascript:;" 
                id="to-${ newArr[i].id }"
                to="${ newArr[i].id }"
                title="${ newArr[i].text }"
              >
                ${ newArr[i].text }
              </a>
              <ul class="level-${lv+1}">
                cy_oo${newArr[i].id + i}
              </ul>
            </li>
          `
          newArr[i].replaceName = 'cy_oo' + newArr[i].id + i
          arr.push(newArr[i])
        }

        if(!newArr[i + 1]) return
        if (((newArr[i + 1].level === 1 || newArr[i + 1].level < lv) && lv !== 1)) return
        // 从 索引 + 1 位置循环
        
        // 判断下一级 是否大于 当前级别
        if(newArr[i + 1].level > newArr[i].level) {
            let lv1ItemEl = lv1Item
          if (lv === 1) {
            lv1ItemEl = newArr[i]
          }
          getLv1(newArr, i + 1, newArr[i].children, newArr[i + 1].level, lv1ItemEl)
        }
        // ===
        if (newArr[i + 1].level === newArr[i].level) {
          // break;
        }
      }               
    }


    getLv1(catalog, 0, arr, 1, null)
    // 拼接dom字符串
    function getDomStr(obj, arr) {
      // console.log(arr, 123456);
      let liStr = ''
      arr.forEach(li => {
        liStr += li.el
      })
      obj.domStr = obj.domStr.replace(obj.lv1Item.replaceName, liStr)

      arr.forEach((item, i) => {
        if (item.children && item.children.length) {
          obj.lv1Item = item

          getDomStr(obj, item.children)
        } else {
          obj.domStr = obj.domStr.replace(item.replaceName, '')
        }
      })
    }

    let catalogue = ''
    arr.forEach(item => {
      const lv1Item = item
      let domStr = lv1Item.el
      const obj = {
        domStr,
        lv1Item,
        rStr: 'cy_oo',
        newStr: ''
      }

      getDomStr(obj, lv1Item.children)
      catalogue+=obj.domStr
    })

      document.querySelector('.catalogue-items').innerHTML = catalogue;
      $('.catalogue-item > a').on('mouseenter', function () {
          $(this).parent().addClass('_active');
      });
      $('.catalogue-item > a').on('mouseleave', function () {
          $(this).parent().removeClass('_active');
      });
      // 根据目录定位到标题
      $('.catalogue-item > a').on('click', function () {
          document.removeEventListener("scroll", autoActive);
          $('.catalogue-item').removeClass('active');
          $(this).parent().addClass('active');
          let aim = document.querySelector('#' + $(this).attr('to'));
          let aim_top = aim.offsetTop;
          let aim_h = aim.clientHeight;
          let above_h = document.querySelector('.joe_header__above').clientHeight;
          let below_h = document.querySelector('.joe_header__below').clientHeight;
          let offset = 0;
          let case1 = !document.querySelector('.joe_header__above').className.includes('active');
          let case2 = document.getElementsByTagName("html")[0].scrollTop + above_h > aim_top;

          window.scrollTo({
              top: aim_top - offset - above_h - below_h - 10,
              behavior: 'smooth'
          });
          setTimeout(() => {
              document.addEventListener("scroll", autoActive);
          }, 500);
      });
      if (catalog.length)
          $('.catalogue-item').eq(0).addClass('active');
      // 目录侧标题自动定位
      let autoActive = function () {
          let html_top = document.getElementsByTagName("html")[0].scrollTop; //获得父级卷去的高度
          for (let i = 0; i < catalog.length; i++) {
              let offset = 0;
              let h_id = '#' + catalog[i].id;
              let h_offset = document.querySelector(h_id).offsetTop;
              let above_h = document.querySelector('.joe_header__above').clientHeight;
              let below_h = document.querySelector('.joe_header__below').clientHeight;

              if (!document.querySelector('.joe_header').className.includes('active'))
                  offset = above_h;
              if (h_offset + below_h + offset + 10 >= html_top) {
                  $('.catalogue-item').removeClass('active');
                  if (i > 0 && i < catalog.length - 1 && document.querySelector('#' + catalog[i].id).offsetTop > html_top + window.innerHeight * 0.2) {
                      //还没到下一个标题
                      i--;
                  }
                  $('#to-' + catalog[i].id).parent().addClass('active');
                  break;
              }
          }
      };
      document.addEventListener("scroll", autoActive);
  }
} else {
  // 不是文章,隐藏目录
  $('.catalogue').hide();
}
// 自定义文章目录侧边栏-end

方法二:效果显示

无插件为Typecho的Joe主题添加文章目录

可以看到,右边会出现对应的目录树,但这方法多一个步骤,要修改aside.php

这也就意味着每次更新主题,你都要修改aside.php文件,不然目录树是不可能出现的

现样的,经测试,代码以javascript跳转,无法在外部引用直接跳转到特定的标题

如方法一:会生成https://www.domain.com/#outline-heading-4这类的网址,后面带#,其他地方可以直接引用这超链接并实行跳转。

而方法二,则不会生成#outline-heading-4后缀,都是直接javascript控制。

无插件为Typecho的Joe主题添加文章目录

因此,用哪个方法生成目录,得看个人需求。