为Fluid新增相册功能

本文最后更新于:2024年9月19日 晚上

0x00 前言

很久没写过博客了,是的,摆烂了很久,以至于我重新打开这个博客时,已经忘记了当时是如何实现的这个相册功能,所以如今就将这个流程记录一遍。

0x01 部署流程

由于我不懂JavaScript和前端,所以这个部署过程的原理我并没有搞清楚,这里只是记录流程

参考的是这篇博客

-1- 创建相册的页面

在项目blog文件夹下执行 Hexo 命令:

1
hexo new page "photo"

Hexo会自动创建 source/photo,里面有一个文件 index.md

这个index.md就是此描述了该页面的内容,其内容如下(注释由GLM AI生成)

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
---
# 页面配置信息
title: 摄影 # 页面的标题
subtitle: '' # 页面的副标题,留空则不显示
excerpt: '' # 页面的摘要或简短描述,留空则不显示
banner_img: /img/cover.jpg # 页面的横幅图片路径
lazyload: true # 图片是否启用懒加载,true为启用
hide: false # 是否隐藏该页面,false为不隐藏
date: 2023-12-01 19:04:03 # 页面的创建或更新日期
layout: photo # 页面的布局类型,此处为专门为照片设计的布局
index_img: # 索引图片,用于在列表中展示的缩略图,留空则不显示
tags: # 页面的标签,用于分类和搜索,留空则不指定
categories: # 页面的分类,用于组织内容,留空则不指定
updated: # 页面的更新日期,留空则不指定
---

