Chic主题分类页面优化

Chic主题默认情况是将所有分类依次展示,但Hexo本身是有父子分类之区分的。
例如,在front-matter中指定:

1
2
3
categories: 
- 摸鱼的间隙总还是要工作
- 数据审阅

则”数据审阅”应为”摸鱼的间隙总还是要工作”的子分类。
但Chic将所有分类同一处理,实际展示为下图:

即,摸鱼的间隙总还是要工作分类下直接展示该分类的所有文章,并且还有一个独立的数据审阅分类下展示该分类的文章。这显然与我的期望不一致。

期望目标

在Categories页面展示时,若仅存在父分类,则展示该分类下文章。若父分类下存在子分类,则展示子分类名称,点击子分类跳转至该子分类页面。

步骤:修改 themes/Chic/layout/category.ejs 文件

由于category_item.path会指示该分类的路径,进而以此得到其分类树情况:

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
let categoryHierarchy = {};

site.categories.forEach(category_item => {
let path = category_item.path;
let pathParts = path.split('/').filter(part => part !== '');

pathParts = pathParts.map(part => decodeURIComponent(part));

if (pathParts[0] === 'categories') {
pathParts = pathParts.slice(1);
}

if (pathParts.length === 1) {
let parentName = pathParts[0];
if (!categoryHierarchy[parentName]) {
categoryHierarchy[parentName] = {
name: parentName,
path: category_item.path,
posts: category_item.posts,
children: {}
};
}
} else if (pathParts.length === 2) {
let parentName = pathParts[0];
let childName = pathParts[1];

if (!categoryHierarchy[parentName]) {
categoryHierarchy[parentName] = {
name: parentName,
path: null,
posts: [],
children: {}
};
}

categoryHierarchy[parentName].children[childName] = {
name: childName,
path: category_item.path,
posts: category_item.posts
};
}
});

随后,在显示时,若仅有父分类,则直接显示文章,否则显示其子分类列表:

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
<% Object.keys(categoryHierarchy).forEach(parentKey => { %>
<% let parentCategory = categoryHierarchy[parentKey]; %>
<div class="card-item">
<div class="categories">
<h3>
<i class="iconfont icon-category" style="padding-right:3px"></i>
<%- parentCategory.name %>
</h3>

<% if (Object.keys(parentCategory.children).length > 0) { %>
<div class="subcategories" style="margin-left: 15px; margin-top: 10px;">
<% Object.keys(parentCategory.children).forEach(childKey => { %>
<% let childCategory = parentCategory.children[childKey]; %>
<div class="subcategory-item" style="margin-bottom: 8px;">
<a href="<%- url_for(childCategory.path) %>" style="font-size: 0.9em; color: #666;">
<i class="iconfont icon-category" style="padding-right:3px; font-size: 0.8em;"></i>
<%- childCategory.name %> (<%- childCategory.posts.length %>)
</a>
</div>
<% }) %>
</div>
<% } else { %>
<% parentCategory.posts.forEach((post_item, index = 0)=>{ %>
<% if(++index <= 5){ %>
<article class="archive-item">
<a class="archive-item-link"
href="<%- url_for(post_item.path) %>"><%- post_item.title %></a>
</article>
<% } %>
<% }) %>
<% if(parentCategory.posts.length > 5){ %>
<a class="more-post-link" href="<%- url_for(parentCategory.path) %>">More >></a>
<% } %>
<% } %>
</div>
</div>
<% }) %>

嗯,其实这样已经可以简单实现了,虽然只支持到二级分类,不过已经实现我的需求了。

问题

但是当我点进单独的分类页面时,即便当前单独分类页面有其子分类,依然是只会显示全部文章,而不是显示子分类。
事实上我已经给不同页面分了很多类别了。

1
2
3
4
5
categories: 
- 摸鱼的间隙总还是要工作
- 代码编辑
- 跑路
- 不玩了

但是其实际显示仍然是将该父分类下所有文章全部显示,无论该文章有无子分类:

这就不能忍了,还是改吧。

分析

考虑到这似乎是一个递归的分类显示模型,让我们忘了前面的那些修改吧。TAT
无论是一级、二级还是更深层级的分类,在其分类路径中都遵循着相同的规则:对于任意一个分类路径:

  1. 该路径下是否包含子分类?若包含,则显示所有子分类。若不包含,则显示所有文章列表。
  2. 显示子分类卡片时,若该子分类下仍然包含下一级子分类,则显示为下一级子分类列表。若不包含,则显示该子分类的所有文章列表。
  3. 点击子分类卡片中的下一级子分类时,跳转到新的单分类显示页面,递归调用该逻辑。

因此,每个分类页面均可使用统一的递归逻辑,实现多级分类管理:

  1. 任意深度的分类都使用相同规则
  2. 子分类卡片中的递归显示逻辑
  3. 点击跳转后的递归调用
  4. 无需区分主分类页面和单分类页面
  5. 支持无限层级的分类嵌套

主分类页面精简

主分类页面精简至如下:

