Hugo|自定义展柜页面

本文代码作者:ChatGPT

date-line
ic_fluent_keyboard_layout_float_24_regular Created with Sketch.

目录

事情的一切起因都源于我懒于更新。

眼看着关注的朋友们发出一篇又一篇好看好玩的博客,每次点开都一边感叹“大家的生活和想法真有趣”,一边为自己落灰长草的站点愧疚0.1秒钟。我也想更新!但我的日常生活千篇一律,有点想说的话都在象上嘟嘟完了,连写文都全是半路弃坑的草稿,要发点什么才好呢?

我很快想到我的小铁块们,一入徽章深似海,从此闲钱是路人。我很喜欢这些闪闪发光的小东西,如果可以把它们放在我的博客做展览就再好不过了。这个想法其实很早之前就存在,只是博客大多都由单篇md文件组成,要想做可以持续更新的展示似乎很麻烦,何况我根本不会写代码,所以也就只是偶尔想一想,从来不奢望能够付诸实践。

正好时隔几个月又去翻别人家的新奇作业,很快发现一个接近我想法的新东西:好物页面

这不就是我想要的那种橱窗展柜吗!在看到它的第一眼,我就觉得这个形式绝不仅仅能做好物分享,它可以被应用到很多很多需要记录展示的地方去,同时整个页面的数据都可以存在一份简单的json文件里,更新只需要按部就班地添加短短几行。(写这篇博客的时候已经看到了它的拓展版本:Shortcode汇总

唯一美中不足的就是一张卡片只能对应一张图,如果能够插入多张图片,可以实现图片轮播不就更完美了?要我自己空写一个模板肯定不可能,但现下左有前人的基础,右有ChatGPT辅助,我还是有胆子试一试的。

模板在文章最后一部分,作为示例,这是我的小铁块赛博展柜,为了方便以后更新,我的徽章是按时间顺序倒序排列的,建议第一次从最底部开始看。你也可以在我的“关于”页面找到掌中方块世界一键直达。

由于图床和JS库引用等等各种各样的问题,图片第一次加载的时候大概有百分之百的概率遇到加载略慢、图片飞出卡片等状况,这个时候只需要等图片加载得差不多再刷新一次就可以恢复正常(意思是,我不知道问题在哪里我也不会修,请大家凑合看)。

从最零碎的知识点开始补课

要想创建一个这样的新界面,步骤就和把大象关进冰箱一样简单:

1
2
3
创建页面模板:HTML文件
新建Hugo页面:MD文件
新建数据文件:JSON文件

步骤其实和创建About界面的标签墙也差不多嘛?在md文件里写好titlelayout,另外在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 */

最后更新于 2023 年, 11 月 14 日。

Copyright since 2023 by 鹤辞, All rights reserved.
Built with Hugo
主题 StackJimmy 设计