This commit is contained in:
陈裕财
2024-09-01 14:20:18 +08:00
parent 95964c98c4
commit 64d81aeda0
8 changed files with 669 additions and 0 deletions

View File

@@ -271,6 +271,12 @@
### 效能分析
![项目立项](/docs/images/xm-zs/xm-zs-16-xnfx.png)
### 人效趋势分析
![项目立项](/docs/images/xm-zs/xm-zs-16-rxfx-trend.png)
### 人效排行榜
![项目立项](/docs/images/xm-zs/xm-zs-16-rxfx-sort.png)
### 燃尽图
![项目立项](/docs/images/xm-zs/xm-zs-17-rjt.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

View File

@@ -47,4 +47,6 @@ export const batchSetSbillIdNull = params => { return axios.post(`${base}/xm/cor
export const listXmWorkloadGroupByTaskIdAndUserid = params => { return axios.get(`${base}/xm/core/xmWorkload/ListGroupByTaskIdAndUserid`, { params: params }); };
export const listXmWorkloadGroupByTaskIdAndUseridToSet = params => { return axios.get(`${base}/xm/core/xmWorkload/ListGroupByTaskIdAndUseridToSet`, { params: params }); };
export const listSumWorkloadHumanEffectTrend = params => { return axios.get(`${base}/xm/core/xmWorkload/listSumWorkloadHumanEffectTrend`, { params: params }); };
export const listSumWorkloadHumanEffectSort = params => { return axios.get(`${base}/xm/core/xmWorkload/listSumWorkloadHumanEffectSort`, { params: params }); };

View File

@@ -102,6 +102,9 @@ export default {
},
datas: [],
comps: [
{ isChecked: false, isCurr: false, rptName: '人效趋势分析', category: '项目级,迭代级,产品级,企业级,部门级,小组级', compId: 'humanEffectTrend', desc: '统计产品、项目、迭代、部门等每日/月/年的平均人效', img: datasetLink },
{ isChecked: false, isCurr: false, rptName: '人效排行榜', category: '项目级,迭代级,产品级,企业级,部门级,小组级', compId: 'humanEffectSort', desc: '按人员、产品、项目、迭代、部门等进行人效排行', img: datasetLink },
{ isChecked: false, isCurr: false, rptName: '迭代总结', category: '迭代级', compId: 'xmIterationRptOverview', desc: '显示迭代总体情况', img: pieSimple },
{ isChecked: false, isCurr: false, rptName: '迭代燃尽图', category: '迭代级', compId: 'xmIterationBurnout', desc: '跟踪迭代的剩余工作量按日期变化趋势,识别迭代当前进度情况', img: ranjintu },
{ isChecked: false, isCurr: false, rptName: '需求每日趋势', category: '迭代级', compId: 'xmIterationMenuDayTrend', desc: '跟踪未开始、执行中、已完成、已关闭状态的需求数量按日期变化趋势,识别需求工作情况', img: lineStack },

View File

@@ -24,6 +24,8 @@ export default {
xmProjectWorkItemDayList:defineAsyncComponent(()=>import("../project/projectWorkItemDayList.vue")),
xmProjectWorkloadSetDayList:defineAsyncComponent(()=>import("../project/projectWorkloadSetDayList.vue")),
xmProjectWorkloadSetMonthList:defineAsyncComponent(()=>import("../project/projectWorkloadSetMonthList.vue")),
humanEffectTrend:defineAsyncComponent(()=>import("../workload/HumanEffectTrend.vue")),
humanEffectSort:defineAsyncComponent(()=>import("../workload/HumanEffectSort.vue")),
xmBudgetCostMonthTrend:defineAsyncComponent(()=>import("../project/budgetCostMonthTrend.vue")),
xmQuestionDayTrend:defineAsyncComponent(()=>import("../product/questionDayTrend.vue")),

View File

@@ -0,0 +1,312 @@
<template>
<MdpRptHeader v-model:title="title" v-model:remark="remark" :isRptCfg="isRptCfg" :showParams="showParams" @delete="$emit('delete',cfg)">
<template #title v-if="showTitle===false">
<span></span>
</template>
<el-form :model="params" class="padding" ref="filtersRef">
<el-form-item label="分组属性">
<mdp-select v-model="params.groupBy" :options="groupBys"/>
</el-form-item>
<el-form-item label="成本中心" v-if="!(xmProduct?.id||xmProject?.id||xmIteration?.id||xmGroup?.id)">
<MdpSelectDept v-model="params.deptid"/>
</el-form-item>
<el-form-item label="归属项目" v-if="!xmProject?.id">
<XmProjectSelect width="100%" v-model="params.projectId" @change2="project=$event" :linkProductId="xmProduct?.id"/>
</el-form-item>
<el-form-item label="归属小组" v-if="!xmGroup?.id && project?.id">
<XmGroupUserSelect width="100%" :xm-product="product" :sel-project="project" @select="onGroupChange"/>
</el-form-item>
<el-form-item label="归属员工">
<XmUserSelect width="100%" v-model="params.userid" :xm-product="product" :sel-project="project" @change2="onGroupUserChange" @clear="user=null"/>
</el-form-item>
<el-form-item label="归属产品" v-if="!xmProduct?.id && !xmIteration?.id">
<XmProductSelect width="100%" v-model="params.productId" :linkProjectId="xmProject?.id" @change2="product=$event"/>
</el-form-item>
<el-form-item label="归属迭代" v-if="!xmIteration?.id && product?.id">
<XmIterationSelect width="100%" v-model="params.linkIterationId" :xm-product="product?.id" @change2="iteration=$event"/>
</el-form-item>
<el-form-item label="人效类型">
<el-radio-group v-model="params.dateType">
<el-radio label="days">日均人效</el-radio>
<el-radio label="months">月均人效</el-radio>
<el-radio label="years">年均人效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="起止时间">
<mdp-date-range start-key="startBizDate" end-key="endBizDate" type="daterange" v-model="params" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="统计时间" :clearable="false" />
</el-form-item>
</el-form>
<template #toolbar>
<el-button type="primary" icon="search" @click="listSumWorkloadHumanEffectSort">查询</el-button>
</template>
</MdpRptHeader>
<div class="row-box">
<div class="echart-box" :id="this.id"></div>
</div>
</template>
<script>
import util from '@/components/mdp-ui/js/util';//全局公共库
import { mapState } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import { listSumWorkloadHumanEffectSort } from '@/api/xm/core/xmWorkload';
import * as echarts from 'echarts';
export default {
components: {
},
props:['id','cfg','category','showToolBar','showParams','isRptCfg','rptDatas','xmProduct','xmProject','xmIteration','xmGroup','showTitle'/**true/false */],
computed: {
...mapState(useUserStore,[
'userInfo','roles'
]),
monthsCpd(){
if(this.rawDatas.length==0){
return []
}else{
return this.rawDatas.map(i=>i.bizDate)
}
},
heWorkloadCpd(){
if(this.rawDatas.length==0){
return []
}else{
return this.rawDatas.map(i=>i.heWorkload)
}
},
legendCpd(){
if (this.rawDatas.length == 0) {
return []
} else {
return this.rawDatas.map(i => i.name)
}
},
titleCpd(){
if(this.cfg?.title){
return this.cfg.title
}
var preName=""
if(this.iteration?.id){
preName=`迭代【${this.iteration.iterationName}`
}else if(this.project?.id){
preName=`项目【${this.project.name||this.project.id}`
}else if(this.group?.id){
preName=`小组【${this.group.name||this.group.id}`
}else if(this.product?.id){
preName=`产品【${this.product.productName||this.product.id}`
}else if(this.dept?.deptid){
preName=`部门【${this.dept.deptName||this.dept.deptid}`
}else{
preName="企业"
}
var groupName=this.groupBys.find(k=>k.id==this.params.groupBy).name
var midName='人效'
var dateTypeName=""
if(this.params.dateType=='days'){
dateTypeName="日"
}else if(this.params.dateType=='months'){
dateTypeName="月"
}else if(this.params.dateType=='years'){
dateTypeName="年"
}
if(this.user?.userid){
return preName+"【"+this.user.username+"】"+midName+"每"+dateTypeName+"按【"+groupName+"】分组排行榜"
}else{
return preName+midName+"每"+dateTypeName+"按【"+groupName+"】分组排行榜"
}
},
},
watch: {
rptDatas(val){
this.rawDatas=val
},
monthsCpd(){
this.$nextTick(()=>{
this.drawCharts();
})
}
},
data() {
return {
product:null,
project:null,
group:null,
iteration:null,
dept:null,
user:null,
params:{
dateType:'days',
startBizDate:'',
endBizDate:'',
projectId:'',
linkIterationId:'',
productId:'',
groupId:'',
deptid:'',
userid:'',
groupBy:'product_id',
},
title:'',//报表配置项
remark:'', //报表配置项
rawDatas:[],
groupBys: [
{ id: 'userid', name: '任务执行人员' },
{ id: 'product_id', name: '产品' },
{ id: 'project_id', name: '项目' },
{ id: 'deptid', name: '部门' },
],
}//end return
},//end data
methods: {
listSumWorkloadHumanEffectSort(){
if(this.rptDatas){
this.rawDatas=this.rptDatas
return;
}
var params={...this.params}
listSumWorkloadHumanEffectSort(params).then(res=>{
if(res.tips.isOk){
let list=res.data
list.sort(this.sort)
list.forEach(d=>{
d.heWorkload=this.$mdp.toFixed(d.heWorkload||0,2)
})
this.rawDatas=list
}else{
this.$message.error(res.tips.msg)
}
})
},
sort(x,y){
let x1=parseInt(x.heWorkload||0)
let y1=parseInt(y.heWorkload||0)
return y1-x1
},
open(){
debugger
this.product=this.xmProduct
this.project=this.xmProject
this.dept=null
this.group=this.xmGroup
this.iteration=this.xmIteration
this.params.dateType='days'
if( this.product && this.product.id){
this.params.productId= this.product.id
}
if( this.project && this.project.id){
this.params.projectId= this.project.id
}
if(this.group?.id){
this.params.groupId=this.group.id
}
if(this.iteration?.id){
this.params.linkIterationId=this.iteration.id
}
if(this.dept?.deptid){
this.params.deptid=this.dept.deptid
}
if(this.xmProduct?.id||this.xmProject?.id||this.xmIteration?.id||this.xmGroup?.id){
this.params.groupBy='userid'
}
if(this.cfg && this.cfg.id){
this.params=this.cfg.params
this.title=this.cfg.title
this.remark=this.cfg.remark
}
if(this.showToolBar && !this.title){
this.title=""
}
this.$nextTick(()=>{
this.listSumWorkloadHumanEffectSort();
})
},
drawCharts() {
this.myChart = echarts.init(document.getElementById(this.id));
this.myChart.setOption(
{
title: {
text: this.titleCpd,
left: 'center'
},
tooltip: {
trigger: 'axis',
},
barMaxWidth: 100,
toolbox: {
show: true,
top:"5%",
right:"10px",
feature: {
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar'] },
saveAsImage: { show: true }
}
},
calculable: true,
xAxis: {
type: 'category',
data: this.legendCpd
},
yAxis: {
type: 'value',
},
series: [
{
data: this.heWorkloadCpd,
type: 'bar',
label:{
show:true
}
}
]
}
)
},
onGroupChange(g){
if(!g.currNodeType=='group'){
this.$message.error("请选中小组节点")
return;
}
this.group=g
},
onGroupUserChange(g){
this.user=g
}
},//end method
mounted() {
this.params.startBizDate=this.$mdp.moment().startOf('year').format("YYYY-MM-DD")
this.params.endBizDate=this.$mdp.moment().endOf('year').format("YYYY-MM-DD")
this.open();
}//end mounted
}
</script>
<style scoped>
.image {
width: 100%;
display: block;
}
</style>

View File

@@ -0,0 +1,344 @@
<template>
<MdpRptHeader v-model:title="title" v-model:remark="remark" :isRptCfg="isRptCfg" :showParams="showParams" @delete="$emit('delete',cfg)">
<template #title v-if="showTitle===false">
<span></span>
</template>
<el-form :model="params" class="padding" ref="filtersRef">
<el-form-item label="成本中心" v-if="!(xmProduct?.id||xmProject?.id||xmIteration?.id||xmGroup?.id)">
<MdpSelectDept v-model="params.deptid"/>
</el-form-item>
<el-form-item label="归属项目" v-if="!xmProject?.id">
<XmProjectSelect width="100%" v-model="params.projectId" @change2="project=$event" :linkProductId="xmProduct?.id"/>
</el-form-item>
<el-form-item label="归属小组" v-if="!xmGroup?.id && project?.id">
<XmGroupUserSelect width="100%" :xm-product="product" :sel-project="project" @select="onGroupChange"/>
</el-form-item>
<el-form-item label="归属员工">
<XmUserSelect width="100%" v-model="params.userid" :xm-product="product" :sel-project="project" @change2="onGroupUserChange" @clear="user=null"/>
</el-form-item>
<el-form-item label="归属产品" v-if="!xmProduct?.id && !xmIteration?.id">
<XmProductSelect width="100%" v-model="params.productId" :linkProjectId="xmProject?.id" @change2="product=$event"/>
</el-form-item>
<el-form-item label="归属迭代" v-if="!xmIteration?.id && product?.id">
<XmIterationSelect width="100%" v-model="params.linkIterationId" :xm-product="product?.id" @change2="iteration=$event"/>
</el-form-item>
<el-form-item label="人效类型">
<el-radio-group v-model="params.dateType">
<el-radio label="days">日均人效</el-radio>
<el-radio label="months">月均人效</el-radio>
<el-radio label="years">年均人效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="起止时间">
<mdp-date-range start-key="startBizDate" end-key="endBizDate" type="daterange" v-model="params" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="统计时间" :clearable="false" />
</el-form-item>
</el-form>
<template #toolbar>
<el-button type="primary" icon="search" @click="listSumWorkloadHumanEffectTrend">查询</el-button>
</template>
</MdpRptHeader>
<div class="row-box">
<div class="echart-box" :id="this.id"></div>
</div>
</template>
<script>
import util from '@/components/mdp-ui/js/util';//全局公共库
import { mapState } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import { listSumWorkloadHumanEffectTrend } from '@/api/xm/core/xmWorkload';
import * as echarts from 'echarts';
export default {
components: {
},
props:['id','cfg','category','showToolBar','showParams','isRptCfg','rptDatas','xmProduct','xmProject','xmIteration','xmGroup','showTitle'/**true/false */,'dateType'/**days、months、years */],
computed: {
...mapState(useUserStore,[
'userInfo','roles'
]),
monthsCpd(){
if(this.rawDatas.length==0){
return []
}else{
return this.rawDatas.map(i=>i.bizDate)
}
},
heWorkloadCpd(){
if(this.rawDatas.length==0){
return []
}else{
return this.rawDatas.map(i=>i.heWorkload)
}
},
baseWeWorkloadCpd(){
if(this.rawDatas.length==0){
return []
}else{
let baseHeWorkload=this.calcBaseHeWorkload(this.params.dateType)
return this.rawDatas.map(i=>i.baseHeWorkload||baseHeWorkload)
}
},
legendCpd(){
return ['人均效能=工作量/总人数/周期','基准平均人效']
},
titleCpd(){
if(this.cfg?.title){
return this.cfg.title
}
var preName=""
if(this.iteration?.id){
preName=`迭代【${this.iteration.iterationName}`
}else if(this.project?.id){
preName=`项目【${this.project.name||this.project.id}`
}else if(this.group?.id){
preName=`小组【${this.group.name||this.group.id}`
}else if(this.product?.id){
preName=`产品【${this.product.productName||this.product.id}`
}else if(this.dept?.deptid){
preName=`部门【${this.dept.deptName||this.dept.deptid}`
}else{
preName="企业"
}
var midName='人效'
var dateTypeName=""
if(this.params.dateType=='days'){
dateTypeName="日"
}else if(this.params.dateType=='months'){
dateTypeName="月"
}else if(this.params.dateType=='years'){
dateTypeName="年"
}
if(this.user?.userid){
return preName+"【"+this.user.username+"】"+midName+"每"+dateTypeName+"趋势图"
}else{
return preName+midName+"每"+dateTypeName+"趋势图"
}
},
},
watch: {
rptDatas(val){
this.rawDatas=val
},
monthsCpd(){
this.$nextTick(()=>{
this.drawCharts();
})
}
},
data() {
return {
product:null,
project:null,
group:null,
iteration:null,
dept:null,
user:null,
params:{
dateType:'days',
startBizDate:'',
endBizDate:'',
projectId:'',
linkIterationId:'',
productId:'',
groupId:'',
deptid:'',
userid:'',
},
title:'',//报表配置项
remark:'', //报表配置项
rawDatas:[],
}//end return
},//end data
methods: {
calcBaseHeWorkload(dateType){
if(dateType=='days'){
return 8
}else if(dateType=='months'){
return 8*22
}else if(dateType=='years'){
return 8*360
}else{
return 8
}
},
listSumWorkloadHumanEffectTrend(){
if(this.rptDatas){
this.rawDatas=this.rptDatas
return;
}
var params={...this.params}
listSumWorkloadHumanEffectTrend(params).then(res=>{
if(res.tips.isOk){
let list=res.data
list.sort(this.sort)
list.forEach(d=>{
d.heWorkload=this.$mdp.toFixed(d.heWorkload||0,2)
d.baseHeWorkload=this.$mdp.toFixed(d.baseHeWorkload||0,2)
})
this.rawDatas=list
}else{
this.$message.error(res.tips.msg)
}
})
},
sort(x,y){
let x1=parseInt(x.bizDate.replace("-",""))
let y1=parseInt(y.bizDate.replace("-",""))
return x1-y1
},
open(){
debugger
this.product=this.xmProduct
this.project=this.xmProject
this.dept=null
this.group=this.xmGroup
this.iteration=this.xmIteration
this.params.dateType='days'
if( this.product && this.product.id){
this.params.productId= this.product.id
}
if( this.project && this.project.id){
this.params.projectId= this.project.id
}
if(this.group?.id){
this.params.groupId=this.group.id
}
if(this.iteration?.id){
this.params.linkIterationId=this.iteration.id
}
if(this.dept?.deptid){
this.params.deptid=this.dept.deptid
}
if(this.cfg && this.cfg.id){
this.params=this.cfg.params
this.title=this.cfg.title
this.remark=this.cfg.remark
}
if(this.showToolBar && !this.title){
this.title=""
}
this.$nextTick(()=>{
this.listSumWorkloadHumanEffectTrend();
})
},
drawCharts() {
this.myChart = echarts.init(document.getElementById(this.id));
this.myChart.setOption(
{
title: {
text: this.titleCpd,
left: 'center'
},
tooltip: {
trigger: 'axis',
},
barMaxWidth: 100,
toolbox: {
show: true,
top:"5%",
right:"10px",
feature: {
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar'] },
saveAsImage: { show: true }
}
},
calculable: true,
legend: {
bottom: 'bottom',
data: this.legendCpd
},
xAxis: {
type: 'category',
data: this.monthsCpd
},
yAxis: {
type: 'value',
},
series: [
{
name:'人均效能=工作量/总人数/周期',
data: this.heWorkloadCpd,
type: 'line',
smooth: true,
itemStyle: {
normal: {
// 折点颜色样式
color: 'blue',
lineStyle: {
// 折线颜色样式
color: 'blue'
}
}
},
},
{
name:'基准平均人效',
data: this.baseWeWorkloadCpd,
type: 'line',
smooth: true,
itemStyle: {
normal: {
// 折点颜色样式
color: 'orange',
lineStyle: {
// 折线颜色样式
color: 'orange'
}
}
},
},
]
}
)
},
onGroupChange(g){
if(!g.currNodeType=='group'){
this.$message.error("请选中小组节点")
return;
}
this.group=g
},
onGroupUserChange(g){
this.user=g
}
},//end method
mounted() {
this.params.startBizDate=this.$mdp.moment().startOf('year').format("YYYY-MM-DD")
this.params.endBizDate=this.$mdp.moment().endOf('year').format("YYYY-MM-DD")
this.open();
}//end mounted
}
</script>
<style scoped>
.image {
width: 100%;
display: block;
}
</style>