feat(motion): 动效层三件——背景辉光/入场/心跳,页脚撤双入口
- 背景鼠标辉光: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
This commit is contained in:
1 parent
0b6524f53c
commit
363cc616f7
6 files changed
+110
-5
No files matched your search
@@ -20,10 +20,6 @@ const locale = propLocale ?? localeOf(Astro.url.pathname);
|
||||
<a href="https://beian.miit.gov.cn/" rel="noopener noreferrer" target="_blank" class="link">
|
||||
{t('footer_icp', locale)}
|
||||
</a>
|
||||
·
|
||||
<a href={locale === 'en' ? '/en/stack/' : '/stack/'} class="link">{t('footer_stack', locale)}</a>
|
||||
·
|
||||
<a href={locale === 'en' ? '/en/colophon/' : '/colophon/'} class="link">{t('footer_colophon', locale)}</a>
|
||||
</p>
|
||||
<!-- T7 注入:built from {commit} · {time} · Astro(链到 gitea commit) -->
|
||||
<slot name="build-info" />
|
||||
|
||||
@@ -44,6 +44,22 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
|
||||
<meta name="theme-color" content="#191d23" media="(prefers-color-scheme: dark)" />
|
||||
<title>{title}</title>
|
||||
<ThemeScript />
|
||||
<script is:inline>
|
||||
// 背景辉光跟随(一次性注册:document 级监听 × raf 节流,VT 换页存活);
|
||||
// pointer:fine 且非 reduced-motion 才启用,其余设备辉光层保持隐藏
|
||||
if (matchMedia('(pointer: fine)').matches && !matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
let raf = 0, mx = 50, my = 30;
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
mx = (e.clientX / innerWidth) * 100;
|
||||
my = (e.clientY / innerHeight) * 100;
|
||||
if (!raf) raf = requestAnimationFrame(() => {
|
||||
raf = 0;
|
||||
document.documentElement.style.setProperty('--mx', mx + '%');
|
||||
document.documentElement.style.setProperty('--my', my + '%');
|
||||
});
|
||||
}, { passive: true });
|
||||
}
|
||||
</script>
|
||||
<ClientRouter />
|
||||
<!-- 非阻塞字体 CSS:preload+onload 回切(Lighthouse P0-1:渲染阻塞 708KB 元凶) -->
|
||||
<link rel="preload" as="style" href={sansVar} onload="this.rel='stylesheet'" />
|
||||
@@ -76,6 +92,9 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
|
||||
<link rel="alternate" hreflang="x-default" href={new URL(withSlash(zhPath), Astro.site)} />
|
||||
</head>
|
||||
<body>
|
||||
<!-- 背景辉光:跟随鼠标的 ink 色径向渐变(丙灰阶语言,3-4% 透明度;
|
||||
pointer:fine + 非 reduced-motion 才有 JS 驱动,其余静态隐藏) -->
|
||||
<div class="bg-glow" aria-hidden="true"></div>
|
||||
<a class="skip-link" href="#main">跳到正文</a>
|
||||
<Header />
|
||||
<main id="main" class="container">
|
||||
|
||||
@@ -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<Heartbeat | null> {
|
||||
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)} 个月前`;
|
||||
}
|
||||
@@ -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();
|
||||
---
|
||||
|
||||
<BaseLayout title="wait4x">
|
||||
@@ -99,9 +101,31 @@ const photos = (await getCollection('photos')).slice(0, 4);
|
||||
}}
|
||||
/>
|
||||
<ProjectWall />
|
||||
{heartbeat && (
|
||||
<p class="heartbeat" aria-label="最近心跳">
|
||||
<span class="beat-dot" aria-hidden="true"></span>
|
||||
最近心跳:{agoZh(heartbeat.when)} · {heartbeat.msg}
|
||||
</p>
|
||||
)}
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.heartbeat {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-3);
|
||||
margin-top: var(--space-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.beat-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%;
|
||||
background: var(--ink-2);
|
||||
animation: beat 2.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes beat { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .beat-dot { animation: none; } }
|
||||
.socials {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -189,6 +189,29 @@ hr { border: none; border-top: 1px solid var(--line); margin-block: var(--space-
|
||||
}
|
||||
|
||||
/* ---------- 动效总闸(唯一曲线已入 tokens) ---------- */
|
||||
/* ===== 动效层(Phase 5e):背景辉光 + 入场 + 微交互 ===== */
|
||||
:root { --mx: 50%; --my: 30%; }
|
||||
.bg-glow {
|
||||
position: fixed; inset: 0; z-index: -1; pointer-events: none;
|
||||
background: radial-gradient(600px circle at var(--mx) var(--my),
|
||||
color-mix(in oklab, var(--ink-1) 4%, transparent), transparent 70%);
|
||||
opacity: 0; transition: opacity 0.6s ease;
|
||||
}
|
||||
@media (pointer: fine) {
|
||||
.bg-glow { opacity: 1; }
|
||||
@media (prefers-reduced-motion: reduce) { .bg-glow { opacity: 0; } }
|
||||
}
|
||||
/* 正文入场:一次性轻上浮(VT 换页内容替换自然重放,克制 12px/0.4s) */
|
||||
#main > * { animation: rise-in 0.4s ease both; }
|
||||
@keyframes rise-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#main > * { animation: none; }
|
||||
}
|
||||
/* 列表行微交互(桌面悬停语言:ink 系已有变色,补 1px 上浮) */
|
||||
@media (pointer: fine) {
|
||||
.postlist .row { transition: transform 0.15s ease; }
|
||||
.postlist .row:hover { transform: translateY(-1px); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
|
||||
Reference in new issue
Block a user