mirror of
https://gitee.com/maimengcloud/xm-ui-web.git
synced 2025-12-30 10:12:26 +00:00
甘特图
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"> {{ ' 红色字体代表关键路径上的节点' }}</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>
|
||||
|
||||
Reference in New Issue
Block a user