From afa1346a0f6f40218d29029147df08cf2cea1c42 Mon Sep 17 00:00:00 2001 From: zqlit Date: Sat, 3 Oct 2026 19:49:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=84=E8=AE=BA=E5=8A=A0=E8=BD=BD=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=94=B9=E4=B8=BA=E6=B8=85=E6=99=B0=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=EF=BC=9A=E6=8E=A5=E7=AE=A1=20Artalk=20=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=B1=82=EF=BC=8C=E6=8C=89=E6=9C=8D=E5=8A=A1=E7=AB=AF=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=A0=81=E7=BB=99=E8=AF=BB=E8=80=85=E4=BA=BA=E8=AF=9D?= =?UTF-8?q?=E6=96=87=E6=A1=88+=E5=8F=AF=E5=B1=95=E5=BC=80=E6=8A=80?= =?UTF-8?q?=E6=9C=AF=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/Ying/assets/js/modules/artalk.js | 173 ++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/themes/Ying/assets/js/modules/artalk.js b/themes/Ying/assets/js/modules/artalk.js index 89ab131d..fe36e98c 100644 --- a/themes/Ying/assets/js/modules/artalk.js +++ b/themes/Ying/assets/js/modules/artalk.js @@ -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, '>'); + } + + 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 = + '' + info.ic + '' + + '
' + + '
' + esc(info.title) + '
' + + '
' + esc(desc) + '
' + + '
' + + (detail ? '
技术详情(给站长排查用)
' +
+                esc((code ? '[' + code + '] ' : '') + detail) + '
' : '') + + '
'; + + 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) // ── 持久化 artalkInitCount (跨 PJAX 切换保留) ──