插件版

以下配置无需改主题配置文件。

压缩文件

具体步骤
  1. 安装minify插件。

    1
    npm install hexo-minify --save
  2. 修改配置,直接在_config.butterfly.yml添加下面代码即可。

    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
    # Hexo-minify
    ## https://github.com/lete114/hexo-minify
    ## 部署自动压缩文件
    minify:
    preview: true # 本地预览时是否压缩,建议打开能随时纠错,部分图片压缩后会出问题
    exclude: ['*.min.*']
    js:
    enable: true
    sourceMap:
    enable: false # 生成 sourceMap
    # 将 sourceMappingURL 插入压缩后的 js 文件,如果为 false 则需要在浏览器开发者工具中手动添加 sourceMap
    sourceMappingURL: false ## //# sourceMappingURL=xxx.js.map
    # 详细配置: https://github.com/terser/terser#minify-options
    options: {}
    css:
    enable: true
    # 详细配置: https://github.com/clean-css/clean-css#compatibility-modes
    options: {}
    html:
    enable: true
    # 详细配置: https://github.com/kangax/html-minifier#options-quick-reference
    options:
    minifyJS: true # Compressed JavaScript
    minifyCSS: true # CSS Compressed
    removeComments: true # Remove the comments
    collapseWhitespace: true # Delete any extra space
    removeAttributeQuotes: true # Delete attribute quotes
    image:
    enable: true
    svg:
    enable: true
    # 详细配置: https://github.com/imagemin/imagemin-svgo#imageminsvgooptionsbuffer
    options: {}
    jpg:
    enable: true
    # 详细配置: https://github.com/imagemin/imagemin-jpegtran#options
    options: {}
    png:
    enable: true
    # 详细配置: https://github.com/imagemin/imagemin-pngquant#options
    options: {}
    gif:
    enable: true
    # 详细配置: https://www.npmjs.com/package/imagemin-gifsicle#options
    options: {}
    webp:
    enable: true
    # 详细配置: https://github.com/imagemin/imagemin-webp#options
    options: {}
    font:
    enable: false
    # 详细配置: https://github.com/Lete114/fontmin-spider#api
    options: {}
  • 参考:

加载动画

具体步骤
  1. 添加[BlogRoot]\source\csscustom-loader.css

    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
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    /* 自定义加载动画样式 */
    .custom-loader-container {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(255, 255, 255, 0.85); /* 半透明背景 */
    backdrop-filter: blur(20px); /* 毛玻璃模糊效果 */
    -webkit-backdrop-filter: blur(20px);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 9999;
    transition: transform 0.6s cubic-bezier(0.7, 0, 0.3, 1), opacity 0.6s ease-out; /* 幕布拉起动画 */
    }

    .custom-loader-container.dark {
    background-color: rgba(13, 13, 13, 0.85);
    }

    .custom-loader-container.fade-out {
    transform: translateY(-100%); /* 向上拉起幕布 */
    /* opacity: 0; 如果想要淡出而不是拉起,可以使用 opacity */
    }

    /* 加载动画内容 */
    .loader-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    }

    /* 图片加载器 */
    .loader-image {
    width: 120px;
    height: 120px;
    animation: spin 1s linear infinite;
    }

    /* 旋转动画 */
    @keyframes spin {
    0% {
    transform: rotate(0deg);
    }
    100% {
    transform: rotate(360deg);
    }
    }

    /* 脉冲动画 */
    @keyframes pulse {
    0%, 100% {
    opacity: 1;
    }
    50% {
    opacity: 0.7;
    }
    }

    .loader-image.pulse {
    animation: pulse 1.5s ease-in-out infinite;
    }

    /* 加载文字 */
    .loader-text {
    font-size: 16px;
    color: #333;
    font-weight: 500;
    }

    .custom-loader-container.dark .loader-text {
    color: #ccc;
    }

    /* 加载进度条 */
    .loader-progress {
    width: 200px;
    height: 4px;
    background-color: #f0f0f0;
    border-radius: 2px;
    overflow: hidden;
    margin-top: 10px;
    }

    .custom-loader-container.dark .loader-progress {
    background-color: #333;
    }

    .loader-progress-bar {
    height: 100%;
    background: linear-gradient(90deg, #49b1f5, #00c4b6);
    border-radius: 2px;
    animation: progress 2s ease-in-out infinite;
    }

    @keyframes progress {
    0% {
    width: 0%;
    }
    50% {
    width: 100%;
    }
    100% {
    width: 100%;
    }
    }

    /* 响应式设计 */
    @media (max-width: 768px) {
    .loader-image {
    width: 80px;
    height: 80px;
    }

    .loader-text {
    font-size: 14px;
    }

    .loader-progress {
    width: 150px;
    }
    }

  2. 添加[BlogRoot]\source\js\custom-loader.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
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    /**
    * 自定义加载动画
    * 配置参数通过 window.loaderConfig 传递
    */

    (function() {
    'use strict';

    // 获取加载动画配置
    function getLoaderConfig() {
    return window.loaderConfig || {};
    }

    // 初始化加载器
    function initLoader() {
    const config = getLoaderConfig();

    // 如果禁用加载器则直接返回
    if (config.enable === false) {
    return;
    }

    const container = document.createElement('div');
    container.className = 'custom-loader-container';

    // 检查是否为深色模式
    if (config.darkMode || (document.documentElement.getAttribute('data-theme') === 'dark')) {
    container.classList.add('dark');
    }

    // 创建加载内容
    const content = document.createElement('div');
    content.className = 'loader-content';

    // 添加加载图片
    if (config.imageUrl) {
    const img = document.createElement('img');
    img.className = 'loader-image ' + (config.animationType || 'spin');
    img.src = config.imageUrl;
    img.alt = 'Loading';
    content.appendChild(img);
    } else {
    // 默认加载动画(纯CSS)
    const defaultLoader = document.createElement('div');
    defaultLoader.className = 'loader-image ' + (config.animationType || 'spin');
    defaultLoader.innerHTML = '<svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><circle cx="25" cy="25" r="20" fill="none" stroke="#49b1f5" stroke-width="3"/></svg>';
    content.appendChild(defaultLoader);
    }

    // 添加加载文字
    if (config.text) {
    const text = document.createElement('div');
    text.className = 'loader-text';
    text.textContent = config.text;
    content.appendChild(text);
    }

    // 添加进度条
    if (config.showProgress !== false) {
    const progress = document.createElement('div');
    progress.className = 'loader-progress';
    const progressBar = document.createElement('div');
    progressBar.className = 'loader-progress-bar';
    progress.appendChild(progressBar);
    content.appendChild(progress);
    }

    container.appendChild(content);
    document.body.insertBefore(container, document.body.firstChild);

    // 监听页面加载完成
    if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', function() {
    hideLoader(container, config.duration || 500);
    });
    } else {
    // 页面已加载完成
    hideLoader(container, config.duration || 500);
    }

    // 监听 pjax 或其他动态加载事件
    if (window.Pjax) {
    document.addEventListener('pjax:beforeSend', function() {
    showLoader(container, config);
    });
    document.addEventListener('pjax:complete', function() {
    hideLoader(container, config.duration || 500);
    });
    }
    }

    // 显示加载器
    function showLoader(container, config) {
    if (container.classList.contains('fade-out')) {
    container.classList.remove('fade-out');
    }

    // 检查暗色模式
    if (config && config.darkMode) {
    container.classList.add('dark');
    } else if (document.documentElement.getAttribute('data-theme') === 'dark') {
    container.classList.add('dark');
    } else {
    container.classList.remove('dark');
    }
    }

    // 隐藏加载器
    function hideLoader(container, duration) {
    setTimeout(function() {
    container.classList.add('fade-out');
    }, duration);
    }

    // 在 DOM 加载完成后初始化
    if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initLoader);
    } else {
    initLoader();
    }
    })();

    3.在_config.butterfly.yml中的inject内添加路径

    1
    2
    3
    4
    5
    inject:
    head:
    - <link rel="stylesheet" href="/css/custom-loader.css">
    bottom:
    - <script async src="/js/custom-loader.js"></script>

评论

Twikoo评论

具体步骤
  • 搭建

    1. 搭建,本博客的评论系统主要通过vercel部署。具体可参照Twikoo 文档
    2. 部署完成后修改_config.butterfly.yml中的commenttwikoo配置项即可。可参考Butterfly 文檔(三) 主題配置 | Butterfly中的评论配置。
      • 第一次进入管理面板需设置管理员登录密码,在管理面板中可管理评论
  • 美化

    1. _config.butterfly.yml的 inject 中添加placeholder参数。

      1
      2
      3
      4
      5
      6
      7
      twikoo:
      envId: https://twikoojs-cyan.vercel.app/
      region:
      # 使用Twikoo访客统计作为页面浏览量
      visitor: false
      option:
      placeholder: '欢迎留言,支持Markdown语法,快来评论吧~'

娱乐页

具体步骤

