feat: 图库相册前端切换/封面下拉选择,修复 /tags 白屏(v0.3.0)

图库页:
- 相册切换改为前端按 URL 的 group 参数完成(各相册瀑布流预渲染并标记 data-album),
  不再依赖服务端的 group/photos 变量,从而不受页面静态缓存与插件版本差异影响,
  彻底修复「点击相册后仍显示 /photos」的问题
- 移除已无用的分组 tag 切换器(全部相册 / 各分组计数)
- 封面固定 4:3 并 object-fit: cover 填满;相册内视图新增「返回全部相册」与相册标题
- 预览模式开关改为作用于全部相册瀑布流,并尊重全局灯箱开关

后台设置:
- 相册封面配置的「相册名称」文本框改为下拉选择,选项通过
  /apis/console.api.photo.halo.run/v1alpha1/photogroups 自动获取已存在分组
  (label=spec.displayName,value=metadata.name);模板匹配同步改为 metadata.name

修复:
- /tags 白屏:tags.html 在 ${...} 内嵌套 #{'tag.count'} 触发 EL1043E
  (Expected 'identifier' but was 'lcurly({)'),改用 |...| 字面量替换

其他:新增 i18n photos.back;版本升至 0.3.0;README 同步

Co-Authored-By: WorkBuddy <noreply@workbuddy.ai>
This commit is contained in:
2026-09-30 11:03:33 +08:00
co-authored by WorkBuddy
parent 0e87b3011e
commit 5e50c01c18
7 changed files with 213 additions and 232 deletions
+19 -18
View File
@@ -3,7 +3,6 @@
一个移植自 Typecho 主题 [whitespace 留白](https://kehua.me) 的 [Halo](https://www.halo.run/) 主题,主打极简留白风格与清晰的排版,并为多款常用插件提供开箱即用的适配页面。
> 本项目由 AI 开发完成,人工仅进行部分功能测试及问题找寻。
> 源码仓库:<https://git.nxxy335.top/nxxy335/halo-theme-whitespace>
## 环境要求
@@ -21,16 +20,16 @@
## 已适配插件
| 插件名称 | 插件标识 | 适配页面路由 | 说明 |
| --- | --- | --- | --- |
| [瞬间](https://github.com/halo-sigs/plugin-moments) | `plugin-moments` | `/moments` | 瞬间动态列表页,含 `/moments/{name}` 详情页(完整内容、媒体、标签、作者信息、评论区) |
| [图库](https://github.com/halo-sigs/plugin-photos) | `plugin-photos` | `/photos` | 图片展示页面,按相册分组、封面自动选取、瀑布流 + 预览模式 |
| [重要日子](https://github.com/yyliucha/plugin-important-dates) | `plugin-important-dates` | `/important-dates` | 即将到来提醒、全部日期倒计时、重要标记、人员生日 |
| [友情链接](https://github.com/halo-sigs/plugin-links) | `plugin-links` | `/links` | 友链展示页,按分组显示标题、卡片统一高度;内嵌申请表单 |
| [链接订阅](https://github.com/halo-sigs/plugin-links) | `plugin-links` | `/subscriptions` | RSS 订阅动态聚合,作者链接来源博客、摘要折叠 5 行 |
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页,按「读过 / 在读 / 看过 / 在追」分栏展示书影音 |
| [微信读书](https://github.com/yeora26/halo-weread-plugin) | `halo-weread-plugin` | 自定义页面 | 微信读书书架展示,访客免登录查看 |
| [API 扩展包](https://github.com/HowieHz/halo-plugin-extra-api) | `extra-api` | 全站 | 服务端字数统计、代码高亮、全站/单条字数展示 |
| 插件名称 | 插件标识 | 适配页面路由 | 说明 |
| ----------------------------------------------------------- | ------------------------ | ------------------ | ---------------------------------------------------- |
| [瞬间](https://github.com/halo-sigs/plugin-moments) | `plugin-moments` | `/moments` | 瞬间动态列表页,含 `/moments/{name}` 详情页(完整内容、媒体、标签、作者信息、评论区) |
| [图库](https://github.com/halo-sigs/plugin-photos) | `plugin-photos` | `/photos` | 图片展示页面,按相册分组、封面自动选取、瀑布流 + 预览模式 |
| [重要日子](https://github.com/yyliucha/plugin-important-dates) | `plugin-important-dates` | `/important-dates` | 即将到来提醒、全部日期倒计时、重要标记、人员生日 |
| [友情链接](https://github.com/halo-sigs/plugin-links) | `plugin-links` | `/links` | 友链展示页,按分组显示标题、卡片统一高度;内嵌申请表单 |
| [链接订阅](https://github.com/halo-sigs/plugin-links) | `plugin-links` | `/subscriptions` | RSS 订阅动态聚合,作者链接来源博客、摘要折叠 5 行 |
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页,按「读过 / 在读 / 看过 / 在追」分栏展示书影音 |
| [微信读书](https://github.com/yeora26/halo-weread-plugin) | `halo-weread-plugin` | 自定义页面 | 微信读书书架展示,访客免登录查看 |
| [API 扩展包](https://github.com/HowieHz/halo-plugin-extra-api) | `extra-api` | 全站 | 服务端字数统计、代码高亮、全站/单条字数展示 |
> **注意**:插件需单独安装,安装后主题自动适配对应页面。若未安装某插件,对应页面或功能自动隐藏 / 降级,不会报错。
> 豆瓣插件建议 **v1.2.6+**(SQLite 数据结构)。
@@ -39,12 +38,12 @@
在 Halo 后台「页面 → 新建 → 自定义模板」中选择使用:
| 模板名 | 模板文件 | 说明 |
| --- | --- | --- |
| 关于 | `page_about.html` | 关于页,头像 / 简介 / 社交链接可配置 |
| 留言板 | `page_guestbook.html` | 以评论区作为留言板的单页 |
| 微信读书 | `weread.html` | 微信读书书架展示,依赖微信读书插件 |
| 豆瓣页面 | `douban.html` | 豆瓣记录页,依赖豆瓣插件 |
| 模板名 | 模板文件 | 说明 |
| ---- | --------------------- | --------------------- |
| 关于 | `page_about.html` | 关于页,头像 / 简介 / 社交链接可配置 |
| 留言板 | `page_guestbook.html` | 以评论区作为留言板的单页 |
| 微信读书 | `weread.html` | 微信读书书架展示,依赖微信读书插件 |
| 豆瓣页面 | `douban.html` | 豆瓣记录页,依赖豆瓣插件 |
## 主要功能
@@ -71,8 +70,10 @@
### 图库
- 首屏按相册分组展示,封面默认自动取相册内第一张图(分组无图时回退取首图),可在后台为每个相册手动配置封面
- 首屏按相册网格展示,封面 4:3 自适应填满,默认自动取相册内第一张图(分组无图时回退取首图)
- 后台可为每个相册手动指定封面:「相册」直接从图库插件**已存在的分组中下拉选择**,无需手输名称
- 点击相册进入 `?group=photo-group-*` 瀑布流视图(一行三张、页面加宽)
- 相册切换由前端按 URL 上的 `group` 参数完成,不依赖服务端变量,因此**不受页面静态缓存影响**
- 预览模式:默认值可在设置中配置(默认开),访客可在前台自行切换(开 → 点击图片灯箱放大;关 → 点击进入图片详情)
### 瞬间
+1
View File
@@ -48,5 +48,6 @@ stats.visits={0} visits
stats.words={0} words
douban.plugin_required=Douban plugin (plugin-douban) not detected. Please install and enable it, then set your Douban ID in the plugin settings.
photos.photo_count={0} photos
photos.back=Back to all albums
moment.back_to_list=Back to moments
moment.tags=Tags
+1
View File
@@ -48,5 +48,6 @@ stats.visits={0} 次访问
stats.words={0} 字
douban.plugin_required=未检测到豆瓣插件(plugin-douban),请安装并启用后在插件设置中填写豆瓣 ID。
photos.photo_count={0} 张照片
photos.back=返回全部相册
moment.back_to_list=返回瞬间列表
moment.tags=标签
+12 -3
View File
@@ -170,12 +170,21 @@ spec:
name: album_covers
label: 相册封面
value: []
help: 为相册手动指定封面;不配置时自动使用相册内第一张图片。「相册名称」填写图库页面显示的相册名
help: 为相册手动指定封面;不配置时自动使用相册内第一张图片。「相册」直接从图库插件已存在的分组中选择
children:
- $formkit: text
- $formkit: select
name: group
label: 相册名称
label: 相册
validation: required
clearable: true
searchable: true
action: /apis/console.api.photo.halo.run/v1alpha1/photogroups
requestOption:
method: GET
itemsField: ""
labelField: spec.displayName
valueField: metadata.name
help: 选项自动取自图库插件中已创建的相册分组(需安装并启用图库插件)
- $formkit: attachment
name: cover
label: 封面图
+177 -208
View File
@@ -6,30 +6,27 @@
.container {
max-width: 75rem;
}
:where(.photos-groups) {
margin: 1.5rem 0;
}
:where(.photos-groups__list, .photos-grid, .photos-card__tags) {
margin: 0;
padding: 0;
list-style: none;
}
:where(.photos-groups__list, .photos-card__tags) {
/* 相册内视图工具条 */
:where(.photos-toolbar) {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1rem;
}
:where(.photos-group__link) {
display: inline-flex;
gap: 0.25rem;
align-items: baseline;
:where(.photos-back) {
color: var(--color-link);
font-size: 0.9rem;
}
:where(.photos-group__link):hover {
:where(.photos-back):hover {
color: var(--color-link-hover);
}
:where(.photos-group.is-active .photos-group__link) {
:where(.photos-album-title) {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
flex: 1 1 auto;
}
/* 相册网格 */
@@ -44,6 +41,20 @@
);
gap: 1.5rem;
}
/* 打开某个相册后:只显示该相册,卡片收起、瀑布流展开 */
:where(.albums-grid.is-album-open) {
display: block;
}
:where(.albums-grid.is-album-open .albums-item) {
display: none;
}
:where(.albums-grid.is-album-open .albums-item.is-active) {
display: block;
}
:where(.albums-item.is-active .albums-card) {
display: none;
}
:where(.albums-card) {
display: block;
color: var(--color-text);
@@ -103,6 +114,9 @@
columns: 3;
column-gap: 1rem;
}
:where(.photos-masonry[hidden]) {
display: none;
}
:where(.photos-masonry > li) {
break-inside: avoid;
margin: 0 0 1rem;
@@ -137,11 +151,6 @@
}
/* 预览模式切换 */
:where(.photos-toolbar) {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
}
:where(.photos-preview-toggle) {
padding: 0.4rem 0.9rem;
border: 1px solid var(--color-border);
@@ -161,13 +170,6 @@
color: var(--color-text-muted);
padding: 2rem 0;
}
:where(.photos-pagination) {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 2rem;
}
</style>
</template>
@@ -177,203 +179,167 @@
<h1 class="photos-header__title" th:text="${title}">图库</h1>
</header>
<nav class="photos-groups" aria-label="照片分组">
<ul class="photos-groups__list">
<li
class="photos-group"
th:classappend="${#strings.isEmpty(group)} ? 'is-active' : ''"
>
<a
class="photos-group__link"
th:href="${photoUrl.list()}"
th:attr="aria-current=${#strings.isEmpty(group) ? 'page' : null}"
>全部相册</a
>
</li>
<li
class="photos-group"
th:each="photoGroup : ${groups}"
th:classappend="${group == photoGroup.metadata.name} ? 'is-active' : ''"
>
<a
class="photos-group__link"
th:href="${photoUrl.list(photoGroup.metadata.name)}"
th:attr="aria-current=${group == photoGroup.metadata.name ? 'page' : null}"
>
<span
class="photos-group__name"
th:text="${photoGroup.spec.displayName}"
>相册</span
>
<span
class="photos-group__count"
th:if="${photoGroup.status != null and photoGroup.status.photoCount != null}"
th:text="|(${photoGroup.status.photoCount})|"
></span>
</a>
</li>
</ul>
</nav>
<!-- 相册内视图工具条:打开具体相册时显示 -->
<div class="photos-toolbar" id="photos-toolbar" hidden>
<a
class="photos-back"
th:href="${photoUrl != null ? photoUrl.list() : '/photos'}"
th:text="#{photos.back}"
>返回全部相册</a
>
<h2 class="photos-album-title" id="photos-album-title"></h2>
<button
class="photos-preview-toggle"
id="photos-preview-toggle"
type="button"
th:attr="data-default=${theme.config?.photos?.preview_default != false ? 'on' : 'off'},data-global=${theme.config?.basic?.enable_lightbox != false ? '1' : '0'}"
>
预览模式
</button>
</div>
<!-- 相册视图:按分组展示各相册 -->
<th:block th:if="${#strings.isEmpty(group)}">
<th:block th:with="albums=${photoFinder.groupBy()}">
<ul class="albums-grid" th:if="${not #lists.isEmpty(albums)}">
<li th:each="album : ${albums}">
<a
class="albums-card"
th:href="${photoUrl.list(album.metadata.name)}"
th:aria-label="|查看相册 ${album.spec.displayName}|"
th:title="|查看相册 ${album.spec.displayName}|"
<th:block th:with="albums=${photoFinder.groupBy()}">
<ul
class="albums-grid"
id="albums-grid"
th:if="${not #lists.isEmpty(albums)}"
>
<li
class="albums-item"
th:each="album : ${albums}"
th:attr="data-album=${album.metadata.name}"
th:data-title="${album.spec.displayName}"
>
<th:block th:with="rawPhotos=${album.photos}">
<th:block
th:with="albumPhotos=${(rawPhotos != null and !#lists.isEmpty(rawPhotos)) ? rawPhotos : photoFinder.listBy(album.metadata.name)}"
>
<span class="albums-card__cover">
<th:block th:with="rawPhotos=${album.photos}">
<a
class="albums-card"
th:href="${photoUrl.list(album.metadata.name)}"
th:aria-label="|查看相册 ${album.spec.displayName}|"
th:title="|查看相册 ${album.spec.displayName}|"
>
<span class="albums-card__cover">
<img
th:if="${rawPhotos != null and !#lists.isEmpty(rawPhotos)}"
th:src="${rawPhotos[0].spec.cover != null and !#strings.isEmpty(rawPhotos[0].spec.cover) ? rawPhotos[0].spec.cover : rawPhotos[0].spec.url}"
th:if="${not #lists.isEmpty(albumPhotos)}"
th:src="${albumPhotos[0].spec.cover != null and !#strings.isEmpty(albumPhotos[0].spec.cover) ? albumPhotos[0].spec.cover : albumPhotos[0].spec.url}"
th:alt="${album.spec.displayName}"
loading="lazy"
/>
<th:block
th:unless="${rawPhotos != null and !#lists.isEmpty(rawPhotos)}"
th:if="${not #lists.isEmpty(theme.config?.photos?.album_covers)}"
>
<th:block
th:with="fbPhotos=${photoFinder.listBy(album.metadata.name)}"
>
<img
th:if="${not #lists.isEmpty(fbPhotos)}"
th:src="${fbPhotos[0].spec.cover != null and !#strings.isEmpty(fbPhotos[0].spec.cover) ? fbPhotos[0].spec.cover : fbPhotos[0].spec.url}"
th:alt="${album.spec.displayName}"
loading="lazy"
/>
</th:block>
<img
class="album-cover-custom"
th:each="cv : ${theme.config.photos.album_covers}"
th:if="${cv != null and cv.group == album.metadata.name and not #strings.isEmpty(cv.cover)}"
th:src="${cv.cover}"
th:alt="${album.spec.displayName}"
loading="lazy"
/>
</th:block>
</th:block>
<th:block
th:if="${not #lists.isEmpty(theme.config?.photos?.album_covers)}"
</span>
<p class="albums-card__name" th:text="${album.spec.displayName}">
相册
</p>
<p
class="albums-card__count"
th:if="${album.status != null and album.status.photoCount != null}"
th:text="#{photos.photo_count(${album.status.photoCount})}"
>
<img
class="album-cover-custom"
th:each="cv : ${theme.config.photos.album_covers}"
th:if="${cv != null and cv.group == album.spec.displayName and not #strings.isEmpty(cv.cover)}"
th:src="${cv.cover}"
th:alt="${album.spec.displayName}"
loading="lazy"
/>
</th:block>
</span>
<p
class="albums-card__name"
th:text="${album.spec.displayName}"
>
相册
</p>
<p
class="albums-card__count"
th:if="${album.status != null and album.status.photoCount != null}"
th:text="#{photos.photo_count(${album.status.photoCount})}"
>
12 张照片
</p>
<span class="albums-card__more">查看相册 →</span>
</a>
</li>
</ul>
<p class="photos-empty" th:if="${#lists.isEmpty(albums)}" role="status">
暂无相册
</p>
</th:block>
</th:block>
<!-- 相册内瀑布流视图 -->
<th:block th:unless="${#strings.isEmpty(group)}">
<th:block
th:with="hasItems=${photos != null and photos.items != null and !#lists.isEmpty(photos.items)}"
>
<th:block
th:with="groupPhotos=${hasItems ? photos.items : photoFinder.listBy(group)}"
>
<div class="photos-toolbar" th:if="${not #lists.isEmpty(groupPhotos)}">
<button
class="photos-preview-toggle"
id="photos-preview-toggle"
type="button"
th:attr="data-default=${theme.config?.photos?.preview_default != false ? 'on' : 'off'}"
>
预览模式
</button>
</div>
<ul
class="photos-masonry"
th:if="${not #lists.isEmpty(groupPhotos)}"
th:attr="data-lightbox=${theme.config?.basic?.enable_lightbox != false ? 'true' : null}"
>
<li th:each="photo : ${groupPhotos}">
<a
class="photos-card__media"
th:href="${photoUrl.detail(photo)}"
th:title="${photo.spec.displayName}"
>
<img
th:src="${#strings.isEmpty(photo.spec.cover) ? photo.spec.url : photo.spec.cover}"
th:alt="${photo.spec.displayName}"
loading="lazy"
/>
12 张照片
</p>
<span class="albums-card__more">查看相册 →</span>
</a>
<p
class="photos-card__caption"
th:if="${not #strings.isEmpty(photo.spec.displayName)}"
th:text="${photo.spec.displayName}"
>
照片
</p>
</li>
</ul>
<p
class="photos-empty"
th:if="${#lists.isEmpty(groupPhotos)}"
role="status"
>
暂无照片
</p>
</th:block>
</th:block>
<nav
class="photos-pagination"
th:if="${photos != null and (photos.hasPrevious() or photos.hasNext())}"
aria-label="分页"
>
<a
class="photos-pagination__previous"
th:if="${photos.hasPrevious()}"
th:href="${photos.prevUrl}"
>上一页</a
>
<span class="photos-pagination__status">
第 <strong th:text="${photos.page}">1</strong> 页,共
<strong th:text="${photos.totalPages}">1</strong> 页
</span>
<a
class="photos-pagination__next"
th:if="${photos.hasNext()}"
th:href="${photos.nextUrl}"
>下一页</a
>
</nav>
<!-- 该相册的照片瀑布流:默认隐藏,由脚本按 URL 的 group 显示 -->
<ul
class="photos-masonry"
hidden
th:attr="data-lightbox=${theme.config?.basic?.enable_lightbox != false ? 'true' : null}"
>
<li th:each="photo : ${albumPhotos}">
<a
class="photos-card__media"
th:href="${photoUrl.detail(photo)}"
th:title="${photo.spec.displayName}"
>
<img
th:src="${#strings.isEmpty(photo.spec.cover) ? photo.spec.url : photo.spec.cover}"
th:alt="${photo.spec.displayName}"
loading="lazy"
/>
</a>
<p
class="photos-card__caption"
th:if="${not #strings.isEmpty(photo.spec.displayName)}"
th:text="${photo.spec.displayName}"
>
照片
</p>
</li>
</ul>
</th:block>
</th:block>
</li>
</ul>
<p class="photos-empty" th:if="${#lists.isEmpty(albums)}" role="status">
暂无相册
</p>
</th:block>
</section>
</div>
<script th:inline="none">
(function () {
var grid = document.getElementById("albums-grid");
if (!grid) return;
var toolbar = document.getElementById("photos-toolbar");
var titleEl = document.getElementById("photos-album-title");
// 按 URL 上的 group 参数决定显示哪个相册
//(不依赖服务端 group 变量,避免页面缓存 / 插件版本差异导致打不开相册)
var group = "";
try {
group = new URLSearchParams(window.location.search).get("group") || "";
} catch (e) {
group = "";
}
var items = grid.querySelectorAll(".albums-item");
var opened = false;
Array.prototype.forEach.call(items, function (item) {
var masonry = item.querySelector(".photos-masonry");
var isMatch = group !== "" && item.getAttribute("data-album") === group;
if (isMatch) {
item.classList.add("is-active");
if (masonry) masonry.removeAttribute("hidden");
if (titleEl) titleEl.textContent = item.getAttribute("data-title") || "";
opened = true;
} else {
item.classList.remove("is-active");
if (masonry) masonry.setAttribute("hidden", "");
}
});
if (opened) {
grid.classList.add("is-album-open");
if (toolbar) toolbar.removeAttribute("hidden");
} else {
grid.classList.remove("is-album-open");
if (toolbar) toolbar.setAttribute("hidden", "");
}
// 预览模式(作用于全部相册瀑布流)
var btn = document.getElementById("photos-preview-toggle");
var masonry = document.querySelector(".photos-masonry");
if (!btn || !masonry) return;
if (!btn) return;
var KEY = "whitespace-photo-preview";
var defaultOn = btn.getAttribute("data-default") === "on";
var globalOn = btn.getAttribute("data-global") === "1";
var stored = null;
try {
stored = localStorage.getItem(KEY);
@@ -383,11 +349,14 @@
var mode = stored === null ? defaultOn : stored === "on";
function apply() {
if (mode) {
masonry.setAttribute("data-lightbox", "true");
} else {
masonry.removeAttribute("data-lightbox");
}
var all = document.querySelectorAll(".photos-masonry");
Array.prototype.forEach.call(all, function (m) {
if (globalOn && mode) {
m.setAttribute("data-lightbox", "true");
} else {
m.removeAttribute("data-lightbox");
}
});
btn.textContent = mode ? "预览模式:开" : "预览模式:关";
btn.setAttribute("aria-pressed", mode ? "true" : "false");
}
@@ -399,7 +368,7 @@
} catch (e) {
/* ignore */
}
// 重新加载以按新模式初始化灯箱
// 重新加载,使灯箱按新模式初始化
window.location.reload();
});
+1 -1
View File
@@ -19,7 +19,7 @@
th:href="@{${tag.status.permalink}}"
class="tag"
th:text="${tag.spec.displayName}"
th:title="${tag.postCount != null ? tag.postCount + ' ' + #{'tag.count'} : ''}"
th:title="|${tag.postCount} #{tag.count}|"
></a>
</div>
+1 -1
View File
@@ -14,7 +14,7 @@ spec:
issues: https://git.nxxy335.top/nxxy335/halo-theme-whitespace/issues
settingName: "whitespace-setting"
configMapName: "whitespace-configMap"
version: 0.2.0
version: 0.3.0
requires: ">=2.26.0"
license:
- name: "GPL-3.0"