事情的一切起因都源于我懒于更新。
眼看着关注的朋友们发出一篇又一篇好看好玩的博客,每次点开都一边感叹“大家的生活和想法真有趣”,一边为自己落灰长草的站点愧疚0.1秒钟。我也想更新!但我的日常生活千篇一律,有点想说的话都在象上嘟嘟完了,连写文都全是半路弃坑的草稿,要发点什么才好呢?
我很快想到我的小铁块们,一入徽章深似海,从此闲钱是路人。我很喜欢这些闪闪发光的小东西,如果可以把它们放在我的博客做展览就再好不过了。这个想法其实很早之前就存在,只是博客大多都由单篇md文件组成,要想做可以持续更新的展示似乎很麻烦,何况我根本不会写代码,所以也就只是偶尔想一想,从来不奢望能够付诸实践。
正好时隔几个月又去翻别人家的新奇作业,很快发现一个接近我想法的新东西:好物页面。
这不就是我想要的那种橱窗展柜吗!在看到它的第一眼,我就觉得这个形式绝不仅仅能做好物分享,它可以被应用到很多很多需要记录展示的地方去,同时整个页面的数据都可以存在一份简单的json文件里,更新只需要按部就班地添加短短几行。(写这篇博客的时候已经看到了它的拓展版本:Shortcode汇总)
唯一美中不足的就是一张卡片只能对应一张图,如果能够插入多张图片,可以实现图片轮播不就更完美了?要我自己空写一个模板肯定不可能,但现下左有前人的基础,右有ChatGPT辅助,我还是有胆子试一试的。
模板在文章最后一部分,作为示例,这是我的小铁块赛博展柜,为了方便以后更新,我的徽章是按时间顺序倒序排列的,建议第一次从最底部开始看。你也可以在我的“关于”页面找到掌中方块世界一键直达。
由于图床和JS库引用等等各种各样的问题,图片第一次加载的时候大概有百分之百的概率遇到加载略慢、图片飞出卡片等状况,这个时候只需要等图片加载得差不多再刷新一次就可以恢复正常(意思是,我不知道问题在哪里我也不会修,请大家凑合看)。
从最零碎的知识点开始补课
要想创建一个这样的新界面,步骤就和把大象关进冰箱一样简单:
1
2
3
|
创建页面模板:HTML文件
新建Hugo页面:MD文件
新建数据文件:JSON文件
|
步骤其实和创建About界面的标签墙也差不多嘛?在md文件里写好title
与layout
,另外在satic
目录下新建一个文件夹来放json文件和图片……等等,我不想要导航栏再多出一项来怎么办?
问chatgpt怎么隐藏页面链接……结果只要删去多余的部分就可以了。
1
2
3
4
|
---
title: 小铁块赛博展柜
layout: "badges"
---
|
要义在于有效沟通!
做好前置工作,接下来就轮到了重头戏:HTML模板和CSS样式。我的想法也很简单,既然这个模板已经允许我添加图片,那我只需要针对图片这一部分修改不就可以了?先前也抄过塔塔图片轮播短代码的作业,移花接木应该不是难事吧?
事实证明我根本看不懂该从哪里下手。我先是尝试让chatGPT单独修改HTML代码添加图片轮播功能,但它给出的结果无一例外地导致网站在生成时就频繁报错;我又将图片轮播短代码粘贴给它,希望它能借鉴一二——3.5的chatGPT很显然还没有这么智能,只会生搬硬套,我的计划再次以失败告终。
抓着报错点反反复复跟chatGPT核对,我才意识到“好物”模板对我的局限性有多大:一来我看不明白它的图片渲染到底要怎么修改,二来我所需求的json文件应当将复数图片以数据组的模式书写,在HTML里也要更换读取数据的函数和命令。
我不得不换了个思路,彻底抛弃了抄来的代码,只跟着页面数据关联json文件这个思路走,其余的一切都交给chatGPT完成。在询问时我也有意加强了引导的意味,情况开始有所好转了。
我先让chatGPT确保能够正确加载json文件的数据并渲染出卡片内容,成功生效的代码如下:
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
|
{{ define "main" }}
{{ $cards := getJSON "static/cards/cards.json" }}
<div id="card-container"></div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var cardContainer = document.getElementById("card-container");
$.getJSON('/cards/cards.json', function(data) {
var cardContainer = $('#card-container');
var cardTemplate = $('#card-template');
$.each(data, function(index, card) {
var cardHtml = `
<div class="card">
<div class="card-images">
${card.images.map(image => `<img src="${image}" class="card-image" alt="Image">`).join('')}
</div>
<h2>${card.title}</h2>
<h3>${card.subtitle}</h3>
<p class="description">${card.description}</p>
</div>
`;
cardContainer.append(cardHtml);
});
});
});
</script>
{{ end }}
|
这里我并没有更改元素的名称,所以它与我最后确定的模板还有些许出入。
下一步就是添加图片轮播功能了,根据我的失败经历,需要在每张卡片内部嵌套一个轮播容器、需要初始化主轮播容器、需要为每个卡片都创建一个独立的Swiper实例……嗯,好吧,其实我也没太听明白chatGPT在修改些什么,但反正在一次次的报错之后,我终于得到了这个能够成功生效的代码:
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
|
{{ define "main" }}
{{ $cards := getJSON "static/cards/cards.json" }}
<div id="card-container"></div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var cardContainer = document.getElementById("card-container");
$.getJSON('/cards/cards.json', function(data) {
var cardContainer = $('#card-container');
var cardTemplate = $('#card-template');
$.each(data, function(index, card) {
var cardHtml = `
<div class="card">
<div class="card-images">
<div class="swiper-container swiper-${index}">
<div class="swiper-wrapper">
${card.images.map(image => `
<div class="swiper-slide">
<img src="${image}" class="card-image" alt="Image">
</div>
`).join('')}
</div>
<div class="swiper-pagination"></div>
</div>
</div>
<h2>${card.title}</h2>
<h3>${card.subtitle}</h3>
<p class="description">${card.description}</p>
</div>
`;
cardContainer.append(cardHtml);
// Initialize Swiper for each card
var cardSwiper = new Swiper('.swiper-' + index, {
pagination: '.swiper-' + index + ' .swiper-pagination',
paginationClickable: true,
autoHeight: true,
keyboardControl: true,
mousewheelControl: true,
lazyLoading: true,
lazyLoadingInPrevNext: true,
loop: true,
initialSlide: 0,
});
});
});
});
</script>
<!-- Include Swiper CSS and JS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/css/swiper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/js/swiper.min.js"></script>
{{ end }}
|
需要注意的是,我已经在加入短代码时定义了Swiper的CSS样式,如果你没有,这里需要额外添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
.swiper-container {
max-width: 820px;
margin: 2em auto;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background-color: transparent;
/* Center slide text vertically */
display: flex;
justify-content: center;
align-items: center;
img {
margin: 0 !important;
}
}
|
其他询问CSS的每个细节的部分就在这里略去,我希望能够放大看图,于是我又让chatGPT加入了灯箱功能。一开始选择的是Magnific Popup的JS库,但它对我来说不够美观,还会出现将所有卡片内图片一起展示的问题,又让chatGPT换成了Fancybox。另外考虑到json和html的换行符不一致,我又加入了一个简单的转换代码。
大功告成了!
最终的代码模板
在最终的结果里,我将所有的元素名称都进行了统一。也就是说,如果你需要将它更换成其他名称,请善用查找替换功能。
在代码内出现的所有badge 都需要换成你自定义的名称,与之对应的还有HTML与JSON文件的文件名、文件路径,甚至MD文件里的Layout等。如果元素名称不一致,就会出现打开网页一片空白的状况。(别问我为什么知道)
但同时也说明,这套搭配不仅能适用一个页面,只要通过更改元素名称,我们就可以在博客里分别加入许多这样的页面,它与标签墙这个模块就十分搭配,可以像我这样展示自己的收藏,也可以用来记录书影音等,有了一套模板之后,后续的改动及更新也十分轻松简单。
美中不足的一点是,我向chatGPT反复请教,也没能够顺利实现切换正序/倒序显示的功能。所以如果有这方面的需要,还是在json文件里手动排序吧……
1.HTML
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
|
{{ define "main" }}
{{ $badges := getJSON "static/badges/badges.json" }}
<div id="badge-container"></div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var badgeContainer = document.getElementById("badge-container");
$.getJSON('/badges/badges.json', function(data) {
var badgeHtml = ''; // Accumulate badge HTML in this variable
$.each(data, function(index, badge) {
badgeHtml += `
<div class="badge">
<div class="badge-images">
<div class="swiper-container swiper-${index}">
<div class="swiper-wrapper">
${badge.images.map((image, imgIndex) => `
<div class="swiper-slide">
<a class="badge-image-link" href="${image}" data-fancybox="group-${index}" data-caption="${badge.title}">
<img src="${image}" class="badge-image" alt="Image">
</a>
</div>
`).join('')}
</div>
<div class="swiper-pagination"></div>
</div>
</div>
<h3>${badge.title}</h3>
<h4>${badge.subtitle}</h4>
<p class="description">${badge.description.replace(/\n/g, '<br>')}</p>
</div>
`;
});
// After the loop, add the accumulated badge HTML to badgeContainer
badgeContainer.innerHTML = badgeHtml;
// Initialize Swiper for each badge
$.each(data, function(index, badge) {
var badgeSwiper = new Swiper('.swiper-' + index, {
pagination: '.swiper-' + index + ' .swiper-pagination',
paginationClickable: true,
autoHeight: true,
keyboardControl: true,
mousewheelControl: true,
lazyLoading: true,
lazyLoadingInPrevNext: true,
loop: true,
initialSlide: 0,
});
});
}); // End of $.getJSON('/static/badges/badges.json')
}); // End of document.addEventListener("DOMContentLoaded")
</script>
<!-- Include Swiper CSS and JS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/css/swiper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/js/swiper.min.js"></script>
<!-- Include Fancybox CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css">
<!-- Include jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Include Fancybox JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js"></script>
{{ end }}
|
2.json
请注意每一张卡片数据末尾的逗号,没有它卡片的数据将不会被读取(最后一张卡片除外)!
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
|
[
{
"title": "Card 1",
"subtitle": "Subtitle for Card 1",
"description": "Description for Card 1",
"images": [
"/badges/1.png",
"/badges/2.png"
]
},
{
"title": "Card 2",
"subtitle": "Subtitle for Card 2",
"description": "Description for Card 2",
"images": [
"/badges/3.png",
"/badges/4.png"
]
},
{
"title": "Card 3",
"subtitle": "Subtitle for Card 3",
"description": "Description for Card 3",
"images": [
"/badges/5.png",
"/badges/6.png"
]
}
]
|
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
|
/* Basic styles for badges */
.badge-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin: 7px; /* Negative margin to counteract card margins */
}
.badge {
border: 1px solid var(--text-color-light);
border-radius: 10px;
padding: 10px;
margin: 7px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: var(--card-background); /* Use light mode background color by default */
color: var(--card-text-color-main); /* Use light mode text color by default */
width: calc(31.6% - 0px);
box-sizing: border-box;
display: inline-block;
vertical-align: top;
}
.badge h3 {
margin-top: -20px;
text-align: center; /* Center align the text */
color: var(--card-text-color-main);
font-size: 10px;
}
.badge > h4 {
margin-top: 10px;
text-align: center;
color: var(--card-text-color-main);
font-size: 10px;
}
.badge-images {
display: flex;
overflow: hidden;
}
.badge-image {
max-width: 275px;
max-height: 100px; /* Set a reasonable max height for the images */
width: 100%;
height: 100%;
margin: 3px;
background-color: transparent; /* 设置图片背景色为透明 */
margin-bottom: 0px; /* 调整图片与其他内容之间的距离 */
}
/* Styles for the description */
.description {
max-height: 100px; /* Limit initial height */
overflow: scroll;
margin-top: 7px;/* 调整描述部分与卡片顶部的距离 */
margin-right: 0px;
margin-left: 7px;
line-height: 1.5; /* 调整行距 */
color: var(--card-text-color-main);
font-size: 10px;
transition: max-height 0.3s ease-in-out;
}
.description.visible {
max-height: none; /* Expand on interaction */
}
@media (max-width: 700px) {
.badge {
width: calc(100% - 14px); /* Full width minus margins */
}
.badge h3, .badge h4, .description {
font-size: 8px; /* Adjust font sizes for smaller screens */
}
.description {
max-height: 80px; /* Reduce the initial max-height */
}
}
@media screen and (min-width: 700px) and (max-width: 900px) {
.badge {
width: calc(48.5% - 14px); /* Adjust width for medium screens */
}
.badge h3, .badge h4, .description {
font-size: 9px; /* Adjust font sizes for medium screens */
}
}
@media (min-width: 900px) {
.badge {
width: calc(31.6% - 0px);
}
.badge h3, .badge h4, .description {
font-size: 10px;
}
}
/* badges -------- end */
|
DLC版本:瀑布流式布局
如果你跟我一样,有一些不那么长宽规整的图片需要展示,我还另做了一个瀑布流式布局可以参考。
这个版本在html文件里额外引用了Masonry的JS库实现瀑布流式布局,CSS样式也进行了相应调整。
1.HTML
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
|
{{ define "main" }}
{{ $badges := getJSON "static/badges/badges.json" }}
<div id="badge-container"></div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var badgeContainer = document.getElementById("badge-container");
var masonryContainer = document.createElement("div");
masonryContainer.className = "masonry-container";
$.getJSON('/badges/badges.json', function(data) {
var badgeHtml = ''; // Accumulate badge HTML in this variable
$.each(data, function(index, badge) {
badgeHtml += `
<div class="badge">
<div class="badge-images">
<div class="swiper-container swiper-${index}">
<div class="swiper-wrapper">
${badge.images.map((image, imgIndex) => `
<div class="swiper-slide">
<a class="badge-image-link" href="${image}" data-fancybox="group-${index}" data-caption="${badge.title}">
<img src="${image}" class="badge-image" alt="Image">
</a>
</div>
`).join('')}
</div>
<div class="swiper-pagination"></div>
</div>
</div>
<h3>${badge.title}</h3>
<h4>${badge.subtitle}</h4>
<p class="description">${badge.description.replace(/\n/g, '<br>')}</p>
</div>
`;
});
// After the loop, add the accumulated badge HTML to masonryContainer
masonryContainer.innerHTML = badgeHtml;
badgeContainer.appendChild(masonryContainer);
// Initialize Swiper for each badge
$.each(data, function(index, badge) {
var badgeSwiper = new Swiper('.swiper-' + index, {
pagination: '.swiper-' + index + ' .swiper-pagination',
paginationClickable: true,
autoHeight: true,
keyboardControl: true,
mousewheelControl: true,
lazyLoading: true,
lazyLoadingInPrevNext: true,
loop: true,
initialSlide: 0,
});
});
// Initialize Masonry after badges are added
var masonry = new Masonry(masonryContainer, {
itemSelector: ".badge",
columnWidth: ".badge",
gutter: 14, // Adjust as needed
});
}); // End of $.getJSON('/static/badges/badges.json')
});
</script>
<!-- Include Masonry.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js"></script>
<!-- Include Swiper CSS and JS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/css/swiper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/js/swiper.min.js"></script>
<!-- Include Fancybox CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css">
<!-- Include jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Include Fancybox JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js"></script>
{{ end }}
|
2.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
|
/* Basic styles for badges */
.badge-container {
display: flex;
flex-wrap: wrap;
margin: -7px; /* Negative margin to counteract card margins */
}
.badge {
border: 1px solid var(--text-color-light);
border-radius: 10px;
padding: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: var(--card-background); /* Use light mode background color by default */
color: var(--card-text-color-main); /* Use light mode text color by default */
flex: 1 1 calc(31.6% - 14px); /* Adjust flex properties to control width and spacing */
box-sizing: border-box;
margin: 7px;
}
.badge h3 {
margin-top: -10px; /* Reset margin */
text-align: center;
color: var(--card-text-color-main);
font-size: 10px;
}
.badge > h4 {
margin-top: 10px;
text-align: center;
color: var(--card-text-color-main);
font-size: 10px;
}
.badge-images {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.badge-image {
max-width: 100%;
max-height: 100%;
width: 100%;
margin: 3px;
background-color: transparent;
}
/* Styles for the description */
.description {
height: 100%;
margin-top: 7px;
margin-right: 0px;
margin-left: 7px;
line-height: 1.5;
color: var(--card-text-color-main);
font-size: 10px;
transition: max-height 0.3s ease-in-out;
}
.description.visible {
max-height: none;
}
/* Media queries */
@media (max-width: 700px) {
.badge {
width: calc(100% - 14px);
margin: 7px;
}
.badge h3,
.badge h4,
.description {
font-size: 8px;
}
.description {
max-height: 80px;
}
}
@media screen and (min-width: 700px) and (max-width: 900px) {
.badge {
width: calc(48.5% - 14px);
}
.badge h3,
.badge h4,
.description {
font-size: 9px;
}
}
@media (min-width: 900px) {
.badge {
width: calc(31.6% - 14px);
}
.badge h3,
.badge h4,
.description {
font-size: 10px;
}
}
/* Adjust Masonry gutter */
@media (min-width: 700px) {
.masonry-container {
margin: -7px;
}
.badge {
margin: 7px;
}
}
/* badges -------- end */
|