Files
blog/themes/Ying/layouts/partials/featured-comments.html
T
zqlit 654de1fdf0 feat(关于页): 重设计为「时间线 + 数据一览」版式,动效对齐全站 UIKit 范式
- 版式:01 关于我(属性四行 + 时间线四节点 + 三张数据卡 + 分类 Top5 条,
  全部构建期实时统计,新增 partial about-intro.html)+ 02 这个博客 + 03 走心
  评论 + 04 热力图,间距统一由 .about-sec 管,字号统一四档
- 动效:纯官方 UIKit scrollspy(与归档页同源)—— target 一律后代选择器、
  动画类用 uk-animation-slide-*-medium(50px 位移)+ repeat:true 滚回重播、
  分节标题 fade 不 repeat;删自研编排器,CSS 只留分类条宽度填充与
  prefers-reduced-motion 守卫
- 修复 PJAX 下 UIKit 不重绑的既有 bug:mypjax.js 换页后调 UIkit.update()
  (评论区/说说的 scrollspy 同样依赖此修复)
- 走心评论:骨架屏、8s 超时、sessionStorage 缓存(10 分钟)、重试按钮;
  异步渲染后手动 UIkit.update() 让卡片参与入场级联
2026-10-07 21:45:46 +08:00

290 lines
15 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.
{{/* 走心评论:管理员在评论区精选的评论,关于页展示。
数据走 Worker 公开接口 /api/v2/featured/comments(悄悄话评论永远不进精选列表)。
本地预览 / 接口故障时优雅降级:整块隐藏或显示占位文案。
调试参数:?featmock=1 用本地 mock 渲染(仅用于排版预览,生产无害)。 */}}
<style>
.featc-wrap { min-width: 0; }
.featc-title {
font-size: var(--ab-fs-title, 1.0625rem); font-weight: 700; color: var(--text-title-color);
line-height: 1.3; margin: 0 0 10px;
}
.featc-title .featc-count {
font-size: var(--ab-fs-micro, 0.75rem); font-weight: 600; vertical-align: super;
margin-left: 3px; color: var(--theme-main, #07c160);
}
.featc-desc { font-size: var(--ab-fs-body, 0.875rem); line-height: 1.8; color: var(--text-desc, #777); margin: 0 0 18px; }
/* 瀑布流:CSS 多列布局,卡片高度不一时自然错落 */
.featc-grid {
columns: 3; column-gap: 14px; min-width: 0;
}
.featc-card {
break-inside: avoid; -webkit-column-break-inside: avoid;
display: inline-flex; flex-direction: column; gap: 8px; width: 100%;
margin: 0 0 14px; box-sizing: border-box;
background: var(--bg-card, #fff);
border: 1px solid var(--border-main, #eee);
border-radius: 10px; padding: 16px 16px 12px;
text-decoration: none; color: inherit; min-width: 0;
transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
}
.featc-card:hover {
transform: translateY(-3px);
border-color: var(--theme-main-30, rgba(7,193,96,.3));
box-shadow: var(--shadow-popover, 0 4px 20px rgba(0,0,0,.1));
text-decoration: none; color: inherit;
}
.featc-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
.featc-avatar { width: 34px; height: 34px; border-radius: 50%; flex: none; object-fit: cover; }
.featc-nick {
font-size: var(--ab-fs-label, 0.8125rem); font-weight: 700; color: var(--text-title-color, #333);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.featc-date { margin-left: auto; flex: none; font-size: var(--ab-fs-micro, 0.75rem); color: var(--text-muted, #999); }
.featc-content {
font-size: var(--ab-fs-body, 0.875rem); line-height: 1.75; color: var(--text-main, #333);
margin: 0; word-break: break-word;
display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden;
}
.featc-content img { max-height: 1.2em; vertical-align: sub; }
.featc-content p { margin: 0; }
.featc-foot {
font-size: var(--ab-fs-micro, 0.75rem); color: var(--text-muted, #999); line-height: 1.6;
padding-left: 8px; border-left: 2px solid var(--border-main, #eee);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.featc-foot a { color: inherit; }
.featc-empty { font-size: var(--ab-fs-body, 0.875rem); color: var(--text-muted, #999); padding: 8px 0; }
/* 暗色模式:主题对文字色是「按元素覆写」而不是重定义变量(--text-title-color
在暗色下仍是深色字),这里必须显式给出暗色文字色,否则标题会隐身 */
[data-theme="dark"] .featc-title,
[data-theme="dark"] .featc-nick { color: var(--text-title-dark-color, #e5e7eb); }
[data-theme="dark"] .featc-content { color: var(--text-content-dark-color, #d1d5db); }
@media (max-width: 1100px) {
.featc-grid { columns: 2; }
}
@media (max-width: 640px) {
.featc-grid { columns: 1; }
.featc-card { margin-bottom: 10px; padding: 13px 14px 10px; }
.featc-desc { margin-bottom: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.featc-card, .featc-card:hover { transition: none; transform: none; }
}
/* ── 骨架屏(等接口返回期间占位)──
接口实测(真实 Chrome,非沙箱代理)首屏请求约 1.0–1.5s。
这 1 秒多的等待用同构灰块填住,比一行「加载中…」体感好得多,
而且尺寸与真实卡片一致 → 数据到位时不会发生布局位移。 */
.featc-card--sk {
pointer-events: none;
cursor: default;
}
.featc-card--sk:hover { transform: none; box-shadow: none; }
.featc-sk-avatar {
display: block; width: 22px; height: 22px; flex: none;
border-radius: 50%;
background: var(--bankuai-color, #f1f1f1);
}
.featc-sk-line {
display: block; height: 10px; border-radius: 4px; margin-bottom: 8px;
background: var(--bankuai-color, #f1f1f1);
/* 缓慢的明暗流动,暗示「正在加载」而不是坏掉了 */
background-image: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
background-size: 220px 100%;
background-repeat: no-repeat;
animation: featc-sk-shimmer 1.4s ease-in-out infinite;
}
.featc-card--sk .featc-head { margin-bottom: 12px; }
@keyframes featc-sk-shimmer {
0% { background-position: -220px 0; }
100% { background-position: calc(100% + 220px) 0; }
}
[data-theme="dark"] .featc-sk-avatar,
[data-theme="dark"] .featc-sk-line {
background-color: rgba(31, 41, 55, 0.9);
background-image: linear-gradient(90deg, transparent, rgba(255,255,255,0.06), transparent);
}
@media (prefers-reduced-motion: reduce) {
.featc-sk-line { animation: none; }
}
/* 超时/失败提示(比骨架屏更明确) */
.featc-retry {
grid-column: 1 / -1;
text-align: center;
padding: 18px 0 6px;
}
.featc-retry button {
font: inherit;
font-size: 0.8125rem;
color: var(--text-content-color, #24292e);
background: none;
border: 1px solid var(--frame-color, #eee);
border-radius: 999px;
padding: 5px 16px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.featc-retry button:hover { border-color: #c7ced8; }
[data-theme="dark"] .featc-retry button {
color: var(--text-content-dark-color, #d1d5db);
border-color: var(--frame-dark-color, #1f2933);
}
</style>
<div class="featc-wrap" uk-scrollspy="target: .featc-item; cls: uk-animation-slide-bottom-medium; delay: 80; repeat: true">
<div class="featc-title">走心评论<span class="featc-count" id="featc-count" hidden></span></div>
<p class="featc-desc">当我看到各位在不同的时间和故事中,发出一份份走心的评论时,一份温暖之意即笼罩在我的心头。</p>
<!-- ★ 卡片是异步渲染的(接口 1–1.5s),UIKit 的 MutationObserver 只在
属性**新增**时反应,新 insert 的子节点不会被自动 connect。
所以脚本渲染完成后要自己调一次 UIkit.update(grid) —— 见下方 render()。
骨架屏刻意不带 featc-item 类,避免被 scrollspy 隐藏和 shimmer 打架。 -->
<div class="featc-grid" id="featc-grid">
<!-- 骨架屏:接口约 1–1.5s 才返回,用 3 张与真实卡片同构的占位块撑住版面,
避免「加载中…」一行字造成的空洞感与布局跳动。 -->
<div class="featc-card featc-card--sk" aria-hidden="true">
<div class="featc-head"><span class="featc-sk-avatar"></span><span class="featc-sk-line" style="width:52px"></span><span class="featc-sk-line" style="width:78px;margin-left:auto"></span></div>
<div class="featc-sk-line" style="width:100%"></div>
<div class="featc-sk-line" style="width:82%"></div>
<div class="featc-sk-line" style="width:40%;margin-top:12px"></div>
</div>
<div class="featc-card featc-card--sk" aria-hidden="true">
<div class="featc-head"><span class="featc-sk-avatar"></span><span class="featc-sk-line" style="width:44px"></span><span class="featc-sk-line" style="width:78px;margin-left:auto"></span></div>
<div class="featc-sk-line" style="width:100%"></div>
<div class="featc-sk-line" style="width:64%"></div>
<div class="featc-sk-line" style="width:40%;margin-top:12px"></div>
</div>
<div class="featc-card featc-card--sk" aria-hidden="true">
<div class="featc-head"><span class="featc-sk-avatar"></span><span class="featc-sk-line" style="width:60px"></span><span class="featc-sk-line" style="width:78px;margin-left:auto"></span></div>
<div class="featc-sk-line" style="width:100%"></div>
<div class="featc-sk-line" style="width:90%"></div>
<div class="featc-sk-line" style="width:100%"></div>
<div class="featc-sk-line" style="width:36%;margin-top:12px"></div>
</div>
</div>
</div>
<script type="text/javascript">
(function () {
var API = (window.artalkConfig && window.artalkConfig.server || 'https://api.200181.xyz').replace(/\/+$/, '') +
'/api/v2/featured/comments?limit=20';
var grid = document.getElementById('featc-grid');
if (!grid) return;
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function render(list) {
var countEl = document.getElementById('featc-count');
if (countEl && list.length) { countEl.textContent = list.length + '个'; countEl.hidden = false; }
if (!list.length) {
grid.innerHTML = '<div class="featc-empty">还没有走心评论,去和博主聊聊吧~</div>';
return;
}
var html = '';
for (var i = 0; i < list.length; i++) {
var c = list[i];
var avatar = 'https://weavatar.com/avatar/' + esc(c.email_encrypted) + '?d=mp&s=120';
var nick = esc(c.nick);
html +=
'<a class="featc-card featc-item" href="' + esc(c.page_url) + '#atk-comment-' + esc(c.id) + '">' +
'<div class="featc-head">' +
'<img class="featc-avatar" src="' + avatar + '" alt="" loading="lazy" referrerpolicy="no-referrer">' +
'<span class="featc-nick">' + nick + '</span>' +
'<span class="featc-date">' + esc(c.date) + '</span>' +
'</div>' +
'<div class="featc-content">' + c.content_html + '</div>' +
'<div class="featc-foot">发表在 ' + esc(c.page_title || '页面') + '</div>' +
'</a>';
}
grid.innerHTML = html;
// ★ 卡片是 fetch 回来后才插进 DOM 的,UIKit 的 MutationObserver 捕捉不到
// (它只对「属性新增」反应,不管新插入的子树)—— 必须手动 boot 一次。
// 之后完全交给 scrollspy(repeat: true):
// · 这一节在视口内 → 同一批 target 进同一个 update 周期,
// delay 依次累加,自动错落浮现;
// · 在视口外 → 先藏住,滚到时逐条入场,滚走再滚回会重播。
if (typeof window.UIkit !== 'undefined' && typeof window.UIkit.update === 'function') {
try { window.UIkit.update(grid); } catch (e) {}
}
}
// 失败/超时提示:明确告知并给一次重试,比一直挂着骨架屏好
function renderRetry(msg) {
grid.innerHTML =
'<div class="featc-retry"><div class="featc-empty" style="margin-bottom:10px">' + esc(msg) + '</div>' +
'<button type="button" id="featc-retry-btn">重新加载</button></div>';
var btn = document.getElementById('featc-retry-btn');
if (btn) btn.addEventListener('click', function () {
grid.innerHTML = '<div class="featc-card featc-card--sk"><div class="featc-head"><span class="featc-sk-avatar"></span><span class="featc-sk-line" style="width:52px"></span></div><div class="featc-sk-line" style="width:100%"></div><div class="featc-sk-line" style="width:82%"></div></div>';
load(true);
});
}
var CACHE_KEY = 'featc_cache_v1';
var CACHE_TTL = 10 * 60 * 1000; // 10 分钟
function readCache() {
try {
var raw = sessionStorage.getItem(CACHE_KEY);
if (!raw) return null;
var o = JSON.parse(raw);
if (!o || !o.t || (Date.now() - o.t) > CACHE_TTL) return null;
return o.list;
} catch (e) { return null; }
}
function writeCache(list) {
try { sessionStorage.setItem(CACHE_KEY, JSON.stringify({ t: Date.now(), list: list })); } catch (e) {}
}
function load(force) {
// 排版预览 mock(?featmock=1):接口未部署 / 无精选数据时也能看样式
if (/[?&]featmock=1/.test(location.search)) {
render([
{ id: 1, nick: '森屿', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: '', date: '2026-06-27 20:57', content_html: '<p>“被记得”本身就是一件幸福的事</p>', page_url: '#', page_title: '碎碎念|被记得 – 去青岛看一场迟到很久的海' },
{ id: 2, nick: '小十', email_encrypted: 'e08b1ed883704d10a4e4b9c05b2bf019', link: '', date: '2026-05-11 17:00', content_html: '<p>哇哦🎉,环境真的太好啦,下次我也想去!</p>', page_url: '#', page_title: '碎碎念|“撩蝉” – 去闯进其他人的生活' },
{ id: 3, nick: '群林', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: 'https://usj.cc', date: '2026-01-25 13:57', content_html: '<p>欢迎来做客,博客新增了留言板,有问题随时找我~</p>', page_url: '#', page_title: '留言' },
{ id: 4, nick: '小柒', email_encrypted: 'e08b1ed883704d10a4e4b9c05b2bf019', link: '', date: '2025-01-21 01:07', content_html: '<p>2025 年新的一年,一起加油,希望一切顺利!</p>', page_url: '#', page_title: '留言' },
{ id: 5, nick: '阿澜', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: '', date: '2025-11-08 22:14', content_html: '<p>这篇文章我反反复复读了三遍。「把日子过成自己喜欢的样子」说起来简单,做起来才知道有多难。谢谢博主的文字,让我在加班的深夜里被治愈了一下,明天继续加油。</p>', page_url: '#', page_title: '碎碎念|深夜书桌 – 与自己和解的十个瞬间' },
{ id: 6, nick: 'Momo', email_encrypted: 'e08b1ed883704d10a4e4b9c05b2bf019', link: '', date: '2025-09-30 09:42', content_html: '<p>签到打卡 ~</p>', page_url: '#', page_title: '碎碎念|秋日漫步 – 路过巷口的桂花香' },
{ id: 7, nick: '北屿', email_encrypted: '897defecd8ecc9ad844d86f615b967d2', link: '', date: '2025-08-12 19:03', content_html: '<p>从友链跳转过来的,一口气读了五篇,博客的排版和摄影都太舒服了。已经加入 RSS 阅读器,期待更新!</p>', page_url: '#', page_title: '碎碎念|山海之间 – 一次说走就走的旅行' }
]);
return;
}
// 会话内缓存:先进关于页已经拉过一次,再进(含 PJAX 来回)直接命中,
// 不用再等那 1 秒多。TTL 10 分钟,兼顾新鲜度。
if (!force) {
var cached = readCache();
if (cached) { render(cached); return; }
}
// ★ 超时兜底:接口实测 1–1.5s,但偶有长尾。
// 8s 还没回就不再干等,切成「重试」提示 —— 比无限挂骨架屏诚实。
var ctrl = ('AbortController' in window) ? new AbortController() : null;
var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, 8000);
fetch(API, ctrl ? { signal: ctrl.signal } : undefined)
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
.then(function (data) {
clearTimeout(timer);
var list = (data && data.comments) || [];
writeCache(list);
render(list);
})
.catch(function (err) {
clearTimeout(timer);
// 有缓存就先用缓存顶(宁可显示旧数据也别显示错误)
var stale = readCache();
if (stale) { render(stale); return; }
renderRetry('评论暂时没加载出来');
});
}
load();
})();
</script>