追番、游戏页面

  1. 插件安装

    1
    npm install hexo-bilibili-bangumi --save
  2. Bangumi 番组计划中收藏番剧或者游戏。然后打开控制台(f12),输入CHOBITS_UID获取用户ID.

  3. 添加配置:在_config.yml 中添加,pathvimd 等参数替换为自己的,具体配置可参考:hexo-bilibili-bangumi

    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
    # hexo-bilibili-bangumi
    # https://github.com/HCLonely/hexo-bilibili-bangumi
    bangumi: # 追番设置
    enable: true # 是否启用
    source: bangumi # 数据源
    path: animations/index.html # 页面路径
    vmid: x1ang # 用户ID
    title: '追番列表' # 页面标题
    quote: '你所热爱的 就是你的生活' # 页面引言
    show: 2 # 初始显示页面: 0=想看, 1=在看, 2=看过
    lazyload: true # 是否启用图片懒加载
    metaColor: '#f2b94b' # meta 信息字体颜色
    color: '#6587b5' # 简介字体颜色
    webp: true # 是否使用 webp 格式图片
    pagination: true
    progress: true
    progressBar: false
    extraOrder: 1
    order: '-score'
    game: # 游戏设置,仅支持source: bgmv0
    enable: true
    path: games/index.html
    source: bgmv0
    vmid: x1ang
    title: '一部游戏一段人生'
    quote: '跌宕起伏的故事,引人入胜的玩法,余音绕梁的旋律,沉浸式体验另一段时空'
    show: 2
    lazyload: true
    metaColor: '#f2b94b'
    color: '#99A9BF'
    webp: true
    pagination: true
    progress: true
    progressBar: false
    extraOrder: 1
    order: -score
  4. 页面美化:在 [BlogRoot]/static/css/custom.css 文件中添加以下代码 (具体颜色可以自己调节):

    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
    /* 番剧插件 */
    #page #article-container .bangumi-tab.bangumi-active {
    background: #425aef !important;
    color: #f7f7fa !important;
    border-radius: 10px;
    }
    #page #article-container .bangumi-tabs .bangumi-tab {
    border-bottom: none;
    border-radius: 10px;
    }
    #page #article-container .bangumi-tabs a.bangumi-tab:hover {
    text-decoration: none !important;
    border-radius: 10px;
    column-gap: #f7f7fa !important;
    }
    #page #article-container .bangumi-pagination a.bangumi-button {
    border-bottom: none;
    border-radius: 10px;
    }
    .bangumi-button:hover {
    background: #425aef !important;
    border-radius: 10px !important;
    color: #f7f7fa !important;
    }
    a.bangumi-button.bangumi-nextpage:hover {
    text-decoration: none !important;
    }
    .bangumi-button {
    padding: 5px 10px !important;
    }

    a.bangumi-tab {
    padding: 5px 10px !important;
    }
    svg.icon.faa-tada {
    font-size: 1.1em;
    }
    .bangumi-info-item {
    border-right: 1px solid #f2b94b;
    }
    .bangumi-info-item span {
    color: #f2b94b;
    }
    .bangumi-info-item em {
    color: #f2b94b;
    }
  5. 参考

  6. 获取数据:命令行运行:

    1
    2
    hexo game -u;
    hexo bangumi -u;

配置版

分类条

