158 lines
7.2 KiB
JavaScript
158 lines
7.2 KiB
JavaScript
/**
|
||||
|
|
* comment-featured —— 「走心评论」精选按钮(仅管理员可见)
|
|||
|
|
*
|
|||
|
|
* 管理员在任意评论页,操作条(赞同 / 回复 / 折叠…)的「回复」右边多一个
|
|||
|
|
* 「精选」按钮:点了把该评论标记为精选(PUT /comments/:id {is_featured}),
|
|||
|
|
* 关于页「走心评论」板块会展示它。已精选的显示「取消精选」,再点一次取消。
|
|||
|
|
*
|
|||
|
|
* 实现要点:
|
|||
|
|
* - 按钮插在「回复」span(纯 <span>回复</span>,无 class)右边,顺序与
|
|||
|
|
* 用户要求一致:赞同 → 回复 → 精选 → 折叠/已审/置顶/编辑/删除。
|
|||
|
|
* - 只对管理员可见:挂官方同款 atk-only-admin-show 属性,Artalk 内核在
|
|||
|
|
* 登录态变化时统一 add/remove .atk-hide,不用自己判断登录态。
|
|||
|
|
* - 精选状态从列表响应里收集:fetch 拦截读 GET /comments 的 JSON,
|
|||
|
|
* 把 is_featured 的评论 id 记进 Set(单条 PUT 的响应也顺手更新)。
|
|||
|
|
* - 编辑器会被 Artalk 反复重建(PJAX / 回复模式),用 MutationObserver 补挂,
|
|||
|
|
* 已挂过的评论跳过(data-feat 标记)。
|
|||
|
|
*/
|
|||
|
|
(function () {
|
|||
|
|
if (window.__atkFeatReady) return; // PJAX 重复执行防抖
|
|||
|
|
window.__atkFeatReady = true;
|
|||
|
|
|
|||
|
|
var featured = new Set(); // 已精选的评论 id
|
|||
|
|
|
|||
|
|
function server() {
|
|||
|
|
return String((window.artalkConfig && window.artalkConfig.server) || '').replace(/\/+$/, '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 精选状态收集:拦截 fetch,读评论列表 / 单条响应 ────────────────────
|
|||
|
|
// 注意只「读取」,请求原样放行;与 comment-anonymous 的注入拦截互不干扰。
|
|||
|
|
var origFetch = window.fetch;
|
|||
|
|
if (typeof origFetch === 'function' && !window.__atkFeatFetchHooked) {
|
|||
|
|
window.__atkFeatFetchHooked = true;
|
|||
|
|
window.fetch = function (input, init) {
|
|||
|
|
var p = origFetch.apply(this, [input, init]);
|
|||
|
|
try {
|
|||
|
|
var url = String(typeof input === 'string' ? input : (input && input.url) || '');
|
|||
|
|
if (/\/api\/v2\/comments(\?|$)/.test(url) && !/lookup/.test(url)) {
|
|||
|
|
p.then(function (res) {
|
|||
|
|
try {
|
|||
|
|
if (!res || !res.ok) return;
|
|||
|
|
res.clone().json().then(function (data) {
|
|||
|
|
var list = (data && data.comments) || [];
|
|||
|
|
for (var i = 0; i < list.length; i++) {
|
|||
|
|
if (list[i] && list[i].is_featured !== undefined) {
|
|||
|
|
if (list[i].is_featured) featured.add(Number(list[i].id));
|
|||
|
|
else featured.delete(Number(list[i].id));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
refreshButtons(); // 按钮可能先于响应解析渲染,文案要修正
|
|||
|
|
}).catch(function () {});
|
|||
|
|
} catch (e) {}
|
|||
|
|
}).catch(function () {});
|
|||
|
|
}
|
|||
|
|
} catch (e) {}
|
|||
|
|
return p;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getToken() {
|
|||
|
|
try {
|
|||
|
|
var u = JSON.parse(localStorage.getItem('ArtalkUser') || '{}');
|
|||
|
|
return u.token || '';
|
|||
|
|
} catch (e) { return ''; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function commentIdOf(actions) {
|
|||
|
|
var wrap = actions.closest('.atk-comment-wrap');
|
|||
|
|
if (!wrap || !wrap.id) return 0;
|
|||
|
|
var m = wrap.id.match(/^atk-comment-(\d+)$/);
|
|||
|
|
return m ? Number(m[1]) : 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggle(commentId, btn) {
|
|||
|
|
if (!commentId) return;
|
|||
|
|
var next = !featured.has(commentId);
|
|||
|
|
var token = getToken();
|
|||
|
|
if (!token) { window.showToast && showToast('请先以管理员身份登录', 'warning'); return; }
|
|||
|
|
btn.style.opacity = '0.5';
|
|||
|
|
fetch(server() + '/api/v2/comments/' + commentId, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|||
|
|
body: JSON.stringify({ is_featured: next }),
|
|||
|
|
}).then(function (r) {
|
|||
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|||
|
|
if (next) featured.add(commentId); else featured.delete(commentId);
|
|||
|
|
btn.textContent = next ? '取消精选' : '精选';
|
|||
|
|
window.showToast && showToast(next ? '已加入走心评论' : '已取消精选', 'success');
|
|||
|
|
}).catch(function () {
|
|||
|
|
window.showToast && showToast('操作失败,请重试', 'error');
|
|||
|
|
}).then(function () { btn.style.opacity = ''; });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在 .atk-actions 里找到「回复」span:Artalk 渲染的纯 <span>回复</span>
|
|||
|
|
function replySpanOf(actions) {
|
|||
|
|
var spans = actions.querySelectorAll(':scope > span');
|
|||
|
|
for (var i = 0; i < spans.length; i++) {
|
|||
|
|
if (spans[i].textContent.trim() === '回复' && !spans[i].classList.contains('atk-common-action-btn')) {
|
|||
|
|
return spans[i];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function refreshButtons() {
|
|||
|
|
try {
|
|||
|
|
var btns = document.querySelectorAll('.atk-feat-btn');
|
|||
|
|
for (var i = 0; i < btns.length; i++) {
|
|||
|
|
var id = commentIdOf(btns[i].closest('.atk-actions'));
|
|||
|
|
if (id) btns[i].textContent = featured.has(id) ? '取消精选' : '精选';
|
|||
|
|
}
|
|||
|
|
} catch (e) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function inject(actions) {
|
|||
|
|
if (actions.__atkFeat) return;
|
|||
|
|
var id = commentIdOf(actions);
|
|||
|
|
if (!id) return; // 编辑器回复预览等没有 id 的区域不动
|
|||
|
|
var replySpan = replySpanOf(actions);
|
|||
|
|
if (!replySpan) return; // 关闭回复的评论没有「回复」按钮,下轮再说
|
|||
|
|
|
|||
|
|
actions.__atkFeat = true;
|
|||
|
|
var btn = document.createElement('span');
|
|||
|
|
btn.className = 'atk-common-action-btn atk-feat-btn';
|
|||
|
|
btn.setAttribute('atk-only-admin-show', '');
|
|||
|
|
btn.textContent = featured.has(id) ? '取消精选' : '精选';
|
|||
|
|
// 初始显隐对齐官方 admin 按钮:官方的 [atk-only-admin-show] 显隐同步
|
|||
|
|
// 在列表渲染时就跑完了,我们是后注入的赶不上那趟车 —— 抄当前时刻
|
|||
|
|
// 官方按钮(如「折叠」)的 atk-hide 状态;之后登录/登出的再同步由
|
|||
|
|
// Artalk 内核统一处理(我们挂着同款属性,会被一起切换)。
|
|||
|
|
var refAdminBtn = actions.querySelector('[atk-only-admin-show]');
|
|||
|
|
if (!refAdminBtn || refAdminBtn.classList.contains('atk-hide')) {
|
|||
|
|
btn.classList.add('atk-hide');
|
|||
|
|
}
|
|||
|
|
btn.addEventListener('click', function (e) {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
toggle(id, btn);
|
|||
|
|
});
|
|||
|
|
replySpan.after(btn);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function scan() {
|
|||
|
|
try {
|
|||
|
|
var list = document.querySelectorAll('.atk-actions');
|
|||
|
|
for (var i = 0; i < list.length; i++) inject(list[i]);
|
|||
|
|
} catch (e) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var timer = null;
|
|||
|
|
function scheduleScan() {
|
|||
|
|
if (timer) return;
|
|||
|
|
timer = setTimeout(function () { timer = null; scan(); }, 150);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
new MutationObserver(scheduleScan).observe(document.body, { childList: true, subtree: true });
|
|||
|
|
} catch (e) {}
|
|||
|
|
scan();
|
|||
|
|
})();
|