Merge branch 'pre_gitlink_ssr' of https://gitlink.org.cn/Gitlink/forgeplus-react into pre_gitlink_ssr

This commit is contained in:
谢思 2024-11-16 14:22:29 +08:00
commit ff3cdd0ac6
35 changed files with 2754 additions and 27 deletions

View File

@ -1,4 +1,5 @@
@charset "utf-8";
/* 头部 */
.header {
width: 100%;

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -198,6 +198,12 @@ form .ant-cascader-picker, form .ant-select {
.linesContent .lines > span:first-child{
margin-right: 0px;
}
.linesContent .lines,.linesContent .linetype,.no-select,.filesInfo{
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
user-select: none; /* 标准语法 */
}
.linesContent .lines > span{
width: 50%;
text-align: right;

View File

@ -23,6 +23,15 @@ const Pages = Loadable({
loader: () => import('./pages'),
loading: Loading,
})
//
const Measurement = Loadable({
loader: () => import('./measurement'),
loading: Loading,
})
const MeasurementDetail = Loadable({
loader: () => import('./measurement/dimension'),
loading: Loading,
})
function ServerIndex(props){
const { projectDetail } = props;
const { owner , projectsId } = props.match.params;
@ -37,11 +46,21 @@ function ServerIndex(props){
return(
<div className="panels">
<Switch {...props}>
<Route path="/:owner/:projectsId/service/measurement/:type"
render={
(p) => (<MeasurementDetail {...props} {...p}/>)
}
></Route>
<Route path="/:owner/:projectsId/service/reposyncer"
render={
() => (<Reposyncer {...props}/>)
}
></Route>
<Route path="/:owner/:projectsId/service/measurement"
render={
(p) => (<Measurement {...props} {...p}/>)
}
></Route>
<Route path="/:owner/:projectsId/service/codeanalysis"
render={
() => (<Data {...props}/>)

View File

@ -8,6 +8,7 @@ function Main(props){
const [ has_trace_user , setHas_trace_user ] = useState(false);
const { current_user , resetUserInfo, projectDetail, showNotification, isDeveloper, isManager, isReporter } = props;
const privateStatus = projectDetail && projectDetail.private;
useEffect(()=>{
//
@ -79,6 +80,19 @@ function Main(props){
<Link to={`/${owner}/${projectsId}/service/pages`} className="btnhover">查看详情</Link>
</span>
</li>}
{
!privateStatus &&
<li>
<span className="servername">
<img src={require('./img/logo4.png')} alt=""/>
<a onClick={openDetail} style={{marginLeft:"-8px"}}>开源软件健康度量服务</a>
</span>
<p className="task-hide-2 serverdesc">支持项目健康度量分析一键生成协助维护项目健康生态</p>
<span className="serverbtn">
<Link to={`/${owner}/${projectsId}/service/measurement`} className="btnhover">查看详情</Link>
</span>
</li>
}
</ul>
</div>
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -461,4 +461,35 @@
.timeBox{
color: rgba(70, 106, 255, 1)
}
}
.nullStoreBox{
background-color:#fafcff;
border-radius:4px 4px 0px 0px;
text-align: center;
color:#333333;
padding-bottom: 65px;
.loBox{width: 68px;}
.introBox{
color:#666666;
width: 57%;
margin: 15px auto;
word-break: break-all;
}
.borBox{
width: 45%;
margin: 0 auto 20px;
border-bottom: 1px solid rgba(90, 117, 193, 0.23);
}
}
.headBox{
height:60px;
line-height: 60px;
background-color:#fafcff;
border:1px solid rgba(42, 97, 255, 0.23);
border-radius:3px 3px 0px 0px;
color:#333333;
position: relative;
em{
font-family: YouSheBiaoTiHei;
}
}

View File

@ -0,0 +1,169 @@
import React, { useEffect, useState } from 'react';
import StackImg from './stackImg';
import Pie from './pie';
import { Select } from 'antd';
import Nodata from '../../../Nodata';
function CompanyData({ data }){
const [ keys , setKeys ] = useState([]);
const [ values , setValues ] = useState([]);
const [ userDrop , setUserDrop ] = useState([]);
const [ userValue , setUserValue ] = useState([]);
const [ c1keys , setC1Keys ] = useState([]);
const [ c1values , setC1Values ] = useState([]);
const [ c1userDrop , setC1UserDrop ] = useState([]);
const [ c1userValue , setC1UserValue ] = useState([]);
const [ c3keys , setC3Keys ] = useState([]);
const [ c3values , setC3Values ] = useState([]);
const [ c3userDrop , setC3UserDrop ] = useState([]);
const [ c3userValue , setC3UserValue ] = useState([]);
const [ pieValues , setPieValues ] = useState([]);
const { c1 ,c2,c3,c4 } = data;
useEffect(()=>{
if(c1 && c1.length > 0){
const values = Array.from(new Set(c1.map(i => i.Module || "")));
setC1UserDrop(values);
setC1UserValue(values[0]);
}
},[c1])
useEffect(()=>{
if(c1userValue && c1 && c1.length > 0){
let fil = c1.filter(i=> i.Module === c1userValue);
const companyArray = Array.from(new Set(fil.map(i => i.Company)));
let monthList = fil.map(obj => obj.Month);
let valueList = fil.map(obj => obj['Weight Ratio']);
setC1Keys(monthList);
if(companyArray && companyArray.length>0 && fil.length>0){
const arr = companyArray.map(item=>{
let v = fil.filter(i=> i.Company == item);
let valueList = v.map(obj => obj['Weight Ratio']);
return {name :item , type: 'line',stack: 'Total',data:valueList}
})
setC1Values(arr);
}else{
setC1Values(valueList);
}
}
},[c1,c1userValue])
useEffect(()=>{
if(c2 && c2.length > 0){
const values = Array.from(new Set(c2.map(i => i.Company || "")));
setUserDrop(values);
setUserValue(values[0]);
}
},[c2])
useEffect(()=>{
if(userValue && c2 && c2.length > 0){
let fil = c2.filter(i=>i.Company === userValue);
let monthList = fil.map(obj => obj.Month);
let valueList = fil.map(obj => obj['Total Weight']);
setKeys(monthList);
setValues(valueList);
}
},[c2,userValue])
useEffect(()=>{
if(c3 && c3.length > 0){
const values = Array.from(new Set(c3.map(i => i.Company || "")));
setC3UserDrop(values);
setC3UserValue(values[0]);
}
},[c3])
useEffect(()=>{
if(c3userValue && c3 && c3.length > 0){
let fil = c3.filter(i=>i.Company === c3userValue);
let monthList = fil.map(obj => obj.Month);
let valueList = fil.map(obj => parseInt(obj['Percentage']));
setC3Keys(monthList);
setC3Values(valueList);
}
},[c3,c3userValue])
useEffect(()=>{
if(c4 && c4.length > 0){
let pieValues = c4.map(i=>{return {name:i['Company'],value:i["Commit Count"]}});
setPieValues(pieValues)
}
},[c4])
return(
(!c1 && !c2 && !c3 && !c4) ? <div>
<div className='dropEchartPanel'>
{
c1userValue && c1userDrop && c1userDrop.length>0 &&
<div style={{textAlign:"left",paddingLeft:20}}>
<span className='mr10'>公司名:</span>
<Select
value={c1userValue}
style={{ width: 220 }}
onChange={(e)=>{setC1UserValue(e);}}
>
{c1userDrop.map(value => {
return <Option key={value}>{value}</Option>
})}
</Select>
</div>
}
<StackImg x={c1keys} values={c1values} title={'每月公司贡献占比'} id={"c1"}/>
</div>
<div className='dropEchartPanel'>
{
userValue && userDrop && userDrop.length>0 &&
<div style={{textAlign:"left",paddingLeft:20}}>
<span className='mr10'>公司名:</span>
<Select
value={userValue}
style={{ width: 220 }}
onChange={(e)=>{setUserValue(e);}}
>
{userDrop.map(value => {
return <Option key={value}>{value}</Option>
})}
</Select>
</div>
}
<StackImg x={keys} values={[{name:"", type: 'line',stack: 'Total',data:values}]} title={'每月公司总贡献'} id={"c2"}/>
</div>
<div className='dropEchartPanel'>
{
c3userValue && c3userDrop && c3userDrop.length>0 &&
<div style={{textAlign:"left",paddingLeft:20}}>
<span className='mr10'>公司名:</span>
<Select
value={c3userValue}
style={{ width: 220 }}
onChange={(e)=>{setC3UserValue(e);}}
>
{c3userDrop.map(value => {
return <Option key={value}>{value}</Option>
})}
</Select>
</div>
}
<StackImg x={c3keys} values={[{name:"", type: 'line',stack: 'Total',data:c3values}]} title={'每月公司贡献最高模块中总贡献'} id={"c3"}/>
</div>
<div className='dropEchartPanel'>
<Pie title={'公司总提交数'} values={pieValues}/>
</div>
</div>
:
<Nodata _html="暂无数据~"/>
)
}
export default CompanyData;

View File

@ -0,0 +1,51 @@
import React, { useEffect, useState } from 'react';
import StackImg from './stackImg';
import { Select } from 'antd';
function DataStack({ data }){
const [ keys , setKeys ] = useState([]);
const [ values , setValues ] = useState([]);
const [ userDrop , setUserDrop ] = useState([]);
const [ userValue , setUserValue ] = useState([]);
useEffect(()=>{
if(data){
let drop = Object.keys(data);
setUserDrop(drop);
setUserValue(drop[0]);
}
},[data])
useEffect(()=>{
if(data && userValue){
let arr = data[userValue];
let ks = arr && Object.keys(arr);
let vs = arr && Object.values(arr);
setKeys(ks);
setValues(vs);
}
},[data,userValue])
return(
<div>
{
userValue && userDrop && userDrop.length>0 &&
<div style={{textAlign:"left",paddingLeft:20}}>
<span className='mr10'>公司名:</span>
<Select
value={userValue}
style={{ width: 220 }}
onChange={(e)=>{setUserValue(e);}}
>
{userDrop.map(value => {
return <Option key={value}>{value}</Option>
})}
</Select>
</div>
}
<StackImg x={keys} values={[{name:"", type: 'line',stack: 'Total',data:values}]} id={"companyNum"}/>
</div>
)
}
export default DataStack;

View File

@ -0,0 +1,54 @@
import React, { useEffect , useState } from 'react';
import { Table } from 'antd';
import { Link } from 'react-router-dom';
import "../index.scss";
import { fieldData , thirdData , subField } from '../config';
function Indicator({owner , projectsId , data , subCardIndex}){
const [ dataSource , setDataSource ] = useState([]);
useEffect(()=>{
if(data && data.length > 0 && subCardIndex){
let name = thirdData[subField[subCardIndex]];
let s = data.filter(i=>name.includes(i.name));
setDataSource(s);
}
},[subCardIndex,data])
const column = [
{
title: "指标名称",
dataIndex: "name",
render:(value)=>{
return(
(value !=="branchNum" && value !== "browseNum") ?
<Link to={`/${owner}/${projectsId}/service/measurement/${value}`} className='color-blue'>{fieldData[value]}</Link>
:
<span>{fieldData[value]}</span>
)
// return fieldData[value]
}
},
{
title: "度量得分",
dataIndex: "InitialScore",
align:"center",
render:(v)=>{
return `${Math.round(v)}`
}
},
{
title: "权重占比",
align:"center",
dataIndex: "Weight",
render:(v)=>{
return `${v ? parseFloat(v*100).toFixed(1) : 0}%`
}
},
]
return(
<Table columns={column} dataSource={dataSource} pagination={false} size='small' className='smalltable'/>
)
}
export default Indicator;

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import * as echarts from 'echarts';
// title:
function Pie({values,title}){
useEffect(()=>{
if(values && values.length>0){
Init(values)
}
},[values])
function Init(data){
var huan_val = document.getElementById("Pie");
var myEcharts = echarts.init(huan_val);
let option = {
title: {
text: title || "",
left: 20,
top:20
},
tooltip: {
trigger: 'item'
},
color:['#5871FF',"#FFBE38","#00bf9a"],
legend: {
orient: 'horizontal',
right: 20,
top:20,
formatter: function (name) {
//
var data = option.series[0].data;
for (var i = 0; i < data.length; i++) {
if (data[i].name == name) {
return data[i].value === 0 ? name + ' (0)' : name;
}
}
return name;
}
},
series: [
{
name: '',
type: 'pie',
radius: '50%',
data: data,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
center: ['50%', '60%']
}
]
};
myEcharts.setOption(option);
}
return(
<div id="Pie" style={{height:"300px"}}></div>
)
}
export default Pie;

View File

@ -0,0 +1,82 @@
import React, { useEffect , useState } from 'react';
import * as echarts from 'echarts';
import { fieldData } from '../config';
function Radar({data, subCardIndex}){
const [ values , setValues ] = useState([]);
const [ titles , setTitles ] = useState([]);
useEffect(()=>{
if(data && data.length > 0 && subCardIndex){
let d = [...data];
let v = [];
d = d.map(i=>{
v = v.concat(i.InitialScore)
return {name:fieldData[i.name],max:100}
})
setValues(v);
setTitles(d);
}
},[subCardIndex,data])
useEffect(()=>{
if(titles && values && values.length>0 &&titles.length>0)
Init();
},[values,titles])
function Init(){
var huan_val = document.getElementById("radar");
var myEcharts = echarts.init(huan_val);
let option = {
tooltip: {
trigger: 'item'
},
title: {
text: ''
},
color:['#5871FF',"#FFBE38"],
radar: {
// shape: 'circle',
indicator: titles,
radius:'50%',
// splitLine:{
// lineStyle:{
// color:'#11B0D3',
// opacity: 0.2
// },
// },
// axisLine:{
// lineStyle:{
// color:'#11B0D3',
// opacity: 0.2
// },
// },
// splitArea:{
// areaStyle:{
// color: 'none',
// }
// },
nameGap: 3,
},
series: [
{
name: '',
type: 'radar',
data: [
{
value: values,
name: ''
}
]
}
]
};
myEcharts.setOption(option);
}
return(
<div id="radar" style={{height:"300px",width:"45%"}}></div>
)
}
export default Radar;

View File

@ -0,0 +1,140 @@
import React, { useEffect, useState } from 'react';
import * as echarts from 'echarts';
import { detailByName } from '../config';
import Pie from './pie';
import { Select } from 'antd';
function Stack({name,data , type , pieData}){
const [ obj , setObj ] = useState({});
const [ list , setList ] = useState(data);
const [ userDrop , setUserDrop ] = useState([]);
const [ userValue , setUserValue ] = useState([]);
const [pieEchartData,setPieEchartData] = useState([]);
useEffect(()=>{
if(name){
let obj = detailByName[name.replace(/\s/g, '')];
setObj(obj);
}
},[name])
useEffect(()=>{
if(data && data.length > 0){
if(obj && obj.user_login){
const values = Array.from(new Set(data.map(i => i.user_login || "")));
setUserDrop(values);
setUserValue(values[0]);
}else{
setList(data);
}
}
},[data,obj && obj.user_login])
useEffect(()=>{
if(userValue){
let valueData = data && data.filter(i=>i.user_login === userValue);
setList(valueData);
}
},[userValue])
useEffect(()=>{
if(list && list.length > 0){
let values = [];
const monthList = list.map(obj => obj.month);
values = list.map(obj => Math.round(obj[type] || obj["count"] || obj['commits_number'] || 0));
Init(monthList,values);
}
},[list])
function Init(x,values){
var huan_val = document.getElementById("line");
var myEcharts = echarts.init(huan_val);
let option = {
title: {
text: ''
},
tooltip: {
trigger: 'axis'
},
color:['#5871FF',"#FFBE38"],
// legend: {
// data: ['Email', 'Union Ads']
// },
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
// toolbox: {
// feature: {
// saveAsImage: {}
// }
// },
xAxis: {
type: 'category',
boundaryGap: false,
data: x
},
yAxis: {
type: 'value'
},
series: [
{
name: name,
type: 'line',
stack: 'Total',
data: values
}
]
};
myEcharts.setOption(option);
}
useEffect(()=>{
if(pieData){
let newData = new Array;
for (var i in pieData) {
var json = {};
if (pieData[i] != "") {
json.name = i;
json.value = pieData[i];
newData.push(json);
}
}
setPieEchartData(newData)
}
},[pieData])
return(
<div>
<div className='dropEchartPanel'>
{
obj && obj.user_login && userValue && userDrop && userDrop.length>0 &&
<div style={{textAlign:"left",paddingLeft:20}}>
<span className='mr10'>贡献者:</span>
<Select
value={userValue}
style={{ width: 220 }}
onChange={(e)=>{setUserValue(e);}}
>
{userDrop.map(value => {
return <Option key={value}>{value}</Option>
})}
</Select>
</div>
}
<div id="line" style={{height:"350px"}}></div>
</div>
{
pieData &&
<div className='dropEchartPanel'>
<Pie values={pieEchartData} title="Commit Message质量分布"/>
</div>
}
</div>
)
}
export default Stack;

View File

@ -0,0 +1,58 @@
import React, { useEffect, useState } from 'react';
import * as echarts from 'echarts';
function StackImg({x,values,title,id}){
useEffect(()=>{
if(x && values && x.length>0 && values.length>0 && id){
Init(x,values)
}
},[x,values,id])
function Init(xData,data){
var huan_val = document.getElementById(id);
var myEcharts = echarts.init(huan_val);
let option = {
title: {
text: title || '',
textStyle:{
fontSize: 17 ,
fontWeight: 'normal',
},
left:20,
top:15
},
tooltip: {
trigger: 'axis'
},
color:['#5871FF',"#FFBE38"],
// legend: {
// data: ['Email', 'Union Ads']
// },
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
// toolbox: {
// feature: {
// saveAsImage: {}
// }
// },
xAxis: {
type: 'category',
boundaryGap: false,
data: xData
},
yAxis: {
type: 'value'
},
series: data
};
myEcharts.setOption(option);
}
return(
<div id={id} style={{height:"350px"}}></div>
)
}
export default StackImg;

View File

@ -0,0 +1,83 @@
export const firstData = ['开发活动', '社区协作','项目影响']
export const secondData = {
开发活动:["开发质量","开发效率"],
社区协作:['个体开发者','商业公司'],
项目影响:["项目影响"]
}
export const field={
社区协作:"communityCooperation",
开发活动:"developmentActivity",
项目影响:"influence"
}
export const subField={
开发质量:"developmentQuality",
开发效率:"developmentEfficiency",
个体开发者:"individual",
商业公司:"company",
项目影响:"influence"
}
export const thirdData = {
developmentEfficiency:["PRNum","PRTime","commitNum","issueTime","ProjectActivity"],
developmentQuality:["commitMessageQuality","issueNum"],
individual:["individualLoss","individualNum","longTermIndividualNum"],
company:["companyLead","companyLoss","companyNum","longTermCompanyNum"],
influence:["branchNum","browseNum","forkNum","praiseNum","storeNum"]
}
export const fieldData = {
PRNum:"PR数量",
PRTime:"PR处理间隔",
commitNum:"Commit数量",
issueTime:"Issue处理间隔",
commitMessageQuality:"Commit message质量",
issueNum:"Issue数量",
ProjectActivity:"项目活跃度",
individualLoss:"长期贡献者稳定性",
individualNum:"活跃开发者占比",
longTermIndividualNum:"长期贡献者占比",
companyLead:"公司主导",
companyLoss:"公司贡献者流失预测",
companyNum:"公司贡献者数量",
longTermCompanyNum:"长期公司贡献者数量",
browseNum:"浏览次数",
forkNum:"Fork次数",
praiseNum:"点赞次数",
storeNum:"关注数量",//followNum
branchNum:"分支数量"
}
export const detailByName={
Commitmessage质量:{desc:"可以通过每月新增Poor Message数量和Commit Message质量分布这两个维度对Commit message质量进行度量",radar:true},
Issue数量:{desc:"通过分析项目每月提交的Issue数量对Issue数量指标进行度量"},
Commit数量:{desc:"通过分析项目每月提交的三种Commit数量对Commit数量指标进行度量"},
PR数量:{desc:"通过分析项目每月提交的PR数量对PR数量指标进行度量"},
PR处理间隔:{desc:"通过分析每个月新增PR的平均处理间隔对PR处理间隔(小时)指标进行度量"},
Issue处理间隔:{desc:"通过分析每个月新增Issue的平均处理间隔对Issue处理间隔(小时)指标进行度量"},
活跃开发者占比:{desc:"通过分析每个月活跃的个体贡献者数量对个体贡献者数量指标进行度量",descTitle:"个体贡献者数量"},//个体贡献者数量
项目活跃度:{desc:"不同类型的贡献者每月/周给master分支的贡献次数",month:true,user_login:true,descTitle:"所有个体开发者贡献历史"},//所有个体开发者贡献历史
长期贡献者占比:{desc:"通过分析每个月活跃的长期个体贡献者数量对长期个体贡献者数量指标进行度量",descTitle:"长期个体贡献者数量"},//长期个体贡献者数量
长期贡献者稳定性:{desc:"通过分析具体的即将流失的长期贡献者流失概率对个体贡献者流失预测指标进行度量",bar:true,descTitle:"个体贡献者流失预测"},//个体贡献者流失预测
公司贡献者数量:{desc:"通过分析每月做出了贡献的公司贡献者数量对公司贡献者数量指标进行度量"},
长期公司贡献者数量:{desc:"通过分析每月的长期对项目做出贡献的公司贡献者数量对长期公司贡献者数量指标进行度量"},
公司主导:{desc:"从每月公司贡献占比、每月公司总贡献、每月公司贡献最高模块中总贡献、公司总提交数四个方面对公司主导指标进行度量",radar:true},
公司贡献者流失预测:{desc:"通过分析各个公司的流失概率对公司贡献者流失预测指标进行度量"},
浏览次数:{desc:"通过分析每月项目的浏览次数对浏览次数指标进行度量"},
点赞次数:{desc:"通过分析每月项目的点赞次数对点赞次数指标进行度量"},
关注数量:{desc:"通过分析每月项目的关注数量对关注数量指标进行度量"},
分支数量:{desc:"通过分析每月项目的分支数量对分支数量指标进行度量"},
Fork次数:{desc:"通过分析每月项目的新增Fork数量对Fork数量指标进行度量"},
}
export const scoreComp = (score)=>{
let value = Math.round(score);
return <span className="faYou" style={{color:value >= 80 ? "#466aff" : (value<80 && value>=60) ? "#E18419" :"#F70202"}}>{value}</span>
}
export const colorsField={
社区协作:"#F58400",
开发活动:"#466AFF",
项目影响:"#00BF9A"
}

View File

@ -0,0 +1,152 @@
import React ,{useEffect, useState} from 'react';
import { Breadcrumb , Select } from 'antd';
import { Link } from 'react-router-dom';
import "./index.scss";
import LineStack from './componet/stack';
import DataStack from './componet/dataStack';
import CompanyData from './componet/companyData';
import { detailByName , secondData , thirdData , subField ,fieldData , scoreComp} from './config';
import axios from 'axios';
import Nodata from '../../Nodata';
const dimensionData = {
开发质量: ['Commit message质量', 'Issue数量'],
开发效率: ['PR数量', 'PR处理间隔', 'Commit数量', 'Issue处理间隔','项目活跃度'],
个体开发者:['活跃开发者占比','长期贡献者占比','长期贡献者稳定性'],
商业公司:['公司主导','公司贡献者流失预测','公司贡献者数量','长期公司贡献者数量'],
项目影响:['Fork次数','点赞次数','关注数量']//'','',
};
function Dimension(props){
const { type , owner , projectsId } = props.match.params;
const [ data , setData ] = useState([]);
const [ pieData , setPieData ] = useState([]);
const [ score , setScore ] = useState(0);
const [title, setTitle] = useState('');
const [seconds, setSeconds] = useState([]);
const [sValue, setSValue] = useState('');
const [ dimensions , setDimensions] = useState([]);
const [dValue, setDValue] = useState('');
const findObjKeyByValue = (obj, value) => {
return Object.keys(obj).find(key => obj[key].includes(value));
};
const findKeyByValue = (obj, value) => {
return Object.keys(obj).find(key => obj[key].includes(value));
};
useEffect(()=>{
if(type){
Init(type==="storeNum" ?"followNum":type);
let find = findObjKeyByValue(thirdData,type);
if(find){
let sub = findKeyByValue(subField,find);
setSValue(sub);
}else{
setSValue(seconds[0])
}
setDValue(fieldData[type])
}
},[type])
function Init(t){
const url = `/v1/${owner}/${projectsId}/oss_health_measuring/keyid`;
axios(url,{params:{
key_id:t
}}).then(res=>{
if(!res){
props.history.push(`/${owner}/${projectsId}/service/measurement`)
return;
}
let data = res && res.data;
if(data){
let d = data[t] ? data[t].value : data.value;
let s = data.score !== undefined ? data.score : data[t].score;
setPieData(data.distribution);
setData(d);
if(t==="companyLoss"){
d = d.map(i=>{return { month:i.Company , count : (i["Predicted State"] || 0.0)}});
setData(d);
}else if(t === "companyNum" || t === "longTermCompanyNum" || t==="companyLead"){
d = Object.keys(data.value).length === 0 ?[]:[Object.assign(data.value)];
setData(d);
}
setScore(s);
}
})
}
useEffect(()=>{
if(sValue){
let arr = dimensionData[sValue];
setDimensions(arr);
let find = findObjKeyByValue(secondData,sValue);
setTitle(find);
let subList = secondData[find];
setSeconds(subList);
}
},[sValue])
return(
<div>
<Breadcrumb separator=">">
<Breadcrumb.Item><Link className="color-blue" to={`/${owner}/${projectsId}/service/measurement`}>首页</Link></Breadcrumb.Item>
<Breadcrumb.Item>{title}</Breadcrumb.Item>
</Breadcrumb>
<div className='headBox font-16 pl15 mt20 df pr30 mb30'>
{title}
<span style={{marginLeft:"auto"}}>
<span>度量值<em className='color-blue font-26'>{scoreComp(score)}</em></span>
{/* <span className='ml30'>健康度权重比:<em className='color-blue font-26'>20%</em></span> */}
</span>
</div>
<div className='dropTeam'>
<Select
value={sValue}
style={{ width: 120 }}
onChange={(e)=>{setSValue(e);props.history.push(`/${owner}/${projectsId}/service/measurement/${findObjKeyByValue(fieldData,dimensionData[e][0])}`)}}
border={null}
>
{seconds.map(province => (
<Option key={province}>{province}</Option>
))}
</Select>
<Select
value={dValue}
style={{ width: 190 }}
onChange={(e)=>{setDValue(e);props.history.push(`/${owner}/${projectsId}/service/measurement/${findObjKeyByValue(fieldData,e)}`)}}
border={null}
>
{dimensions.map(province => (
<Option key={province}>{province}</Option>
))}
</Select>
</div>
<div className='dropEcharts'>
<p className='dropName'>{dValue && (detailByName[dValue.replace(/\s/g, '')].descTitle ? detailByName[dValue.replace(/\s/g, '')].descTitle : dValue)}</p>
<p className='dropDesc'>{dValue && detailByName[dValue.replace(/\s/g, '')].desc}</p>
{
data && data.length > 0 ?
<div>
{
type === "companyNum" || type === "longTermCompanyNum" ?
<div className='dropEchartPanel'><DataStack data={data[0]} /></div>
:
type === "companyLead" ?
<CompanyData data={data[0]} />
:
<LineStack name={dValue} data={data} type={type==="storeNum" ?"followNum":type} pieData={pieData}/>
}
</div>
:<Nodata _html={`暂无数据~`}/>
}
</div>
</div>
)
}
export default Dimension;

View File

@ -0,0 +1,260 @@
import React, {useState, useEffect , useRef} from "react";
import './index.scss';
import Logo from '../img/logo4.png';
import { Button , Spin } from 'antd';
import Large from '../img/large.png';
import Middle from '../img/middle.png';
import Small from '../img/small.png';
import team from '../img/team.png';
import person from '../img/person.png';
import Rader from './componet/radar';
import Indicator from './componet/indicator';
import { scoreComp , field , firstData , colorsField, secondData , subField , thirdData } from './config';
import axios from "axios";
import cookie from 'react-cookies';
// import res from './xiuos.json';
function Index(props){
const [ dataLoading , setDataLoading ] = useState(true);
const [cardIndex, setCardIndex] = useState(firstData[0]);
const [ subCardIndex, setSubCardIndex] = useState(1);
const [ loading , setLoading ] = useState(false);
const { owner , projectsId } = props.match.params;
const [ datas , setDatas ] = useState();
const [ scoreNormal , setScoreNormal ] = useState();
const [ score , setScore ] = useState([]);
const [ scoreNum , setScoreNum ] = useState(0);
const [ subMenus , setSubMenus ] = useState([]);
const [ subDatas , setSubDatas ] = useState([]);
const [ fieldDatas , setFieldDatas ] = useState([]);
let interval = undefined;
const count = useRef();
useEffect(()=>{
if(owner && projectsId){
console.log(cookie.load("opensource"));
if(cookie.load("opensource") && cookie.load("opensource")===`${owner}_${projectsId}`){
getReportStart();
}else{
checkData();
}
}
},[owner,projectsId])
//
async function checkData(){
const url = `/v1/${owner}/${projectsId}/oss_health_measuring/can_get`;
axios(url).then(res=>{
if(res && res.data && res.data.statue === "YES"){
Init();
clearInterval(interval);
cookie.remove("opensource");
}else if(res && res.data && res.data.statue === "NO"){
setDataLoading(false);
}
})
}
//
function Init(){
const url = `/v1/${owner}/${projectsId}/oss_health_measuring/keyid`;
axios(url,{params:{
key_id:"score"
}}).then(res=>{
if(res && res.data){
let data = res.data;
setTimeout(()=>{setDataLoading(false)},300);
setDatas(data);
setScoreNormal(data);
getlist(data);
}
})
}
//
function getReportStart(){
setLoading(true);
count.current = 20;
checkData();
interval = setInterval(() => {
if (count.current > 1) {
let old = count.current;
count.current = old - 1;
console.log(count);
checkData();
} else {
//
clearInterval(interval);
setLoading(false);
props.showNotification("当前服务繁忙,请稍后再试");
}
}, 30000)
}
// tab
useEffect(()=>{
if(score && cardIndex){
//
let sub = secondData[cardIndex];
sub && sub.length > 0 && setSubCardIndex(sub[0]);
let subData = score.filter(i=>i.name === cardIndex);
setSubMenus(sub);
//
let value = subData && subData.length > 0 && subData[0];
if(value){
setSubDatas(value);
let name = value.name;
let subname = secondData[name];
let list = subname.map(i=>{
return subField[i]
})
let listData = [];
for(var i =0;i<list.length;i++){
let l = value[list[i]] || value;
let d = thirdData[list[i]];
if(d){
let items = d.map(j=>{
return {name:j,...l[j],InitialScore:l[j] ? Math.round(l[j].InitialScore) : 0};
})
listData = listData.concat(items);
}
}
setFieldDatas(listData);
}
}
},[cardIndex,score])
//
function getlist(values){
let d = firstData.map(i=>{
return {name:i,...values[field[i]],color:colorsField[i]};
})
setScore(d);
let sn = d.reduce((sum, item) => Math.round(sum + Math.round(item.WeightedScore)), 0);
setScoreNum(sn);
}
console.log(subDatas);
return(
<div>
<div class="headBox font-16 pl15">开源软件健康度量服务</div>
{ dataLoading && <Spin><div style={{minHeight:400,width:"100%"}}></div></Spin>}
{
!dataLoading && !datas &&
<div class="nullStoreBox mt25">
<img src={Logo} class="loBox mt50" />
<p class="font-22 mt10">欢迎使用开源软件健康度量服务</p>
<div class="introBox font-15">开源软件健康度量服务是一种代码库分析工具旨在通过深入分析代码库的关键活动数据来评估开源项目的健康状况该服务通过收集和分析代码库中的贡献者提交commit合并请求pullRequest疑修issue等数据指标为项目开发者维护者提供了一个全面的项目健康报告</div>
<div class="borBox"></div>
{!loading && <Button type="primary" onClick={()=>{cookie.save("opensource",`${owner}_${projectsId}`,{ expires: new Date(new Date().getTime() + 24 * 3600 * 1000),path:`/` });getReportStart();}}>开始体验</Button> }
{ loading && <div><Spin /><span className="ml5 color-blue">服务报告生成中...</span></div> }
</div>
}
{
!dataLoading && datas &&
<div className="scorePanel">
{
scoreNormal &&
<div className="scoreMain">
<div className="scoreimg">
<span className="scoreValue">
<img src={scoreNum >= 80 ? Large : scoreNum < 60 ? Small : Middle} alt="" width={"100%"}/>
<div className="s_content">
<span>健康值</span>
<span className="s_num">{scoreComp(scoreNum)}</span>
</div>
</span>
<p className="font-18 fw500 mt10 mb15">本项目开源生态健康度</p>
<p className="s_detail">开源生态健康度由开发活动社区协作项目影响三个总体维度对开源项目的健康状况进行分析并加权求和用于反馈项目的总体健康状况您可点击查询每一个维度的度量指标及可视化图表</p>
</div>
{
score &&
<ul className="scoreProgress">
{
<li>
<span className="index">{Math.round(scoreNum)}</span>
<span className="name">开源生态健康度</span>
<div className="progress">
{
score.map((i,k)=>{
return <p style={{width:`${i.WeightedScore}%`,backgroundColor:`${i.color}`}}></p>
})
}
</div>
<span><span className="color-blue">{Math.round(scoreNum)}</span><span className="ml3 mr3">/</span>100</span>
</li>
}
{
score.map((i,k)=>{
return(
<li>
<span className="index">{Math.round(i.WeightedScore)}</span>
<span className="name">{i.name}</span>
<div className="progress"><span style={{width:`${i.WeightedScore}%`,backgroundColor:`${i.color}`}}></span></div>
<span><span className="color-blue">{Math.round(i.WeightedScore)}</span><span className="ml3 mr3">/</span>{i.Weight*100}</span>
</li>
)
})
}
</ul>
}
</div>
}
<div className="scoreDetail">
<ul className="typeLi">
{
firstData.map((i,k)=>{
return <li className={cardIndex === i ?"active":""} onClick={()=>{setCardIndex(i)}}><span>{k<10 && `0${k+1}`}</span>{i}</li>
})
}
</ul>
{
subDatas &&
<div className="typeContent">
<p className="typeValue fw500">
{cardIndex}总分{scoreComp(subDatas.InitialScore)}<span className="mr40 faYou">/100</span>
{cardIndex}加权得分{scoreComp(subDatas.WeightedScore)}<span className="mr40 faYou">/{subDatas.Weight*100}</span>
</p>
<div className="typeEchart">
<ul className="echartCate">
{
subMenus && subMenus.length > 0 &&
subMenus.map((i,k)=>{
let detail = subDatas[subField[i]];
return(
<li className={`${subCardIndex === i ?"active":""}`} onClick={()=>setSubCardIndex(i)}>
<div className="echartNum">
<span className="fw500 font-16">{i}</span>
<span><span className="font-17">{i}总分</span>{scoreComp(detail ? detail.InitialScore : subDatas.InitialScore)}</span>
<span><span className="font-17">健康度权重比</span><span className="faYou">{(detail ? detail.Weight : subDatas.Weight) *100}%</span></span>
</div>
<img src={team} width={76} alt="" />
</li>
)
})
}
</ul>
<div className="echartData">
<Rader data={fieldDatas} subCardIndex={subCardIndex}/>
<div style={{width:"50%"}}>
<Indicator data={fieldDatas} subCardIndex={subCardIndex} owner={owner} projectsId={projectsId}/>
</div>
</div>
</div>
</div>
}
</div>
</div>
}
</div>
)
}
export default Index;

View File

@ -0,0 +1,325 @@
.scorePanel{
.scoreMain{
display: flex;
padding:35px 0px;
.scoreimg{
width: 47%;
padding-right: 80px;
text-align: center;
.s_detail{
font-weight: 400;
font-size: 15px;
color: #131415;
line-height: 30px;
text-align: left;
}
.scoreValue{
position: relative;
display: block;
width: 270px;
text-align: center;
margin:0px auto;
.s_content{
position: absolute;
width: 100%;
bottom: 0px;
text-align: center;
font-weight: 400;
font-size: 13px;
color: #B2C6D9;
display: flex;
flex-direction: column;
.s_num{
font-family: "YouSheBiaoTiHei";
font-weight: 400;
font-size: 41px;
color: #466AFF;
height: 50px;
line-height: 50px;
}
}
}
}
.scoreProgress{
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding:20px 0px;
li{
height: 36px;
width: 100%;
background: #F8F8F8;
padding-right: 28px;
display: flex;
align-items: center;
color: #61708B;
position: relative;
&::before{
width: 0;
height: 0;
border: 26px solid;
border-color: transparent transparent transparent #F8F8F8;
transform: rotate(45deg);
position: absolute;
left: -26px;
content: "";
top: 10px;
}
.index{
width: 56px;
background-image: url(../img/index.png);
background-repeat: no-repeat;
background-size: 100% 100%;
display: block;
text-align: center;
line-height: 36px;
font-family: YouSheBiaoTiHei;
font-weight: 400;
font-size: 16px;
color: #466AFF;
margin-right: 15px;
}
.name{
width: 120px;
margin-right: 14px;
font-weight: 500;
font-size: 16px;
color: #181818;
}
.progress{
width: 330px;
height: 16px;
background: #F3F3F3;
position: relative;
display: flex;
margin-right: 15px;
&>span{
position: absolute;
height: 100%;
left: 0px;
top:0px;
}
&>p{
border-right: 2px solid #fff;
&:last-child{
border:none;
}
}
}
}
}
}
}
.typeLi{
display: flex;
align-items: center;
justify-content: center;
padding:44px 0px;
li{
height: 62px;
background: #F5F6F7;
border-radius: 8px 8px 8px 8px;
border: 1px solid #DBE0E9;
width: 280px;
font-weight: 500;
font-size: 16px;
color: #212F54;
line-height: 60px;
text-align: center;
margin:0px 25px;
cursor: pointer;
span{
font-family: YouSheBiaoTiHei;
font-weight: 400;
font-size: 25px;
margin-right: 15px;
position: relative;
&::after{
content: "";
right: -8px;
bottom: -3px;
width: 19px;
height: 15px;
background-image: url('../img/trans1.png');
background-repeat: no-repeat;
position: absolute;
}
}
&.active{
background-image: url('../img/blue.png');
border:none;
line-height: 62px;
color: #fff;
span{
&::after{
background-image: url('../img/trans.png');
}
}
}
&:hover{
border-color: #6F93D4;
}
}
}
.fw500{
font-weight: 500;
}
.typeContent{
background: rgba(102,102,102,0.03);
border-radius: 0px 0px 0px 0px;
border: 1px solid rgba(70,106,255,0.13);
padding:40px 30px;
.typeValue{
font-size: 17px;
color: #181818;
text-align: center;
margin-bottom: 30px!important;
span{
font-weight: 400;
font-size: 26px;
}
}
.typeEchart{
display: flex;
}
.echartCate{
width: 298px;
margin-right: 30px;
.echartNum{
display: flex;
flex-direction: column;
flex:1;
font-weight: 400;
font-size: 22px;
color: #212F54;
span{
display: flex;
justify-content: space-between;
}
}
li{
background-image: url('../img/back.png');
background-size: 100% 100%;
width: 100%;
height: 170px;
box-shadow: 0px 0px 10px 1px rgba(28,48,175,0.04);
border-radius: 8px 8px 8px 8px;
display: flex;
align-items: center;
padding-left: 30px;
font-weight: 400;
font-size: 15px;
color: #181818;
line-height: 40px;
border: 1px solid #fff;
cursor: pointer;
&.active{
border-color: #466AFF;
position: relative;
&::after{
content: "";
position: absolute;
right: 0px;
top: 0px;
background-image: url("../img/true.png");
width: 56px;
height: 44px;
}
}
&:first-child{
margin-bottom: 30px!important;
}
}
}
.echartData{
background: #FFFFFF;
box-shadow: 0px 0px 10px 1px rgba(28,48,175,0.08);
border-radius: 0px 0px 0px 0px;
flex: 1;
min-height: 370px;
padding:25px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.smalltable{
border-color: rgba(65,84,241,0.04)!important;
.ant-table-body{
margin:0px!important;
.ant-table-thead th {
background-color:rgba(65,84,241,0.04)!important;
}
.ant-table-tbody tr td{
background: #F5F6F7;
}
.ant-table-tbody tr:hover{
background: #F5F6FA;
}
}
}
.dropTeam{
background-image: url('../img/blue-large.png');
background-size: 100% 100%;
background-repeat: no-repeat;
width: 330px;
height: 50px;
margin:40px auto;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
.ant-select-selection{
border:none;
color: #fff;
background-color: transparent;
box-shadow: none;
.ant-select-arrow .ant-select-arrow-icon svg{
color: #fff;
}
}
.ant-select{
position: relative;
&:last-child::before{
content: "";
position: absolute;
width: 1px;
height: 14px;
border: 1px solid rgba(255,255,255,0.36);
left: 0px;
top:9px;
}
}
}
.dropEcharts{
background-image: url('../img/echartBack.png');
text-align: center;
padding: 30px;
.dropName{
font-weight: 500;
font-size: 20px;
color: #181818;
}
.dropDesc{
font-weight: 400;
font-size: 15px;
color: #212F54;
width: 75%;
margin:0px auto;
margin-top: 15px;
}
.dropEchartPanel{
background: #FFFFFF;
box-shadow: 0px 0px 10px 1px rgba(28,48,175,0.08);
border-radius: 6px 6px 6px 6px;
padding:25px;
width: 75%;
margin:0px auto;
margin-top: 20px;
}
}
.faYou{
font-family: YouSheBiaoTiHei;
}

File diff suppressed because it is too large Load Diff

View File

@ -30,15 +30,6 @@
border-color:#1a47ff;
}
}
.reposyncerBox .headBox, .storeListBox .headBox{
height:60px;
line-height: 60px;
background-color:#fafcff;
border:1px solid rgba(42, 97, 255, 0.23);
border-radius:3px 3px 0px 0px;
color:#333333;
position: relative;
}
.reposyncerBox{
padding-bottom: 60px;
.helpBox1{
@ -53,24 +44,6 @@
align-items: center;
cursor: default;
}
.nullStoreBox{
background-color:#fafcff;
border-radius:4px 4px 0px 0px;
text-align: center;
color:#333333;
padding-bottom: 65px;
.loBox{width: 68px;}
.introBox{
color:#666666;
width: 57%;
margin: 15px auto;
}
.borBox{
width: 45%;
margin: 0 auto 20px;
border-bottom: 1px solid rgba(90, 117, 193, 0.23);
}
}
.rightContentBox{
display: flex;
.leftNav{

View File

@ -1,3 +1,5 @@
ul,ol,dl{
margin-bottom: 0px;
}