📚 更多文档目录

🚀 搭建教程 | 1 - 📑 前置教程 | 2 - 🎈 主题调整 | 3 - ✨ 魔改教程 | 4 - 🐈 重构自用数据记录


本篇教程基于 Hexo 6.3.0 & Butterfly 4.9.0 为博主的魔改教程记录,以防自己日后因魔改迷失所记录 📝

本小节魔改内容不包括 顶部banner栏,如有需要请移步至 ✨ 第二章 - 三小节 | 魔改页前置

240410 更新:调整Bar配置问题,修复多个相册页面造成的问题

240112 更新:新增Bar条切换相册分类;修复Bar栏重复点击的问题

如果没有服务器可以搭建memos,可以使用iCat自用的memos服务

效果预览

创建数据

  • 新建 [blogRoot]/themes/butterfly/layout/includes/page/photo.pug 页面,并新增以下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if page.bar
.icat-status-bar
.status-bar-tips= page.bar.tips || '类别'
.status-bar
#bar-box
each item in page.bar.list
- const content = item.split(' || ')
.status-bar-item
a(onclick="photos('" + content[0] + "')") #{content[1]}
#status-bar-button(onclick="statusbar()")
i.iconfont.icat-chevron-too
if page.bar.more
- const contents = page.bar.more.split(' || ')
if contents[0].startsWith('/')
a.status-bar-more(href="javascript:void(0)" onclick="pjax.loadUrl('" + contents[0] + "')") #{contents[1]}
else
a.status-bar-more(href=contents[0]) #{contents[1]}
div.gallery-photos.page
img(src="https://img.meuicat.com/blog/loading.svg" style="margin:auto")
  • 修改 [blogRoot]/themes/butterfly/layout/page.pug 来使页面匹配
    + 号直接删除 即是正常缩进)
1
2
3
4
5
6
      when 'categories'
include includes/page/categories.pug
+ when 'photo'
+ include includes/page/photo.pug
default
include includes/page/default-page.pug
  • 新建 [blogRoot]/themes/butterfly/source/css/_page/photo.styl 样式文件,并新增以下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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
.icat-status-bar
margin: 16px 0
display: flex
white-space: nowrap
align-items: center
background: var(--icat-card-bg)
border-radius: 8px
padding: 0 12px
border: var(--style-border)
transition: .6s
box-shadow: var(--icat-shadow-border)

&:hover
border-color: var(--icat-blue)
box-shadow: var(--icat-shadow-blue)

.status-bar
padding: 0.4rem 0 0.4rem 0.4rem
white-space: nowrap
overflow: hidden
transition: .3s
width: 100%
justify-content: space-between
user-select: none
display: flex
align-items: center
font-size: 15px

#bar-box
white-space: nowrap
overflow-x: scroll
overflow-y: hidden
display: flex
border-radius: 8px
align-items: center
height: 30px

.status-bar-item
a
padding: 0.1rem 0.5rem
margin-right: 10px
font-weight: 700
border-radius: 8px
display: flex
align-items: center
height: 30px
color: var(--icat-fontcolor)
opacity: .8
transition: .6s

&:hover
background: var(--icat-blue)
opacity: 1
color: var(--icat-white)

&.selected
a
background: var(--icat-blue) !important
opacity: 1 !important
color: var(--icat-white) !important

#status-bar-button
margin-left: 12px
cursor: pointer
height: 22px
display: flex
align-items: center
transition: .6s

&:hover
color: var(--icat-blue)

.status-bar-more
margin-left: 12px
font-weight: 400
color: var(--icat-fontcolor)
opacity: .8
transition: .6s

&:hover
color: var(--icat-blue)

#bar-box::-webkit-scrollbar
display: none

.gallery-photos
width: 100%
text-align: center
animation: slide-in .6s .4s backwards

.gallery-photo
min-height: 5rem
width: 24.99%
padding: 4px
position: relative
animation: slide-in 0.6s 0.4s backwards

+maxWidth1024()
width: 33.3%

+maxWidth768()
width: 49.9%
padding: 3px

+minWidth2000()
width: 20%

&:hover
img
transform: scale(1.1)

a
border-radius: 8px
border: var(--style-border)
box-shadow: var(--icat-shadow-border)
display: block
overflow: hidden
transition: .6s

