- 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
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
// 项目墙数据(极客层 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);
|
|
}
|