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:
@@ -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)} 年前`
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user