身为一个喜欢拍照的猿,博空中怎么可能少了相册呢。
Hexo博客自身没有相册功能,网上找了一些资料都是瀑布流的样式较多,这不是我想要的,于是借鉴了 水寒写的添加瀑布流相册 自己写了一个。先上几张图看一下效果(ps:css现学现卖拼凑出为的,页面效果有待优化哈,完整效果可以去我博客看过愙相册)
1. 创建页面和导航栏
进入博客根目录,用命令hexo new page photos
新建相册页面 ,这样会在 /source/ 下创建 photos/index.md,在其中添加 type: picture。
在主题配置文件themes\next\_config.yml
中对应位置 menu 里添加 photos: /photos || camera-retro ,这样生成后就有导航栏菜单了。
有了语言,还要到语言对应的目录中修改。
2. json 文件处理图片信息
2.1 文件分析
为了生成多级的相册,这里的目录分为二级。
- 第一级目录是相册,目录名就是相册名。
- 第二级目录是分类目录,可以把同一时间或同一主题的放在一起,类似于QQ空间旅游相册中的地点分类。
- 如果没有第二级目录,则以相片的日期分类。
- 二级目录文件名如果带时间,如:2020-02-02.分类1。 拆分时间和目录名;如果不带时间,则用目录创建时间为分类时间,目录名为分类名
目录结构如下:
相册根目录
├─相册1
│ │ 图片1.jpg
│ │ 图片2.jpg
│ │
│ └─2020-02-02.分类1
│ 图片3.jpg
│ 图片4.jpg
│
└─相册2
图片5.jpg
图片6.jpg
我们需要生成的目录json数据为:
[{
"mini": "/min",
"dir": "相册2",
"date": "2020-03-14",
"num": 2,
"cover": {
"url": "/min/相册2/图片6.jpg",
"width": 1024,
"height": 768,
}
"photos": [{
"date": "2020-02-12",
"name": "",
"photo": [{
"name": "图片5",
"url": "/相册2/图片5.jpg"
},{
"name": "图片6",
"url": "/相册2/图片6.jpg"
}]
}]
},{
"mini": "/min",
"dir": "相册1",
"date": "2020-03-14",
"num": 4,
"cover": {
"url": "/min//相册1/图片2.jpg",
"width": 1024,
"height": 768,
}
"photos": [{
"date": "2020-02-02",
"name": "分类1",
"photo": [{
"name": "图片3",
"url": "/相册1/2020-02-02.分类1/图片3.jpg"
},{
"name": "图片4",
"url": "/相册1/2020-02-02.分类1/图片4.jpg"
}]
},{
"date": "2020-02-27",
"name": "",
"photo": [{
"name": "图片1",
"url": "/相册1/图片1.jpg"
},{
"name": "图片2",
"url": "/相册1/图片2.jpg"
}]
}]
}]
2.2 编写处理脚本
在博客根目录的 /scripts/ 文件夹下新建一个 photo.js 文件,内容如下
const fs = require('fs')
const path = require('path')
const util = require('util')
const images = require("images");
// 默认相册图片根目录
const defaultRootPath = '/source/photos';
// 默认是否开启缩略图
const defaultUseMini = true;
// 默认缩略图存放目录
const defaultMiniPath = '/min'
// 是否在hexo g状态
const isInHexo = "undefined" != typeof hexo;
if (isInHexo && !hexo.config.photo.enable )
return;
const promisifyReaddir = util.promisify(fs.readdir)
const promisifyStat = util.promisify(fs.stat)
// 图片格式
const photoExtname = ['.jpg', '.bmp', '.png', '.gif']
// 相册目录相对博客根目录,引用主题里的 photoPath 目录,如果没有则用 /source/photos
const rootPath = ((isInHexo && hexo.config.photo.path) ? hexo.photo.config.path : defaultRootPath);
// 相册绝对路径
const photoPath = path.resolve(__dirname, '..' + rootPath);
// 是否使用缩略图,主题中存在
const useMini = (isInHexo && hexo.config.photo.mini.enable) ? hexo.photo.mini.enable : defaultUseMini;
// 缩略图目录
const minPath = useMini ? ((isInHexo && hexo.config.photo.mini.path) ? hexo.config.photo.mini.path : defaultMiniPath) : '';
// 相册json数据
var photoArray = new Array();
async function init() {
// 是否成生缩略图
if (useMini) {
deleteFolder(photoPath + minPath);
}
// 获取相册数据
await getPhotoData()
// 相册内按时间排序
for (let i = 0; i < photoArray.length; i++) {
if (photoArray[i].photos == null) {
continue;
}
photoArray[i].photos = photoArray[i].photos.sort(function (a, b) { return a.date > b.date ? 1 : -1 })
console.log(photoArray[i].date, photoArray[i].photos[0].date);
// 刷新创建时间,不晚于最早的分类时间
if (photoArray[i].date > photoArray[i].photos[0].date) {
photoArray[i].date = photoArray[i].photos[0].date
}
}
photoArray = photoArray.sort(function (a, b) { return a.date > b.date ? -1 : 1 })
console.log(JSON.stringify(photoArray))
// 写入jsonp数据
fs.writeFile(photoPath + path.sep + 'photo.jsonp', 'callback('+JSON.stringify(photoArray)+')', function (err) {
if (err) {
return console.error(err);
}
console.log("数据写入成功!");
});
}
// 获取相册数据
async function getPhotoData(proPath = '') {
const dir = await promisifyReaddir(photoPath + proPath)
// 当前相册数据
var photoData = null;
// 相册名使用相册目录名
var photoName = null;
// 存在分类目录,
// 分类目录没带时间,则用目录创建时间为分类时间,目录名为分类名
// 分类目录存在时间,如:2020-02-02.分类1。 拆分时间和目录名
// 分类名。
var categoryName = "";
// 分类时间。
var categoryDate = null;
// 相册只有一层,非根目录相册直接加入到对应一级目录中
if (proPath) {
// 获取最后一级目录名
let index = proPath.split(path.sep).join('/').lastIndexOf("\/"); //兼容两个平台 并获取最后位置index
let lastDir = proPath.substring(index + 1, proPath.length); //截取获得结果
// 获取第一次目录名,即 相册名
index = proPath.substring(1).split(path.sep).join('/').indexOf("\/");
photoName = index > 0 ? proPath.substring(1, index + 1) : proPath.substring(1);
// 最后一级目录与相册名相等,说明当前目录是相册目录,否当前目录为分类目录
if (lastDir != photoName) {
// 拆分目录名
var tmp = lastDir.split('.');
if (tmp.length > 1) {
categoryDate = new Date(tmp[0]).toISOString().substring(0, 10);
categoryName = lastDir.substring(tmp[0].length + 1);
}
// 不存在时间格式或时间格式不对,都认为无效时间,以分类目录创建时间处理
if (categoryDate == null) {
categoryName = lastDir;
const stat = await promisifyStat(`${photoPath}${proPath}`)
categoryDate = stat.mtime.toISOString().substring(0, 10);
}
}
for (let i = 0; i < photoArray.length; i++) {
if (photoArray[i].dir === photoName) {
photoData = photoArray[i];
if (!photoData.photos) {
photoData.photos = new Array()
}
break;
}
}
}
// 遍历目录中文件和文件夹
for (let i = 0; i < dir.length; i++) {
const stat = await promisifyStat(path.resolve(photoPath + proPath, dir[i]))
if (stat.isFile()) {
// 根目录文件不处理,只处理子级以以下的图片文件
if (proPath) {
// 匹配图片后缀
var ext = path.extname(dir[i]).toLowerCase();
if (photoExtname.indexOf(ext) != -1) {
var name = path.basename(dir[i], ext)
var date = stat.mtime.toISOString().substring(0, 10)
// 相册分类数据
var photoCategory;
for (let j = 0; j < photoData.photos.length; j++) {
// 根据时间和分类获取现在分类数据
if (photoData.photos[j].date == categoryDate && photoData.photos[j].name == categoryName) {
photoCategory = photoData.photos[j];
if (!photoCategory.photo) {
photoCategory.photo = new Array();
}
break;
}
}
// 不存分类则创建
if (!photoCategory) {
photoCategory = {
date: categoryDate ? categoryDate : date,
name: categoryName ? categoryName : "",
photo: new Array()
}
photoData.photos.push(photoCategory);
}
// 插入相片
let img = images(photoPath + proPath + path.sep + dir[i]);
photoCategory.photo.push({
name: name,
width: img.width(),
height: img.height(),
url: proPath + path.sep + dir[i]
})
// 更新相册信息
photoData.num++;
photoData.cover={
width: img.width(),
height: img.height(),
url: minPath + proPath + path.sep + dir[i]
}
if (useMini) {
var outPath = path.resolve(photoPath + minPath + path.sep + proPath + path.sep) + path.sep;
checkDirExist(outPath);
// 宽度小于1024的不缩放,大于1024的以宽度等比缩到1024
let width = img.width();
let height = img.height();
if(width>1024){
height = 1024 * height / width;
width = 1024;
}
img.resize(width,height)
.save(outPath + path.sep + dir[i],{
quality: 50 //保存图片到文件,图片质量为50
});
}
}
}
} else if (stat.isDirectory()) {
// 缩略图目录忽略
if (path.sep + dir[i] === minPath) {
continue;
}
if (!proPath) {
// 根目录时创建相册
var photoData = {
mini: minPath,
dir: dir[i],
date: stat.mtime.toISOString().substring(0, 10),
num: 0,
photos: new Array()
}
photoArray.push(photoData)
}
// 递归子目录
await getPhotoData(proPath + path.sep + dir[i])
}
}
}
// 删除目录
function deleteFolder(filePath) {
const files = []
if (fs.existsSync(filePath)) {
const files = fs.readdirSync(filePath)
files.forEach((file) => {
const nextFilePath = filePath + path.sep + file
const states = fs.statSync(nextFilePath)
if (states.isDirectory()) {
//recurse
deleteFolder(nextFilePath)
} else {
//delete file
fs.unlinkSync(nextFilePath)
}
})
fs.rmdirSync(filePath)
}
}
// 判断目录是否存在,否存创建
function checkDirExist(folderpath) {
const pathArr = folderpath.split(path.sep);
let _path = '';
for (let i = 0; i < pathArr.length; i++) {
if (pathArr[i]) {
_path += `${pathArr[i]}${path.sep}`;
if (!fs.existsSync(_path)) {
fs.mkdirSync(_path);
}
}
}
}
init()
在博客配置_config.yml
中添加
photo:
enable: false
path: /source/photos/
mini:
enable: true
path: /mini
下载图片nodejs的插件
npm install images
配置说明
- photo.enable: 是否在
hexo g
时编译, 不是相册不是很次都有更新,也不需要每次遍相册的时候都来编译这个,所以加了一个标志控制使用hexo g
编译时编译相册,false-不编译
,true-编译
- photo.path: 相册图片根目录, 如果不通过hexo编译,可以配置js脚本中的
defaultRootPath
- photo.mini.enable: 是否开启图片压缩生成缩略图。如果不通过hexo编译,可以配置js脚本中的
defaultUseMini
配置 - photo.mini.path: 缩略图生成的相对路径。如果开启了缩略图功能,这项不能添空,不然图片会被覆盖。 如果不通过hexo编译,可以配置js脚本中的
defaultMiniPath
开启photo.enable: true
直接hexo g
,或者在根目录执行node script/photo.js
就能生成json数据了,json数据在相册跟目录
3. 编写页面处理js脚本
在 /themes/next/source/js/src/
目录下创建一个 photo.js
,内容如下:
const imgRoot = '/photos'
photo = {
// 当前显示类型、dir-目录,photo-照片
type: 'dir',
// 相册索引
photoIndex: 0,
// 分类索引
categoryIndex: 0,
// 相册数据数组
photoArray: null,
//offset 每次加载照片数量,以分类为一个单位
offset: 10,
init: function () {
var that = this;
if (this.photoArray == null) {
//这里设置的是刚才生成的 jsonp 文件路径
// json文件如果用代码仓库做图床会出现跨域问题
$.ajax({
type:"GET",
url:imgRoot + "/photo.jsonp",
dataType:"jsonp",
jsonpCallback:"callback",
success:function(data){
photoArray = data;
that.randerDir();
}
});
} else {
this.randerDir();
}
},
randerDir: function () {
// 显示相册目录
let data = photoArray;
this.type = 'dir';
$(".photo-div").empty();
let li = '<div class="photo-box">';
for (let i = 0; i < data.length; i++) {
let cover = data[i].cover; // 封面
let width = 275; // 相册目录定宽
let height = width * cover.height / cover.width;
li += '<div class="photo-box-item" style="width: ' + width + 'px">' +
'<div class="photo-box-item-click" id="' + i + '" style="height:' + height + 'px">' +
'<img class="nofancybox" style="padding: 0; border: none;" src="' + imgRoot + cover.url + '"/>' +
'</div>' +
'<div>' + data[i].dir + '</div>' +
'</div>'
}
li += '</div>';
$(".photo-div").append(li);
this.minigrid();
},
render: function (page) {
if (!photoArray || photoArray.length <= this.photoIndex) {
return;
}
// 相册数据
var photoData = photoArray[this.photoIndex];
this.type = 'photo';
// 插入相册标题
if (page == 1) {
let li = '<div class="photo-title">' +
'<div class="photo-title-text">' + photoData.dir +
'<a href="" class="photo-back">返回相册</a>' +'</div>' +
'<div class="photo-title-desc"> 创建于' + photoData.date + '/ 共' + photoData.num +' 张</div>' +
'</div>'
$(".photo-div").append(li);
}
let showCount = 0;
for (let i = 0; i < photoData.photos.length; i++) {
// 分类
photoCategory = photoData.photos[i];
if (i < this.categoryIndex) {
// 跳过
continue;
} else if (page <= i && showCount>=this.offset) {
// 结束
break;
} else {
// 加入分类信息
this.categoryIndex++;
let li = '<div class="photo-category-text">' +photoCategory.date + (photoCategory.name?'| ':'')+photoCategory.name + '</div>' +
'<div class="photo-category photo-category-box-' + this.categoryIndex + '">'
li += '</div>'
$(".photo-div").append(li);
for (let j = 0; j < photoCategory.photo.length; j++) {
showCount++;
// 相片数据
let data = photoCategory.photo[j];
let imgNameWithPattern = data.url;
let imgName = data.name;
let li = '<a data-fancybox="gallery" href="' +imgRoot + imgNameWithPattern + '?raw=true" data-caption="' + imgName + '">' +
'<img style="padding: 0; border: none;" src="' + imgRoot + photoData.mini + imgNameWithPattern + '" alt="' + imgName + '" title="' + imgName + '"></a>'
$('.photo-category-box-' + this.categoryIndex).append(li);
$('.photo-category-box-' + this.categoryIndex).justifiedGallery({
rowHeight: 300,
margins: 4,
randomize: true
});
}
}
}
$(".photo-div").lazyload();
this.minigrid();
},
minigrid: function () {
// 使用 minigrid动态布局相册
var grid = null;
if (this.type == "dir") {
grid = new Minigrid({
container: '.photo-box',
item: '.photo-box-item',
gutter: 12
});
grid.mount();
}
var that = this;
// 监听窗口大小事件
$(window).resize(function () {
if (that.type == "dir") {
grid.mount();
}
// 计算宽度是否加载新相片
that.loading();
});
// 监听滚动事件
$(window).on('scroll', function () {
// 计算宽度是否加载新相片
that.loading();
});
// 相册点击事件
$(".photo-box-item-click").bind("click", function () {
if (!photoArray || photoArray.length <= this.id) {
return;
}
$(".photo-div").empty();
that.photoIndex = this.id;
that.categoryIndex = 0;
that.render(1)
});
// 返回相册事件
$(".photo-back").bind("click", function () {
if (!photoArray || photoArray.length <= this.id) {
return;
}
$(".photo-div").empty();
that.randerDir();
return false;
});
},
// 判断滚动长度大于时加载新的
loading: function () {
if (this.type != 'photo') {
return;
}
var scrollTop = $(window).scrollTop();
if(scrollTop+$(window).height()>$(".photo-div").height()){
this.render(this.categoryIndex+1);
}
}
}
photo.init();
这里imgRoot 配置相册相对网站的相对路径,也可以用图床填写图床的目录。
我用的是coding代码仓库为图床,这里可以配置
const imgRoot = '//guoke3915.coding.net/p/guoke3915/d/img/git/raw/master/photos'
4. 配置主题
这里用到了几个第三方的布局js插件,所以在主题中配置一下。这里都引用了cdn的代码,也可以自己下过来放到自己的网站上。
- minigrid:等宽瀑布布局,用于相册目录布局
- justifiedGallery: 画廊式图片布局,用于相册照片显示
- fancybox:照片展示插件
4.1 引用js文件
修改/themes/next/layout/_scripts/commons.swig
文件,在最后加入代码
{% if page.type ==='picture' %}
<script type="text/javascript" src="//cdn.jsdelivr.net/gh/zngw/cdn/minigrid.min.js"></script>
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>
<script type="text/javascript" src="/js/src/photo.js"></script>
{% endif %}
4.2 引用css文件
修改themes\next\layout\_partials\head.swig
文件,在最后添加代码
{% if page.type ==='picture' %}
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/css/justifiedGallery.min.css" />
{% endif %}
4.3 开启fancybox
打开主题配置文件themes\next\_config.yml
,
找到fancybox:
设置为true
找到vendors:添加cdn地址
vendors:
fancybox: //cdn.jsdelivr.net/gh/zngw/cdn/fancybox/jquery.fancybox.min.js
fancybox_css: //cdn.jsdelivr.net/gh/zngw/cdn/fancybox/jquery.fancybox.min.css
5 修改CSS样式
找到themes\next\source\css\_custom\custom.styl
文件,在后面添加
.photo-box {
width: 100%;
max-width: 1040px;
margin: 0 auto;
text-align: center;
}
.photo-box-item {
overflow: hidden;
transition: .3s ease-in-out;
border-radius: 8px;
background-color: #ddd;
}
.photo-box-item-img {
}
.photo-box-item-img img {
transition: opacity 500ms ease-in
}
.photo-box-item .caption {
visibility: hidden;
position: absolute;
bottom: 0;
padding: 5px;
background-color: #000000;
left: 0;
right: 0;
margin: 0;
color: white;
font-size: 12px;
font-weight: 300;
font-family: sans-serif;
opacity: 0.7;
}
/* 鼠标移动上去后显示提示框 */
.photo-box-item:hover .caption {
visibility: visible;
}
.photo-box-item-click {
overflow:hidden;
}
.photo-box-item-click img {
obj-fit: cover;
transition: opacity 500ms ease-in;
}
.photo-category-card {
overflow: hidden;
transition: .3s ease-in-out;
border-radius: 8px;
background-color: #ddd;
}
.photo-category-card-img {
}
.photo-category-card-img img {
transition: opacity 500ms ease-in
}
.photo-category-card .caption {
visibility: hidden;
position: absolute;
bottom: 0;
padding: 5px;
background-color: #000000;
left: 0;
right: 0;
margin: 0;
color: white;
font-size: 12px;
font-weight: 300;
font-family: sans-serif;
opacity: 0.7;
}
/* 鼠标移动上去后显示提示框 */
.photo-category-card:hover .caption {
visibility: visible;
}
.photo-title-text {
line-height: 54px;
background-color: #479ac7;
color: white;
font-size: 24px;
}
.photo-title-desc {
line-height: 20px;
background-color: #479ac7;
color: white;
font-size: 12px;
}
.photo-back{
line-height: 34px;
margin-top: 10px;
position: absolute;
display: inline-block;
right:10px;
padding: 10px 10px;
border-radius: 4px;
background-color: #63b7ff;
color: #fff;
cursor: pointer;
}
.photo-back:hover{
background-color: #99c6ff;
}
.photo-category-text {
line-height: 30px;
background-color: #f1fafa;
color: #336699;;
font-size: 18px;
}
6. 部分图片禁用fancybox
相册目录中的图片需要点击事件,所以,这里的图片需要禁用fancybox。
加一个nofancybox
类标签,
修改一下theme\next\source\js\src\utils.js
文件
找到wrapImageWithFancyBox: function
函数,在循环里加一句if ($(this).hasClass('nofancybox')) return;
wrapImageWithFancyBox: function () {
$('.content img')
.not('[hidden]')
.not('.group-picture img, .post-gallery img')
.each(function () {
if ($(this).hasClass('nofancybox')) return; // 就加这一句就可以了
var $image = $(this);
var imageTitle = $image.attr('title');
var $imageWrapLink = $image.parent('a');
然后在用的时候,img标签中加入 ‘‘
7. 放入相册
在第一步创建好的 /source/photos/index.md
文档中编辑好自己需要的相册页面内容,在需要放置相册的位置加入以下内容即可
---
title: 相册
date: 2020-03-14 12:26:45
type: picture
---
<div class="photo-div"></div>
8. 编译发布
最后 hexo clean && hexo deploy -g
就可以把相册页设置好了
如果不想编译博客,可以用node script/photo.js
直接编译。然后的动将’/source/photos’目录下的所有图片文件夹、缩略图文件夹以及 photo.json数据文件直接上传就可以更新相册了。