update:工作负载界面功能更新,修复了查找用户组成员接口调用bug

This commit is contained in:
2025-11-24 14:51:48 +08:00
parent 8eceafa526
commit c4ea3eced1
4 changed files with 546 additions and 3 deletions

View File

@@ -52,7 +52,9 @@
"vxe-pc-ui": "^4.9.13",
"vxe-table": "^4.16.0",
"xlsx": "^0.18.5",
"@antv/g6": "^4.8.24"
"@antv/g6": "^4.8.24",
"dhtmlx-gantt": "^8.0.6",
"chinese-workday": "^1.10.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",

View File

@@ -40,7 +40,7 @@ export const userQueryGroupApi = (params: any) => {
// 用户组详情
export const userQueryGroupDetailApi = (params: any) => {
return get(`${PREFIX}user/queryGroupDetail`, params);
return post(`${PREFIX}user/queryGroupDetail`, params);
};
// 更新用户组

View File

@@ -58,6 +58,8 @@ watch(() => props.modelValue, (val: boolean) => {
const getDetailFun = () => {
const params = {
id: props.id,
current: 1,
size: 999,
};
userQueryGroupDetailApi(params).then((res: any) => {
if (res.code === 200) {

View File

@@ -1,6 +1,545 @@
<template>
<div class="gl-page-content">工作负载</div>
<div class="gl-page-content">
<div class="page-filter-box">
<el-form :model="filterFprmData" inline>
<el-form-item label="用户组:">
<el-select v-model="filterFprmData.userGroupId" class="mw200" @change="filterWorkLoadFn">
<el-option v-for="item in groupList" :key="item.id" :label="item.groupName" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="开始时间:">
<el-date-picker
v-model="filterFprmData.beginTime"
type="datetime"
placeholder="请选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
@change="filterWorkLoadFn"
/>
</el-form-item>
<el-form-item label="结束时间:">
<el-date-picker
v-model="filterFprmData.endTime"
type="datetime"
placeholder="请选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
@change="filterWorkLoadFn"
/>
</el-form-item>
<el-form-item>
<el-radio-group v-model="filterFprmData.dateType" @change="changeDateTypeFn">
<el-radio value="month"></el-radio>
<el-radio value="week"></el-radio>
<el-radio value="day"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="mr0">
<el-button type="primary" @change="filterWorkLoadFn">查询</el-button>
<el-button type="" @click="clearParamFn">清空</el-button>
</el-form-item>
</el-form>
</div>
<div class="page-content">
<div ref="ganttRef" class="gant-page" v-loading="loading"></div>
</div>
<div class="page-footer">
<template v-for="(item, index) in colorList" :key="index">
<div class="color-item" v-if="index > 0">
<div :style="{ background: item }"></div>
<div>{{ index >= 5 ? '大于或等于5个任务' : index + '个任务' }}</div>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, onBeforeUnmount } from 'vue';
import { gantt } from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import dayjs from 'dayjs';
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 } from '@/api/project/task';
import { ElMessage } from 'element-plus';
import { isWorkday } from 'chinese-workday';
dayjs.extend(isBetween);
dayjs.extend(weekOfYear);
dayjs.extend(isoWeek);
let Gantt: any = gantt;
const ganttEvents = ref<any>([]);
const colorList: string[] = ['#ffffff', '#fff6e6', '#ffdca3', '#ff9729', '#ff7d00', '#d96200']; // 格子颜色
const taskOriginData = ref<any>([]); // 所有用户的任务数据
const loading = ref<boolean>(false);
const workDaysRange = ref<number>(0);
const filterFprmData = reactive<any>({
userGroupId: '',
userIds: [],
beginTime: '',
endTime: '',
dateType: 'week',
});
const ganttRef = ref();
const changeDateTypeFn = () => {
loading.value = true;
// 加载动画
setTimeout(() => {
setUserDayWork(filterFprmData.beginTime, filterFprmData.endTime);
Gantt.ext.zoom.setLevel(filterFprmData.dateType);
loading.value = false;
}, 200);
};
const groupList = ref<any>([]);
const getUserGroupFn = async () => {
const res: any = await userQueryGroupApi({
current: 1,
size: 999,
});
if (res && res.code === 200) {
groupList.value = res.data.data;
if (groupList.value?.length) {
filterFprmData.userGroupId = groupList.value[0].id;
filterFprmData.userIds = await queryGroupUserListFn(filterFprmData.userGroupId);
}
}
};
// 获取用户组内成员
const queryGroupUserListFn = async (id: any) => {
const res: any = await userQueryGroupDetailApi({
id,
current: 1,
size: 999,
});
if (res && res.code === 200) {
const usrids = res.data.users.data.map((item: any) => {
return item.userId;
});
return usrids;
} else {
return [];
}
};
const getWorkLoadDataFn = async () => {
if (!filterFprmData.userIds.length) {
return;
}
loading.value = true;
Gantt.clearAll();
taskOriginData.value = [];
const res: any = await getListUserWorkloadsApi({
...filterFprmData,
});
if (res && res.code === 200) {
taskOriginData.value = res.data;
Gantt.config.start_date = filterFprmData.beginTime;
Gantt.config.end_date = filterFprmData.endTime;
Gantt.ext.zoom.setLevel(filterFprmData.dateType);
if (res.data.length > 0) {
calculateWorkDay([filterFprmData.beginTime, filterFprmData.endTime]);
formatUserTask(res.data, [filterFprmData.beginTime, filterFprmData.endTime]);
}
setUserDayWork(filterFprmData.beginTime, filterFprmData.endTime);
nextTick(() => {
Gantt.render();
});
loading.value = false;
}
};
const formatUserTask = (data: any, time: any) => {
const ganttData = data.map((item: any, index: any) => {
let jobSaturation = 0;
if (item.taskList?.length > 0) {
item.taskList.forEach((item_1: any) => {
jobSaturation = jobSaturation + workDaysRange.value * item_1.workRate;
});
}
return {
id: index + 1,
userName: item.userName,
userId: item.userId,
taskNum: item.taskNum ? item.taskNum : 0,
jobSaturation: jobSaturation.toFixed(2),
start_date: time[0],
duration: dayjs(time[1]).diff(dayjs(time[0]), 'day'),
};
});
Gantt.clearAll();
Gantt.parse({
data: ganttData,
});
};
// 获取用户工作负载信息
const filterWorkLoadFn = async () => {
// 判断开始时间和截止时间
const begin = new Date(filterFprmData.beginTime).getTime();
const end = new Date(filterFprmData.endTime).getTime();
if (end < begin) {
ElMessage.warning('结束时间无法小于开始时间!');
filterFprmData.endTime = dayjs(new Date(filterFprmData.beginTime)).add(3, 'months').format('YYYY-MM-DD HH:mm:ss');
}
filterFprmData.userIds = await queryGroupUserListFn(filterFprmData.userGroupId);
await refreshGanttFn();
};
const clearParamFn = async () => {
await getUserGroupFn();
filterFprmData.beginTime = dayjs().subtract(3, 'month').format('YYYY-MM-DD HH:mm:ss');
filterFprmData.endTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
await filterWorkLoadFn();
};
// 计算时间段中的工作日天数
const calculateWorkDay = ([startDate, endDate]: any) => {
let workdays = 0;
let currentDate = dayjs(startDate);
while (currentDate.isBefore(dayjs(endDate)) || currentDate.isSame(dayjs(endDate), 'day')) {
if (isWorkday(currentDate)) {
workdays++;
}
currentDate = currentDate.add(1, 'day');
}
workDaysRange.value = workdays;
};
// 生成日期列表的函数
const generateDateList = (startDate: any, endDate: any, mode = 'day') => {
const result = [];
let formatStart = null;
let formatEnd = null;
if (mode === 'day') {
formatStart = dayjs(startDate);
formatEnd = dayjs(endDate);
} else if (mode === 'week') {
formatStart = dayjs(startDate).startOf('isoWeek');
formatEnd = dayjs(endDate).startOf('isoWeek');
} else if (mode === 'month') {
formatStart = dayjs(startDate).startOf('month');
formatEnd = dayjs(endDate).startOf('month');
}
if (!formatStart || !formatEnd) {
return [];
}
// 根据模式生成不同的日期列表
while (formatStart.isBefore(endDate) || formatStart.isSame(formatEnd)) {
result.push(formatStart.format('YYYY-MM-DD')); // 将日期加入结果列表
if (mode === 'day') {
formatStart = formatStart.add(1, 'day'); // 每天
} else if (mode === 'week') {
formatStart = formatStart.startOf('isoWeek').add(1, 'week'); // 每周第一天(星期一)
} else if (mode === 'month') {
formatStart = formatStart.startOf('month').add(1, 'month'); // 每月第一天
}
}
return result;
};
// 每个用户gantt单元格的任务
const userTimeItemWorkList: any = new Map();
const setUserDayWork: any = (startDate: any, endDate: any) => {
userTimeItemWorkList.clear();
const dateList = generateDateList(startDate, endDate, filterFprmData.dateType);
taskOriginData.value.forEach((item: any) => {
const dateWorkMap: any = new Map();
dateList.forEach((item_1: any) => {
const tasks = filterUserTask(item.taskList, item_1);
dateWorkMap.set(item_1, tasks);
});
userTimeItemWorkList.set(item.userName, dateWorkMap);
});
};
// 筛选某一用户在某一周或者某月的任务
const filterUserTask = (taskList: any, date: any) => {
let filterTask: any = [];
filterTask = taskList
?.filter((item: any) => {
const startTime = dayjs(item.beginTime);
const endTime = startTime.add(item.days - 1, 'day');
const weekOrMonthStart = dayjs(date);
const addDays = filterFprmData.dateType === 'day' ? 0 : filterFprmData.dateType === 'week' ? 6 : weekOrMonthStart.daysInMonth() - 1;
const weekOrMonthEnd = weekOrMonthStart.add(addDays, 'day');
// 判断当前任务的时间段是否在这周
// 开始时间在这周||结束时间在这周||这周整个在任务的时间段中
return (
startTime.isBetween(weekOrMonthStart, weekOrMonthEnd, 'day', '[]') ||
endTime.isBetween(weekOrMonthStart, weekOrMonthEnd, 'day', '[]') ||
(weekOrMonthStart.isBetween(startTime, endTime, 'day', '[]') &&
weekOrMonthEnd.isBetween(startTime, endTime, 'day', '[]'))
);
})
?.map((item: any) => item.id);
return filterTask;
};
const initGantt = () => {
const zoomConfig: any = {
levels: [
{
name: 'day',
scales: [
{ unit: 'year', step: 1, format: '%Y' },
{ unit: 'month', step: 1, format: '%M' },
{ unit: 'week', step: 1, format: '%W周' },
{ unit: 'day', format: '%d' },
],
},
{
name: 'week',
min_column_width: 50,
scales: [
{ unit: 'year', step: 1, format: '%Y' },
{ unit: 'month', step: 1, format: '%M' },
{ unit: 'week', step: 1, format: '%W周' },
],
},
{
name: 'month',
scales: [
{ unit: 'year', step: 1, format: '%Y' },
{ unit: 'month', step: 1, format: '%M' },
],
},
],
};
Gantt.ext.zoom.init(zoomConfig);
Gantt.ext.zoom.setLevel('week');
Gantt.config.autosize = false;
Gantt.config.show_grid = true;
// 只读模式:打开后不可以操作甘特图
Gantt.config.readonly = true;
// 设置空数据
Gantt.config.show_empty_state = false;
Gantt.config.fit_tasks = true;
Gantt.config.min_column_width = 30;
Gantt.config.date_format = '%Y-%m-%d';
Gantt.config.scale_height = 28 * 3;
Gantt.config.select_task = false;
Gantt.i18n.setLocale('cn');
Gantt.config.columns = [
{
name: 'userName',
label: '姓名',
width: 200,
align: 'left',
onrender: (task: any, node: any) => {
console.log(node);
return `<img src="" style="width:15px;height:15px;margin-right:12px"></img>${task.userName}`;
},
},
{
name: 'taskNum', label: '任务数量', align: 'left', width: 80,
onrender: (task: any, node: any) => {
console.log(node);
return `${task.taskNum}`;
},
},
];
Gantt.templates.timeline_cell_content = function (task: any, date: any) {
let tasks: any = [];
if (task.userName && userTimeItemWorkList && userTimeItemWorkList.has(task.userName)) {
const currentNameMap = userTimeItemWorkList.get(task.userName);
const dateFormat = dayjs(date).format('YYYY-MM-DD');
if (currentNameMap && currentNameMap.has(dateFormat)) {
tasks = currentNameMap.get(dateFormat);
}
}
return `<div class='${tasks?.length > 0 ? 'cell-style' : ''}' style='background:${colorList[tasks?.length >= 5 ? 5 : tasks?.length]
};cursor:${tasks?.length > 0 ? 'pointer' : 'default'}' userName=${task?.userName} userId=${task?.userId
} tasks=${JSON.stringify(tasks)} date=${dayjs(date).format('YYYY-MM-DD')}></div>`;
};
// gantt渲染结束添加监听单元格点击事件
const onGanttRender = Gantt.attachEvent('onGanttRender', function (e:any, date:any) {
console.log(e, 'e');
console.log(date, 'date');
nextTick(() => {
monitorCellClickFn();
});
});
ganttEvents.value.push(onGanttRender);
const onLoadStart = Gantt.attachEvent('onGanttScroll', function (e:any, date:any) {
console.log(e, 'e');
console.log(date, 'date');
nextTick(() => {
monitorCellClickFn();
});
});
ganttEvents.value.push(onLoadStart);
Gantt.init(ganttRef.value as HTMLElement);
Gantt.parse({
data: [],
});
};
const monitorCellClickFn = () => {
};
const refreshGanttFn = async () => {
Gantt.clearAll();
initGantt();
await getWorkLoadDataFn();
};
onMounted(async () => {
await getUserGroupFn();
filterFprmData.beginTime = dayjs().subtract(3, 'month').format('YYYY-MM-DD HH:mm:ss');
filterFprmData.endTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
await refreshGanttFn();
console.log(filterFprmData, 'filterFprmData');
});
onBeforeUnmount(() => {
ganttEvents.value?.forEach((item: any) => {
Gantt.detachEvent(item);
});
Gantt.templates.timeline_cell_content = function (task: any, date: any) {
console.log(task);
console.log(date);
return '';
};
Gantt.clearAll();
Gantt = null;
});
</script>
<style lang="scss" scoped>
.gl-page-content {
width: 100%;
height: 100%;
.page-filter-box {
width: 100%;
height: 50px;
display: flex;
align-items: center;
.mw200 {
width: 200px;
}
.el-form {
width: 100%;
height: 100%;
display: flex;
justify-content: flex-end;
}
.mr0 {
margin-right: 0px;
}
}
.page-content {
width: 100%;
height: calc(100% - 80px);
.gant-page {
width: 100%;
height: 100%;
}
}
.page-footer {
height: 30px;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
.color-item {
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: 12px;
&>div:first-child {
height: 16px;
width: 16px;
border-radius: 50%;
margin-right: 3px;
}
}
}
}
:deep(.cell-style) {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.gantt_links_area) {
display: none;
}
:deep(.gantt_bars_area) {
display: none;
}
:deep(.gantt_tree_icon.gantt_file) {
background-image: url('~@/assets/imgs/versionTree/user.svg') !important;
background-size: 60%;
}
:deep(
.gantt_grid_data .gantt_row.gantt_selected,
.gantt_grid_data .gantt_row.odd.gantt_selected,
.gantt_task_row.gantt_selected
) {
background-color: #96bdff;
&:hover {
background-color: #96bdff !important;
}
}
:deep(.gantt_grid_data .gantt_row.odd:hover, .gantt_grid_data .gantt_row:hover) {
background-color: #ecf7ff;
}
:deep(.gantt_grid_data .gantt_row:hover) {
background-color: #ecf7ff;
}
:deep(.task-table-container) {
height: 100%;
width: 100%;
}
:deep(.gantt_row.gantt_row_task) {
position: none;
}
</style>