Skip to content

Commit d00df1f

Browse files
committed
feat(项目显示星星数)
1 parent a99b034 commit d00df1f

File tree

1 file changed

+111
-1
lines changed

1 file changed

+111
-1
lines changed

docs/projects/ProjectPage.vue

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</header>
77

88
<section class="cards">
9-
<article v-for="p in items" :key="p.id" class="card">
9+
<article v-for="p in viewItems" :key="p.id" class="card">
1010
<a :href="p.url" class="image" target="_blank" rel="noopener noreferrer">
1111
<img
1212
:src="p.image || defaultImg"
@@ -18,6 +18,12 @@
1818
</a>
1919

2020
<div class="content">
21+
<div class="stats stats-top">
22+
<span class="stat" title="GitHub Stars">
23+
<span class="icon">⭐</span>
24+
{{ p.stars ?? '—' }}
25+
</span>
26+
</div>
2127
<h3 class="title">
2228
<a :href="p.url" target="_blank" rel="noopener noreferrer">{{ p.title }}</a>
2329
</h3>
@@ -45,9 +51,88 @@
4551
</template>
4652

4753
<script setup>
54+
import { ref, onMounted } from 'vue'
4855
import { projects, sortProjects } from './data.js'
4956
57+
// 排序后的项目数据
5058
const items = sortProjects(projects)
59+
// 展示用数据,附加 stars 字段
60+
const viewItems = ref(items.map(p => ({ ...p, stars: null })))
61+
62+
// 你的 GitHub 用户名(批量拉取该用户仓库)
63+
const GH_USER = 'cpython666'
64+
65+
// 从链接解析 GitHub 仓库 owner/repo
66+
const parseRepoFromUrl = (url) => {
67+
try {
68+
const u = new URL(url)
69+
if (u.hostname !== 'github.com') return null
70+
const segs = u.pathname.split('/').filter(Boolean)
71+
if (segs.length < 2) return null
72+
return { owner: segs[0], repo: segs[1] }
73+
} catch (_) {
74+
return null
75+
}
76+
}
77+
78+
// 缓存 stars,减少请求次数
79+
const userRepoStars = ref(new Map())
80+
const otherRepoStars = new Map()
81+
82+
async function fetchUserRepos() {
83+
try {
84+
const map = new Map()
85+
let page = 1
86+
const per = 100
87+
while (true) {
88+
const res = await fetch(`https://api.github.com/users/${GH_USER}/repos?per_page=${per}&page=${page}`)
89+
if (!res.ok) break
90+
const data = await res.json()
91+
if (!Array.isArray(data) || data.length === 0) break
92+
data.forEach(r => map.set(r.name.toLowerCase(), r.stargazers_count || 0))
93+
if (data.length < per) break
94+
page += 1
95+
if (page > 10) break // 安全上限,避免异常循环
96+
}
97+
userRepoStars.value = map
98+
} catch (_) {}
99+
}
100+
101+
async function fetchRepoStars(owner, repo) {
102+
const key = `${owner}/${repo}`.toLowerCase()
103+
if (otherRepoStars.has(key)) return otherRepoStars.get(key)
104+
try {
105+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`)
106+
if (!res.ok) { otherRepoStars.set(key, 0); return 0 }
107+
const j = await res.json()
108+
const stars = j.stargazers_count || 0
109+
otherRepoStars.set(key, stars)
110+
return stars
111+
} catch (_) {
112+
otherRepoStars.set(key, 0)
113+
return 0
114+
}
115+
}
116+
117+
async function updateStars() {
118+
for (const p of viewItems.value) {
119+
const info = parseRepoFromUrl(p.url)
120+
if (!info) continue
121+
const { owner, repo } = info
122+
let stars = 0
123+
if (owner.toLowerCase() === GH_USER.toLowerCase()) {
124+
stars = userRepoStars.value.get(repo.toLowerCase()) ?? 0
125+
} else {
126+
stars = await fetchRepoStars(owner, repo)
127+
}
128+
p.stars = stars
129+
}
130+
}
131+
132+
onMounted(async () => {
133+
await fetchUserRepos()
134+
await updateStars()
135+
})
51136
52137
const defaultImg = '/imgs/app/default.svg'
53138
const onImgErr = (e) => {
@@ -126,6 +211,7 @@ const onImgErr = (e) => {
126211
flex-direction: column;
127212
gap: 8px;
128213
padding: 14px 16px 16px;
214+
position: relative;
129215
}
130216
131217
.title {
@@ -186,6 +272,30 @@ const onImgErr = (e) => {
186272
margin-top: auto;
187273
}
188274
275+
.stats {
276+
display: flex;
277+
gap: 8px;
278+
}
279+
.stat {
280+
display: inline-flex;
281+
align-items: center;
282+
gap: 4px;
283+
font-size: 12px;
284+
line-height: 1;
285+
padding: 6px 8px;
286+
border-radius: 999px;
287+
border: 1px solid var(--vp-c-divider);
288+
color: var(--vp-c-text-2);
289+
background: var(--vp-c-bg-soft);
290+
}
291+
.stat .icon { font-size: 14px; }
292+
293+
.stats-top {
294+
position: absolute;
295+
top: 14px;
296+
right: 16px;
297+
}
298+
189299
.btn {
190300
display: inline-block;
191301
text-decoration: none;

0 commit comments

Comments
 (0)