From 363cc616f7b68fee5f7aa9900e2fa9754dbad472 Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sun, 25 Oct 2026 22:09:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(motion):=20=E5=8A=A8=E6=95=88=E5=B1=82?= =?UTF-8?q?=E4=B8=89=E4=BB=B6=E2=80=94=E2=80=94=E8=83=8C=E6=99=AF=E8=BE=89?= =?UTF-8?q?=E5=85=89/=E5=85=A5=E5=9C=BA/=E5=BF=83=E8=B7=B3=EF=BC=8C?= =?UTF-8?q?=E9=A1=B5=E8=84=9A=E6=92=A4=E5=8F=8C=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 背景鼠标辉光:ink 色 4% 径向渐变跟随(pointer:fine×非 reduced-motion 才有 JS,raf 节流 document 级监听 VT 存活)—— 丙灰阶语言的"活背景",触屏/减动效自动隐藏 - 正文入场 rise-in 12px/0.4s(VT 换页自然重放)+ 列表行悬停 1px 微浮;全量 prefers-reduced-motion 豁免 - 首页"最近心跳":构建期拉 blog 仓最新 commit(gitea API 8s 超时 静默降级),脉冲点+相对时长——本站自己的脉搏 - 页脚撤「装备/制作」入口(页面保留,用户决策:只删入口) - E2E 加固:g-chords 修好后 location.href 与 goto 竞态(en 页弦 导航迟到提交打断 goto)——中断重试模式 - 过程教训:str.replace 静默无操作再犯(锚点瞄错组件文件)—— 脚本未进产物却报"全落地",replace 必须 assert 配对 grep 产物 验证: --mx 鼠标双跳 15.6% 实测跟随;E2E 31/31 PASS(竞态重试后 确定性);构建 34 页 exit 0 Spec: none (动效层+页脚撤口+测试加固,车道级交付) Co-authored-by: GLM-5.3 --- src/components/Footer.astro | 4 ---- src/layouts/BaseLayout.astro | 19 ++++++++++++++++++ src/lib/heartbeat.ts | 38 ++++++++++++++++++++++++++++++++++++ src/pages/index.astro | 24 +++++++++++++++++++++++ src/styles/global.css | 23 ++++++++++++++++++++++ tests/e2e.mjs | 7 ++++++- 6 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 src/lib/heartbeat.ts diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 8e80bcf..30024a9 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -20,10 +20,6 @@ const locale = propLocale ?? localeOf(Astro.url.pathname); {t('footer_icp', locale)} - · - {t('footer_stack', locale)} - · - {t('footer_colophon', locale)}

diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 629742a..0f3c529 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -44,6 +44,22 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`); {title} + @@ -76,6 +92,9 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`); + +
diff --git a/src/lib/heartbeat.ts b/src/lib/heartbeat.ts new file mode 100644 index 0000000..66442f5 --- /dev/null +++ b/src/lib/heartbeat.ts @@ -0,0 +1,38 @@ +/** + * 首页"最近心跳":构建期拉 blog 仓最新 commit——本站自己的脉搏。 + * 失败静默(返回 null,首页渲染"很久以前"兜底文案),绝不阻塞构建。 + */ +export interface Heartbeat { + /** commit 首行(截 40 字符) */ + msg: string; + /** commit 时间(ISO) */ + when: string; +} + +const API = 'https://git.wait4x.online/api/v1/repos/wait4x/blog/commits?limit=1'; + +export async function fetchHeartbeat(): Promise { + try { + const res = await fetch(API, { + headers: { Accept: 'application/json', 'User-Agent': 'wait4x-blog-build' }, + signal: AbortSignal.timeout(8_000), + }); + if (!res.ok) return null; + const [c] = (await res.json()) as Array<{ commit?: { message?: string; author?: { date?: string } } }>; + const msg = c?.commit?.message?.split('\n')[0]?.slice(0, 40); + const when = c?.commit?.author?.date; + return msg && when ? { msg, when } : null; + } catch { + return null; + } +} + +/** 相对时长(中文短句;en 页由调用方换词条) */ +export function agoZh(iso: string, now = Date.now()): string { + const h = Math.floor((now - new Date(iso).getTime()) / 3_600_000); + if (h < 1) return '刚刚'; + if (h < 24) return `${h} 小时前`; + const d = Math.floor(h / 24); + if (d < 30) return `${d} 天前`; + return `${Math.floor(d / 30)} 个月前`; +} diff --git a/src/pages/index.astro b/src/pages/index.astro index 78660df..a1167ca 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -6,6 +6,7 @@ import IdCard from '../components/IdCard.astro'; import PostList from '../components/PostList.astro'; import ProjectWall from '../components/geek/ProjectWall.astro'; import Heatmap from '../components/geek/Heatmap.astro'; +import { fetchHeartbeat, agoZh } from '../lib/heartbeat'; import { t } from '../i18n/ui'; const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort( @@ -13,6 +14,7 @@ const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.t ); // 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切 const photos = (await getCollection('photos')).slice(0, 4); +const heartbeat = await fetchHeartbeat(); --- @@ -99,9 +101,31 @@ const photos = (await getCollection('photos')).slice(0, 4); }} /> + {heartbeat && ( +

+ + 最近心跳:{agoZh(heartbeat.when)} · {heartbeat.msg} +

+ )}