diff --git a/README.md b/README.md index 82ae52e..09f2ea8 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,20 @@ -## TODO +【WIP】:Website not yet ready -- The picture is not completely loaded +You can generate as many repository contributor avatar group photos as you want. -## How use +![Home](public//home.png) -1. Pull request your repo info to [`config.ts`](/config.ts) -2. Waiting for us to merge your PR. -3. Add svg path to your repo `README.md`: - - format: `https://raw.githubusercontent.com/veaba/contributors/main/repos/{owner}/{repo}.svg` - - try it: `![](https://raw.githubusercontent.com/veaba/contributors/main/repos/vuejs-translations/docs-zh-cn.svg)` +## TODO How use -```markdown +### Github Actions -![docs-zh-cn.svg](https://raw.githubusercontent.com/veaba/contributors/main/repos/vuejs-translations/docs-zh-cn.svg) +### Website -``` - -## Design +## Design - [x] Automatic clipping and rounding of Avatar - [x] Horizontal Auto Center - ![auto-center.svg](docs/default/auto-center.svg) + ![auto-center.svg](docs/default/auto-center.svg) ### size @@ -51,22 +45,22 @@ export default { } ``` - ### circle -- `circle`: default: circle +- `circle`: default: circle ![](docs/circle/circle-default.svg) - `isRadius:false`: you can disabled the feature -```diff +```diff export default { + 'vuejs-translations/docs-zh-cn': { + isRadius: false, + } } ``` + ![](docs/circle/no-circle.svg) ### margin @@ -75,7 +69,7 @@ export default { - [] configure text color? (It't necessary?) -### config +### config maybe you need filter some users. @@ -93,12 +87,11 @@ export default { ## Examples - ### demovuejs-translations/docs-zh-cn contributors in `config.js`: -```diff +```diff + const config: ConfigItem = + { + // https://github.com/vuejs-translations/docs-zh-cn diff --git a/auto-imports.d.ts b/auto-imports.d.ts index a51b7a6..08908ed 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -1,5 +1,5 @@ // Generated by 'unplugin-auto-import' export {} declare global { - const ElNotification: typeof import('element-plus/es')['ElNotification'] + } diff --git a/components.d.ts b/components.d.ts index f7cd2b0..ef7e54d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -23,6 +23,7 @@ declare module '@vue/runtime-core' { ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElResult: typeof import('element-plus/es')['ElResult'] ElSapce: typeof import('element-plus/es')['ElSapce'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSpace: typeof import('element-plus/es')['ElSpace'] diff --git a/public/home.png b/public/home.png new file mode 100644 index 0000000..202b94a Binary files /dev/null and b/public/home.png differ diff --git a/repos/vuejs-translations/docs-zh-cn.svg b/repos/vuejs-translations/docs-zh-cn.svg index 55971f3..1f98121 100644 --- a/repos/vuejs-translations/docs-zh-cn.svg +++ b/repos/vuejs-translations/docs-zh-cn.svg @@ -1,772 +1,547 @@ - + + + + + + + + + + + + + - + + + + + - + - + - + - + + + + + + + + + + + + + + + + + - + + + + + - + - + - - + + + + + + - + + + + + - + - + - + - + - + - + - + - - + + - - + + - + - + - + - + - - + + - + - - + + - - + + - + - + - - + + - - + + - + - - + + - - + + - - + + - - + + - + - + - - - - - + - + - + - - + + - + - - - - - - + + - + - - + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - ShenQingchuan - - - - - phanan - - - - - Jinjiang + + + yyx990803 - - - wxsms + + + NataliaTepluhina - - - jCodeLife + + + skirtles-code - - - veaba + + + ShenQingchuan - - - Matrix53 + + + bencodezen - - - KimYangOfCat + + + Jinjiang - - - pakchoily + + + phanan - - - Yorksh1re + + + wxsms - - - pikax + + + jCodeLife - - - rahaug + + + Alex-Sokolov - - - pieer + + + sdras - - - kiaking + + + KiritaniAyaka - - - xfq + + + dependabot[bot] - - - peterhauke + + + veaba - - - kazupon + + + marina-mosti - - - tanquar + + + Matrix53 - - - JessicaSachs + + + KimYangOfCat - - - znck + + + CyberAP - - - byog + + + LinusBorg - - - ghostcode + + + Yorksh1re - - - visualfanatic + + + pakchoily - - - edimitchel + + + pieer - - - posva + + + pikax - - - watonyweng + + + rahaug - - - SkyeYoung + + + kiaking - - - Rolanddoda + + + xfq - - - Justineo + + + peterhauke - - - wleven + + + tanquar - - - Xenonym + + + JessicaSachs - - - zhanglolo + + + kazupon - - - sharonytlau + + + znck - - - ota-meshi + + + byog - - - ajycc20 + + + edimitchel - - - MarvinXu + + + ghostcode - - - Amour1688 + + + posva - - - ch1lam + + + visualfanatic - - - unbyte + + + antfu - - - Timibadass + + + Rolanddoda - - - kawamataryo + + + SkyeYoung - - - donggua-nor + + + watonyweng - - - wenfangdu + + + MarvinXu - - - ferry77 + + + sharonytlau - - - likev + + + Xenonym - - - Gregg + + + wleven - - - jasmin92 + + + Justineo - - - Husky-Yellow + + + kawamataryo - - - connectkushal + + + zhanglolo - - - tomek-ch + + + ota-meshi - - - jillztom + + + ajycc20 - - - web-wgf + + + Amour1688 - - - bigyifeng + + + Timibadass - - - NoelDeMartin + + + unbyte - - - anilsahindev + + + danielkellyio - - - itsyuxuan + + + wenfangdu - - jonksc - - - - - docyx + + jonksc - - - btea - - - - - earthaYan - - - - - abinea - - - - - jeff-fe - - - - - cnryb - - - - - web2033 - - - - - thedamon - - - - - aouos - - - - - novrain - - - - - bambalamm - - - - - zhenzhenChange - - - - - kidonng - - - - - ErickPetru - - - - - fimion - - - - - benben6515 - - - - - dev-itsheng - - - - - wangyan666 - - - - - initdc - - - - - liulinboyi - - - - - demahom18 - - - - - webxmsj - - - - - changshou83 - - - - - ImCa0 + + + likev - - - AXVin + + + docyx - - - nicodevs + + + jasmin92 - - - dammy001 + + + connectkushal - - - antfu + + + tomek-ch diff --git a/src/components/ASider.vue b/src/components/ASider.vue index 42cb774..13f8275 100644 --- a/src/components/ASider.vue +++ b/src/components/ASider.vue @@ -12,8 +12,6 @@ const {defaultRepo, defaultRepoConfig, setDefaultRepo} = inject('defaultRepo') const onClickCard = (key: string, item: UserConfig) => { console.log('item>', key, item) setDefaultRepo(key, item) - - } const codeToString = (codeItem: UserConfig) => { return JSON.stringify(codeItem, null, 2) diff --git a/src/components/DisplaySVG.vue b/src/components/DisplaySVG.vue index c98052d..3b16a2c 100644 --- a/src/components/DisplaySVG.vue +++ b/src/components/DisplaySVG.vue @@ -2,33 +2,38 @@ /** * @TODO editable the user input code? * */ -import {computed,readonly, inject, ref,toRaw} from 'vue' +import {computed, inject, ref, toRaw} from 'vue' import {sortBy} from 'lodash' import {Download} from '@element-plus/icons-vue' -import {getOwnerRepo, getTotalList, axiosGet} from '@/utils' +import {axiosGet, getOwnerRepo, getTotalList} from '@/utils' import {mockData} from '../mock' import docsZhCN from '../../repos/vuejs-translations/docs-zh-cn.svg' -import {UserConfig, UserItem} from "@/types"; -import {generateUserListSVG} from "@/core/app"; +import {GithubContributorItem, UserConfig, UserItem} from "@/types"; +import {generateUserListSVG} from "@/core/generate"; +// import {loadImage} from "@/core/svg"; -const {defaultRepo, defaultRepoConfig, setDefaultRepo} = inject('defaultRepo') +const {defaultRepo, defaultRepoConfig} = inject('defaultRepo') const defaultSort = sortBy(mockData, (o) => -o.total) || [] const sortList = getTotalList(defaultSort, defaultRepoConfig) /* ******************** ref ******************* */ -const svgData = ref(docsZhCN) +const searchRepo = ref(defaultRepo.value) +const svgData = ref(docsZhCN as string) const loading = ref(false) const originData = ref(sortList || []) +const contributorList = ref([] as GithubContributorItem[]) // from api not sort +const isNoFountSearchRepo = ref(false) // repo is 404 +const isNoContributors = ref(false) // if the repo no contributors /* ******************** computed ************** */ const isRepo = computed(() => { - const {owner, repo} = getOwnerRepo(defaultRepo.value) + const {owner, repo} = getOwnerRepo(searchRepo.value) return !!(owner && repo); }) /* ******************** function ************** */ const onSearch = () => { - const {owner, repo} = getOwnerRepo(defaultRepo.value) + const {owner, repo} = getOwnerRepo(searchRepo.value) if (!owner || !repo) { ElNotification({ title: 'Please check it', @@ -40,8 +45,14 @@ const onSearch = () => { getGithubContributors(`${owner}/${repo}`) } -const onChange = (t: string) => { - console.info('t=>', t); +// TODO 有些迟滞 +const onChange = (v: string) => { + if (v !== defaultRepo.value) { + svgData.value = '' + } else if (!v) { + isNoContributors.value = false + isNoFountSearchRepo.value = false + } } const downloadSVG = () => { @@ -49,48 +60,55 @@ const downloadSVG = () => { let vNode = document.createDocumentFragment(); const linkNode = document.createElement('a') vNode.appendChild(linkNode) - linkNode.innerHTML = defaultRepo.value + '.svg'; + linkNode.innerHTML = searchRepo.value + '.svg'; linkNode.href = window.URL.createObjectURL(blob); - linkNode.download = defaultRepo.value + '.svg'; + linkNode.download = searchRepo.value + '.svg'; linkNode.click() } +const init = async () => { + loading.value = true + isNoFountSearchRepo.value = false + isNoContributors.value = false +} const getGithubContributors = async (repoKey: string) => { - // TODO - const testItem = { - "total": 6, - "author": "kiaking", - "avatar": "https://avatars.githubusercontent.com/u/3753672?v=4", - "id": 3753672 + // TODO test + // await generate(searchRepo.value, defaultRepoConfig.value, sortList.slice(0, 60))// .slice(0,20) + try { + await init() + const resp = await axiosGet(`http://api.github.com/repos/${repoKey}/stats/contributors`) + if (Array.isArray(resp)) { + isNoFountSearchRepo.value = false + isNoContributors.value = false + contributorList.value = resp + const sortTotalList = sortBy(resp, (o) => -o.total); + originData.value = sortTotalList + const cleanData = getTotalList(sortTotalList, defaultRepoConfig) + await generate(searchRepo, defaultRepoConfig, cleanData) + + } else { + // if resp = {} + contributorList.value = [] + originData.value = [] + svgData.value = '' + isNoFountSearchRepo.value = false + isNoContributors.value = true + + } + } catch (err) { + console.error('get repo stats contributors err=>', err) + // Request failed with status code 404 + isNoContributors.value = true + isNoFountSearchRepo.value = true + + } finally { + loading.value = false } - - await generate(defaultRepo.value, defaultRepoConfig.value, sortList)// .slice(0,20) - // try { - // loading.value = true - // const resp = await axiosGet(`http://api.github.com/repos/${repoKey}/stats/contributors`) - // if (Array.isArray(resp)) { - // // originData.value = sortBy(resp, (o) => o.total); TODO - // const cleanData = getTotalList(resp, defaultRepoConfig) - // console.info('cleanData=>', cleanData) - // await generate(defaultRepo, defaultRepoConfig, cleanData) - // } - // } catch (err) { - // console.error('get repo stats contributors err=>', err) - // } finally { - // loading.value = false - // } } const generate = async (repo: string, userConfig: UserConfig, contributors: UserItem[]) => { - console.time('generate time'); - // const tasks = await Promise.allSettled(contributors.map(async userItem => { - // return await loadImage(userItem) - // await saveSVG(repo, userConfig, contributors) - // })) - // TODO 比较 循环里有两个异步链快还是 两个循环里分别一个异步链 - const svgStr = await generateUserListSVG(contributors, userConfig) - svgData.value = svgStr + svgData.value = await generateUserListSVG(contributors, userConfig) console.timeEnd('generate time'); } @@ -99,7 +117,7 @@ const generate = async (repo: string, userConfig: UserConfig, contributors: User
- @@ -152,4 +183,13 @@ const generate = async (repo: string, userConfig: UserConfig, contributors: User text-align: center; margin: 50px; } + +.no-contributors { + width: 320px; + height: 320px; + margin: 0 auto; + background: #fff; + border-radius: 50%; + box-shadow: var(--el-box-shadow-dark); +} diff --git a/src/core/app.ts b/src/core/generate.ts similarity index 87% rename from src/core/app.ts rename to src/core/generate.ts index 02606d7..9a771be 100644 --- a/src/core/app.ts +++ b/src/core/generate.ts @@ -13,7 +13,6 @@ export const generateUserListSVG = async (userList: UserItem[], config: UserConf let splitList: UserItem[] | UserItem[][] = userList const svgWidth = config.width || SVG_WIDTH - // const svgHeight = config.height const baseSize = config.size || BASE_SIZE const fontSize = config.fontSize || FONT_SIZE @@ -24,11 +23,12 @@ export const generateUserListSVG = async (userList: UserItem[], config: UserConf // split new array if (userList.length > oneRowMax) { splitList = chunk(userList, oneRowMax) + } else { + splitList = chunk(userList, userList.length) } - // TODO => 計算不對 - console.info('splitList=>', splitList, splitList.length + 1, outSize + 5); - const svgHeight = (splitList.length + 1) * (outSize + 5) + // link <-> link = font-size + const svgHeight = splitList.length * outSize + ((splitList.length || 1) - 1) * fontSize const svgConfig = { baseSize, diff --git a/src/core/github.ts b/src/core/github.ts index a182a0d..5910134 100644 --- a/src/core/github.ts +++ b/src/core/github.ts @@ -1,11 +1,10 @@ import axios from "axios" - import { createWriteStream } from 'node:fs' import { resolve } from 'path' -import { UserConfig, UserItem } from "../types" +import { UserConfig, UserItem } from "@/types" import { sortBy } from 'lodash' import { data } from '../../tests/mock' -import { getTotalList, getOwnerRepo } from "../utils" +import { getTotalList, getOwnerRepo } from "@/utils" diff --git a/src/core/svg.ts b/src/core/svg.ts index 1cfe1fa..454c9f2 100644 --- a/src/core/svg.ts +++ b/src/core/svg.ts @@ -1,8 +1,8 @@ -import {ImageXYItem, SvgConfig, UserItem, XYItem} from "../types" -import {autoCenter, getImageX, getImageY, getTextX, getTextY} from "../utils" +import {ImageXYItem, SvgConfig, UserItem, XYItem} from "@/types" +import {autoCenter, getImageX, getImageY, getTextX, getTextY} from "@/utils" export const svgStart = (width: number, height: number) => { - return `` + return `` } export const svgEnd = () => `` @@ -17,11 +17,12 @@ export const svgNoFound = () => { * generate svg block string * @param {UserItem} userItem * @param {number} xyItem + * @param childrenLen * @param {number} svgConfig -*/ + */ export const svgBlockANode = async (userItem: UserItem, xyItem: XYItem, childrenLen: number, svgConfig: SvgConfig): Promise => { - const { xIndex, yIndex } = xyItem + const {xIndex, yIndex} = xyItem // image const imageX = getImageX(yIndex, svgConfig) + autoCenter(childrenLen, svgConfig) @@ -43,18 +44,9 @@ export const svgBlockANode = async (userItem: UserItem, xyItem: XYItem, children } -export const svgBlockImage = async (userItem: UserItem, xyItem: XYItem, childrenLen: number, svgConfig: SvgConfig): Promise => { - const { xIndex, yIndex } = xyItem - // image - const imageX = getImageX(yIndex, svgConfig) + autoCenter(childrenLen, svgConfig) - const imageY = getImageY(xIndex, svgConfig) - const imageXYItem = { imageX, imageY } - return await renderImageNode(imageXYItem, userItem, svgConfig) -} - // isRadius = false export const svgBlockCircle = async (userItem: UserItem, xyItem: XYItem, childrenLen: number, svgConfig: SvgConfig): Promise => { - const { xIndex, yIndex } = xyItem + const {xIndex, yIndex} = xyItem const imageX = getImageX(yIndex, svgConfig) + autoCenter(childrenLen, svgConfig) const imageY = getImageY(xIndex, svgConfig) @@ -65,7 +57,7 @@ export const svgBlockCircle = async (userItem: UserItem, xyItem: XYItem, childre ` } -const loadImage = async (userItem: UserItem) => { +export const loadImage = async (userItem: UserItem) => { try { const resp = await fetch(`https://avatars.githubusercontent.com/u/${userItem.id}?v=4`) const blob = await resp.blob() @@ -76,44 +68,42 @@ const loadImage = async (userItem: UserItem) => { }) } catch (err) { console.error('err=>', err) + return Promise.reject(err) } } // render svg image node const renderImageNode = async (imageXYItem: ImageXYItem, userItem: UserItem, svgConfig: SvgConfig) => { - const { imageX, imageY } = imageXYItem + const {imageX, imageY} = imageXYItem const imageHeight = svgConfig.baseSize const imageWidth = svgConfig.baseSize const base64Data = await loadImage(userItem) const clipPath = svgConfig.isRadius ? `clip-path="url(#${userItem.author})"` : '' - return ` ` + return `` } // render svg text node const renderTextNode = (childrenLen: number, xyItem: XYItem, userItem: UserItem, svgConfig: SvgConfig) => { - const { xIndex, yIndex } = xyItem + const {xIndex, yIndex} = xyItem const textX = getTextX(yIndex, svgConfig) + autoCenter(childrenLen, svgConfig); const textY = getTextY(xIndex, svgConfig) - return `${userItem.author}` + return `${userItem.author}` } // rectangle avatar export const asyncHandleUsersSVG = async (splitList: (UserItem | UserItem[])[], config: SvgConfig) => { - let userBlockData = '' - await Promise.all(splitList.map(async (item, xIndex) => { + let userLinkPositionData = '' + await Promise.all(splitList.map(async (item, index) => { if (Array.isArray(item)) { - await Promise.all(item.map(async (child, yIndex) => { - const childBlock = await svgBlockANode(child, { xIndex, yIndex }, item.length, config) - userBlockData += childBlock - }) + return await Promise.all(item.map(async (child, yIndex) => { + const childBlock = await svgBlockANode(child, {xIndex: index, yIndex}, item.length, config) + userLinkPositionData += childBlock + }) ) - } else { - const currentBlock = await svgBlockANode(item, { xIndex: 0, yIndex: xIndex }, splitList.length, config) - userBlockData += currentBlock } })) - return userBlockData + return userLinkPositionData } @@ -122,14 +112,11 @@ export const asyncHandlerUserDefsSVG = async (splitList: (UserItem | UserItem[]) let userBlockData = '' await Promise.all(splitList.map(async (item, xIndex) => { if (Array.isArray(item)) { - await Promise.all(item.map(async (child, yIndex) => { - const childBlock = await svgBlockCircle(child, { xIndex, yIndex }, item.length, config) - userBlockData += childBlock - }) + return await Promise.all(item.map(async (child, yIndex) => { + const childBlock = await svgBlockCircle(child, {xIndex, yIndex}, item.length, config) + userBlockData += childBlock + }) ) - } else { - const currentBlock = await svgBlockCircle(item, { xIndex: 0, yIndex: xIndex }, splitList.length, config) - userBlockData += currentBlock } })) return userBlockData diff --git a/src/types.d.ts b/src/types.d.ts index 998e7a5..b3e1f8b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -6,7 +6,7 @@ export interface UserItem { } export interface UserConfig { - ignore: Array // github login user + ignore: Array // GitHub login user height: number width: number fontSize: number @@ -83,4 +83,4 @@ export interface GithubContributorItem { export interface MD5Item { filename: string md5: string -} \ No newline at end of file +} diff --git a/tests/basic.test.ts b/tests/basic.test.ts index d03d505..f9ab3ac 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -1,5 +1,7 @@ -import { assert, expect, test } from 'vitest' -import { getOwnerRepo } from '../src/utils' +import {assert, expect, test} from 'vitest' +import {getOwnerRepo} from '../src/utils' +import {chunk} from 'lodash' +import {listTen} from './mock' // Edit an assertion and save to see HMR in action @@ -22,9 +24,14 @@ test('JSON', () => { }) test('veaba/contributors is a repo', () => { - const { owner, repo } = getOwnerRepo('veaba/contributors') - expect({ owner, repo }).toEqual({ + const {owner, repo} = getOwnerRepo('veaba/contributors') + expect({owner, repo}).toEqual({ owner: 'veaba', repo: 'contributors' }) }) + +test('split one row', () => { + const ret = chunk(listTen, listTen.length) + expect(ret).toEqual([listTen]) +})