Hexo Bamboo 主题添加相册
发表于:2026-06-10 | 分类: 网站建设
字数统计: 1.9k | 阅读时长: 10分钟 | 阅读量:

📘 Hexo Bamboo 主题添加相册

实现功能

✅ 自动扫描图片(含子文件夹)
✅ 自动读取照片真实拍摄时间(精确到秒)
时间线:年 → 月 → 日,日内按拍摄时间排序
✅ 分组视图(文件夹分组,可折叠)
顶部搜索框:关键词搜索分组 / 图片
✅ 双视图一键切换
✅ 瀑布流布局
✅ 点击预览大图
hexo g 自动扫描
✅ Bamboo 主题完美适配


一、安装依赖

1
npm install exifreader --save

二、目录结构

Text
1
2
3
4
5
6
7
8
9
博客根目录/
├ source/
│ └ photos/ # 相册目录
│ ├ index.md # 相册页面
│ └ 任意文件夹/
├ scripts/
│ └ auto-scan.js # 自动扫描钩子
├ scan-images.js # 扫描脚本


三、1. 自动扫描脚本(根目录:scan-images.js)

在blog目录下放scan-images.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
const fs = require('fs');
const path = require('path');
const ExifReader = require('exifreader');

const baseDir = path.join(__dirname, 'source/photos');
const outputFile = path.join(__dirname, 'source/photos/image-list.json');

const images = [];

function scan(dir) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const full = path.join(dir, file);
const stat = fs.statSync(full);

if (stat.isDirectory()) {
scan(full);
return;
}

if (/\.(jpg|jpeg|png|webp)$/i.test(file)) {
let rel = path.relative(baseDir, full).replace(/\\/g, '/');
let time = null;
let fullTime = null;
let group = '默认相册';

const parts = rel.split('/');
if (parts.length >= 2) group = parts[0];

try {
const buffer = fs.readFileSync(full);
const tags = ExifReader.load(buffer);
if (tags.DateTimeOriginal) {
let str = tags.DateTimeOriginal.description;
fullTime = str.replace(/:/g, '-');
time = str.replace(/:/g, '-').split(' ')[0];
}
} catch (e) {}

images.push({
src: rel,
time: time,
fullTime: fullTime,
group: group
});
}
});
}

scan(baseDir);
fs.writeFileSync(outputFile, JSON.stringify(images, null, 2), 'utf8');
console.log('✅ 扫描完成:' + images.length + ' 张图片');

三、2. 自动扫描钩子(scripts/auto-scan.js)

在blog下新建scripts目录,放入auto-scan-images.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { execSync } = require('child_process');
const path = require('path');
let scanned = false;

hexo.extend.filter.register('before_generate', function () {
if (scanned) return;
scanned = true;

if (this.env.mode === 'server') {
console.log('ℹ️ 开发模式,跳过扫描');
return;
}

try {
execSync('node scan-images.js', {
cwd: path.join(__dirname, '../'),
stdio: 'inherit'
});
} catch (e) {}
});

四、相册页面(source/photos/index.md)

创建相册

1
hexo new page photos

包含:搜索 + 双视图切换 + 时间到日 + 日内排序

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
---
title: 我的相册
layout: page
aside: false
comments: false
---

<!-- 搜索框 -->
<div style="text-align:center; margin:20px 0;">
<input id="searchInput" placeholder="搜索分组或图片..." style="width:90%;max-width:600px;padding:12px 16px;border-radius:12px;border:1px solid #ddd;font-size:1rem;">
</div>

<!-- 视图切换 -->
<div style="text-align:center; margin:20px 0;">
<button id="gview" style="padding:10px 24px; margin:0 8px; border:none; border-radius:8px; background:#4299e1; color:white; cursor:pointer;">分组视图</button>
<button id="tview" style="padding:10px 24px; margin:0 8px; border:none; border-radius:8px; background:#eee; cursor:pointer;">时间线视图</button>
</div>

<div id="galleryResult"></div>

<!-- 国内稳定CDN -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js"></script>

<style>
.album-group{margin:35px 0;}
.album-title{
font-size:1.3rem;
border-left:4px solid #4299e1;
padding-left:12px;
margin-bottom:16px;
font-weight:600;
cursor:pointer;
user-select:none;
display:flex;
justify-content:space-between;
align-items:center;
}
.album-title::after{content:"−";font-size:18px;margin-right:8px;}
.album-title.collapsed::after{content:"+";}
.waterfall{
display:flex;
flex-wrap:wrap;
gap:14px;
transition:all .3s;
}
.waterfall.hidden{display:none!important;}
.waterfall-item{
width:calc(33.333% - 10px);
border-radius:10px;
overflow:hidden;
background:#f5f5f5;
}
.waterfall-item img{
width:100%;
height:200px;
object-fit:cover;
transition:.3s;
background:#fff;
}
.waterfall-item img:hover{transform:scale(1.05);}

