Hexo-Butterfly博客进阶:魔改教程
插件版
以下配置无需改主题配置文件。
压缩文件
具体步骤
安装minify插件。
1
npm install hexo-minify --save
修改配置,直接在
_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: {}
- 参考:
加载动画
具体步骤
添加
[BlogRoot]\source\csscustom-loader.css1
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;
}
}添加
[BlogRoot]\source\js\custom-loader.js1
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
5inject:
head:
- <link rel="stylesheet" href="/css/custom-loader.css">
bottom:
- <script async src="/js/custom-loader.js"></script>
评论
Twikoo评论
具体步骤
搭建
- 搭建,本博客的评论系统主要通过vercel部署。具体可参照Twikoo 文档
- 部署完成后修改
_config.butterfly.yml中的comment和twikoo配置项即可。可参考Butterfly 文檔(三) 主題配置 | Butterfly中的评论配置。- 第一次进入管理面板需设置管理员登录密码,在管理面板中可管理评论
美化
在
_config.butterfly.yml的 inject 中添加placeholder参数。1
2
3
4
5
6
7twikoo:
envId: https://twikoojs-cyan.vercel.app/
region:
# 使用Twikoo访客统计作为页面浏览量
visitor: false
option:
placeholder: '欢迎留言,支持Markdown语法,快来评论吧~'
娱乐页
具体步骤
追番、游戏页面
插件安装
1
npm install hexo-bilibili-bangumi --save
在Bangumi 番组计划中收藏番剧或者游戏。然后打开控制台(
f12),输入CHOBITS_UID获取用户ID.添加配置:在
_config.yml中添加,path、vimd等参数替换为自己的,具体配置可参考:hexo-bilibili-bangumi1
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页面美化:在
[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 ;
color: #f7f7fa ;
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 ;
border-radius: 10px;
column-gap: #f7f7fa ;
}
#page #article-container .bangumi-pagination a.bangumi-button {
border-bottom: none;
border-radius: 10px;
}
.bangumi-button:hover {
background: #425aef ;
border-radius: 10px ;
color: #f7f7fa ;
}
a.bangumi-button.bangumi-nextpage:hover {
text-decoration: none ;
}
.bangumi-button {
padding: 5px 10px ;
}
a.bangumi-tab {
padding: 5px 10px ;
}
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;
}参考
获取数据:命令行运行:
1
2hexo game -u;
hexo bangumi -u;
配置版
分类条
具体步骤
新建文件:
[BlogRoot]\themes\butterfly\layout\includes\categoryBar.pug1
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/'))= __('更多分类')新建文件
[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)修改
[BlogRoot]\themes\butterfly\layout\category.pug,添加其中两行代码,去掉加号即为正常缩进:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15extends 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打开文件
[BlogRoot]\themes\butterfly\layout\index.pug文件,添加下面两行:1
2
3
4
5
6
7
8
9extends 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打开
[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
22const 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 = () => {然后再在引用部分执行这个函数,在同一个文件,找到下面的函数并添加函数的调用,位置看下方注释:
1
2
3
4
5
6
7GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
scrollFn()
// ========== 新增:调用分类栏激活态切换函数 ==========
setCategoryBarActive()
forPostFn()
参考
以下与分类条的改动无关,为我个人修改,以防忘记,特记录下来
首页文章列表 (indexPostUI.pug)
文件路径: themes/butterfly/layout/includes/mixins/indexPostUI.pug
目的是为了在文章列表容器 #recent-posts 的最开始插入分类条代码。
1 | //- 原始代码 (Before) |
分类归档页面 (category.pug)
文件路径: themes/butterfly/layout/category.pug
为了让分类页面的布局不崩坏(保持 74% 宽度),并显示分类条,重构 else 分支。
1 | //- 原始代码 (Before) |
外挂标签
具体步骤
note标签
修改
[BlogRoot]\themes\liushen\scripts\tag\note.js1
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/
*/
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 });美化标签样式
[BlogRoot]\themes\liushen\source\css\_tags\note.styl1
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标签
新建文件
[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
81function 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 });修改样式
[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
display flex
border 1px solid var(--tag-link-border-color)
flex-direction column
padding 0.5rem 1rem
margin-top 1rem
text-decoration none
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
- 参考文章
info标签
1
2
3{% note info 这是标题 %}
这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
{% endnote %}- 效果
这是标题这个是内容展示,里面可以正常使用
Markdown的相关渲染格式warning标签
1
2
3{% note warning 这是标题 %}
这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
{% endnote %}- 效果
这是标题这个是内容展示,里面可以正常使用
Markdown的相关渲染格式question标签
1
2
3{% note question 这是标题 %}
这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
{% endnote %}- 效果
这是标题这个是内容展示,里面可以正常使用
Markdown的相关渲染格式error标签
1
2
3{% note error 这是标题 %}
这个是内容展示,里面可以正常使用`Markdown`的相关渲染格式
{% endnote %}- 效果
这是标题这个是内容展示,里面可以正常使用
Markdown的相关渲染格式
侧边栏
来访者
具体步骤
新建
[BlogRoot]\themes\liushen\layout\includes\widget\card_welcome.pug1
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   👋🏻我是Kaitumei,一个
span(style="font-weight: bold; color: #3498db") 热爱记录、喜欢分享
| 的小卡拉米。😊
p   希望在这里你能探寻到你心中的那座
span(style="font-weight: bold; color: #7a3debff") 岛屿🗻。
br
|   疲惫时,你可以点击左下方的神秘小物件,然后放松放松哦~
| 如果遇到什么问题请通过
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;") 请刷新重试呀🤗~同目录底下index.pug
1
2
3
4
5
6
7
8
9
10else
//- 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新建
[BlogRoot]\themes\liushen\source\css\_layout\card-welcome.styl1
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在任意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);
};参考
导航栏美化
具体步骤
[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
37nav#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添加自定义 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
50document.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;
}
}
}添加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 ;
}打开
[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
29if 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
npm install cheerio --save
新建charts页面
1
hexo new page charts
新建
[BlogRoot]\theme\scripts\helpers\charts.js文件,然后添加以下代码:
1 | const cheerio = require('cheerio') |
categories 页面添加统计图
注意:一定要注释掉 type: categories 如果想保留分类页面原功能,参考完整教程
1 | --- |
archives 页面添加统计图
修改 [ThemeRoot]\layout\archive.pug,如下:
1 | @@ -5,4 +5,8 @@ |
pjax 适配
修改 [ThemeRoot]/layout/includes/third-party/pjax.pug 如下:
1 | @@ -1,6 +1,6 @@ |
自定义右键
具体步骤
本教程将指导你如何为 Butterfly 主题添加一个仿 macOS 风格的自定义右键菜单,包含导航、随机文章、模式切换等实用功能。
效果预览
实现后的右键菜单将包含以下功能区域:
- 顶部导航:后退、前进、刷新、首页。
- 快捷入口:随便逛逛、博客分类、文章标签。
- 工具栏:深色模式切换、繁简切换、打印页面、复制链接。
在主题目录
[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);
});修改主题的布局文件
[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 | a.rightMenu-item(href="/about/") |
只需将这段代码插入到 rightMenu-group 中的合适位置即可。然后,重新生成并部署你的博客,即可看到全新的右键菜单效果!
自定义复制提示
具体步骤
在使用 Hexo Butterfly 主题时,默认的复制提示比较单调。本文记录了如何将复制提示修改为 “😊 复制成功!转载请注明出处哦~”,并解决代码块复制时的兼容性问题,以及统一普通文本复制时的提示效果。
首先,我们需要修改主题的语言文件,将默认的 “复制成功” 修改为我们喜欢的文案。
编辑文件:
[BlogRoot]/themes/butterfly/languages/zh-CN.yml1
2
3
4copy:
success: 😊 复制成功!转载请注明出处哦~ # 修改这里
error: 复制失败
noSupport: 浏览器不支持为了实现更好的体验,我们需要修改
themes/butterfly/source/js/main.js文件。主要解决两个问题:
兼容性:部分浏览器不支持新的 Clipboard API,需要回退方案。
统一体验:让普通文本复制(Ctrl+C)也能弹出漂亮的提示。
修复Bug:防止代码块复制时同时触发两次提示(一次代码块逻辑,一次全局监听)。
找到
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
42const 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
}
}找到
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
37const 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 模板有基本了解。
找到文件
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();
}
})();找到文件
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();
}
})();
原理说明
- 数据注入:我们利用 Pug 的插值语法
!{JSON.stringify(...)}将 Hexo 的site.categories和site.tags数据直接转换为 HTML 中的 JavaScript 数组。这样就无需调用外部 API。 - 动态加载:脚本会检查
echarts是否已加载。如果没有,它会从 CDN 动态加载该库。这确保了即使直接访问页面或通过 PJAX 跳转,图表也能正常工作。 - 响应式设计:添加了 resize 监听器,确保窗口大小改变时图表会自动调整。
- 图表类型:
- 分类:使用”南丁格尔玫瑰图”(Nightingale Rose Diagram)展示不同分类下的文章分布。
- 标签:使用水平柱状图展示使用频率最高的前 20 个标签。
结语
只需几行代码,你就为博客添加了专业级的数据可视化功能!你可以参考 ECharts 官方文档 进一步自定义图表的选项(如颜色、字体、动画效果等)。






