Thymeleaf目录页原理 发布于

2023-10-21 11:40:41 浏览数 (3)

简介

为Halo搭建的博客配上如同《新华字典》那样的目录是一个不错的主意,不仅能让分类更加清晰,还能帮助读者更轻松地查找和理解文章的内容。在这篇文章中,《Thymeleaf目录页原理》将深入探讨如何实现这种目录结构的设计,从基本原理开始,逐步深入到实际操作。

问题背景

在给定如下的关于CategoryVoPostVo的方法和关系中,选择最为合适的算法与方案来实现一个文章分类目录表。与之相关的查找类为PostFinderCategoryFinder(更详细的可以前往Halo文档进行查阅):

  1. postFinder.listByCategory(page, size, categoryName);
    • 根据分类标识和分页参数获取文章列表(文章 | Halo Documents)
  2. 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样式了:

代码语言:javascript复制
/****** 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的顺序进行排序并重新组合。

代码语言:javascript复制
(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:

代码语言:javascript复制
(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

1 人点赞