更新日志

2024-09-07

更新主题更换提醒。

2024-09-03

更新「时间轴」相关内容。

更新了「选项卡」相关内容,修复了 BUG 解决部分中 mathjax相关的修复方式无法生效的问题。

规范了更新日志格式。

2024-08-30

更新了「选项卡」相关内容,将避坑提醒转移到 BUG 解决部分。

2024-08-28

更新了「选项卡」相关内容,新增避坑提醒。新增「链接卡片」相关内容。

2024-08-27

发布文章。

本博客已更换至 Solitude 主题,Fluid 主题相关内容仅供参考。

选项卡

Tabs 标签是从 Butterfly 主题上扒下来的,也参考了 hexo-tag-common 插件。

先看效果演示。

效果演示

1
2
3
4
5
{% tabs name, [index] %}
<!-- tab [Tab caption] [@icon] -->
Any content (support inline tags too).
<!-- endtab -->
{% endtabs %}
  • name :
    • 选项卡块标签的名称,不带逗号。
  • [index]:
    • 活动选项卡的索引号。
    • 如果未指定,将选择第一个标签。
    • 如果 index-1,则不会选择任何选项卡。
    • 可选参数。
  • [Tab caption]:
    • 当前选项卡的标题。
    • 如果未指定标题,则带有制表符索引后缀的唯一名称将用作制表符的标题。
    • 如果未指定标题,但指定了图标,则标题将为空。
    • 可选参数。
  • [@icon]:
  • Iconfont 图标名称(全名,如:iconfont icon-font
  • 可以指定带空格或不带空格;
  • 例如 Tab caption @iconTab caption@icon
  • 可选参数。

Demo 1 - 预设选择第一个(默认)

This is Tab 1.

This is Tab 2.

This is Tab 3.

Demo 2 - 预设选择 tabs

This is Tab 1.

This is Tab 2.

This is Tab 3.

Demo 3 - 没有预设值

This is Tab 1.

This is Tab 2.

This is Tab 3.

Demo 4 - 自定义Tab名 + 只有 icon + icon 和 Tab 名

tab名字为第一个Tab

只有图标 没有Tab名字

名字+icon

Demo 1 - 预设选择第一个(默认)

1
2
3
4
5
6
7
8
9
10
11
12
13
{% tabs test1 %}
<!-- tab -->
**This is Tab 1.**
<!-- endtab -->

<!-- tab -->
**This is Tab 2.**
<!-- endtab -->

<!-- tab -->
**This is Tab 3.**
<!-- endtab -->
{% endtabs %}

Demo 2 - 预设选择 tabs

1
2
3
4
5
6
7
8
9
10
11
12
13
{% tabs test2, 3 %}
<!-- tab -->
**This is Tab 1.**
<!-- endtab -->

<!-- tab -->
**This is Tab 2.**
<!-- endtab -->

<!-- tab -->
**This is Tab 3.**
<!-- endtab -->
{% endtabs %}

Demo 3 - 没有预设值

1
2
3
4
5
6
7
8
9
10
11
12
13
{% tabs test3, -1 %}
<!-- tab -->
**This is Tab 1.**
<!-- endtab -->

<!-- tab -->
**This is Tab 2.**
<!-- endtab -->

<!-- tab -->
**This is Tab 3.**
<!-- endtab -->
{% endtabs %}

Demo 4 - 自定义Tab名 + 只有 icon + icon 和 Tab 名

1
2
3
4
5
6
7
8
9
10
11
12
13
{% tabs test4 %}
<!-- tab 第一个Tab -->
**tab名字为第一个Tab**
<!-- endtab -->

<!-- tab @iconfont icon-github-fill -->
**只有图标 没有Tab名字**
<!-- endtab -->

<!-- tab 随机@iconfont icon-random -->
**名字+icon**
<!-- endtab -->
{% endtabs %}

是的,与 Next 主题和 Butterfly 主题的 Tabs 语法一致,因为就是移植过来的。

接下来看配置方法。

配置

增加 tags

scripts\tags 目录下新建 tab.js 文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
'use strict'

function getUuid() {
    return Math.random().toString(36).substring(2, 8) + Date.now().toString(36);
}

const postTabs = (args, content) => {
    const tabBlock = /<!--\s*tab (.*?)\s*-->\n([\w\W\s\S]*?)<!--\s*endtab\s*-->/g
    args = args.join(' ').split(',')
    const tabName = args[0] || 'tab'
    const tabActive = Number(args[1]) || 0
    const matches = []
    let match
    let tabId = 0
    let tabNav = ''
    let tabContent = ''
    let noDefault = true

    while ((match = tabBlock.exec(content)) !== null) {
        matches.push(match[1], match[2])
    }

    const tabsId = "t" + getUuid();

    for (let i = 0; i < matches.length; i += 2) {
        const tabParameters = matches[i].split('@')
        let postContent = matches[i + 1]
        let tabCaption = tabParameters[0] || ''
        let tabIcon = tabParameters[1] || ''

        postContent = hexo.render.renderSync({ text: postContent, engine: 'markdown' }).trim()

        tabId += 1

        if (tabCaption.length === 0 && tabIcon.length === 0) tabCaption = tabName + ' ' + tabId

        const icon = tabIcon.trim()
        tabIcon.length > 0 && (tabIcon = `<i class="${icon}"></i>`)

        // Generate unique IDs for tabs and content
        const contentIdAttr = `${tabsId}-${tabId}`

        let isActive = ''
        if ((tabActive > 0 && tabActive === tabId) || (tabActive === 0 && tabId === 1)) {
            isActive = ' active'
            noDefault = false
        }

        tabNav += `<button type="button" class="tab${isActive}" data-target="${contentIdAttr}">${tabIcon + tabCaption.trim()}</button>`
        tabContent += `<div class="tab-item-content${isActive}" id="${contentIdAttr}">${postContent}</div>`
    }

    const toTop = '<div class="tab-to-top"><button type="button" aria-label="scroll to top"><i class="iconfont icon-arrow-up-bold"></i></button></div>'

    tabNav = `<div class="nav-tabs${noDefault ? ' no-default' : ''}">${tabNav}</div>`
    tabContent = `<div class="tab-contents">${tabContent}</div>`

    return `<div class="tabs" id=${tabsId}>${tabNav + tabContent + toTop}</div>`
}

hexo.extend.tag.register('tabs', postTabs, { ends: true })
hexo.extend.tag.register('subtabs', postTabs, { ends: true })
hexo.extend.tag.register('subsubtabs', postTabs, { ends: true })
此时使用 hexo cl && hexo g && hexo s 三连,就已经可以解析并生成对应的 HTML 代码了。

增加样式

source/custom/css/_tags 中创建 tab.styl 文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@css {
    :root {
        --tab-border-color: rgba(100, 100, 100, 0.25);
        --tab-botton-bg: rgba(100, 100, 100, 0.1);
        --tab-botton-color: var(--text-color);
        --tab-button-hover-bg: rgba(170, 170, 170, 0.15);
        --tab-hover-border-color: rgba(240, 140, 140, 0.4);
        --tab-active-border-color: rgba(240, 30, 30, 0.9);
        --tab-button-active-bg: rgba(210, 210, 210, 0.1.5);
        --tab-to-top-color: var(--text-color);
        --tab-to-top-hover-color: rgba(203, 60, 60, 1);
    }
}

addBorderRadius(x = 6)
  border-radius: unit(x, 'px')

.tabs
  position: relative
  margin: 0 0 20px
  border: none
  addBorderRadius()
  background: rgba(200, 200, 200, 0.1)
  overflow: hidden

  > .nav-tabs
    display: flex
    flex-wrap: wrap
    margin: 0
    padding: 0
    background: var(--tab-botton-bg)

    > .tab
      flex-grow: 1
      padding: 8px 18px
      border-top: none
      border-bottom: 2px solid var(--tab-border-color)
      border-right: 0.75px solid var(--tab-border-color)
      border-left: 0.75px solid var(--tab-border-color)
      background: var(--tab-botton-bg)
      color: var(--tab-botton-color)
      line-height: 2
      transition: all .4s

      i
        width: 1.5em

      &.active
        border-bottom: 2px solid var(--tab-active-border-color)
        background: var(--tab-button-active-bg)
        cursor: default

      &:not(.active)
        &:hover
          border-bottom: 2px solid var(--tab-hover-border-color)
          background: var(--tab-button-hover-bg)

    &.no-default
      & ~ .tab-to-top
        display: none

  > .tab-contents
    .tab-item-content
      position: relative
      display: none
      padding: 36px 24px 10px

      @media screen and (max-width: 768px)
        padding: 24px 14px
        

      &.active
        display: block
        animation: tabshow .5s

      > :last-child
        margin-bottom: 0

  > .tab-to-top
    padding: 0 16px 10px 0
    width: 100%
    text-align: right

    button
      addBorderRadius()
      color: var(--tab-to-top-color)
      border: 1px  solid var(--tab-border-color)

      &:hover
        color: var(--tab-to-top-hover-color)

@keyframes tabshow
  0%
    transform: translateY(15px)

  100%
    transform: translateY(0)

然后在 source/custom/css 中创建 tags.styl,内容为 @import "_tags/*";,方便后续添加更多标签。最后在 _config.fluid.yml 中修改 custom_css 配置项:

1
2
custom_css:
  - custom/css/tags
使用 hexo cl && hexo g && hexo s 三连,就能看到选项卡已经正确渲染了。

增加逻辑跳转

source/custom/js 中创建 tags.js 文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
"use strict";

const getEleTop = (ele) => {
    let actualTop = ele.offsetTop
    let current = ele.offsetParent
    while (current !== null) {
        actualTop += current.offsetTop
        current = current.offsetParent
    }
    return actualTop
}

/* <---- Tabs Begin ----> */
// 初始化选项卡
const initializeTabs = () => {
    document.querySelectorAll('.tabs').forEach(tabs => {
        addTabClickEventListeners(tabs);
    });
}

// 为选项卡添加点击事件监听器
const addTabClickEventListeners = (tabs) => {
    tabs.querySelectorAll('.nav-tabs .tab').forEach(tab => {
        tab.addEventListener('click', (event) => handleTabClick(event));
    });
}

// 初始化回到顶部按钮
const initializeScrollToTopButtons = () => {
    document.querySelectorAll('.tabs .tab-to-top button').forEach(button => {
        button.addEventListener('click', (event) => handleScrollToTop(event));
    });
}

const handleTabClick = (event) => {
    event.preventDefault();
    const clickedTab = event.currentTarget;

    // 找到当前选项卡组
    const targetId = clickedTab.dataset.target;
    const tabsContainerId = targetId.split('-')[0];

    const tabsContainer = document.querySelector(`#${tabsContainerId}`);
    if (!tabsContainer) return;

    // 处理 nav-tabs 部分
    tabsContainer.querySelectorAll('.nav-tabs .tab').forEach(tab => {
        if (tab.dataset.target.split('-')[0] === tabsContainerId) {
            tab.classList.toggle('active', tab === clickedTab);
        }
    });

    // 处理 tab-contents 部分
    if (targetId) {
        const contentToShow = tabsContainer.querySelector(`#${targetId}`);
        if (contentToShow) {
            tabsContainer.querySelectorAll('.tab-contents .tab-item-content').forEach(content => {
                if (content.id.split('-')[0] === tabsContainerId) {
                    content.classList.toggle('active', content === contentToShow);
                }
            });
        }
    }
}

const handleScrollToTop = (event) => {
    event.stopPropagation(); // 阻止事件冒泡

    // 找到当前按钮所在的选项卡组
    const tabGroup = event.currentTarget.closest('.tabs');
    window.scrollTo({ top: getEleTop(tabGroup) - 70, behavior: 'smooth' });
}

const tabsFn = () => {
    initializeTabs();
    initializeScrollToTopButtons();
}
/* <---- Tabs End ----> */

function tagsFn() {
    tabsFn();
}

document.addEventListener('DOMContentLoaded', () => {
    tagsFn();
});
_config.fluid.yml 中修改 custom_js 配置项:
1
2
custom_js:
  - custom/js/tags.js
使用 hexo cl && hexo g && hexo s 三连,即可正常渲染并使用选项卡了。

BUG 解决

移植的 Tabs 选项卡标签插件可能会与 Fluid 主题的某些配置冲突,以下是解决方案:

数学公式引擎为KaTeX

KaTeX 使用的 hexo-markdown-it 渲染引擎会与 tocbot 冲突,导致右侧的目录栏的一级目录无法正确跳转,请参考我的另一篇博文解决该 bug。

数学公式引擎为mathjax

mathjax 使用的 hexo-renderer-pandoc 渲染引擎会与主题自带 note 标签冲突,导致内部嵌套的 note 标签会出错: 就像这样

解决方案:虽然很不情愿,也有违本篇博客描述的「非侵入式」,但由于在博客的 scripts 目录下修改,会因为加载时间问题无法覆盖主题的 note tag,所以这个 bug 必须修改主题源文件。把 node_modules/hexo-theme-fluid/scripts/tags/note.js 返回语句的换行去掉即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict'

const note = (args, content) => {
    if (!args || !args[0] || args[0].toLowerCase() === "default") {
        args = [hexo.theme.config.post.updated.note_class || "info"];
    }
    // 删掉换行
    // return `<div class="note note-${args.join(' ')}">
    //         ${hexo.render.renderSync({ text: content, engine: 'markdown' }).split('\n').join('')}
    //       </div>`;
    return `<div class="note note-${args.join(' ')}">${hexo.render.renderSync({ text: content, engine: 'markdown' }).split('\n').join('')}</div>`;
};

hexo.extend.tag.unregister('note') // 重新注册 tag
hexo.extend.tag.register('note', note, { ends: true })

此外,按照上述方法修复 BUG 后,还会有三重嵌套的 Tab 无法使用的 BUG,不过正常情况下应该用不上三重嵌套就没管。

链接卡片

这个标签移植自 hexo-butterfly-tag-plugins-plus 插件,基本属于复制过来就完事。

效果演示

1
{% linkcard 标题, 链接, 图片链接(可选) %}
1
{% linkcard HPCesia 的摆烂小屋, https://blog.hpcesia.com/, /img/pwa/favicon.png %}

配置

scripts\tags 目录下新建 linkcard.js 文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
'use strict';

// {% linkcard title, url %}
// {% linkcard title, url, img %}
hexo.extend.tag.register('linkcard', function (args) {
    var configtemp = hexo.config.tag_plugins || hexo.theme.config.tag_plugins
    args = args.join(' ').split(',')
    let text = ''
    let url = ''
    let img = ''
    if (args.length < 2) {
        return
    } else if (args.length == 2) {
        text = args[0].trim()
        url = args[1].trim()
    } else if (args.length == 3) {
        text = args[0].trim()
        url = args[1].trim()
        img = args[2].trim()
    }
    let result = '';
    result += '<div class="tag linkcard"><a class="link-card" title="' + text + '" href="' + url + '">';
    result += '<div class="left">';
    result += '<img src="' + (img || configtemp.link.placeholder) + '"/>';
    result += '</div>';
    result += '<div class="right"><p class="text">' + text + '</p><p class="url">' + url + '</p></div>';
    result += '</a></div>';

    return result;
});

source/custom/css/_tags 中创建 linkcard.styl 文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
.tag.linkcard
  text-align center

a
  &.link-card
    margin 0.25rem auto
    background #f6f6f6
    display inline-flex
    align-items center
    cursor pointer
    text-align center
    min-width 200px
    max-width 361px
    color #444
    border-radius 12px
    text-decoration none
    &:hover
      box-shadow 0 4px 8px 0 rgba(0, 0, 0, 0.1)
    div
      &.left
        width 48px
        height 48px
        margin 12px
        overflow hidden
        flex-shrink 0
        position relative
        i
          font-size 32px
          line-height 48px
          margin-left 4px
        img
          display block
          position absolute
          border-radius 2px
          top 50%
          left 50%
          transform translate(-50%, -50%)
      &.right
        overflow hidden
        margin-right 12px
    p
      margin 0
      &.text
        font-weight bold
      &.url
        flex-shrink 0
        color rgba(68, 68, 68, 0.65)
        font-size 13px

@media screen and (max-width: 425px)
  a
    &.link-card
      max-width 100%

@media screen and (max-width: 375px)
  a
    &.link-card
      width 100%

a.link-card div.left,
a.link-card div.right
  pointer-events none

[data-user-color-scheme="dark"]
  a
    &.link-card
      filter brightness(0.7)
      img
        filter brightness(1)

使用 hexo cl && hexo g && hexo s 三连,即可正常渲染并使用链接卡片了。

时间轴

这个标签移植自 hexo-butterfly-tag-plugins-plus 插件的 1.0.15 版本,基本属于复制过来就完事。

效果演示

1
2
3
4
5
6
7
8
{% timeline [时间轴标题(可选)] %}
{% timenode [时间节点标题(可选)] %}
内容
{% endtimenode %}
{% timenode [时间节点标题(可选)] %}
内容
{% endtimenode %}
{% endtimeline %}
时间轴样式预览

2024-09-03

更新时间线标签。更新了选项卡相关内容,修复了 BUG 解决部分中 mathjax相关的修复方式无法生效的问题。

2024-08-30

更新了选项卡相关内容,将避坑提醒转移到 BUG 解决部分。

2024-08-28

更新了选项卡相关内容,新增避坑提醒。新增链接卡片相关内容。

2024-08-27

发布文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% timeline 时间轴样式预览 %}
{% timenode 2024-09-03 %}
更新时间线标签。更新了选项卡相关内容,修复了 BUG 解决部分中 mathjax 相关的修复方式无法生效的问题。
{% endtimenode %}
{% timenode 2024-08-30 %}
更新了选项卡相关内容,将避坑提醒转移到 BUG 解决部分。
{% endtimenode %}
{% timenode 2024-08-28 %}
更新了选项卡相关内容,新增避坑提醒。新增链接卡片相关内容。
{% endtimenode %}
{% timenode 2024-08-27 %}
发布文章。
{% endtimenode %}
{% endtimeline %}

配置

scripts\tags 目录下新建 timeline.js 文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
'use strict';

function postTimeline(args, content) {
  if (args.length > 0) {
    return `<div class="timeline"><p class='p h3'>${args}</p>${content}</div>`;
  } else {
    return `<div class="timeline">${content}</div>`;
  }
}

function postTimenode(args, content) {
  args = args.join(' ').split(',')
  var time = args[0]
  return `<div class="timenode"><div class="meta"><p>${hexo.render.renderSync({text: time, engine: 'markdown'})}</p></div><div class="body">${hexo.render.renderSync({text: content, engine: 'markdown'}).split('\n').join('')}</div></div>`;
}


// {% timeline %}
// ... timenode ...
// {% endtimeline %}
hexo.extend.tag.register('timeline', postTimeline, {ends: true});

// {% timenode time %}
// what happened
// {% endtimenode %}
hexo.extend.tag.register('timenode', postTimenode, {ends: true});

source/custom/css/_tags 中创建 timeline.styl 文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
div
  &.timenode
    position relative
    &:before
      top 0
      height 6px
    &:after
      top 26px
      height calc(100% - 26px)
    &:last-child
      &:after
        height calc(100% - 26px - 16px)
        border-bottom-left-radius 2px
        border-bottom-right-radius 2px
    .meta
      position relative
      color var(--text-color)
      font-size 0.75rem
      line-height 32px
      height 32px
      &:before
        background rgba(68, 215, 182, 0.5)
        width 16px
        height 16px
        border-radius 8px
      &:after
        background #44d7b6
        margin-left 2px
        margin-top 2px
        width 12px
        height 12px
        border-radius 6px
        transform scale(0.5)
      p
        font-weight bold !important
        margin 0 0 0 24px !important
    .body
      margin 4px 0 10px 24px
      padding 10px
      border-radius 12px
      background rgba(127, 127, 127, 0.1)
      display inline-block
      p
        &:first-child
          margin-top 0 !important
        &:last-child
          margin-bottom 0 !important
      .highlight
        background #fff7ea
        filter grayscale(0%)
        figcaption
          background #ffeed2
        .gutter
          background #ffedd0
    &:hover
      .meta
        &:before
          background rgba(68, 215, 182, 0.5)
        &:after
          background #96cec2
          transform scale(1)

div.timenode:before,
div.timenode:after
  content ""
  z-index 1
  position absolute
  background rgba(68, 215, 182, 0.5)
  width 2px
  left 7px

div.timenode .meta,
div.timenode .body
  max-width calc(100% - 24px)

div.timenode .meta:before,
div.timenode .meta:after
  content ""
  position absolute
  top 8px
  z-index 2

[data-theme="dark"]
  div
    &.timenode
      .body
        background #2c2c2c
      &:hover
        .meta
          color #ccd0d7
      .meta
        color rgba(255, 255, 255, 0.6)

使用 hexo cl && hexo g && hexo s 三连,即可正常渲染并使用时间轴了。

更多标签

会想办法移植更多好用的标签,敬请期待。