1
2
3
4
5
6
7
8
9
10
11
12
<% if(is_category()){ %>
<%- partial('_page/category', {pagination: config.category, index: true}) %>
<% }else{ %>
<%
let virtualPage = {
path: 'categories/index.html',
category: 'All Categories'
};
%>

<%- partial('_page/category', {page: virtualPage}) %>
<% } %>

由于主页面只是一个特殊的根节点,其在显示逻辑上与任意的单分类页面并无区别,因此修改一下主页面path,使其与单页面一致,方便统一处理。

单页面配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%
let currentPath = page.path || 'categories/index.html'; // 主分类页面默认路径
let pathParts = currentPath.split('/').filter(part => part !== '');

pathParts = pathParts.map(part => decodeURIComponent(part));

if (pathParts[0] === 'categories') {
pathParts = pathParts.slice(1);
}

if (pathParts[pathParts.length - 1] === 'index.html') {
pathParts = pathParts.slice(0, -1);
}

let currentDepth = pathParts.length;
let currentCategoryPath = pathParts.join('/');

let pageTitle = '';
if (currentDepth === 0) {
pageTitle = 'Categories';
} else {
pageTitle = 'Categories · ' + (page.category || pathParts[pathParts.length - 1]);
}

递归获取子分类

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
function getDirectSubCategories(categoryPath, depth) {
let subCategories = [];

site.categories.forEach(category_item => {
let catPath = category_item.path;
let catPathParts = catPath.split('/').filter(part => part !== '');
catPathParts = catPathParts.map(part => decodeURIComponent(part));

if (catPathParts[0] === 'categories') {
catPathParts = catPathParts.slice(1);
}

if (catPathParts[catPathParts.length - 1] === 'index.html') {
catPathParts = catPathParts.slice(0, -1);
}

if (catPathParts.length === depth + 1) {
if (depth === 0) {
subCategories.push({
name: catPathParts[0],
fullName: category_item.name,
path: category_item.path,
posts: category_item.posts,
depth: catPathParts.length,
fullPath: catPathParts.join('/')
});
} else {
let parentPath = catPathParts.slice(0, depth).join('/');
if (parentPath === categoryPath) {
subCategories.push({
name: catPathParts[catPathParts.length - 1],
fullName: category_item.name,
path: category_item.path,
posts: category_item.posts,
depth: catPathParts.length,
fullPath: catPathParts.join('/')
});
}
}
}
});

return subCategories;
}

分类页面布局

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
<div class="container">
<div class="post-wrap categories">
<h2 class="post-title">-&nbsp;<%- pageTitle %>&nbsp;-</h2>
</div>

<%
let directSubCategories = getDirectSubCategories(currentCategoryPath, currentDepth);
let hasDirectSubCategories = directSubCategories.length > 0;
%>

<% if (hasDirectSubCategories) { %>
<div class="post-wrap categories">
<div class="categories-card">
<% directSubCategories.forEach(subCategory => { %>
<div class="card-item">
<div class="categories">
<a href="<%- url_for(subCategory.path) %>">
<h3>
<i class="iconfont icon-category" style="padding-right:3px"></i>
<%- subCategory.name %>
</h3>
</a>

<%
let grandChildren = getDirectSubCategories(subCategory.fullPath, subCategory.depth);
let hasGrandChildren = grandChildren.length > 0;
%>

<% if (hasGrandChildren) { %>
<div class="subcategories" style="margin-left: 15px; margin-top: 10px;">
<% grandChildren.forEach(grandChild => { %>
<div class="subcategory-item" style="margin-bottom: 8px;">
<a href="<%- url_for(grandChild.path) %>" style="font-size: 1em; color: #000000;">
<i class="iconfont icon-category" style="padding-right:3px; font-size: 0.8em;"></i>
<%- grandChild.name %> (<%- grandChild.posts.length %>)
</a>
</div>
<% }) %>
</div>
<% } else { %>
<% subCategory.posts.forEach((post_item, index = 0)=>{ %>
<% if(++index <= 5){ %>
<article class="archive-item">
<a class="archive-item-link"
href="<%- url_for(post_item.path) %>"><%- post_item.title %></a>
</article>
<% } %>
<% }) %>
<% if(subCategory.posts.length > 5){ %>
<a class="more-post-link" href="<%- url_for(subCategory.path) %>">More >></a>
<% } %>
<% } %>
</div>
</div>
<% }) %>
</div>
</div>
<% } else { %>
<% if (currentDepth === 0) { %>
<div class="container">
<div class="post-wrap categories">
<p style="text-align: center; color: #999; padding: 20px;">No Categories</p>
</div>
</div>
<% } else { %>
<%- partial('archive', {pagination: config.category, index: true}) %>
<% } %>
<% } %>
</div>

效果

主页面分类显示

子分类多级嵌套显示1

子分类多级嵌套显示2


总结

以上修改实现了Chic主题分类页面的递归逻辑,支持多级分类嵌套展示,改进了原有分类页面的展示方式,使得子分类可以递归显示。通过统一的递归规则,任意深度的分类都能够正确展示子分类或文章列表。