Files
blog/themes/Ying/assets/js/modules/comment-anonymous.js
T
zqlit 215929a67f feat(comments): 匿名评论 + 表情最近使用;fix: 自动填充聚焦提示 & 邮件模板回退原版 & 返回顶部改 CSS 动画
- 匿名评论:comments.is_anonymous 列;对外昵称/头像/网址/徽标/等级/IP归属全部脱敏,
  真实身份留 users 表(回复通知照发、autofill 不受污染);邮件昵称也用「匿名」防泄露
- 表情包最近使用:面板顶部「最近」栏,localStorage 记录 8 个,零侵入复用 Artalk 插入逻辑
- 匿名开关插在 textarea 与底栏之间独立一行,不挤移动端底栏
- 自动填充:提示改 focusin 触发(原 input+sessionStorage 一次性门导致永不触发);
  已填昵称/网址时不覆盖并弹提示
- 邮件模板 UI 回退原版深色气泡设计,保留表情内联尺寸/主题文案等逻辑修复
- 返回顶部:rAF 自绘改 CSS scroll-behavior(滚动期临时挂类),修移动端掉帧
2026-10-05 09:02:23 +08:00

99 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* comment-anonymous —— 「匿名评论」开关
*
* 场景:有人想说话但不想暴露身份(昵称/头像/网址都能对上号)。
* 勾选后提交的评论对外显示为「匿名」:头像换通用图、网址/徽标/等级/IP归属
* 全部隐藏。真实昵称和邮箱仍存在后端 users 表里 —— 有人回复时通知照发、
* 下次来「填邮箱自动带出昵称」不受污染。博主后台能看到真实身份(审核需要)。
*
* 实现要点:
* - 开关插在 textarea 和底栏之间**独立一行**(右对齐小开关),不挤底栏 ——
* 移动端底栏本来就塞了表情/图片/发送按钮,再塞一个就挤爆了。
* - Artalk 是 editorTravel 单实例游走,全站只有一个活的 .atk-main-editor,
* fetch 拦截读它的开关状态即可。
* - 借已有的 fetch 拦截链给 POST /api/v2/comments 的 body 注入 anonymous:true。
*/
(function () {
if (window.__atkAnonReady) return; // PJAX 重复执行防抖
window.__atkAnonReady = true;
var TIP = '对外显示为「匿名」,头像和网址会隐藏;邮箱只用于接收回复通知,对外不可见';
// 取「当前活着的」编辑器里的开关:优先可见的(editorTravel 游走的那个)
function currentToggle() {
var eds = document.querySelectorAll('.atk-main-editor');
var fallback = null;
for (var i = 0; i < eds.length; i++) {
var cb = eds[i].querySelector('.atk-anon-toggle input');
if (!cb) continue;
if (eds[i].offsetParent !== null) return cb; // 可见编辑器优先
if (!fallback) fallback = cb;
}
return fallback;
}
function inject(editor) {
if (!editor || !editor.querySelector) return;
if (editor.querySelector('.atk-anon-toggle')) return; // 已注入
var bottom = editor.querySelector('.atk-bottom');
if (!bottom) return; // 结构没就绪,等下轮扫描
var label = document.createElement('label');
label.className = 'atk-anon-toggle';
label.title = TIP;
var cb = document.createElement('input');
cb.type = 'checkbox';
var txt = document.createElement('span');
txt.textContent = '匿名评论';
label.appendChild(cb);
label.appendChild(txt);
bottom.parentNode.insertBefore(label, bottom);
}
var scanTimer = null;
function scheduleScan() {
if (scanTimer) return;
scanTimer = setTimeout(function () {
scanTimer = null;
try {
var eds = document.querySelectorAll('.atk-main-editor');
for (var i = 0; i < eds.length; i++) inject(eds[i]);
} catch (e) {}
}, 120);
}
try {
// 编辑器由 Artalk 动态创建(PJAX / 点回复时游走重建位置),观察兜底
new MutationObserver(scheduleScan).observe(document.body, { childList: true, subtree: true });
} catch (e) {}
// MutationObserver 不监听「隐藏节点变可见」,用点击/聚焦兜底扫一遍
document.addEventListener('click', scheduleScan, true);
document.addEventListener('focusin', scheduleScan, true);
scheduleScan();
// ── fetch 拦截:给新建评论请求注入 anonymous 标志 ──────────────────────
// Artalk 的提交走 fetch + JSON body;链上已有别的拦截器(提交反馈/失败
// 翻译),这里只改 init.body,然后原样放行,互不干扰。
var origFetch = window.fetch;
if (typeof origFetch === 'function') {
window.fetch = function (input, init) {
try {
var url = String(typeof input === 'string' ? input : (input && input.url) || '');
var method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
if (method === 'POST' && /\/api\/v2\/comments(\?|$)/.test(url) &&
init && typeof init.body === 'string') {
var cb = currentToggle();
if (cb && cb.checked) {
var data = JSON.parse(init.body);
if (data && typeof data === 'object' && !data.anonymous) {
data.anonymous = true;
init = Object.assign({}, init, { body: JSON.stringify(data) });
}
}
}
} catch (e) {} // 解析失败就按原样发,绝不能因为匿名开关挂掉评论
return origFetch.apply(this, [input, init]);
};
}
})();