feat: 添加 GitHub 统计数据展示功能

新增 GitHub 统计卡片和徽章组件,在项目卡片、详情页和侧边栏中展示 GitHub stars、forks 等统计数据

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-30 14:14:02 +08:00
co-authored by Claude
parent 443dc8370e
commit cae5686d2d
12 changed files with 1766 additions and 41 deletions
+92
View File
@@ -0,0 +1,92 @@
/**
* GitHub API 服务
* 获取仓库的统计数据(stars, forks, issues, license 等)
*/
export interface GitHubStats {
stargazers_count: number
forks_count: number
open_issues_count: number
license: { key: string; name: string } | null
pushed_at: string
}
/**
* 从 GitHub API 获取仓库统计信息
* @param owner - 仓库所有者
* @param repo - 仓库名称
* @returns GitHub 统计数据或 null
*/
export async function getGitHubStats(
owner: string,
repo: string
): Promise<GitHubStats | null> {
try {
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
headers: {
Accept: 'application/vnd.github.v3+json',
// 如果需要更高的速率限制,可以添加 GitHub token
// Authorization: `token ${process.env.GITHUB_TOKEN}`,
},
next: { revalidate: 300 } // 缓存 5 分钟
})
if (!response.ok) {
console.error(`GitHub API error: ${response.status}`)
return null
}
const data = await response.json()
return {
stargazers_count: data.stargazers_count || 0,
forks_count: data.forks_count || 0,
open_issues_count: data.open_issues_count || 0,
license: data.license || null,
pushed_at: data.pushed_at || ''
}
} catch (error) {
console.error('Failed to fetch GitHub stats:', error)
return null
}
}
/**
* 格式化数字(如 142000 -> 142k
*/
export function formatNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'
}
if (num >= 1000) {
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'
}
return num.toString()
}
/**
* 格式化相对时间(如 "2 days ago"
*/
export function formatRelativeTime(dateString: string, locale: string = 'zh'): string {
if (!dateString) return ''
const date = new Date(dateString)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (locale === 'en') {
if (diffDays === 0) return 'today'
if (diffDays === 1) return 'yesterday'
if (diffDays < 7) return `${diffDays} days ago`
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`
return `${Math.floor(diffDays / 365)} years ago`
} else {
if (diffDays === 0) return '今天'
if (diffDays === 1) return '昨天'
if (diffDays < 7) return `${diffDays} 天前`
if (diffDays < 30) return `${Math.floor(diffDays / 7)} 周前`
if (diffDays < 365) return `${Math.floor(diffDays / 30)} 月前`
return `${Math.floor(diffDays / 365)} 年前`
}
}
+140
View File
@@ -0,0 +1,140 @@
import { ExternalLink, LinkType } from '@prisma/client'
/**
* 从 GitHub URL 提取 owner 和 repo
* @param url - GitHub 仓库 URL
* @returns owner 和 repo,或 null 如果无法解析
*/
export function parseGitHubUrl(url: string): { owner: string; repo: string } | null {
const patterns = [
/github\.com\/([^\/]+)\/([^\/\?#]+)/,
/github\.com\/([^\/]+)\/([^\/\?#]+)\.git/
]
for (const pattern of patterns) {
const match = url.match(pattern)
if (match) {
const repo = match[2].replace(/\.git$/, '').replace(/\/$/, '')
return { owner: match[1], repo }
}
}
return null
}
/**
* 生成 GitHub Stars 徽章 URL
* @param owner - 仓库所有者
* @param repo - 仓库名称
* @returns Shields.io 徽章 URL
*/
export function getGitHubStarsBadgeUrl(owner: string, repo: string): string {
return `https://img.shields.io/github/stars/${owner}/${repo}?style=social`
}
/**
* 生成所有 GitHub 徽章 URL
* @param owner - 仓库所有者
* @param repo - 仓库名称
* @returns 包含所有徽章 URL 的对象
*/
export function getAllGitHubBadgeUrls(owner: string, repo: string): {
stars: string
forks: string
issues: string
license: string
lastCommit: string
} {
return {
stars: `https://img.shields.io/github/stars/${owner}/${repo}?style=social`,
forks: `https://img.shields.io/github/forks/${owner}/${repo}?style=social`,
issues: `https://img.shields.io/github/issues/${owner}/${repo}`,
license: `https://img.shields.io/github/license/${owner}/${repo}`,
lastCommit: `https://img.shields.io/github/last-commit/${owner}/${repo}`
}
}
/**
* 从项目链接中提取 GitHub Stars 徽章 URL
* @param links - 项目外部链接数组
* @returns stars 徽章 URL 或 null
*/
export function getGitHubBadgesFromLinks(
links: ExternalLink[]
): { stars: string | null } {
const githubLink = links.find(link => link.type === 'GITHUB')
if (!githubLink) {
return { stars: null }
}
const parsed = parseGitHubUrl(githubLink.url)
if (!parsed) {
return { stars: null }
}
return {
stars: getGitHubStarsBadgeUrl(parsed.owner, parsed.repo)
}
}
/**
* 从项目链接中提取 GitHub 信息(用于文本显示)
* @param links - 项目外部链接数组
* @returns GitHub 统计数据对象
*/
export function getGitHubInfoFromLinks(
links: Array<{ type: string; url: string }>
): {
owner: string | null
repo: string | null
stars: string | null
forks: string | null
issues: string | null
license: string | null
lastCommit: string | null
} {
const githubLink = links.find(link => link.type === 'GITHUB')
if (!githubLink) {
return {
owner: null,
repo: null,
stars: null,
forks: null,
issues: null,
license: null,
lastCommit: null
}
}
const parsed = parseGitHubUrl(githubLink.url)
if (!parsed) {
return {
owner: null,
repo: null,
stars: null,
forks: null,
issues: null,
license: null,
lastCommit: null
}
}
// 返回徽章 URL,组件中可以使用这些 URL 获取实际数据
// 或者通过 GitHub API 获取实际数字
const badgeUrls = getAllGitHubBadgeUrls(parsed.owner, parsed.repo)
return {
owner: parsed.owner,
repo: parsed.repo,
// 暂时返回徽章 URL,后续可以通过 API 获取实际数字
stars: badgeUrls.stars,
forks: badgeUrls.forks,
issues: badgeUrls.issues,
license: badgeUrls.license,
lastCommit: badgeUrls.lastCommit
}
}