diff --git a/website/templates/site.css b/website/templates/site.css
index 600243ec..f577aa50 100644
--- a/website/templates/site.css
+++ b/website/templates/site.css
@@ -334,3 +334,16 @@ html { scrollbar-gutter: stable; }
.token.namespace { opacity: 1; color: var(--muted); }
.article-toc li a { display: inline-block; padding: 3px 0; }
.markdown-section :not(pre) > code { overflow-wrap: anywhere; }
+
+/* sidebar-scrim:手机上打开目录时,底部按钮条用页面底色,正文盖一层淡的遮罩 */
+@media (max-width: 768px) {
+ body.close .sidebar-toggle { background-color: var(--bg); box-shadow: 0 -1px 0 var(--border); }
+ .content::after { content: ""; position: fixed; inset: 0; z-index: 25; background: rgba(20, 24, 23, .32); opacity: 0; pointer-events: none; transition: opacity .25s ease; }
+ body.close .content::after { opacity: 1; pointer-events: auto; }
+}
+
+/* 读完的文章:侧边栏标 ✓,首页写已读完几篇 */
+.sidebar-nav a.read::after { content: "✓"; content: "✓" / ""; margin-left: 6px; color: var(--theme-color); font-size: 12px; font-weight: 600; }
+.markdown-section .recent-read .read-count { margin-left: 12px; }
+.markdown-section .recent-read .clear-read { margin-left: 8px; padding: 2px 4px; border: 0; background: none; color: var(--muted); font: inherit; font-size: 13px; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
+.markdown-section .recent-read .clear-read:hover { color: var(--theme-color); }
diff --git a/website/templates/site.js b/website/templates/site.js
index a4ab4904..24dd555e 100644
--- a/website/templates/site.js
+++ b/website/templates/site.js
@@ -760,6 +760,17 @@
});
}
+ // 侧边栏中读完的文章后面标 ✓
+ function markRead() {
+ var all = storage(PROGRESS_KEY) || {};
+ document.querySelectorAll('.sidebar-nav a[href^="#/"]').forEach(function (link) {
+ var href = link.getAttribute('href');
+ if (href.indexOf('?') !== -1) return;
+ var done = !!(all[decode(href.slice(1))] || {}).done;
+ link.classList.toggle('read', done);
+ if (done) link.setAttribute('aria-description', '已读完'); else link.removeAttribute('aria-description');
+ });
+ }
// 记住每篇文章读到的小节:再次打开时提示继续阅读,首页给出最近读过的文章
var stopProgress = null;
function readingProgress(vm, section) {
@@ -780,6 +791,16 @@
link.href = '#' + encodedRoute(last) + (saved[last].id ? '?id=' + encodeURIComponent(saved[last].id) : '');
link.textContent = saved[last].page + (saved[last].heading ? ' · ' + saved[last].heading : '');
recent.appendChild(link);
+ var finished = Object.keys(saved).filter(function (key) { return saved[key].done; }).length;
+ if (finished) {
+ recent.insertAdjacentHTML('beforeend', '已读完 ' + finished + ' 篇');
+ recent.querySelector('.clear-read').addEventListener('click', function () {
+ if (!window.confirm('清除全部阅读记录?侧边栏的已读标记、上次阅读的位置都会清除。')) return;
+ try { localStorage.removeItem(PROGRESS_KEY); } catch (error) {}
+ recent.remove();
+ markRead();
+ });
+ }
grid.after(recent);
return;
}
@@ -788,7 +809,9 @@
var page = ((section.querySelector('h1') || {}).textContent || path.split('/').pop()).trim();
var record = saved[path];
var target = record && record.id && document.getElementById(record.id);
- if (!vm.route.query.id && target && heads.indexOf(target) > 1) {
+ var tail = heads.filter(function (h) { return h.tagName === 'H2' && /^(附录|参考资料)/.test(headingText(h)); })[0];
+ var finishedHere = record && record.done && tail && target && (target === tail || tail.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_FOLLOWING);
+ if (!vm.route.query.id && target && heads.indexOf(target) > 1 && !finishedHere) {
var toast = document.createElement('div');
toast.className = 'resume-toast';
toast.setAttribute('role', 'status');
@@ -811,21 +834,32 @@
setTimeout(function () { toast.remove(); }, 12000);
}
var lastId = null, timer = null;
+ // 读完:读到附录或参考资料(或文末),并且读过的二级小节不少于一半;只跳到文末不算
+ var parts = heads.filter(function (h) { return h.tagName === 'H2'; });
+ var body = parts.filter(function (h) { return !tail || h.compareDocumentPosition(tail) & Node.DOCUMENT_POSITION_FOLLOWING; });
+ var seen = (record && record.seen || []).slice(), done = !!(record && record.done);
function save() {
- var current = null;
+ var current = null, part = null;
for (var i = 0; i < heads.length; i++) {
if (heads[i].getBoundingClientRect().top > 90) break;
current = heads[i];
+ if (current.tagName === 'H2') part = current;
}
var id = current ? current.id : null;
- if (id === lastId) return;
+ var changed = id !== lastId;
+ if (part && body.indexOf(part) !== -1 && seen.indexOf(part.id) === -1) { seen.push(part.id); changed = true; }
+ var end = (tail && tail.getBoundingClientRect().top < window.innerHeight * .6) ||
+ window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 400;
+ if (!done && end && seen.length >= Math.max(1, Math.ceil(body.length / 2))) { done = true; changed = true; }
+ if (!changed) return;
lastId = id;
var all = storage(PROGRESS_KEY) || {};
- all[path] = { page: page, id: id, heading: current ? headingText(current) : '', time: Date.now() };
- // 只保留最近的 100 篇
- Object.keys(all).sort(function (a, b) { return all[b].time - all[a].time; }).slice(100)
+ all[path] = { page: page, id: id, heading: current ? headingText(current) : '', time: Date.now(), seen: seen, done: done };
+ // 只保留最近的 300 篇
+ Object.keys(all).sort(function (a, b) { return all[b].time - all[a].time; }).slice(300)
.forEach(function (key) { delete all[key]; });
storage(PROGRESS_KEY, all);
+ if (done) markRead();
}
function onScroll() { clearTimeout(timer); timer = setTimeout(save, 400); }
window.addEventListener('scroll', onScroll, { passive: true });
@@ -1017,6 +1051,7 @@
learnBanner(section);
dailyQuestion(section);
if (section) readingProgress(vm, section);
+ markRead();
document.querySelectorAll('.article-toc').forEach(function (toc) {
toc.open = window.matchMedia('(min-width: 769px)').matches;
});