vue3中,项目甘特图实现项目进度条——封装组件之tabs标签页的使用、定义方法实现动态样式style、reduce方法、数组每个下标对应的当前下标前几位之和、new Date()转换日期格式
效果图
1.1、预警阈值
1.2、
2、项目进度甘特图
需求:
1、展示项目各个节点的执行进度情况
2、只有项目节点实施开始日期,没有百分比或节点实施结束日期,则只标识出起点位置即可
3、根据项目进度或完成情况的百分比或结束日期,展示不同的颜色进行预警
4、结束日期或百分比的打点位置超出项目计划结束日期进行截取至项目计划日期打点位置
代码
1、主页代码
index.vue
<!--
@Description 项目中心 - 项目基本情况
@author wdd
@date 2023/11/11
-->
<template>
<centerHead title="项目基本情况"></centerHead>
<tab :tabList="tabList" :defaultState="tabIndex" @changeTab="tabChange"></tab>
<ProjectBasicInfo v-show="tabIndex === '0'" />
<ProjectSchedule v-show="tabIndex === '1'" />
</template>
<script lang="ts" setup>
import ProjectBasicInfo from './components/projectBasicInfo.vue'
import ProjectSchedule from './components/projectSchedule.vue'
import {ref} from 'vue'
const tabList = ref([
{state: "0", name: '基本情况'},
{state: "1", name: '项目进度表'},
]);
const tabIndex = ref('0');
// tab切换
const tabChange = (val:any) => {
tabIndex.value = val;
}
</script>
2、标签页代码
src\views\myCenter\projectCenter\components\projectSchedule.vue
<!--
@Description 项目中心 - 项目管理 - 项目进度表
@author wdd
@date 2023/11/15
-->
<template>
<div class="content">
<div class="right">
<div class="left">
<div><span class="type-size">项目名称:</span> <span>{{type}}</span></div>
<div class="type-size">里程碑节点:</div>
</div>
<el-table :header-cell-style="{ background: '#eef1f6' }" ref="tableRef" :data="nodeList" class="table">
<el-table-column type="index" width="55" label="序号"></el-table-column>
<el-table-column prop="description" align="center" label="项目节点"></el-table-column>
<el-table-column prop="startDate" align="center" label="计划开始日期" width="150"></el-table-column>
<el-table-column prop="endDate" align="center" label="计划结束日期" width="150"></el-table-column>
<el-table-column prop="realityStartDate" align="center" label="实际开始日期" width="150"></el-table-column>
<el-table-column prop="realityEndDate" align="center" label="实际结束日期" width="150">
<!-- <template #default="scope">
<el-date-picker v-model="scope.row.realityEndDate" :editable="false" format="YYYY-MM-DD"
placeholder="选择日期" disabled type="date" value-format="YYYY-MM-DD" style="width:100%" clearable>
</el-date-picker>
</template> -->
</el-table-column>
<el-table-column prop="schedule" align="center" label="节点进度(%)" width="122"></el-table-column>
<el-table-column align="center" label="风险提示(天数)" width="150">
<template #default="scope">
<div v-if="scope.row.realityEndDate"
:style="{width:'120px',height:'24px',fontSize:'14px',backgroundColor:colorVal(datePro(scope.row.realityEndDate,scope.row.endDate).value),color:'#fff'}">
{{datePro(scope.row.realityEndDate,scope.row.endDate).text}}</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="notes" align="center" label="备注" width="122"></el-table-column>
</el-table>
</div>
<div class="type-size">项目甘特图进度表:</div>
<ScheduleTable :dateList="nodeList" class="box"></ScheduleTable>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import ScheduleTable from './scheduleTable.vue'
const type = ref('项目XXXXX')
const nodeList = ref([{
id: 1,
description: '节点一',
editType: true,
startDate: '2023-01-05',
endDate: '2023-02-05',
realityStartDate: '2023-01-20',
realityEndDate: '',
schedule: '90',
notes: '无'
}, {
id: 2,
description: '节点二',
editType: true,
startDate: '2023-02-05',
endDate: '2023-05-05',
realityStartDate: '2023-03-05',
realityEndDate: '',
schedule: '40',
notes: '无'
}, {
id: 3,
description: '节点三',
editType: true,
startDate: '2023-05-05',
endDate: '2023-10-05',
realityStartDate: '2023-05-05',
realityEndDate: '2023-11-04',
schedule: '75',
notes: '无'
}, {
id: 4,
description: '节点四',
editType: true,
startDate: '2023-10-05',
endDate: '2023-12-05',
realityStartDate: '2023-10-10',
realityEndDate: '',
schedule: '',
notes: '无'
}, {
id: 5,
description: '节点五',
editType: true,
startDate: '2023-12-05',
endDate: '2023-12-29',
realityStartDate: '2023-12-05',
realityEndDate: '2024-12-30',
schedule: '35',
notes: '无'
}])
// 计算日期天数差值 示例:('2023-02-04','2023-02-07') ==> 3
const datePro = (startDate: any, endDate: any) => {
const date1 = new Date(startDate)
const date2 = new Date(endDate)
const diff = Math.abs(date1.getTime() - date2.getTime());
const days = { text: '', value: 0 };
days.text = date1.getTime() - date2.getTime() > 0 ? '超出' + Math.ceil(diff / (1000 * 60 * 60 * 24)) : '还剩' + Math.ceil(diff / (1000 * 60 * 60 * 24))
days.value = date1.getTime() - date2.getTime() > 0 ? Math.ceil(diff / (1000 * 60 * 60 * 24)) : -1 * Math.ceil(diff / (1000 * 60 * 60 * 24))
return days
}
const colorVal = (val: any) => {
if (val <= 15) {
return '#67c23a'
}
if (15 < val && val <= 30) {
return '#e6a23c'
}
if (30 < val && val <= 60) {
return '#f56c6c'
}
if (60 < val) {
return '#da7171'
}
}
</script>
<style lang="scss" scoped>
.content {
margin-top: 20px;
margin-left: 16px;
width: 90%;
.box {
margin-left: 0;
width: 100%;
}
.type-size {
font-weight: 700;
margin: 10px 0;
}
.right {
border-radius: 4px;
.left {
font-size: 16px;
color: #333;
margin: 4px;
}
}
.el-button {
float: right;
margin-bottom: 10px;
margin-left: 10px;
}
}
</style>
3、甘特图组件代码
src\views\myCenter\projectCenter\components\scheduleTable.vue
<!--
@Description 项目管理 - 项目进度表
@author wdd
@date 2023/11/15
-->
<template>
<div class="mark-box">
<CaretTop class="mark"></CaretTop> <span class="mark-name">:项目节点实施启动时间</span>
<div class="color-box">
<div style="width:84px;float: left;" v-for="item in colorList" :key="item.color">
<span style=" font-size:14px;color:#666;position: absolute;margin-left: 20px;">{{item.describe}}</span>
<div :style="{display:'inline-block',backgroundColor:item.color,width:'14px',height:'14px'}"></div>
</div>
</div>
</div>
<div class="content">
<div class="left">
<div class="name-box">
<div class="title-name"><span>节点名称</span></div>
<div class="name-right">
<div class="name-val">节点进度(%)</div>
<div class="title-show">
<div v-for="(item,index) in props.dateList" :key="item.id">
<div class="name-title" :style="{width:item.diff+'%'}"> <span>{{item.description}}</span>
</div>
<!-- 每个节点计划结束日期分割线 -->
<div class="line-mark"
:style="{height:props.dateList.length*80+42+props.dateList.length*1+'px' ,position: 'absolute',borderRight: '1px solid #666',top: '0px',left: item.widthPro+'%'}">
</div>
<!-- 节点实际进度 1.1.开始打点 -->
<CaretTop
:style="{width:'20px',height:'20px',color:'red',position: 'absolute',top: (index)*80+86+props.dateList.length*1+'px',left: item.pointPro+'%',marginLeft:'-10px'}">
></CaretTop>
<!-- 节点实际进度开始打点 -->
<div class="line-mark"
:style="{height:'20px' ,width:item.diff+'%',borderRadius:'6px',position: 'absolute',top: (index)*80+72+props.dateList.length*1+'px',left: item.pointPro+'%'}">
</div>
<!-- 节点实际进度宽度条 2.1.进度百分比版本 -->
<div v-if="!item.realityEndDate && item.schedule" class="line-mark"
:style="{height:'20px' ,width:item.diff*item.schedule/100+'%',position: 'absolute',borderRadius:'6px',backgroundColor:showColor('schedule',item.diff,item.schedule,item.pointPro,item.widthPro),border: '1px solid #67c23a',top: (index)*80+72+props.dateList.length*1+'px',left: item.pointPro+'%'}">
<span style="font-size:12px;position: absolute;right:-26px">{{item.schedule}}%</span>
</div>
<!-- 节点实际进度 1.2.结束打点 -->
<!-- <CaretTop :style="{width:'20px',height:'20px',color:'blue',position: 'absolute',top: (index)*80+86+props.dateList.length*1+'px',left: item.pointEndPro+'%',marginLeft:'-10px'}">></CaretTop> -->
<!-- 节点实际进度宽度条 2.2.结束日期版本 -->
<div v-if="item.realityEndDate"
:style="{width:endWith(item.pointEndPro,item.pointPro) ,height:'20px',borderRadius:'6px',border:'1px solid #67c23a',backgroundColor:showColor('realityEndDate',item.pointEndPro,item.schedule,item.pointPro,item.widthPro),position: 'absolute',color:'#67c23a',top: (index)*80+72+props.dateList.length*1+'px',left: item.pointPro+'%'}">
<span style="font-size:12px;position: absolute;right:-40px">已完成</span>
</div>
</div>
</div>
</div>
</div>
<div v-for="item in props.dateList" :key="item.id">
<div class="title-box">
<div class="title-one"> <span>{{item.description}}</span></div>
<div class="title-two"> </div>
</div>
</div>
</div>
<div class="right">
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, defineProps } from 'vue';
const props = defineProps({
dateList: {
type: Array,
default() {
return []
},
}
})
// 严重风险备用色号 #8e74c2
const colorList = ref([
{
describe: '无风险',
color: '#67c23a',
},
{
describe: '低风险',
color: '#67c23a',
},
{
describe: '中风险',
color: '#e6a23c',
},
{
color: '#f56c6c',
describe: '高风险',
},
{
color: '#da7171',
describe: '严重风险'
}])
console.log(123, props.dateList);
// 计算日期天数差值 示例:[{'2023-02-01','2023-02-02'},{'2023-02-02','2023-02-04'},{'2023-02-04','2023-02-07'}] ==> [1,2,3]
const dayNum = (arr: any) => {
let difference = arr.reduce((acc: any, current: any) => {
let startDate = new Date(current.startDate);
let endDate = new Date(current.endDate);
let timeDiff = Math.abs(endDate.getTime() - startDate.getTime()); // 计算毫秒差
let daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24)); // 将毫秒差转换为天数,并向上取整
acc.push(daysDiff);
return acc;
}, [])// 初始值为空数组
return difference
}
// 给数组每个对象增加参数diff
props.dateList.forEach((el, index) => {
dayNum(props.dateList).forEach((m, n) => {
if (index == n) {
el.diff = (m / dayNum(props.dateList).reduce((j, k) => j + k, 0) * 100).toFixed(2) // 计算各自天数差值的占比
el.sevenDays = (7 / dayNum(props.dateList).reduce((j, k) => j + k, 0) * 100).toFixed(2) // 7天的占比
el.fifteenDays = (15 / dayNum(props.dateList).reduce((j, k) => j + k, 0) * 100).toFixed(2) // 15天的占比
el.thirtyDays = (30 / dayNum(props.dateList).reduce((j, k) => j + k, 0) * 100).toFixed(2) // 30天的占比
el.sixtyDays = (60 / dayNum(props.dateList).reduce((j, k) => j + k, 0) * 100).toFixed(2) // 60天的占比
}
})
})
// 计算天数差数组每个下标对应的当前下标前几位之和 示例:[1,2,3] ==> [1,3,6]
const sums = (arr: any) => {
// arr = [1,2,3,4]
let sums = []; // 用于存储结果的数组
for (let i = 0; i < arr.length; i++) {
let sum = 0;
for (let j = 0; j <= i; j++) {
sum += arr[j]
}
sums.push(sum)
}
return sums
}
const proList = sums(dayNum(props.dateList))
// 节点开始实施时间打点
const pointList = JSON.parse(JSON.stringify(props.dateList))
pointList.forEach(el => {
el.startDate = pointList[0].startDate
el.endDate = el.realityStartDate
})
// 节点结束实施时间打点
const pointEndList = JSON.parse(JSON.stringify(props.dateList))
pointEndList.forEach(el => {
el.startDate = pointEndList[0].startDate
el.endDate = el.realityEndDate
})
props.dateList.forEach((el, index) => {
proList.forEach((m, n) => {
if (index == n) {
el.widthPro = (m / proList[proList.length - 1] * 100).toFixed(2) // 计算各自天数差值所属盒子分割线的占比
}
})
dayNum(pointList).forEach((m, n) => {
if (index == n) {
el.pointPro = (m / proList[proList.length - 1] * 100).toFixed(2) // 计算各自节点开始实施时间的打点位置占比
}
})
dayNum(pointEndList).forEach((m, n) => {
if (index == n) {
el.pointEndPro = (m / proList[proList.length - 1] * 100).toFixed(2) // 计算各自节点结束实施时间的打点位置占比
}
})
})
// 动态提示颜色
const showColor = (type: any, diff: any, schedule: any, pointPro: any, widthPro: any) => {
const date1 = type == 'schedule' ? Number(diff) * Number(schedule) / 100 + Number(pointPro) : Number(diff)
const date2 = Number(widthPro)
// console.log(7777,date1,Number(props.dateList[0].sevenDays),Number(props.dateList[0].fifteenDays),Number(props.dateList[0].thirtyDays),Number(props.dateList[0].thirtyDays))
if (date1 <= Number(props.dateList[0].sevenDays) + date2) {
return '#67c23a'
}
if (Number(props.dateList[0].sevenDays) + date2 < date1 && date1 <= Number(props.dateList[0].fifteenDays) + date2) {
return '#e6a23c'
}
if (Number(props.dateList[0].fifteenDays) + date2 < date1 && date1 <= Number(props.dateList[0].thirtyDays) + date2) {
return '#f56c6c'
}
if (Number(props.dateList[0].thirtyDays) + date2 < date1) {
return '#da7171'
}
}
// 超出计划结束日期进行截取至项目计划结束日期
const endWith = (pointEndPro: any, pointPro: any) => {
const withVal = Number(pointEndPro) - 100 > 0 ? 100 - Number(pointPro) + '%' : Number(pointEndPro) - Number(pointPro) + '%'
return withVal
}
const numList = ref([
{
id: 1,
name: '节点一',
value: 12,
},
{
id: 2,
name: '节点二',
value: 15,
},
{
id: 3,
name: '节点三',
value: 46,
},
{
id: 4,
name: '节点四',
value: 85,
},
{
id: 5,
name: '节点五',
value: 85,
},
])
console.log(numList);
</script>
<style lang="scss" scoped>
.content {
width: 100%;
margin-left: 0;
border: 1px solid #666;
background-color: beige;
.mark-box {
position: relative;
height: 30px;
.mark {
width: 20px;
height: 20px;
color: red;
}
.mark-name {
font-size: 14px;
color: #666;
position: absolute;
top: 0px
}
.color-box {
position: absolute;
left: 200px;
top: 0px
}
}
.left {
width: 100%;
.name-box {
display: flex;
width: 100%;
height: 80px;
border: 1px solid #666;
.name-right {
flex: 1;
height: 80px;
border-left: 1px solid #666;
.name-val {
width: 100%;
border-bottom: 1px solid #666;
height: 40px;
font-weight: 700;
line-height: 40px;
text-align: center;
}
.title-show {
width: 100%;
height: 40px;
position: relative;
.name-title {
float: left;
height: 40px;
font-weight: 700;
line-height: 40px;
text-align: center;
font-size: 14px;
}
}
}
.title-name {
width: 260px;
height: 80px;
line-height: 80px;
text-align: center;
font-weight: 700;
border-right: 1px solid #666;
font-size: 16px;
}
}
.title-box {
display: flex;
.title-one {
width: 260px;
font-weight: 700;
height: 80px;
line-height: 80px;
text-align: center;
border: 1px solid #666;
font-size: 14px;
}
.title-two {
flex: 1;
border: 1px solid #666;
height: 80px;
line-height: 80px;
display: inline-block;
}
}
}
.right {
// flex: 1;
}
}
</style>
4、抽离方法
src\utils\utils.js
/**
* 计算日期天数差值
* 示例:[{'2023-02-01','2023-02-02'},{'2023-02-02','2023-02-04'},{'2023-02-04','2023-02-07'}] ==> [1,2,3]
*/
export function dayNum(arr) {
let difference = arr.reduce((acc, current) => {
let startDate = new Date(current.startDate);
let endDate = new Date(current.endDate);
let timeDiff = Math.abs(endDate.getTime() - startDate.getTime()); // 计算毫秒差
let daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24)); // 将毫秒差转换为天数,并向上取整
acc.push(daysDiff);
return acc;
}, []); // 初始值为空数组
return difference;
}
/**
* 计算天数差数组每个下标对应的当前下标前几位之和
* 示例:[1,2,3] ==> [1,3,6]
*/
export function sums(arr) {
// arr = [1,2,3,4]
let sums = []; // 用于存储结果的数组
for (let i = 0; i < arr.length; i++) {
let sum = 0;
for (let j = 0; j <= i; j++) {
sum += arr[j];
}
sums.push(sum);
}
return sums;
}
使用
index.vue
<script lang="ts" setup>
import { ref, defineProps } from 'vue';
import { dayNum,sums } from "@/utils/utils.js";
const proList = sums(dayNum(props.dateList))
const pointList = JSON.parse(JSON.stringify(props.dateList))
</script>