Skip to content

Commit 6510c7d

Browse files
committed
feat(archive): random-post button, '全部' category fix, client-side paginator
- Tier 3 (search box row): add 🎲 '抽一篇旧的看看' button. Weighted-random by post age; falls back to uniform when <=3 candidates. Brief yellow flash on picked entry before navigation. - tier2.css line 81: append :not(#archive-cat-all) to the offending hide selector so clicking the '全部' pill no longer hides all entries (pre-existing bug — the whitelist only covered the three real categories). - Tier 3 paginator: client-side, 10 per page, URL hash sync (#page=N), wraps applySearch/sortEntries so filter+search resets to page 1. Compact 1 / current-1 / current / current+1 / last control row with « 上一页 / 下一页 ».
1 parent 9f7cfcf commit 6510c7d

4 files changed

Lines changed: 252 additions & 1 deletion

File tree

assets/css/extended/archive-tier2.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
}
7979

8080
/* When a specific category is selected, hide posts that aren't in it. */
81-
.archive-radio[id^="archive-cat-"]:checked ~ .archive-list .archive-entry {
81+
.archive-radio[id^="archive-cat-"]:not(#archive-cat-all):checked ~ .archive-list .archive-entry {
8282
display: none;
8383
}
8484
#archive-cat-随笔:checked ~ .archive-list .archive-entry--cat-随笔,

assets/css/extended/archive-tier3.css

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,40 @@
3838
cursor: pointer;
3939
}
4040

41+
/* Random pick button (Tier 3) */
42+
.archive-random-btn {
43+
padding: 0.5rem 0.95rem;
44+
font-size: 0.9em;
45+
color: var(--primary);
46+
background: var(--entry);
47+
border: 1px solid var(--border);
48+
border-radius: var(--radius, 8px);
49+
cursor: pointer;
50+
transition: transform 0.1s ease, border-color 0.15s ease, background 0.15s ease;
51+
white-space: nowrap;
52+
}
53+
54+
.archive-random-btn:hover {
55+
border-color: var(--secondary);
56+
background: var(--secondary);
57+
color: #fff;
58+
}
59+
60+
.archive-random-btn:active {
61+
transform: scale(0.97);
62+
}
63+
64+
/* Brief highlight on the entry that was randomly picked, before navigation */
65+
.archive-entry--picked {
66+
animation: archive-picked-flash 0.18s ease-out;
67+
}
68+
69+
@keyframes archive-picked-flash {
70+
0% { background: rgba(245, 158, 11, 0); }
71+
50% { background: rgba(245, 158, 11, 0.25); }
72+
100% { background: rgba(245, 158, 11, 0); }
73+
}
74+
4175
.archive-no-results {
4276
padding: 1.5rem;
4377
text-align: center;
@@ -108,3 +142,56 @@
108142
text-align: right;
109143
flex-shrink: 0;
110144
}
145+
146+
/* ===== Paginator (Tier 3) ===== */
147+
.archive-paginator {
148+
display: flex;
149+
flex-wrap: wrap;
150+
gap: 0.4rem;
151+
align-items: center;
152+
justify-content: center;
153+
margin: 1.5rem 0 0.5rem;
154+
padding: 0.6rem;
155+
border-top: 1px solid var(--border);
156+
}
157+
158+
.archive-page-btn {
159+
padding: 0.35rem 0.7rem;
160+
font-size: 0.85em;
161+
color: var(--primary);
162+
background: var(--entry);
163+
border: 1px solid var(--border);
164+
border-radius: var(--radius, 6px);
165+
cursor: pointer;
166+
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
167+
}
168+
169+
.archive-page-btn:hover:not(.archive-page-btn--disabled):not(.archive-page-btn--current) {
170+
border-color: var(--secondary);
171+
color: var(--secondary);
172+
}
173+
174+
.archive-page-btn--current {
175+
color: #fff;
176+
background: var(--secondary);
177+
border-color: var(--secondary);
178+
cursor: default;
179+
}
180+
181+
.archive-page-btn--disabled {
182+
opacity: 0.4;
183+
cursor: not-allowed;
184+
}
185+
186+
.archive-page-ellipsis {
187+
color: var(--secondary);
188+
padding: 0 0.25rem;
189+
}
190+
191+
.archive-page-summary {
192+
flex: 1 1 100%;
193+
text-align: center;
194+
margin-top: 0.4rem;
195+
font-size: 0.8em;
196+
color: var(--secondary);
197+
}

