甘特图

This commit is contained in:
陈裕财
2025-03-30 04:46:59 +08:00
parent 8e1ed4f22a
commit 620ccc57df
4 changed files with 256 additions and 16 deletions

View File

@@ -72,6 +72,7 @@
"pizzip": "^3.1.7",
"qrcode": "^1.5.4",
"qs": "^6.13.0",
"screenfull": "^6.0.2",
"steady-xml": "^0.1.0",
"url": "^0.11.4",
"v-region": "^3.1.0",

View File

@@ -119,12 +119,16 @@
<el-menu-item :index="'/'">
<template #title><i class="s-home"></i>首页</template>
</el-menu-item>
<el-menu-item>
<Screenfull class="custom-hover" color="var(--top-header-text-color)"/>
</el-menu-item>
</el-menu>
</template>
<script>
import { Screenfull } from '@/layout/components/Screenfull'
import { mapState } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import { useXmStore } from '@/store/modules/xm'
@@ -156,7 +160,7 @@ export default {
}, //end methods
components: {
Screenfull
//在下面添加其它组件
},
mounted() {

View File

@@ -43,7 +43,7 @@
<el-button v-if="isTaskCenter != '1' && isMy != '1'" type="primary" icon="plus" :title="'新建'+name" plain/>
</template>
</el-popover>
<el-button @click="showGanttChange" title="再次点击隐藏甘特图;在甘特图任意位置右键可以弹出工具条" type="warning" plain></el-button>
<el-button @click="showGanttChange" title="再次点击隐藏甘特图;连续点击可以刷新甘特图;在此按钮上点击鼠标右键可以弹出甘特图配置工具条" type="warning" plain ref="ganttBtn"></el-button>
<el-button @click="$refs['kanbanDlg'].open()" title="看板" type="success" plain></el-button>
<el-button title="根据项目类型初始化项目的阶段计划" v-if="queryScope=='plan'" type="warning" @click="initProjectPhasePlans" plain>初始化</el-button>
@@ -335,7 +335,7 @@
</el-table>
</template>
<template #default>
<WbsGantt :gantt-id="'task'+(deriveId||'deriveId')" @dateChange="(t,start,end,callback)=>onGanttDateChange(t,start,end,callback)" v-if="ganttVisible&&tableDatas?.length>0" @show-left="expand=$event" :tasks="tableDatas" :columnMap="{dependencies:'preTaskid',start:'startTime',end:'endTime',progress:'initRate',hours:'initWorkload'}" :ganttCfg="{header_height:40,bar_height: 24}"
<WbsGantt :ganttBtn="$refs['ganttBtn']" ref="ganttRef" :gantt-id="'task'+(deriveId||'deriveId')" @dateChange="(t,start,end,callback)=>onGanttDateChange(t,start,end,callback)" v-if="ganttVisible&&tableDatas?.length>0" @show-left="expand=$event" :tasks="tableDatas" :columnMap="{dependencies:'preTaskid',start:'startTime',end:'endTime',progress:'initRate',hours:'initWorkload'}" :ganttCfg="{header_height:40,bar_height: 24}"
@barClick="(t,cb)=>showEditForCallback(t,callback)"
/>
</template>
@@ -549,18 +549,19 @@ import {
} from "@/api/xm/core/xmTask";
import { mapState } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import { useUserStore } from '@/store/modules/user'
import { sn } from "@/components/mdp-ui/js/sequence";
import { addXmTaskExecuser } from '@/api/xm/core/xmTaskExecuser';
import { defineAsyncComponent } from 'vue';
import { MdpTableMixin } from '@/components/mdp-ui/mixin/MdpTableMixin.js'
import InitPhaseVue from '../components/InitPhase.vue';
import InitPhaseVue from '../components/InitPhase.vue';
import { useFullscreen } from '@vueuse/core'
export default {
mixins: [MdpTableMixin],
computed: {
...mapState(useUserStore, ["userInfo", "roles"]),
...mapState(useUserStore, ["userInfo", "roles"]),
isFullscreen(){ return useFullscreen().isFullscreen},
currentProject() {
if (this.project) {
return this.project;
@@ -655,6 +656,7 @@ export default {
},
data() {
return {
currOpType: 'mng',
product:null,
project:null,
@@ -1541,6 +1543,15 @@ export default {
onInit(){
this.$refs['initProjectPhasePlanDialog'].close()
this.refresh();
},
afterList(tableDatas){
if(this.ganttVisible){
this.ganttVisible=false
setTimeout(() => {
this.ganttVisible=true
}, 1);
}
}
/**end 自定义函数请在上面加**/
}, //end methods

View File

@@ -1,7 +1,58 @@
<template>
<!-- 右侧甘特图 -->
<div class="gantt-section" ref="ganttContainer"></div>
<div class="gantt-section" ref="ganttContainer"></div>
<div>
<el-popover teleported offset="-100"
ref="popoverRef"
:virtual-ref="ganttBtn"
trigger="contextmenu"
title="甘特图工具条 (点【甘】按钮刷新甘特图)"
virtual-triggering
placement="top-end"
width="800px"
>
<el-form>
<el-form-item label="时间单位">
<el-space wrap>
<el-radio-group v-model="viewMode">
<el-radio @click="changeViewMode('Day')" label="Day"></el-radio>
<el-radio @click="changeViewMode('Week')" label="Week"></el-radio>
<el-radio @click="changeViewMode('Month')" label="Month"></el-radio>
<el-radio @click="changeViewMode('Year')" label="Year"></el-radio>
</el-radio-group>
<el-checkbox v-model="listVisible" :true-label="true" :false-label="false">显示列表</el-checkbox>
<el-button @click="refreshGantt()" type="primary" plain>刷新甘特图</el-button>
</el-space>
</el-form-item>
<el-form-item label="关键路径">
<el-space wrap>
<el-button @click="calcKeyPathTasks()" type="primary" plain>计算关键路径</el-button>
<el-button @click="$refs['keyPathsDlg'].open()" type="warning" plain>任务列表</el-button>
</el-space>
<el-text type="danger"> &nbsp;{{ ' 红色字体代表关键路径上的节点' }}</el-text>
<el-text title="甘特图操作说明" type="warning" >1.避免循环依赖2.双击任务编辑3.按钮刷新甘特图</el-text>
</el-form-item>
</el-form>
</el-popover>
<MdpDialog ref="keyPathsDlg" title="关键路径上的任务列表" width="80%">
<el-table :data="keyPathTasks">
<el-table-column prop="name" label="任务名称">
<template #default="{row,$index}">
<el-link @click="showEdit(row)">
{{ ($index+1) }}{{ row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="preTaskids" label="前置编号"/>
<el-table-column prop="id" label="任务编号"/>
</el-table>
</MdpDialog>
</div>
</template>
<script setup lang="ts">
@@ -19,7 +70,8 @@ const emitss = defineEmits(['dateChange','progressChange','barClick','viewChange
const props = defineProps<{
tasks: [],
columnMap: ColumnMap,
ganttCfg?: GanttCfg
ganttCfg?: GanttCfg,
ganttBtn?:any
}>()
// Gantt 实例管理
@@ -30,7 +82,7 @@ const state = reactive<{tasks:Task[]}>({
tasks: [ ],
})
let viewMode=ref('Day')
const allColumnMap=ref<ColumnMap>({
id: 'id',
name: 'name',
@@ -66,13 +118,14 @@ const initGantt = () => {
date_format: "YYYY-MM-DD", // 日期格式
popup_on: 'hover',//鼠标进入显示任务信息
on_click: (task) => handleTaskClick(task),
on_date_change: (task, start, end,$event) => handleDateChange(task, start, end,$event),
on_date_change: (task, start, end) => handleDateChange(task, start, end),
custom_popup_html: (task)=>custom_popup_html(task),
on_double_click: (task) => showEdit(task),
...props.ganttCfg
})
} catch (error) {
console.error('初始化失败:', error);
ElNotification.success({
ElNotification.error({
title: 'error',
message: '甘特图初始化失败请检查数据是否正确。规则1.不允许有循环依赖2.起止日期不能为空,且结束日期不能比开始日期小。',
showClose: true,
@@ -81,7 +134,19 @@ const initGantt = () => {
}
}
const popoverRef = ref()
const onClickOutside = () => {
unref(popoverRef).popperRef?.delayHide?.()
}
const changeViewMode = (mode) => {
viewMode.value=mode
ganttInstance.change_view_mode(mode)
emitss('viewChange',mode)
};
const dataChangeEventList=[]
// 任务折叠/展开
@@ -203,10 +268,169 @@ const formatTasks = (taskList) => {
return allTasks;
}
const showEdit=(row)=>{
let task=props.tasks.find(k=>k[allColumnMap.value.id]==row.id)
let callback=(d)=>{
Object.assign(task,d)
}
emitss('barClick',task,callback)
}
const calcOkTasks = ()=>{
let minStartDate=util.initMoment('2020-01-01 00:00:00')
// 计算每个节点到上一个节点的最短工期、最长工期
let idMap=new Map()
state.tasks.forEach(k=>idMap.set(k.id,{...k}))
// 计算一遍每个节点的自身工期,不计算依赖关系
state.tasks.forEach(t=>{
let startHours=util.initMoment(t.start).diff(minStartDate,'hours')
idMap.get(t.id).endHours=util.initMoment(t.end).diff(minStartDate,'hours')
idMap.get(t.id).startHours=startHours
let hours=util.initMoment(t.end).diff(util.initMoment(t.start),'hours')
idMap.get(t.id).hours=idMap.get(t.id).hours||hours
})
state.tasks.forEach(t=>{
if(t.dependencies){
let deps=[]
t.dependencies.forEach(d=>{
if(idMap.has(d)){
deps.push(d)
}
})
idMap.get(t.id).preTaskids=deps
}else{
idMap.get(t.id).preTaskids=[]
}
})
return Array.from(idMap.values())
}
const calculateCriticalPath=(activities)=>{
let earliestStart = {};
let latestEnd = {};
let earliestEnd = {};
// 初始化最早开始时间和最晚结束时间
activities.forEach(activity => {
earliestStart[activity.id] = activity.startHours;
latestEnd[activity.id] = Math.max(activity.startHours+activity.hours,activity.endHours);
earliestEnd[activity.id] =activity.startHours+activity.hours;
});
// 迭代计算最早开始时间和最晚结束时间 需要用深度优先算法
var idMap=new Map();
// 1 先构建一颗树
activities.forEach(a=>{
let n={...a}
n.children=[]
idMap.set(a.id,n)
})
activities.forEach(a=>{
if(a.preTaskids){
a.preTaskids.forEach(pid=>{
if(idMap.has(pid)){
let n=idMap.get(pid)
n.children.push(a.id)
}
})
}
})
// 2 进行深度优先算法
let nodeList=Array.from(idMap.values())
function dfs(n){
if(n.children && n.children.length>0){
n.children.forEach(cid=>{
let cn=idMap.get(cid)
cn.pidd=n.id
earliestStart[cn.id] = Math.max(earliestStart[cn.id], latestEnd[n.id]);
earliestEnd[cn.id] = earliestStart[cn.id]+cn.hours
latestEnd[cn.id] = Math.max(earliestEnd[cn.id] + cn.hours,cn.endHours);
if(cn.children && cn.children.length>0){
dfs(cn)
}
})
}
}
nodeList.forEach(n=>{
dfs(n)
})
// 收集关键路径上的活动
let criticalPath = [];
let allPaths=new Set()//多条线路 <root.id,[n1,n2,n3,..]>
function dfs2(n,paths){
let npaths=paths?(paths+","+n.id):n.id
allPaths.add(npaths)
if(n.children && n.children.length>0){
n.children.forEach(c=>dfs2(idMap.get(c),npaths))
}
}
// 找出所有没有上级的节点,根节点
let rootList=nodeList.filter(n=>!n.pidd)
rootList.forEach(n=>{
dfs2(n,null)
})
// 计算最长的工期的路径即为关键路径
let maxHoursNode=null;
let maxPaths=null
allPaths.forEach(keyPaths=>{
var lastIndex = keyPaths.lastIndexOf(',');
let lastNid=keyPaths;
if (lastIndex !== -1) {
lastNid= keyPaths.substring(lastIndex + 1);
}
let node=idMap.get(lastNid)
if(maxHoursNode==null){
maxHoursNode=node
maxPaths=keyPaths
}else{
if(latestEnd[lastNid]>latestEnd[maxHoursNode.id]){
maxHoursNode=node
maxPaths=keyPaths
}
}
})
maxPaths.split(",").forEach(id=>{
criticalPath.push(idMap.get(id))
})
return criticalPath;
}
const keyPathTasks=ref([])
const calcKeyPathTasks=()=>{
keyPathTasks.value=calculateCriticalPath(calcOkTasks())
highlightElement('.gantt-target.'+(props.ganttId||'default')+' .bar-wrapper',keyPathTasks.value.map(k=>k.id))
ElNotification.success({
title: '计算成功',
message: '计算成功!甘特图红色字体的即为关键路径上的任务',
showClose: true,
})
}
// 高亮具有特定data-id的元素
const highlightElement = (selector, ids) => {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
if (ids.some(id=>element.getAttribute('data-id') === id)) {
element.classList.add('hight-light');
} else {
element.classList.remove('hight-light');
}
});
}
let listVisible=ref(true)
// 生命周期
onMounted(initGantt)
watch(() => props.tasks, ()=>{state.tasks=formatTasks(props.tasks);}, { deep: false })
//watch(() => state.tasks, refreshGantt, { deep: true })
watch(() => props.tasks, ()=>{state.tasks=formatTasks(props.tasks);refreshGantt();}, { deep: true })
// watch(() => state.tasks, refreshGantt, { deep: true })
watch(()=>listVisible.value,(v)=>{ emitss('showLeft',v) })
</script>
<style scoped>