&:hover
border-color: var(--icat-blue)
box-shadow: var(--icat-shadow-blue)

img
display: block
width: 100%
animation: fadeIn 1s
cursor: pointer
transition: all .4s ease-in-out

.photo-title,
.photo-time
max-width: calc(100% - 7px)
line-height: 1.8
position: absolute
left: 4px
font-size: 14px
background: rgba(0,0,0,0.3)
padding: 0px 8px
color: #fff
animation: fadeIn 1s

.photo-title
bottom:4px
border-radius: 0 8px 0 8px

+maxWidth768()
font-size: 12px
left: 3px
bottom: 3px

.photo-time
top:4px
border-radius: 8px 0 8px 0
可选CSS样式
  • 新建 [blogRoot]/source/css/photo.css 样式文件,并新增以下内容
    (也可以在自建的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
.icat-status-bar {
margin: 16px 0;
display: flex;
white-space: nowrap;
align-items: center;
background: var(--icat-card-bg);
border-radius: 8px;
padding: 0 12px;
border: var(--style-border);
transition: 0.6s;
box-shadow: var(--icat-shadow-border);
}
.icat-status-bar:hover {
border-color: var(--icat-blue);
box-shadow: var(--icat-shadow-blue);
}
.icat-status-bar .status-bar {
padding: 0.4rem 0 0.4rem 0.4rem;
white-space: nowrap;
overflow: hidden;
transition: 0.3s;
width: 100%;
justify-content: space-between;
user-select: none;
display: flex;
align-items: center;
font-size: 15px;
}
.icat-status-bar .status-bar #bar-box {
white-space: nowrap;
overflow-x: scroll;
overflow-y: hidden;
display: flex;
border-radius: 8px;
align-items: center;
height: 30px;
}
.icat-status-bar .status-bar #bar-box .status-bar-item a {
padding: 0.1rem 0.5rem;
margin-right: 10px;
font-weight: 700;
border-radius: 8px;
display: flex;
align-items: center;
height: 30px;
color: var(--icat-fontcolor);
opacity: 0.8;
transition: 0.6s;
}
.icat-status-bar .status-bar #bar-box .status-bar-item a:hover {
background: var(--icat-blue);
opacity: 1;
-ms-filter: none;
filter: none;
color: var(--icat-white);
}
.icat-status-bar .status-bar #bar-box .status-bar-item.selected a {
background: var(--icat-blue) !important;
opacity: 1 !important;
-ms-filter: none !important;
filter: none !important;
color: var(--icat-white) !important;
}
.icat-status-bar #status-bar-button {
margin-left: 12px;
cursor: pointer;
height: 22px;
display: flex;
align-items: center;
transition: 0.6s;
}
.icat-status-bar #status-bar-button:hover {
color: var(--icat-blue);
}
.icat-status-bar .status-bar-more {
margin-left: 12px;
font-weight: 400;
color: var(--icat-fontcolor);
opacity: 0.8;
transition: 0.6s;
}
.icat-status-bar .status-bar-more:hover {
color: var(--icat-blue);
}
#bar-box::-webkit-scrollbar {
display: none;
}

/* Bar栏样式 */

