perf(comments): 评论加载提速 + 骨架屏 + 友链弹窗 pjax 修复 + toast 统一 Message.js
优化: - initArtalk 加节点级幂等守卫(mount.__atkInited,失败回滚), 消除 artalk.html/mypjax 多入口导致的 4× comments + 4× pv 重复请求 - fetch 包装层对无 body 的 GET/HEAD 去掉多余 content-type, 消掉 Artalk 对 GET /comments 的 CORS 预检(按完整 URL 缓存, 每篇文章都重付一次 RTT)。实测评论列表 +1150~1565ms → +626ms 修复: - 评论列表骨架屏改注入 .atk-list-body(原骨架在 #Comments 内, 被 Artalk.init 清空),list-loaded/故障/12s 兜底移除 - 友链申请弹窗抽成 modules/friendlink.js 进 page-only bundle (原内联脚本在 #pjax-container 外,pjax 后 flApplyOpenModal 未定义) - toast 统一走本地化 Message.js/Qmsg 适配层(保留 window.Toast/showToast 旧 API) 文档: - README / 架构总览 更新;新增 评论加载优化方案.md
This commit is contained in:
1 parent
9e972f2b61
commit
50a906e07c
14 files changed
+1018
-486
No files matched your search
@@ -4126,76 +4126,41 @@ input[type=submit] {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Toast ── */
|
||||
.ying-toast-container {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
/* ── Toast(Message.js / Qmsg 适配,见 assets/js/modules/toast.js)──
|
||||
组件自带样式在 css/libs/message.min.css(随 style.css 打包)。
|
||||
这里只做主题融合:跟随主题变量 + 暗色适配。 */
|
||||
.qmsg {
|
||||
border-radius: 8px;
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.ying-toast {
|
||||
padding: 9px 18px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
.qmsg-content {
|
||||
color: var(--text-main, #333);
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.28s ease, transform 0.28s ease;
|
||||
pointer-events: auto;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.ying-toast--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
[data-theme="dark"] .qmsg {
|
||||
background: #2a2e33;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.ying-toast--success {
|
||||
background: rgba(34, 197, 94, 0.88);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 32px rgba(34, 197, 94, 0.18);
|
||||
[data-theme="dark"] .qmsg-content {
|
||||
color: #d6dade;
|
||||
}
|
||||
|
||||
.ying-toast--error {
|
||||
background: rgba(239, 68, 68, 0.88);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 32px rgba(239, 68, 68, 0.18);
|
||||
[data-theme="dark"] .qmsg-close {
|
||||
color: #7a828a;
|
||||
}
|
||||
|
||||
.ying-toast--warning {
|
||||
background: rgba(245, 158, 11, 0.88);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 32px rgba(245, 158, 11, 0.18);
|
||||
}
|
||||
|
||||
.ying-toast--info {
|
||||
background: rgba(29, 61, 108, 0.85);
|
||||
color: #f0f4fb;
|
||||
box-shadow: 0 8px 32px rgba(29, 61, 108, 0.18);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .ying-toast--info {
|
||||
background: rgba(159, 196, 244, 0.22);
|
||||
color: #f3f7fd;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
[data-theme="dark"] .qmsg-close:hover {
|
||||
color: #a8b0b8;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.ying-toast {
|
||||
.qmsg {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,17 +21,94 @@
|
||||
internal: { ic: '⚠️', title: '评论暂时加载不出来', desc: '服务出了点小状况,请稍后再试。' }
|
||||
};
|
||||
|
||||
// ── /api/v2/conf 内联快照 ──────────────────────────────────────────
|
||||
// 构建期把 conf 响应内联进页面(见 partials/artalk.html)。服务端这份 conf
|
||||
// 只由 settings 表决定,唯一因人而异的字段是 imgUpload(imgUpload || admin),
|
||||
// 所以匿名访客看到的 conf 人人都一样 —— 匿名请求直接本地合成 Response,
|
||||
// 不发网络。Artalk 内部 conf 与评论列表是严格串行的,省掉这一跳等于首屏
|
||||
// 少一整个跨境往返。
|
||||
// 带 Authorization 的请求来自已登录用户(管理员拿到 imgUpload=true),属个性化
|
||||
// 响应,放行真实网络。
|
||||
var CONF_URI = /\/api\/v2\/conf(\?|$)/;
|
||||
function hasAuthHeader(init, input) {
|
||||
try {
|
||||
var h = (init && init.headers) || null;
|
||||
if (!h && input && typeof input !== 'string') h = input.headers || null;
|
||||
if (!h) return false;
|
||||
if (typeof h.get === 'function') return !!h.get('Authorization');
|
||||
if (Array.isArray(h)) {
|
||||
for (var i = 0; i < h.length; i++) {
|
||||
if (String(h[i][0]).toLowerCase() === 'authorization') return !!h[i][1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return !!(h.Authorization || h.authorization);
|
||||
} catch (e) {
|
||||
return true; // 判不准就当有凭据 → 放行真实请求(保守,宁可多一跳也不串号)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 0) 去掉 GET/HEAD 上多余的 content-type:省掉一次 CORS 预检 ──────
|
||||
// Artalk 给「所有」请求都带上 content-type: application/json,可 GET 根本没有 body。
|
||||
// content-type 属于「非简单头」→ 浏览器必须先发一个 OPTIONS 预检(Access-Control-Request-Headers: content-type)。
|
||||
// 预检按「完整 URL」缓存(含 query),而 comments 的 URL 带 page_key、每篇文章都不同,
|
||||
// 于是每进一篇新文章都要重付一次预检 —— 跨域下这是实打实的整整一个 RTT,
|
||||
// 而它正好卡在评论列表的关键路径上(预检 → 才发 GET)。
|
||||
// GET/HEAD 没有 body,删掉这个头不改变任何语义,却能直接把预检砍掉。
|
||||
function sansContentType(init) {
|
||||
var out = {}, k;
|
||||
for (k in init) out[k] = init[k];
|
||||
var h = out.headers;
|
||||
try {
|
||||
if (h && typeof Headers === 'function' && h instanceof Headers) {
|
||||
var nh = new Headers();
|
||||
h.forEach(function (v, n) { if (n.toLowerCase() !== 'content-type') nh.append(n, v); });
|
||||
out.headers = nh;
|
||||
} else if (Array.isArray(h)) {
|
||||
out.headers = h.filter(function (p) { return String(p[0]).toLowerCase() !== 'content-type'; });
|
||||
} else if (h && typeof h === 'object') {
|
||||
var nh2 = {};
|
||||
for (k in h) { if (k.toLowerCase() !== 'content-type') nh2[k] = h[k]; }
|
||||
out.headers = nh2;
|
||||
}
|
||||
} catch (e) {
|
||||
return init; // headers 形态不认识就原样放行(宁可多一次预检,不能出错)
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── 1) 记录 API 失败原因。只读不改:包装层原样返回上游响应,
|
||||
// 失败时顺带 clone 一份读 body(clone 不影响 Artalk 自己读)。
|
||||
var origFetch = window.fetch;
|
||||
if (typeof origFetch === 'function') {
|
||||
window.fetch = function (input) {
|
||||
window.fetch = function (input, init) {
|
||||
var url = typeof input === 'string' ? input : (input && input.url) || '';
|
||||
url = String(url);
|
||||
var p = origFetch.apply(this, arguments);
|
||||
if (url.indexOf('/api/v2/') === -1) return p;
|
||||
// 学习 API 源(探测恢复用)
|
||||
if (url.indexOf('/api/v2/') === -1) return origFetch.apply(this, arguments);
|
||||
// 学习 API 源(探测恢复用)—— 拦快照之前先学,别因为少了一次网络请求而丢掉来源
|
||||
if (!apiBase && url.indexOf('/api/v2/') > 0) apiBase = url.split('/api/v2/')[0];
|
||||
|
||||
// conf 快照命中:本地合成响应,完全不发网络
|
||||
if (window.__artalkConfSnapshot && CONF_URI.test(url)
|
||||
&& typeof Response === 'function' && !hasAuthHeader(init, input)) {
|
||||
return Promise.resolve(new Response(
|
||||
JSON.stringify(window.__artalkConfSnapshot),
|
||||
{
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// GET/HEAD 且无 body:去掉 content-type,避免不必要的 CORS 预检
|
||||
var fargs = arguments;
|
||||
var method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
||||
if ((method === 'GET' || method === 'HEAD') && !(init && init.body)) {
|
||||
fargs = [input, sansContentType(init || {})];
|
||||
}
|
||||
|
||||
var p = origFetch.apply(this, fargs);
|
||||
return p.then(function (res) {
|
||||
if (res && res.ok === false) {
|
||||
try {
|
||||
@@ -105,6 +182,7 @@
|
||||
// 这里:编辑器整体置灰(内容原样可见,只是变灰)+ 编辑框上方插窄横幅
|
||||
// 说明原因和恢复时间,并每 60s 用一次 1 行 D1 成本的探针(/captcha/status,
|
||||
// 只读 1 行 settings)探测恢复,恢复后自动解除禁用并 toast,不用刷新页面。
|
||||
// toast 统一走 Message.js(modules/toast.js 适配层),不再自绘浮层。
|
||||
|
||||
// conf 加载失败时 Artalk 渲染的是"空壳编辑器"(i18n 没加载):发送按钮
|
||||
// 没有字、输入框没有占位符、工具栏按钮整排缺失。置灰态要保留完整外观,
|
||||
@@ -130,22 +208,18 @@
|
||||
|
||||
var svcDown = false;
|
||||
var downCode = '';
|
||||
var downAt = 0; // 进入禁用态的时刻(用来判断这次"故障"是不是一闪而过)
|
||||
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);
|
||||
// 统一通知组件(Message.js):Toast 在 core bundle 里先于本模块加载,
|
||||
// 但 PJAX 极端时序下仍兜底判一次,避免 undefined 直接抛错
|
||||
if (window.Toast && typeof window.Toast.info === 'function') {
|
||||
window.Toast.info(msg, 3200);
|
||||
} else {
|
||||
try { console.log('[artalk]', msg); } catch (e) {}
|
||||
}
|
||||
t.textContent = msg;
|
||||
requestAnimationFrame(function () { t.style.opacity = '1'; });
|
||||
setTimeout(function () { t.style.opacity = '0'; }, 3200);
|
||||
}
|
||||
|
||||
function ensureBar(code) {
|
||||
@@ -185,8 +259,11 @@
|
||||
|
||||
function setSvcDown(code) {
|
||||
downCode = code;
|
||||
// 评论服务故障:列表不会返回了,别让骨架屏一直转(故障态有自己的横幅/卡片)
|
||||
listSkeletonDone();
|
||||
if (svcDown) { ensureBar(code); return; } // 已在禁用态,只刷新文案
|
||||
svcDown = true;
|
||||
downAt = Date.now();
|
||||
ensureBar(code);
|
||||
if (!probeTimer) {
|
||||
probeTimer = setInterval(function () {
|
||||
@@ -203,12 +280,26 @@
|
||||
svcDown = false;
|
||||
if (probeTimer) { clearInterval(probeTimer); probeTimer = null; }
|
||||
removeBar();
|
||||
svcToast('评论服务已恢复,可以继续评论了 ✓');
|
||||
// 禁用态只持续了一瞬间就别报喜:读者眨个眼就"恢复"的故障,弹提示纯属噪音
|
||||
// (真实故障从出现到恢复不会只有两三秒,所以门槛不会漏报)
|
||||
if (downAt && Date.now() - downAt >= 3000) {
|
||||
svcToast('评论服务已恢复,可以继续评论了 ✓');
|
||||
}
|
||||
downAt = 0;
|
||||
}
|
||||
|
||||
function rewrite(layer) {
|
||||
if (!layer) return;
|
||||
|
||||
// ── 不是评论列表的错误层,一律不碰 ──────────────────────────────
|
||||
// Artalk 会把「编辑器插件资源加载失败」(表情包 / 图片等)的错误层也渲染成
|
||||
// .atk-error-layer,挂在编辑器插件面板(.atk-plug-panel-wrap)里。它既不是
|
||||
// 评论服务故障,也不该被改写成我们的故障卡片 —— 直接放行走 Artalk 原样式。
|
||||
// 曾经没这道判断时:这类层的文本不在 .error-message 里 → raw 读成空串 →
|
||||
// 兜底判成 internal → 因为 internal !== network 就置灰整个评论区,随后
|
||||
// 数据正常返回又弹「评论服务已恢复」toast(读者视角:页面顿一下 + 莫名提示)。
|
||||
if (layer.closest('.atk-plug-panel-wrap, .atk-editor-plug-emoticons')) return;
|
||||
|
||||
var raw = '';
|
||||
var em = layer.querySelector('.error-message');
|
||||
if (em) raw = (em.innerText || em.textContent || '').trim();
|
||||
@@ -236,8 +327,10 @@
|
||||
? fresh.msg.replace(/^评论服务暂时不可用(?:[((][^))]*[))])?[::]\s*/, '')
|
||||
: info.desc;
|
||||
|
||||
// 服务端故障(非读者自身网络问题)→ 禁用编辑器并挂说明横幅
|
||||
if (code !== 'network') setSvcDown(code);
|
||||
// 服务端故障(非读者自身网络问题)→ 禁用编辑器并挂说明横幅。
|
||||
// 但「读不到任何可归因证据」时不下结论:raw 为空且结构化记录也没命中,
|
||||
// 说明这是个我们识别不了的错误层,宁可只渲染卡片,也不要误置灰评论区。
|
||||
if (code !== 'network' && (raw || (fresh && fresh.code))) setSvcDown(code);
|
||||
|
||||
// 原始「重试」节点:Vue 会整层重建,所以每次重新找;
|
||||
// 若本层已重写过,就从我们自己卡片里把它取回来(移动 DOM 保留已绑事件)
|
||||
@@ -501,8 +594,91 @@ window.loadScriptOnce = window.loadScriptOnce || function(src, globalName, callb
|
||||
document.head.appendChild(script);
|
||||
};
|
||||
|
||||
/* ── 评论列表骨架屏 ────────────────────────────────────────────────────
|
||||
Artalk 初始化时会把 #Comments 整个清空(连服务端渲染出来的骨架屏一起清),
|
||||
而评论列表要等一次跨境 /api/v2/comments 往返才有内容 —— 这段时间评论区是
|
||||
一片空白(本地实测空档约 1.0~1.5s,跨境只会更久),读者视角就是"进来啥也没有"。
|
||||
这里在 Artalk 挂载后往 .atk-list-body 里补一份骨架,list-loaded / 服务故障 /
|
||||
12s 兜底时移除,把这段空档抹平。
|
||||
延迟 120ms 才显示:命中缓存、极快返回的情况不会闪一下骨架。 */
|
||||
var LIST_SKELETON_HTML =
|
||||
'<div class="atk-loading-item">' +
|
||||
'<div class="atk-loading-avatar"></div>' +
|
||||
'<div class="atk-loading-content">' +
|
||||
'<div class="atk-loading-line width-name"></div>' +
|
||||
'<div class="atk-loading-line width-80"></div>' +
|
||||
'<div class="atk-loading-line width-60"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="atk-loading-item">' +
|
||||
'<div class="atk-loading-avatar"></div>' +
|
||||
'<div class="atk-loading-content">' +
|
||||
'<div class="atk-loading-line width-name"></div>' +
|
||||
'<div class="atk-loading-line width-60"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="atk-loading-item">' +
|
||||
'<div class="atk-loading-avatar"></div>' +
|
||||
'<div class="atk-loading-content">' +
|
||||
'<div class="atk-loading-line width-name"></div>' +
|
||||
'<div class="atk-loading-line width-80"></div>' +
|
||||
'<div class="atk-loading-line width-40"></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
var listSkelTimer = null; // 延迟布防用的定时器
|
||||
var listSkelSettled = false; // 本次导航的列表是否已经有结果(含失败)
|
||||
|
||||
function listSkeletonShow() {
|
||||
var body = document.querySelector('#Comments .atk-list-body');
|
||||
if (!body || body.querySelector('.atk-ys-skel')) return;
|
||||
var skel = document.createElement('div');
|
||||
skel.className = 'atk-ys-skel atk-loading-placeholder';
|
||||
skel.setAttribute('aria-hidden', 'true');
|
||||
skel.innerHTML = LIST_SKELETON_HTML;
|
||||
body.insertBefore(skel, body.firstChild);
|
||||
// 注意:Artalk 渲染评论时若整体重写 .atk-list-body 的 innerHTML,
|
||||
// 骨架会随之被自然移除 —— 因此这里不做任何"占位"逻辑,移除是幂等的。
|
||||
}
|
||||
|
||||
function listSkeletonHide() {
|
||||
var skels = document.querySelectorAll('#Comments .atk-ys-skel');
|
||||
for (var i = 0; i < skels.length; i++) {
|
||||
if (skels[i].parentNode) skels[i].parentNode.removeChild(skels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* 布防:Artalk 挂载后调用(可重复调用,只生效一次) */
|
||||
function listSkeletonArm() {
|
||||
if (listSkelSettled || listSkelTimer) return;
|
||||
listSkelTimer = setTimeout(function () {
|
||||
listSkelTimer = null;
|
||||
if (listSkelSettled) return;
|
||||
listSkeletonShow();
|
||||
// 极端时序下 .atk-list-body 可能还没渲染出来:短轮询补一次
|
||||
if (!document.querySelector('#Comments .atk-ys-skel')) {
|
||||
var tries = 0;
|
||||
var spin = setInterval(function () {
|
||||
if (listSkelSettled || ++tries > 12) { clearInterval(spin); return; }
|
||||
if (document.querySelector('#Comments .atk-list-body')) {
|
||||
listSkeletonShow();
|
||||
clearInterval(spin);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, 120);
|
||||
}
|
||||
|
||||
/* 撤防:列表有结果(成功/失败/超时)后调用 */
|
||||
function listSkeletonDone() {
|
||||
listSkelSettled = true;
|
||||
if (listSkelTimer) { clearTimeout(listSkelTimer); listSkelTimer = null; }
|
||||
listSkeletonHide();
|
||||
}
|
||||
|
||||
window.initArtalk = function() {
|
||||
if (!document.querySelector("#Comments")) return;
|
||||
var mount = document.querySelector("#Comments");
|
||||
if (!mount) return;
|
||||
|
||||
// Check if configuration exists
|
||||
if (!window.artalkConfig) {
|
||||
@@ -517,6 +693,17 @@ window.initArtalk = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 幂等守卫:同一份 #Comments 只初始化一次 ──────────────────────────
|
||||
// initArtalk 有多个调用入口(artalk.html 内联脚本 / main.js DOMContentLoaded /
|
||||
// mypjax 的 script.onload 与 requestIdleCallback),不去重就会在同一页上创建
|
||||
// 多个 Artalk 实例:实测每次 PJAX 进文章页发出 4× /api/v2/comments + 4×
|
||||
// /pages/pv,列表还要等最慢那条响应才渲染(既拖首屏,又 4 倍消耗 D1 读额度)。
|
||||
// 用「节点级」标记而不是全局标志:PJAX 换页会换掉 #Comments 节点,新节点无标记
|
||||
// → 照常重新初始化,切页行为完全不受影响。
|
||||
// 第二道判断(节点内已有 .atk-list-body)兜底 Artalk 若替换了挂载节点的情况。
|
||||
if (mount.__atkInited || mount.querySelector('.atk-list-body')) return;
|
||||
mount.__atkInited = true;
|
||||
|
||||
try {
|
||||
// Logic for caching Artalk configuration to speed up rendering
|
||||
// 1. Manual Refresh (F5): Request from server and save to localStorage
|
||||
@@ -578,6 +765,14 @@ window.initArtalk = function() {
|
||||
artalk.setDarkMode(currentTheme === 'dark');
|
||||
}
|
||||
|
||||
// ── 评论列表骨架屏布防(见文件上方 listSkeleton* 注释) ──
|
||||
listSkelSettled = false;
|
||||
listSkeletonArm();
|
||||
artalk.on('mounted', listSkeletonArm);
|
||||
artalk.on('list-loaded', listSkeletonDone);
|
||||
// 兜底:长时间没结果(严重慢网 / 列表接口异常)也别让骨架一直转
|
||||
setTimeout(listSkeletonDone, 12000);
|
||||
|
||||
// ── 增强评论框字段属性 (兼容油猴自动填充脚本) ──
|
||||
// 为 Artalk 输入框添加额外属性,确保第三方脚本(如博客网站留言评论信息自动填充)
|
||||
// 能够通过 type/id/name/placeholder/class/aria-label 准确匹配
|
||||
@@ -1183,5 +1378,9 @@ artalk.on('list-loaded', function() {
|
||||
|
||||
} catch (e) {
|
||||
console.error('Artalk init failed:', e);
|
||||
// 初始化失败要撤回幂等标记,否则后续入口的调用会被守卫拦掉、评论区再也起不来
|
||||
mount.__atkInited = false;
|
||||
listSkelSettled = true;
|
||||
listSkeletonHide();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 友链自助申请弹窗
|
||||
*
|
||||
* 为什么独立成模块(而不是留在 layouts/page/comment.html 的内联 <script>):
|
||||
* 那段脚本写在 #pjax-container 外面,PJAX 只换容器内容、不会重新执行它 ——
|
||||
* 用户从首页 PJAX 进留言页时 window.flApplyOpenModal 根本没定义,
|
||||
* 「申请友链」卡片点了没反应;直接刷新(整页加载)才正常。抽到
|
||||
* page-only bundle 里随 PJAX 加载,两种进入方式行为一致。
|
||||
*
|
||||
* 幂等性:弹窗节点挂 document.body(跨 PJAX 存活),buildModal 二次调用直接复用;
|
||||
* 事件监听只在首次构建时挂,不会重复注册。
|
||||
*/
|
||||
(function () {
|
||||
if (window.__flApplyInit) return; // PJAX 重复执行防抖
|
||||
window.__flApplyInit = true;
|
||||
|
||||
var BASE = (window.rssApiBase || 'https://api.200181.xyz').replace(/\/$/, '');
|
||||
|
||||
function buildModal() {
|
||||
if (document.getElementById('flApplyMask')) return document.getElementById('flApplyMask');
|
||||
var mask = null;
|
||||
var style = document.createElement('style');
|
||||
style.textContent =
|
||||
'#flApplyMask{position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;padding:16px;' +
|
||||
'background:rgba(15,23,42,.35);-webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3)}' +
|
||||
'#flApplyMask.on{display:flex}' +
|
||||
'#flApplyBox{background:var(--bg-card,#fff);color:var(--text-main,#253244);border-radius:12px;max-width:420px;width:100%;' +
|
||||
'box-sizing:border-box;padding:22px;box-shadow:0 16px 48px rgba(15,23,42,.25);max-height:88vh;overflow:auto}' +
|
||||
'[data-theme="dark"] #flApplyBox{box-shadow:0 16px 48px rgba(0,0,0,.55)}' +
|
||||
'#flApplyBox .fl-field{display:block;margin-bottom:10px}' +
|
||||
'#flApplyBox .fl-label{display:block;font-size:12px;color:var(--text-muted,#8a94a6);margin-bottom:4px}' +
|
||||
'#flApplyBox input,#flApplyBox textarea{width:100%;box-sizing:border-box;padding:8px 10px;' +
|
||||
'border:1px solid var(--border-main,#e7e9ee);border-radius:6px;font-size:14px;' +
|
||||
'background:var(--bg-input,#fff);color:inherit;font-family:inherit}' +
|
||||
'#flApplyBox input:focus,#flApplyBox textarea:focus{outline:none;border-color:var(--theme-main,#07c160);' +
|
||||
'box-shadow:0 0 0 3px var(--theme-main-20,rgba(7,193,96,.2))}' +
|
||||
/* 主题 main.css 有一条全局 button 归一化规则(margin-left:23%; margin-top:10px;
|
||||
border:1px solid; background:#222),会把提交按钮推右溢出弹窗 → 这里用 id 选择器压过它 */
|
||||
'#flSubmit{display:block;width:100%;margin:0;background:var(--theme-main,#07c160);border:none;color:#fff;' +
|
||||
'padding:11px 0;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:filter .2s}' +
|
||||
'#flSubmit:hover{filter:brightness(1.06)}' +
|
||||
'#flSubmit:disabled{opacity:.6;cursor:default}' +
|
||||
/* 弹窗滚动条:细一点、跟随主题,别用系统那根粗滚动条 */
|
||||
'#flApplyBox{overflow-x:hidden;scrollbar-width:thin;scrollbar-color:rgba(140,150,160,.45) transparent}' +
|
||||
'#flApplyBox::-webkit-scrollbar{width:6px;height:6px}' +
|
||||
'#flApplyBox::-webkit-scrollbar-track{background:transparent}' +
|
||||
'#flApplyBox::-webkit-scrollbar-thumb{background:rgba(140,150,160,.45);border-radius:3px}' +
|
||||
'[data-theme="dark"] #flApplyBox{scrollbar-color:rgba(200,205,212,.35) transparent}' +
|
||||
'[data-theme="dark"] #flApplyBox::-webkit-scrollbar-thumb{background:rgba(200,205,212,.35)}';
|
||||
document.head.appendChild(style);
|
||||
|
||||
mask = document.createElement('div');
|
||||
mask.id = 'flApplyMask';
|
||||
mask.innerHTML =
|
||||
'<div id="flApplyBox">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">' +
|
||||
'<b style="font-size:16px;display:flex;align-items:center;gap:8px">' +
|
||||
'<span style="width:4px;height:16px;border-radius:2px;background:var(--theme-main,#07c160);display:inline-block"></span>友链自助申请</b>' +
|
||||
'<span id="flApplyClose" style="cursor:pointer;color:var(--text-muted,#8a94a6);font-size:22px;line-height:1" title="关闭">×</span>' +
|
||||
'</div>' +
|
||||
'<label class="fl-field"><span class="fl-label">站点名称 *</span>' +
|
||||
'<input id="flName" placeholder="我的小站"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">站点链接 *</span>' +
|
||||
'<input id="flUrl" placeholder="https://example.com"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">图标链接(选填,留空自动为你抓取站点图标)</span>' +
|
||||
'<input id="flIcon" placeholder="https://example.com/logo.png"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">订阅地址(RSS/Atom)*</span>' +
|
||||
'<input id="flFeed" placeholder="https://example.com/feed"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">邮箱 *(接收审核结果)</span>' +
|
||||
'<input id="flEmail" type="email" placeholder="you@example.com"></label>' +
|
||||
'<label class="fl-field" style="margin-bottom:14px"><span class="fl-label">站点简介(选填)</span>' +
|
||||
'<textarea id="flDesc" rows="2" style="resize:vertical"></textarea></label>' +
|
||||
'<div id="flMsg" hidden style="font-size:13px;margin-bottom:10px"></div>' +
|
||||
'<button id="flSubmit">提交申请</button>' +
|
||||
'</div>';
|
||||
document.body.appendChild(mask);
|
||||
|
||||
mask.addEventListener('click', function (e) { if (e.target === mask) close(); });
|
||||
document.getElementById('flApplyClose').addEventListener('click', close);
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
||||
|
||||
document.getElementById('flSubmit').addEventListener('click', function () {
|
||||
var btn = this, msg = document.getElementById('flMsg');
|
||||
var payload = {
|
||||
name: document.getElementById('flName').value.trim(),
|
||||
url: document.getElementById('flUrl').value.trim(),
|
||||
image: document.getElementById('flIcon').value.trim(),
|
||||
feed: document.getElementById('flFeed').value.trim(),
|
||||
email: document.getElementById('flEmail').value.trim(),
|
||||
description: document.getElementById('flDesc').value.trim()
|
||||
};
|
||||
if (!payload.name || !payload.url || !payload.feed || !payload.email) {
|
||||
msg.hidden = false; msg.style.color = '#d75a4a'; msg.textContent = '请填写全部必填项'; return;
|
||||
}
|
||||
btn.disabled = true; btn.textContent = '提交中…';
|
||||
msg.hidden = true;
|
||||
fetch(BASE + '/api/link-apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}).then(function (r) { return r.json().then(function (d) { return { code: r.status, d: d }; }); })
|
||||
.then(function (res) {
|
||||
if (res.code === 200 && res.d.ok) {
|
||||
msg.hidden = false; msg.style.color = 'var(--theme-main,#07c160)'; msg.textContent = res.d.message || '申请已提交';
|
||||
btn.textContent = '已提交 ✓';
|
||||
setTimeout(close, 2200);
|
||||
} else {
|
||||
msg.hidden = false; msg.style.color = '#d75a4a'; msg.textContent = res.d.error || '提交失败,请稍后再试';
|
||||
btn.disabled = false; btn.textContent = '提交申请';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
msg.hidden = false; msg.style.color = '#d75a4a'; msg.textContent = '网络异常,请稍后再试';
|
||||
btn.disabled = false; btn.textContent = '提交申请';
|
||||
});
|
||||
});
|
||||
return mask;
|
||||
}
|
||||
|
||||
function close() {
|
||||
var m = document.getElementById('flApplyMask');
|
||||
if (m) m.classList.remove('on');
|
||||
}
|
||||
|
||||
/* 供 artalk.js 的 comment-rendered(置顶评论卡片)调用 */
|
||||
window.flApplyOpenModal = function () {
|
||||
buildModal().classList.add('on');
|
||||
};
|
||||
|
||||
/* 兜底:卡片节点被 PJAX/Artalk 重建后自身监听可能丢失,
|
||||
这里用 document 级事件委托再接一层(打开是幂等的,重复触发无害) */
|
||||
document.addEventListener('click', function (e) {
|
||||
var hit = e.target && e.target.closest && e.target.closest('.atk-fl-card, .atk-fl-card-wrap');
|
||||
if (hit) window.flApplyOpenModal();
|
||||
});
|
||||
})();
|
||||
@@ -1,63 +1,55 @@
|
||||
/**
|
||||
* Toast — lightweight notification
|
||||
* Usage: window.Toast.success('msg'), Toast.error('msg'), Toast.warning('msg'), Toast.info('msg')
|
||||
* window.Toast.show('msg', { type: 'info', duration: 2500 })
|
||||
* Toast — 统一通知组件(Message.js / Qmsg)
|
||||
*
|
||||
* 全站 toast 一律走 Message.js(https://github.com/snwjas/Message.js),
|
||||
* 库本体已本地化:js/libs/message.min.js + css/libs/message.min.css(随 style.css 打包)。
|
||||
* 这里只做一层薄适配,保留旧 API,调用点(utils/floating-tools/artalk…)不用改:
|
||||
* Toast.show(text, { type, duration })
|
||||
* Toast.success / error / warning / info(text, duration?)
|
||||
* showToast(text, type) ← 全局快捷方式
|
||||
*/
|
||||
(function() {
|
||||
var container = null;
|
||||
var counter = 0;
|
||||
(function () {
|
||||
if (window.__yingToastReady) return; // PJAX 重复执行防抖
|
||||
window.__yingToastReady = true;
|
||||
|
||||
function ensureContainer() {
|
||||
if (container) return;
|
||||
container = document.createElement('div');
|
||||
container.className = 'ying-toast-container';
|
||||
document.body.appendChild(container);
|
||||
var Q = window.Qmsg;
|
||||
if (!Q) {
|
||||
// Message.js 没加载出来时兜底:不打断业务,仅降级为无样式 console
|
||||
var fallback = function (text) {
|
||||
try { console.log('[toast]', text); } catch (e) {}
|
||||
};
|
||||
fallback.success = fallback.error = fallback.warning = fallback.info = fallback;
|
||||
window.Toast = fallback;
|
||||
window.showToast = function (text) { fallback(text); };
|
||||
return;
|
||||
}
|
||||
|
||||
function remove(el, delay) {
|
||||
setTimeout(function() {
|
||||
el.classList.remove('ying-toast--visible');
|
||||
el.addEventListener('transitionend', function onEnd(e) {
|
||||
if (e.propertyName !== 'opacity') return;
|
||||
el.removeEventListener('transitionend', onEnd);
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
}, delay);
|
||||
// 全站统一默认值:最多同时 3 条,自动关闭
|
||||
try { Q.config({ maxNums: 3, timeout: 2500 }); } catch (e) {}
|
||||
|
||||
function opt(duration) {
|
||||
return duration ? { timeout: duration } : undefined;
|
||||
}
|
||||
|
||||
window.Toast = {
|
||||
show: function(text, options) {
|
||||
show: function (text, options) {
|
||||
options = options || {};
|
||||
var type = options.type || 'info';
|
||||
var duration = options.duration || 2200;
|
||||
|
||||
ensureContainer();
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'ying-toast ying-toast--' + type;
|
||||
el.textContent = text;
|
||||
el.setAttribute('role', 'status');
|
||||
el.setAttribute('aria-live', 'polite');
|
||||
container.appendChild(el);
|
||||
|
||||
/* Force layout then animate in */
|
||||
void el.offsetWidth;
|
||||
el.classList.add('ying-toast--visible');
|
||||
|
||||
remove(el, duration);
|
||||
if (typeof Q[type] === 'function') return Q[type](text, opt(options.duration));
|
||||
return Q.info(text, opt(options.duration));
|
||||
},
|
||||
success: function (text, duration) { return Q.success(text, opt(duration)); },
|
||||
error: function (text, duration) { return Q.error(text, opt(duration)); },
|
||||
warning: function (text, duration) { return Q.warning(text, opt(duration)); },
|
||||
info: function (text, duration) { return Q.info(text, opt(duration)); },
|
||||
loading: function (text) { return Q.loading(text); },
|
||||
closeAll: function () { try { Q.closeAll(); } catch (e) {} }
|
||||
};
|
||||
|
||||
success: function(text, duration) {
|
||||
this.show(text, { type: 'success', duration: duration });
|
||||
},
|
||||
error: function(text, duration) {
|
||||
this.show(text, { type: 'error', duration: duration });
|
||||
},
|
||||
warning: function(text, duration) {
|
||||
this.show(text, { type: 'warning', duration: duration });
|
||||
},
|
||||
info: function(text, duration) {
|
||||
this.show(text, { type: 'info', duration: duration });
|
||||
}
|
||||
// 全局快捷方式(utils.js 原实现迁到这里,单一来源)
|
||||
window.showToast = function (text, type) {
|
||||
var t = type || 'info';
|
||||
if (window.Toast && typeof window.Toast[t] === 'function') window.Toast[t](text);
|
||||
else window.Toast.info(text);
|
||||
};
|
||||
})();
|
||||
@@ -20,17 +20,8 @@ function themeSweep(newTheme, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// Toast Message Helper
|
||||
window.showToast = function(text, type) {
|
||||
if (window.Toast) {
|
||||
var t = type || 'info';
|
||||
if (typeof window.Toast[t] === 'function') {
|
||||
window.Toast[t](text);
|
||||
} else {
|
||||
window.Toast.info(text);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Toast 快捷方式已统一迁到 modules/toast.js(Message.js/Qmsg 适配层),
|
||||
// 这里不再重复定义 window.showToast,避免双实现漂移。
|
||||
|
||||
// Theme Toggle Logic
|
||||
window.initThemeToggle = function() {
|
||||
|
||||
@@ -20,126 +20,10 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* 友链自助申请:弹窗动态挂到 document.body(评论容器带 transform 动画,
|
||||
会把 position:fixed 劫持成相对定位,导致遮罩/表单跟着容器变形——
|
||||
挂 body 彻底规避),遮罩用 backdrop-filter 毛玻璃(GPU 合成,不卡页面)。
|
||||
显隐用 class 而非 hidden 属性(内联 display 会覆盖 hidden 导致关不掉);
|
||||
盒子必须 box-sizing:border-box(否则 width:100% + padding 溢出出横向滚动条)。 */
|
||||
(function () {
|
||||
var BASE = '{{ $rssBase := site.Params.rssapi.base | default "https://api.200181.xyz" }}{{ $rssBase }}';
|
||||
|
||||
function buildModal() {
|
||||
if (document.getElementById('flApplyMask')) return document.getElementById('flApplyMask');
|
||||
var style = document.createElement('style');
|
||||
style.textContent =
|
||||
'#flApplyMask{position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;padding:16px;' +
|
||||
'background:rgba(15,23,42,.35);-webkit-backdrop-filter:blur(10px) saturate(1.3);backdrop-filter:blur(10px) saturate(1.3)}' +
|
||||
'#flApplyMask.on{display:flex}' +
|
||||
'#flApplyBox{background:var(--bg-card,#fff);color:var(--text-main,#253244);border-radius:12px;max-width:420px;width:100%;' +
|
||||
'box-sizing:border-box;padding:22px;box-shadow:0 16px 48px rgba(15,23,42,.25);max-height:88vh;overflow:auto}' +
|
||||
'[data-theme="dark"] #flApplyBox{box-shadow:0 16px 48px rgba(0,0,0,.55)}' +
|
||||
'#flApplyBox .fl-field{display:block;margin-bottom:10px}' +
|
||||
'#flApplyBox .fl-label{display:block;font-size:12px;color:var(--text-muted,#8a94a6);margin-bottom:4px}' +
|
||||
'#flApplyBox input,#flApplyBox textarea{width:100%;box-sizing:border-box;padding:8px 10px;' +
|
||||
'border:1px solid var(--border-main,#e7e9ee);border-radius:6px;font-size:14px;' +
|
||||
'background:var(--bg-input,#fff);color:inherit;font-family:inherit}' +
|
||||
'#flApplyBox input:focus,#flApplyBox textarea:focus{outline:none;border-color:var(--theme-main,#07c160);' +
|
||||
'box-shadow:0 0 0 3px var(--theme-main-20,rgba(7,193,96,.2))}' +
|
||||
/* 主题 main.css 有一条全局 button 归一化规则(margin-left:23%; margin-top:10px;
|
||||
border:1px solid; background:#222),会把提交按钮推右溢出弹窗 → 这里用 id 选择器压过它 */
|
||||
'#flSubmit{display:block;width:100%;margin:0;background:var(--theme-main,#07c160);border:none;color:#fff;' +
|
||||
'padding:11px 0;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:filter .2s}' +
|
||||
'#flSubmit:hover{filter:brightness(1.06)}' +
|
||||
'#flSubmit:disabled{opacity:.6;cursor:default}' +
|
||||
/* 弹窗滚动条:细一点、跟随主题色,别用系统那根粗滚动条 */
|
||||
'#flApplyBox{overflow-x:hidden;scrollbar-width:thin;scrollbar-color:rgba(140,150,160,.45) transparent}' +
|
||||
'#flApplyBox::-webkit-scrollbar{width:6px;height:6px}' +
|
||||
'#flApplyBox::-webkit-scrollbar-track{background:transparent}' +
|
||||
'#flApplyBox::-webkit-scrollbar-thumb{background:rgba(140,150,160,.45);border-radius:3px}' +
|
||||
'[data-theme="dark"] #flApplyBox{scrollbar-color:rgba(200,205,212,.35) transparent}' +
|
||||
'[data-theme="dark"] #flApplyBox::-webkit-scrollbar-thumb{background:rgba(200,205,212,.35)}';
|
||||
document.head.appendChild(style);
|
||||
|
||||
mask = document.createElement('div');
|
||||
mask.id = 'flApplyMask';
|
||||
mask.innerHTML =
|
||||
'<div id="flApplyBox">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">' +
|
||||
'<b style="font-size:16px;display:flex;align-items:center;gap:8px">' +
|
||||
'<span style="width:4px;height:16px;border-radius:2px;background:var(--theme-main,#07c160);display:inline-block"></span>友链自助申请</b>' +
|
||||
'<span id="flApplyClose" style="cursor:pointer;color:var(--text-muted,#8a94a6);font-size:22px;line-height:1" title="关闭">×</span>' +
|
||||
'</div>' +
|
||||
'<label class="fl-field"><span class="fl-label">站点名称 *</span>' +
|
||||
'<input id="flName" placeholder="我的小站"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">站点链接 *</span>' +
|
||||
'<input id="flUrl" placeholder="https://example.com"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">图标链接(选填,留空自动为你抓取站点图标)</span>' +
|
||||
'<input id="flIcon" placeholder="https://example.com/logo.png"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">订阅地址(RSS/Atom)*</span>' +
|
||||
'<input id="flFeed" placeholder="https://example.com/feed"></label>' +
|
||||
'<label class="fl-field"><span class="fl-label">邮箱 *(接收审核结果)</span>' +
|
||||
'<input id="flEmail" type="email" placeholder="you@example.com"></label>' +
|
||||
'<label class="fl-field" style="margin-bottom:14px"><span class="fl-label">站点简介(选填)</span>' +
|
||||
'<textarea id="flDesc" rows="2" style="resize:vertical"></textarea></label>' +
|
||||
'<div id="flMsg" hidden style="font-size:13px;margin-bottom:10px"></div>' +
|
||||
'<button id="flSubmit">提交申请</button>' +
|
||||
'</div>';
|
||||
document.body.appendChild(mask);
|
||||
|
||||
mask.addEventListener('click', function (e) { if (e.target === mask) close(); });
|
||||
document.getElementById('flApplyClose').addEventListener('click', close);
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
||||
|
||||
document.getElementById('flSubmit').addEventListener('click', function () {
|
||||
var btn = this, msg = document.getElementById('flMsg');
|
||||
var payload = {
|
||||
name: document.getElementById('flName').value.trim(),
|
||||
url: document.getElementById('flUrl').value.trim(),
|
||||
image: document.getElementById('flIcon').value.trim(),
|
||||
feed: document.getElementById('flFeed').value.trim(),
|
||||
email: document.getElementById('flEmail').value.trim(),
|
||||
description: document.getElementById('flDesc').value.trim()
|
||||
};
|
||||
if (!payload.name || !payload.url || !payload.feed || !payload.email) {
|
||||
msg.hidden = false; msg.style.color = '#d75a4a'; msg.textContent = '请填写全部必填项'; return;
|
||||
}
|
||||
btn.disabled = true; btn.textContent = '提交中…';
|
||||
msg.hidden = true;
|
||||
fetch(BASE + '/api/link-apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}).then(function (r) { return r.json().then(function (d) { return { code: r.status, d: d }; }); })
|
||||
.then(function (res) {
|
||||
if (res.code === 200 && res.d.ok) {
|
||||
msg.hidden = false; msg.style.color = 'var(--theme-main,#07c160)'; msg.textContent = res.d.message || '申请已提交';
|
||||
btn.textContent = '已提交 ✓';
|
||||
setTimeout(close, 2200);
|
||||
} else {
|
||||
msg.hidden = false; msg.style.color = '#d75a4a'; msg.textContent = res.d.error || '提交失败,请稍后再试';
|
||||
btn.disabled = false; btn.textContent = '提交申请';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
msg.hidden = false; msg.style.color = '#d75a4a'; msg.textContent = '网络异常,请稍后再试';
|
||||
btn.disabled = false; btn.textContent = '提交申请';
|
||||
});
|
||||
});
|
||||
return mask;
|
||||
}
|
||||
|
||||
function close() {
|
||||
var m = document.getElementById('flApplyMask');
|
||||
if (m) m.classList.remove('on');
|
||||
}
|
||||
|
||||
/* 供置顶评论卡片调用(artalk.js 的 comment-rendered 里触发) */
|
||||
window.flApplyOpenModal = function () {
|
||||
buildModal().classList.add('on');
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
{{/* 友链自助申请弹窗已抽到 assets/js/modules/friendlink.js(随 page-only.js 加载)。
|
||||
原先这段内联脚本写在 #pjax-container 外面:PJAX 只换容器内容、不会重新执行它,
|
||||
从首页 PJAX 进留言页时 window.flApplyOpenModal 未定义 → 「申请友链」卡片点了没反应;
|
||||
直接刷新(整页加载)才正常。抽成模块后两种进入方式行为一致。 */}}
|
||||
|
||||
<script>
|
||||
/* 人机验证(自研「一键验证」)—— 服务端见 artalk-cf/src/lib/human.ts
|
||||
|
||||
@@ -229,6 +229,16 @@
|
||||
</div>
|
||||
<!-- JS -->
|
||||
<!-- <script src="/js/Artalk.js"></script> -->
|
||||
<!-- ── /api/v2/conf 内联快照 ─────────────────────────────────────────────
|
||||
构建期由 .cnb.yml 拉一次 /api/v2/conf 落到 data/artalk_conf.json,这里原样内联。
|
||||
artalk.js 对匿名 /conf 请求直接本地合成响应,省掉一次跨境 RTT ——
|
||||
Artalk 内部 conf 与评论列表是「严格串行」的,去掉这一跳首屏即少一整个往返。
|
||||
文件不存在时(本地预览 / 拉取失败)不输出,前端自动回退到远程请求。 -->
|
||||
{{ with site.Data.artalk_conf }}
|
||||
{{- $snapJson := . | jsonify }}
|
||||
{{- $snapJson = replace $snapJson "</" "<\\/" }}
|
||||
<script>window.__artalkConfSnapshot = {{ $snapJson | safeJS }};</script>
|
||||
{{ end }}
|
||||
<script>
|
||||
// Configuration for Artalk and AI Comment
|
||||
window.artalkConfig = {
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
{{ $iconfont := resources.Get "js/libs/iconfont.js" }}
|
||||
{{ $uikit := resources.Get "js/libs/uikit.min.js" }}
|
||||
{{ $viewimage := resources.Get "js/libs/view-image.min.js" }}
|
||||
{{/* Message.js(Qmsg)+ Toast 适配层放在 utils 之前,保证任何 toast 调用点执行时组件已就绪 */}}
|
||||
{{ $message := resources.Get "js/libs/message.min.js" }}
|
||||
{{ $toast := resources.Get "js/modules/toast.js" }}
|
||||
{{ $utils := resources.Get "js/modules/utils.js" }}
|
||||
{{ $loader := resources.Get "js/modules/loader.js" }}
|
||||
{{ $cache := resources.Get "js/modules/cache.js" }}
|
||||
@@ -67,7 +70,7 @@
|
||||
{{ $lazyload := resources.Get "js/modules/lazyload.js" }}
|
||||
{{ $main := resources.Get "js/main.js" }}
|
||||
|
||||
{{ $coreScripts := slice $iconfont $uikit $viewimage $utils $loader $cache $search $floatingTools $nprogress $pjaxLib $mypjax $pangu $linkify $shortcodesJs $lazyload $main | resources.Concat "js/core.js" | resources.Minify | resources.Fingerprint }}
|
||||
{{ $coreScripts := slice $iconfont $uikit $viewimage $message $toast $utils $loader $cache $search $floatingTools $nprogress $pjaxLib $mypjax $pangu $linkify $shortcodesJs $lazyload $main | resources.Concat "js/core.js" | resources.Minify | resources.Fingerprint }}
|
||||
|
||||
<script defer src="{{ $coreScripts.RelPermalink }}"></script>
|
||||
|
||||
@@ -136,21 +139,24 @@
|
||||
|
||||
{{/* 文章详情页专用JS */}}
|
||||
{{ if .IsPage }}
|
||||
{{/* 友链申请弹窗放最前:artalk.js 渲染申请卡片时会调 window.flApplyOpenModal */}}
|
||||
{{ $friendlink := resources.Get "js/modules/friendlink.js" }}
|
||||
{{ $artalkModule := resources.Get "js/modules/artalk.js" }}
|
||||
{{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }}
|
||||
{{ $reward := resources.Get "js/modules/reward.js" }}
|
||||
|
||||
{{ $pageScripts := slice $artalkModule $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
{{ $pageScripts := slice $friendlink $artalkModule $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
|
||||
<script defer src="{{ $pageScripts.RelPermalink }}"></script>
|
||||
<script>window._pageOnlyScriptUrl = '{{ $pageScripts.RelPermalink }}';</script>
|
||||
{{ else }}
|
||||
{{/* 非文章页面:仍然计算URL,但不加载,用于PJAX动态加载 */}}
|
||||
{{ $friendlink := resources.Get "js/modules/friendlink.js" }}
|
||||
{{ $artalkModule := resources.Get "js/modules/artalk.js" }}
|
||||
{{ $paragraphComments := resources.Get "js/modules/paragraph-comments.js" }}
|
||||
{{ $reward := resources.Get "js/modules/reward.js" }}
|
||||
|
||||
{{ $pageScripts := slice $artalkModule $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
{{ $pageScripts := slice $friendlink $artalkModule $paragraphComments $reward | resources.Concat "js/page-only.js" | resources.Minify | resources.Fingerprint }}
|
||||
|
||||
<script>window._pageOnlyScriptUrl = '{{ $pageScripts.RelPermalink }}';</script>
|
||||
{{ end }}
|
||||
@@ -169,31 +175,7 @@
|
||||
<script defer src="{{ $tiaozhuan.RelPermalink }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{/* ====== 5. 延迟加载的非关键JS ====== */}}
|
||||
{{ $toast := resources.Get "js/modules/toast.js" }}
|
||||
|
||||
{{ $deferredScripts := slice $toast | resources.Concat "js/deferred.js" | resources.Minify | resources.Fingerprint }}
|
||||
|
||||
<script>
|
||||
// 使用requestIdleCallback在浏览器空闲时加载
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(function() {
|
||||
var script = document.createElement('script');
|
||||
script.src = '{{ $deferredScripts.RelPermalink }}';
|
||||
script.defer = true;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
var script = document.createElement('script');
|
||||
script.src = '{{ $deferredScripts.RelPermalink }}';
|
||||
script.defer = true;
|
||||
document.body.appendChild(script);
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
{{/* ====== 6. 第三方脚本 ====== */}}
|
||||
{{/* ====== 5. 第三方脚本 ====== */}}
|
||||
<script async src="/js/bsz.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -42,7 +42,17 @@
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<!-- Resource Hints -->
|
||||
<link rel="preconnect" href="https://cravatar.cn">
|
||||
{{ with site.Params.rssapi.base }}<link rel="preconnect" href="{{ . }}">{{ end }}
|
||||
{{- /* 评论 / RSS API:Artalk 与友链数据都发 CORS 跨域请求。
|
||||
preconnect 必须带 crossorigin —— 浏览器对「带凭据」和「匿名」两条连接分池,
|
||||
不带 crossorigin 预热的连接 CORS 请求无法复用,等于白预连(net::ERR 之外
|
||||
还要重付一次 DNS + TCP + TLS)。dns-prefetch 兜底不支持 preconnect 的浏览器。 */ -}}
|
||||
{{- $apiHosts := slice -}}
|
||||
{{- with site.Params.artalk.server }}{{ $apiHosts = $apiHosts | append (strings.TrimSuffix "/" .) }}{{ end -}}
|
||||
{{- with site.Params.rssapi.base }}{{ $apiHosts = $apiHosts | append (strings.TrimSuffix "/" .) }}{{ end -}}
|
||||
{{- range ($apiHosts | uniq) }}
|
||||
<link rel="preconnect" href="{{ . }}" crossorigin>
|
||||
<link rel="dns-prefetch" href="{{ . }}">
|
||||
{{- end }}
|
||||
{{ with site.Params.rssapi.base }}<script>window.rssApiBase = "{{ . }}";</script>{{ end }}
|
||||
<link rel="dns-prefetch" href="https://bsz.dusays.com:9001">
|
||||
<!-- Analytics -->
|
||||
|
||||
Reference in new issue
Block a user