背景是公司的需求,要做一个项目人力展示,刚接手这个项目,这个项目里有类似的甘特图用的是dhtmlx-gantt,这个国外的库功能很强大,但是缺点也很明显:
文档是英文的,没有中文版
一些功能是收费的
样式很难自定义
...
基于这样的考虑,再加上需求不是很难,只是做进度展示,就自己写了,先上成果图
功能很简单只做年月的切换,左侧的树结构只有一层。
下面说一下思路:
1.布局:由于设计稿就这样左右中间有个缝隙,我就左右都基于树的数据去遍历,只是右侧不展示树而已,右侧也是有滚动条的只是隐藏掉了,和左侧的进度条同步滚动,同理上面的日期也是和下面的同步滚动。 2.日期:日期的展示是最重要的,我的思路是拿到后台的数据,找到最早的和最晚的,基于这个去计算两个日期经过了多少天月年,展示出来,进度条是用定位做的,需要计算出宽度和left,宽度就是每条数据两个日期经过的天数,(月就/30),left就是当前数据的开始时间和所有数据的最早时间的跨度。 3.不多说了,直接看代码吧,我把数据用json写好,放在vue项目里能直接运行 4.只是提供一个雏形,更多的功能和样式自定义完全可以基于这个去做
<!--
项目资源占用模块
@ lizibin
-->
<template>
<div class="resourceOccupy">
<div class="resourceOccupy-search">
<!-- <div class="resourceOccupy-search-left">
<div style="margin:0 5px;">
<el-date-picker style="height: 36px;width: 250px" v-model="date" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd"
@change="changeList"></el-date-picker>
</div>
<div style="margin: 0 5px; width: 250px;font-size: small;">
<treeselect v-model="orderOrgLimit" :options="datalist" :multiple="false" placeholder="选择组织" />
</div>
<div>
<el-button type="primary" size="mini"
style="margin-left:5px;background-color:#2675FB;color:#FFFFFF;height: 34px;" @click="_search()">搜索</el-button>
</div>
</div> -->
<div class="resourceOccupy-search-right">
<el-radio-group v-model="dateChaneValue" size="medium" @change="_dateChange">
<el-radio-button label="month">月</el-radio-button>
<el-radio-button label="year">年</el-radio-button>
</el-radio-group>
</div>
</div>
<div v-loading="loading" style="height: 100%;">
<div class="resourceOccupy-top">
<div class="resourceOccupy-top-left">
<div class="resourceOccupy-top-left-title">
<!-- 表头固定死的 -->
<div>工号</div>
<div>负责人</div>
</div>
</div>
<div class="resourceOccupy-top-right" ref="taskDiv">
<!-- 啊啊啊啊啊啊啊 头要爆炸了 -->
<!--**************************** 第一行年 start *****************************-->
<!-- 固定的 -->
<div class="resourceOccupy-top-right-one"
v-if="taskWidth < (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year' && yearData.length !== 0">
<!-- 年 -->
<!-- 如果大于taskWidth -->
<span :style="{ 'width': (item.num * itemWidth) + 'px' }" v-for="item in yearData" :key="item.name">{{
item.name
}}</span>
</div>
<!-- 自适应的 -->
<div class="resourceOccupy-top-right-one" style="display: flex;"
v-if="taskWidth >= (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year' && yearData.length !== 0">
<!-- 年 -->
<!-- 判断宽度是不是比taskWidth要宽 -->
<!-- 如果小于taskWidth -->
<span :style="{ 'width': (item.num * (taskWidth / monthData.length)) + 'px' }" v-for="item in yearData"
:key="item.name">{{
item.name
}}</span>
</div>
<!--yearData为空的 -->
<div class="resourceOccupy-top-right-one" style="display: flex;"
v-if="dateChaneValue === 'year' && yearData.length === 0">
<!-- 年 -->
<!-- 判断宽度是不是比taskWidth要宽 -->
<!-- 如果小于taskWidth -->
<span style="width: 100%;">{{ thisYearData.year }}</span>
</div>
<!--**************************** 第一行年 end *****************************-->
<!--**************************** 第一行月 start *****************************-->
<!-- 固定 -->
<div class="resourceOccupy-top-right-one"
v-if="taskWidth < (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month' && monthDataToOne.length !== 0">
<span :style="{ 'width': (item.num * itemWidth) + 'px' }" v-for="(item, index) in monthDataToOne"
:key="index">{{
`${item.name.split('-')[0]}年${item.name.split('-')[1]}月`
}}</span>
</div>
<!-- 自适应 -->
<div class="resourceOccupy-top-right-one" style="display: flex;"
v-if="taskWidth >= (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month' && monthDataToOne.length !== 0">
<span :style="{ 'width': (item.num * (taskWidth / monthDataToOne.length)) + 'px' }"
v-for="(item, index) in monthDataToOne" :key="index">{{
`${item.name.split('-')[0]}年${item.name.split('-')[1]}月`
}}</span>
</div>
<!-- monthDataToOne为空的 -->
<div class="resourceOccupy-top-right-one" v-if="dateChaneValue === 'month' && monthDataToOne.length === 0">
<span style="width: 100%;">{{ thisMonthData.month }}</span>
</div>
<!--**************************** 第一行月 end *****************************-->
<!--**************************** 第二行年 start *****************************-->
<!-- 固定 -->
<div class="resourceOccupy-top-right-two"
v-if="taskWidth < (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year' && monthData.length !== 0">
<span :style="{ width: itemWidth + 'px' }" v-for="(item, index) in monthData" :key="index">{{
item.slice(-2) }}</span>
</div>
<!-- 自适应的 -->
<div class="resourceOccupy-top-right-two" style="display: flex;"
v-if="taskWidth >= (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year' && monthData.length !== 0">
<!-- 月 -->
<span :style="{ 'width': (taskWidth / monthData.length) + 'px' }" v-for="(item, index) in monthData"
:key="index">{{
item.slice(-2) }}</span>
</div>
<!-- monthData为空 -->
<div class="resourceOccupy-top-right-two" style="display: flex;"
v-if="dateChaneValue === 'year' && monthData.length === 0">
<!-- 月 -->
<span style="width: 100%;" v-for="(item, index) in thisYearData.month" :key="index">{{
item }}</span>
</div>
<!--**************************** 第二行年 end *****************************-->
<!--**************************** 第二行月 start *****************************-->
<!-- 固定 -->
<div class="resourceOccupy-top-right-two"
v-if="taskWidth < (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month' && dayData.length !== 0">
<span :style="{ width: itemWidth + 'px' }" v-for="(item, index) in dayData" :key="index">{{ item }}</span>
</div>
<!-- 自适应 -->
<div class="resourceOccupy-top-right-two" style="display: flex;"
v-if="taskWidth >= (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month' && dayData.length !== 0">
<span :style="{ 'width': (taskWidth / dayData.length) + 'px' }" v-for="(item, index) in dayData"
:key="index">{{
item }}</span>
</div>
<!-- dayData为空 -->
<div class="resourceOccupy-top-right-two" style="display: flex;"
v-if="dateChaneValue === 'month' && dayData.length === 0">
<span style="width: 100%;" v-for="(item, index) in thisMonthData.day" :key="index">{{
item }}</span>
</div>
<!--**************************** 第二行月 end *****************************-->
</div>
</div>
<!-- 表格数据 -->
<div class="resourceOccupy-bottom">
<!-- 只有一层的树没啥难度 -->
<div class="resourceOccupy-bottom-left" ref="verticalLeft" @scroll="sysHandleScroll()">
<div class="resourceOccupy-bottom-left-item" v-for="(item, index) in TaskData" :key="item.id">
<div class="item-father"><span v-if="!item.open" @click="_imgChange(index)"><i
class="el-icon-plus"></i></span> <span v-if="item.open" @click="_imgChange(index)"><i
class="el-icon-minus"></i></span> <span :title="item.label">{{ item.label }}</span></div>
<div v-if="item.open">
<div class="item-son" :style="{ 'background': i.user === currentPeople.user ? '#ededed' : 'none' }"
@click="_clickSon(i)" v-for="i in item.children" :key="i.user">
<div :title="i.user">{{ i.user }}</div>
<div>{{ i.label }}</div>
</div>
</div>
</div>
</div>
<div class="resourceOccupy-bottom-right" ref="verticalRight" @scroll="sysHandleScrol2()">
<div class="resourceOccupy-bottom-right-item" v-for="(item, index) in TaskData" :key="item.id">
<!--**************************** 年 start *****************************-->
<!-- 固定 年 -->
<div class="item-father"
v-if="taskWidth < (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year'">
<span :style="{ width: itemWidth + 'px' }" v-for="(item, index) in monthData" :key="index">
</span>
</div>
<!-- 自适应的 年-->
<div class="item-father" style="display: flex;"
v-if="taskWidth >= (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year'">
<span :style="{ 'width': (taskWidth / monthData.length) + 'px' }" v-for="(item, index) in monthData"
:key="index"></span>
</div>
<!--***************************** 年 end ******************************-->
<!--**************************** 月 start ****************************-->
<!-- 固定 月 -->
<div class="item-father"
v-if="taskWidth < (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month'">
<span :style="{ width: itemWidth + 'px' }" v-for="(item, index) in dayData" :key="index">
</span>
</div>
<!-- 自适应的 月-->
<div class="item-father" style="display: flex;"
v-if="taskWidth >= (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month'">
<span :style="{ 'width': (taskWidth / dayData.length) + 'px' }" v-for="(item, index) in dayData"
:key="index"></span>
</div>
<!-- *****************************月 end ****************************-->
<div v-if="item.open" class="item-son" v-for="i in item.children" :key="i.user">
<!--***************************** 年 start ****************************-->
<!-- 固定 -->
<div v-if="taskWidth < (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year'">
<span :style="{ width: itemWidth + 'px' }" v-for="(item, index) in monthData" :key="index"></span>
</div>
<!-- 自适应的 -->
<div style="display: flex;"
v-if="taskWidth >= (monthData.length * itemWidth + monthData.length) && dateChaneValue === 'year'">
<span :style="{ 'width': (taskWidth / monthData.length) + 'px' }" v-for="(item, index) in monthData"
:key="index"></span>
</div>
<!--***************************** 年 end ******************************-->
<!--***************************** 月 start ****************************-->
<!-- 固定 -->
<div v-if="taskWidth < (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month'">
<span :style="{ width: itemWidth + 'px' }" v-for="(item, index) in dayData" :key="index"></span>
</div>
<!-- 自适应的 -->
<div style="display: flex;"
v-if="taskWidth >= (dayData.length * itemWidth + dayData.length) && dateChaneValue === 'month'">
<span :style="{ 'width': (taskWidth / dayData.length) + 'px' }" v-for="(item, index) in dayData"
:key="index"></span>
</div>
<!--***************************** 月 end ******************************-->
<!-- 进度条 需要判断是否有时间 然后判断是年还是 月 -->
<!-- 分两个写吧,太乱了 -->
<!--***************************** 年 start ******************************-->
<div class="item-son-progress" v-if="i.start !== '' && dateChaneValue === 'year'"
:title="`${i.start}--${i.end}`" :style="{
width: _handleProgressWidth('year', taskWidth < (monthData.length * itemWidth + monthData.length) ? itemWidth : taskWidth / monthData.length, i.start, i.end) + 'px',
left: _handleToLeftWidth('year', taskWidth < (monthData.length * itemWidth + monthData.length) ? itemWidth : taskWidth / monthData.length, i.start, i.end) + 'px'
}"></div>
<!--***************************** 年 end ******************************-->
<!--***************************** 月 start ******************************-->
<div class="item-son-progress" v-if="i.start !== '' && dateChaneValue === 'month'"
:title="`${i.start}--${i.end}`" :style="{
width: _handleProgressWidth('month', taskWidth < (dayData.length * itemWidth + dayData.length) ? itemWidth : taskWidth / dayData.length, i.start, i.end) + 'px',
left: _handleToLeftWidth('month', taskWidth < (dayData.length * itemWidth + dayData.length) ? itemWidth : taskWidth / dayData.length, i.start, i.end) + 'px'
}"></div>
<!--***************************** 月 end ******************************-->
</div>
</div>
</div>
</div>
</div>
<!-- 详情弹窗 -->
<el-dialog custom-class="dialog" :visible.sync="dialogVisible" :append-to-body="true" :destroy-on-close="false"
width="862px" heigh="520px" :before-close="handleClose">
<div slot="title" class="dialog-title">
详情
</div>
<!-- body -->
<div class="dialog-body" v-if="dialogVisible && detailData.length !== 0">
<div class="dialog-body-detail">
<div>
<span :style="{ 'background-color': _idToColor(currentPeople.user, currentPeople.label) }">{{
currentPeople.label.slice(-2) }}</span><span>{{ currentPeople.label }}</span><span>{{
currentPeople.user }}</span>
</div>
<div>
<span>项目总周期:</span>
<span>{{ detailData[0].totalProjectPeriods }}</span>
<!-- <span>xxx天</span> -->
<span>项目总天数:</span>
<span> {{ detailData[0].totalProjectDays }}天</span>
<!-- <span> xxx天</span> -->
</div>
<div>
<span>参与项目数:</span>
<span>{{ detailData.length }}</span>
<!-- <span>xx</span> -->
<span>项目实际天数:</span>
<span> {{ detailData[0].actualProjectDays }}天</span>
<!-- <span> xxx天</span> -->
</div>
</div>
<div class="dialog-body-table">
<div class="dialog-body-table-left">
<div class="dialog-body-table-left-title">
项目名称
</div>
<div class="dialog-body-table-left-table" ref="detailScrollLeft">
<div class="dialog-body-table-left-table-item" v-for="(item, index) in detailData" :key="index">
{{ item.label }}
</div>
</div>
</div>
<div class="dialog-body-table-right">
<div class="dialog-body-table-right-title" ref="detailScrollTop" @scroll="_handleDetailScroll()">
<span v-for="(item, index) in detailDays" :key="index">{{ item.slice(-2) }}</span>
</div>
<div class="dialog-body-table-right-table" ref="detailScrollRightAndBottom" @scroll="_handleDetailScroll()">
<div class="dialog-body-table-right-table-item" v-for="(item, index) in detailData" :key="index">
<span v-for="(i, num) in detailDays" :key="num"></span>
<div class="dialog-body-table-right-table-item-progress" :style="{
width: _handleDetailProWidth(item.start, item.end) + 'px',
left: _handleDetailLeftWidth(item.start, item.end) + 'px'
}">
<div><i class="el-icon-time" style="color: #52C41A;margin-right: 5px;"></i>{{ item.start.replace(/-/g,
".") }} - {{ item.end.replace(/-/g, ".") }}</div>
<div>工期:{{ _getTimeTwo(item.start, item.end).length }}天</div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
screenWidth: "",
screenHeight: "",
itemWidth: null,
dateChaneValue: 'month',
loading: false,
deadLineEnd: "",// 最后截止时间结束范围
deadLineStart: "",// 最后截止时间开始范围
TaskData: [], // 列表数据当前数据
allData: [], // 列表全部数据
orderOrgLimit: null,
taskWidth: null,
datalist: [], // 搜索树用的
date: null,
yearData: [],
monthData: [],
monthDataToOne: [],
dayData: [],
earliestStart: '', // 最早时间
latestEnd: '', // 最晚时间
currentPeople: {},
// ***********详情弹窗相关************
dialogVisible: false,
detailData: [],
detailDays: [],
detailStartTime: '', // 弹窗内最早时间
detailEndTime: '', // 弹窗内最晚时间
// ***********详情弹窗相关************
pageNum: 1, // 第几页
pageSize: 20, // 每页x条数据
thisYearData: {},
thisMonthData: {},
};
},
components: {
},
computed: {
},
mounted() {
this._search()
this.taskWidth = this.$refs.taskDiv.offsetWidth;
this.itemWidth = this.taskWidth / 24;
// 监听页面变化
this.screenWidth = document.body.clientWidth; //监听页面缩放
window.onresize = () => {
return (() => {
// 要适配浏览器的放大,那么每个单元格的宽度就要动态起来,那么这个值怎么确定呢????
// this.taskWidth / 24先这样试试
this.screenWidth = document.body.clientWidth;
this.taskWidth = this.$refs.taskDiv.offsetWidth;
this.itemWidth = this.taskWidth / 24;
})();
};
this.$refs.verticalLeft.addEventListener('scroll', this._isScrollToBottom)
this.thisYearData = this._creatThisYearData();
this.thisMonthData = this._creatThisMonthData();
},
watch: {
screenWidth() {
// console.log('this.screenWidth',this.screenWidth);
},
taskWidth() {
// console.log('this.taskWidth',this.taskWidth / 24);
},
itemWidth() {
// console.log('this.itemWidth',this.itemWidth);
}
},
methods: {
/**
* 代码描述: 滚动加载
* 作者:lizibin
* 创建时间:2023/12/29 14:17:55
*/
_isScrollToBottom() {
const scrollTop = this.$refs.verticalLeft.scrollTop
// 获取可视区的高度
const windowHeight = this.$refs.verticalLeft.clientHeight
// 获取滚动条的总高度
const scrollHeight = this.$refs.verticalLeft.scrollHeight
// 滚动到最底部
if (scrollTop + windowHeight >= scrollHeight) {
// 把距离顶部的距离加上可视区域的高度 等于或者大于滚动条的总高度就是到达底部
if (this.TaskData.length < this.allData.length) {
this.loading = true;
this.pageNum += 1;
let nextData = this._getDataForPage(this.pageNum, this.pageSize);
console.log('+1后的数据', nextData);
this.TaskData = [...this.TaskData, ...nextData];
console.log('到底了');
setTimeout(() => {
this.loading = false;
}, 2000)
}
}
},
/**
* 获取指定页数的数据数组
* @param {number} page 要获取的页数
* @param {number} pageSize 每页多少条数据
* @returns {Array} 返回对应页数的数据数组
*/
_getDataForPage(page, pageSize) {
// 假设这是你的总数据
const totalData = this.allData;
// 计算起始索引和结束索引
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
// 使用 slice 方法截取对应页数的数据
const pageData = totalData.slice(startIndex, endIndex);
return pageData;
},
/**
* 代码描述: 获取项目详情
* 作者:lizibin
* 创建时间:2023/12/26 11:17:59
*/
_getProjectResourceDetail(userId) {
getProjectResourceDetail({ userId }).then((res) => {
if (res.data.code == "S") {
this.detailData = res.data.data;
if (this.detailData.length > 0) {
this.dialogVisible = true;
} else {
this.dialogVisible = false;
}
let earliestStart = ''; // 最早
let latestEnd = ''; // 最晚
// 循环数组找到最早的 start 和最晚的 end
for (const item of res.data.data) {
if (item.start) {
if (earliestStart === '') {
earliestStart = item.start
}
const startDate = item.start;
if (new Date(startDate) < new Date(earliestStart)) {
earliestStart = startDate;
}
}
if (item.end) {
if (latestEnd === '') {
latestEnd = item.end
}
const endDate = item.end;
if (new Date(endDate) > new Date(latestEnd)) {
latestEnd = endDate;
}
}
}
this.detailStartTime = earliestStart;
this.detailEndTime = latestEnd;
this.detailDays = this._getTimeTwo(this.detailStartTime, this.detailEndTime);
}
})
},
_search() {
// getProjectResourceGantList(params).then(res => {
// if (res.data.code = 'S') {
this.TaskData = [];
this.yearData = [];
this.monthData = [];
this.monthDataToOne = [];
this.dayData = [];
this.pageNum = 1;
let backData = [
{
"id": "c4cd08201b6e4be7a8f8e4b6a05a62ce",
"attribute": "",
"parentId": "",
"label": "老年web开发组",
"user": "",
"start": "",
"end": "",
"duration": "",
"percent": "",
"type": "",
"status": ""
},
{
"id": "",
"attribute": "",
"parentId": "c4cd08201b6e4be7a8f8e4b6a05a62ce",
"label": "张一",
"user": "11111",
"start": "",
"end": "",
"duration": "",
"percent": "",
"type": "",
"status": ""
},
{
"id": "",
"attribute": "",
"parentId": "c4cd08201b6e4be7a8f8e4b6a05a62ce",
"label": "张二",
"user": "22222",
"start": "2022-05-07",
"end": "2022-12-31",
"duration": "",
"percent": "",
"type": "",
"status": ""
},
{
"id": "",
"attribute": "",
"parentId": "c4cd08201b6e4be7a8f8e4b6a05a62ce",
"label": "张二",
"user": "3333333",
"start": "",
"end": "",
"duration": "",
"percent": "",
"type": "",
"status": ""
},
{
"id": "",
"attribute": "",
"parentId": "c4cd08201b6e4be7a8f8e4b6a05a62ce",
"label": "张二",
"user": "44444",
"start": "2023-11-14",
"end": "2023-11-29",
"duration": "",
"percent": "",
"type": "",
"status": ""
},
{
"id": "",
"attribute": "",
"parentId": "c4cd08201b6e4be7a8f8e4b6a05a62ce",
"label": "张二",
"user": "555555",
"start": "",
"end": "",
"duration": "",
"percent": "",
"type": "",
"status": ""
},
{
"id": "",
"attribute": "",
"parentId": "c4cd08201b6e4be7a8f8e4b6a05a62ce",
"label": "66666",
"user": "66666",
"start": "2022-05-05",
"end": "2022-12-31",
"duration": "",
"percent": "",
"type": "",
"status": ""
},
];
let earliestStart = ''; // 最早
let latestEnd = ''; // 最晚
// 循环数组找到最早的 start 和最晚的 end
for (const item of backData) {
if (item.start) {
if (earliestStart === '') {
earliestStart = item.start
}
const startDate = item.start;
if (new Date(startDate) < new Date(earliestStart)) {
earliestStart = startDate;
}
}
if (item.end) {
if (latestEnd === '') {
latestEnd = item.end
}
const endDate = item.end;
if (new Date(endDate) > new Date(latestEnd)) {
latestEnd = endDate;
}
}
}
console.log('最早时间', earliestStart);
console.log('最晚时间', latestEnd);
this.earliestStart = earliestStart;
this.latestEnd = latestEnd;
let year = this._getYearBetween(earliestStart, latestEnd);
// 拿到年数据和月数据
this.monthData = this._getMonthBetween(earliestStart, latestEnd);
for (let a = 0; a < year.length; a++) {
let num = this._countElementsStarting(year[a], this.monthData);
let obj = {
name: year[a],
num
}
this.yearData.push(obj)
}
for (let b = 0; b < this.monthData.length; b++) {
let obj = {
name: this.monthData[b],
num: this._getMonthDays(this.monthData[b].split('-')[0], this.monthData[b].split('-')[1])
}
let days = this._generateArray(obj.num);
this.dayData = [...this.dayData, ...days];
this.monthDataToOne.push(obj);
}
console.log('年数据', this.yearData);
console.log('月数据', this.monthData);
console.log('月数据', this.monthDataToOne);
console.log('天数据', this.dayData);
// 重新组织下结构 -- 展示树
let newData = [];
for (let i = 0; i < backData.length; i++) {
if (backData[i].parentId === '') {
backData[i].children = [];
backData[i].open = true;
backData[i].children = backData.filter((item) => {
return item.parentId === backData[i].id
})
newData.push(backData[i]);
}
}
console.log('总数据', newData);
this.allData = newData;
this.TaskData = this._getDataForPage(this.pageNum, this.pageSize);
this.loading = false;
// } else {
// this.$message.error(res.data.info);
// }
// })
},
/**
* 代码描述: 生成今年12个月
* 作者:lizibin
* 创建时间:2024/01/02 10:44:35
*/
_creatThisYearData() {
const currentYear = new Date().getFullYear();
return {
year: currentYear,
month: this._generateArray(12)
}
},
/**
* 代码描述: 生成当月多少天
* 作者:lizibin
* 创建时间:2024/01/02 10:44:35
*/
_creatThisMonthData() {
const currentYear = new Date().getFullYear();
let currentMonth = new Date().getMonth() + 1;
currentMonth = currentMonth.length === 1 ? '0' + currentMonth : currentMonth;
return {
month: `${currentYear}年${currentMonth}月`,
day: this._generateArray(this._getMonthDays(currentYear, currentMonth))
}
},
/**
* 代码描述: 生成1-n的数组
* 作者:lizibin
* 创建时间:2023/12/25 17:03:03
*/
_generateArray(n) {
const arr = [];
for (let i = 1; i <= n; i++) {
arr.push(i);
}
return arr;
},
/**
* 代码描述: 年月切换
* 作者:lizibin
* 创建时间:2023/12/20 16:06:26
*/
_dateChange(val) {
this.loading = true;
setTimeout(() => {
this.loading = false;
}, 1500)
},
/**
* 代码描述: 搜索日期框改变
* 作者:lizibin
* 创建时间:2023/12/20 16:14:20
*/
changeList(val) {
if (val == null) {
this.deadLineStart = '';
this.deadLineEnd = '';
} else {
this.deadLineStart = val[0];
this.deadLineEnd = val[1];
}
},
/**
* 获取两日期之间日期列表函数
* 返回两个时间之间所有的日期
* 参数示例 ('2021-05-31','2021-06-30')
* **/
_getTimeTwo(start, end) {
//初始化日期列表,数组
let diffdate = new Array();
let arr = []
let i = 0;
//开始日期小于等于结束日期,并循环
while (start <= end) {
diffdate[i] = start;
//获取开始日期时间戳
let stime_ts = new Date(start).getTime();
//增加一天时间戳后的日期
let next_date = stime_ts + (24 * 60 * 60 * 1000);
//拼接年月日,这里的月份会返回(0-11),所以要+1
let next_dates_y = new Date(next_date).getFullYear() + '-';
let next_dates_m = (new Date(next_date).getMonth() + 1 < 10) ? '0' + (new Date(next_date).getMonth() + 1) + '-' : (new Date(next_date).getMonth() + 1) + '-';
let next_dates_d = (new Date(next_date).getDate() < 10) ? '0' + new Date(next_date).getDate() : new Date(next_date).getDate();
start = next_dates_y + next_dates_m + next_dates_d;
//增加数组key
i++;
}
return diffdate;
},
/**
* 获取两个日期中所有的月份
* 返回两个时间之间所有的月份
* 参数示例 ('2021-01-01','2021-06-01')
* **/
_getMonthBetween(start1, end1) {
const start = new Date(start1);
const end = new Date(end1);
const months = [];
// 设置开始时间为每月的第一天
start.setDate(1);
while (start <= end) {
const year = start.getFullYear();
const month = String(start.getMonth() + 1).padStart(2, '0'); // 补全月份,例如 "01"
months.push(`${year}-${month}`);
// 增加一个月
start.setMonth(start.getMonth() + 1);
}
return months;
},
/**
* 获取两个日期中所有的年份
* 返回两个时间之间所有的年份
* 参数示例 ('2021-01-01','2021-01-01')
* **/
_getYearBetween(start, end) {
let result = [];
let min = new Date(start).getFullYear();
let max = new Date(end).getFullYear();
while (min <= max) {
result.push(min);
min = (Number(min) + 1)
}
return result;
},
/**
* 代码描述:编写函数统计以 "2023" 开头的元素数量
* 作者:lizibin
* 创建时间:2023/12/22 13:13:45
*/
_countElementsStarting(prefix, array) {
return array.filter(element => element.startsWith(prefix)).length;
},
/**
* 代码描述: 改变下拉
* 作者:lizibin
* 创建时间:2023/12/22 14:48:20
*/
_imgChange(index) {
this.TaskData[index].open = !this.TaskData[index].open;
},
// 垂直滚动条滚动同步
sysHandleScroll() {
this.$nextTick(() => {
this.$refs.verticalRight.scrollTop = this.$refs.verticalLeft.scrollTop;
this.$refs.taskDiv.scrollLeft = this.$refs.verticalRight.scrollLeft;
})
},
sysHandleScrol2() {
this.$nextTick(() => {
this.$refs.verticalLeft.scrollTop = this.$refs.verticalRight.scrollTop;
this.$refs.taskDiv.scrollLeft = this.$refs.verticalRight.scrollLeft;
})
},
_handleDetailScroll() {
this.$refs.detailScrollTop.scrollLeft = this.$refs.detailScrollRightAndBottom.scrollLeft;
},
/**
* 代码描述: 传入年份和月份 获取该年对应月份的天数
* 作者:lizibin
* 创建时间:2023/12/25 16:55:36
*/
_getMonthDays(year, month) {
var thisDate = new Date(year, month, 0); //当天数为0 js自动处理为上一月的最后一天
return thisDate.getDate();
},
/**
* 代码描述: 计算进度条的宽度
* 作者:lizibin
* 创建时间:2023/12/25 09:29:10
* @ model 年模式还是月模式
* @ itemWidth 每个进度块的宽度
* @ startTime
* @ endTime
*/
_handleProgressWidth(model, itemWidth, startTime, endTime) {
// 年模式
if (model === 'year') {
// 历时几个月
let monthArr = this._getMonthBetween(startTime, endTime)
let monthWidth = monthArr.length;
// 如果是同一个月
if (monthWidth === 1) {
let startWidth = +startTime.split('-')[2];
let endWidth = +endTime.split('-')[2];
if (endWidth === 31) endWidth = 30;
return (itemWidth / 30) * (endWidth - startWidth);
}
if (monthWidth === 2) monthWidth = 0;
if (monthWidth > 2) monthWidth -= 2;
let startWidth = +startTime.split('-')[2];
// 31天 统一按30计算 月份不用这么精确
if (startWidth === 31) startWidth = 30;
startWidth = 30 - startWidth === 0 ? 1 : 30 - startWidth;
let endWidth = +endTime.split('-')[2];
// 31天 统一按30计算 月份不用这么精确
if (endWidth === 31) endWidth = 30;
return monthWidth * itemWidth + (itemWidth / 30) * startWidth + (itemWidth / 30) * endWidth;
} else if (model === 'month') {
// 月模式
let days = this._getTimeTwo(startTime, endTime).length; // 两个日期经过的天数
return days * itemWidth;
}
},
/**
* 代码描述: 计算进度条左侧跨度
* 作者:lizibin
* 创建时间:2023/12/25 10:24:10
* @ model 年模式还是月模式
* @ itemWidth 每个进度块的宽度
* @ endTime 进度条的开始时间作为结束时间,真正的开始时间是这个数据里面最早的时间
*/
_handleToLeftWidth(model, itemWidth, start, end) {
let earliestStart = this.earliestStart; // 所有数据中最早的
// 年模式
if (model === 'year') {
// 历时几个月
let monthWidth = this._getMonthBetween(earliestStart, start).length;
if (monthWidth === 1 || monthWidth === 2) monthWidth = 0;
if (monthWidth > 2) monthWidth -= 1;
let endWidth = +start.split('-')[2];
// 31天 28 29 统一按30计算 月份不用这么精确
if (endWidth === 31) endWidth = 30;
if (endWidth === 30) endWidth -= 1;
if (endWidth === 1) endWidth = 0;
return monthWidth * itemWidth + endWidth * (itemWidth / 30);
} else if (model === 'month') {
// 进度条的开始时间与最早时间经过几天就是左侧宽度
let days = this._getTimeTwo(earliestStart.slice(0, -2) + '01', start).length - 1; // 两个日期经过的天数
let allWidth = days * itemWidth
if (allWidth > 20000) {
allWidth = allWidth - 5
}
return allWidth;
}
},
/**
* 代码描述: 计算详情的进度宽度
* 作者:lizibin
* 创建时间:2023/12/26 15:16:50
*/
_handleDetailProWidth(start, end) {
let days = this._getTimeTwo(start, end).length;
return days * 31;
},
/**
* 代码描述: 计算详情的进度距离左侧的宽度
* 作者:lizibin
* 创建时间:2023/12/26 15:16:50
*/
_handleDetailLeftWidth(start, end) {
let days = this._getTimeTwo(this.detailStartTime, start).length - 1;
return days * 31;
},
/**
* 代码描述: 点击子弹窗并变色
* 作者:lizibin
* 创建时间:2023/12/26 10:08:26
*/
_clickSon(i) {
this.currentPeople = i;
let width = 0;
if (this.dateChaneValue === 'year' && i.start !== '') {
width = this._handleToLeftWidth('year', this.taskWidth < (this.monthData.length * 60 + this.monthData.length) ? 61 : this.taskWidth / this.monthData.length, i.start, i.end)
} else if (this.dateChaneValue === 'month' && i.start !== '') {
width = this._handleToLeftWidth('month', this.taskWidth < (this.dayData.length * 60 + this.dayData.length) ? 61 : this.taskWidth / this.dayData.length, i.start, i.end)
}
this._getProjectResourceDetail(i.user);
this.$nextTick(() => {
this.$refs.taskDiv.scrollLeft = width;
this.$refs.verticalRight.scrollLeft = width;
})
},
_getPositionOfLetter(letter) {
// 将字母转换为对应的ASCII码值
const asciiValue = letter.charCodeAt(0);
// 计算字母在26个字母中的位置(a为1,b为2,依此类推)
const position = asciiValue - 96;
return position;
},
_isLowerCase(char) {
return /^[a-z]$/.test(char);
},
// 根据id生成固定颜色
// 生成0-9 10个颜色 根据最后一位去匹配颜色
_idToColor(id) {
let end = id[id.length - 1]
if (this._isLowerCase(end)) {
// 是字母
end = this._getPositionOfLetter(end)
}
let colorArr = [
'#F39898',
'#CFD181',
'#FAAD14',
'#52C41A',
'#C58BD0',
'#5bb3db',
'#7de2cc',
'#bfec77',
'#f7c387',
'#ec81db',
'#f58ce3',
]
return colorArr[end]
},
/**
* 代码描述: 详情弹窗关闭时
* 作者:lizibin
* 创建时间:2024/01/04 13:43:41
*/
handleClose(done) {
this.dialogVisible = false;
this.detailData = [];
done()
}
}
};
</script>
<style scoped lang="less">
.resourceOccupy {
width: 100%;
background: #FFFFFF;
height: 100%;
padding: 16px;
overflow: hidden;
span {
box-sizing: border-box;
}
&-search {
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
&-left {
display: flex;
}
&-right {}
}
&-top {
margin-top: 20px;
height: 54px;
display: flex;
&-left {
width: 180px;
margin-right: 20px;
&-title {
display: flex;
justify-content: space-evenly;
align-items: center;
height: 54px;
border: 1px solid #EDEDED;
background: #F4F6FD;
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #606266;
}
}
&-right {
border-top: 1px solid #EDEDED;
border-left: 1px solid #EDEDED;
// border-right: 1px solid #EDEDED;
&::-webkit-scrollbar {
width: 0px;
height: 0px;
}
width: calc(100% - 200px);
overflow-x: auto;
white-space: nowrap;
overflow-y: hidden;
background: #F4F6FD;
&-one {
height: 28px;
span {
display: inline-block;
height: 28px;
line-height: 28px;
text-align: center;
border-right: 1px solid #EDEDED;
font-size: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
color: #303133;
border-bottom: 1px solid #EDEDED;
}
}
&-two {
// display: flex;
// align-items: center;
height: 26px;
span {
display: inline-block;
height: 26px;
line-height: 26px;
text-align: center;
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #606266;
border-right: 1px solid #EDEDED;
}
}
}
}
&-bottom {
height: calc(100% - 155px);
display: flex;
// background-color: red;
&-left {
&::-webkit-scrollbar {
width: 10px;
height: 0px;
}
border-left: 1px solid #EDEDED;
border-right: 1px solid #EDEDED;
border-bottom: 1px solid #EDEDED;
width: 180px;
margin-right: 20px;
overflow-y: auto;
overflow-x: hidden;
.item-father {
display: flex;
align-items: center;
border-bottom: 1px solid #EDEDED;
// border-left: 1px solid #EDEDED;
// border-right: 1px solid #EDEDED;
width: 180px;
height: 32px;
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #010000;
span:nth-child(1) {
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
color: #2675FB;
border: 1px solid #2675FB;
margin-left: 20px;
margin-right: 5px;
cursor: pointer;
}
span:nth-child(2) {
width: 140px;
white-space: nowrap; //禁止换行
overflow: hidden;
text-overflow: ellipsis; //...
text-align: left;
margin-left: 5px;
}
}
.item-son {
display: flex;
align-items: center;
width: 180px;
height: 32px;
border-bottom: 1px solid #EDEDED;
// border-left: 1px solid #EDEDED;
// border-right: 1px solid #EDEDED;
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #606266;
justify-content: space-evenly;
cursor: pointer;
&>div:nth-child(1) {
width: 43px;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
white-space: nowrap;
}
&>div:nth-child(2) {
text-align: center;
width: 38px;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
white-space: nowrap;
}
}
}
&-right {
width: calc(100% - 200px);
overflow: auto;
white-space: nowrap;
border-left: 1px solid #EDEDED;
&::-webkit-scrollbar {
width: 0px;
height: 10px;
}
// border-right: 1px solid #EDEDED;
// &-item :last-child {
// border-bottom: 1px solid #EDEDED;
// }
&-item {}
.item-father {
height: 32px;
// span:nth-child(1) {
// border-left: 1px solid #EDEDED;
// }
span {
display: inline-block;
height: 32px;
border-right: 1px solid #EDEDED;
background: rgba(38, 117, 251, 0.12);
}
}
.item-son {
height: 32px;
position: relative;
// span:nth-child(1) {
// border-left: 1px solid #EDEDED;
// }
span {
display: inline-block;
height: 32px;
border-right: 1px solid #EDEDED;
}
&-progress {
position: absolute;
top: 15px;
height: 4px;
background: #2675FB;
border-radius: 3px;
// width: 100px;
}
}
}
}
}
.dialog {
&-title {
// width: 112px;
// height: 22px;
font-size: 16px;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
color: #303133;
}
&-body {
border-top: 1px solid #E9E9E9;
padding-top: 20px;
&-detail {
&>div:nth-child(1) {
height: 32px;
display: flex;
align-items: center;
&>span:nth-child(1) {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #FAAD14;
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #FFFFFF;
line-height: 32px;
text-align: center;
}
&>span:nth-child(2) {
margin: 0 10px;
font-size: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
color: #181818;
}
&>span:nth-child(3) {
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #909399;
}
}
&>div:nth-child(2),
&>div:nth-child(3) {
display: flex;
margin-top: 20px;
&>span:nth-child(1),
&>span:nth-child(3) {
width: 98px;
font-size: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #606266;
}
&>span:nth-child(2),
&>span:nth-child(4) {
width: 170px;
font-size: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #010000;
}
&>span:nth-child(3) {
margin-left: 100px;
}
}
}
&-table {
margin-top: 20px;
margin-bottom: 30px;
display: flex;
&-left {
width: 160px;
// height: 360px;
&-title {
height: 32px;
line-height: 32px;
text-align: center;
background: #F4F6FD;
}
&-table {
&-item {
height: 80px;
border-bottom: 1px solid #E9E9E9;
border-left: 1px solid #E9E9E9;
border-right: 1px solid #E9E9E9;
line-height: 80px;
text-align: center;
}
}
}
&-right {
margin-left: 20px;
width: calc(100% - 180px);
// height: 360px;
border-left: 1px solid #EDEDED;
&-title {
&::-webkit-scrollbar {
width: 0px;
height: 0px;
}
height: 32px;
width: 100%;
line-height: 32px;
text-align: center;
background: #F4F6FD;
overflow-x: auto;
white-space: nowrap;
overflow-y: hidden;
span {
display: inline-block;
height: 32px;
width: 30px;
border-right: 1px solid #EDEDED;
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
// color: #606266;
}
}
&-table {
width: 100%;
overflow-x: auto;
white-space: nowrap;
overflow-y: hidden;
&::-webkit-scrollbar {
width: 0px;
height: 5px;
}
&-item {
height: 80px;
width: 100%;
position: relative;
span {
display: inline-block;
height: 80px;
width: 30px;
border-right: 1px solid #EDEDED;
}
&-progress {
position: absolute;
height: 56px;
top: 12px;
background: #FFFFFF;
box-shadow: 0px 0px 6px 0px rgba(227, 227, 227, 0.5);
border-radius: 4px;
padding: 10px;
border-bottom: 2px solid #52C41A;
&>div:nth-child(1) {
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #52C41A;
margin-bottom: 5px;
}
&>div:nth-child(2) {
font-size: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #606266;
}
}
}
}
}
}
}
}
</style>