.gallery-photos {
width:100%;
margin-top:16px;
text-align: center;
}
.gallery-photo {
min-height:5rem;
width:24.99%;
padding:4px;
position:relative;
}
.gallery-photo a {
border-radius:8px;
border: var(--style-border-always);
box-shadow: var(--icat-shadow-border);
display:block;
overflow:hidden;
}
.gallery-photo img {
display:block;
width:100%;
animation:fadeIn 1s;
cursor:pointer;
transition:all .4s ease-in-out !important;
}
.gallery-photo span.photo-title,.gallery-photo span.photo-time {
max-width:calc(100% - 7px);
line-height:1.8;
position:absolute;
left:4px;
font-size:14px;
background:rgba(0,0,0,0.3);
padding:0px 8px;
color:#fff;
animation:fadeIn 1s;
}
.gallery-photo span.photo-title {
bottom:4px;
border-radius:0 8px 0 8px;
}
.gallery-photo span.photo-time {
top:4px;
border-radius:8px 0 8px 0;
}
.gallery-photo:hover img {
transform:scale(1.1);
}
@media screen and (max-width:1100px) {
.gallery-photo {
width:33.3%;
}
}@media screen and (max-width:768px) {
.gallery-photo {
width:49.9%;
padding:3px
}
/* .gallery-photo span.photo-time {
display:none
}
手机端隐藏时间显示 */
.gallery-photo span.photo-title {
font-size:12px
}
.gallery-photo span.photo-title {
left:3px;
bottom:3px;
}
@keyframes fadeIn {
0% {
opacity:0;
}
100% {
opacity:1;
}
}

/* Memos动态相册样式 */
  • _config.butterfly.yml 主题配置文件中 inject 下的 head 引入 photo.css
1
2
3
4
5
6
7
8
9
  ···

inject:
head:
- <link rel="stylesheet" href="/css/photo.css"> # 动态相册样式
bottom:
- ···

···
  • 创建 [blogRoot]/source/js/memos/photo.js 文件,并新增以下内容,用来处理Memos动态相册的函数
    (或写在自建的公共 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
function whenDOMReady() {
if (location.pathname == '/photos/') photos('相册'); // 首次进入需要刷新展示的相评分类
}
whenDOMReady()
document.addEventListener("pjax:complete", whenDOMReady)

// 适配pjax

window.onresize = () => {
if (location.pathname == '/photos/') waterfall('.gallery-photos');
};

// 自适应

function photos(tag) {
let apiUrl = `你的memos地址/api/v1/memo?creatorId=用户UID&tag=${tag}`; // 修改Memos API地址以及 UID

fetch(apiUrl).then(res => res.json()).then(data => {
let html = '',
imgs = []
data.forEach(item => {
let ls = item.content.match(/\!\[.*?\]\(.*?\)/g)
if (ls) imgs = imgs.concat(ls)
if (item.resourceList.length) {
item.resourceList.forEach(t => {
if (t.externalLink) imgs.push(`![](${t.externalLink})`)
else imgs.push(`![](${url}/o/r/${t.id}/${t.publicId}/${t.filename})`)
})
}
})

if (imgs) imgs.forEach(item => {
let img = item.replace(/!\[.*?\]\((.*?)\)/g, '$1'),
time, title, tat = item.replace(/!\[(.*?)\]\(.*?\)/g, '$1')
if (tat.indexOf(' ') != -1) {
time = tat.split(' ')[0]
title = tat.split(' ')[1]
} else title = tat

html += `<div class="gallery-photo"><a href="${img}" data-fancybox="gallery" class="fancybox" data-thumb="${img}"><img class="no-lazyload photo-img" loading='lazy' decoding="async" src="${img}"></a>`
title ? html += `<span class="photo-title">${title}</span>` : ''
time ? html += `<span class="photo-time">${time}</span>` : ''
html += `</div>`
})

document.querySelector('.gallery-photos.page').innerHTML = html
imgStatus.watch('.photo-img', () => { waterfall('.gallery-photos') })
window.Lately && Lately.init({ target: '.photo-time' })
}).catch()

if (document.querySelector(".icat-status-bar")) {
var statusBarItemItems = document.querySelectorAll('.status-bar-item');
let firstElement = statusBarItemItems[1];
firstElement.classList.add('selected');

Array.from(statusBarItemItems).forEach(function (element) {
element.onclick = function (event) {
var selectedElements = document.querySelectorAll('.status-bar-item.selected');
Array.from(selectedElements).forEach(function (selectedElement) {
selectedElement.classList.remove('selected');
});
element.classList.add('selected');

event.stopPropagation();
event.preventDefault();
return false;
};
});
}
}

// 相册页处理函数

function statusbar() {
var e;
var t = document.getElementById("bar-box");
var o = document.getElementById("status-bar-button");
var n = t.clientWidth;
if (t) {
if (t.scrollLeft + t.clientWidth >= t.scrollWidth - 8) {
t.scroll({ left: 0, behavior: "smooth" });
} else {
t.scrollBy({ left: n, behavior: "smooth" });
}
t.addEventListener("scroll", function n() {
clearTimeout(e);
e = setTimeout(function () {
o.style.transform =
t.scrollLeft + t.clientWidth >= t.scrollWidth - 8 ? "rotate(180deg)" : "";
t.removeEventListener("scroll", n);
}, 150);
});
}
};
// Bar滚动处理

memos api地址格式如下所示:
https://memos地址/api/v1/memo?creatorId=用户UID&tag=标签名

memos地址就是首页地址,如:memos.meuicat.com

Memos 0.20.1以下版本UID的获取方式:

  • 点击个人头像,然后点击 RSS

  • 根据浏览器链接获取ID

如url是:https://memos.meuicat.com/u/1/rss.xml
则creatorId就是1
最后完整链接如下:
https://memos.meuicat.com/api/v1/memo?creatorId=1&tag=相册
能看到数据则为正确链接

Memos 0.21.0版本UID获取方式:

  • 点击设置 - 我的账号 - 编辑

  • 用户名上显示的数字即是你的UID

  • 创建 [blogRoot]/source/js/memos/imgStatus.min.js 文件,并新增以下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
! function() {
this.loaded = 0, this.failed = 0, this.total = 0, this.watch = function(a, b) {
var c = document.querySelectorAll(a);
if (!c.length) return console.log("[imgStatus]: There aren't any images associated with this selector (" + a + ")!");
this.total = c.length;
for (var d = 0; d < this.total; d++) isCached(c[d].src) ? this._setLoaded(b) : c[d].addEventListener ? (c[d].addEventListener("load", this._setLoaded.bind(this, b)), c[d].addEventListener("error", this._setFailed.bind(this, b))) : (c[d].attachEvent("onload", this._setLoaded.bind(this, b)), c[d].attachEvent("onerror", this._setFailed.bind(this, b)))
}, this.isCached = function(a) {
var b = new Image;
return b.src = a, b.complete
}, this._setFailed = function(a, b) {
++this.failed, "function" == typeof a && a(this)
}, this._setLoaded = function(a, b) {
++this.loaded, "function" == typeof a && a(this)
}, this.isDone = function() {
return this.loaded + this.failed === this.total ? !0 : !1
}, "object" == typeof window && (window.imgStatus = this)
}();

// imgStatus.min.js
  • 创建 [blogRoot]/source/js/memos/lately.min.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
! function() {
window.Lately = new function() {
var t = this;
this.lang = {
second: "秒",
minute: "分钟",
hour: "小时",
day: "天",
month: "个月",
year: "年",
ago: "前",
error: "NaN"
};
var e = function(e) {
e = new Date(n(e));
var r = new function() {
this.second = (Date.now() - e.getTime()) / 1e3, this.minute = this.second / 60, this.hour = this.minute / 60, this.day = this.hour / 24, this.month = this.day / 30, this.year = this.month / 12
}, i = Object.keys(r).reverse().find(function(t) {
return r[t] >= 1
});
return (i ? function(t, e) {
return Math.floor(t) + e
}(r[i], t.lang[i]) : t.lang.error) + t.lang.ago
}, n = function(t) {
return t = new Date(t && ("number" == typeof t ? t : t.replace(/-/g, "/").replace("T", " "))), !isNaN(t.getTime()) && t.getTime()
};
return {
init: function() {
var r = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, i = r.target,
a = void 0 === i ? "time" : i,
o = r.lang;
o && (t.lang = o);
var u = !0,
h = !1,
l = void 0;
try {
for (var s, c = document.querySelectorAll(a)[Symbol.iterator](); !(u = (s = c.next()).done); u = !0) {
var f = s.value,
g = n(f.dateTime) || n(f.title) || n(f.innerHTML) || 0;
if (!g) return;
f.title = new Date(g).toLocaleString(), f.innerHTML = e(g)
}
} catch (t) {
h = !0, l = t
} finally {
try {
!u && c.
return &&c.
return ()
} finally {
if (h) throw l
}
}
},
format: e
}
}
}();

/*
* Lately.min.js 2.5.2
* MIT License - http://www.opensource.org/licenses/mit-license.php
* https://tokinx.github.io/lately/
*/
  • 创建 [blogRoot]/source/js/memos/waterfall.min.js 文件,并新增以下内容,用来处理Memos动态相册的瀑布流
    (在上几节的即刻短文教程里添加了瀑布流的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
function waterfall(a) {
function b(a, b) {
var c = window.getComputedStyle(b);
return parseFloat(c["margin" + a]) || 0
}

function c(a) {
return a + "px"
}

function d(a) {
return parseFloat(a.style.top)
}

function e(a) {
return parseFloat(a.style.left)
}

function f(a) {
return a.clientWidth
}

function g(a) {
return a.clientHeight
}

function h(a) {
return d(a) + g(a) + b("Bottom", a)
}

function i(a) {
return e(a) + f(a) + b("Right", a)
}

function j(a) {
a = a.sort(function(a, b) {
return h(a) === h(b) ? e(b) - e(a) : h(b) - h(a)
})
}

function k(b) {
f(a) != t && (b.target.removeEventListener(b.type, arguments.callee), waterfall(a))
}
"string" == typeof a && (a = document.querySelector(a));
var l = [].map.call(a.children, function(a) {
return a.style.position = "absolute", a
});
a.style.position = "relative";
var m = [];
l.length && (l[0].style.top = "0px", l[0].style.left = c(b("Left", l[0])), m.push(l[0]));
for (var n = 1; n < l.length; n++) {
var o = l[n - 1],
p = l[n],
q = i(o) + f(p) <= f(a);
if (!q) break;
p.style.top = o.style.top, p.style.left = c(i(o) + b("Left", p)), m.push(p)
}
for (; n < l.length; n++) {
j(m);
var p = l[n],
r = m.pop();
p.style.top = c(h(r) + b("Top", p)), p.style.left = c(e(r)), m.push(p)
}
j(m);
var s = m[0];
a.style.height = c(h(s) + b("Bottom", s));
var t = f(a);
window.addEventListener ? window.addEventListener("resize", k) : document.body.onresize = k
}

// 瀑布流处理
  • _config.butterfly.yml 主题配置文件中 inject 下的 headbottom 分别引入 waterfall.min.js lately.min.js imgStatus.min.js photo.js
1
2
3
4
5
6
7
8
9
10
11
12
  ···

inject:
head:
- ···
bottom:
- <script type="text/javascript" src="/js/memos/waterfall.min.js"></script> # memos动态相册 - waterfall
- <script type="text/javascript" src="/js/memos/imgStatus.min.js"></script> # memos动态相册 - imgStatus
- <script type="text/javascript" src="/js/memos/lately.min.js"></script> # memos动态相册 - lately
- <script type="text/javascript" src="/js/memos/photo.js"></script> # memos动态相册 - 格式链JS

···
  • 创建并修改 [blogRoot]/source/photos/index.md 页面
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
---
title: 生活相册
date: 2023-03-14 21:47:47
type: photo
top_img: false
aside: false
top_page: true
top_bg: https://img.meuicat.com/banner
top_item: 相册
top_title: 快门の色彩瞬间
top_tips: 活在当下 热烈且自由
comments: false
bar:
tips: 年份类别
list:
- 相册 || 全部
- 2023 || 2023
- 2022 || 2022
- 2021 || 2021
- 2020 || 2020
- 2019 || 2019
- 2018 || 2018
- 2017 || 2017
more: /album/ || 影集
---

<!-- 页面内容 -->
参数类型释义
bar可选是否使用Bar分类栏
bar.tips可选Bar分类栏的提示文字
bar.list必选Bar分类栏的tag和显示文字,第一个参数为tag;第二个参数为需要显示的文字
bar.more可选Bar分类栏更多按钮的跳转链接和显示文字,第一个参数为跳转链接地址(支持/album/https://meuicat.com这种格式,不支持不带头标的meuicat.com格式;第二个参数为需要显示的文字

使用参数

1
2
3
4
5
6
7
8
9
#相册
<!-- 写法就是markdown的写法,中括号里先写时间再写标题,中间使用空格隔开 -->
![2023-01-29 我是标题](图片链接)
<!-- 若不想要时间只写标题即可 -->
![我是标题](图片链接)
<!-- 若不想要标题只写时间即可,只不过后面需要添加空格 -->
![2023-01-29 ](图片链接)
<!-- 也可以只填写图片链接 -->
![](图片链接)
  • memos 内的写法
1
2
3
4
5
#相册
![2023-02-09 ](https://s11.ax1x.com/2023/03/16/pp3jQ3V.jpg)
![ 犹豫没要微信](https://s11.ax1x.com/2023/03/17/ppGlZid.jpg)
![2022-10-12 可可爱爱没有脑袋](https://s11.ax1x.com/2023/03/16/pp34dpj.jpg)
![](https://s11.ax1x.com/2023/03/15/pp39iRI.jpg)

魔改适配

已适配Solitude主题,具体魔改教程可前往下方文章查看