Hexo中添加类QQ空间旅游相册

身为一个喜欢拍照的猿,博空中怎么可能少了相册呢。
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数据文件直接上传就可以更新相册了。

0%