diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 08908ed..a51b7a6 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 906442f..f7cd2b0 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,14 +7,29 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { + ASider: typeof import('./src/components/ASider.vue')['default'] Config: typeof import('./src/components/Config.vue')['default'] DisplaySVG: typeof import('./src/components/DisplaySVG.vue')['default'] + ElAlert: typeof import('element-plus/es')['ElAlert'] + ElAside: typeof import('element-plus/es')['ElAside'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElFooter: typeof import('element-plus/es')['ElFooter'] + ElHeader: typeof import('element-plus/es')['ElHeader'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElLink: typeof import('element-plus/es')['ElLink'] + ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] - ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElSapce: typeof import('element-plus/es')['ElSapce'] + ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSpace: typeof import('element-plus/es')['ElSpace'] Footer: typeof import('./src/components/Footer.vue')['default'] } export interface ComponentCustomProperties { + vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll'] vLoading: typeof import('element-plus/es')['ElLoadingDirective'] } } diff --git a/config.ts b/config.ts index 862a54d..dc63097 100644 --- a/config.ts +++ b/config.ts @@ -1,8 +1,8 @@ -import { ConfigItem } from "./src/types" +import { ConfigItem } from "@/types" /** * Github user custom contributors svg here - * ignore: {array} if you need ignore some users + * ignore: {array} if you need ignore some users. * height: svg height * width: svg width * fontSize: svg login name width @@ -33,7 +33,7 @@ const config: ConfigItem = 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'danielkellyio', 'tylermercer', - ], // if you need ignore some users + ], // if you need ignore some users. // users: [],?? maybe we need this field. ignoreTotal: 1, // 过滤低于这个 total 的用户 size: 100, @@ -48,7 +48,7 @@ const config: ConfigItem = 'yyx990803', 'NataliaTepluhina', 'skirtles-code', 'bencodezen', 'dependabot[bot]', 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'danielkellyio', 'tylermercer', - ], // if you need ignore some users + ], // if you need ignore some users.. // users: [],?? maybe we need this field. ignoreTotal: 1, // 过滤低于这个 total 的用户 size: 100, @@ -64,7 +64,7 @@ const config: ConfigItem = 'yyx990803', 'NataliaTepluhina', 'skirtles-code', 'bencodezen', 'dependabot[bot]', 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'danielkellyio', 'tylermercer', - ], // if you need ignore some users + ], // if you need ignore some users. // users: [],?? maybe we need this field. ignoreTotal: 1, // 过滤低于这个 total 的用户 size: 100, @@ -73,14 +73,14 @@ const config: ConfigItem = fontSize: 30, // isRadius: false, }, - + // https://github.com/vuejs-translations/docs-zh-cn 'vuejs-translations/veaba2': { ignore: [ 'yyx990803', 'NataliaTepluhina', 'skirtles-code', 'bencodezen', 'dependabot[bot]', 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'danielkellyio', 'tylermercer', - ], // if you need ignore some users + ], // if you need ignore some users. // users: [],?? maybe we need this field. ignoreTotal: 1, // 过滤低于这个 total 的用户 size: 100, @@ -89,14 +89,14 @@ const config: ConfigItem = fontSize: 30, // isRadius: false, }, - + // https://github.com/vuejs-translations/docs-zh-cn 'vuejs-translations/veaba3': { ignore: [ 'yyx990803', 'NataliaTepluhina', 'skirtles-code', 'bencodezen', 'dependabot[bot]', 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'danielkellyio', 'tylermercer', - ], // if you need ignore some users + ], // if you need ignore some users. // users: [],?? maybe we need this field. ignoreTotal: 1, // 过滤低于这个 total 的用户 size: 100, @@ -105,14 +105,14 @@ const config: ConfigItem = fontSize: 30, // isRadius: false, }, - + // https://github.com/vuejs-translations/docs-zh-cn 'vuejs-translations/veaba4': { ignore: [ 'yyx990803', 'NataliaTepluhina', 'skirtles-code', 'bencodezen', 'dependabot[bot]', 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'danielkellyio', 'tylermercer', - ], // if you need ignore some users + ], // if you need ignore some users. // users: [],?? maybe we need this field. ignoreTotal: 1, // 过滤低于这个 total 的用户 size: 100, @@ -121,14 +121,14 @@ const config: ConfigItem = fontSize: 30, // isRadius: false, }, - + // https://github.com/vuejs-translations/docs-zh-cn 'vuejs-translations/veaba5': { ignore: [ 'yyx990803', 'NataliaTepluhina', 'skirtles-code', 'bencodezen', 'dependabot[bot]', 'LinusBorg', 'KiritaniAyaka', 'Alex-Sokolov', 'sdras', 'marina-mosti', 'CyberAP', 'danielkellyio', 'tylermercer', - ], // if you need ignore some users + ], // if you need ignore some users. // users: [],?? maybe we need this field. ignoreTotal: 1, // 过滤低于这个 total 的用户 size: 100, diff --git a/index.html b/index.html index eabb007..d5d5bfe 100644 --- a/index.html +++ b/index.html @@ -1,17 +1,22 @@ - + - - - - A automatic generate Github Repository contributor group photo tool + + + + A automatic generate Github Repository contributor group photo tool + -
- +
+ - \ No newline at end of file + diff --git a/src/App.vue b/src/App.vue index d391b0c..083f1bd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,11 +6,18 @@ import Home from './pages/Home.vue' - diff --git a/src/actions/README.md b/src/actions/README.md new file mode 100644 index 0000000..331b6a6 --- /dev/null +++ b/src/actions/README.md @@ -0,0 +1,2 @@ +# A github-actions for generate github repo's contributors pictures + diff --git a/src/components/ASider.vue b/src/components/ASider.vue index 45a45b5..42cb774 100644 --- a/src/components/ASider.vue +++ b/src/components/ASider.vue @@ -1,30 +1,26 @@ - + diff --git a/src/components/Footer.vue b/src/components/Footer.vue index 623a6ec..d204497 100644 --- a/src/components/Footer.vue +++ b/src/components/Footer.vue @@ -1,7 +1,7 @@ - diff --git a/src/core/app.ts b/src/core/app.ts index 6e3fbdb..02606d7 100644 --- a/src/core/app.ts +++ b/src/core/app.ts @@ -1,34 +1,19 @@ - -// 1. get the repo's contributors list -// 2. generate svg -// 3. save to local -// 4. 根据仓库地址,会保存头像在本地,如果通过actions 触发后,通过对比,本地存在头像则不会再更新 -// 4.1 第4点中,bug:如果用户更新,则还是旧头像。出现用户头像,除非用头像生成 md5 存储作为指纹 -// import { http } from 'node:http' - -import { writeFile } from 'node:fs/promises'; -import { chunk } from 'lodash'; -import { svgStart, svgEnd, asyncHandleUsersSVG, asyncHandlerUserDefsSVG } from './svg'; -import { listTen } from '../../tests/mock' -import { UserConfig, UserItem } from '../types'; -import { getOwnerRepo } from '../utils'; - -import config from '../../config' -import console from 'node:console'; +import {chunk} from 'lodash'; +import {svgStart, svgEnd, asyncHandleUsersSVG, asyncHandlerUserDefsSVG} from './svg'; +import {UserConfig, UserItem} from '@/types'; // global constants const SVG_WIDTH = 800; -const SVG_HEIGHT = 370; -const FONT_SIZE = 0 +const FONT_SIZE = 20 const BASE_SIZE = 100 -const generateUserListSVG = async (userList: UserItem[], config: UserConfig) => { +export const generateUserListSVG = async (userList: UserItem[], config: UserConfig) => { - // split => two dimensional array + // split => two-dimensional array let splitList: UserItem[] | UserItem[][] = userList const svgWidth = config.width || SVG_WIDTH - const svgHeight = config.height || SVG_HEIGHT + // const svgHeight = config.height const baseSize = config.size || BASE_SIZE const fontSize = config.fontSize || FONT_SIZE @@ -41,6 +26,10 @@ const generateUserListSVG = async (userList: UserItem[], config: UserConfig) => splitList = chunk(userList, oneRowMax) } + // TODO => 計算不對 + console.info('splitList=>', splitList, splitList.length + 1, outSize + 5); + const svgHeight = (splitList.length + 1) * (outSize + 5) + const svgConfig = { baseSize, fontSize, @@ -52,7 +41,7 @@ const generateUserListSVG = async (userList: UserItem[], config: UserConfig) => } // radius - if (config.isRadius === undefined || config.isRadius === true) { + if (config.isRadius === undefined || config.isRadius) { return `${svgStart(svgWidth, svgHeight)} ${await asyncHandlerUserDefsSVG(splitList, svgConfig)} @@ -61,7 +50,7 @@ const generateUserListSVG = async (userList: UserItem[], config: UserConfig) => ${await asyncHandleUsersSVG(splitList, svgConfig)} ${svgEnd()} ` - } else if (config.isRadius === false) { + } else if (!config.isRadius) { const userBlockData = await asyncHandleUsersSVG(splitList, svgConfig) return `${svgStart(svgWidth, svgHeight)} ${userBlockData} @@ -70,26 +59,3 @@ ${svgEnd()} } return '' } - -export const saveSVG = async (ownerRepo: string, repoUserList: UserItem[]) => { - console.log('saveSVG=>', ownerRepo, repoUserList.length) - const { owner, repo } = getOwnerRepo && getOwnerRepo(ownerRepo) || {} - if (!owner || !repo) { - console.error('Invalid repo address:', ownerRepo) - return - } - - const userConfig: UserConfig = config[ownerRepo] - try { - // const controller = new AbortController(); - const svgStr = await generateUserListSVG(repoUserList, userConfig) - - const filename = `./repos/${owner}/${repo}.svg` - await writeFile(filename, svgStr, { encoding: 'utf-8' }) - } catch (error) { - console.error('err=>', error) - } -} - -// saveSVG('veaba/contributors',listTen) -// saveSVG('vuejs-translations/docs-zh-cn',listTen) diff --git a/src/core/circle.ts b/src/core/circle.ts deleted file mode 100644 index 2824975..0000000 --- a/src/core/circle.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @TODO -* - - - - - - - - - -*/ -export const getCircleX = () => { - -} - -export const getCircleY = () => { - -} \ No newline at end of file diff --git a/src/core/github.ts b/src/core/github.ts index 0432b5b..a182a0d 100644 --- a/src/core/github.ts +++ b/src/core/github.ts @@ -2,7 +2,7 @@ import axios from "axios" import { createWriteStream } from 'node:fs' import { resolve } from 'path' -import { MD5Item, UserConfig, UserItem } from "../types" +import { UserConfig, UserItem } from "../types" import { sortBy } from 'lodash' import { data } from '../../tests/mock' import { getTotalList, getOwnerRepo } from "../utils" diff --git a/src/core/image.ts b/src/core/image.ts deleted file mode 100644 index 7a33662..0000000 --- a/src/core/image.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { readFile } from 'node:fs/promises' -/** - * save image locally - * -*/ - -/** - * locally image to base64 - * -*/ -export const imagePathToBase64 = async (imagePath: string) => { - const imageBuffer = await readFile(imagePath) - return imageBuffer.toString('base64') -} \ No newline at end of file diff --git a/src/core/index.ts b/src/core/index.ts deleted file mode 100644 index 60bec58..0000000 --- a/src/core/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import config from '../../config' -import { resolve } from 'path' -import { saveSVG } from './app'; -import { writeFile, access } from "node:fs/promises" // access -import { downloadAvatar, getRepoData } from './github'; -import { MD5Item, UserItem } from '../types'; -// import { isHasFile, readMD5 } from "../utils" - -import md5JSON from '../../public/avatars/avatarsMD5.json' - -const md5s: any = md5JSON - -interface TypesContributors { - repo: string; -} - -const updateAvatars = async (sortList: UserItem[]) => { - await Promise.all(sortList.map(async userItem => { - - const isHas = await isHasFile(resolve(__dirname, `../public/avatars/${userItem.id}.jpg`)) - if (!isHas) { - return await downloadAvatar(userItem) - } - // const md5Item: MD5Item = md5s[userItem.id] || {} - // const filename = md5Item.filename || userItem.id + '.jpg' - // let { md5 } = md5Item - // TODO 开销较大,暂时移除 - // if (md5) { - // // const readAvatarMD5 = await readMD5(filename) - // // // 两个 md5 不相等,需要 download 文件,并更改 md5 的值 - // // if (readAvatarMD5 !== md5) { - // // md5 = readAvatarMD5 - // // await downloadAvatar(userItem) - // // md5Item.md5 = readAvatarMD5 - // // const buffer = Buffer.from(JSON.stringify(md5s)); - // // await writeFile(resolve(__dirname, `../public/avatars/avatarsMD5.json`), buffer, 'utf-8') - // // } - // return Promise.resolve() - // } else { - // md5s[userItem.id] = { - // filename: `${userItem.id}.jpg`, - // md5: await readMD5(filename) - // } - // await downloadAvatar(userItem) - // const buffer = Buffer.from(JSON.stringify(md5s)); - // await writeFile(resolve(__dirname, `../public/avatars/avatarsMD5.json`), buffer, 'utf-8') - // return Promise.resolve() - // } - })) -} - -export const serverStart = async () => { - const now = Date.now() - console.log('开始=>', new Date()) - - console.time('task=>') - const repos = Object.keys(config) - // const reposConfigs = Object.values(config) - - await Promise.all(repos.map(async repo => { - const repoConfig = config[repo] - const repoList = await getRepoData(repo, repoConfig) - await updateAvatars(repoList) - await saveSVG(repo, repoList) - })) - - console.timeEnd('task=>') - console.log('结束=>', new Date(), Date.now() - now) - - -} - -// serverStart() diff --git a/src/core/svg.ts b/src/core/svg.ts index ccb66b9..1cfe1fa 100644 --- a/src/core/svg.ts +++ b/src/core/svg.ts @@ -1,6 +1,5 @@ -import { imagePathToBase64 } from "./image" -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 `` @@ -31,17 +30,16 @@ export const svgBlockANode = async (userItem: UserItem, xyItem: XYItem, children // if BASE_SIZE too small,or font-size less than 20 // hidden svg > text node let SVGText = ''; - if (svgConfig.baseSize >= 50 && svgConfig.fontSize >= 20) { + if (svgConfig.baseSize <= 50 || svgConfig.fontSize <= 20) { SVGText = renderTextNode(childrenLen, xyItem, userItem, svgConfig) } - const SVGData = ` + return ` - ${await renderImageNode({ imageX, imageY }, userItem, svgConfig)} + ${await renderImageNode({imageX, imageY}, userItem, svgConfig)} ${SVGText} ` - return SVGData } @@ -67,16 +65,29 @@ export const svgBlockCircle = async (userItem: UserItem, xyItem: XYItem, childre ` } +const loadImage = async (userItem: UserItem) => { + try { + const resp = await fetch(`https://avatars.githubusercontent.com/u/${userItem.id}?v=4`) + const blob = await resp.blob() + return new Promise((resolve) => { + const reader = new FileReader() + reader.readAsDataURL(blob) + reader.onloadend = () => resolve(reader.result) + }) + } catch (err) { + console.error('err=>', err) + } +} + // render svg image node const renderImageNode = async (imageXYItem: ImageXYItem, userItem: UserItem, svgConfig: SvgConfig) => { const { imageX, imageY } = imageXYItem const imageHeight = svgConfig.baseSize const imageWidth = svgConfig.baseSize - const localImagePath = 'public/avatars/' + userItem.id + '.jpg' - const base64Image = await imagePathToBase64(localImagePath) - const imageBase64Data = 'data:image/PNG;base64,' + base64Image + + const base64Data = await loadImage(userItem) const clipPath = svgConfig.isRadius ? `clip-path="url(#${userItem.author})"` : '' - return ` ` + return ` ` } // render svg text node diff --git a/src/pages/Home.vue b/src/pages/Home.vue index 11fdd4c..a4b9453 100644 --- a/src/pages/Home.vue +++ b/src/pages/Home.vue @@ -1,31 +1,41 @@
Home - - - - Github + + + + + Github + - + - + @@ -42,4 +52,13 @@ const activeIndex = ref('1') .el-menu--horizontal { border-bottom: none; } - \ No newline at end of file + +.main { + padding: 0; +} + +.container { + background-image: -webkit-linear-gradient(120deg, #bd34fe 30%, #41d1ff); + filter: var(--vp-home-hero-image-filter); +} + diff --git a/src/utils.ts b/src/utils.ts index 426bff2..d94d4da 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,14 +1,14 @@ -import { GithubContributorItem, OwnerRepoItem, SvgConfig, UserConfig } from './types'; -import axios, { Axios, AxiosInstance, AxiosRequestConfig } from 'axios' +import {GithubContributorItem, OwnerRepoItem, SvgConfig, UserConfig} from './types'; +import axios from 'axios' /** * Automatic center filling * @param {number} childrenLen * @param {SvgConfig} svgConfig * -*/ + */ export const autoCenter = (childrenLen: number, svgConfig: SvgConfig) => { - const { svgWidth, outSize } = svgConfig + const {svgWidth, outSize} = svgConfig if (childrenLen * outSize < svgWidth) { return (svgWidth - outSize * childrenLen) / 2 } @@ -16,22 +16,22 @@ export const autoCenter = (childrenLen: number, svgConfig: SvgConfig) => { } export const getImageX = (yIndex: number, svgConfig: SvgConfig) => { - const { outSize } = svgConfig + const {outSize} = svgConfig return outSize * yIndex } export const getImageY = (xIndex: number, svgConfig: SvgConfig) => { - const { baseSize, fontSize } = svgConfig + const {baseSize, fontSize} = svgConfig return (baseSize + fontSize * 2) * xIndex } export const getTextX = (yIndex: number, svgConfig: SvgConfig) => { - const { baseSize, outSize } = svgConfig + const {baseSize, outSize} = svgConfig return (baseSize / 2) + outSize * yIndex } export const getTextY = (xIndex: number, svgConfig: SvgConfig) => { - const { outSize, fontSize } = svgConfig + const {outSize, fontSize} = svgConfig return (outSize) * (xIndex + 1) + (xIndex * fontSize) } @@ -40,25 +40,32 @@ export const getOwnerRepo = (ownerRepo: string): OwnerRepoItem => { const ownerRepoSplit = ownerRepo.split('/') if (ownerRepoSplit.length && ownerRepoSplit.length === 2) { const [owner = '', repo = ''] = ownerRepoSplit - return { owner, repo } + return {owner, repo} } - return { owner: '', repo: '' } + return {owner: '', repo: ''} } // get clean data for github api export const getTotalList = (data: GithubContributorItem[], repoConfig: UserConfig) => { const cleanList = [] + const {ignore = [], ignoreTotal = 0} = repoConfig || {} for (let i = 0; i < data.length; i++) { const item = data[i] // filter total && ignore - if (Number(repoConfig.ignoreTotal) <= item.total && !repoConfig.ignore.includes(item.author.login)) { + if (Number(ignoreTotal) <= item.total && !(ignore || []).includes(item.author.login)) { + cleanList.push({ + total: item.total, + author: item.author.login, + avatar: item.author.avatar_url, + id: item.author.id, + }) + } else if (!repoConfig.ignore.length) cleanList.push({ total: item.total, author: item.author.login, avatar: item.author.avatar_url, id: item.author.id, }) - } } return cleanList @@ -67,25 +74,6 @@ export const getTotalList = (data: GithubContributorItem[], repoConfig: UserConf const axiosIns = axios.create({}); -class AxiosFn extends Axios { - instance: AxiosInstance - constructor(config: AxiosRequestConfig) { - super(config) - this.instance = axios.create(config) - - this.instance.interceptors.response.use((res) => { - console.log('res==========>', res) - if (res && res.data) { - return Promise.resolve(res.data); - } - return res.data; - }, (err) => { - return Promise.reject(err.response) - }) - - } - -} export const axiosGet = async (...args: any) => { try { const res = await axiosIns.get(args);