Files
blog/themes/Ying/layouts/partials/artalk.html
T
zqlit 50a906e07c 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
2026-10-04 15:00:52 +08:00

266 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{ $artalkCss := resources.Get "css/libs/Artalk.css" | resources.Minify | resources.Fingerprint }}
<link id="artalk-css" rel="stylesheet" href="{{ $artalkCss.RelPermalink }}">
<style>
.atk-loading{
background: #FFFFFF;
}
.atk-loading-editor-box {
margin-bottom: 30px;
border: 1px solid var(--frame-color);
border-radius: 4px;
padding: 10px;
background: #fff;
}
[data-theme="dark"] .atk-loading-editor-box {
border-color: var(--frame-dark-color);
background: transparent;
}
/* ================= 评论徽章统一规格(单一来源) =================
几何沿用原设计:inline-block + margin-left:4px + padding 0 6px + 12px/500
+ line-height:18px + 1px 描边 + 3px 圆角。
artalk.js 会把所有徽章(博主/置顶/头衔/友链/友圈/官职)都挂上 .atk-cmt-badge
并拆掉 Artalk 的 .atk-badge-wrap 包装层,所以几何只在这里定义一次,
不会再出现两种 DOM / 两种尺寸。各类型只保留配色差异。
注:margin-right 清零是为了不叠加 Artalk 给"非最后一个徽章"的 6px 右间距
(否则 徽章→徽章 间距一宽一窄,看起来不齐)。 */
.atk-comment .atk-cmt-badge,
.atk-fl-card .atk-cmt-badge,
.atk-comment .atk-badge,
.atk-fl-card .atk-badge,
.atk-comment .atk-pinned-badge,
.atk-fl-card .atk-pinned-badge,
.atk-comment .atk-title-badge,
.atk-fl-card .atk-title-badge,
.atk-comment .atk-link-badge,
.atk-fl-card .atk-link-badge,
.atk-comment .atk-feed-badge,
.atk-fl-card .atk-feed-badge,
.atk-comment .atk-rank-badge,
.atk-fl-card .atk-rank-badge {
display: inline-block !important;
margin-left: 4px !important;
margin-right: 0 !important;
padding: 0 6px !important;
font-size: 12px !important;
font-weight: 600 !important;
border-radius: 3px !important;
line-height: 18px !important;
vertical-align: middle !important;
pointer-events: none;
white-space: nowrap;
}
.atk-link-badge {
color: #059669;
background: rgba(5,150,105,0.08);
border: 1px solid rgba(5,150,105,0.15);
}
.atk-feed-badge {
color: #d97706;
background: rgba(217,119,6,0.08);
border: 1px solid rgba(217,119,6,0.15);
}
.atk-rank-badge {
color: #6b7280;
background: rgba(107,114,128,0.08);
border: 1px solid rgba(107,114,128,0.15);
}
.atk-title-badge {
color: #b8860b;
background: rgba(184,134,11,0.08);
border: 1px solid rgba(184,134,11,0.22);
}
/* 博主(.atk-badge):实底 —— 白字 + 浅黑底
(!important 是为了盖掉 Artalk 写在元素上的内联背景色) */
.atk-comment .atk-badge,
.atk-fl-card .atk-badge {
border: 1px solid #333333;
color: #ffffff !important;
background: #333333 !important;
}
/* 置顶徽章:实底 —— 白字 + 浅红底(比 #d9534f / #ff6262 更浅,不刺眼) */
.atk-comment .atk-pinned-badge,
.atk-fl-card .atk-pinned-badge {
border: 1px solid #e05a5a;
color: #ffffff !important;
background: #e05a5a !important;
}
/* ---- 暗色模式配色 ---- */
[data-theme="dark"] .atk-link-badge {
color: #34d399;
background: rgba(52,211,153,0.12);
border-color: rgba(52,211,153,0.2);
}
[data-theme="dark"] .atk-feed-badge {
color: #fbbf24;
background: rgba(251,191,36,0.12);
border-color: rgba(251,191,36,0.2);
}
[data-theme="dark"] .atk-rank-badge {
color: rgb(148,163,184);
background: rgba(148,163,184,0.1);
border-color: rgba(148,163,184,0.18);
}
[data-theme="dark"] .atk-title-badge {
color: #fbbf24;
background: rgba(251,191,36,0.12);
border-color: rgba(251,191,36,0.2);
}
/* ================= 友链申请入口卡片 =================
PJAX 下每次切页评论区都会重建卡片 → 入场动画与打字机自然重播;
打字机定时器在 artalk.js 里会检测卡片是否还在文档中(切页后自动停)。 */
/* 被转换成卡片的评论:整条从列表里隐藏(卡片已移到列表上方单独显示,
用 class + !important 声明式隐藏,Artalk 重渲染也压得住) */
.atk-comment-wrap.atk-fl-apply {
display: none !important;
}
/* 卡片容器:固定在评论列表最上方 */
.atk-fl-card-wrap {
margin: 0 0 6px;
}
/* ---- 移动端:卡片更紧凑(隐藏头像 + 收紧间距 + 微调字号/字距) ---- */
@media (max-width: 640px) {
.atk-fl-card {
gap: 8px !important;
padding: 10px 12px !important;
}
.atk-fl-card .atk-fl-avatar {
display: none !important; /* 窄屏省掉头像,把宽度让给文字 */
}
.atk-fl-card .atk-fl-nick {
font-size: 13.5px !important;
letter-spacing: 0.2px !important;
}
.atk-fl-card .atk-fl-desc {
font-size: 12.5px !important;
letter-spacing: 0.2px !important;
margin-top: 3px !important;
}
.atk-fl-card .atk-fl-btn {
font-size: 12.5px !important;
padding: 5px 11px !important;
letter-spacing: 0.2px !important;
}
}
.atk-fl-card {
animation: flApplyIn .5s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes flApplyIn {
from { opacity: 0; transform: translateY(12px) scale(.98); }
to { opacity: 1; transform: none; }
}
.atk-fl-card:hover {
border-color: var(--theme-main-30, rgba(7,193,96,.3)) !important;
}
/* 打字机光标 */
.atk-fl-card .atk-fl-caret {
display: inline-block;
width: 1px;
height: 1em;
margin-left: 2px;
vertical-align: -2px;
background: currentColor;
animation: flCaret .9s steps(1, end) infinite;
}
@keyframes flCaret {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* 申请友链按钮:轻微上浮 + 主题色投影 */
.atk-fl-card .atk-fl-btn {
transition: transform .2s ease, box-shadow .2s ease;
}
.atk-fl-card:hover .atk-fl-btn {
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--theme-main-20, rgba(7,193,96,.2));
}
.atk-fl-card .atk-fl-btn:active {
transform: translateY(0);
}
/* 尊重系统"减弱动态效果":只关掉入场动画和按钮位移,
打字机与光标保留(打字机是文字渐显,不属于会造成不适的位移类动效) */
@media (prefers-reduced-motion: reduce) {
.atk-fl-card { animation: none; }
.atk-fl-card .atk-fl-btn { transition: none; }
.atk-fl-card:hover .atk-fl-btn { transform: none; box-shadow: none; }
}
.atk-main {
backface-visibility: hidden;
will-change: transform;
-webkit-font-smoothing: antialiased;
}
</style>
<!-- Artalk -->
<div id="Comments" uk-scrollspy="target: > *; cls: uk-animation-slide-bottom-small; delay: 100">
<!-- Loading Skeleton -->
<div class="atk-loading-placeholder">
<!-- Editor Skeleton -->
<div class="atk-loading-editor-box">
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
<div class="atk-loading-line" style="width: 32%; height: 34px; border-radius: 4px; margin-bottom: 0;"></div>
<div class="atk-loading-line" style="width: 32%; height: 34px; border-radius: 4px; margin-bottom: 0;"></div>
<div class="atk-loading-line" style="width: 32%; height: 34px; border-radius: 4px; margin-bottom: 0;"></div>
</div>
<div class="atk-loading-line" style="width: 100%; height: 120px; border-radius: 4px; margin-bottom: 10px;"></div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="atk-loading-line" style="width: 120px; height: 20px; margin-bottom: 0;"></div>
<div class="atk-loading-line" style="width: 80px; height: 34px; border-radius: 4px; margin-bottom: 0;"></div>
</div>
</div>
<!-- Comment List Skeleton -->
<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>
</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 = {
el: '#Comments',
server: '{{ .Site.Params.artalk.server }}',
site: '{{ .Site.Params.artalk.site }}',
pageKey: '{{ .RelPermalink }}',
emoticons: '{{ .Site.Params.artalk.emoticons | absURL }}',
aiComment: null
};
// Execute Initialization immediately if function exists (PJAX re-execution),
// otherwise wait for DOMContentLoaded (Initial load)
(function() {
if (typeof window.initArtalk === 'function') {
window.initArtalk();
} else {
document.addEventListener("DOMContentLoaded", function() {
if (typeof window.initArtalk === 'function') {
window.initArtalk();
}
});
}
})();
</script>