简介
为Halo搭建的博客配上如同《新华字典》那样的目录是一个不错的主意,不仅能让分类更加清晰,还能帮助读者更轻松地查找和理解文章的内容。在这篇文章中,《Thymeleaf目录页原理》将深入探讨如何实现这种目录结构的设计,从基本原理开始,逐步深入到实际操作。
问题背景
在给定如下的关于CategoryVo
和PostVo
的方法和关系中,选择最为合适的算法与方案来实现一个文章分类目录表。与之相关的查找类为PostFinder
与CategoryFinder
(更详细的可以前往Halo文档进行查阅):
postFinder.listByCategory(page, size, categoryName);
- 根据分类标识和分页参数获取文章列表(文章 | Halo Documents)
categoryFinder.listAll()
- 获取所有文章分类(文章分类 | Halo Documents)
解决方案
因为Halo官方并不直接提供“获取分类下的所有文章”的相关方法,那么最好的解决方案就是三次for循环来拆解每个分类下的文章(在接下来的代码中每次都是结合了前面步骤中的完整代码)。
第一层for循环,遍历所有文章分类,并构建完整的Thymeleaf骨架
代码语言:javascript复制 <th:block
th:fragment="directory"
th:with="categories = ${categoryFinder.listAll()}">
<div class="categories-container">
<th:block th:each="category : ${categories}" th:if="${category.status.visiblePostCount > 0}">
<!-- ... -->
</th:block>
</div>
</th:block>
${categoryFinder.listAll()}
负责罗列出所有的分类分类并在循环遍历中将每个对象分配给category
对象- 同时为了让没有任何文章的分类不显示出来需要加上条件判断
th:if="${category.status.visiblePostCount > 0}"
- 这里的
th:each
是由Thymeleaf提供的一种For循环标签(可以对比到Vue中的v-for
)
第二层for循环,用来处理目录列分页 什么意思呢?如果不分页则会导致一列中出现大量元素标签,这使得同一行其他的文章数较少的列中会出现很大一片空白区域。
代码语言:javascript复制 <th:block
th:fragment="directory"
th:with="categories = ${categoryFinder.listAll()}">
<div class="categories-container">
<th:block th:each="category : ${categories}" th:if="${category.status.visiblePostCount > 0}">
<th:block th:each="i : ${#numbers.sequence(0, (category.status.visiblePostCount - 1) / site.post.postPageSize)}">
<!-- ... -->
</th:block>
</th:block>
</div>
</th:block>
${#numbers.sequence(0, (category.status.visiblePostCount - 1) / site.post.postPageSize)}
在有了当前分类下(已发布的)文章总数和每列最大显示文章数后,通过除法运算就可以得到这个分类一共需要多少列了${i}
变量i为迭代的列数,用来后面分页获取文章集合
第三层for循环,正式开始分页获取文章
代码语言:javascript复制 <th:block
th:fragment="directory"
th:with="categories = ${categoryFinder.listAll()}">
<div class="categories-container">
<th:block th:each="category : ${categories}" th:if="${category.status.visiblePostCount > 0}">
<th:block th:each="i : ${#numbers.sequence(0, (category.status.visiblePostCount - 1) / site.post.postPageSize)}">
<div class="category" th:with="posts = ${postFinder.listByCategory(i 1, site.post.postPageSize, category.metadata.name)}" th:attr="data-collection=${category.spec.displayName}">
<h2 th:if="${i == 0}" class="category-title" th:text="${category.spec.displayName}"></h2>
<div th:if="${i != 0}" class="category-block"></div>
<ol class="posts-container" th:attr="data-collection=${category.spec.displayName}">
<li th:each="post,it : ${posts}" th:attr="data-order=${i * site.post.postPageSize it.index 1}">
<a th:href="@{${post.status.permalink}}" class="post" th:text="${post.spec.title}"></a>
</li>
</ol>
</div>
</th:block>
</th:block>
</div>
</th:block>
- {i 1}上一次的for循环中的变量{i}指代了页码由于是从0开始的所以页码需要加1处理
- {postFinder.listByCategory(i 1, site.post.postPageSize, category.metadata.name)}这个方法获取了category.metadata.name分类中第{i 1}页的site.post.postPageSize条数据,并另外存储为
data-collection=${category.spec.displayName}
这里使用category.spec.displayName
来标记元素标签,在后面它可以帮助我们来对这些目录进行首字母排序th:each="post,it : ${posts}"
这里就是遍历posts
中的所有文章了,这些结果会逐个存储到<li>
元素标签对中- th:if="
样式处理
在前面的解决方案中我们提到了一个关于样式的问题:如果不分页则会导致一列中出现大量元素标签,这使得同一行其他的文章数较少的列中会出现很大一片空白区域。同样的我们还需要来解决不同屏幕尺寸下一行只显示1个<div class="category">
、以及<div class="category-block">
的对齐作用。在明确了需求后就可以开始写入css样式了:
/****** category start ******/
.categories-container {display: flex; flex-wrap: wrap;}
.categories-container .category {
margin: 0;
padding: 10px;
max-width: calc(50% - 20px); /* 去除padding的影响 */
flex: 1 1 calc(100% - 20px); /* 去除padding的影响 */
}
/* 让目录的头部对齐 */
.categories-container .category .category-block {
width: 100%;
height: 27.59px;
margin-top: 20px;
margin-bottom: 10px;
}
.categories-container .category .category-title {
font-weight: normal;
color: #bbbbbb;
margin-top: 20px;
margin-bottom: 10px;
}
.categories-container .category .posts-container {
display: flex; /* 使用flex弹性布局 */
flex-direction: column;
padding-left: 0px;
margin: 0px;
}
.categories-container .category .posts-container li {
display: flex;
align-items: flex-start;
list-style-type: none;
margin-bottom: 7px;
}
/* before伪元素实现有序列表 */
.categories-container .category .posts-container li:before {
padding: 4px 0;
content: attr(data-order) ". "; /* 有序列表的标号来源 */
color: #bbbbbb;
counter-increment: li;
margin-right: 5px;
}
.categories-container .category .posts-container a {
padding: 4px 0;
border-radius: 4px;
}
.categories-container .category .posts-container a:hover {
color: #fff;
padding: 4px 6px;
background-color: #0084FF;
box-shadow: 0 8px 12px -3px #0084FF23;
}
.categories-container .category .posts-container .post {flex: 1;}
.categories-container .category .posts-container .post {margin: 0;}
/* 屏幕尺寸大于等于1600px */
@media screen and (min-width: 1600px) {
.categories-container .category {
max-width: calc(100% / 3 - 20px); /* 占用1/3的宽度 */
flex: 1 1 calc(100% / 3 - 20px); /* 占用1/3的宽度 */
}
}
/* 屏幕尺寸在750px到1600px之间 */
@media screen and (min-width: 751px) and (max-width: 1599px) {
.categories-container .category {
max-width: calc(50% - 20px); /* 占用1/2的宽度 */
flex: 1 1 calc(50% - 20px); /* 占用1/2的宽度 */
}
}
/* 屏幕尺寸小于等于750px */
@media screen and (max-width: 750px) {
/* 占用100%的宽度 */
.categories-container .category {flex: 1 1 calc(100% - 20px);}
.categories-container .category .category-block {
display: none;
}
}
/****** category end ******/
排序函数
在样式处理的.categories-container .category .posts-container li:before
的class中,使用了content: attr(data-order) ". ";
,这一段的作用是让before伪元素使用data-order
属性的值来进行头部内容。那么我们就需要为一个分类下的所有文章进行这个属性的编号。
同时为了方便读者或博客博主能更快速的查找到分类,需要引入更有效的首字母排序功能,让英文与中文部分都分开按照A-Za-z0-9
的顺序进行排序并重新组合。
(function() {
window.onload = function() {
var { pinyin } = pinyinPro;
var container = document.querySelector('.categories-container');
if (container !== null) {
var categories = Array.from(container.children);
// 对categories中的元素进行排序
categories.sort(function(a, b) {
var displayNameA = a.querySelector('.posts-container').dataset.collection;
var displayNameB = b.querySelector('.posts-container').dataset.collection;
// 把"其它"分类移至最后
if (displayNameA === "其它") return 1;
if (displayNameB === "其它") return -1;
var isLetterA = isLetterOrNumber(displayNameA.charAt(0));
var isLetterB = isLetterOrNumber(displayNameB.charAt(0));
// 如果 displayNameA 的首字符是字母或数字,但 displayNameB 的首字符不是,则 displayNameA 排在前面
if (isLetterA && !isLetterB) return -1;
// 如果 displayNameB 的首字符是字母或数字,但 displayNameA 的首字符不是,则 displayNameB 排在前面
if (isLetterB && !isLetterA) return 1;
// 如果 displayNameA 或 displayNameB 不满足全是英文字母的正则表达式,则将其转换为拼音
if (!isAllLetterOrNumber(displayNameA)) displayNameA = pinyin(displayNameA, { toneType: 'none' }).replaceAll(" ", "");
if (!isAllLetterOrNumber(displayNameB)) displayNameB = pinyin(displayNameB, { toneType: 'none' }).replaceAll(" ", "");
// 最后按照字母和数字的顺序排序进行组合
return displayNameA.localeCompare(displayNameB);
});
categories.forEach(function(category) {
container.appendChild(category);
});
// 定义一个函数,判断一个字符是否为字母或数字
function isLetterOrNumber(char) {
return /^[a-zA-Z0-9]$/.test(char);
}
// 定义一个函数,判断一个字符串是否全是英文字母或数字
function isAllLetterOrNumber(str) {
return /^[a-zA-Z0-9] $/.test(str);
}
}
}
})();
在这个函数中,使用了一个第三方库pinyin-pro
,开发者在尝试时可以在script标签中引入这个CDN库https://cdn.jsdelivr.net/gh/zh-lx/pinyin-pro@latest/dist/pinyin-pro.js
。
细节处理
到这里其实主要的功能都已经实现完成了,但是在样式处理中当屏幕尺寸在750px以下后,每个<div class="category-block">
之间仍然存在20px的padding,既然css已经无法解决,那么仍然需要引入JavaScript来动态控制这些padding:
(function() {
window.addEventListener('resize', adjustPadding);
function adjustPadding() {
let adjustPadding = window.innerWidth < 750 ? "0" : "10px";
// 以data-collection值为键将元素分组
const collectionGroups = {};
document.querySelectorAll('.category').forEach(el => {
const collection = el.getAttribute('data-collection');
if(!collectionGroups[collection]) {
collectionGroups[collection] = [];
}
collectionGroups[collection].push(el);
});
// 遍历每个分组并调整padding
for(let collection in collectionGroups) {
collectionGroups[collection].forEach((el, index, array) => {
if(index === 0) {
el.style.paddingBottom = adjustPadding;
} else if(index === array.length - 1) {
el.style.paddingTop = adjustPadding;
} else {
el.style.paddingTop = adjustPadding;
el.style.paddingBottom = adjustPadding;
}
});
}
}
// 页面加载完后就调用一次来适应padding
adjustPadding();
})();
注意事项
至此,这个可排序的目录功能就完成了。值得读者注意的是,每列的文章数这里是直接取了Halo的全局变量site.post.postPageSize
,读者可以将其进行扩展到其他变量中,具体请参考:全局变量 | Halo Documents