This commit is contained in:
2026-01-22 16:50:38 +08:00
3 changed files with 260 additions and 214 deletions

View File

@@ -0,0 +1,242 @@
<template>
<Dialog v-model="visible" diaTitle="工作负载详情" width="80%" height="70%" @close="closeFun">
<div class="table-content">
<div class="filter-box">
<el-radio-group v-model="radioData" @change="changeRadioDataFun">
<el-radio-button label="表格" value="table" />
<el-radio-button label="图表" value="chart" />
</el-radio-group>
</div>
<div class="content-inner" v-if="radioData === 'table'">
<BaseTable
:fullHeight="true"
ref="workloadTableRef"
:tableName="'PROJECT_TASK_WORKLOAD'"
:hidePagination="true"
>
<template #exeStatus="{ row }">
<StatusDot
:status="getTaskExeStyleClass(row.exeStatus)"
:title="
TASK_PROCESS_STATUS_OBJ[row.exeStatus as keyof typeof TASK_PROCESS_STATUS_OBJ]
"
/>
</template>
<template #achieveStatus="{ row }">
<StatusDot
:status="getTaskAchieveStyleClass(row.achieveStatus)"
:title="
TASK_CALCULATE_STATUS_OBJ[
row.achieveStatus as keyof typeof TASK_CALCULATE_STATUS_OBJ
] || '未分析'
"
/>
</template>
<template #progress="{ row }"> {{ row.progress || 0 }}% </template>
<template #approvalStatus="{ row }">
<StatusDot
class="clcik-approval"
:status="
getApproveStyleClass(row.approvalStatus || TASK_APPROVE_STATUS_ENUM.NOT_APPROVED)
"
:title="
TASK_APPROVE_STATUS.O[row.approvalStatus || TASK_APPROVE_STATUS_ENUM.NOT_APPROVED]
"
>
</StatusDot>
</template>
</BaseTable>
</div>
<div class="content-inner" v-else>
<EchartCard
title=""
ref="workloadChartRef"
:barType="'barChart'"
:chartsId="'workload-chart'"
@refresh="initWorkloadChartFun"
></EchartCard>
</div>
</div>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import Dialog from '@/components/common/dialog/index.vue';
import { listTaskWorkDaysApi } from '@/api/project/task';
import BaseTable from '@/components/common/table/baseTable.vue';
import {
getApproveStyleClass,
getTaskAchieveStyleClass,
getTaskExeStyleClass,
} from '@/components/common/statusDot/statusMap';
import {
TASK_APPROVE_STATUS_ENUM,
TASK_CALCULATE_STATUS_OBJ,
TASK_PROCESS_STATUS_OBJ,
} from '@/utils/enum/task';
import StatusDot from '@/components/common/statusDot/index.vue';
import { useDict } from '@/utils/useDict';
import EchartCard from '@/components/common/echartCard/index.vue';
const { TASK_APPROVE_STATUS } = useDict('TASK_APPROVE_STATUS');
const emit = defineEmits(['update:modelValue', 'confirm']);
interface Props {
modelValue: boolean;
list: any[];
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
list: () => [],
});
const visible = ref(false);
const tableData = ref<any>([]);
const radioData = ref('table');
const workloadTableRef = ref();
const workloadChartRef = ref();
const changeRadioDataFun = async () => {
if (radioData.value === 'table') {
nextTick(() => {
workloadTableRef.value.setDataFun(tableData.value);
});
} else {
await initWorkloadChartFun();
}
};
const getUserWorkLoadDataFun = async () => {
const taskIds = tableData.value.map((item: any) => {
return item.uuid;
});
const param = {
taskIds,
};
const xData: any = [];
const seriesData: any = [];
const datas: any = [];
const res: any = await listTaskWorkDaysApi(param);
if (res && res.code === 200) {
res.data.forEach((item: any) => {
xData.push(item.taskName);
datas.push(item.days);
});
seriesData.push({
type: 'bar',
barWidth: '30%',
// name: res.data[i].userName,
data: datas,
});
return {
xData,
seriesData,
};
} else {
return {
xData,
seriesData,
};
}
};
const initWorkloadChartFun = async () => {
const { xData, seriesData } = await getUserWorkLoadDataFun();
workloadChartRef.value.commonChartRef.disposeEchartsByKey('chart-1');
workloadChartRef.value.commonChartRef.option = {
title: {
show: false,
},
tooltip: {
trigger: 'axis',
},
legend: {
show: false,
top: '0%',
left: 'center',
},
grid: {
top: '10%',
bottom: '10%',
left: '5%',
right: '5%',
},
xAxis: {
type: 'category',
data: xData,
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function (value: any) {
return value + 'h';
},
},
},
dataZoom:
xData.length > 4
? [
{
type: 'slider',
show: true,
xAxisIndex: [0],
start: 0,
end: 100,
textStyle: {
color: 'transparent',
},
maxValueSpan: 4,
minValueSpan: 4,
moveHandleSize: 10,
height: 0,
filterMode: 'empty',
bottom: 15,
},
]
: null,
series: seriesData,
};
workloadChartRef.value.commonChartRef.initChart();
};
watch(
() => props.modelValue,
(val: boolean) => {
radioData.value = 'table';
tableData.value = [...props.list];
visible.value = val;
changeRadioDataFun();
}
);
const closeFun = () => {
visible.value = false;
tableData.value = [];
emit('update:modelValue', false);
};
</script>
<style lang="scss" scoped>
.table-content {
width: 100%;
height: 100%;
// height: 600px;
.filter-box {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.content-inner {
width: 100%;
height: calc(100% - 40px);
}
}
</style>

View File

@@ -87,66 +87,7 @@
</div>
</template>
</div>
<Dialog v-model="visible" diaTitle="工作负载详情" width="80%" height="70%" @close="closeFun">
<div class="table-content">
<div class="filter-box">
<el-radio-group v-model="radioData" @change="changeRadioDataFun">
<el-radio-button label="表格" value="table" />
<el-radio-button label="图表" value="chart" />
</el-radio-group>
</div>
<div class="content-inner" v-if="radioData === 'table'">
<BaseTable
:fullHeight="true"
ref="workloadTableRef"
:tableName="'PROJECT_TASK_WORKLOAD'"
:hidePagination="true"
>
<template #exeStatus="{ row }">
<StatusDot
:status="getTaskExeStyleClass(row.exeStatus)"
:title="
TASK_PROCESS_STATUS_OBJ[row.exeStatus as keyof typeof TASK_PROCESS_STATUS_OBJ]
"
/>
</template>
<template #achieveStatus="{ row }">
<StatusDot
:status="getTaskAchieveStyleClass(row.achieveStatus)"
:title="
TASK_CALCULATE_STATUS_OBJ[
row.achieveStatus as keyof typeof TASK_CALCULATE_STATUS_OBJ
] || '未分析'
"
/>
</template>
<template #progress="{ row }"> {{ row.progress || 0 }}% </template>
<template #approvalStatus="{ row }">
<StatusDot
class="clcik-approval"
:status="
getApproveStyleClass(row.approvalStatus || TASK_APPROVE_STATUS_ENUM.NOT_APPROVED)
"
:title="
TASK_APPROVE_STATUS.O[row.approvalStatus || TASK_APPROVE_STATUS_ENUM.NOT_APPROVED]
"
>
</StatusDot>
</template>
</BaseTable>
</div>
<div class="content-inner" v-else>
<EchartCard
title=""
ref="workloadChartRef"
:barType="'barChart'"
:chartsId="'workload-chart'"
@refresh="initWorkloadChartFun"
></EchartCard>
</div>
</div>
</Dialog>
<DetailDia v-model="visible" :list="tableData"></DetailDia>
</div>
</template>
@@ -159,24 +100,10 @@ import isBetween from 'dayjs/plugin/isBetween';
import isoWeek from 'dayjs/plugin/isoWeek';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import { userQueryGroupApi, userQueryGroupDetailApi } from '@/api/system/user';
import { getListUserWorkloadsApi, listTaskWorkDaysApi } from '@/api/project/task';
import { getListUserWorkloadsApi } from '@/api/project/task';
import { ElMessage } from 'element-plus';
import type { ChineseWorkday } from 'chinese-workday';
import Dialog from '@/components/common/dialog/index.vue';
import BaseTable from '@/components/common/table/baseTable.vue';
import {
getApproveStyleClass,
getTaskAchieveStyleClass,
getTaskExeStyleClass,
} from '@/components/common/statusDot/statusMap';
import {
TASK_APPROVE_STATUS_ENUM,
TASK_CALCULATE_STATUS_OBJ,
TASK_PROCESS_STATUS_OBJ,
} from '@/utils/enum/task';
import StatusDot from '@/components/common/statusDot/index.vue';
import { useDict } from '@/utils/useDict';
import EchartCard from '@/components/common/echartCard/index.vue';
import DetailDia from '@/views/task/workLoad/detailDia.vue';
const props = defineProps({
// 维度工作负载以人person为维度 人力负载以项目project为维度
@@ -200,6 +127,11 @@ const props = defineProps({
type: Boolean,
default: false,
},
// 字段映射方法
fieldMapFun: {
type: Function,
default: (data: any) => data,
},
api: {
type: Function,
default: getListUserWorkloadsApi,
@@ -210,8 +142,6 @@ dayjs.extend(isBetween);
dayjs.extend(weekOfYear);
dayjs.extend(isoWeek);
const { TASK_APPROVE_STATUS } = useDict('TASK_APPROVE_STATUS');
let Gantt: any = gantt;
const ganttEvents = ref<any>([]);
const colorList: string[] = ['#ffffff', '#d9ecff', '#c6e2ff', '#c6e2ff', '#79bbff', '#409eff']; // 格子颜色
@@ -220,9 +150,7 @@ const loading = ref<boolean>(false);
const workDaysRange = ref<number>(0);
const ww = ref<ChineseWorkday>();
const visible = ref(false);
const radioData = ref('table');
const workloadTableRef = ref();
const workloadChartRef = ref();
const filterFprmData = reactive<any>({
userGroupId: '',
userIds: [],
@@ -454,16 +382,8 @@ const getWorkLoadDataFun = async () => {
},
],
};
const dzh = res1.data.map((item: any) => {
return {
userName: item.projectName,
userId: item.projectId,
taskNum: item.taskNum,
workNum: item.personNum,
taskList: item.taskList,
};
});
res.data = dzh;
const list = props.fieldMapFun(res1.data);
res.data = list;
taskOriginData.value = res.data;
Gantt.config.start_date = filterFprmData.beginTime;
Gantt.config.end_date = filterFprmData.endTime;
@@ -542,12 +462,12 @@ const filterWorkLoadFun = async (flag?: any) => {
await refreshGanttFun();
};
const clearParamFun = async () => {
await getUserGroupFun();
filterFprmData.beginTime = dayjs().subtract(3, 'month').format('YYYY-MM-DD HH:mm:ss');
filterFprmData.endTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
await filterWorkLoadFun();
};
// const clearParamFun = async () => {
// await getUserGroupFun();
// filterFprmData.beginTime = dayjs().subtract(3, 'month').format('YYYY-MM-DD HH:mm:ss');
// filterFprmData.endTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
// await filterWorkLoadFun();
// };
// 计算时间段中的工作日天数
const calculateWorkDay = ([startDate, endDate]: any) => {
@@ -806,107 +726,6 @@ const monitorCellClickFun = () => {
return obj.currentIdTasksId.includes(item.id);
});
visible.value = true;
radioData.value = 'table';
nextTick(() => {
workloadTableRef.value.setDataFun(tableData.value);
});
};
}
};
const initWorkloadChartFun = async () => {
const { xData, seriesData } = await getUserWorkLoadDataFun();
workloadChartRef.value.commonChartRef.disposeEchartsByKey('chart-1');
workloadChartRef.value.commonChartRef.option = {
title: {
show: false,
},
tooltip: {
trigger: 'axis',
},
legend: {
show: false,
top: '0%',
left: 'center',
},
grid: {
top: '10%',
bottom: '10%',
left: '5%',
right: '5%',
},
xAxis: {
type: 'category',
data: xData,
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function (value: any) {
return value + 'h';
},
},
},
dataZoom:
xData.length > 4
? [
{
type: 'slider',
show: true,
xAxisIndex: [0],
start: 0,
end: 100,
textStyle: {
color: 'transparent',
},
maxValueSpan: 4,
minValueSpan: 4,
moveHandleSize: 10,
height: 0,
filterMode: 'empty',
bottom: 15,
},
]
: null,
series: seriesData,
};
workloadChartRef.value.commonChartRef.initChart();
};
const getUserWorkLoadDataFun = async () => {
const taskIds = tableData.value.map((item: any) => {
return item.uuid;
});
const param = {
taskIds,
};
const xData: any = [];
const seriesData: any = [];
const datas: any = [];
const res: any = await listTaskWorkDaysApi(param);
if (res && res.code === 200) {
res.data.forEach((item: any) => {
xData.push(item.taskName);
datas.push(item.days);
});
seriesData.push({
type: 'bar',
barWidth: '30%',
// name: res.data[i].userName,
data: datas,
});
return {
xData,
seriesData,
};
} else {
return {
xData,
seriesData,
};
}
};
@@ -918,21 +737,6 @@ const refreshGanttFun = async () => {
await getWorkLoadDataFun();
};
const closeFun = () => {
visible.value = false;
tableData.value = [];
};
const changeRadioDataFun = async () => {
if (radioData.value === 'table') {
nextTick(() => {
workloadTableRef.value.setDataFun(tableData.value);
});
} else {
await initWorkloadChartFun();
}
};
const focusUserFun = () => {
if (!filterFprmData.userGroupId) {
ElMessage.warning('请选择用户组后再选择用户进行筛选!');

View File

@@ -24,7 +24,7 @@
import { reactive } from 'vue';
import workLoad from '@/views/task/workLoad/index.vue';
import ProjectSelect from '@/components/common/projectSelect/index.vue';
import { getListUserWorkloadsApi, getListUserWorkloadsApi1 } from '@/api/project/task';
import { getListUserWorkloadsApi1 } from '@/api/project/task';
// 筛选条件:项目
const filterData = reactive<{