.timeline-box{
max-width:1000px;
margin:0 auto;
padding-left:26px;
border-left:3px solid #4299e1;
}
.timeline-year{
font-size:1.5rem;font-weight:bold;color:#4299e1;margin:36px 0 18px;
}
.timeline-day{
position:relative;
font-size:1.1rem;font-weight:600;
margin:22px 0 14px;
padding-left:18px;
cursor:pointer;
}
.timeline-day::before{
content:"";position:absolute;left:-32px;top:6px;
width:12px;height:12px;border-radius:50%;background:#4299e1;
}

@media(max-width:768px){
.waterfall-item{width:calc(50% - 7px);}
}
</style>

<script>
let allData = [];
let mode = 'group';
let searchTimer;

$(function(){
fetch('/photos/image-list.json')
.then(res => res.json())
.then(data => {
allData = data;
render();
});

// 视图切换按钮
$('#gview').click(function(){
mode = 'group';
$(this).css({'background':'#4299e1','color':'#fff'});
$('#tview').css({'background':'#eee','color':'#333'});
render();
});
$('#tview').click(function(){
mode = 'time';
$(this).css({'background':'#4299e1','color':'#fff'});
$('#gview').css({'background':'#eee','color':'#333'});
render();
});

$('#searchInput').on('input', function(){
clearTimeout(searchTimer);
searchTimer = setTimeout(render, 300);
});

function render(){
let kw = $('#searchInput').val().toLowerCase();
let list = allData.filter(it =>
it.group.toLowerCase().includes(kw) || it.src.toLowerCase().includes(kw)
);
$('#galleryResult').html(mode === 'group' ? renderGroup(list) : renderTime(list));
// 无需额外 fancybox 绑定
}

function renderGroup(list){
let groups = {};
list.forEach(it => {
let g = it.group;
if(!groups[g]) groups[g] = [];
groups[g].push('/photos/' + it.src);
});
let h = '';
for(let g in groups){
h += `<div class="album-group">`;
h += `<div class="album-title">${g}</div>`;
h += `<div class="waterfall">`;
groups[g].forEach(src => {
h += `<div class="waterfall-item"><img src="${src}" data-fancybox-src="${src}" class="no-lazy gallery-img" loading="lazy" alt=""></div>`;
});
h += `</div></div>`;
}
return h;
}

function renderTime(list){
let dayMap = {};
list.forEach(it => {
let d = it.time || '未知时间';
if(!dayMap[d]) dayMap[d] = [];
dayMap[d].push(it);
});
let days = Object.keys(dayMap).sort().reverse();
let h = '<div class="timeline-box">', lastYear = '';
days.forEach(day => {
let items = dayMap[day].sort((a,b) => (a.fullTime||'') > (b.fullTime||'') ? 1 : -1);
let y = day.substring(0,4);
if(y !== lastYear){
h += `<div class="timeline-year">📅 ${y} 年</div>`;
lastYear = y;
}
h += `<div class="timeline-day">${day}</div>`;
h += `<div class="waterfall">`;
items.forEach(it => {
let src = '/photos/' + it.src;
h += `<div class="waterfall-item"><img src="${src}" data-fancybox-src="${src}" class="no-lazy gallery-img" loading="lazy" alt=""></div>`;
});
h += `</div>`;
});
return h + '</div>';
}

// 自定义 fancybox 打开逻辑(解决冲突的关键)
$(document).on('click', '.gallery-img', function(e) {
let currentImages = [];
$('.waterfall:not(.hidden) .gallery-img').each(function() {
let src = $(this).data('fancybox-src');
if(src) currentImages.push({ src: src });
});
let clickedSrc = $(this).data('fancybox-src');
let index = currentImages.findIndex(img => img.src === clickedSrc);
if(index !== -1) {
$.fancybox.open(currentImages, { loop: true }, index);
}
e.preventDefault();
});

// 折叠/展开事件(保持不变)
$(document).on('click','.album-title,.timeline-day',function(){
$(this).toggleClass('collapsed');
$(this).next('.waterfall').toggleClass('hidden');
});
});
</script>

五、菜单配置(themes/bamboo/_config.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
menu:
Home:
url: /
icon: fas fa-home
Archives:
url: /archives
icon: fas fa-archive
Tags:
url: /tags
icon: fas fa-tags
Categories:
url: /categories
icon: fas fa-bookmark
相册:
url: /photos
icon: fas fa-camera-retro

六、使用方法

  1. 图片放入 source/photos/分组文件夹/

  2. 运行:

1
hexo clean && hexo g && hexo s
  1. 打开 http://localhost:4000/photos

七、所有要求已全部实现

时间线精确到【日】
日内按【时分秒】排序
image-list.json 增加 group 分组标签
搜索框:关键词实时筛选
✅ 双视图切换
✅ 折叠 / 展开
✅ 自动扫描
✅ 真实拍摄时间
✅ 瀑布流
✅ 预览大图
✅ 完美适配 Bamboo


加关注

关注公众号“生信之巅”。

生信之巅微信公众号 生信之巅小程序码
下一篇:
CentOS7.9 Ollama GPU 调用血泪史!从编译失败到 Docker 部署全攻略