Files
blog/themes/Ying/assets/js/modules/comment-autofill.js
T
zqlit 6046eb494c 邮件模板微信化 + 评论老友自动填充 + CF 后台国内线路一键跳
- 邮件模板重做成微信聊天式:灰底、自己评论绿色气泡靠右、对方白色气泡靠左、
  40px 圆角方头像(weavatar,邮箱 md5 不外泄)、时间居中、#07C160 主按钮;
  布局全嵌套表格(Outlook 稳),表情内联 max-height:28px(修掉「表情太大」)、
  配图限宽 100%;管理员主题从「新的评论待审」改成「您有一个新的评论」
- 老友自动填充:新增 /comments/lookup(只回昵称+网址,绝不含邮箱;IP 限速
  30 次/10 分钟)+ 主题 comment-autofill 模块(填昵称弹提示、填邮箱自动带出
  上次的昵称/网址,事件委托不怕 Artalk 重建编辑器)
- CF 后台新增「国内线路」一键跳:/editor/handoff 用共享令牌对时间戳 HMAC
  签名 → editor-api /admin/handoff 验签发会话 Cookie(2 分钟有效、防重放),
  晚高峰丢包时直达国内机后台免重新登录
2026-10-05 07:36:26 +08:00

142 lines
6.1 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-autofill —— 老朋友「填邮箱自动带出昵称和网址」
*
* 场景:很多人隔几个月来评论一次,早忘了上次填的昵称/网址,每次都重新想。
* 这个模块做两件事:
* 1. 访客开始在昵称框打字时,弹一次 toast 提示「填邮箱可以自动带回来」;
* 2. 邮箱填完(失焦或格式完整)就问后端:这个邮箱上次评论用的什么昵称/网址,
* 拿到后回填到空着的输入框,让老朋友少打几个字。
*
* 隐私红线:后端 /comments/lookup **只回昵称和网址,永远不回邮箱**;
* 邮箱只作为查询参数发给评论后端(跟提交评论本身一样),不在本地落任何存储,
* 也不打日志。查不到就安静地什么都不做,绝不给访客「你在被查询」的感觉。
*
* 实现说明:用 document 级事件委托(capture),不绑定具体输入框 ——
* Artalk 的编辑器会反复重建(PJAX 切页、点回复、登录态变化),委托一劳永逸。
*/
(function () {
if (window.__atkAutofillReady) return; // PJAX 重复执行防抖
window.__atkAutofillReady = true;
var HINT_KEY = 'atk_friend_hint'; // sessionStorage:提示只弹一次
var asked = {}; // 查过的邮箱 → 结果,避免重复请求
var timer = null;
function toast(msg, type, duration) {
try {
if (window.Toast && typeof window.Toast[type || 'info'] === 'function') {
window.Toast[type || 'info'](msg, duration);
}
} catch (e) {}
}
function isEmail(s) { return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(String(s || '').trim()); }
function editorOf(el) {
var box = el && el.closest ? el.closest('.atk-editor') : null;
return box || null;
}
function fieldIn(editor, name) {
return editor ? editor.querySelector('input[name="' + name + '"]') : null;
}
/** Artalk 对受控输入有自己的状态,改完必须补一个 input 事件它才认 */
function setVal(input, value) {
if (!input) return;
input.value = value;
try { input.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
}
function showHint(editor) {
try {
if (sessionStorage.getItem(HINT_KEY)) return;
sessionStorage.setItem(HINT_KEY, '1');
} catch (e) { return; } // 存储被禁用就别弹了,宁缺毋滥
var email = fieldIn(editor, 'email');
if (email && email.value.trim()) return; // 已经填了邮箱,不用提示
toast('老朋友?填一下邮箱,昵称和网址会自动帮你带回来 ✨', 'info', 4500);
}
function fillFrom(editor, data) {
if (!data || !data.found) return;
var nick = fieldIn(editor, 'nick');
var link = fieldIn(editor, 'link');
var name = String(data.name || '').trim();
var url = String(data.link || '').trim();
if (!name) return;
var filled = [];
// 昵称:空着,或者只打了历史昵称的开头(说明 TA 正在凭印象敲同一个名字)
// 才替 TA 补全 —— 已经敲了一个完全不同的名字就别抢,那是故意的。
if (nick) {
var cur = nick.value.trim();
var lower = cur.toLowerCase();
var lowerName = name.toLowerCase();
if (!cur || lowerName.indexOf(lower) === 0) {
if (cur !== name) { setVal(nick, name); filled.push('昵称'); }
}
}
if (link && !link.value.trim() && url) { setVal(link, url); filled.push('网址'); }
if (filled.length) {
toast('欢迎回来,' + name + '!已帮你填好' + filled.join('和') + ' 👋', 'success', 4000);
}
}
function lookup(editor, email) {
var key = email.toLowerCase();
if (asked[key]) { fillFrom(editor, asked[key]); return; }
var server = (window.artalkConfig && window.artalkConfig.server || '').replace(/\/+$/, '');
if (!server) return;
// 超时兜底:评论后端在国内访问偶尔抖,别让访客干等
var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
var killer = null;
if (ctrl) killer = setTimeout(function () { try { ctrl.abort(); } catch (e) {} }, 8000);
fetch(server + '/api/v2/comments/lookup?email=' + encodeURIComponent(email), {
method: 'GET',
credentials: 'omit',
signal: ctrl ? ctrl.signal : undefined
}).then(function (r) { return r.ok ? r.json() : null; }).then(function (d) {
asked[key] = d || { found: false };
fillFrom(editor, asked[key]);
}).catch(function () {
asked[key] = { found: false }; // 失败也记一下,别反复打同一个请求
}).finally(function () { if (killer) clearTimeout(killer); });
}
// ── 事件委托(capture:Artalk 内部可能 stopPropagation)──────────────
document.addEventListener('input', function (e) {
var el = e.target;
if (!el || el.tagName !== 'INPUT' || !el.name) return;
var editor = editorOf(el);
if (!editor) return; // 只管 Artalk 的评论框
if (el.name === 'nick') {
if (el.value.trim()) showHint(editor); // 打了第一个字就开始提示
return;
}
if (el.name === 'email') {
var v = el.value.trim();
if (isEmail(v)) {
if (timer) clearTimeout(timer);
timer = setTimeout(function () { lookup(editor, v); }, 700); // 停手再查
}
}
}, true);
document.addEventListener('focusout', function (e) {
var el = e.target;
if (!el || el.tagName !== 'INPUT' || el.name !== 'email') return;
var editor = editorOf(el);
if (!editor) return;
var v = el.value.trim();
if (!isEmail(v)) return;
if (timer) clearTimeout(timer);
lookup(editor, v); // 离开邮箱框就立刻查
}, true);
})();