From 0ff4ede91fbdc43b22745d4a456da7761715b06f Mon Sep 17 00:00:00 2001 From: wait4xx Date: Tue, 3 Nov 2026 11:24:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(opensource):=20=E9=A1=B9=E7=9B=AE=E5=A2=99?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=B1=85=E4=B8=AD=EF=BC=8CREADME=20=E5=8A=A8?= =?UTF-8?q?=E6=95=88=E4=BB=A5=E8=87=AA=E6=89=98=E7=AE=A1=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=EF=BC=885i=20T6/T8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户两项要求:开源页 Projects「依旧采取居中即可,不用左对齐」;README 迁移后 「很多图片元素还有一些 svg 基本上都摒弃了,可以在 readme 的基础上进行再创新, 保留那些动效的图,以及创新更多一些显示方式」。 T6 项目墙居中:`.repo` 从 align-items:stretch/text-align:left 改回 center/center, `.repo-desc` 加 max-width:34em 防居中后长描述散架;语言点保持灰阶(与对齐无关)。 T8 动效回归(零运行时外链): - 新增 geek/TypingLines.astro:自绘打字机(SSR 先出首行防闪白,JS 逐字打出/删除/ 轮播,光标 CSS 闪烁,prefers-reduced-motion 与单条文案时保持静态,min-height 防抖,astro:page-load 重挂兼容软导航)——替代 readme-typing-svg 外链,且跟随主题 - 新增 scripts/build-perf/fetch-readme-assets.mjs:抓 stats/top-langs/streak/snake 到 public/github-assets/(10s 超时 + UA、透明底、亮暗两套站点色),产物入仓保证 离线可构建;单资产失败保留旧文件、无旧文件则该卡不渲染 - GithubProfile 新增「数据板」:灰阶 hairline 卡 + mono 小标题 + 2 列网格(移动单列), 与原生统计四格、语言占比、热力图组合成展示区;贡献蛇在暗色加 grayscale+invert 滤镜(SVG 内部是自带绿阶,直接贴暗底刺眼) - build.sh 只做存在性守卫 + 缺资产 WARN,**不挂抓取**(发布链不被外网抖动拖累) - 后台开源页新增 typing 字段(list,zh/en 双语,≤6 条,summary 取 zh)+ schema 声明 验证: 构建 51 页 exit 0;config-check 211 字段/66 hint 全过;E2E 46/46; 产物断言第三方外链零命中、本地引用 streak-light/streak-dark/snake; Playwright 实测打字机文本 900ms 内变化(动效在跑)、streak naturalWidth=495、 snake=880、暗色自动切 streak-dark、项目墙 center/center、1280 与 390 均零横向溢出 备注: github-readme-stats 今日全局 503,stats/top-langs 两卡按契约跳过(原生四格与 语言条已覆盖同一信息);服务恢复后跑一次 fetch-readme-assets.mjs 即自动补上。 Spec: 2026-11-02_blog-5i-fonts-polish (T6/T8) Co-authored-by: GLM-5.3 --- infra/deploy/build.sh | 7 ++ public/admin/config.yml | 9 ++ public/github-assets/manifest.json | 22 ++++ public/github-assets/snake.svg | 1 + public/github-assets/streak-dark.svg | 111 +++++++++++++++++++ public/github-assets/streak-light.svg | 111 +++++++++++++++++++ scripts/build-perf/fetch-readme-assets.mjs | 121 +++++++++++++++++++++ src/components/geek/GithubProfile.astro | 106 ++++++++++++++++++ src/components/geek/ProjectWall.astro | 14 ++- src/components/geek/TypingLines.astro | 106 ++++++++++++++++++ src/content.config.ts | 5 + 11 files changed, 608 insertions(+), 5 deletions(-) create mode 100644 public/github-assets/manifest.json create mode 100644 public/github-assets/snake.svg create mode 100644 public/github-assets/streak-dark.svg create mode 100644 public/github-assets/streak-light.svg create mode 100644 scripts/build-perf/fetch-readme-assets.mjs create mode 100644 src/components/geek/TypingLines.astro diff --git a/infra/deploy/build.sh b/infra/deploy/build.sh index d84a4ff..1a7b3e0 100755 --- a/infra/deploy/build.sh +++ b/infra/deploy/build.sh @@ -39,6 +39,13 @@ pnpm astro build [ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过" [ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过" +# README 动效资产(5i T8):**不在此跑抓取**(避免发布链被外网抖动拖累),只做存在性守卫 + 缺资产告警 +if [ -f public/github-assets/manifest.json ]; then + echo "[build] github-assets 就绪:$(find public/github-assets -name '*.svg' | wc -l) 个自托管 SVG" +else + echo "[build] WARN public/github-assets/manifest.json 缺失——开源页数据板将只显示原生统计(可手动跑 node scripts/build-perf/fetch-readme-assets.mjs 补齐)" +fi + # 内容 lint(T3):断链/缺图在部署前拦截;CONTENT_LINT=skip|warn 逃生门见脚本头注 [ -f scripts/build-perf/content-lint.mjs ] && node scripts/build-perf/content-lint.mjs dist || echo "[build] content-lint 未就绪,跳过" diff --git a/public/admin/config.yml b/public/admin/config.yml index ad0c1cb..83c627e 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -535,6 +535,15 @@ collections: file: content/site/opensource.yaml format: yaml fields: + - label: 打字机动效文案(可选,≤6 条) + name: typing + widget: list + required: false + summary: '{{fields.zh}}' + hint: '档案卡顶部逐字打出的几行文案,2–6 条最佳;留空用内置默认。中英各一行,切换语言同步' + fields: + - { label: 中文, name: zh, widget: string } + - { label: English, name: en, widget: string } - label: 叙述(链接前) name: blurb widget: object diff --git a/public/github-assets/manifest.json b/public/github-assets/manifest.json new file mode 100644 index 0000000..aaff915 --- /dev/null +++ b/public/github-assets/manifest.json @@ -0,0 +1,22 @@ +{ + "assets": { + "streak-light.svg": { + "source": "https://streak-stats.demolab.com/?user=wait4xx&hide_border=true&background=00000000&ring=3454d1&fire=3454d1&currStreakLabel=3454d1&currStreakNum=33343a&sideNums=33343a&sideLabels=8b8f98&dates=8b8f98", + "fetchedAt": "2026-11-03T03:07:28.521Z", + "label": "连续贡献", + "theme": "light" + }, + "streak-dark.svg": { + "source": "https://streak-stats.demolab.com/?user=wait4xx&hide_border=true&background=00000000&ring=3454d1&fire=3454d1&currStreakLabel=3454d1&currStreakNum=c9ccd4&sideNums=c9ccd4&sideLabels=9aa0aa&dates=9aa0aa", + "fetchedAt": "2026-11-03T03:07:28.521Z", + "label": "连续贡献", + "theme": "dark" + }, + "snake.svg": { + "source": "https://raw.githubusercontent.com/wait4xx/wait4xx/output/github-contribution-grid-snake.svg", + "fetchedAt": "2026-11-03T03:07:28.521Z", + "label": "贡献蛇", + "theme": "any" + } + } +} diff --git a/public/github-assets/snake.svg b/public/github-assets/snake.svg new file mode 100644 index 0000000..2527054 --- /dev/null +++ b/public/github-assets/snake.svg @@ -0,0 +1 @@ +Generated with https://github.com/Platane/snk \ No newline at end of file diff --git a/public/github-assets/streak-dark.svg b/public/github-assets/streak-dark.svg new file mode 100644 index 0000000..8f62d84 --- /dev/null +++ b/public/github-assets/streak-dark.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 344 + + + + + + + Total Contributions + + + + + + + May 24, 2019 - Present + + + + + + + + Current Streak + + + + + + + Nov 3 + + + + + + + + + + + + + + + + + 0 + + + + + + + + + 6 + + + + + + + Longest Streak + + + + + + + Oct 19 - Oct 24 + + + + + + diff --git a/public/github-assets/streak-light.svg b/public/github-assets/streak-light.svg new file mode 100644 index 0000000..29250c7 --- /dev/null +++ b/public/github-assets/streak-light.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 344 + + + + + + + Total Contributions + + + + + + + May 24, 2019 - Present + + + + + + + + Current Streak + + + + + + + Nov 3 + + + + + + + + + + + + + + + + + 0 + + + + + + + + + 6 + + + + + + + Longest Streak + + + + + + + Oct 19 - Oct 24 + + + + + + diff --git a/scripts/build-perf/fetch-readme-assets.mjs b/scripts/build-perf/fetch-readme-assets.mjs new file mode 100644 index 0000000..f12763f --- /dev/null +++ b/scripts/build-perf/fetch-readme-assets.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env node +// README 动效资产抓取器(5i T8):把只能靠外部生成的 SVG 数据卡抓成本地静态文件 +// 设计取舍: +// · 产物入仓(public/github-assets/)→ 构建确定性与离线可构建优先,不在 CI 里跑本脚本 +// · 单资产失败保留旧文件;无旧文件则跳过(组件侧按存在性决定是否渲染该块) +// · 配色按站点 tokens 近似:透明底 + 文字 #33343a(亮)/#c9ccd4(暗) + 线条 #3454d1 +// · manifest.json 记录来源 URL 与抓取时间(可追溯,不含任何凭据) +// 用法:node scripts/build-perf/fetch-readme-assets.mjs [--force] +import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const OUT_DIR = 'public/github-assets'; +const LOGIN = 'wait4xx'; +const TIMEOUT_MS = 10_000; +const UA = 'wait4x-blog-assets/1.0 (+https://wait4x.online)'; + +// 站点 tokens 近似值(与灰阶纪律一致:底透明,线条单一强调色) +const ACCENT = '3454d1'; +const INK_LIGHT = '33343a'; +const INK_DARK = 'c9ccd4'; +const MUTED_LIGHT = '8b8f98'; +const MUTED_DARK = '9aa0aa'; + +/** 同一张卡按明暗两套参数各存一份(组件内用 CSS 切换,零运行时请求) */ +const ASSETS = [ + { + name: 'stats', + url: (theme) => + `https://github-readme-stats.vercel.app/api?username=${LOGIN}` + + '&show_icons=true&hide_border=true&include_all_commits=true&count_private=true&rank_icon=github' + + `&bg_color=00000000&title_color=${ACCENT}&icon_color=${ACCENT}` + + `&text_color=${theme === 'dark' ? INK_DARK : INK_LIGHT}`, + label: 'GitHub 统计', + }, + { + name: 'top-langs', + url: (theme) => + `https://github-readme-stats.vercel.app/api/top-langs/?username=${LOGIN}` + + '&layout=compact&hide_border=true&langs_count=6' + + `&bg_color=00000000&title_color=${ACCENT}` + + `&text_color=${theme === 'dark' ? INK_DARK : INK_LIGHT}`, + label: '语言分布', + }, + { + name: 'streak', + url: (theme) => + `https://streak-stats.demolab.com/?user=${LOGIN}&hide_border=true` + + '&background=00000000' + + `&ring=${ACCENT}&fire=${ACCENT}&currStreakLabel=${ACCENT}` + + `&currStreakNum=${theme === 'dark' ? INK_DARK : INK_LIGHT}` + + `&sideNums=${theme === 'dark' ? INK_DARK : INK_LIGHT}` + + `&sideLabels=${theme === 'dark' ? MUTED_DARK : MUTED_LIGHT}` + + `&dates=${theme === 'dark' ? MUTED_DARK : MUTED_LIGHT}`, + label: '连续贡献', + }, + { + // 贡献蛇:GitHub Action 生成,配色由 action 侧决定,无法传参 → 只抓一份 + name: 'snake', + url: () => `https://raw.githubusercontent.com/${LOGIN}/${LOGIN}/output/github-contribution-grid-snake.svg`, + label: '贡献蛇', + single: true, + }, +]; + +/** 10s 超时抓取;返回 SVG 文本或 null */ +async function grab(url) { + try { + const res = await fetch(url, { + headers: { 'User-Agent': UA, Accept: 'image/svg+xml,image/*;q=0.8,*/*;q=0.5' }, + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + if (!res.ok) { + console.warn(`[assets] HTTP ${res.status}: ${url.slice(0, 90)}…`); + return null; + } + const text = await res.text(); + if (!text.includes(' Math.max(m, l.pct), 0); const stats = await githubUserStats(); + +// 打字机动效文案:content/site/opensource.yaml 的 typing[](双语,后台可改) +// 注:schema 由 orchestrator 统一扩字段;未声明时这里的类型断言取不到值 → 走内置默认 +type TypingEntry = { zh?: string; en?: string }; +const typingRaw = (copy as unknown as { typing?: TypingEntry[] }).typing ?? []; +const typingLines = typingRaw + .map((t) => (locale === 'zh' ? t.zh : (t.en ?? t.zh)) ?? '') + .filter(Boolean); +const DEFAULT_TYPING = { + zh: ['气象出身,写代码为乐', '自托管一切能自托管的东西', '拥抱开源,热爱开源'], + en: ['Trained in meteorology, coding for fun', 'Self-hosting whatever can be self-hosted', 'Embrace open source, love open source'], +}[locale]; +const lines = typingLines.length ? typingLines : DEFAULT_TYPING; + +// 自托管 SVG 数据卡(public/github-assets/,由 scripts/build-perf/fetch-readme-assets.mjs 产出入仓) +// 缺文件即跳过该卡(异步抓取失败不影响构建),明暗各一份时用 CSS 切换 +const assetPath = (name: string) => `/github-assets/${name}`; +const hasAsset = (name: string) => existsSync(`public/github-assets/${name}`); +const boardCards: { label: string; light: string; dark?: string }[] = []; +for (const [key, label] of [ + ['stats', locale === 'zh' ? '统计' : 'Stats'], + ['top-langs', locale === 'zh' ? '语言分布' : 'Top languages'], + ['streak', locale === 'zh' ? '连续贡献' : 'Streak'], +] as const) { + if (!hasAsset(`${key}-light.svg`)) continue; + boardCards.push({ + label, + light: assetPath(`${key}-light.svg`), + dark: hasAsset(`${key}-dark.svg`) ? assetPath(`${key}-dark.svg`) : undefined, + }); +} +const snakeCard = hasAsset('snake.svg') + ? { label: locale === 'zh' ? '贡献蛇' : 'Contribution snake', src: assetPath('snake.svg') } + : null; const L = { zh: { followers: '关注者', repos: '公开仓库', stars: '总星标', langs: '语言占比' }, en: { followers: 'Followers', repos: 'Public repos', stars: 'Total stars', langs: 'Languages' }, @@ -66,6 +102,7 @@ const L = {

wait4x@wait4xx

+

{copy.blurb}{copy.repoLabel}{' '}{copy.blurbAfter.trim()}

@@ -123,6 +160,28 @@ const L = { )} + {(boardCards.length > 0 || snakeCard) && ( +
+

{locale === 'zh' ? '数据板' : 'Data board'}

+
+ {boardCards.map((card) => ( +
+ {card.label} + {card.dark && ( + {card.label} + )} +
{card.label}
+
+ ))} + {snakeCard && ( +
+ {snakeCard.label} +
{snakeCard.label}
+
+ )} +
+
+ )} diff --git a/src/components/geek/ProjectWall.astro b/src/components/geek/ProjectWall.astro index a2ee66c..bcc1f38 100644 --- a/src/components/geek/ProjectWall.astro +++ b/src/components/geek/ProjectWall.astro @@ -73,10 +73,9 @@ try { .repo { display: flex; flex-direction: column; - /* 显式接管交叉轴:全局 .tap 是 inline-flex + align-items:center, - 列向 flex 下会把标题/元信息行水平居中(视觉终审 P1,卡片左缘参差) */ - align-items: stretch; - text-align: left; + /* 用户拍板(5i T6):Projects 卡片内容居中,不用左对齐 */ + align-items: center; + text-align: center; gap: 2px; padding: var(--space-3); border: 1px solid var(--line); @@ -86,7 +85,12 @@ try { } .repo:hover { border-color: var(--accent); } .repo-name { font-family: var(--font-mono); font-size: 14px; color: var(--ink-1); } - .repo-desc { font-size: 13px; color: var(--ink-3); } + .repo-desc { + font-size: 13px; + color: var(--ink-3); + /* 居中后长描述限行宽,避免整行摊平散架 */ + max-width: 34em; + } .repo-meta { display: flex; gap: var(--space-3); font-size: 12px; color: var(--ink-3); margin-top: 4px; } .lang { display: inline-flex; align-items: center; gap: 4px; } .dot { diff --git a/src/components/geek/TypingLines.astro b/src/components/geek/TypingLines.astro new file mode 100644 index 0000000..e2af038 --- /dev/null +++ b/src/components/geek/TypingLines.astro @@ -0,0 +1,106 @@ +--- +// 自绘打字机动效(5i T8):替代外链 readme-typing-svg +// 纯 CSS + 极少 JS:SSR 先渲染第一行(无 JS / reduced-motion 时就是静态首行,不闪不跳), +// JS 接管后循环「逐字打出 → 停留 → 逐字删除 → 下一行」;光标闪烁走 CSS keyframes。 +interface Props { + /** 已本地化的文案行(保持 ≤6 行) */ + lines: string[]; + /** 每字毫秒 */ + speed?: number; +} +const { lines, speed = 55 } = Astro.props; +const safe = lines.map((l) => l.trim()).filter(Boolean).slice(0, 6); +const first = safe[0] ?? ''; +--- + +{safe.length > 0 && ( +

+ + +

+)} + + + + diff --git a/src/content.config.ts b/src/content.config.ts index 255be34..12390ec 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -292,6 +292,11 @@ export const collections = { perSource: z.enum(['both', 'github', 'gitea']).optional(), /** 开篇文案来源:off(默认,用本文件 blurb)/ github-readme(构建期取 profile 仓 README) */ intro_from: z.enum(['off', 'github-readme']).optional(), + /** + * 档案卡顶部的打字机动效文案(自绘 CSS/JS,非第三方 SVG)。 + * 越界(>6 条)由访问器截断;全空则组件用内置默认三行。 + */ + typing: z.array(bilingual).optional(), }), }), // slug 即文件名(glob loader 默认按文件名生成 id)