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() 让卡片参与入场级联
This commit is contained in:
1 parent
a884180787
commit
654de1fdf0
7 files changed
+692
-88
No files matched your search
+29
-23
@@ -8,27 +8,33 @@ slug: "about"
|
||||
draft: false
|
||||
---
|
||||
|
||||
<div class="about-card">
|
||||
<div class="uk-grid-match uk-child-width-1-2@m uk-grid-small" uk-grid>
|
||||
<div>
|
||||
<h3 class="uk-card-title uk-margin-remove-bottom" style="font-size: 1.2rem; font-weight: 600;"><i class="ri-user-smile-line uk-margin-small-right"></i>关于博主</h3>
|
||||
<p class="uk-text-meta uk-margin-remove-top uk-margin-small-bottom">Hello, World!</p>
|
||||
<div class="uk-margin-small-top">
|
||||
<p class="uk-margin-small-bottom"><i class="ri-map-pin-user-line uk-margin-small-right"></i>湖北 · 襄阳 -> 广州</p>
|
||||
<p class="uk-margin-small-bottom"><i class="ri-user-heart-line uk-margin-small-right"></i>00后,性格随和,风趣幽默</p>
|
||||
<p class="uk-margin-small-bottom"><i class="ri-flag-line uk-margin-small-right"></i>不活跃就是在跟生活对线~</p>
|
||||
<p class="uk-margin-remove-bottom"><i class="ri-briefcase-line uk-margin-small-right"></i>工作:牛马打工人</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="uk-card-title uk-margin-remove-bottom" style="font-size: 1.2rem; font-weight: 600;"><i class="ri-terminal-box-line uk-margin-small-right"></i>博客构建</h3>
|
||||
<p class="uk-text-meta uk-margin-remove-top uk-margin-small-bottom">Powered by Open Source</p>
|
||||
<ul class="uk-list uk-list-divider uk-margin-small-top">
|
||||
<li><i class="ri-layout-masonry-line uk-margin-small-right"></i>框架:Hugo</li>
|
||||
<li><i class="ri-cloud-windy-line uk-margin-small-right"></i>托管:Vercel / GitHub Pages</li>
|
||||
<li><i class="ri-github-line uk-margin-small-right"></i>源码:GitHub(heysiu/Ying) </li>
|
||||
<li><i class="ri-palette-line uk-margin-small-right"></i>主题:二开Ying主题(自用不开源)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 关于页:属性/时间线/数据在 themes/Ying/layouts/partials/about-intro.html;本文件只管「02 / 这个博客」。注意:内容文件里不要写 Hugo 模板注释(会原样显示在页面上)。 -->
|
||||
|
||||
<div class="about-sec">
|
||||
|
||||
<div class="section-title" uk-scrollspy="cls: uk-animation-fade; delay: 100">02 / 这个博客 (Build)</div>
|
||||
|
||||
<div class="ab-attrs" style="border-top: 0;" uk-scrollspy="target: .ab-attr; cls: uk-animation-slide-bottom-medium; delay: 80; repeat: true">
|
||||
|
||||
<div class="ab-attr"><span class="k">框架</span><span class="v">Hugo · 主题 Ying</span></div>
|
||||
|
||||
<div class="ab-attr"><span class="k">托管</span><span class="v">CNB 构建 · 又拍云 / 多吉云 / EdgeOne</span></div>
|
||||
|
||||
<div class="ab-attr"><span class="k">评论</span><span class="v">Artalk · 自建(Cloudflare Workers)</span></div>
|
||||
|
||||
<div class="ab-attr"><span class="k">备份</span><span class="v">CNB 主仓 + 自建 Gitea 辅仓 + 加密离线包</span></div>
|
||||
|
||||
<div class="ab-attr"><span class="k">始于</span><span class="v">2020 年,一写六年</span></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ab-stack" uk-scrollspy="target: .ab-tag; cls: uk-animation-slide-bottom-medium; delay: 60; repeat: true">
|
||||
<span class="ab-tag">Hugo</span>
|
||||
<span class="ab-tag">Cloudflare</span>
|
||||
<span class="ab-tag">Docker</span>
|
||||
<span class="ab-tag">Nginx</span>
|
||||
<span class="ab-tag">Python</span>
|
||||
<span class="ab-tag">Shell</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -4322,6 +4322,284 @@ input[type=submit] {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
关于页 · B+C 方案(时间线 + 数据一览)
|
||||
──────────────────────────────────────────────────────────────────────────
|
||||
设计约束:只使用主题既有语言——618px 容器 / zql 衬线 / 8·12px 圆角 /
|
||||
.section-title 大写小标题 / 黑白灰单色。不引入任何强调色。
|
||||
数据由 partials/about-intro.html 在构建期从 content 实时统计。
|
||||
★ 关于页文字必须显式左对齐 —— .main 上有 text-align:center,
|
||||
靠继承会让整页居中。
|
||||
|
||||
── 字号阶梯(★ 全节唯一字号来源,不要再写裸 rem/px)──
|
||||
之前每个元素各写各的 rem,实际算出 11.2 / 11.52 / 12.48 / 13.68 /
|
||||
13.76 / 16.8 六七种「互不相干的小字」,视觉上就是"不统一"。
|
||||
现在收敛成 4 档 + 2 个特例,所有小字只有「正文 / 标签 / 脚注」三种:
|
||||
--ab-fs-title 17px 区块内小标题(衬线)
|
||||
--ab-fs-body 14px 正文(衬线)
|
||||
--ab-fs-label 13px 标签 · 元信息(等宽/无衬线)
|
||||
--ab-fs-micro 12px 脚注 · 注脚(等宽/无衬线)
|
||||
特例:--ab-fs-num 28px 数据大数字,--ab-fs-when 13px 时间线日期。
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
.about-page {
|
||||
text-align: left;
|
||||
/* 字号阶梯 */
|
||||
--ab-fs-title: 1.0625rem; /* 17px 衬线小标题 */
|
||||
--ab-fs-body: 0.875rem; /* 14px 正文 */
|
||||
--ab-fs-label: 0.8125rem; /* 13px 标签/元信息 */
|
||||
--ab-fs-micro: 0.75rem; /* 12px 脚注 */
|
||||
--ab-fs-when: 0.8125rem; /* 13px 时间线日期 */
|
||||
--ab-fs-num: 1.75rem; /* 28px 数据大字 */
|
||||
/* 行高 */
|
||||
--ab-lh-tight: 1.5;
|
||||
--ab-lh-body: 1.85;
|
||||
}
|
||||
|
||||
/* 区块间距统一在这里管:about-intro.html / about.html 里的分节
|
||||
都用 .about-sec 包裹,不要再写内联 margin-top。 */
|
||||
.about-page .about-sec { margin-top: 44px; }
|
||||
.about-page .about-sec:first-child { margin-top: 0; }
|
||||
/* 小节标题(section-title)本身已有 14px 下边距,紧邻区块时不用再加。
|
||||
全局 .section-title 是 0.9rem(14.4px),比正文(14px)还小一点点、比标签(13px)大,
|
||||
落在阶梯的缝里 —— 关于页统一压到「标签级」并加大字距,让它明显是"题头"而不是正文。 */
|
||||
.about-page .section-title {
|
||||
font-size: var(--ab-fs-label);
|
||||
letter-spacing: 0.18em;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
/* 移动端收紧 */
|
||||
@media (max-width: 700px) {
|
||||
.about-page .about-sec { margin-top: 34px; }
|
||||
}
|
||||
|
||||
.ab-attrs { margin-top: 26px; border-top: 1px solid var(--frame-color); }
|
||||
[data-theme="dark"] .ab-attrs { border-top-color: var(--frame-dark-color); }
|
||||
.ab-attr {
|
||||
display: flex; gap: 16px; align-items: baseline;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--frame-color);
|
||||
font-size: var(--ab-fs-body); line-height: var(--ab-lh-body);
|
||||
}
|
||||
[data-theme="dark"] .ab-attr { border-bottom-color: var(--frame-dark-color); }
|
||||
.ab-attr:last-child { border-bottom: 0; }
|
||||
.ab-attr .k {
|
||||
flex: none; width: 64px;
|
||||
font-size: var(--ab-fs-label); color: var(--sanji-color);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.ab-attr .v { color: var(--text-content-color); min-width: 0; }
|
||||
[data-theme="dark"] .ab-attr .v { color: var(--text-content-dark-color); }
|
||||
|
||||
/* 技术栈标签:与主题 .skill-tag 同观感 */
|
||||
.ab-stack { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 14px; }
|
||||
.ab-tag {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: 3px 11px; border-radius: 999px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.7);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
||||
font-size: var(--ab-fs-micro); color: #374151;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
[data-theme="dark"] .ab-tag {
|
||||
border-color: rgba(31, 41, 55, 0.85);
|
||||
background: rgba(15, 23, 42, 0.96);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* ── B 方案:时间线 ──
|
||||
时间线紧跟在属性列表后面,需要一点上方留白把它和属性区分开 */
|
||||
.ab-tl { position: relative; padding-left: 26px; margin-top: 30px; }
|
||||
.ab-tl::before {
|
||||
content: ''; position: absolute;
|
||||
left: 5px; top: 7px; bottom: 7px;
|
||||
width: 1px; background: var(--frame-color);
|
||||
}
|
||||
[data-theme="dark"] .ab-tl::before { background: var(--frame-dark-color); }
|
||||
|
||||
.ab-item { position: relative; padding-bottom: 26px; }
|
||||
.ab-item:last-child { padding-bottom: 0; }
|
||||
.ab-item::before {
|
||||
content: ''; position: absolute;
|
||||
left: -26px; top: 6px;
|
||||
width: 11px; height: 11px; border-radius: 50%;
|
||||
background: var(--background-color);
|
||||
border: 1.5px solid #bec8d2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
[data-theme="dark"] .ab-item::before {
|
||||
background: #0f172a; border-color: #4b5563;
|
||||
}
|
||||
/* 当前节点:实心,用于标出"此刻" */
|
||||
.ab-item.is-now::before {
|
||||
background: #111827; border-color: #111827;
|
||||
}
|
||||
[data-theme="dark"] .ab-item.is-now::before {
|
||||
background: #e5e7eb; border-color: #e5e7eb;
|
||||
}
|
||||
.ab-when {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
||||
font-size: var(--ab-fs-when); letter-spacing: 0.06em;
|
||||
color: #a8b3c0;
|
||||
}
|
||||
[data-theme="dark"] .ab-when { color: #6b7280; }
|
||||
.ab-h {
|
||||
font-family: 'zql', serif;
|
||||
font-size: var(--ab-fs-title); line-height: var(--ab-lh-tight);
|
||||
color: var(--text-title-color);
|
||||
margin: 4px 0 6px;
|
||||
}
|
||||
[data-theme="dark"] .ab-h { color: var(--text-title-dark-color); }
|
||||
.ab-p {
|
||||
font-size: var(--ab-fs-body); line-height: var(--ab-lh-body);
|
||||
color: var(--text-content-color);
|
||||
margin: 0;
|
||||
}
|
||||
[data-theme="dark"] .ab-p { color: var(--text-content-dark-color); }
|
||||
.ab-p em { font-style: normal; color: var(--sanji-color); }
|
||||
|
||||
/* ── C 方案:数据一览 ──
|
||||
时间线结束 → 数据卡之间需要明显一档间距,否则和最后一条时间线粘在一起 */
|
||||
.ab-stats {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
.ab-stat {
|
||||
text-align: center; padding: 16px 6px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 12px;
|
||||
}
|
||||
[data-theme="dark"] .ab-stat { border-color: rgba(255, 255, 255, 0.08); }
|
||||
.ab-stat .n {
|
||||
display: block;
|
||||
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
||||
font-size: var(--ab-fs-num); line-height: 1.1;
|
||||
color: var(--text-title-color);
|
||||
}
|
||||
[data-theme="dark"] .ab-stat .n { color: var(--text-title-dark-color); }
|
||||
.ab-stat .l {
|
||||
display: block; margin-top: 7px;
|
||||
font-size: var(--ab-fs-micro); letter-spacing: 0.1em;
|
||||
color: var(--sanji-color);
|
||||
}
|
||||
|
||||
.ab-bars { margin-top: 26px; }
|
||||
.ab-bar {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
margin-bottom: 11px; font-size: var(--ab-fs-label);
|
||||
}
|
||||
.ab-bar:last-child { margin-bottom: 0; }
|
||||
.ab-bar .n {
|
||||
flex: none; width: 58px;
|
||||
font-size: var(--ab-fs-label); color: var(--erji-color);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
[data-theme="dark"] .ab-bar .n { color: #9ca3af; }
|
||||
.ab-bar .t {
|
||||
flex: 1; height: 6px; border-radius: 99px;
|
||||
background: var(--bankuai-color); overflow: hidden;
|
||||
}
|
||||
[data-theme="dark"] .ab-bar .t { background: rgba(31, 41, 55, 0.75); }
|
||||
.ab-bar .t i {
|
||||
display: block; height: 100%; border-radius: 99px;
|
||||
background: #bec8d2;
|
||||
}
|
||||
[data-theme="dark"] .ab-bar .t i { background: #6b7280; }
|
||||
.ab-bar .c {
|
||||
flex: none; width: 34px; text-align: right;
|
||||
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
||||
font-size: var(--ab-fs-micro); color: #a8b3c0;
|
||||
}
|
||||
[data-theme="dark"] .ab-bar .c { color: #6b7280; }
|
||||
.ab-footnote {
|
||||
font-size: var(--ab-fs-micro); line-height: 1.75;
|
||||
color: #a8b3c0;
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
[data-theme="dark"] .ab-footnote { color: #6b7280; }
|
||||
|
||||
/* ── 关于页响应式 ── */
|
||||
@media (max-width: 700px) {
|
||||
.ab-attr { gap: 12px; }
|
||||
.ab-attr .k { width: 54px; }
|
||||
.ab-stats { gap: 8px; }
|
||||
.ab-stat { padding: 13px 4px; }
|
||||
.ab-stat .n { font-size: 1.4rem; }
|
||||
.ab-bar .n { width: 48px; }
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
关于页 · 动效层(v7:纯 UIKit 官方写法,与其他页面完全同源,零自定义)
|
||||
──────────────────────────────────────────────────────────────────────────
|
||||
全部动画只由 UIKit scrollspy 驱动(归档页/文章页同款机制):
|
||||
· 官方动画类:uk-animation-fade(0.8s 淡入)、
|
||||
uk-animation-slide-{bottom,right}-medium(0.5s / 50px 滑入)
|
||||
—— 官方默认时长就在打包 CSS 里:[class*=uk-animation-]{duration:.5s},
|
||||
fade 0.8s。★ 这里不再覆盖时长/keyframes(v6 的自定义层已删,
|
||||
之前注释里说的「主题 0.2s 压制」实测并不存在)。
|
||||
· 内容组 repeat: true:移出视口重藏、滚回重播 —— 滚动时页面是活的。
|
||||
本文件只保留两件 UIKit 管不了的事:分类条宽度填充、减弱动效守卫。
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── 1) 分类条:填充宽度从 0 长出 ──
|
||||
UIKit 只管「淡入+位移」,宽度是它不管的,用 transition 补。
|
||||
条体被 scrollspy 判定进视口后会带上 .uk-scrollspy-inview,以此放行到目标宽度;
|
||||
repeat 移出视口时类被摘掉 → 宽度回到 0,下次滚进重新长出。 */
|
||||
.ab-bar .t i {
|
||||
width: 0;
|
||||
transition: width 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) 0.15s;
|
||||
}
|
||||
.ab-bar.uk-scrollspy-inview .t i,
|
||||
.uk-scrollspy-inview .ab-bar .t i {
|
||||
width: var(--ab-bar-w, 0%);
|
||||
}
|
||||
|
||||
/* ── 2) 无障碍:尊重系统「减弱动态效果」──
|
||||
scrollspy 默认 hidden:true,会用**内联** visibility:hidden 把未进视口的
|
||||
target 藏起来(注意是打在 target 指向的子元素上)。开了「减弱动效」的用户
|
||||
若遇到观察器异常就会看到空白,所以这里一律放行可见 + 取消动画。
|
||||
★ !important 必需 —— 对手是内联样式。 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* ★ 必须同时覆盖「带 uk-scrollspy 的元素本身」和它的后代:
|
||||
scrollspy 把内联 visibility:hidden 打在 **target 指向的元素**上。
|
||||
当 target 就是元素自身(如 .section-title 自己写 cls:)时,它自己中招;
|
||||
当 target 是子元素(如 target: .ab-attr)时,子元素中招。
|
||||
只写 `[uk-scrollspy] *` 会漏掉前一种情况 → 分节标题在 reduce 下空白。
|
||||
两行都要有。 */
|
||||
.about-page [uk-scrollspy],
|
||||
.about-page [uk-scrollspy] *,
|
||||
.about-page .uk-animation-fade,
|
||||
.about-page .uk-animation-slide-bottom-medium,
|
||||
.about-page .uk-animation-slide-right-medium,
|
||||
.about-page .uk-animation-slide-top-medium,
|
||||
.about-page .uk-animation-slide-left-medium,
|
||||
.featc-item {
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
.ab-bar .t i { width: var(--ab-bar-w, 0%); transition: none; }
|
||||
}
|
||||
|
||||
/* ── 6) 数据卡悬停:极轻的边框加深,与主题其他卡片观感一致 ──
|
||||
只为「可点」之外的静态卡增加一点呼吸感,不加阴影跳变。 */
|
||||
.ab-stat {
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
.ab-stat:hover { border-color: rgba(0, 0, 0, 0.16); }
|
||||
[data-theme="dark"] .ab-stat:hover { border-color: rgba(255, 255, 255, 0.16); }
|
||||
|
||||
/* ── 关于页响应式 ── */
|
||||
@media (max-width: 700px) {
|
||||
.ab-attr { gap: 12px; }
|
||||
.ab-attr .k { width: 54px; }
|
||||
.ab-stats { gap: 8px; }
|
||||
.ab-stat { padding: 13px 4px; }
|
||||
.ab-stat .n { font-size: 1.4rem; }
|
||||
.ab-bar .n { width: 48px; }
|
||||
}
|
||||
|
||||
/* Selection Popup (Quote Button) */
|
||||
#selection-popup {
|
||||
position: fixed;
|
||||
|
||||
@@ -21,6 +21,32 @@
|
||||
console.log("Pjax initialized:", pjax);
|
||||
})();
|
||||
|
||||
// UIKit 重新扫描(PJAX 必需)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// UIKit 只在页面加载时做一次「全量 boot」:它遍历 document.body,把带
|
||||
// uk-* 属性的元素逐个 connect 成组件。PJAX 换入的 DOM 是我们用
|
||||
// innerHTML 塞进去的,UIKit 的 MutationObserver 虽然监听了 childList,
|
||||
// 但因为「换入前属性就已存在」,observer 拿不到 attribute 的新增事件,
|
||||
// 加上 PJAX 的替换发生在 UIKit 首次 boot 之后,组件就永远绑不上。
|
||||
//
|
||||
// 症状:uk-scrollspy 的元素停在 visibility:hidden(动画类的初始态),
|
||||
// 下折叠的内容永久不可见,滚动也不会出现。
|
||||
//
|
||||
// 解法:换页后手动调一次 UIkit.update()。它会从传入节点向上走到
|
||||
// document.body,再对整棵子树递归 connect —— 正好补上这次 boot。
|
||||
function reinitUIKit(root) {
|
||||
if (typeof window.UIkit === 'undefined' || typeof window.UIkit.update !== 'function') return;
|
||||
var target = root || document.getElementById('pjax-container') || document.body;
|
||||
try {
|
||||
window.UIkit.update(target);
|
||||
} catch (e) {
|
||||
console.warn('UIkit.update failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给页面脚本:动态插入带 uk-* 属性的内容后,调一次即可让 UIKit 生效
|
||||
window.reinitUIKit = reinitUIKit;
|
||||
|
||||
// pjax预先加载代码
|
||||
function pjax_reload() {
|
||||
console.log("Pjax reload triggered");
|
||||
@@ -99,6 +125,12 @@ document.addEventListener("pjax:complete", function () {
|
||||
if (typeof NProgress !== 'undefined') NProgress.done();
|
||||
pjax_reload();
|
||||
|
||||
// ★ 关键:pjax_reload() 里刚给评论/说说等元素新加了 uk-scrollspy 属性,
|
||||
// 此刻连同换入的整棵新 DOM 一起交给 UIKit 重新 boot一遍。
|
||||
// 必须在 pjax_reload() 之后调用,否则新加的属性会被漏掉。
|
||||
// (关于页的分节动画也走 UIKit scrollspy,所以同样靠这一步复活。)
|
||||
reinitUIKit();
|
||||
|
||||
// 动态加载页面特定JS(PJAX适配)
|
||||
// 检测当前页面是否为文章详情页,如果是则加载page-only.js
|
||||
var currentPath = window.location.pathname;
|
||||
|
||||
@@ -1,57 +1,87 @@
|
||||
{{ define "main" }}
|
||||
<div id="pjax-container" data-needs-echarts="true">
|
||||
<div class="post-content" itemprop="articleBody" uk-scrollspy="target: > *; cls: uk-animation-slide-bottom-small; delay: 100">
|
||||
{{ .Content }}
|
||||
|
||||
{{ $sectionCount := 0 }}
|
||||
{{/* 关于页分节顺序:01 关于我(partial,含属性+时间线+数据)
|
||||
02 这个博客(content/about.md)
|
||||
03+ 走心评论 / 热力图 / 技能栈 / 相册,按 hugo.toml 开关动态编号。
|
||||
间距统一由 .about-sec 管,别写内联 margin。
|
||||
|
||||
<!-- Featured Comments Section(走心评论:管理员精选的评论,数据来自 Worker) -->
|
||||
{{ if site.Params.about.enablefeatured | default true }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div style="margin-top: 40px;">
|
||||
<div class="section-title">{{ printf "%02d" $sectionCount }} / 走心评论 (Featured)</div>
|
||||
{{ partial "featured-comments.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
── 动效:与归档页 archives.html 完全同源的官方写法(v7,零自定义 JS)──
|
||||
官方文档(uikit.cn/docs/animation):
|
||||
「将 .uk-animation-* 类之一添加到任何元素。当添加类时会显示动画…要在
|
||||
其他时间点显示动画,例如当元素进入视口时,您需要使用 JavaScript
|
||||
添加该类 —— 例如,使用滚动监听组件。」
|
||||
|
||||
<!-- Heatmap Section -->
|
||||
{{ if site.Params.about.enableheatmap }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div style="margin-top: 40px;">
|
||||
<div class="section-title">{{ printf "%02d" $sectionCount }} / 热力图 (Hot Map)</div>
|
||||
{{ partial "heatmap.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
四条要点(全部页面既有范式,本页照办):
|
||||
① scrollspy 挂在被包裹的容器上,一个容器托管一组元素。
|
||||
② target 用后代选择器(.ab-attr),不写 `>` —— 写 `>` 只匹配直接
|
||||
子元素,结构一变就一个都选不中(v4 动画全灭的根因)。
|
||||
③ 动画类一律官方:uk-animation-fade(标题淡入 0.8s)、
|
||||
uk-animation-slide-{bottom,right}-medium(内容 50px 滑入 0.5s)。
|
||||
★ 不要用 -small:位移只有 10px,在这么宽的版面里根本看不出来。
|
||||
④ 内容组一律 repeat: true —— 移出视口重藏、滚回重播(归档页年份
|
||||
标题同款),页面滚动时才是「活的」;分节标题不 repeat(同归档 h3)。
|
||||
|
||||
<!-- Skills Section -->
|
||||
{{ if site.Params.about.enableskills }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div style="margin-top: 40px;">
|
||||
<div class="section-title">{{ printf "%02d" $sectionCount }} / 技能栈 (System Ops)</div>
|
||||
{{ partial "skills.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Album Section -->
|
||||
{{ if site.Params.about.enablealbum }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div style="margin-top: 40px;">
|
||||
<div class="section-title">{{ printf "%02d" $sectionCount }} / 光影 (Album)</div>
|
||||
<div class="album-content" uk-scrollspy="target: > .album-item; cls: uk-animation-slide-bottom-small; delay: 100">
|
||||
{{ range site.Data.album }}
|
||||
<div class="album-item">
|
||||
<div class="album-img-box">
|
||||
<img src="{{ .image }}" loading="lazy">
|
||||
{{ if .date }}<span class="album-date-badge">{{ .date }}</span>{{ end }}
|
||||
<div class="album-info">
|
||||
<span class="album-title">{{ .title }}</span>
|
||||
PJAX:换入的 DOM 由 mypjax.js 的 reinitUIKit() 重新 boot(UIKit 的
|
||||
MutationObserver 只反应属性新增,管不了 PJAX 整树换入)。
|
||||
异步渲染的内容(走心评论卡片)渲染完要自己调 UIkit.update()。 */}}
|
||||
|
||||
<div id="pjax-container" data-needs-echarts="true">
|
||||
<div class="post-content about-page" itemprop="articleBody">
|
||||
<div class="about-sec">
|
||||
<div class="section-title" uk-scrollspy="cls: uk-animation-fade; delay: 100">01 / 关于我 (Timeline)</div>
|
||||
{{ partial "about-intro.html" . }}
|
||||
</div>
|
||||
|
||||
{{ .Content }}
|
||||
|
||||
{{ $sectionCount := 2 }}
|
||||
|
||||
<!-- Featured Comments Section(走心评论:管理员精选的评论,数据来自 Worker) -->
|
||||
{{ if site.Params.about.enablefeatured | default true }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div class="about-sec">
|
||||
<div class="section-title" uk-scrollspy="cls: uk-animation-fade; delay: 100">{{ printf "%02d" $sectionCount }} / 走心评论 (Featured)</div>
|
||||
{{ partial "featured-comments.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Heatmap Section -->
|
||||
{{ if site.Params.about.enableheatmap }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div class="about-sec">
|
||||
<div class="section-title" uk-scrollspy="cls: uk-animation-fade; delay: 100">{{ printf "%02d" $sectionCount }} / 热力图 (Hot Map)</div>
|
||||
{{ partial "heatmap.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Skills Section -->
|
||||
{{ if site.Params.about.enableskills }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div class="about-sec">
|
||||
<div class="section-title" uk-scrollspy="cls: uk-animation-fade; delay: 100">{{ printf "%02d" $sectionCount }} / 技能栈 (System Ops)</div>
|
||||
{{ partial "skills.html" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Album Section -->
|
||||
{{ if site.Params.about.enablealbum }}
|
||||
{{ $sectionCount = add $sectionCount 1 }}
|
||||
<div class="about-sec">
|
||||
<div class="section-title" uk-scrollspy="cls: uk-animation-fade; delay: 100">{{ printf "%02d" $sectionCount }} / 光影 (Album)</div>
|
||||
<div class="album-content" uk-scrollspy="target: .album-item; cls: uk-animation-slide-bottom-medium; delay: 100; repeat: true">
|
||||
{{ range site.Data.album }}
|
||||
<div class="album-item">
|
||||
<div class="album-img-box">
|
||||
<img src="{{ .image }}" loading="lazy">
|
||||
{{ if .date }}<span class="album-date-badge">{{ .date }}</span>{{ end }}
|
||||
<div class="album-info">
|
||||
<span class="album-title">{{ .title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,110 @@
|
||||
{{/* 关于页 · B+C 方案:时间线(B) + 数据一览(C)
|
||||
────────────────────────────────────────────────────────────────
|
||||
数据全部在构建期从 content/ 实时统计,不依赖任何外部接口:
|
||||
· 篇数 / 年数 / 站龄 → .Site.RegularPages 与最早文章日期
|
||||
· 分类占比 → .Site.Taxonomies.categories
|
||||
· 热力图 → 由 heatmap.html 提供(本 partial 不重复渲染)
|
||||
★ 时间线节点写在下方 $nodes 里,改文案只动那一处。
|
||||
──────────────────────────────────────────────────────────────── */}}
|
||||
|
||||
{{- $posts := where .Site.RegularPages "Type" "posts" -}}
|
||||
|
||||
{{/* ── 统计:总篇数 ── */}}
|
||||
{{- $total := len $posts -}}
|
||||
|
||||
{{/* ── 统计:首篇日期与年数 ──
|
||||
用最早的一篇(ByDate 升序后的 index 0)作为起点,算站龄。 */}}
|
||||
{{- $ordered := $posts.ByDate -}}
|
||||
{{- $first := index $ordered 0 -}}
|
||||
{{- $firstYear := $first.Date.Format "2006" -}}
|
||||
{{- $nowYear := now.Format "2006" -}}
|
||||
{{- $spanYears := sub (int $nowYear) (int $firstYear) -}}
|
||||
|
||||
{{/* ── 统计:今年更新篇数(当年发文数) ── */}}
|
||||
{{- $thisYear := len (where $posts "Date.Year" (int $nowYear)) -}}
|
||||
|
||||
{{/* ── 统计:分类占比(取前 5,按篇数降序) ── */}}
|
||||
{{- $cats := slice -}}
|
||||
{{- with .Site.Taxonomies.categories -}}
|
||||
{{- range . -}}
|
||||
{{- $cats = $cats | append (dict "name" .Page.LinkTitle "count" (len .Pages)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $cats = sort $cats "count" "desc" -}}
|
||||
{{- $top := first 5 $cats -}}
|
||||
{{- $maxC := 1 -}}
|
||||
{{- with $top }}{{ $maxC = (index . 0).count }}{{ end -}}
|
||||
|
||||
{{/* ── 时间线节点 ──
|
||||
想改文案/加节点,只改这个数组即可。isNow = true 的节点会画成实心圆点。 */}}
|
||||
{{- $nodes := slice
|
||||
(dict "when" "2020 · 起点"
|
||||
"title" "在襄阳,给博客开了个头"
|
||||
"body" "高考完的那个夏天,怀揣着对互联网的好奇搭了这个小站。第一篇文章还留在归档里。"
|
||||
"isNow" false)
|
||||
(dict "when" "2021 — 2024 · 折腾"
|
||||
"title" "换主题、磨 CSS、把 Typecho 拆了个遍"
|
||||
"body" "那时候不懂代码,到处找教程,把好好的主题改得面目全非,但乐在其中。"
|
||||
"isNow" false)
|
||||
(dict "when" "2025 · 做减法"
|
||||
"title" "从 Typecho 搬到 Hugo"
|
||||
"body" "文章多了才明白,博客的本质是内容,不是花哨的外表。"
|
||||
"isNow" false)
|
||||
(dict "when" "此刻 · 在广州"
|
||||
"title" "毕业两年,深漂广漂中"
|
||||
"body" "不活跃就是在跟生活对线。想去南京落脚,先把手里的事做好。"
|
||||
"isNow" true)
|
||||
-}}
|
||||
|
||||
<!-- ─────────── 01 / 关于我(属性 + 时间线 · 方案 B) ───────────
|
||||
动效:与归档页 page/archives.html 完全同源的官方写法:
|
||||
· target 用后代选择器(不写 `>`,避免结构耦合选不中)
|
||||
· 动画类用官方 uk-animation-slide-*-medium(50px 位移,看得清)
|
||||
· repeat: true —— 每次滚进视口都重播(归档页年份标题同款),
|
||||
这是「滚动时页面是活的」的关键;hidden 用默认值
|
||||
· delay 官方语义:同一批进视口的 target 依次累加 → 自动错落 -->
|
||||
<div class="ab-attrs" uk-scrollspy="target: .ab-attr; cls: uk-animation-slide-bottom-medium; delay: 80; repeat: true">
|
||||
<div class="ab-attr"><span class="k">坐标</span><span class="v">湖北 · 襄阳 → 广州</span></div>
|
||||
<div class="ab-attr"><span class="k">其人</span><span class="v">00 后,性格随和,风趣幽默</span></div>
|
||||
<div class="ab-attr"><span class="k">打算</span><span class="v">未来计划定居江苏南京</span></div>
|
||||
<div class="ab-attr"><span class="k">近况</span><span class="v">深漂广漂中,不活跃就是在跟生活对线</span></div>
|
||||
</div>
|
||||
|
||||
<!-- 时间线:节点自左向右滑入(官方 slide-right-medium),四点次第点亮 -->
|
||||
<div class="ab-tl" uk-scrollspy="target: .ab-item; cls: uk-animation-slide-right-medium; delay: 110; repeat: true">
|
||||
{{- range $nodes }}
|
||||
<div class="ab-item{{ if .isNow }} is-now{{ end }}">
|
||||
<div class="ab-when">{{ .when }}</div>
|
||||
<p class="ab-h">{{ .title }}</p>
|
||||
<p class="ab-p">{{ .body }}</p>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
|
||||
<!-- ─────────── 02 / 数据一览(方案 C) ───────────
|
||||
三张数据卡依次自下浮起(官方 slide-bottom-medium)。 -->
|
||||
<div class="ab-stats" uk-scrollspy="target: .ab-stat; cls: uk-animation-slide-bottom-medium; delay: 120; repeat: true">
|
||||
<div class="ab-stat"><span class="n">{{ $total }}</span><span class="l">篇文章</span></div>
|
||||
<div class="ab-stat"><span class="n">{{ $spanYears }}</span><span class="l">年积累</span></div>
|
||||
<div class="ab-stat"><span class="n">{{ $thisYear }}</span><span class="l">今年更新</span></div>
|
||||
</div>
|
||||
|
||||
{{- if $top }}
|
||||
<!-- 分类条:条体浮起,条内填充宽度由 CSS transition 从 0 长出(UIKit 不管宽度)。
|
||||
★ 目标宽度写进内联 --ab-bar-w,CSS 在 .uk-scrollspy-inview 时放行;
|
||||
repeat 移出视口时类被摘掉 → 宽度回到 0,下次滚进重新长出。 -->
|
||||
<div class="ab-bars" uk-scrollspy="target: .ab-bar; cls: uk-animation-slide-bottom-medium; delay: 60; repeat: true">
|
||||
{{- range $top }}
|
||||
{{- $pct := div (mul .count 100) $maxC }}
|
||||
<div class="ab-bar" style="--ab-bar-w: {{ $pct }}%;">
|
||||
<span class="n">{{ .name }}</span>
|
||||
<span class="t"><i></i></span>
|
||||
<span class="c">{{ .count }}</span>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
<p class="ab-footnote">
|
||||
首篇发布于 {{ $first.Date.Format "2006 年 1 月" }} · 以上统计在构建时实时生成
|
||||
</p>
|
||||
{{- end }}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<style>
|
||||
.featc-wrap { min-width: 0; }
|
||||
.featc-title {
|
||||
font-size: 1.35rem; font-weight: 700; color: var(--text-title-color);
|
||||
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: 0.72rem; font-weight: 600; vertical-align: super;
|
||||
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: 0.86rem; line-height: 1.8; color: var(--text-desc, #777); margin: 0 0 18px; }
|
||||
.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;
|
||||
@@ -36,24 +36,24 @@
|
||||
.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: 0.92rem; font-weight: 700; color: var(--text-title-color, #333);
|
||||
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: 0.72rem; color: var(--text-muted, #999); }
|
||||
.featc-date { margin-left: auto; flex: none; font-size: var(--ab-fs-micro, 0.75rem); color: var(--text-muted, #999); }
|
||||
.featc-content {
|
||||
font-size: 0.88rem; line-height: 1.7; color: var(--text-main, #333);
|
||||
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: 0.72rem; color: var(--text-muted, #999); line-height: 1.6;
|
||||
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: 0.86rem; color: var(--text-muted, #999); padding: 8px 0; }
|
||||
.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,
|
||||
@@ -70,13 +70,96 @@
|
||||
@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">
|
||||
<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">
|
||||
<div class="featc-empty" id="featc-loading">加载中…</div>
|
||||
<!-- 骨架屏:接口约 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>
|
||||
|
||||
@@ -106,7 +189,7 @@
|
||||
var avatar = 'https://weavatar.com/avatar/' + esc(c.email_encrypted) + '?d=mp&s=120';
|
||||
var nick = esc(c.nick);
|
||||
html +=
|
||||
'<a class="featc-card" href="' + esc(c.page_url) + '#atk-comment-' + esc(c.id) + '">' +
|
||||
'<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>' +
|
||||
@@ -117,9 +200,47 @@
|
||||
'</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 load() {
|
||||
// 失败/超时提示:明确告知并给一次重试,比一直挂着骨架屏好
|
||||
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([
|
||||
@@ -133,11 +254,33 @@
|
||||
]);
|
||||
return;
|
||||
}
|
||||
fetch(API)
|
||||
|
||||
// 会话内缓存:先进关于页已经拉过一次,再进(含 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) { render((data && data.comments) || []); })
|
||||
.catch(function () {
|
||||
if (grid) grid.innerHTML = '<div class="featc-empty">还没有走心评论,去和博主聊聊吧~</div>';
|
||||
.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('评论暂时没加载出来');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,14 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="heatmap-wrapper" class="heatmap-scroll-wrapper">
|
||||
<div id="heatmap-wrapper" class="heatmap-scroll-wrapper" uk-scrollspy="cls: uk-animation-slide-bottom-medium; delay: 60">
|
||||
<div id="heatmap" style="max-width: 800px; height: 160px; margin: 0 auto 0px auto; padding: 10px; border-radius: 10px; transition: all 0.3s ease; display: block !important; visibility: visible !important; min-width: 300px;"></div>
|
||||
</div>
|
||||
{{/* ★ 动效说明:散点图画在 #heatmap 里,那行内联 `visibility:visible!important`
|
||||
是 ECharts 测量容器尺寸所需,不能被 scrollspy 的 hidden 覆盖。
|
||||
所以 scrollspy 挂在**外层 wrapper** 上(用 cls:,target 默认为自身),
|
||||
官方 slide-bottom-medium(50px 上浮)入场;图本身不受影响。
|
||||
不开 repeat —— 单个大块反复重播观感太跳。 */}}
|
||||
|
||||
<script type="text/javascript">
|
||||
function renderHeatmap() {
|
||||
|
||||
Reference in new issue
Block a user