编辑器禁用态:置灰但内容完整(补发送字样/占位符/表情预览图标),svc-down 隐藏 Powered By 版权
Deploy to Production / pre-check (push) Successful in 35s
Deploy to Production / build (push) Successful in 3m40s
Deploy to Production / deploy-edgeone (push) Successful in 3m35s
Deploy to Production / finalize (push) Successful in 22s
Deploy to Production / notify-failure (push) Skipped
Deploy to Production / pre-check (push) Successful in 35s
Deploy to Production / build (push) Successful in 3m40s
Deploy to Production / deploy-edgeone (push) Successful in 3m35s
Deploy to Production / finalize (push) Successful in 22s
Deploy to Production / notify-failure (push) Skipped
This commit is contained in:
1 parent
298f10e6bc
commit
855a001046
1 file changed
+151
-8
@@ -27,8 +27,11 @@
|
||||
if (typeof origFetch === 'function') {
|
||||
window.fetch = function (input) {
|
||||
var url = typeof input === 'string' ? input : (input && input.url) || '';
|
||||
url = String(url);
|
||||
var p = origFetch.apply(this, arguments);
|
||||
if (String(url).indexOf('/api/v2/') === -1) return p;
|
||||
if (url.indexOf('/api/v2/') === -1) return p;
|
||||
// 学习 API 源(探测恢复用)
|
||||
if (!apiBase && url.indexOf('/api/v2/') > 0) apiBase = url.split('/api/v2/')[0];
|
||||
return p.then(function (res) {
|
||||
if (res && res.ok === false) {
|
||||
try {
|
||||
@@ -40,10 +43,15 @@
|
||||
detail: (d && d.detail) || '',
|
||||
at: Date.now()
|
||||
};
|
||||
if (last.code && last.code !== 'network') setSvcDown(last.code);
|
||||
}).catch(function () {
|
||||
last = { status: res.status, code: '', msg: '', detail: 'HTTP ' + res.status, at: Date.now() };
|
||||
});
|
||||
} catch (e) { /* clone 不支持就放弃记录,不影响请求 */ }
|
||||
} else if (res && res.ok) {
|
||||
// 某些端点不读 D1(/human/* 走 KV),它们 200 不代表服务恢复;
|
||||
// 只有真正读 D1 的端点成功才算恢复
|
||||
if (svcDown && /\/(conf|comments|captcha|votes|pages)/.test(url)) setSvcUp();
|
||||
}
|
||||
return res;
|
||||
}).catch(function (e) {
|
||||
@@ -63,8 +71,9 @@
|
||||
var st = document.createElement('style');
|
||||
st.id = 'atk-err-css';
|
||||
st.textContent =
|
||||
'.atk-error-layer{padding:0!important;margin:14px 0 30px!important;background:transparent!important}' +
|
||||
'.atk-err-card{display:block;text-align:center;padding:30px 22px;box-sizing:border-box;' +
|
||||
'.atk-error-layer{position:relative!important;top:0!important;left:0!important;height:auto!important;' +
|
||||
'padding:0!important;margin:14px 0 40px!important;background:transparent!important;z-index:auto!important}' +
|
||||
'.atk-err-card{display:block;width:100%;box-sizing:border-box;text-align:center;padding:30px 22px;' +
|
||||
'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{display:block;font-size:30px;line-height:1;margin-bottom:12px}' +
|
||||
@@ -77,11 +86,126 @@
|
||||
'font-size:13px;font-weight:600;cursor:pointer;transition:background .2s,color .2s,transform .2s;user-select:none}' +
|
||||
'.atk-err-retry:hover{background:var(--theme-main,#07c160);color:#fff}' +
|
||||
'.atk-err-retry:active{transform:scale(.96)}' +
|
||||
'@media (max-width:640px){.atk-error-layer{margin:10px 0 24px!important}.atk-err-card{padding:20px 14px}.atk-err-ic{font-size:26px}' +
|
||||
'.atk-err-t{font-size:14px}.atk-err-d{font-size:12.5px}}';
|
||||
'@media (max-width:640px){.atk-error-layer{margin:10px 0 30px!important}.atk-err-card{padding:20px 14px}.atk-err-ic{font-size:26px}' +
|
||||
'.atk-err-t{font-size:14px}.atk-err-d{font-size:12.5px}}' +
|
||||
'.atk-svc-down .atk-main-editor{opacity:.55!important;filter:grayscale(1)!important}' +
|
||||
'.atk-svc-down .atk-copyright{display:none!important}' +
|
||||
'.atk-svc-down .atk-main-editor *{pointer-events:none}' +
|
||||
'.atk-svc-down .atk-human-box{display:none}' +
|
||||
'.atk-svc-down .atk-list-body{min-height:0!important}' +
|
||||
'.atk-editor-off-bar{display:flex;align-items:center;gap:8px;margin:12px 0 10px;padding:9px 14px;border-radius:8px;' +
|
||||
'background:rgba(7,193,96,.06);border:1px solid rgba(7,193,96,.22);font-size:13px;line-height:1.6;color:var(--text-main,#333)}' +
|
||||
'[data-theme="dark"] .atk-editor-off-bar{background:rgba(7,193,96,.08);border-color:rgba(7,193,96,.3)}' +
|
||||
'@media (max-width:640px){.atk-editor-off-bar{margin:10px 0 8px;padding:8px 12px;font-size:12.5px}}';
|
||||
document.head.appendChild(st);
|
||||
}
|
||||
|
||||
// ── 服务不可用时的编辑器禁用态 ─────────────────────────────────────
|
||||
// 列表挂了编辑器还开着,用户打完字点提交却毫无反馈 —— 最差的体验。
|
||||
// 这里:编辑器整体置灰(内容原样可见,只是变灰)+ 编辑框上方插窄横幅
|
||||
// 说明原因和恢复时间,并每 60s 用一次 1 行 D1 成本的探针(/captcha/status,
|
||||
// 只读 1 行 settings)探测恢复,恢复后自动解除禁用并 toast,不用刷新页面。
|
||||
|
||||
// conf 加载失败时 Artalk 渲染的是"空壳编辑器"(i18n 没加载):发送按钮
|
||||
// 没有字、输入框没有占位符、工具栏按钮整排缺失。置灰态要保留完整外观,
|
||||
// 这里按官方标记把缺的字和图标补齐(幂等,Artalk 重建编辑器后可重复跑)。
|
||||
// SVG 与 libs/Artalk.js 里 useBtn() 的原生节点逐字节一致,视觉与正常态无差。
|
||||
var PLUG_BTNS =
|
||||
'<span class="atk-plug-btn"><i aria-label="表情"><svg fill="currentColor" aria-hidden="true" height="14" viewBox="0 0 14 14" width="14"><path d="m4.26829 5.29294c0-.94317.45893-1.7074 1.02439-1.7074.56547 0 1.02439.76423 1.02439 1.7074s-.45892 1.7074-1.02439 1.7074c-.56546 0-1.02439-.76423-1.02439-1.7074zm4.43903 1.7074c.56546 0 1.02439-.76423 1.02439-1.7074s-.45893-1.7074-1.02439-1.7074c-.56547 0-1.02439.76423-1.02439 1.7074s.45892 1.7074 1.02439 1.7074zm-1.70732 2.73184c-1.51883 0-2.06312-1.52095-2.08361-1.58173l-1.29551.43231c.03414.10244.868 2.51604 3.3798 2.51604 2.51181 0 3.34502-2.41291 3.37982-2.51604l-1.29484-.43573c-.02254.06488-.56683 1.58583-2.08498 1.58583zm7-2.73252c0 3.86004-3.1401 7.00034-7 7.00034s-7-3.1396-7-6.99966c0-3.86009 3.1401-7.00034 7-7.00034s7 3.14025 7 7.00034zm-1.3659 0c0-3.10679-2.5275-5.63442-5.6341-5.63442-3.10663 0-5.63415 2.52832-5.63415 5.6351 0 3.10676 2.52752 5.63446 5.63415 5.63446 3.1066 0 5.6341-2.5277 5.6341-5.63446z"/></svg></i></span>' +
|
||||
'<span class="atk-plug-btn"><i aria-label="预览"><svg fill="currentColor" aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg></i></span>';
|
||||
|
||||
function dressEditor(ed) {
|
||||
var btn = ed.querySelector('.atk-send-btn');
|
||||
if (btn && !btn.textContent.trim()) btn.textContent = '发送';
|
||||
var ta = ed.querySelector('.atk-textarea');
|
||||
if (ta && !ta.getAttribute('placeholder')) ta.setAttribute('placeholder', '输入内容(支持 Markdown)…');
|
||||
var ph = { 'atk-nick': '昵称', 'atk-email': '邮箱', 'atk-link': '网址' };
|
||||
for (var cls in ph) {
|
||||
var inp = ed.querySelector('input.' + cls);
|
||||
if (inp && !inp.getAttribute('placeholder')) inp.setAttribute('placeholder', ph[cls]);
|
||||
}
|
||||
var plugWrap = ed.querySelector('.atk-plug-btn-wrap');
|
||||
if (plugWrap && !plugWrap.childElementCount) plugWrap.innerHTML = PLUG_BTNS;
|
||||
}
|
||||
|
||||
var svcDown = false;
|
||||
var downCode = '';
|
||||
var probeTimer = null;
|
||||
var apiBase = ''; // 从拦截到的请求里学习 API 源(跨域探测需要绝对地址)
|
||||
|
||||
function svcToast(msg) {
|
||||
var t = document.getElementById('atk-svc-toast');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.id = 'atk-svc-toast';
|
||||
t.style.cssText = 'position:fixed;left:50%;bottom:110px;transform:translateX(-50%);z-index:99999;' +
|
||||
'padding:12px 24px;border-radius:999px;background:var(--theme-main,#07c160);color:#fff;' +
|
||||
'font-size:14px;box-shadow:0 10px 32px rgba(0,0,0,.35);opacity:0;transition:opacity .35s,transform .35s;pointer-events:none';
|
||||
document.body.appendChild(t);
|
||||
}
|
||||
t.textContent = msg;
|
||||
requestAnimationFrame(function () { t.style.opacity = '1'; });
|
||||
setTimeout(function () { t.style.opacity = '0'; }, 3200);
|
||||
}
|
||||
|
||||
function ensureBar(code) {
|
||||
// 状态类无条件加:conf 失败时 Artalk 可能连编辑器都不渲染,
|
||||
// 但类挂在 body 上无副作用;编辑器晚点出现时 scan() 会补挂横幅
|
||||
document.body.classList.add('atk-svc-down');
|
||||
var ed = document.querySelector('.atk-main-editor');
|
||||
if (!ed || !ed.parentNode) return;
|
||||
dressEditor(ed); // 幂等:横幅已插好也要补(PJAX 重建编辑器 / 晚渲染场景)
|
||||
var bar = document.getElementById('atk-editor-off-bar');
|
||||
if (bar && bar.parentNode === ed.parentNode) return; // 已插好
|
||||
if (bar && bar.parentNode) bar.parentNode.removeChild(bar);
|
||||
bar = document.createElement('div');
|
||||
bar.id = 'atk-editor-off-bar';
|
||||
bar.className = 'atk-editor-off-bar uk-animation-slide-top-small'; // UIKit 入场动画
|
||||
bar.innerHTML = code === 'd1_quota'
|
||||
? '⏸ 评论服务暂时不可用(数据库今日额度已用尽),预计明早 8:00 自动恢复,先看看文章吧'
|
||||
: '⏸ 评论服务暂时不可用,暂时无法发表评论,请稍后再试';
|
||||
ed.parentNode.insertBefore(bar, ed);
|
||||
// 编辑器全部子元素已 pointer-events:none,点击会落到编辑器容器本身 ——
|
||||
// 借这个事件弹 toast 告诉用户为什么点不动(幂等:节点换新才重挂)
|
||||
if (!ed.dataset.svcGuard) {
|
||||
ed.dataset.svcGuard = '1';
|
||||
ed.addEventListener('click', function () {
|
||||
svcToast(downCode === 'd1_quota'
|
||||
? '评论服务暂时不可用(额度已用尽),预计明早 8:00 自动恢复'
|
||||
: '评论服务暂时不可用,请稍后再试');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeBar() {
|
||||
document.body.classList.remove('atk-svc-down');
|
||||
var bar = document.getElementById('atk-editor-off-bar');
|
||||
if (bar && bar.parentNode) bar.parentNode.removeChild(bar);
|
||||
}
|
||||
|
||||
function setSvcDown(code) {
|
||||
downCode = code;
|
||||
if (svcDown) { ensureBar(code); return; } // 已在禁用态,只刷新文案
|
||||
svcDown = true;
|
||||
ensureBar(code);
|
||||
if (!probeTimer) {
|
||||
probeTimer = setInterval(function () {
|
||||
if (!apiBase) return;
|
||||
fetch(apiBase + '/api/v2/captcha/status').then(function (r) {
|
||||
if (r.ok) setSvcUp();
|
||||
}).catch(function () { /* 还没恢复 */ });
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
function setSvcUp() {
|
||||
if (!svcDown) return;
|
||||
svcDown = false;
|
||||
if (probeTimer) { clearInterval(probeTimer); probeTimer = null; }
|
||||
removeBar();
|
||||
svcToast('评论服务已恢复,可以继续评论了 ✓');
|
||||
}
|
||||
|
||||
function rewrite(layer) {
|
||||
if (!layer) return;
|
||||
|
||||
@@ -90,7 +214,6 @@
|
||||
if (em) raw = (em.innerText || em.textContent || '').trim();
|
||||
|
||||
var prev = layer.querySelector('.atk-err-card');
|
||||
if (prev && prev.getAttribute('data-raw') === raw) return; // 内容没变,不重复渲染
|
||||
|
||||
// 优先用结构化错误码;没有就从 Artalk 原始错误文本里认特征词
|
||||
// (D1 额度用尽 / 网络失败都有固定措辞)。结构化记录有时效且 Artalk
|
||||
@@ -102,6 +225,10 @@
|
||||
: /D1_ERROR|exceeded D1|free tier|额度已用尽|暂时不可用|加载失败|无法获取|出了点小状况/i.test(raw) ? 'd1_quota'
|
||||
: /failed to fetch|network ?error|load failed/i.test(raw) ? 'network'
|
||||
: 'internal');
|
||||
// 幂等:同一份「原文+结论」渲染过就不再动。结论会变(结构化记录晚到时
|
||||
// internal 升级成 d1_quota),所以 key 里带上 code,变了就重渲染
|
||||
if (prev && prev.getAttribute('data-raw') === raw + '|' + code) return;
|
||||
|
||||
var info = INFO[code] || INFO.internal;
|
||||
// 服务端文案是给「直接调 API 的人」写的,自带一句"评论服务暂时不可用:",
|
||||
// 放到卡片里会和标题重复,这里去掉冗余前缀
|
||||
@@ -109,6 +236,9 @@
|
||||
? fresh.msg.replace(/^评论服务暂时不可用(?:[((][^))]*[))])?[::]\s*/, '')
|
||||
: info.desc;
|
||||
|
||||
// 服务端故障(非读者自身网络问题)→ 禁用编辑器并挂说明横幅
|
||||
if (code !== 'network') setSvcDown(code);
|
||||
|
||||
// 原始「重试」节点:Vue 会整层重建,所以每次重新找;
|
||||
// 若本层已重写过,就从我们自己卡片里把它取回来(移动 DOM 保留已绑事件)
|
||||
var retry = prev ? prev.querySelector('.atk-err-retry') : null;
|
||||
@@ -125,7 +255,7 @@
|
||||
injectCss();
|
||||
var card = document.createElement('div');
|
||||
card.className = 'atk-err-card';
|
||||
card.setAttribute('data-raw', raw);
|
||||
card.setAttribute('data-raw', raw + '|' + code);
|
||||
card.innerHTML =
|
||||
'<span class="atk-err-ic">' + info.ic + '</span>' +
|
||||
'<div class="atk-err-t">' + esc(info.title) + '</div>' +
|
||||
@@ -154,13 +284,26 @@
|
||||
function scan() {
|
||||
var layers = document.querySelectorAll('.atk-error-layer');
|
||||
for (var i = 0; i < layers.length; i++) rewrite(layers[i]);
|
||||
// PJAX 换页后编辑器是新的 DOM:禁用态还在就把横幅补回去;恢复了就清掉残留
|
||||
if (svcDown) {
|
||||
if (downCode) ensureBar(downCode);
|
||||
} else if (document.getElementById('atk-editor-off-bar')) {
|
||||
removeBar();
|
||||
}
|
||||
}
|
||||
|
||||
function boot() {
|
||||
scan();
|
||||
if (!document.body) { setTimeout(boot, 300); return; }
|
||||
// 错误层是 Artalk 异步渲染出来的,用观察器盯着;PJAX 重建也能命中
|
||||
new MutationObserver(function () { scan(); }).observe(document.body, { childList: true, subtree: true });
|
||||
var scanT = null;
|
||||
new MutationObserver(function () {
|
||||
scan();
|
||||
// 延迟再扫一次:等 fetch 包装层的结构化记录落地(竞态兜底 ——
|
||||
// 晚到的结论会通过 data-raw 变化触发重渲染)
|
||||
if (scanT) clearTimeout(scanT);
|
||||
scanT = setTimeout(scan, 250);
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
|
||||
|
||||
Reference in new issue
Block a user