具体步骤
  1. 新建文件:[BlogRoot]\themes\butterfly\layout\includes\categoryBar.pug

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .category-bar-items#category-bar-items(class=is_home() ? 'home' : '')
    .category-bar-item(class=is_home() ? 'select' : '', id="category-bar-home")
    a(href=url_for('/'))= __('博客首页')
    each item in site.categories.find({ parent: { $exists: false } }).data
    .category-bar-item(class=select ? (select === item.name ? 'select' : '') : '', id=item.name)
    a(href=url_for(item.path))= item.name
    .category-bar-item
    a(href=url_for('/archives/'))= __('文章存档')
    div.category-bar-right
    a.category-bar-more(href=url_for('/categories/'))= __('更多分类')

  2. 新建文件[BlogRoot]\themes\butterfly\source\css\_layout\category-bar.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
    #category-bar
    padding 7px 11px
    // 方案 1:推荐(混合 --card-bg 和浅灰,无透明,层次感强,不影响子元素)
    background color-mix(var(--card-bg), #f8f9fa, 80%)
    // 方案 2:直接使用 --card-bg 并降低轻微不透明度(背景独立不透明,不影响子元素)
    // background rgba(hue(var(--card-bg)), saturation(var(--card-bg)), lightness(var(--card-bg)), 0.9)
    // 方案 3:--card-bg 基础上加轻微渐变(无透明,视觉更立体)
    // background linear-gradient(180deg, var(--card-bg), color-mix(var(--card-bg), #eee, 95%))
    border-radius 8px
    display flex
    white-space nowrap
    overflow hidden
    transition 0.3s
    height 50px
    width 100%
    justify-content space-between
    user-select none
    align-items center
    margin-bottom 20px

    .category-bar-right
    display flex
    border-radius 8px
    align-items center

    .category-bar-more
    margin-left 4px
    margin-right 4px
    font-weight 700
    border-radius 8px
    padding 0 8px

    .category-bar-items
    width 100%
    white-space nowrap
    overflow-x scroll
    scrollbar-width: none
    -ms-overflow-style: none
    overflow-y hidden
    display flex
    border-radius 8px
    align-items center
    height 30px

    &::-webkit-scrollbar
    display: none

    .category-bar-item
    a
    padding .1rem .5rem
    margin-right 6px
    font-weight 700
    border-radius 8px
    display flex
    align-items center
    height 30px
    color var(--font-color) // 增加默认文字颜色

    &:hover
    color var(--theme-color) // 悬停颜色

    &.select
    a
    background #3eb8be
    color var(--btn-color)
  3. 修改[BlogRoot]\themes\butterfly\layout\category.pug,添加其中两行代码,去掉加号即为正常缩进:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    extends includes/layout.pug

    block content
    if theme.category_ui == 'index'
    include ./includes/mixins/indexPostUI.pug
    +indexPostUI
    else
    include ./includes/mixins/article-sort.pug
    + .recent-posts.nc(id="category-bar-wrapper")
    + #category-bar.category-bar
    + include includes/categoryBar.pug
    #category
    .article-sort-title= _p('page.category') + ' - ' + page.category
    +articleSort(page.posts)
    include includes/pagination.pug
  4. 打开文件[BlogRoot]\themes\butterfly\layout\index.pug文件,添加下面两行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    extends includes/layout.pug

    block content
    + .recent-posts.nc(id="category-bar-wrapper")
    + #category-bar.category-bar
    + include includes/categoryBar.pug

    include ./includes/mixins/indexPostUI.pug
    +indexPostUI
  5. 打开[BlogRoot]\themes\butterfly\source\js\main.js,添加js函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
      const toggleCardCategory = () => {

    // 新增
    const setCategoryBarActive = () => {
    const categoryBar = document.querySelector("#category-bar");
    const currentPath = decodeURIComponent(window.location.pathname);
    const isHomePage = currentPath === GLOBAL_CONFIG.root;

    if (categoryBar) {
    const categoryItems = categoryBar.querySelectorAll(".category-bar-item");
    categoryItems.forEach(item => item.classList.remove("select"));

    const activeItemId = isHomePage ? "category-bar-home" : currentPath.split("/").slice(-2, -1)[0];
    const activeItem = document.getElementById(activeItemId);

    if (activeItem) {
    activeItem.classList.add("select");
    }
    }
    };

    const addPostOutdateNotice = () => {
  6. 然后再在引用部分执行这个函数,在同一个文件,找到下面的函数并添加函数的调用,位置看下方注释:

    1
    2
    3
    4
    5
    6
    7
    GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
    scrollFn()

    // ========== 新增:调用分类栏激活态切换函数 ==========
    setCategoryBarActive()

    forPostFn()

以下与分类条的改动无关,为我个人修改,以防忘记,特记录下来


首页文章列表 (indexPostUI.pug)

文件路径: themes/butterfly/layout/includes/mixins/indexPostUI.pug

目的是为了在文章列表容器 #recent-posts 的最开始插入分类条代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//- 原始代码 (Before)
mixin indexPostUI()
// ... (省略变量定义)
#recent-posts.recent-posts.nc
(class=masonryLayoutClass)
.recent-post-items
each article, index in page.
posts.data
// ...

//- 现在代码 (After)
mixin indexPostUI()
// ... (省略变量定义)
#recent-posts.recent-posts.nc
(class=masonryLayoutClass)
// [新增] 插入分类条。Swiper 会通
过 JS 插在它上面,文章在它下面。
#category-bar.category-bar
include ../categoryBar.pug
.recent-post-items
each article, index in page.
posts.data
// ...

分类归档页面 (category.pug)

文件路径: themes/butterfly/layout/category.pug

为了让分类页面的布局不崩坏(保持 74% 宽度),并显示分类条,重构 else 分支。

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
//- 原始代码 (Before)
else
include ./includes/mixins/
article-sort.pug
#category
.article-sort-title= _p('page.
category') + ' - ' + page.
category
+articleSort(page.posts)
include includes/pagination.
pug

//- 现在代码 (After)
else
include ./includes/mixins/
article-sort.pug
// [修改] 添加外层容器 .
recent-posts.nc 以修复布局宽度问题
.recent-posts.nc
(id="category-bar-wrapper")
// [新增] 添加分类条
#category-bar.category-bar
include includes/
categoryBar.pug
// 原有的分类内容
#category
.article-sort-title= _p
('page.category') + ' - ' +
page.category
+articleSort(page.posts)
include includes/pagination.
pug

外挂标签

具体步骤

note标签

  1. 修改[BlogRoot]\themes\liushen\scripts\tag\note.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
    /**
    * note 外挂标签,分为警告、错误、问题、信息,比原版更加简单,样式更加漂亮
    * by: LiuShen
    * 样式参考: https://blog.zhilu.cyou/
    */

    'use strict'

    const postNote = (args, content) => {
    // 定义五种类型及其对应的 Font Awesome 图标
    const types = {
    warning: 'fa-circle-dot', // 警告
    error: 'fa-circle-xmark', // 错误
    question: 'fa-circle-question', // 问题
    info: 'fa-circle-check', // 信息
    };

    // 获取标签类型和标题
    const type = args[0] || 'info'; // 如果未提供类型,默认为 info
    const title = args.slice(1).join(' ') || '附加信息'; // 提取标题,默认为 "提示"
    const icon = types[type] || types.info; // 如果类型未定义,使用 info 类型的图标

    // 判断标签类型是否在 types 中定义
    if (!types[type]) {
    console.warn(`\`${type}\` 类型未定义,已自动切换为 \`info\` 类型`);
    type = 'info';
    title = '附加信息';
    icon = types.info;
    }

    // 返回 HTML 结构
    return `
    <div class="note note-${type}">
    <div class="note-header">
    <i class="note-icon fa-regular ${icon}"></i>
    <span class="note-title">${title}</span>
    </div>
    <div class="note-content">
    ${hexo.render.renderSync({ text: content, engine: 'markdown' })}
    </div>
    </div>
    `;
    };

    // 注册自定义标签
    hexo.extend.tag.register('note', postNote, { ends: true });

  2. 美化标签样式[BlogRoot]\themes\liushen\source\css\_tags\note.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
    .note
    addBorderRadius()
    border var(--liushen-card-border)
    position: relative
    margin: 0 0 20px
    padding: 15px 15px 7px 15px
    background-color: var(--liushen-card-bg)
    transition: transform 0.3s ease

    &:hover
    transform: translateY(-3px)

    &.note-warning
    background-image: radial-gradient(circle at 4em -25em, #f0ad4e, transparent 30em),
    linear-gradient(#f0ad4e -2000%, transparent)
    .note-header
    color: #f0ad4e

    &.note-info
    background-image: radial-gradient(circle at 4em -25em, #5cb85c, transparent 30em),
    linear-gradient(#5cb85c -2000%, transparent)
    .note-header
    color: #5cb85c

    &.note-error
    background-image: radial-gradient(circle at 4em -25em, #d9534f, transparent 30em),
    linear-gradient(#d9534f -2000%, transparent)
    .note-header
    color: #d9534f

    &.note-question
    background-image: radial-gradient(circle at 4em -25em, #5bc0de, transparent 30em),
    linear-gradient(#5bc0de -2000%, transparent)
    .note-header
    color: #5bc0de

    .note-header
    display: flex
    align-items: center
    font-weight: bold
    margin-bottom: 10px
    font-size: 1.0em
    i.note-icon
    margin-right: 10px
    color: inherit

link标签

  1. 新建文件[BlogRoot]\themes\butterfly\scripts\tag\link.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
    function link(args) {
    args = args.join(' ').split(',');
    let title = args[0];
    let sitename = args[1];
    let link = args[2];

    // 定义不同域名对应的头像URL
    const avatarUrls = {
    'github.com': 'https://p.liiiu.cn/i/2024/07/27/66a461a3098aa.webp',
    'csdn.net': 'https://p.liiiu.cn/i/2024/07/27/66a461b627dc2.webp',
    'gitee.com': 'https://p.liiiu.cn/i/2024/07/27/66a461c3dea80.webp',
    'zhihu.com': 'https://p.liiiu.cn/i/2024/07/27/66a461cc20eb4.webp',
    'stackoverflow.com': 'https://p.liiiu.cn/i/2024/07/27/66a461d3be02e.webp',
    'wikipedia.org': 'https://p.liiiu.cn/i/2024/07/27/66a461db48579.webp',
    'baidu.com': 'https://p.liiiu.cn/i/2024/07/27/66a461e1ae5b5.webp',
    'qyliu.top': 'https://p.liiiu.cn/i/2024/08/01/66aae601dbc9b.webp',
    'liushen.fun': 'https://p.liiiu.cn/i/2024/08/01/66aae601dbc9b.webp',
    'lius.me': 'https://p.liiiu.cn/i/2024/08/01/66aae601dbc9b.webp',
    };

    // 定义白名单域名
    const whitelistDomains = [
    'lius.me', 'qyliu.top', 'liushen.fun'
    ];

    // 获取URL的根域名
    function getRootDomain(url) {
    const hostname = new URL(url).hostname;
    const domainParts = hostname.split('.').reverse();
    if (domainParts.length > 1) {
    return domainParts[1] + '.' + domainParts[0];
    }
    return hostname;
    }

    // 根据URL获取对应的头像URL
    function getAvatarUrl(url) {
    const rootDomain = getRootDomain(url);
    for (const domain in avatarUrls) {
    if (domain.endsWith(rootDomain)) {
    return avatarUrls[domain];
    }
    }
    return 'https://b5dbf24.webp.li/hexo/PixPin_2026-02-01_22-43-45.webp'; // 默认头像URL
    }

    // 检查是否在白名单中
    function isWhitelisted(url) {
    const rootDomain = getRootDomain(url);
    for (const domain of whitelistDomains) {
    if (rootDomain.endsWith(domain)) {
    return true;
    }
    }
    return false;
    }

    // 获取对应的头像URL
    let imgUrl = getAvatarUrl(link);

    // 判断并生成提示信息
    // 判断并生成提示信息
    let tipMessage = isWhitelisted(link)
    ? "✅来自本站,本站可确保其安全性,请放心点击跳转"
    : "🪧引用站外地址,不保证站点的可用性和安全性";

    return `<div class='liushen-tag-link'><a class="tag-Link" target="_blank" href="${link}">
    <div class="tag-link-tips">${tipMessage}</div>
    <div class="tag-link-bottom">
    <div class="tag-link-left" style="background-image: url(${imgUrl});"></div>
    <div class="tag-link-right">
    <div class="tag-link-title">${title}</div>
    <div class="tag-link-sitename">${sitename}</div>
    </div>
    <i class="fa-solid fa-angle-right"></i>
    </div>
    </a></div>`;
    }

    hexo.extend.tag.register('link', link, { ends: false });

  2. 修改样式[BlogRoot]\themes\butterfly\source\css\_tags\link.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
    :root
    --tag-link-bg-color white
    --tag-link-text-color black
    --tag-link-border-color white
    --tag-link-hover-bg-color rgb(141, 216, 233)
    --tag-link-hover-border-color black
    --tag-link-tips-border-color black
    --tag-link-sitename-color rgb(144, 144, 144)
    --tag-link-hover-sitename-color black

    [data-theme=dark]
    --tag-link-bg-color #2d2d2d
    --tag-link-text-color white
    --tag-link-border-color black
    --tag-link-hover-bg-color #339297
    --tag-link-hover-border-color white
    --tag-link-tips-border-color white
    --tag-link-sitename-color rgb(144, 144, 144)
    --tag-link-hover-sitename-color white

    #article-container
    .tag-Link
    background var(--tag-link-bg-color)
    border-radius 12px !important
    display flex
    border 1px solid var(--tag-link-border-color)
    flex-direction column
    padding 0.5rem 1rem
    margin-top 1rem
    text-decoration none !important
    color var(--tag-link-text-color)
    margin-bottom 10px
    transition background-color 0.3s, border-color 0.3s, box-shadow 0.3s

    &:hover
    border-color var(--tag-link-hover-border-color)
    background-color var(--tag-link-hover-bg-color)
    box-shadow 0 0 5px rgba(0, 0, 0, 0.2)

    .tag-link-tips
    color var(--tag-link-text-color)
    border-bottom 1px solid var(--tag-link-tips-border-color)
    padding-bottom 4px
    font-size 0.6rem
    font-weight normal

    .tag-link-bottom
    display flex
    margin-top 0.5rem
    align-items center
    justify-content space-around

    .tag-link-left
    width 60px
    min-width 60px
    height 60px
    background-size cover
    border-radius 25%

    .tag-link-right
    margin-left 1rem

    .tag-link-title
    font-size 1rem
    line-height 1.2

    .tag-link-sitename
    font-size 0.7rem
    color var(--tag-link-sitename-color)
    font-weight normal
    margin-top 4px
    transition color 0.3s

    &:hover .tag-link-sitename
    color var(--tag-link-hover-sitename-color)

    i
    margin-left auto

  • 参考文章
  1. info标签

    1
    2
    3
    {% note info 这是标题 %}
    这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
    {% endnote %}
    • 效果
    这是标题

    这个是内容展示,里面可以正常使用Markdown的相关渲染格式

  2. warning标签

    1
    2
    3
    {% note warning 这是标题 %}
    这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
    {% endnote %}
    • 效果
    这是标题

    这个是内容展示,里面可以正常使用Markdown的相关渲染格式

  3. question标签

    1
    2
    3
    {% note question 这是标题 %}
    这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
    {% endnote %}
    • 效果
    这是标题

    这个是内容展示,里面可以正常使用Markdown的相关渲染格式

  4. error标签

    1
    2
    3
    {% note error 这是标题 %}
    这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
    {% endnote %}
    • 效果
    这是标题

    这个是内容展示,里面可以正常使用Markdown的相关渲染格式

侧边栏

来访者

具体步骤
  1. 新建[BlogRoot]\themes\liushen\layout\includes\widget\card_welcome.pug

    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
    .card-widget.card-welcome
    .item-headline
    i.fa.fa-user
    span 欢迎来访者!
    .item-content
    p &nbsp&nbsp👋🏻我是Kaitumei,一个
    span(style="font-weight: bold; color: #3498db") 热爱记录、喜欢分享
    | 的小卡拉米。😊
    p &nbsp&nbsp希望在这里你能探寻到你心中的那座
    span(style="font-weight: bold; color: #7a3debff") 岛屿🗻。
    br
    | &nbsp&nbsp疲惫时,你可以点击左下方的神秘小物件,然后放松放松哦~
    | 如果遇到什么问题请通过
    a(href="kaitumei@163.com" style="font-weight: bold; color: #9b59b6") 邮箱
    | 联系我!📧
    #welcome-info
    .error-message(style="height: 200px; display: flex; justify-content: center; align-items: center;")
    p(style="text-align: center;")
    span(style="font-size: 40px;") 😥
    br
    span(style="font-size: 16px;") 由于网络问题
    br
    span(style="font-size: 16px;") 位置API请求错误
    br
    span(style="font-size: 16px;") 请刷新重试呀🤗~
  2. 同目录底下index.pug

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      else
    //- page
    !=partial('includes/widget/card_author', {}, {cache: true})
    + if is_home()
    + !=partial('includes/widget/card_welcome', {}, {cache: true})
    !=partial('includes/widget/card_announcement', {}, {cache: true})
    !=partial('includes/widget/card_top_self', {}, {cache: true})


    .sticky_layout
  3. 新建[BlogRoot]\themes\liushen\source\css\_layout\card-welcome.styl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #aside-content
    .card-widget.card-welcome
    padding: 8px
    .item-headline
    margin: 12px 16px 0 16px
    .item-content
    margin: 0 16px 10px 16px
    #welcome-info
    text-align center
    background-color: var(--liushen-card-bg)
    border-radius: 12px
    padding: 16px
    margin: 8px
    // border: 2px solid var(--liushen-text)
    .ip-address
    // 模糊效果
    filter: blur(5px)
    color: var(--default-bg-color)
    transition: filter 0.5s
    &:hover
    filter: none
  4. 在任意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
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    // 进行 fetch 请求
    fetch('https://uapis.cn/api/v1/network/myip?source=commercial')
    .then(response => {
    if (!response.ok) {
    throw new Error('Network response was not ok');
    }
    return response.json();
    })
    .then(data => {
    // 适配 uapis.cn API 返回格式
    if (data && data.region) {
    const regions = data.region.split(' ');
    ipLocation = {
    ip: data.ip,
    data: {
    lng: data.longitude,
    lat: data.latitude,
    country: regions[0] || '',
    prov: regions[1] || '',
    city: regions[2] || '',
    district: data.district || ''
    }
    };
    } else {
    ipLocation = data;
    }

    if (isHomePage()) {
    showWelcome();
    }
    })
    .catch(error => console.error('Error fetching IP data:', error));

    let ipLocation; // 显式声明变量

    function getDistance(e1, n1, e2, n2) {
    if ([e1, n1, e2, n2].some(v => typeof v !== 'number' || isNaN(v))) {
    console.warn('Invalid coordinates for getDistance:', e1, n1, e2, n2);
    return 0;
    }
    const R = 6371;
    const { sin, cos, asin, PI, hypot } = Math;
    let getPoint = (e, n) => {
    e *= PI / 180;
    n *= PI / 180;
    return { x: cos(n) * cos(e), y: cos(n) * sin(e), z: sin(n) };
    };

    let a = getPoint(e1, n1);
    let b = getPoint(e2, n2);
    let c = hypot(a.x - b.x, a.y - b.y, a.z - b.z);
    let r = asin(c / 2) * 2 * R;
    return Math.round(r);
    }

    function showWelcome() {
    if (!ipLocation || !ipLocation.data) {
    console.error('ipLocation data is not available.');
    return;
    }

    let dist = getDistance(112.377552,26.975294, ipLocation.data.lng, ipLocation.data.lat); // 修改自己的经度(121.413921)纬度(31.089290)
    let pos = ipLocation.data.country;
    let ip = ipLocation.ip;
    let posdesc;

    // 新增ipv6显示为指定内容
    if (ip.includes(":")) {
    ip = "<br>好复杂,咱看不懂呢~(ipv6)";
    }

    // 以下的代码需要根据新API返回的结果进行相应的调整
    switch (ipLocation.data.country) {
    case "日本":
    posdesc = "よろしく,一起去看樱花吗";
    break;
    case "美国":
    posdesc = "Let us live in peace!";
    break;
    case "英国":
    posdesc = "想同你一起夜乘伦敦眼";
    break;
    case "俄罗斯":
    posdesc = "干了这瓶伏特加!";
    break;
    case "法国":
    posdesc = "C'est La Vie";
    break;
    case "德国":
    posdesc = "Die Zeit verging im Fluge.";
    break;
    case "澳大利亚":
    posdesc = "一起去大堡礁吧!";
    break;
    case "加拿大":
    posdesc = "拾起一片枫叶赠予你";
    break;
    case "中国":
    pos = ipLocation.data.prov + " " + ipLocation.data.city + " " + ipLocation.data.district;
    switch (ipLocation.data.prov) {
    case "北京市":
    posdesc = "北——京——欢迎你~~~";
    break;
    case "天津市":
    posdesc = "讲段相声吧";
    break;
    case "河北省":
    posdesc = "山势巍巍成壁垒,天下雄关铁马金戈由此向,无限江山";
    break;
    case "山西省":
    posdesc = "展开坐具长三尺,已占山河五百余";
    break;
    case "内蒙古自治区":
    posdesc = "天苍苍,野茫茫,风吹草低见牛羊";
    break;
    case "辽宁省":
    posdesc = "我想吃烤鸡架!";
    break;
    case "吉林省":
    posdesc = "状元阁就是东北烧烤之王";
    break;
    case "黑龙江省":
    posdesc = "很喜欢哈尔滨大剧院";
    break;
    case "上海市":
    posdesc = "众所周知,中国只有两个城市";
    break;
    case "江苏省":
    switch (ipLocation.data.city) {
    case "南京市":
    posdesc = "这是我挺想去的城市啦";
    break;
    case "苏州市":
    posdesc = "上有天堂,下有苏杭";
    break;
    default:
    posdesc = "散装是必须要散装的";
    break;
    }
    break;
    case "浙江省":
    switch (ipLocation.data.city) {
    case "杭州市":
    posdesc = "东风渐绿西湖柳,雁已还人未南归";
    break;
    default:
    posdesc = "望海楼明照曙霞,护江堤白蹋晴沙";
    break;
    }
    break;
    case "河南省":
    switch (ipLocation.data.city) {
    case "郑州市":
    posdesc = "豫州之域,天地之中";
    break;
    case "信阳市":
    posdesc = "品信阳毛尖,悟人间芳华";
    break;
    case "南阳市":
    posdesc = "臣本布衣,躬耕于南阳此南阳非彼南阳!";
    break;
    case "驻马店市":
    posdesc = "峰峰有奇石,石石挟仙气嵖岈山的花很美哦!";
    break;
    case "开封市":
    posdesc = "刚正不阿包青天";
    break;
    case "洛阳市":
    posdesc = "洛阳牡丹甲天下";
    break;
    default:
    posdesc = "可否带我品尝河南烩面啦?";
    break;
    }
    break;
    case "安徽省":
    posdesc = "蚌埠住了,芜湖起飞";
    break;
    case "福建省":
    posdesc = "井邑白云间,岩城远带山";
    break;
    case "江西省":
    posdesc = "落霞与孤鹜齐飞,秋水共长天一色";
    break;
    case "山东省":
    posdesc = "遥望齐州九点烟,一泓海水杯中泻";
    break;
    case "湖北省":
    switch (ipLocation.data.city) {
    case "黄冈市":
    posdesc = "红安将军县!辈出将才!";
    break;
    default:
    posdesc = "来碗热干面~";
    break;
    }
    break;
    case "湖南省":
    switch (ipLocation.data.city) {
    case "衡阳市":
    posdesc = "老乡见老乡,两眼泪汪汪";
    break;
    case "长沙市":
    posdesc = "74751,长沙斯塔克";
    break;
    }
    break;
    case "广东省":
    switch (ipLocation.data.city) {
    case "广州市":
    posdesc = "看小蛮腰,喝早茶了嘛~";
    break;
    case "深圳市":
    posdesc = "今天你逛商场了嘛~";
    break;
    case "阳江市":
    posdesc = "阳春合水!博主家乡~ 欢迎来玩~";
    break;
    default:
    posdesc = "我们都是地球人~";
    break;
    }
    break;
    case "广西壮族自治区":
    posdesc = "桂林山水甲天下";
    break;
    case "海南省":
    posdesc = "朝观日出逐白浪,夕看云起收霞光";
    break;
    case "四川省":
    posdesc = "康康川妹子";
    break;
    case "贵州省":
    posdesc = "茅台,学生,再塞200";
    break;
    case "云南省":
    posdesc = "玉龙飞舞云缠绕,万仞冰川直耸天";
    break;
    case "西藏自治区":
    posdesc = "躺在茫茫草原上,仰望蓝天";
    break;
    case "陕西省":
    posdesc = "来份臊子面加馍";
    break;
    case "甘肃省":
    posdesc = "羌笛何须怨杨柳,春风不度玉门关";
    break;
    case "青海省":
    posdesc = "牛肉干和老酸奶都好好吃";
    break;
    case "宁夏回族自治区":
    posdesc = "大漠孤烟直,长河落日圆";
    break;
    case "新疆维吾尔自治区":
    posdesc = "驼铃古道丝绸路,胡马犹闻唐汉风";
    break;
    case "台湾省":
    posdesc = "我在这头,大陆在那头";
    break;
    case "香港特别行政区":
    posdesc = "永定贼有残留地鬼嚎,迎击光非岁玉";
    break;
    case "澳门特别行政区":
    posdesc = "性感荷官,在线发牌";
    break;
    default:
    posdesc = "带我去你的城市逛逛吧!";
    break;
    }
    break;
    default:
    posdesc = "带我去你的国家逛逛吧";
    break;
    }

    // 根据本地时间切换欢迎语
    let timeChange;
    let date = new Date();
    if (date.getHours() >= 5 && date.getHours() < 11) timeChange = "<span>🌤️ 早上好,一日之计在于晨</span>";
    else if (date.getHours() >= 11 && date.getHours() < 13) timeChange = "<span>☀️ 中午好,记得午休喔~</span>";
    else if (date.getHours() >= 13 && date.getHours() < 17) timeChange = "<span>🕞 下午好,饮茶先啦!</span>";
    else if (date.getHours() >= 17 && date.getHours() < 19) timeChange = "<span>🚶‍♂️ 即将下班,记得按时吃饭~</span>";
    else if (date.getHours() >= 19 && date.getHours() < 24) timeChange = "<span>🌙 晚上好,夜生活嗨起来!</span>";
    else timeChange = "夜深了,早点休息,少熬夜";

    let welcomeInfoElement = document.getElementById("welcome-info");

    if (welcomeInfoElement) {
    welcomeInfoElement.innerHTML =
    `🎉欢迎!!!━(*`∀´*)ノ亻!<b><span style="color: var(--tags-blue-color)"> <br>${pos}</span></b>
    <br>的大神👀<br>您的IP:<b><span class="blur-text" style="color: var(--tags-blue-color)">${ip}</span></b>
    <br>当前位置距博主约 <b><span class="blur-text" style="color: var(--anzhiyu-main)">${dist.toFixed(2)}</span></b> 公里!
    <br>${timeChange}<br>Tip:<b><span style="font-size: 15px;">${posdesc}</span></b>`;
    } else {
    console.log("Pjax无法获取元素");
    }
    }

    // Pjax完成页面切换的事件回调处理
    function handlePjaxComplete() {
    if (isHomePage()) {
    if (ipLocation && ipLocation.data) {
    showWelcome();
    }
    }
    }

    function isHomePage() {
    return window.location.pathname === '/' || window.location.pathname === '/index.html';
    }


    // 添加pjax:complete事件监听
    window.onload = function () {
    if (isHomePage()) {
    if (ipLocation && ipLocation.data) {
    showWelcome();
    }
    }
    document.addEventListener("pjax:complete", handlePjaxComplete);
    };
  5. 参考

导航栏美化

具体步骤
  1. [BlogRoot]\themes\butterfly\layout\includes\header\nav.pug 文件中下方代码标出来的内容,去掉加号即为正常缩进。

    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
    nav#nav
    span#blog-info
    a.nav-site-title(href=url_for('/') title=config.title)
    if theme.nav.logo
    img.site-icon(src=url_for(theme.nav.logo))
    if theme.nav.display_title
    span.site-name=config.title

    #menus
    if theme.menu
    != partial('includes/header/menu_item', {}, {cache: true})

    div#name-container
    a(id="page-name" href="javascript:btf.scrollToDest(0, 500)") PAGE_NAME

    #nav-right
    #travellings
    a.site-page(href=url_for('https://www.travellings.cn/go.html') title="友链接力-随机开往" target="_blank" rel="noopener")
    i.fa-solid.fa-bus.fa-fw

    #ten-years
    a.site-page(href=url_for('https://foreverblog.cn/go.html') title="友链接力-十年之约" target="_blank" rel="noopener")
    i.fa-brands.fa-nfc-symbol.fa-fw

    #random
    a.site-page(href="javascript:toRandomPost()" title="随机前往一个文章")
    i.fa-solid.fa-shuffle.fa-fw

    if theme.search.use
    #search-button
    a.site-page.social-icon.search(href="javascript:void(0);" title="站内搜索")
    i.fas.fa-search.fa-fw

    #toggle-menu
    a.site-page(href="javascript:void(0);" title="展开菜单")
    i.fas.fa-bars.fa-fw

  2. 添加自定义 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
    document.addEventListener('pjax:complete', tonav);
    document.addEventListener('DOMContentLoaded', tonav);

    function tonav() {
    var nameContainer = document.querySelector("#nav #name-container");
    var menusItems = document.querySelector("#nav .menus_items");

    // 如果找不到元素,直接返回
    if (!nameContainer || !menusItems) return;

    // 使用 requestAnimationFrame 优化性能
    var position = window.scrollY;
    var ticking = false;

    window.addEventListener('scroll', function() {
    if (!ticking) {
    window.requestAnimationFrame(function() {
    var scroll = window.scrollY;

    if (scroll > position + 5) {
    // 向下滚动
    nameContainer.classList.add("visible");
    menusItems.classList.remove("visible");
    } else if (scroll < position - 5) {
    // 向上滚动
    nameContainer.classList.remove("visible");
    menusItems.classList.add("visible");
    }

    position = scroll;
    ticking = false;
    });

    ticking = true;
    }
    });

    // 初始化 page-name
    var pageName = document.getElementById("page-name");
    if (pageName) {
    // 获取标题并去除后缀
    var title = document.title;
    var separator = " | "; // 根据实际标题分隔符调整
    if (title.includes(separator)) {
    pageName.innerText = title.split(separator)[0];
    } else {
    pageName.innerText = title;
    }
    }
    }
  3. 添加CSS,对样式进行美化。

    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
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    /* 导航栏基础布局 */
    #nav-right{
    flex:1;
    justify-content: flex-end;
    margin-left: auto;
    display: flex;
    flex-wrap:nowrap;
    }

    #nav.show {
    display: flex;
    justify-content: center;
    }

    #nav .site-page {
    padding-bottom: 14px;
    }

    #page-header.not-top-img #nav {
    display: flex;
    justify-content: center;
    }

    #nav-group {
    width: 1400px;
    display: flex;
    align-items: center;
    padding: 0 0.6rem;
    margin-left: auto;
    margin-right: auto;
    }

    /* 顶部栏宽度定义 */
    /* 修复:移除这里的 margin-top,防止覆盖后续的样式 */
    #nav .menus_items .menus_item .menus_item_child {
    margin-top: 0;
    }

    #nav .menus_items .menus_item .menus_item_child li a {
    padding-top: 6px;
    padding-right: 18px;
    padding-bottom: 6px;
    padding-left: 15px;
    text-align: center;
    }
    #nav #travellings {
    padding: 0 14px 0 ;
    }

    #nav #ten-years {
    padding: 0 14px 0 0;
    }

    /* 移动端菜单逻辑(如果主题有移动端菜单,这里主要处理桌面端) */
    @media (max-width: 768px) {
    #nav #page-name {
    display: none;
    }
    }

    /* ===========================================
    核心动画与交互逻辑
    =========================================== */

    /* 默认状态:隐藏 */
    #nav #name-container {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -100%); /* 向上偏移 */
    opacity: 0;
    pointer-events: none;
    transition: all 0.3s ease-in-out;
    }

    #nav .menus_items {
    position: absolute;
    width: fit-content;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -100%); /* 向上偏移 */
    opacity: 0;
    pointer-events: none;
    transition: all 0.3s ease-in-out;
    }

    /* 显示状态:居中可见 */
    #nav #name-container.visible {
    opacity: 1;
    transform: translate(-50%, -50%);
    pointer-events: auto;
    }

    #nav .menus_items.visible {
    opacity: 1;
    transform: translate(-50%, -50%);
    pointer-events: auto;
    }


    /* ===========================================
    导航栏样式优化 (桌面端)
    =========================================== */
    @media (min-width: 768px) {

    /* 菜单居中布局补充 */
    #nav .menus_items {
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0;
    padding: 0;
    }

    /* 菜单项样式 */
    #nav .menus_items .menus_item {
    font-size: 1.2em;
    font-weight: bold;
    margin: 0 5px;
    padding: 5px 10px;
    border-radius: 8px;
    transition: all 0.3s ease;
    flex-shrink: 0;
    white-space: nowrap;
    position: relative;
    }

    /* 修复:增加透明桥梁连接父子菜单,防止鼠标移出时菜单消失 */
    #nav .menus_items .menus_item::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    height: 20px; /* 覆盖子菜单的 margin-top */
    }

    /* 悬停效果 */
    #nav .menus_items .menus_item:hover {
    background: rgba(0, 0, 0, 0.05);
    }

    [data-theme="dark"] #nav .menus_items .menus_item:hover {
    background: rgba(255, 255, 255, 0.1);
    }

    /* 子菜单容器优化 - 增加 #nav 提高权重 */
    #nav .menus_items .menus_item .menus_item_child {
    display: none; /* 默认隐藏 */
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%); /* 确保居中 */
    white-space: nowrap;

    background: rgba(255, 255, 255, 0.95);
    border-radius: 20px;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
    padding: 5px;
    margin-top: 10px; /* 这里重新定义 margin-top */
    overflow: hidden;

    /* 动画 */
    animation: subMenuSlide 0.3s ease forwards;
    }

    /* hover显示子菜单 */
    #nav .menus_items .menus_item:hover .menus_item_child {
    display: flex;
    flex-direction: row;
    gap: 5px;
    }

    /* 深色模式子菜单 */
    [data-theme="dark"] #nav .menus_items .menus_item .menus_item_child {
    background: rgba(40, 40, 40, 0.95);
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
    }

    /* 子菜单项样式 */
    #nav .menus_items .menus_item .menus_item_child li {
    display: inline-block;
    margin: 0 2px;
    border-radius: 5px;
    }

    #nav .menus_items .menus_item .menus_item_child li a {
    display: block;
    padding: 5px 10px;
    border-radius: 5px;
    transition: background 0.2s;
    color: var(--font-color);
    }

    #nav .menus_items .menus_item .menus_item_child li a:hover {
    background: rgba(0, 0, 0, 0.05);
    color: var(--theme-color);
    }

    [data-theme="dark"] #nav .menus_items .menus_item .menus_item_child li a:hover {
    background: rgba(255, 255, 255, 0.1);
    }
    }

    /* 标题样式 */
    #nav #page-name {
    position: relative;
    padding: 10px 30px;
    font-weight: bold;
    white-space: nowrap;
    }

    /* 回到顶部 悬停效果 */
    #nav #page-name::before {
    font-size: 18px;
    position: absolute;
    width: 100%;
    height: 100%;
    border-radius: 12px;
    color: #4C4948;
    top: 0;
    left: 0;
    content: '回到顶部';
    background-color: #ffffff;
    transition: all 0.3s;
    opacity: 0;
    line-height: 45px;
    border: 1px solid #4C4948;
    display: flex;
    align-items: center;
    justify-content: center;
    }

    [data-theme=dark] #nav #page-name::before {
    color: #B9BABB;
    background-color: #000000;
    border: 1px solid #B9BABB;
    }

    #nav #page-name:hover:before {
    opacity: 1;
    }

    /* Logo和文字优化 */
    #blog-info .site-name {
    font-weight: bold;
    letter-spacing: 1px;
    }

    /* 隐藏导航栏下拉箭头 */
    #nav .menus_items .menus_item .fa-chevron-down {
    display: none;
    }

    /* 随机文章按钮样式 */
    #randomPost_button {
    display: inline-block;
    padding: 0 0 0 14px;
    vertical-align: middle;
    }

    /* 子菜单动画定义 */
    @keyframes subMenuSlide {
    0% {
    opacity: 0;
    transform: translateX(-50%) translateY(10px);
    }
    100% {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    }
    }

    a.site-page.child {
    border-radius: 4px;
    padding: 5px 10px !important;
    }
  4. 打开 [root]\themes\butterfly\layout\includes\header\menu_item.pug,添加visible参数,以防止刷新后被隐藏。

    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
    if theme.menu
    + .menus_items.visible
    - .menus_items
    each value, label in theme.menu
    if typeof value !== 'object'
    .menus_item
    - const valueArray = value.split('||')
    a.site-page(href=url_for(trim(valueArray[0])))
    if valueArray[1]
    i.fa-fw(class=trim(valueArray[1]))
    span=' '+label
    else
    .menus_item
    - const labelArray = label.split('||')
    - const hideClass = labelArray[2] && trim(labelArray[2]) === 'hide' ? 'hide' : ''
    a.site-page.group(class=`${hideClass}` href='javascript:void(0);')
    if labelArray[1]
    i.fa-fw(class=trim(labelArray[1]))
    span=' '+ trim(labelArray[0])
    i.fas.fa-chevron-down
    ul.menus_item_child
    each val,lab in value
    - const valArray = val.split('||')
    li
    a.site-page.child(href=url_for(trim(valArray[0])))
    if valArray[1]
    i.fa-fw(class=trim(valArray[1]))
    span=' '+ lab

文章统计图

具体步骤
  1. 安装模块

    1
    npm install cheerio --save
  2. 新建charts页面

    1
    hexo new page charts
  3. 新建 [BlogRoot]\theme\scripts\helpers\charts.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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
const cheerio = require('cheerio')
const moment = require('moment')

hexo.extend.filter.register('after_render:html', function (locals) {
const $ = cheerio.load(locals)
const post = $('#posts-chart')
const tag = $('#tags-chart')
const category = $('#categories-chart')
const htmlEncode = false

if (post.length > 0 || tag.length > 0 || category.length > 0) {
if (post.length > 0 && $('#postsChart').length === 0) {
if (post.attr('data-encode') === 'true') htmlEncode = true
post.after(postsChart(post.attr('data-start')))
}
if (tag.length > 0 && $('#tagsChart').length === 0) {
if (tag.attr('data-encode') === 'true') htmlEncode = true
tag.after(tagsChart(tag.attr('data-length')))
}
if (category.length > 0 && $('#categoriesChart').length === 0) {
if (category.attr('data-encode') === 'true') htmlEncode = true
category.after(categoriesChart(category.attr('data-parent')))
}

if (htmlEncode) {
return $.root().html().replace(/&amp;#/g, '&#')
} else {
return $.root().html()
}
} else {
return locals
}
}, 15)

function postsChart (startMonth) {
const startDate = moment(startMonth || '2020-01')
const endDate = moment()

const monthMap = new Map()
const dayTime = 3600 * 24 * 1000
for (let time = startDate; time <= endDate; time += dayTime) {
const month = moment(time).format('YYYY-MM')
if (!monthMap.has(month)) {
monthMap.set(month, 0)
}
}
hexo.locals.get('posts').forEach(function (post) {
const month = post.date.format('YYYY-MM')
if (monthMap.has(month)) {
monthMap.set(month, monthMap.get(month) + 1)
}
})
const monthArr = JSON.stringify([...monthMap.keys()])
const monthValueArr = JSON.stringify([...monthMap.values()])

return `
<script id="postsChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var postsChart = echarts.init(document.getElementById('posts-chart'), 'light');
var postsOption = {
title: {
text: '文章发布统计图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {
trigger: 'axis'
},
xAxis: {
name: '日期',
type: 'category',
boundaryGap: false,
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
},
data: ${monthArr}
},
yAxis: {
name: '文章篇数',
type: 'value',
nameTextStyle: {
color: color
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
}
},
series: [{
name: '文章篇数',
type: 'line',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
itemStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
areaStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
}, {
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
data: ${monthValueArr},
markLine: {
data: [{
name: '平均值',
type: 'average',
label: {
color: color
}
}]
}
}]
};
postsChart.setOption(postsOption);
window.addEventListener('resize', () => {
postsChart.resize();
});
postsChart.on('click', 'series', (event) => {
if (event.componentType === 'series') window.location.href = '/archives/' + event.name.replace('-', '/');
});
</script>`
}

function tagsChart (len) {
const tagArr = []
hexo.locals.get('tags').map(function (tag) {
tagArr.push({ name: tag.name, value: tag.length, path: tag.path })
})
tagArr.sort((a, b) => { return b.value - a.value })

const dataLength = Math.min(tagArr.length, len) || tagArr.length
const tagNameArr = []
for (let i = 0; i < dataLength; i++) {
tagNameArr.push(tagArr[i].name)
}
const tagNameArrJson = JSON.stringify(tagNameArr)
const tagArrJson = JSON.stringify(tagArr)

return `
<script id="tagsChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var tagsChart = echarts.init(document.getElementById('tags-chart'), 'light');
var tagsOption = {
title: {
text: 'Top ${dataLength} 标签统计图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {},
xAxis: {
name: '标签',
type: 'category',
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color,
interval: 0
},
axisLine: {
show: true,
lineStyle: {
color: color
}
},
data: ${tagNameArrJson}
},
yAxis: {
name: '文章篇数',
type: 'value',
splitLine: {
show: false
},
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
}
},
series: [{
name: '文章篇数',
type: 'bar',
data: ${tagArrJson},
itemStyle: {
borderRadius: [5, 5, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 195)'
},
{
offset: 1,
color: 'rgba(1, 211, 255)'
}])
}
},
markLine: {
data: [{
name: '平均值',
type: 'average',
label: {
color: color
}
}]
}
}]
};
tagsChart.setOption(tagsOption);
window.addEventListener('resize', () => {
tagsChart.resize();
});
tagsChart.on('click', 'series', (event) => {
if(event.data.path) window.location.href = '/' + event.data.path;
});
</script>`
}

function categoriesChart (dataParent) {
const categoryArr = []
let categoryParentFlag = false
hexo.locals.get('categories').map(function (category) {
if (category.parent) categoryParentFlag = true
categoryArr.push({
name: category.name,
value: category.length,
path: category.path,
id: category._id,
parentId: category.parent || '0'
})
})
categoryParentFlag = categoryParentFlag && dataParent === 'true'
categoryArr.sort((a, b) => { return b.value - a.value })
function translateListToTree (data, parent) {
let tree = []
let temp
data.forEach((item, index) => {
if (data[index].parentId == parent) {
let obj = data[index];
temp = translateListToTree(data, data[index].id);
if (temp.length > 0) {
obj.children = temp
}
if (tree.indexOf())
tree.push(obj)
}
})
return tree
}
const categoryNameJson = JSON.stringify(categoryArr.map(function (category) { return category.name }))
const categoryArrJson = JSON.stringify(categoryArr)
const categoryArrParentJson = JSON.stringify(translateListToTree(categoryArr, '0'))

return `
<script id="categoriesChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var categoriesChart = echarts.init(document.getElementById('categories-chart'), 'light');
var categoryParentFlag = ${categoryParentFlag}
var categoriesOption = {
title: {
text: '文章分类统计图',
x: 'center',
textStyle: {
color: color
}
},
legend: {
top: 'bottom',
data: ${categoryNameJson},
textStyle: {
color: color
}
},
tooltip: {
trigger: 'item'
},
series: []
};
categoriesOption.series.push(
categoryParentFlag ?
{
nodeClick :false,
name: '文章篇数',
type: 'sunburst',
radius: ['15%', '90%'],
center: ['50%', '55%'],
sort: 'desc',
data: ${categoryArrParentJson},
itemStyle: {
borderColor: '#fff',
borderWidth: 2,
emphasis: {
focus: 'ancestor',
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(255, 255, 255, 0.5)'
}
}
}
:
{
name: '文章篇数',
type: 'pie',
radius: [30, 80],
roseType: 'area',
label: {
color: color,
formatter: '{b} : {c} ({d}%)'
},
data: ${categoryArrJson},
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(255, 255, 255, 0.5)'
}
}
}
)
categoriesChart.setOption(categoriesOption);
window.addEventListener('resize', () => {
categoriesChart.resize();
});
categoriesChart.on('click', 'series', (event) => {
if(event.data.path) window.location.href = '/' + event.data.path;
});
</script>`
}

categories 页面添加统计图

注意:一定要注释掉 type: categories 如果想保留分类页面原功能,参考完整教程

1
2
3
4
5
6
7
8
9
---
title: 分类
date: 2025-07-31 15:59:03
# type: categories
---
<!-- 引入echarts.js -->
<script src="https://npm.elemecdn.com/echarts@4.9.0/dist/echarts.min.js"></script>
<!-- 文章分类统计图 -->
<div id="categories-chart" data-parent="true" style="height: 320px; padding: 10px;"></div>

archives 页面添加统计图

修改 [ThemeRoot]\layout\archive.pug,如下:

1
2
3
4
5
6
7
8
9
@@ -5,4 +5,8 @@
block content
include ./includes/mixins/article-sort.pug
#archive
+ <!-- 引入echarts.js -->
+ <script src="https://npm.elemecdn.com/echarts@4.9.0/dist/echarts.min.js"></script>
+ <!-- 文章发布时间统计图 -->
+ <div id="posts-chart" data-start="2024-09" style="border-radius: 8px; height: 320px; padding: 5px;"></div>
.article-sort-title= `${_p('page.articles')} - ${getArchiveLength()}`

pjax 适配

修改 [ThemeRoot]/layout/includes/third-party/pjax.pug 如下:

1
2
3
4
5
6
7
8
@@ -1,6 +1,6 @@
- var pjaxExclude = 'a:not([target="_blank"])'
if theme.pjax.exclude
each val in theme.pjax.exclude
- pjaxExclude += `:not([href="${val}"])`

- - let pjaxSelectors = ['head > title', '#config-diff', '#body-wrap', '#rightside-config-hide', '#rightside-config-show', '.js-pjax']
+ - let pjaxSelectors = ['head > title', '#config-diff', '#body-wrap', '#rightside-config-hide', '#rightside-config-show', '.js-pjax', '#posts-chart', '#categories-chart']

自定义右键

具体步骤

本教程将指导你如何为 Butterfly 主题添加一个仿 macOS 风格的自定义右键菜单,包含导航、随机文章、模式切换等实用功能。


效果预览

实现后的右键菜单将包含以下功能区域:

  1. 顶部导航:后退、前进、刷新、首页。
  2. 快捷入口:随便逛逛、博客分类、文章标签。
  3. 工具栏:深色模式切换、繁简切换、打印页面、复制链接。

  1. 在主题目录 [BlogRoot]/themes/butterfly/layout/includes/ 下新建一个名为 rightmenu.pug 的文件。

    文件路径: [BlogRoot]/themes/butterfly/layout/includes/rightmenu.pug

    文件内容:

    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
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    #rightMenu
    .rightMenu-group.rightMenu-small
    a.rightMenu-item(href="javascript:window.history.back();")
    i.fas.fa-arrow-left
    a.rightMenu-item(href="javascript:window.history.forward();")
    i.fas.fa-arrow-right
    a.rightMenu-item(href="javascript:window.location.reload();")
    i.fas.fa-redo
    a.rightMenu-item(href="/")
    i.fas.fa-home
    .rightMenu-group.rightMenu-line
    a.rightMenu-item(href="javascript:toRandomPost()")
    i.fas.fa-shoe-prints
    span 随便逛逛
    a.rightMenu-item(href="/categories/")
    i.fas.fa-shapes
    span 博客分类
    a.rightMenu-item(href="/tags/")
    i.fas.fa-tags
    span 文章标签
    .rightMenu-group.rightMenu-line
    a.rightMenu-item(href="javascript:rmf.switchDarkMode();")
    i.fas.fa-moon
    span 切换模式
    if theme.translate && theme.translate.enable
    a.rightMenu-item(href="javascript:rmf.translate();")
    i.fas.fa-language
    span 繁简切换
    a.rightMenu-item(href="javascript:rmf.printPage();")
    i.fas.fa-print
    span 打印页面
    a.rightMenu-item(href="javascript:rmf.copyUrl();")
    i.fas.fa-share-alt
    span 分享本页

    style.
    #rightMenu {
    display: none;
    position: fixed;
    width: 160px;
    background: var(--card-bg);
    border: 1px solid var(--btn-hover-color);
    border-radius: 8px;
    box-shadow: 0 5px 12px -4px rgba(0, 0, 0, 0.2);
    z-index: 9999;
    padding: 4px 0;
    transition: 0.2s;
    }
    #rightMenu .rightMenu-group {
    padding: 8px 12px;
    border-bottom: 1px solid var(--hr-border);
    }
    #rightMenu .rightMenu-group:last-child {
    border-bottom: none;
    }
    #rightMenu .rightMenu-group.rightMenu-small {
    display: flex;
    justify-content: space-between;
    padding: 8px 16px;
    }
    #rightMenu .rightMenu-group.rightMenu-small .rightMenu-item {
    width: 24px;
    height: 24px;
    line-height: 24px;
    text-align: center;
    border-radius: 4px;
    display: block;
    color: var(--font-color);
    }
    #rightMenu .rightMenu-group.rightMenu-small .rightMenu-item:hover {
    background-color: var(--text-bg-hover);
    color: var(--white);
    }
    #rightMenu .rightMenu-line .rightMenu-item {
    display: block;
    height: 36px;
    line-height: 36px;
    padding: 0 8px;
    border-radius: 4px;
    color: var(--font-color);
    transition: 0.3s;
    }
    #rightMenu .rightMenu-line .rightMenu-item:hover {
    background-color: var(--text-bg-hover);
    color: var(--white);
    }
    #rightMenu .rightMenu-line .rightMenu-item i {
    margin-right: 8px;
    width: 20px;
    text-align: center;
    }

    script.
    let rmf = {};
    rmf.showRightMenu = function(isTrue, x=0, y=0){
    let $rightMenu = document.getElementById('rightMenu');
    $rightMenu.style.display = isTrue ? 'block' : 'none';
    if(isTrue){
    $rightMenu.style.left = x + 'px';
    $rightMenu.style.top = y + 'px';
    }
    }
    rmf.switchDarkMode = function(){
    const nowMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'
    if (nowMode === 'light') {
    btf.activateDarkMode()
    saveToLocal.set('theme', 'dark', 2)
    GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
    } else {
    btf.activateLightMode()
    saveToLocal.set('theme', 'light', 2)
    GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
    }
    typeof utterancesTheme === 'function' && utterancesTheme()
    typeof changeGiscusTheme === 'function' && changeGiscusTheme()
    typeof FB === 'object' && FB.XFBML.parse()
    typeof runMermaid === 'function' && window.mermaid && runMermaid()
    }
    rmf.translate = function(){
    if(typeof translateFn !== 'undefined') translateFn.translatePage();
    }
    rmf.printPage = function(){
    window.print();
    }
    rmf.copyUrl = function(){
    const input = document.createElement('input');
    input.setAttribute('readonly', 'readonly');
    input.setAttribute('value', window.location.href);
    document.body.appendChild(input);
    input.select();
    if (document.execCommand('copy')) {
    btf.snackbarShow('复制成功');
    }
    document.body.removeChild(input);
    }

    window.oncontextmenu = function(event){
    if (event.ctrlKey) return true; // 按住 Ctrl + 右键 显示系统默认菜单
    event.preventDefault();
    let x = event.clientX + 10;
    let y = event.clientY;
    let menuWidth = 160;
    let menuHeight = 250;
    let clientWidth = document.documentElement.clientWidth;
    let clientHeight = document.documentElement.clientHeight;
    if(x + menuWidth > clientWidth){
    x = clientWidth - menuWidth - 10;
    }
    if(y + menuHeight > clientHeight){
    y = clientHeight - menuHeight - 10;
    }
    rmf.showRightMenu(true, x, y);
    return false;
    }

    window.addEventListener('click', function(){
    rmf.showRightMenu(false);
    });
  2. 修改主题的布局文件 [BlogRoot]themes/butterfly/layout/includes/layout.pug,将我们刚刚创建的 rightmenu.pug 引入进去。

    文件路径: [BlogRoot]/themes/butterfly/layout/includes/layout.pug

    在文件末尾,include ./additional-js.pug 的上方或下方,添加一行:

    1
    2
    3
    4
    5
    // ...
    include ./rightside.pug
    include ./rightmenu.pug <-- 添加这一行
    include ./additional-js.pug
    // ...

功能说明

  • Ctrl + 右键**:如果你需要使用浏览器默认的右键菜单(例如查看网页源代码),只需按住 Ctrl 键再点击右键即可。
  • 深色模式适配:代码中使用了 Butterfly 主题的 CSS 变量(如 var(--card-bg)),因此菜单会自动适配深色模式和浅色模式,无需额外修改。
  • 繁简切换:菜单中的繁简切换按钮会自动检测主题配置。如果你在 _config.yml 中关闭了 translate 功能,该按钮会自动隐藏。

自定义修改

如果你想添加更多的菜单项,可以直接编辑 rightmenu.pug 文件。例如,添加一个跳转到“关于我”的按钮:

1
2
3
a.rightMenu-item(href="/about/")
i.fas.fa-user
span 关于我

只需将这段代码插入到 rightMenu-group 中的合适位置即可。然后,重新生成并部署你的博客,即可看到全新的右键菜单效果!

自定义复制提示

具体步骤

在使用 Hexo Butterfly 主题时,默认的复制提示比较单调。本文记录了如何将复制提示修改为 “😊 复制成功!转载请注明出处哦~”,并解决代码块复制时的兼容性问题,以及统一普通文本复制时的提示效果。

  1. 首先,我们需要修改主题的语言文件,将默认的 “复制成功” 修改为我们喜欢的文案。

    编辑文件:[BlogRoot]/themes/butterfly/languages/zh-CN.yml

    1
    2
    3
    4
    copy:
    success: 😊 复制成功!转载请注明出处哦~ # 修改这里
    error: 复制失败
    noSupport: 浏览器不支持
  2. 为了实现更好的体验,我们需要修改 themes/butterfly/source/js/main.js 文件。主要解决两个问题:

  • 兼容性:部分浏览器不支持新的 Clipboard API,需要回退方案。

  • 统一体验:让普通文本复制(Ctrl+C)也能弹出漂亮的提示。

  • 修复Bug:防止代码块复制时同时触发两次提示(一次代码块逻辑,一次全局监听)。

  1. 找到 const copy = async (text, ctx) => {,将其替换为以下内容:

    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
    const copy = async (text, ctx) => {
    // 设置标志位,防止触发全局的 copy 事件监听,避免双重提示
    window.isCodeCopy = true
    try {
    // 优先使用现代 Clipboard API
    if (navigator.clipboard && navigator.clipboard.writeText) {
    try {
    await navigator.clipboard.writeText(text)
    alertInfo(ctx, GLOBAL_CONFIG.copy.success)
    return
    } catch (err) {
    console.error('Failed to copy via clipboard API: ', err)
    }
    }

    // 兼容性回退方案:创建一个隐藏的 textarea 进行复制
    try {
    const textArea = document.createElement('textarea')
    textArea.value = text
    // 确保 textarea 不可见但存在于 DOM 中
    textArea.style.position = 'fixed'
    textArea.style.left = '-9999px'
    textArea.style.top = '0'
    document.body.appendChild(textArea)
    textArea.focus()
    textArea.select()
    const successful = document.execCommand('copy')
    document.body.removeChild(textArea)
    if (successful) {
    alertInfo(ctx, GLOBAL_CONFIG.copy.success)
    } else {
    alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport)
    }
    } catch (err) {
    console.error('Fallback copy failed: ', err)
    alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport)
    }
    } finally {
    // 恢复标志位
    window.isCodeCopy = false
    }
    }
  2. 找到 const addCopyright = () => {,更新 handleCopy 逻辑:

    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
    const addCopyright = () => {
    const { limitCount, languages } = GLOBAL_CONFIG.copyright

    const handleCopy = e => {
    // 如果是代码块复制引起的 copy 事件,直接返回,不处理
    if (window.isCodeCopy) return

    e.preventDefault()
    const copyFont = window.getSelection(0).toString()
    let textFont = copyFont
    if (copyFont.length > limitCount) {
    textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}`
    }
    if (e.clipboardData) {
    e.clipboardData.setData('text', textFont)
    } else {
    window.clipboardData.setData('text', textFont)
    }

    // 显示复制成功的提示 (使用 Butterfly 内置的 Snackbar 或 fallback)
    if (GLOBAL_CONFIG.Snackbar !== undefined) {
    btf.snackbarShow(GLOBAL_CONFIG.copy.success)
    } else {
    // 如果没有开启 Snackbar,手动创建一个 toast
    const newEle = document.createElement('div')
    newEle.className = 'copy-notice'
    newEle.textContent = GLOBAL_CONFIG.copy.success
    newEle.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 9999; background: #49b1f5; color: #fff; padding: 10px 20px; border-radius: 4px;'
    document.body.appendChild(newEle)
    setTimeout(() => {
    newEle.remove()
    }, 2000)
    }
    }

    document.body.addEventListener('copy', handleCopy)
    }

至此已经实现,快去试试吧!

图表

具体步骤

准备工作

  • 使用 Butterfly 主题的 Hexo 博客。
  • 对 Pug 模板有基本了解。
  1. 找到文件 themes/butterfly/layout/includes/page/categories.pug。在文件顶部(现有列表之前)添加以下代码:

    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
    #categories-chart(style="height: 400px; padding: 10px; margin-bottom: 20px;")
    .category-lists!= list_categories()

    script(src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js")
    script.
    (function() {
    var initChart = function() {
    var chartDom = document.getElementById('categories-chart');
    if (!chartDom) return;

    var myChart = echarts.init(chartDom);
    // 将 Hexo 数据序列化为 JSON
    var data = !{JSON.stringify(site.categories.map(function(c){ return {name: c.name, value: c.length}; }).sort(function(a,b){ return b.value - a.value; }))};

    var option = {
    title: {
    text: '文章分类分析',
    left: 'center'
    },
    tooltip: {
    trigger: 'item',
    formatter: '{a} <br/>{b} : {c} ({d}%)'
    },
    legend: {
    bottom: 10,
    left: 'center',
    data: data.map(function(item){ return item.name; })
    },
    series: [
    {
    name: '分类',
    type: 'pie',
    radius: [30, 110],
    center: ['50%', '50%'],
    roseType: 'area',
    itemStyle: {
    borderRadius: 8
    },
    data: data
    }
    ]
    };

    myChart.setOption(option);

    window.addEventListener('resize', function() {
    myChart.resize();
    });
    };

    if (typeof echarts === 'undefined') {
    var script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js';
    script.onload = initChart;
    document.body.appendChild(script);
    } else {
    initChart();
    }
    })();
  2. 找到文件 themes/butterfly/layout/includes/page/tags.pug。在顶部添加以下代码:

    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
    #tags-chart(style="height: 400px; padding: 10px; margin-bottom: 20px;")
    .tag-cloud-list.text-center
    !=cloudTags({source: site.tags, orderby: page.orderby || 'random', order: page.order || 1, minfontsize: 1.2, maxfontsize: 1.5, limit: 0, unit: 'em', custom_colors: page.custom_colors})

    script(src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js")
    script.
    (function() {
    var initChart = function() {
    var chartDom = document.getElementById('tags-chart');
    if (!chartDom) return;

    var myChart = echarts.init(chartDom);
    var data = !{JSON.stringify(site.tags.map(function(c){ return {name: c.name, value: c.length}; }).sort(function(a,b){ return b.value - a.value; }))};
    // 限制显示前 20 个标签
    var topData = data.slice(0, 20);

    var option = {
    title: {
    text: '热门标签 Top 20',
    left: 'center'
    },
    tooltip: {
    trigger: 'axis',
    axisPointer: {
    type: 'shadow'
    }
    },
    grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
    },
    xAxis: {
    type: 'value'
    },
    yAxis: {
    type: 'category',
    data: topData.map(function(item){ return item.name; }).reverse()
    },
    series: [
    {
    name: '文章数',
    type: 'bar',
    data: topData.map(function(item){ return item.value; }).reverse(),
    itemStyle: {
    color: function(params) {
    var colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
    return colorList[params.dataIndex % colorList.length];
    }
    }
    }
    ]
    };

    myChart.setOption(option);

    window.addEventListener('resize', function() {
    myChart.resize();
    });
    };

    if (typeof echarts === 'undefined') {
    var script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js';
    script.onload = initChart;
    document.body.appendChild(script);
    } else {
    initChart();
    }
    })();

原理说明

  1. 数据注入:我们利用 Pug 的插值语法 !{JSON.stringify(...)} 将 Hexo 的 site.categoriessite.tags 数据直接转换为 HTML 中的 JavaScript 数组。这样就无需调用外部 API。
  2. 动态加载:脚本会检查 echarts 是否已加载。如果没有,它会从 CDN 动态加载该库。这确保了即使直接访问页面或通过 PJAX 跳转,图表也能正常工作。
  3. 响应式设计:添加了 resize 监听器,确保窗口大小改变时图表会自动调整。
  4. 图表类型
    • 分类:使用”南丁格尔玫瑰图”(Nightingale Rose Diagram)展示不同分类下的文章分布。
    • 标签:使用水平柱状图展示使用频率最高的前 20 个标签。

结语

只需几行代码,你就为博客添加了专业级的数据可视化功能!你可以参考 ECharts 官方文档 进一步自定义图表的选项(如颜色、字体、动画效果等)。