评论加载失败改为清晰提示:接管 Artalk 错误层,按服务端错误码给读者人话文案+可展开技术详情
Deploy to Production / pre-check (push) Successful in 1m0s
Deploy to Production / build (push) Successful in 4m18s
Deploy to Production / deploy-edgeone (push) Successful in 3m55s
Deploy to Production / finalize (push) Successful in 24s
Deploy to Production / notify-failure (push) Skipped
Deploy to Production / pre-check (push) Successful in 1m0s
Deploy to Production / build (push) Successful in 4m18s
Deploy to Production / deploy-edgeone (push) Successful in 3m55s
Deploy to Production / finalize (push) Successful in 24s
Deploy to Production / notify-failure (push) Skipped
This commit is contained in:
1 parent
45afb53580
commit
afa1346a0f
1 file changed
+173
@@ -1,3 +1,176 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// 评论加载失败 —— 给读者看的清晰提示
|
||||||
|
//
|
||||||
|
// Artalk 自带的错误层只会写「Failed to load comments / TypeError: Failed to fetch」,
|
||||||
|
// 既没说原因也没给下一步。服务端现在已经会返回结构化的错误码
|
||||||
|
// (见 artalk-cf/src/lib/errors.ts:d1_quota / d1_unavailable / timeout / ...),
|
||||||
|
// 所以这里把它换成一句人话 + 可展开的技术详情(留着给站长排查)。
|
||||||
|
// ============================================================================
|
||||||
|
(function () {
|
||||||
|
if (window.__atkErrGuard) return; // PJAX 会重复执行本文件,只装一次
|
||||||
|
window.__atkErrGuard = true;
|
||||||
|
|
||||||
|
var last = null; // 最近一次 /api/v2 失败:{ status, code, msg, detail, at }
|
||||||
|
|
||||||
|
var INFO = {
|
||||||
|
d1_quota: { ic: '😴', title: '评论暂时加载不出来', desc: '数据库今日读取额度已用尽,北京时间明早 8:00 自动恢复。你可以先看看文章,稍后再来~' },
|
||||||
|
d1_unavailable: { ic: '🛠️', title: '评论服务正在维护', desc: '数据库连接异常,我们已经在处理,请稍后再试。' },
|
||||||
|
timeout: { ic: '🐢', title: '评论服务响应有点慢', desc: '这次请求超时了,稍后再试一次。' },
|
||||||
|
unavailable: { ic: '📡', title: '评论服务暂时不可用', desc: '服务或网络有点问题,请稍后再试。' },
|
||||||
|
network: { ic: '📡', title: '网络好像不太稳定', desc: '评论没能加载出来,检查一下网络再点重试。' },
|
||||||
|
internal: { ic: '⚠️', title: '评论暂时加载不出来', desc: '服务出了点小状况,请稍后再试。' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 1) 记录 API 失败原因。只读不改:包装层原样返回上游响应,
|
||||||
|
// 失败时顺带 clone 一份读 body(clone 不影响 Artalk 自己读)。
|
||||||
|
var origFetch = window.fetch;
|
||||||
|
if (typeof origFetch === 'function') {
|
||||||
|
window.fetch = function (input) {
|
||||||
|
var url = typeof input === 'string' ? input : (input && input.url) || '';
|
||||||
|
var p = origFetch.apply(this, arguments);
|
||||||
|
if (String(url).indexOf('/api/v2/') === -1) return p;
|
||||||
|
return p.then(function (res) {
|
||||||
|
if (res && res.ok === false) {
|
||||||
|
try {
|
||||||
|
res.clone().json().then(function (d) {
|
||||||
|
last = {
|
||||||
|
status: res.status,
|
||||||
|
code: (d && d.code) || '',
|
||||||
|
msg: (d && d.msg) || '',
|
||||||
|
detail: (d && d.detail) || '',
|
||||||
|
at: Date.now()
|
||||||
|
};
|
||||||
|
}).catch(function () {
|
||||||
|
last = { status: res.status, code: '', msg: '', detail: 'HTTP ' + res.status, at: Date.now() };
|
||||||
|
});
|
||||||
|
} catch (e) { /* clone 不支持就放弃记录,不影响请求 */ }
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}).catch(function (e) {
|
||||||
|
last = { status: 0, code: 'network', msg: '', detail: (e && e.message) || 'network error', at: Date.now() };
|
||||||
|
throw e; // 原样抛回,Artalk 自己的重试逻辑不受影响
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(x) {
|
||||||
|
return String(x == null ? '' : x)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectCss() {
|
||||||
|
if (document.getElementById('atk-err-css')) return;
|
||||||
|
var st = document.createElement('style');
|
||||||
|
st.id = 'atk-err-css';
|
||||||
|
st.textContent =
|
||||||
|
'.atk-error-layer{padding:0!important;background:transparent!important}' +
|
||||||
|
'.atk-err-card{display:flex;gap:12px;align-items:flex-start;padding:16px 18px;box-sizing:border-box;' +
|
||||||
|
'border:1px solid var(--border-main,#eee);border-radius:10px;background:var(--bg-card,#fff);' +
|
||||||
|
'animation:atkErrIn .35s cubic-bezier(.22,1,.36,1)}' +
|
||||||
|
'@keyframes atkErrIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}' +
|
||||||
|
'.atk-err-ic{flex:none;font-size:22px;line-height:1.2}' +
|
||||||
|
'.atk-err-body{flex:1;min-width:0}' +
|
||||||
|
'.atk-err-t{font-size:14px;font-weight:600;color:var(--text-main,#333);margin-bottom:4px}' +
|
||||||
|
'.atk-err-d{font-size:13px;line-height:1.7;color:var(--text-desc,#777);word-break:break-word}' +
|
||||||
|
'.atk-err-acts{display:flex;align-items:center;gap:10px;margin-top:10px;font-size:13px}' +
|
||||||
|
'.atk-err-retry{display:inline-flex;align-items:center;padding:5px 14px;border-radius:5px;' +
|
||||||
|
'background:var(--theme-main-20,rgba(7,193,96,.2));color:var(--theme-main,#07c160);' +
|
||||||
|
'font-weight:600;cursor:pointer;transition:background .2s,transform .2s;user-select:none}' +
|
||||||
|
'.atk-err-retry:hover{background:var(--theme-main-30,rgba(7,193,96,.3))}' +
|
||||||
|
'.atk-err-retry:active{transform:scale(.97)}' +
|
||||||
|
'.atk-err-det{margin-top:10px;font-size:12px;color:var(--text-muted,#999)}' +
|
||||||
|
'.atk-err-det>summary{cursor:pointer;outline:none;user-select:none}' +
|
||||||
|
'.atk-err-det pre{margin:8px 0 0;padding:8px 10px;border-radius:6px;background:rgba(127,127,127,.08);' +
|
||||||
|
'white-space:pre-wrap;word-break:break-all;font-size:11px;line-height:1.6;max-height:150px;overflow:auto}' +
|
||||||
|
'[data-theme="dark"] .atk-err-card{border-color:rgba(255,255,255,.14)}' +
|
||||||
|
'@media (max-width:640px){.atk-err-card{padding:13px 14px;gap:8px}.atk-err-ic{font-size:18px}' +
|
||||||
|
'.atk-err-t{font-size:13.5px}.atk-err-d{font-size:12.5px}}';
|
||||||
|
document.head.appendChild(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewrite(layer) {
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
var raw = '';
|
||||||
|
var em = layer.querySelector('.error-message');
|
||||||
|
if (em) raw = (em.innerText || em.textContent || '').trim();
|
||||||
|
|
||||||
|
var prev = layer.querySelector('.atk-err-card');
|
||||||
|
if (prev && prev.getAttribute('data-raw') === raw) return; // 内容没变,不重复渲染
|
||||||
|
|
||||||
|
// 优先用结构化错误码;没有就按「网络层失败」处理
|
||||||
|
var fresh = last && (Date.now() - last.at < 30000) ? last : null;
|
||||||
|
var code = (fresh && fresh.code) || (/failed to fetch|network ?error|load failed/i.test(raw) ? 'network' : 'internal');
|
||||||
|
var info = INFO[code] || INFO.internal;
|
||||||
|
// 服务端文案是给「直接调 API 的人」写的,自带一句"评论服务暂时不可用:",
|
||||||
|
// 放到卡片里会和标题重复,这里去掉冗余前缀
|
||||||
|
var desc = fresh && fresh.msg
|
||||||
|
? fresh.msg.replace(/^评论服务暂时不可用(?:[((][^))]*[))])?[::]\s*/, '')
|
||||||
|
: info.desc;
|
||||||
|
var detail = (fresh && fresh.detail) || raw;
|
||||||
|
|
||||||
|
// 原始「重试」节点:Vue 会整层重建,所以每次重新找;
|
||||||
|
// 若本层已重写过,就从我们自己卡片里把它取回来(移动 DOM 保留已绑事件)
|
||||||
|
var retry = prev ? prev.querySelector('.atk-err-retry') : null;
|
||||||
|
if (!retry) {
|
||||||
|
var box = layer.querySelector('.atk-error-text span');
|
||||||
|
if (box) {
|
||||||
|
box.querySelectorAll('span').forEach(function (sp) {
|
||||||
|
if (!retry && /(重试|重新|retry)/i.test(sp.textContent || '')) retry = sp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var admin = layer.querySelector('[atk-only-admin-show]');
|
||||||
|
|
||||||
|
injectCss();
|
||||||
|
var card = document.createElement('div');
|
||||||
|
card.className = 'atk-err-card';
|
||||||
|
card.setAttribute('data-raw', raw);
|
||||||
|
card.innerHTML =
|
||||||
|
'<span class="atk-err-ic">' + info.ic + '</span>' +
|
||||||
|
'<div class="atk-err-body">' +
|
||||||
|
'<div class="atk-err-t">' + esc(info.title) + '</div>' +
|
||||||
|
'<div class="atk-err-d">' + esc(desc) + '</div>' +
|
||||||
|
'<div class="atk-err-acts"></div>' +
|
||||||
|
(detail ? '<details class="atk-err-det"><summary>技术详情(给站长排查用)</summary><pre>' +
|
||||||
|
esc((code ? '[' + code + '] ' : '') + detail) + '</pre></details>' : '') +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
layer.textContent = '';
|
||||||
|
layer.appendChild(card);
|
||||||
|
|
||||||
|
var acts = card.querySelector('.atk-err-acts');
|
||||||
|
if (retry) {
|
||||||
|
retry.textContent = '重新加载';
|
||||||
|
retry.removeAttribute('style');
|
||||||
|
retry.className = 'atk-err-retry';
|
||||||
|
acts.appendChild(retry); // 移动节点 → 原点击事件保留
|
||||||
|
} else {
|
||||||
|
var b = document.createElement('span');
|
||||||
|
b.className = 'atk-err-retry';
|
||||||
|
b.textContent = '重新加载';
|
||||||
|
b.onclick = function () { location.reload(); };
|
||||||
|
acts.appendChild(b);
|
||||||
|
}
|
||||||
|
if (admin && admin.offsetParent !== null) acts.appendChild(admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scan() {
|
||||||
|
var layers = document.querySelectorAll('.atk-error-layer');
|
||||||
|
for (var i = 0; i < layers.length; i++) rewrite(layers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function boot() {
|
||||||
|
scan();
|
||||||
|
if (!document.body) { setTimeout(boot, 300); return; }
|
||||||
|
// 错误层是 Artalk 异步渲染出来的,用观察器盯着;PJAX 重建也能命中
|
||||||
|
new MutationObserver(function () { scan(); }).observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
|
||||||
|
else boot();
|
||||||
|
})();
|
||||||
|
|
||||||
// Global Artalk Initialization Function (Supports PJAX)
|
// Global Artalk Initialization Function (Supports PJAX)
|
||||||
|
|
||||||
// ── 持久化 artalkInitCount (跨 PJAX 切换保留) ──
|
// ── 持久化 artalkInitCount (跨 PJAX 切换保留) ──
|
||||||
|
|||||||
Reference in new issue
Block a user