开发思路

这今天利用安知鱼主题的 timeline 标签来整理大叔作品年表。时间跨度从 1951 年至今,条目众多,想着要不搞个筛选器,分门别类显示,应该还不错。老规矩,说干就干。

流程分析:

  • 建立筛选器:在对应页面插入<button>,通过添加 active 类显示激活
  • 建立按钮关联:建立 bindFilterButtons()
  • 筛选条件:在每条内容里添加个图标作为筛选条件 getCategoryFromIcon(icon),返回筛选类别
  • 进行筛选:filterTimeline(category) 给对应的 category 打上 hidden
  • pjax 处理

不算复杂,快速搞定。等下,做都做了,给站点更新记录也加上这个功能吧,但是更新记录用图标不太得劲,改为文字标题吧,加一个 getCategoryFromText(text)。再等下,顺便做个计数器吧,给各个分类做个统计 updateFilterCounts()

行了,测试通过。

但是,漫画类还是太长了,在底部同步加个筛选器吧。额……为啥头尾的筛选器不同步?得,接着改,来个 syncActiveButtons()

最终成果

1
2
3
4
5
inject:
head:
- <link rel="stylesheet" href="/css/timeline-filter.css">
bottom:
- <script src="/js/timeline-filter.js"></script>
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
200
201
(function() {
function getCategoryFromIcon(spanElem) {
const classNames = spanElem.className.split(/\s+/);
for (let cls of classNames) {
if (cls.startsWith('icon-')) {
return cls.replace(/^icon-/, '').toLowerCase();
}
}
return '';
}

function getCategoryFromText(textElem) {
let text = textElem.innerText.trim();
text = text.replace(/[::、\s]+$/, '');
return text.toLowerCase();
}

function findItemCategories(item) {
const categories = new Set();

const textElems = item.querySelectorAll('.timeline-item-content strong');
textElems.forEach(elem => {
const cat = getCategoryFromText(elem);
if (cat) categories.add(cat);
});

if (categories.size === 0) {
const spans = item.querySelectorAll('.timeline-item-content span');
for (let span of spans) {
const cat = getCategoryFromIcon(span);
if (cat) categories.add(cat);
}
}
return Array.from(categories);
}

function filterTimeline(category) {
let effectiveCategory = category;
if (category !== 'all') {
if (category.startsWith('icon ') || category.startsWith('text ')) {
effectiveCategory = category.substring(5);
} else {
effectiveCategory = category;
}
}
effectiveCategory = effectiveCategory.trim().toLowerCase();

const containers = document.querySelectorAll('.timeline');
containers.forEach(container => {
container.style.display = '';

let hasMatch = false;
const items = container.querySelectorAll('.timeline-item');
items.forEach(item => {
if (item.classList.contains('headline')) return;

const itemCategories = findItemCategories(item);
const hasIdentifier = itemCategories.length > 0;
if (!hasIdentifier) {
item.style.display = (category === 'all') ? '' : 'none';
return;
}

const matches = (category === 'all') || itemCategories.includes(effectiveCategory);
if (matches) {
hasMatch = true;
item.style.display = '';
} else {
item.style.display = 'none';
}
});

const headline = container.querySelector('.timeline-item.headline');
if (headline) {
headline.style.display = (category === 'all' || hasMatch) ? '' : 'none';
}
if (category !== 'all' && !hasMatch) {
container.style.display = 'none';
}
});

const allContainers = document.querySelectorAll('.timeline');
let firstVisible = null;
for (let i = 0; i < allContainers.length; i++) {
if (allContainers[i].style.display !== 'none') {
firstVisible = allContainers[i];
break;
}
}

allContainers.forEach(container => {
container.style.marginTop = '';
});
if (firstVisible && document.querySelector('.category-filter')) {
firstVisible.style.marginTop = '0';
}
}

function bindFilterButtons() {
const btns = document.querySelectorAll('.filter-btn');
if (btns.length === 0) return;

btns.forEach(btn => {
if (btn._filterBound) return;
btn._filterBound = true;
btn.addEventListener('click', function() {
const category = this.getAttribute('data-category');
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.filter-btn').forEach(b => {
if (b.getAttribute('data-category') === category) {
b.classList.add('active');
}
});
filterTimeline(category);
});
});
}

