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:
zqlit committed 2026-10-07 21:45:46 +08:00
1 parent a884180787
commit 654de1fdf0
7 files changed
+692 -88

No files matched your search

+278
View File
@@ -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;
+32
View File
@@ -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;