layouts/_partials/archive-search.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@
1919
<option value="reading-asc">阅读时长 短→长</option>
2020
<option value="reading-desc">阅读时长 长→短</option>
2121
</select>
22+
23+
<button type="button"
24+
id="archive-random-btn"
25+
class="archive-random-btn"
26+
title="随机挑一篇(按发布时间加权,越老的文章越容易被抽到)"
27+
aria-label="随机挑一篇文章">
28+
🎲 抽一篇旧的看看
29+
</button>
2230
</div>
2331

2432
<div id="archive-no-results" class="archive-no-results" hidden>
2533
没找到匹配的文章。换个关键词试试?
2634
</div>
35+
<nav id="archive-paginator" class="archive-paginator" aria-label="分页"></nav>

static/js/archive-search.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,161 @@
154154
});
155155
}
156156

157+
// ---------- RANDOM PICK ----------
158+
// Weighted toward older posts so the button truly helps you "discover"
159+
// something you wrote long ago and forgot about. Weight = (now - date).
160+
// If you only have a few posts (<=3) just pick uniformly to avoid surprises.
161+
const randomBtn = document.getElementById('archive-random-btn');
162+
163+
function pickRandom() {
164+
const now = Date.now();
165+
const candidates = entries.filter((el) => !el.dataset.hidden);
166+
if (candidates.length === 0) return;
167+
168+
let pick;
169+
if (candidates.length <= 3) {
170+
pick = candidates[Math.floor(Math.random() * candidates.length)];
171+
} else {
172+
const weights = candidates.map((el) => {
173+
const t = new Date(el.dataset.date || 0).getTime();
174+
// Older => larger weight; clamp to >=1 day so same-day posts still have mass
175+
return Math.max(now - t, 24 * 3600 * 1000);
176+
});
177+
const total = weights.reduce((a, b) => a + b, 0);
178+
let r = Math.random() * total;
179+
pick = candidates[candidates.length - 1];
180+
for (let i = 0; i < candidates.length; i++) {
181+
r -= weights[i];
182+
if (r <= 0) { pick = candidates[i]; break; }
183+
}
184+
}
185+
186+
const link = pick.querySelector('.archive-entry-link');
187+
if (link && link.href) {
188+
// Tiny visual feedback: brief pulse on the picked card before navigating
189+
pick.classList.add('archive-entry--picked');
190+
window.setTimeout(() => { window.location.href = link.href; }, 180);
191+
}
192+
}
193+
194+
if (randomBtn) {
195+
randomBtn.addEventListener('click', pickRandom);
196+
}
197+
198+
199+
// ---------- PAGINATOR ----------
200+
// Client-side pagination: respect search/sort visibility, sync URL hash.
201+
const PAGE_SIZE = 10;
202+
const paginatorEl = document.getElementById('archive-paginator');
203+
let currentPage = 1;
204+
205+
function visibleEntries() {
206+
return entries.filter((el) => !el.dataset.hidden);
207+
}
208+
209+
function readHashPage() {
210+
const m = /(?:#|&)page=(\d+)/.exec(window.location.hash || '');
211+
const n = m ? parseInt(m[1], 10) : 1;
212+
return Number.isFinite(n) && n > 0 ? n : 1;
213+
}
214+
215+
function writeHashPage(n) {
216+
const target = '#page=' + n;
217+
if (window.location.hash !== target) {
218+
// replaceState avoids spamming browser history
219+
try { history.replaceState(null, '', window.location.pathname + window.location.search + target); }
220+
catch (_) { window.location.hash = target; }
221+
}
222+
}
223+
224+
function applyPage(page) {
225+
const total = visibleEntries().length;
226+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
227+
currentPage = Math.min(Math.max(1, page), totalPages);
228+
229+
// Show only entries within the current page slice
230+
const visible = visibleEntries();
231+
const slice = visible.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
232+
const sliceSet = new Set(slice);
233+
entries.forEach((el) => {
234+
// Within current page slice AND not already hidden by search/filter => display
235+
if (sliceSet.has(el)) {
236+
el.style.display = '';
237+
} else if (el.dataset.hidden) {
238+
el.style.display = 'none';
239+
} else {
240+
// Outside page slice but still matches current filter => hide by page
241+
el.style.display = 'none';
242+
}
243+
});
244+
245+
// Year/month grouping: hide headers whose entries are all hidden (page OR search)
246+
document.querySelectorAll('.archive-month').forEach((m) => {
247+
const any = Array.from(m.querySelectorAll('.archive-entry')).some((e) => e.style.display !== 'none');
248+
m.style.display = any ? '' : 'none';
249+
});
250+
document.querySelectorAll('.archive-year').forEach((y) => {
251+
const any = Array.from(y.querySelectorAll('.archive-month')).some((m) => m.style.display !== 'none');
252+
y.style.display = any ? '' : 'none';
253+
});
254+
255+
// Render the paginator control bar
256+
if (paginatorEl) {
257+
paginatorEl.innerHTML = '';
258+
if (totalPages <= 1) return; // single page: no controls
259+
260+
const mkBtn = (label, page, opts = {}) => {
261+
const b = document.createElement('button');
262+
b.type = 'button';
263+
b.className = 'archive-page-btn' + (opts.current ? ' archive-page-btn--current' : '') + (opts.disabled ? ' archive-page-btn--disabled' : '');
264+
b.textContent = label;
265+
if (!opts.disabled && !opts.current) {
266+
b.addEventListener('click', () => {
267+
writeHashPage(page);
268+
applyPage(page);
269+
paginatorEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
270+
});
271+
}
272+
return b;
273+
};
274+
275+
paginatorEl.appendChild(mkBtn('« 上一页', currentPage - 1, { disabled: currentPage === 1 }));
276+
277+
// Compact page-number list: show first, last, current ±1
278+
const numbers = new Set([1, totalPages, currentPage - 1, currentPage, currentPage + 1]);
279+
[...numbers].filter((n) => n >= 1 && n <= totalPages).sort((a, b) => a - b).reduce((prev, n) => {
280+
if (prev !== null && n - prev > 1) {
281+
const ell = document.createElement('span');
282+
ell.className = 'archive-page-ellipsis';
283+
ell.textContent = '…';
284+
paginatorEl.appendChild(ell);
285+
}
286+
paginatorEl.appendChild(mkBtn(String(n), n, { current: n === currentPage }));
287+
return n;
288+
}, null);
289+
290+
paginatorEl.appendChild(mkBtn('下一页 »', currentPage + 1, { disabled: currentPage === totalPages }));
291+
292+
const summary = document.createElement('span');
293+
summary.className = 'archive-page-summary';
294+
summary.textContent = `第 ${currentPage} / ${totalPages} 页 · 共 ${total} 篇`;
295+
paginatorEl.appendChild(summary);
296+
}
297+
}
298+
299+
// Hook paginator into existing search/sort flow.
300+
// Easiest: re-run applyPage after every applySearch/sortEntries/hideEmptyMonths.
301+
// We add it as a wrapper to applySearch without changing its body.
302+
const _origApplySearch = applySearch;
303+
applySearch = function (q) { _origApplySearch(q); applyPage(1); writeHashPage(1); };
304+
const _origSortEntries = sortEntries;
305+
sortEntries = function (m) { _origSortEntries(m); applyPage(currentPage); };
306+
const _origHideEmptyMonths = hideEmptyMonths;
307+
hideEmptyMonths = function () { _origHideEmptyMonths(); };
308+
309+
window.addEventListener('hashchange', () => applyPage(readHashPage()));
310+
157311
// ---------- INIT ----------
158312
buildHistogram();
313+
applyPage(readHashPage());
159314
})();

0 commit comments

Comments
 (0)