1+ <template >
2+ <div class =" doc-contributors" >
3+ <div class =" contributors-header" >
4+ <h3 class =" contributors-title" >
5+ <svg class =" title-icon" viewBox =" 0 0 16 16" width =" 18" height =" 18" >
6+ <path fill =" currentColor" d =" M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
7+ </svg >
8+ 贡献者
9+ </h3 >
10+ </div >
11+
12+ <div class =" contributors-list" >
13+ <div
14+ v-for =" contributor in contributorsList"
15+ :key =" contributor.id"
16+ class =" contributor-wrapper"
17+ @click =" openProfile(contributor.html_url)"
18+ @mouseenter =" showTooltip = contributor.id"
19+ @mouseleave =" showTooltip = null"
20+ >
21+ <img
22+ :src =" contributor.avatar_url"
23+ :alt =" contributor.login"
24+ class =" contributor-avatar"
25+ @error =" handleImageError"
26+ />
27+ <div
28+ v-if =" showTooltip === contributor.id"
29+ class =" contributor-tooltip"
30+ >
31+ {{ contributor.login }}
32+ </div >
33+ </div >
34+
35+ <!-- 加载状态 -->
36+ <div v-if =" loading" class =" loading-contributors" >
37+ <div class =" loading-text" >
38+ 正在加载贡献者信息...
39+ </div >
40+ </div >
41+
42+ <!-- 如果没有找到贡献者,显示默认信息 -->
43+ <div v-else-if =" contributorsList.length === 0" class =" no-contributors" >
44+ <div class =" no-contributors-text" >
45+ 暂无贡献者信息
46+ </div >
47+ </div >
48+ </div >
49+ </div >
50+ </template >
51+
52+ <script setup lang="ts">
53+ import { computed , onMounted , ref } from ' vue'
54+
55+ interface Contributor {
56+ login: string
57+ id: number
58+ avatar_url: string
59+ html_url: string
60+ contributions: number
61+ type: string
62+ }
63+
64+ interface FileContributor {
65+ file: string
66+ contributors: Contributor []
67+ }
68+
69+ interface Props {
70+ componentName? : string
71+ maxCount? : number
72+ }
73+
74+ const props = withDefaults (defineProps <Props >(), {
75+ componentName: ' ' ,
76+ maxCount: 50
77+ })
78+
79+ // 加载实际的贡献者数据
80+ let contributorsData: any = null
81+
82+ // 动态导入贡献者数据
83+ const loadContributorsData = async () => {
84+ if (contributorsData ) return contributorsData
85+
86+ try {
87+ // 在客户端环境下动态导入
88+ if (typeof window !== ' undefined' ) {
89+ const module = await import (' ../../../../contributors/data.ts' )
90+ contributorsData = module .contributorsData
91+ return contributorsData
92+ } else {
93+ // 服务端渲染时返回空数据
94+ return {
95+ contributors: [],
96+ componentContributors: [],
97+ fileContributors: []
98+ }
99+ }
100+ } catch (error ) {
101+ console .warn (' Failed to load contributors data:' , error )
102+ // 返回模拟数据作为后备
103+ return {
104+ contributors: [
105+ {
106+ login: ' wzc520pyfm' ,
107+ id: 69044080 ,
108+ avatar_url: ' https://avatars.githubusercontent.com/u/69044080?v=4' ,
109+ html_url: ' https://github.com/wzc520pyfm' ,
110+ contributions: 127 ,
111+ type: ' User'
112+ }
113+ ],
114+ componentContributors: [],
115+ fileContributors: []
116+ }
117+ }
118+ }
119+
120+ // 响应式数据
121+ const allContributorsData = ref <any >(null )
122+ const loading = ref (true )
123+ const showTooltip = ref <number | null >(null )
124+
125+ // 过滤当前组件的贡献者
126+ const contributorsList = computed (() => {
127+ if (! allContributorsData .value || loading .value ) {
128+ return []
129+ }
130+
131+ if (! props .componentName ) {
132+ return allContributorsData .value .contributors ?.slice (0 , props .maxCount ) || []
133+ }
134+
135+ // 优先使用新的 componentContributors 结构
136+ if (allContributorsData .value .componentContributors ) {
137+ // 查找所有包含当前组件名的组件(包括完全匹配和包含关系)
138+ const matchingComponents = allContributorsData .value .componentContributors .filter ((comp : any ) => {
139+ const compName = comp .component .toLowerCase ()
140+ const targetName = props .componentName .toLowerCase ()
141+
142+ // 完全匹配或者组件名包含目标名称(如 prompts-docs 包含 prompts)
143+ return compName === targetName || compName .includes (targetName )
144+ })
145+
146+ if (matchingComponents .length > 0 ) {
147+ // 按贡献者 login 去重并合并贡献数
148+ const contributorsMap = new Map ()
149+
150+ matchingComponents .forEach ((componentData : any ) => {
151+ componentData .contributors .forEach ((contributor : Contributor ) => {
152+ const existing = contributorsMap .get (contributor .login )
153+ if (existing ) {
154+ // 累加贡献数
155+ existing .contributions += contributor .contributions
156+ } else {
157+ // 新增贡献者
158+ contributorsMap .set (contributor .login , { ... contributor })
159+ }
160+ })
161+ })
162+
163+ // 按贡献度排序并返回
164+ return Array .from (contributorsMap .values ())
165+ .sort ((a , b ) => b .contributions - a .contributions )
166+ .slice (0 , props .maxCount )
167+ }
168+ }
169+
170+ // 回退到 fileContributors 结构(向后兼容)
171+ if (allContributorsData .value .fileContributors ) {
172+ const componentFiles = allContributorsData .value .fileContributors .filter ((fc : FileContributor ) => {
173+ const fileName = fc .file .toLowerCase ()
174+ const componentName = props .componentName .toLowerCase ()
175+
176+ return fileName .includes (` src/${componentName }/ ` ) ||
177+ fileName .includes (` /${componentName }.vue ` ) ||
178+ fileName .includes (` /${componentName }header.vue ` ) ||
179+ fileName .endsWith (` ${componentName }/index.vue ` )
180+ })
181+
182+ // 合并所有相关文件的贡献者
183+ const contributorsMap = new Map ()
184+
185+ componentFiles .forEach ((fileContrib : FileContributor ) => {
186+ fileContrib .contributors .forEach ((contributor : Contributor ) => {
187+ const existing = contributorsMap .get (contributor .login )
188+ if (existing ) {
189+ existing .contributions += contributor .contributions
190+ } else {
191+ contributorsMap .set (contributor .login , { ... contributor })
192+ }
193+ })
194+ })
195+
196+ if (contributorsMap .size > 0 ) {
197+ return Array .from (contributorsMap .values ())
198+ .sort ((a , b ) => b .contributions - a .contributions )
199+ .slice (0 , props .maxCount )
200+ }
201+ }
202+
203+ // 最后回退到总体贡献者
204+ return allContributorsData .value .contributors ?.slice (0 , props .maxCount ) || []
205+ })
206+
207+ // 方法
208+ const openProfile = (url : string ) => {
209+ window .open (url , ' _blank' )
210+ }
211+
212+ const handleImageError = (event : Event ) => {
213+ const img = event .target as HTMLImageElement
214+ img .src = ' https://github.com/identicons/github.png'
215+ }
216+
217+ // 加载数据
218+ const loadData = async () => {
219+ try {
220+ loading .value = true
221+ const data = await loadContributorsData ()
222+ allContributorsData .value = data
223+ console .log (` Contributors data loaded for component: ${props .componentName } ` , data )
224+ } catch (error ) {
225+ console .error (' Failed to load contributors data:' , error )
226+ } finally {
227+ loading .value = false
228+ }
229+ }
230+
231+ onMounted (async () => {
232+ console .log (` DocContributors mounted for component: ${props .componentName } ` )
233+ await loadData ()
234+ })
235+ </script >
236+
237+ <style scoped>
238+ .doc-contributors {
239+ margin-top : 48px ;
240+ padding-top : 24px ;
241+ border-top : 1px solid var (--vp-c-divider );
242+ }
243+
244+ .contributors-header {
245+ display : flex ;
246+ align-items : center ;
247+ justify-content : space-between ;
248+ margin-bottom : 16px ;
249+ }
250+
251+ .contributors-title {
252+ display : flex ;
253+ align-items : center ;
254+ margin : 0 ;
255+ font-size : 18px ;
256+ font-weight : 600 ;
257+ color : var (--vp-c-text-1 );
258+ }
259+
260+ .title-icon {
261+ margin-right : 8px ;
262+ color : var (--vp-c-brand );
263+ flex-shrink : 0 ;
264+ }
265+
266+ .contributors-list {
267+ display : flex ;
268+ flex-wrap : wrap ;
269+ gap : 6px ; /* Reduced gap */
270+ min-height : 60px ;
271+ align-items : flex-start ;
272+ }
273+
274+ .contributor-wrapper {
275+ position : relative ;
276+ cursor : pointer ;
277+ }
278+
279+ .contributor-wrapper :hover .contributor-avatar {
280+ border-color : var (--vp-c-brand );
281+ transform : translateY (-1px );
282+ }
283+
284+ .contributor-avatar {
285+ width : 32px ;
286+ height : 32px ;
287+ border-radius : 50% ;
288+ border : 2px solid var (--vp-c-bg );
289+ transition : all 0.2s ease ;
290+ flex-shrink : 0 ;
291+ display : block ;
292+ }
293+
294+ .contributor-tooltip {
295+ position : absolute ;
296+ top : -36px ;
297+ left : 50% ;
298+ transform : translateX (-50% );
299+ background-color : var (--vp-c-bg-alt );
300+ color : var (--vp-c-text-1 );
301+ padding : 6px 10px ;
302+ border-radius : 6px ;
303+ font-size : 13px ;
304+ font-weight : 500 ;
305+ white-space : nowrap ;
306+ z-index : 1000 ;
307+ opacity : 1 ;
308+ box-shadow : 0 4px 12px rgba (0 , 0 , 0 , 0.15 );
309+ border : 1px solid var (--vp-c-divider );
310+ pointer-events : none ;
311+ animation : tooltip-fade-in 0.2s ease-out ;
312+ }
313+
314+ .contributor-tooltip ::after {
315+ content : ' ' ;
316+ position : absolute ;
317+ top : 100% ;
318+ left : 50% ;
319+ transform : translateX (-50% );
320+ border : 5px solid transparent ;
321+ border-top-color : var (--vp-c-bg-alt );
322+ }
323+
324+ @keyframes tooltip-fade-in {
325+ from {
326+ opacity : 0 ;
327+ transform : translateX (-50% ) translateY (-4px );
328+ }
329+ to {
330+ opacity : 1 ;
331+ transform : translateX (-50% ) translateY (0 );
332+ }
333+ }
334+
335+ .loading-contributors {
336+ display : flex ;
337+ align-items : center ;
338+ justify-content : center ;
339+ width : 100% ;
340+ padding : 20px ;
341+ color : var (--vp-c-text-2 );
342+ font-size : 14px ;
343+ }
344+
345+ .loading-text {
346+ opacity : 0.7 ;
347+ }
348+
349+ .no-contributors {
350+ display : flex ;
351+ align-items : center ;
352+ justify-content : center ;
353+ width : 100% ;
354+ padding : 20px ;
355+ color : var (--vp-c-text-2 );
356+ font-size : 14px ;
357+ }
358+
359+ .no-contributors-text {
360+ opacity : 0.7 ;
361+ }
362+
363+ @media (max-width : 768px ) {
364+ .contributors-header {
365+ flex-direction : column ;
366+ align-items : flex-start ;
367+ gap : 8px ;
368+ }
369+
370+ .contributors-list {
371+ justify-content : center ;
372+ }
373+
374+ .contributor-avatar {
375+ width : 28px ;
376+ height : 28px ;
377+ }
378+ }
379+ </style >
0 commit comments