From f14faf9d10d34a2aa401bb66563ae01c8b296c81 Mon Sep 17 00:00:00 2001 From: MrH233 <1658721519@qq.com> Date: Wed, 24 Apr 2024 21:01:26 +0800 Subject: [PATCH] =?UTF-8?q?#808=20task=20feat(contributor-button):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=95=B0=E6=8D=AE=E6=9C=AA=E6=89=BE=E5=88=B0?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=B9=B6=E5=88=9D=E5=A7=8B=E5=8C=96React?= =?UTF-8?q?=E6=A8=A1=E5=9D=97-=20=E5=88=9B=E5=BB=BADataNotFound=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=9C=A8=E6=9C=AA=E6=89=BE?= =?UTF-8?q?=E5=88=B0=E6=95=B0=E6=8D=AE=E6=97=B6=E5=B1=95=E7=A4=BA=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E7=9A=84=E5=8E=9F=E5=9B=A0=E3=80=82-=20=E5=9C=A8index?= =?UTF-8?q?.ts=E4=B8=AD=E5=BC=95=E5=85=A5contributor=5Fbutton=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=85=B6=E5=9C=A8=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=8A=A0=E8=BD=BD=E6=97=B6=E8=A2=AB=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E3=80=82=20-=20=E5=88=9D=E5=A7=8B=E5=8C=96contributor?= =?UTF-8?q?=5Fbutton=E6=A8=A1=E5=9D=97=E7=9A=84React=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E6=8B=AC=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=80=81=E6=95=B0=E6=8D=AE=E8=8E=B7=E5=8F=96=E5=92=8C=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91=E3=80=82=20-=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BF=85=E8=A6=81=E7=9A=84=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=92=8C=E5=85=A8=E5=B1=80=E5=8F=98=E9=87=8F?= =?UTF-8?q?=EF=BC=8C=E4=BB=A5=E6=94=AF=E6=8C=81=E8=B4=A1=E7=8C=AE=E8=80=85?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=8A=9F=E8=83=BD=E7=9A=84=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E3=80=82=20-=20=E9=80=9A=E8=BF=87ReactModal?= =?UTF-8?q?=E5=92=8CGraph=E7=BB=84=E4=BB=B6=E5=A2=9E=E5=BC=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=95=8C=E9=9D=A2=EF=BC=8C=E6=8F=90=E4=BE=9B=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E8=A7=86=E5=9B=BE=E7=9A=84=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在贡献者提供了贡献者网络视图 Active Developer Collaboration Network,以及切换按钮,通过点击可以切换贡献者展示视图.增强了对GitHub仓库页面中贡献者列表的展示功能,提供了更丰富的交互体验。 --- src/api/repo.ts | 5 + src/locales/en/messages.json | 3 + src/locales/zh_CN/messages.json | 3 + .../contributor_button/DataNotFound.tsx | 26 +++ .../features/contributor_button/index.tsx | 78 +++++++++ .../features/contributor_button/view.tsx | 64 +++++++ .../repo-header-labels/ContributorChart.tsx | 157 ++++++++++++++++++ .../features/repo-header-labels/index.tsx | 10 +- .../features/repo-header-labels/view.tsx | 22 ++- src/pages/ContentScripts/index.ts | 2 +- 10 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 src/pages/ContentScripts/features/contributor_button/DataNotFound.tsx create mode 100644 src/pages/ContentScripts/features/contributor_button/index.tsx create mode 100644 src/pages/ContentScripts/features/contributor_button/view.tsx create mode 100644 src/pages/ContentScripts/features/repo-header-labels/ContributorChart.tsx diff --git a/src/api/repo.ts b/src/api/repo.ts index 63858546..0b857aa7 100644 --- a/src/api/repo.ts +++ b/src/api/repo.ts @@ -5,6 +5,7 @@ const metricNameMap = new Map([ ['activity', 'activity'], ['openrank', 'openrank'], ['participant', 'participants'], + ['contributor', 'new_contributors'], ['forks', 'technical_fork'], ['stars', 'stars'], ['issues_opened', 'issues_new'], @@ -33,6 +34,10 @@ export const getParticipant = async (repo: string) => { return getMetricByName(repo, metricNameMap, 'participant'); }; +export const getContributor = async (repo: string) => { + return getMetricByName(repo, metricNameMap, 'contributor'); +}; + export const getForks = async (repo: string) => { return getMetricByName(repo, metricNameMap, 'forks'); }; diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index ca230938..461eafb9 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -305,6 +305,9 @@ "header_label_participant": { "message": "Participants" }, + "header_label_contributor": { + "message": "Contributors" + }, "fork_popup_title": { "message": "Fork Events" }, diff --git a/src/locales/zh_CN/messages.json b/src/locales/zh_CN/messages.json index de70b5f6..382f29c5 100644 --- a/src/locales/zh_CN/messages.json +++ b/src/locales/zh_CN/messages.json @@ -305,6 +305,9 @@ "header_label_participant": { "message": "参与人数" }, + "header_label_contributor": { + "message": "贡献人数" + }, "fork_popup_title": { "message": "Fork 事件" }, diff --git a/src/pages/ContentScripts/features/contributor_button/DataNotFound.tsx b/src/pages/ContentScripts/features/contributor_button/DataNotFound.tsx new file mode 100644 index 00000000..9ee69d34 --- /dev/null +++ b/src/pages/ContentScripts/features/contributor_button/DataNotFound.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +const DataNotFound = () => { + return ( +
+