# CSS样式定义
<style>
/* 图片网格容器样式 */
.ImageGrid {
width: 100%; /* 宽度占满容器 */
max-width: 1040px; /* 最大宽度限制 */
margin: 0 auto; /* 水平居中 */
text-align: center; /* 文本居中 */
}
/* 图片卡片样式 */
.card {
overflow: hidden; /* 超出部分隐藏 */
transition: .3s ease-in-out; /* 过渡效果,持续时间和缓动函数 */
border-radius: 8px; /* 边框圆角 */
background-color: #efefef; /* 背景颜色 */
padding: 1.4px; /* 内边距 */
}
/* 图片在卡片中的样式 */
.ImageInCard img {
padding: 0; /* 图片内边距 */
border-radius: 8px; /* 图片圆角 */
width:100%; /* 图片宽度占满卡片 */
height:100%; /* 图片高度占满卡片 */
}
/* 暗色模式下的样式 */
@media (prefers-color-scheme: dark) {
.card {background-color: #333;} /* 暗色模式下卡片的背景颜色 */
}
</style>

# HTML容器定义
<div id="imageTab"></div> <!-- 图片标签容器 -->
<div class="ImageGrid"></div> <!-- 图片网格容器 -->

_config.fluid.yml 文件的 menu 里添加如下内容,这样就可以在页面右上角生成图标快速访问此页面了:

image-20240918210034185

1
- { key: "摄影", link: "/photo/", icon: "iconfont icon-image" }

-2- 准备图片

由于照片的展示是瀑布流的形式,所以需要先生成缩略图来加快页面加载,生成缩略图采用的是python脚本,将其放在了tools目录下

需要用到pillow模块,所以需要先安装PIL

1
pip install pillow

其主要功能就是从同级的ori_picture目录下读取文件,生成缩略图后添加_mini文件名后缀,放入mini_picture目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
from PIL import Image

# 创建目录用于存储生成的图片
save_dirname = "mini_picture"
if not os.path.exists(save_dirname):
os.mkdir(save_dirname)

dirname = "ori_picture"
imgs = [os.path.join(dirname, i) for i in os.listdir(dirname)]

for idx, img_path in enumerate(imgs):
if img_path.endswith("jpg") or img_path.endswith("png") or img_path.endswith("jpeg"):
original_filename, extension = os.path.splitext(os.path.basename(img_path))
img = Image.open(img_path)
img.thumbnail((240, 480))

# 构造新的文件名,加上 "_mini" 后缀
new_filename = f"{original_filename}_mini{extension}"

img.save(os.path.join(save_dirname, new_filename))

-3- 生成image.json

先安装image-size

1
npm i image-size

image-size 的功能是获取图片的宽高信息,不支持获取非图片格式的宽高,比如视频。

然后使用 phototool.js 生成 image.json,脚本由来自为 Hexo + Fluid 博客添加承载相册的页面 - 『魏超』的 blog,注释由GLM AI生成

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
"use strict"; // 启用严格模式,确保代码运行在更严格的语法环境中

const sizeOf = require('image-size'); // 引入image-size模块,用于获取图片尺寸
const path = require('path'); // 引入path模块,用于处理文件路径
const { readdir, writeFileSync, stat } = require('fs').promises; // 引入fs模块的Promise版本,用于异步文件操作

const imagesSrcPath = 'images/original-image/'; // 原始图片存放路径
const imagesThumbPath = 'images/mini-image/'; // 缩略图存放路径
const sourcePath = 'source/'; // 输出JSON文件的路径

// 定义异步函数,处理每个图片页面
async function processPages() {
try {
const pageArr = await readdir(imagesSrcPath); // 读取原始图片目录下的所有页面目录

for (const page of pageArr) { // 遍历每个页面目录
const pageSrcPath = path.join(imagesSrcPath, page); // 构建当前页面的原始图片路径
const pageThumbPath = path.join(imagesThumbPath, page); // 构建当前页面的缩略图路径
const output = path.join(sourcePath, page, 'image.json'); // 构建输出JSON文件的路径

const filesInPage = await readdir(pageSrcPath); // 读取当前页面目录下的所有相册目录

let imagesInPage = []; // 初始化当前页面的图片信息数组

for (const album of filesInPage) { // 遍历每个相册目录
const albumSrcPath = path.join(pageSrcPath, album); // 构建当前相册的原始图片路径
const albumThumbPath = path.join(pageThumbPath, album); // 构建当前相册的缩略图路径

try {
const albumStat = await stat(albumSrcPath); // 获取当前相册的文件状态

if (albumStat.isDirectory()) { // 如果是目录,则处理该相册
let imagesInAlbum = { name: album }; // 初始化当前相册的图片信息对象
let children = []; // 初始化当前相册的图片子项数组

const imageFile = await readdir(albumSrcPath); // 读取当前相册目录下的所有图片文件

for (const file of imageFile) { // 遍历每个图片文件
if (path.extname(file) !== ".mp4") { // 如果文件不是视频,则处理图片
const dimensions = sizeOf(path.join(albumSrcPath, file)); // 获取图片尺寸
// 生成并添加缩略图信息
const thumbnailName = file.replace(path.extname(file), '_mini' + path.extname(file));
children.push(dimensions.width + '.' + dimensions.height + ' ' + file + ' ' + thumbnailName);
} else { // 如果是视频文件,则处理对应的缩略图
const dimensions = sizeOf(path.join(albumThumbPath, path.basename(file, ".mp4") + ".png"));
children.push(dimensions.width + '.' + dimensions.height + ' ' + file + ' ' + path.basename(file, ".mp4") + ".png");
}
}

imagesInAlbum.children = children; // 将图片子项数组赋值给相册信息对象的children属性
imagesInPage.push(imagesInAlbum); // 将相册信息对象添加到当前页面的图片信息数组
const fs = require('fs'); // 引入fs模块,用于写入文件
fs.writeFileSync(output, JSON.stringify(imagesInPage, null, "\t")); // 将当前页面的图片信息数组写入JSON文件

}
} catch (error) {
console.error("Error:", error); // 捕获并打印错误信息
}
}
}
} catch (error) {
console.error("Error:", error); // 捕获并打印错误信息
}
}

processPages(); // 调用函数,开始处理图片页面

执行后会在 source 内对应页面的文件夹内生成 image.json

放在 scripts 内的 js 文件在执行 Hexo 部分命令 比如 hexo s -d 时会被自动执行。

-4- 加载图片到页面

现在图片和描述图片信息的json文件都准备好了,就差把图片加载到页面上了

这里需要两个js脚本,一个是injector.js,用到了[Hexo 注入器](进阶用法 | Hexo Fluid 用户手册 (fluid-dev.com)),用于将 HTML 片段注入生成页面的 <head><body> 节点中。

另一个是用于生成瀑布流形式的页面布局的gallery.js

gallery.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
var imgDataPath = "image.json"; // 图片数据文件路径,包含图片所在相册、高宽、名称等信息
var imgPath = "https://gcore.jsdelivr.net/gh/GoooForward/blog@source/images/original-image/photo/"; // 原图的网络访问路径
var imgThumbPath = "https://gcore.jsdelivr.net/gh/GoooForward/blog@source/images/mini-image/photo/"; // 缩略图的网络访问路径

// 获取窗口宽度,用于判断是否为手机端
var windowWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
// 根据窗口宽度设置图片显示宽度
if (windowWidth < 768) { // 如果窗口宽度小于768px,认为是手机端
var imageWidth = 145; // 图片显示宽度设置为145px
} else {
var imageWidth = 250; // 图片显示宽度设置为250px
}

const photo = {
page: 1, // 当前页面索引
init: function () { // 初始化函数
var that = this;
// 使用jQuery的getJSON方法异步加载图片数据文件
$.getJSON(imgDataPath, function (data) {
that.render(that.page, data); // 渲染图片
//that.scroll(data); // 假设这里有滚动相关处理,已被注释
that.eventListen(data); // 绑定事件监听
});
},
constructHtml: function (options) { // 构建HTML字符串的函数
// 解构赋值获取参数
const {
imageWidth,
imageX,
imageY,
name,
imgPath,
imgThumbPath,
imgName,
imgNameWithPattern,
imgThumbNameWithPattern,
} = options;
// 构建图片卡片HTML元素
const htmlEle = `<div class="card lozad" style="width:${imageWidth}px">
<div class="ImageInCard" style="height:${
(imageWidth * imageY) / imageX
}px">
<a data-fancybox="gallery" href="${imgPath}${name}/${imgNameWithPattern}"
data-caption="${imgName}" title="${imgName}">
<img class="lazyload" data-src="${imgThumbPath}${name}/${imgThumbNameWithPattern}"
src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
onload="lzld(this)"
lazyload="auto">
</a>
</div>
</div>`;
return htmlEle; // 返回构建好的HTML字符串
},
render: function (page, data = []) { // 渲染图片的函数
this.data = data; // 将数据赋值给实例属性
if (!data.length) return; // 如果没有数据,则直接返回
var html,
imgNameWithPattern,
imgThumbNameWithPattern,
imgName,
imageSize,
imageX,
imageY,
li = "";

let liHtml = ""; // 初始化相册导航HTML
let contentHtml = ""; // 初始化相册内容HTML

// 遍历数据,构建相册导航和内容
data.forEach((item, index) => {
const activeClass = index === 0 ? "active" : ""; // 判断是否为激活状态
liHtml += `<li class="nav-item" role="presentation">
<a class="nav-link ${activeClass} photo-tab" id="home-tab" photo-uuid="${item.name}" data-toggle="tab" href="#${item.name}" role="tab" aria-controls="${item.name}" aria-selected="true">${item.name}</a>
</li>`;
});
// 获取第一个相册的数据
const [initData = {}] = data;
const { children = [], name } = initData;
// 遍历相册中的图片,构建HTML
children.forEach((item, index) => {
imgNameWithPattern = item.split(" ")[1];
imgThumbNameWithPattern = item.split(" ")[2];
imgName = imgNameWithPattern.split(".")[0];
imageSize = item.split(" ")[0];
imageX = imageSize.split(".")[0];
imageY = imageSize.split(".")[1];
let imgOptions = {
imageWidth,
imageX,
imageY,
name,
imgName,
imgPath,
imgThumbPath,
imgNameWithPattern,
imgThumbNameWithPattern,
};
li += this.constructHtml(imgOptions); // 构建图片HTML
});
// 构建相册内容HTML
contentHtml += ` <div class="tab-pane fade show active" id="${initData.name}" role="tabpanel" aria-labelledby="home-tab">${li}</div>`; // 添加第一个相册的内容HTML,并设置为激活状态

const ulHtml = `<ul class="nav nav-tabs" id="myTab" role="tablist">${liHtml}</ul>`; // 构建相册导航的HTML
const tabContent = `<div class="tab-content" id="myTabContent">${contentHtml}</div>`; // 构建包含所有相册内容的HTML

// 将相册导航和内容添加到页面对应的元素中
$("#imageTab").append(ulHtml);
$(".ImageGrid").append(tabContent);
this.minigrid(); // 调用minigrid函数,初始化图片网格布局
},
eventListen: function (data) { // 事件监听函数
let self = this;
// 监听标签页切换事件
$('a[data-toggle="tab"]').on("shown.bs.tab", function (e) {
$(".ImageGrid").empty(); // 清空当前图片网格内容
const selectId = $(e.target).attr("photo-uuid"); // 获取被选中标签的相册ID
const selectedData = data.find((data) => data.name === selectId) || {}; // 从数据中找到被选中的相册数据
const { children, name } = selectedData; // 解构赋值获取相册的图片数据
let li = ""; // 初始化相册内容HTML
// 遍历相册中的图片,构建HTML
children.forEach((item, index) => {
imgNameWithPattern = item.split(" ")[1];
imgThumbNameWithPattern = item.split(" ")[2];
imgName = imgNameWithPattern.split(".")[0];
imageSize = item.split(" ")[0];
imageX = imageSize.split(".")[0];
imageY = imageSize.split(".")[1];
let imgOptions = {
imageWidth,
imageX,
imageY,
name,
imgName,
imgPath,
imgThumbPath,
imgNameWithPattern,
imgThumbNameWithPattern,
};
li += self.constructHtml(imgOptions); // 构建图片HTML
});
// 将构建好的相册内容HTML添加到页面对应的元素中
$(".ImageGrid").append(li);
self.minigrid(); // 重新初始化图片网格布局
});
},
minigrid: function () { // 初始化图片网格布局的函数
var grid = new Minigrid({ // 创建Minigrid实例
container: ".ImageGrid", // 网格容器的选择器
item: ".card", // 网格项的选择器
gutter: 12, // 网格项之间的间隔
});
grid.mount(); // 初始化网格布局
// 监听窗口大小变化事件,重新初始化网格布局
$(window).resize(function () {
grid.mount();
});
},
};
photo.init(); // 调用photo对象的init方法,开始初始化过程

injector.js

layout: photo 时会导入下面的 js 与 css,这里的 layout 就是 index.md 里的 layout,其中包括 gallery.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const { root: siteRoot = "/" } = hexo.config;
// layout 为 photo的时候导入这些js与css
hexo.extend.injector.register(
"body_end",
`
<link rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css">
<script src="//cdn.jsdelivr.net/npm/minigrid@3.1.1/dist/minigrid.min.js"></script>
<script src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lazyloadjs/3.2.2/lazyload.js"></script>
<script defer src="${siteRoot}js/gallery.js"></script>
`,
"photo"
);

injector.js放在scripts目录下,自动执行,将gallery.js注入到photo下的index页面中


为Fluid新增相册功能
https://goooforward.github.io/blog/2024/09/02/study/博客新增相册功能/
作者
tangyuwei
发布于
2024年9月2日
许可协议