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:
+
+ - This repository is too new, so its data has not been generated
+ - This repository is not active enough, so its data are not generated
+
+
+
+ );
+};
+
+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';