Data Not Found

+
+

Possible reasons are:

+ +
+
+ ); +}; + +export default DataNotFound; diff --git a/src/pages/ContentScripts/features/contributor_button/index.tsx b/src/pages/ContentScripts/features/contributor_button/index.tsx new file mode 100644 index 00000000..b984e4bf --- /dev/null +++ b/src/pages/ContentScripts/features/contributor_button/index.tsx @@ -0,0 +1,78 @@ +/** + * 该模块负责在GitHub仓库页面中,增强贡献者列表的功能。 + * 它使用React来渲染贡献者列表,并通过API获取额外的网络数据。 + */ + +import React, { useState, useEffect } from 'react'; +import { render } from 'react-dom'; +import $ from 'jquery'; +import View from './view'; +import { getRepoName } from '../../../../helpers/get-repo-info'; +import {getDeveloperNetwork, getRepoNetwork} from "../../../../api/repo"; +import features from '../../../../feature-manager'; +import * as pageDetect from 'github-url-detection'; +import elementReady from "element-ready"; + +// 全局变量用于存储仓库名称和网络数据,以便在不同函数间共享 +// 定义全局变量,用于存储仓库名称和网络数据。 +let repoName: string; +let developerNetworks: any; +let repoNetworks: any; +let target: any; + +// 获取当前模块的特征ID,用于特性管理。 +const featureId = features.getFeatureID(import.meta.url); + +/** + * 异步获取仓库开发者和仓库的网络数据。 + */ +const getData = async () => { + developerNetworks = await getDeveloperNetwork(repoName); + repoNetworks = await getRepoNetwork(repoName); +}; + +/** + * 替换贡献者列表为React组件。 + * @param target 要替换的目标元素。 + */ +const replaceContributorList = (target: HTMLElement) => { + const originalHTML = target.innerHTML; + + render( + + + , + document.querySelector('.list-style-none.d-flex.flex-wrap.mb-n2') as HTMLElement + ); +}; + +/** + * 初始化功能,包括获取仓库名称和数据,以及替换贡献者列表。 + */ +const init = async (): Promise => { + repoName = getRepoName(); + const targetElement = document.querySelector('.list-style-none.d-flex.flex-wrap.mb-n2') as HTMLElement; + await getData(); + replaceContributorList(targetElement); +}; + +/** + * 在页面刷新或导航时恢复功能,重新加载数据和渲染列表。 + */ +const restore = async () => { + if (repoName !== getRepoName()) { + repoName = getRepoName(); + await getData(); + } + $('div.ReactModalPortal').remove(); + replaceContributorList(target); +}; + + +// 将功能添加到特性管理器中,配置初始化和恢复函数。 +features.add(featureId, { +// asLongAs: [pageDetect.isUserProfile], + awaitDomReady: false, + init, + restore, +}); diff --git a/src/pages/ContentScripts/features/contributor_button/view.tsx b/src/pages/ContentScripts/features/contributor_button/view.tsx new file mode 100644 index 00000000..efbb0841 --- /dev/null +++ b/src/pages/ContentScripts/features/contributor_button/view.tsx @@ -0,0 +1,64 @@ +import React, { useState, useEffect } from 'react'; +import ReactModal from 'react-modal'; +import Graph from '../../../../components/Graph'; +import optionsStorage, { HypercrxOptions, defaults } from '../../../../options-storage'; +import { useTranslation } from 'react-i18next'; +import '../../../../helpers/i18n'; + +// 定义开发者和仓库的时间周期 +const DEVELOPER_PERIOD = 90; +const REPO_PERIOD = 90; + +// 定义Props接口,包括开发者网络和目标HTML +interface Props { + developerNetwork: any; + target: any; +} + +// 定义图表样式 +const graphStyle = { + width: '296px', + height: '400px', +}; + +// 定义View组件 +const View = ({ developerNetwork, target}: Props): JSX.Element => { + // 定义状态变量,包括选项、是否显示图表和是否显示仓库网络 + const [options, setOptions] = useState(defaults); + const [showGraph, setShowGraph] = useState(true); + const [showRepoNetwork, setShowRepoNetwork] = useState(false); + + // 使用翻译函数 + const { t, i18n } = useTranslation(); + + // 使用useEffect钩子来处理副作用,包括获取选项和改变语言 + useEffect(() => { + (async function () { + setOptions(await optionsStorage.getAll()); + i18n.changeLanguage(options.locale); + })(); + }, [options.locale]); + + // 返回JSX元素,包括一个按钮和一个条件渲染的图表或目标HTML + return ( +
+ + {showGraph ? ( +
+
+
+ +
+
+
+ ) : ( +
+ )} +
+ ); +}; + +// 导出View组件 +export default View; diff --git a/src/pages/ContentScripts/features/repo-header-labels/ContributorChart.tsx b/src/pages/ContentScripts/features/repo-header-labels/ContributorChart.tsx new file mode 100644 index 00000000..d1e4c0a9 --- /dev/null +++ b/src/pages/ContentScripts/features/repo-header-labels/ContributorChart.tsx @@ -0,0 +1,157 @@ +import React, { useEffect, useRef } from 'react'; +import * as echarts from 'echarts'; + +import { formatNum, numberWithCommas } from '../../../../helpers/formatter'; + +const LIGHT_THEME = { + FG_COLOR: '#24292F', + BG_COLOR: '#ffffff', + SPLIT_LINE_COLOR: '#D0D7DE', + BAR_COLOR: '#3E90F1', + LINE_COLOR: '#267FE8', +}; + +const DARK_THEME = { + FG_COLOR: '#c9d1d9', + BG_COLOR: '#0d1118', + SPLIT_LINE_COLOR: '#30363D', + BAR_COLOR: '#3E90F1', + LINE_COLOR: '#82BBFF', +}; + +interface ContributorChartProps { + theme: 'light' | 'dark'; + width: number; + height: number; + data: [string, number][]; +} + +const ContributorChart = (props: ContributorChartProps): JSX.Element => { + const { theme, width, height, data } = props; + + const divEL = useRef(null); + + const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME; + + const option: echarts.EChartsOption = { + tooltip: { + trigger: 'axis', + textStyle: { + color: TH.FG_COLOR, + }, + backgroundColor: TH.BG_COLOR, + formatter: tooltipFormatter, + }, + grid: { + top: '10%', + bottom: '5%', + left: '8%', + right: '5%', + containLabel: true, + }, + xAxis: { + type: 'time', + // 30 * 3600 * 24 * 1000 milliseconds + minInterval: 2592000000, + splitLine: { + show: false, + }, + axisLabel: { + color: TH.FG_COLOR, + formatter: { + year: '{yearStyle|{yy}}', + month: '{MMM}', + }, + rich: { + yearStyle: { + fontWeight: 'bold', + }, + }, + }, + }, + yAxis: [ + { + type: 'value', + position: 'left', + axisLabel: { + color: TH.FG_COLOR, + formatter: formatNum, + }, + splitLine: { + lineStyle: { + color: TH.SPLIT_LINE_COLOR, + }, + }, + }, + ], + dataZoom: [ + { + type: 'inside', + start: 0, + end: 100, + minValueSpan: 3600 * 24 * 1000 * 180, + }, + ], + series: [ + { + type: 'bar', + data: data, + itemStyle: { + color: '#ff8061', + }, + emphasis: { + focus: 'series', + }, + yAxisIndex: 0, + }, + { + type: 'line', + symbol: 'none', + lineStyle: { + color: '#ff8061', + }, + data: data, + emphasis: { + focus: 'series', + }, + yAxisIndex: 0, + }, + ], + animationEasing: 'elasticOut', + animationDelayUpdate: function (idx: any) { + return idx * 5; + }, + }; + + useEffect(() => { + let chartDOM = divEL.current; + const instance = echarts.init(chartDOM as any); + + return () => { + instance.dispose(); + }; + }, []); + + useEffect(() => { + let chartDOM = divEL.current; + const instance = echarts.getInstanceByDom(chartDOM as any); + if (instance) { + instance.setOption(option); + } + }, []); + + return
; +}; + +const tooltipFormatter = (params: any) => { + const res = ` + ${params[0].data[0]}
+ ${params[0].marker} + + ${numberWithCommas(params[0].data[1])} + + `; + return res; +}; + +export default ContributorChart; diff --git a/src/pages/ContentScripts/features/repo-header-labels/index.tsx b/src/pages/ContentScripts/features/repo-header-labels/index.tsx index 42def69d..c44ddb2e 100644 --- a/src/pages/ContentScripts/features/repo-header-labels/index.tsx +++ b/src/pages/ContentScripts/features/repo-header-labels/index.tsx @@ -9,7 +9,12 @@ import { hasRepoContainerHeader, isPublicRepoWithMeta, } from '../../../../helpers/get-repo-info'; -import { getActivity, getOpenrank, getParticipant } from '../../../../api/repo'; +import { + getActivity, + getOpenrank, + getParticipant, + getContributor, +} from '../../../../api/repo'; import { RepoMeta, metaStore } from '../../../../api/common'; import View from './view'; @@ -18,12 +23,14 @@ let repoName: string; let activity: any; let openrank: any; let participant: any; +let contributor: any; let meta: RepoMeta; const getData = async () => { activity = await getActivity(repoName); openrank = await getOpenrank(repoName); participant = await getParticipant(repoName); + contributor = await getContributor(repoName); meta = (await metaStore.get(repoName)) as RepoMeta; }; @@ -33,6 +40,7 @@ const renderTo = (container: Container) => { activity={activity} openrank={openrank} participant={participant} + contributor={contributor} meta={meta} />, container diff --git a/src/pages/ContentScripts/features/repo-header-labels/view.tsx b/src/pages/ContentScripts/features/repo-header-labels/view.tsx index 8b94e105..21dce2e9 100644 --- a/src/pages/ContentScripts/features/repo-header-labels/view.tsx +++ b/src/pages/ContentScripts/features/repo-header-labels/view.tsx @@ -14,6 +14,7 @@ import generateDataByMonth from '../../../../helpers/generate-data-by-month'; import ActivityChart from './ActivityChart'; import OpenRankChart from './OpenRankChart'; import ParticipantChart from './ParticipantChart'; +import ContributorChart from './ContributorChart'; import { RepoMeta } from '../../../../api/common'; const githubTheme = getGithubTheme(); @@ -22,6 +23,7 @@ interface Props { activity: any; openrank: any; participant: any; + contributor: any; meta: RepoMeta; } @@ -29,6 +31,7 @@ const View = ({ activity, openrank, participant, + contributor, meta, }: Props): JSX.Element | null => { const [options, setOptions] = useState(defaults); @@ -43,11 +46,18 @@ const View = ({ })(); }, []); - if (isNull(activity) || isNull(openrank) || isNull(participant)) return null; + if ( + isNull(activity) || + isNull(openrank) || + isNull(participant) || + isNull(contributor) + ) + return null; const activityData = generateDataByMonth(activity, meta.updatedAt); const openrankData = generateDataByMonth(openrank, meta.updatedAt); const participantData = generateDataByMonth(participant, meta.updatedAt); + const contributorData = generateDataByMonth(contributor, meta.updatedAt); return (
@@ -134,6 +144,7 @@ const View = ({ d="M448 170.666667a192 192 0 0 1 98.56 356.821333A320.085333 320.085333 0 0 1 768 832a42.666667 42.666667 0 0 1-85.333333 0 234.666667 234.666667 0 0 0-469.333334 0 42.666667 42.666667 0 0 1-85.333333 0 320.128 320.128 0 0 1 221.44-304.554667A192 192 0 0 1 448 170.666667z m256 42.666666a149.333333 149.333333 0 0 1 107.434667 253.056A212.992 212.992 0 0 1 917.333333 650.666667a42.666667 42.666667 0 0 1-85.333333 0 128 128 0 0 0-128-128 42.666667 42.666667 0 0 1-42.325333-48.042667 42.666667 42.666667 0 0 1 37.376-47.701333L704 426.666667a64 64 0 0 0 6.144-127.701334L704 298.666667a42.666667 42.666667 0 0 1 0-85.333334z m-256 42.666667a106.666667 106.666667 0 1 0 0 213.333333 106.666667 106.666667 0 0 0 0-213.333333z" /> + {numberWithCommas(contributorData[contributorData.length - 1][1])}/ {numberWithCommas(participantData[participantData.length - 1][1])} +
+ {getMessageByLocale('header_label_contributor', options.locale)} +
+
{getMessageByLocale('header_label_participant', options.locale)}
diff --git a/src/pages/ContentScripts/index.ts b/src/pages/ContentScripts/index.ts index bab274a3..c2e3cbb8 100644 --- a/src/pages/ContentScripts/index.ts +++ b/src/pages/ContentScripts/index.ts @@ -1,5 +1,5 @@ import './index.scss'; - +import './features/contributor_button'; import './features/repo-activity-openrank-trends'; import './features/developer-activity-openrank-trends'; import './features/repo-header-labels';