function syncActiveButtons() {
const activeBtn = document.querySelector('.filter-btn.active');
if (activeBtn) {
const category = activeBtn.getAttribute('data-category');
document.querySelectorAll('.filter-btn').forEach(btn => {
if (btn.getAttribute('data-category') === category) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
} else {
const allBtn = document.querySelector('.filter-btn[data-category="all"]');
if (allBtn) allBtn.classList.add('active');
syncActiveButtons();
}
}

function updateFilterCounts() {
const containers = document.querySelectorAll('.timeline');
const allItems = [];
containers.forEach(container => {
const items = container.querySelectorAll('.timeline-item:not(.headline)');
items.forEach(item => allItems.push(item));
});

const itemsData = allItems.map(item => ({
item: item,
categories: findItemCategories(item)
}));

const totalCount = itemsData.length;
const btns = document.querySelectorAll('.filter-btn');
btns.forEach(btn => {
const existingCount = btn.querySelector('.filter-count');
if (existingCount) existingCount.remove();

let count = 0;
const category = btn.getAttribute('data-category');

if (category === 'all') {
count = totalCount;
} else {
let effectiveCategory = category;
if (category.startsWith('icon ') || category.startsWith('text ')) {
effectiveCategory = category.substring(5);
}
effectiveCategory = effectiveCategory.trim().toLowerCase();
count = itemsData.filter(data => data.categories.includes(effectiveCategory)).length;
}

const countSpan = document.createElement('span');
countSpan.className = 'filter-count';
countSpan.textContent = ` (${count})`;

btn.appendChild(countSpan);
if (count === 0) {
btn.setAttribute('hidden', 'hidden');
} else {
btn.removeAttribute('hidden');
}
});
}

function init() {
bindFilterButtons();
syncActiveButtons();
updateFilterCounts();
document.addEventListener('pjax:complete', function() {
bindFilterButtons();
syncActiveButtons();
updateFilterCounts();
const allBtn = document.querySelector('.filter-btn[data-category="all"]');
if (allBtn) allBtn.click();
});
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
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
#article-container .category-filter {
display: flex;
flex-wrap: wrap;
margin: 20px 0;
gap: 12px;
}

#article-container .category-filter .filter-btn {
padding: 6px 16px;
border-radius: 30px;
border: 1px solid var(--anzhiyu-theme);
color: var(--anzhiyu-theme);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}

#article-container .category-filter .filter-btn.active,
#article-container .category-filter .filter-btn:hover {
border-color: var(--anzhiyu-theme);
background: var(--anzhiyu-theme);
color: #fff;
}

#article-container .category-filter .filter-btn.active span::before {
color: #fff;
}

#article-container .category-filter .filter-btn[hidden] {
display: none;
}

#article-container .timeline {
border-left-color: var(--anzhiyu-theme);
}

#article-container .timeline .timeline-item.headline .timeline-item-title .item-circle::before,
#article-container .timeline .timeline-item .item-circle::before {
border-color:var(--anzhiyu-theme);
}

#article-container .timeline .timeline-item.headline .timeline-item-title:hover .item-circle::before,
#article-container .timeline .timeline-item:hover .item-circle::before {
border-color: var(--pseudo-hover);
}

#article-container .timeline .timeline-item .item-circle:not(:has(p))::before {
display: none;
}
1
2
3
4
5
6
# example
<div class="category-filter">
<button class="filter-btn active" data-category="all">全部</button>
<button class="filter-btn" data-category="icon book-text">漫画</button>
<button class="filter-btn" data-category="text docs">记录文档</button>
</div>

LeoC © 2026 | 萌 ICP 备 20260209 号

本站已运行...数学不好,还在数呢...,发表 6 篇文章,总计 5.5k 字,有 位访客,访问