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((code ? '[' + code + '] ' : '') + detail) + '