feat(geek): 极客层 P0 四件套——键盘/构建透明/页面转场/项目墙

- Keyboard:? 帮助浮层 + t 明暗 + g-chords 导航(300ms 缓冲,输入域旁路);
  路由表经 data-routes 注入(module 脚本 currentScript 为 null 的坑)
- BuildInfo:vite.define 四重回退注入 commit/时间,hash 链到 gitea
- ClientRouter 页面转场(reduced-motion 降级在 global.css 总闸)
- ProjectWall:构建时双源拉取(gitea+github)置顶排序+缓存兜底,零运行时 JS
- Header 补主题可视切换钮:键盘 t 的触屏等价物(同 key 同逻辑)

验证: pnpm astro build exit 0;dist 标记 built from×2/kbd-help×4/
      view-transition×2; astro check 未跑(无类型错误构建期可证伪面有限)

Spec: 2026-10-24_add-blog-skeleton
Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-10-24 23:13:23 +08:00
1 parent b50eddf2bb
commit 725bfde21f
8 files changed
+324 -2

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
// 项目墙数据(极客层 P0):构建时双源拉取 + 仓内缓存兜底
// 双源 = 自家 gitea(数据自控叙事)+ GitHub(社区 star 真值);置顶清单人工维护
export interface RepoCard {
source: 'gitea' | 'github';
name: string;
description: string;
language: string | null;
stars: number;
url: string;
updatedAt: string;
}
interface RawRepo {
full_name?: string;
name?: string;
description?: string | null;
language?: string | null;
stars_count?: number;
stargazers_count?: number;
html_url?: string;
updated_at?: string;
}
export const GITEA_API = 'https://git.wait4x.online/api/v1/users/wait4x/repos?limit=50';
export const GITHUB_API = 'https://api.github.com/users/wait4xx/repos?per_page=100&sort=updated';
/** 置顶清单:跨源手选,图钉顺序即展示顺序(name = 仓库短名) */
export const PINNED: string[] = ['blog'];
async function fetchJson(url: string): Promise<RawRepo[]> {
const res = await fetch(url, {
headers: { Accept: 'application/json', 'User-Agent': 'wait4x-blog-build' },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error(`${url} -> ${res.status}`);
return (await res.json()) as RawRepo[];
}
function toCard(r: RawRepo, source: RepoCard['source']): RepoCard | null {
const name = r.name ?? r.full_name?.split('/')[1];
if (!name || !r.html_url) return null;
return {
source,
name,
description: r.description ?? '',
language: r.language ?? null,
stars: r.stars_count ?? r.stargazers_count ?? 0,
url: r.html_url,
updatedAt: r.updated_at ?? '',
};
}
/**
* 拉双源合并去重(同名优先 gitea 自托管叙事,star 取 GitHub 真值)。
* 任一源失败不抛:置顶项回落仓内缓存 JSON(构建产物绝不被网络单点卡死)。
*/
export async function loadProjects(): Promise<RepoCard[]> {
const [gitea, github] = await Promise.allSettled([fetchJson(GITEA_API), fetchJson(GITHUB_API)]);
const byName = new Map<string, RepoCard>();
for (const r of gitea.status === 'fulfilled' ? gitea.value : []) {
const c = toCard(r, 'gitea');
if (c) byName.set(c.name, c);
}
for (const r of github.status === 'fulfilled' ? github.value : []) {
const c = toCard(r, 'github');
if (!c) continue;
const existing = byName.get(c.name);
if (!existing) byName.set(c.name, c);
else if (c.stars > existing.stars) existing.stars = c.stars; // 社区真值覆盖镜像计数
}
if (byName.size === 0) {
const fb = (await import('../data/projects-fallback.json')).default as RepoCard[];
return orderPinned(fb);
}
return orderPinned([...byName.values()]);
}
function orderPinned(list: RepoCard[]): RepoCard[] {
return [...list].sort((a, b) => {
const pa = PINNED.indexOf(a.name), pb = PINNED.indexOf(b.name);
if (pa !== -1 || pb !== -1) return (pa === -1 ? 99 : pa) - (pb === -1 ? 99 : pb);
return b.stars - a.stars;
}).slice(0, 6);
}