当前位置:   article > 正文

vue3使用element Plus中的table组件进行封装实现甘特图(gantt)任务管理_vue3 甘特图组件

vue3 甘特图组件

1、理解甘特图实现的方式和原理

  甘特图是一种用于管理时间和任务活动的工具,它能够将活动列表以及时间、顺序以图形方式直观展示,方便管理者查看活动计划、跟进任务进度、合理分配资源。甘特图主要应用于项目管理,具有直观展示、制作简单、便于理解等特点。

  其主要是展示任务起始、结束时间和任务进度等信息的一个图表。根据这些要展示的数据,其需要解决的问题有几点。

1.怎么在table组件上显示时间轴;

2.计算任务的进度条长度;

3.怎么算任务在时间轴上跨越了哪些时间;

4.怎么计算起始时间在第一个显示时间轴的左侧距离。

2、计算甘特图时间轴

  通过方法判断任务数据中的最小值和最大值来确定时间轴上的最小时间轴和最大时间轴,在通过循环的方法,获取所有时间轴的数据用来渲染时间轴。

2.1、任务数据处理获取最小值和最大值

  1. const getMinMax = async () => {
  2. if (ganttList.value.length === 0) {
  3. return;
  4. } else {
  5. ganttList.value.forEach((element, ind) => {
  6. if (ind === 0) {
  7. timeMin.value = element.startTime;
  8. timeMax.value = element.endTime;
  9. // console.log(
  10. // timeMin.value,
  11. // "最小时间轴值",
  12. // timeMax.value,
  13. // "最大时间轴值",
  14. // "0000000"
  15. // );
  16. } else {
  17. if (
  18. new Date(ganttList.value[ind].startTime) < new Date(timeMin.value)
  19. ) {
  20. timeMin.value = element.startTime;
  21. }
  22. if (new Date(ganttList.value[ind].endTime) > new Date(timeMax.value)) {
  23. timeMax.value = element.endTime;
  24. }
  25. // console.log(
  26. // timeMin.value,
  27. // "最小时间轴值",
  28. // timeMax.value,
  29. // "最大时间轴值",
  30. // "1111111"
  31. // );
  32. }
  33. });
  34. }
  35. };

2.2、通过算出的最小和最大时间,算出所有时间轴上的时间数据

  1. //通过时间轴最大值和最小值获取table表头时间轴数据
  2. const time1 = ref("");
  3. if (timeMin.value !== "") {
  4. time1.value = new Date(timeMin.value.substring(0, 10)).getTime();
  5. }
  6. // console.log(time1.value, "999999");
  7. const date = new Date();
  8. const timeArr = ref([]);
  9. if (timeMin.value !== "") {
  10. timeArr.value = [timeMin.value.substring(0, 10)];
  11. }
  12. //循环获取表头时间轴数组
  13. if (time1.value !== "") {
  14. for (let i = 1; i++; ) {
  15. time1.value = time1.value + 3600 * 1000 * 24;
  16. date.setTime(time1.value);
  17. const a =
  18. date.getFullYear() +
  19. "-" +
  20. (date.getMonth() + 1) +
  21. "-" +
  22. (date.getDate() < 10 ? "0" + date.getDate() : date.getDate());
  23. timeArr.value.push(a);
  24. if (a === timeMax.value.substring(0, 10)) {
  25. break;
  26. }
  27. }
  28. }

2.3、时间轴数据渲染方式是通过循环渲染table-column来实现的

  1. <el-table-column
  2. prop=""
  3. :label="item.time"
  4. width="100"
  5. v-for="(item, ind) in dateArr"
  6. :key="ind"
  7. align="center"
  8. >
  9. </el-table-column>

3、gantt任务所在的位置、长度和跨度等处理

3.1、计算处理任务下标、长度和起始距左侧距离

  1. //gantt数据处理获取每条数据的展示时间在table组件的下标值、跨度和长度
  2. const getTask = async () => {
  3. if (ganttList.value.length !== 0) {
  4. ganttList.value.forEach((element, ind) => {
  5. // console.log(ind, "啦啦啦,德玛西亚111111");
  6. //进度条跨度
  7. const timeLength = (
  8. (new Date(element.endTime.substring(0, 10)) -
  9. new Date(element.startTime.substring(0, 10))) /
  10. (1000 * 3600 * 24)
  11. ).toFixed(2);
  12. //进度条宽度
  13. const taskLength =
  14. (new Date(element.endTime) - new Date(element.startTime)) /
  15. (1000 * 3600 * 24);
  16. // console.log(timeLength, "啦啦啦,德玛西亚");
  17. //进度条时间跨度
  18. let timeSpan = 0;
  19. if (element.endTime.substring(11) !== "00:00:00") {
  20. timeSpan = Math.ceil(timeLength) + 1;
  21. } else {
  22. timeSpan = Math.ceil(timeLength);
  23. }
  24. //赋值共执行的天数
  25. element.days = Math.ceil(timeLength);
  26. //任务条数据宽度计算
  27. element.width = (Number(taskLength) / timeSpan).toFixed(4) * 100 + "%";
  28. // console.log((new Date(element.startTime).getTime()-(new Date(element.startTime.substring(0,10)+" "+"00:00:00")).getTime())/(1000 * 3600 * 24));
  29. //任务离左侧的距离
  30. element.marginLeft =
  31. (
  32. (new Date(element.startTime).getTime() -
  33. new Date(
  34. element.startTime.substring(0, 10) + " " + "00:00:00"
  35. ).getTime()) /
  36. (1000 * 3600 * 24)
  37. ).toFixed(4) *
  38. 100 +
  39. "px";
  40. //起始的table竖轴下标
  41. const startInd = (
  42. (new Date(element.startTime.substring(0, 10)) -
  43. new Date(timeMin.value.substring(0, 10))) /
  44. (1000 * 3600 * 24)
  45. ).toFixed(0);
  46. // console.log(startInd, timeSpan, "啦啦啦,德玛西亚");
  47. taskArr.value.push({ index: startInd, span: timeSpan });
  48. });
  49. }
  50. };

3.2、基本处理完成,但需要处理table组件行内合并显示任务条

  1. //gantt列合并方法
  2. const arraySpanMethod = ({ row, column, rowIndex, columnIndex }) => {
  3. const num = taskArr.value.length;
  4. for (let i = 0; i <= num; i++) {
  5. if (rowIndex === i) {
  6. // console.log(1, "-------");
  7. if (columnIndex === Number(taskArr.value[i].index) + 1) {
  8. // console.log(2, "--------");
  9. // console.log(element.span,"99999");
  10. return [1, Number(taskArr.value[i].span)];
  11. } else if (
  12. columnIndex > Number(taskArr.value[i].index) &&
  13. columnIndex <=
  14. Number(taskArr.value[i].index) + Number(taskArr.value[i].span)
  15. ) {
  16. return [0, 0];
  17. }
  18. }
  19. }
  20. };

4、如何渲染任务条,确保数据对应显示

  通过数据处理,判断数据对应的id和起始时间是否相同来渲染任务条的显示。

  1. //时间轴表头数据处理显示
  2. const dateArr = ref([]);
  3. if (timeArr.value.length !== 0) {
  4. timeArr.value.forEach((element, ind) => {
  5. const obj = { id: ind + 1, time: element, arr: [] };
  6. dateArr.value.push(obj);
  7. });
  8. }
  9. // console.log(dateArr.value, "时间轴标题数据处理值");
  10. //判断每一天任务起始时间数据在时间轴的下标,存入数据的arr属性里面做数据显示处理
  11. if (ganttList.value.length !== 0) {
  12. ganttList.value.forEach((element) => {
  13. dateArr.value.forEach((ele) => {
  14. if (element.startTime.substring(0, 10) === ele.time) {
  15. // console.log(element.startTime);
  16. ele.arr.push(element.id);
  17. }
  18. });
  19. });
  20. }
  21. //处理后的数据如下所示
  22. //const arr=[
  23. //{id:0,time:"2023-11-15",arr:[1,4]},
  24. //{id:1,time:"2023-11-16",arr:[3,7]},
  25. //{id:2,time:"2023-11-17",arr:[2,6]},
  26. //];

   通过处理后的数据,判断每一个任务起始时间属于哪个时间轴就渲染显示,这就能确定每一个任务条能准确的在时间轴上按起始时间到结束时间显示渲染。

  1. <el-table
  2. :data="ganttList"
  3. border
  4. class="table-style boderRNone"
  5. style="width: 100%"
  6. :span-method="arraySpanMethod"
  7. max-height="270"
  8. >
  9. <el-table-column
  10. prop="name"
  11. fixed
  12. label="导管概览示意图"
  13. width="150"
  14. align="center"
  15. >
  16. <template #default="scope">
  17. <div class="con-head">
  18. <div class="name">{{ scope.row.name }}</div>
  19. <div class="position">
  20. {{ scope.row.positionName ? scope.row.positionName : "" }}
  21. </div>
  22. </div>
  23. </template>
  24. </el-table-column>
  25. <el-table-column
  26. prop=""
  27. :label="item.time"
  28. width="100"
  29. v-for="(item, ind) in dateArr"
  30. :key="ind"
  31. align="center"
  32. >
  33. <template #default="scope">
  34. <div v-for="(ele, ind) in item.arr" :key="ind" style="width: 100%">
  35. <div
  36. class="gantt-box"
  37. style="height: 20px"
  38. v-if="scope.row.id === ele"
  39. >
  40. <div
  41. class="gantt-progress"
  42. :style="{
  43. backgroundColor: scope.row.color,
  44. height: taskHeight,
  45. width: scope.row.width,
  46. marginLeft: scope.row.marginLeft,
  47. }"
  48. >
  49. <el-tooltip
  50. class="box-item"
  51. effect="light"
  52. :content="'置管时间:' + scope.row.startTime"
  53. placement="right"
  54. >
  55. <div class="start"></div>
  56. </el-tooltip>
  57. <el-tooltip
  58. class="box-item"
  59. effect="light"
  60. :content="'拔管时间:' + scope.row.endTime"
  61. placement="left"
  62. >
  63. <div class="end" v-if="scope.row.status === 1"></div>
  64. </el-tooltip>
  65. </div>
  66. </div>
  67. </div>
  68. </template>
  69. </el-table-column>
  70. <el-table-column prop="" fixed="right" label="" width="120" align="center">
  71. <template #default="scope">
  72. <span>共{{ scope.row.days }}天</span>
  73. </template>
  74. </el-table-column>
  75. </el-table>
5、该代码已写成组件,可通过任务数据的父向子传值直接生成对应的甘特图,所有代码如下
  1. <script setup>
  2. import { ref, toRefs } from "vue";
  3. const props = defineProps({
  4. //子组件接收父组件传递过来的值
  5. info: String,
  6. });
  7. // console.log(props.info, "父组件传过来的数据");
  8. const ganttList = ref([]);
  9. ganttList.value = props.info;
  10. //gantt数据列表数据处理获取table组件表头时间最大值和最小值
  11. const timeMin = ref("");
  12. const timeMax = ref("");
  13. const getMinMax = async () => {
  14. if (ganttList.value.length === 0) {
  15. return;
  16. } else {
  17. ganttList.value.forEach((element, ind) => {
  18. if (ind === 0) {
  19. timeMin.value = element.startTime;
  20. timeMax.value = element.endTime;
  21. // console.log(
  22. // timeMin.value,
  23. // "最小时间轴值",
  24. // timeMax.value,
  25. // "最大时间轴值",
  26. // "0000000"
  27. // );
  28. } else {
  29. if (
  30. new Date(ganttList.value[ind].startTime) < new Date(timeMin.value)
  31. ) {
  32. timeMin.value = element.startTime;
  33. }
  34. if (new Date(ganttList.value[ind].endTime) > new Date(timeMax.value)) {
  35. timeMax.value = element.endTime;
  36. }
  37. // console.log(
  38. // timeMin.value,
  39. // "最小时间轴值",
  40. // timeMax.value,
  41. // "最大时间轴值",
  42. // "1111111"
  43. // );
  44. }
  45. });
  46. }
  47. };
  48. getMinMax();
  49. // console.log(
  50. // timeMin.value,
  51. // "最小时间轴值",
  52. // timeMax.value,
  53. // "最大时间轴值",
  54. // "22222222"
  55. // );
  56. //通过时间轴最大值和最小值获取table表头时间轴数据
  57. const time1 = ref("");
  58. if (timeMin.value !== "") {
  59. time1.value = new Date(timeMin.value.substring(0, 10)).getTime();
  60. }
  61. // console.log(time1.value, "999999");
  62. const date = new Date();
  63. const timeArr = ref([]);
  64. if (timeMin.value !== "") {
  65. timeArr.value = [timeMin.value.substring(0, 10)];
  66. }
  67. //循环获取表头时间轴数组
  68. if (time1.value !== "") {
  69. for (let i = 1; i++; ) {
  70. time1.value = time1.value + 3600 * 1000 * 24;
  71. date.setTime(time1.value);
  72. const a =
  73. date.getFullYear() +
  74. "-" +
  75. (date.getMonth() + 1) +
  76. "-" +
  77. (date.getDate() < 10 ? "0" + date.getDate() : date.getDate());
  78. timeArr.value.push(a);
  79. if (a === timeMax.value.substring(0, 10)) {
  80. break;
  81. }
  82. }
  83. }
  84. // console.log(timeArr.value, "时间轴表头数据列表");
  85. //时间轴表头数据处理显示
  86. const dateArr = ref([]);
  87. if (timeArr.value.length !== 0) {
  88. timeArr.value.forEach((element, ind) => {
  89. const obj = { id: ind + 1, time: element, arr: [] };
  90. dateArr.value.push(obj);
  91. });
  92. }
  93. // console.log(dateArr.value, "时间轴标题数据处理值");
  94. //判断每一天任务起始时间数据在时间轴的下标,存入数据的arr属性里面做数据显示处理
  95. if (ganttList.value.length !== 0) {
  96. ganttList.value.forEach((element) => {
  97. dateArr.value.forEach((ele) => {
  98. if (element.startTime.substring(0, 10) === ele.time) {
  99. // console.log(element.startTime);
  100. ele.arr.push(element.id);
  101. }
  102. });
  103. });
  104. }
  105. console.log(dateArr.value, "时间轴标题数据处理存入任务id值");
  106. //数据处理判断
  107. const taskArr = ref([]);
  108. //gantt数据处理获取每条数据的展示时间在table组件的下标值、跨度和长度
  109. const getTask = async () => {
  110. if (ganttList.value.length !== 0) {
  111. ganttList.value.forEach((element, ind) => {
  112. // console.log(ind, "啦啦啦,德玛西亚111111");
  113. //进度条跨度
  114. const timeLength = (
  115. (new Date(element.endTime.substring(0, 10)) -
  116. new Date(element.startTime.substring(0, 10))) /
  117. (1000 * 3600 * 24)
  118. ).toFixed(2);
  119. //进度条宽度
  120. const taskLength =
  121. (new Date(element.endTime) - new Date(element.startTime)) /
  122. (1000 * 3600 * 24);
  123. // console.log(timeLength, "啦啦啦,德玛西亚");
  124. //进度条时间跨度
  125. let timeSpan = 0;
  126. if (element.endTime.substring(11) !== "00:00:00") {
  127. timeSpan = Math.ceil(timeLength) + 1;
  128. } else {
  129. timeSpan = Math.ceil(timeLength);
  130. }
  131. //赋值共执行的天数
  132. element.days = Math.ceil(timeLength);
  133. //任务条数据宽度计算
  134. element.width = (Number(taskLength) / timeSpan).toFixed(4) * 100 + "%";
  135. // console.log((new Date(element.startTime).getTime()-(new Date(element.startTime.substring(0,10)+" "+"00:00:00")).getTime())/(1000 * 3600 * 24));
  136. //任务离左侧的距离
  137. element.marginLeft =
  138. (
  139. (new Date(element.startTime).getTime() -
  140. new Date(
  141. element.startTime.substring(0, 10) + " " + "00:00:00"
  142. ).getTime()) /
  143. (1000 * 3600 * 24)
  144. ).toFixed(4) *
  145. 100 +
  146. "px";
  147. //起始的table竖轴下标
  148. const startInd = (
  149. (new Date(element.startTime.substring(0, 10)) -
  150. new Date(timeMin.value.substring(0, 10))) /
  151. (1000 * 3600 * 24)
  152. ).toFixed(0);
  153. // console.log(startInd, timeSpan, "啦啦啦,德玛西亚");
  154. taskArr.value.push({ index: startInd, span: timeSpan });
  155. });
  156. }
  157. };
  158. getTask();
  159. // console.log(ganttList.value, "计算任务跨度,长度,距离左侧的距离等后的数据值");
  160. // console.log(taskArr.value, "任务数据处理后的值");
  161. //gantt列合并方法
  162. const arraySpanMethod = ({ row, column, rowIndex, columnIndex }) => {
  163. const num = taskArr.value.length;
  164. for (let i = 0; i <= num; i++) {
  165. if (rowIndex === i) {
  166. // console.log(1, "-------");
  167. if (columnIndex === Number(taskArr.value[i].index) + 1) {
  168. // console.log(2, "--------");
  169. // console.log(element.span,"99999");
  170. return [1, Number(taskArr.value[i].span)];
  171. } else if (
  172. columnIndex > Number(taskArr.value[i].index) &&
  173. columnIndex <=
  174. Number(taskArr.value[i].index) + Number(taskArr.value[i].span)
  175. ) {
  176. return [0, 0];
  177. }
  178. }
  179. }
  180. };
  181. //任务条颜色
  182. const taskColor = "#436ff6";
  183. //任务条高度
  184. const taskHeight = ref("8px");
  185. </script>
  186. <template>
  187. <el-table
  188. :data="ganttList"
  189. border
  190. class="table-style boderRNone"
  191. style="width: 100%"
  192. :span-method="arraySpanMethod"
  193. max-height="270"
  194. >
  195. <el-table-column
  196. prop="name"
  197. fixed
  198. label="导管概览示意图"
  199. width="150"
  200. align="center"
  201. >
  202. <template #default="scope">
  203. <div class="con-head">
  204. <div class="name">{{ scope.row.name }}</div>
  205. <div class="position">
  206. {{ scope.row.positionName ? scope.row.positionName : "" }}
  207. </div>
  208. </div>
  209. </template>
  210. </el-table-column>
  211. <el-table-column
  212. prop=""
  213. :label="item.time"
  214. width="100"
  215. v-for="(item, ind) in dateArr"
  216. :key="ind"
  217. align="center"
  218. >
  219. <template #default="scope">
  220. <div v-for="(ele, ind) in item.arr" :key="ind" style="width: 100%">
  221. <div
  222. class="gantt-box"
  223. style="height: 20px"
  224. v-if="scope.row.id === ele"
  225. >
  226. <div
  227. class="gantt-progress"
  228. :style="{
  229. backgroundColor: scope.row.color,
  230. height: taskHeight,
  231. width: scope.row.width,
  232. marginLeft: scope.row.marginLeft,
  233. }"
  234. >
  235. <el-tooltip
  236. class="box-item"
  237. effect="light"
  238. :content="'置管时间:' + scope.row.startTime"
  239. placement="right"
  240. >
  241. <div class="start"></div>
  242. </el-tooltip>
  243. <el-tooltip
  244. class="box-item"
  245. effect="light"
  246. :content="'拔管时间:' + scope.row.endTime"
  247. placement="left"
  248. >
  249. <div class="end" v-if="scope.row.status === 1"></div>
  250. </el-tooltip>
  251. </div>
  252. </div>
  253. </div>
  254. </template>
  255. </el-table-column>
  256. <el-table-column prop="" fixed="right" label="" width="120" align="center">
  257. <template #default="scope">
  258. <span>共{{ scope.row.days }}天</span>
  259. </template>
  260. </el-table-column>
  261. </el-table>
  262. </template>
  263. <script setup></script>
  264. <style lang="scss" scoped>
  265. .main {
  266. height: 100%;
  267. }
  268. .gantt-box {
  269. display: flex;
  270. align-items: center;
  271. .gantt-progress {
  272. // width: 100%;
  273. // height: 8px;
  274. position: relative;
  275. height: 100%;
  276. // display: flex;
  277. // justify-content: space-between;
  278. > .start {
  279. position: absolute;
  280. left: -8px;
  281. top: -4px;
  282. // background-color: chocolate;
  283. // width: 16px;
  284. // height: 16px;
  285. width: 0;
  286. height: 0;
  287. border-left: 8px solid transparent;
  288. border-right: 8px solid transparent;
  289. border-top: 16px solid #436ff6;
  290. // border-bottom:0;
  291. z-index: 10;
  292. }
  293. > .end {
  294. position: absolute;
  295. right: -8px;
  296. top: -4px;
  297. // background-color: crimson;
  298. // width: 16px;
  299. // height: 16px;
  300. width: 0px;
  301. height: 0px;
  302. border-left: 8px solid transparent;
  303. border-right: 8px solid transparent;
  304. border-bottom: 16px solid #00b7ee;
  305. z-index: 12;
  306. }
  307. }
  308. }
  309. :deep(.cell) {
  310. padding: 0;
  311. width: 100%;
  312. }
  313. .con-head {
  314. width: 90%;
  315. height: 100%;
  316. margin: auto;
  317. display: flex;
  318. align-items: center;
  319. justify-content: space-between;
  320. .name {
  321. height: 14px;
  322. line-height: 14px;
  323. font-size: 14px;
  324. font-family: Microsoft YaHei UI;
  325. font-weight: 400;
  326. color: #000000;
  327. }
  328. .position {
  329. height: 12px;
  330. font-size: 12px;
  331. font-family: Microsoft YaHei UI;
  332. font-weight: 400;
  333. color: #666666;
  334. line-height: 12px;
  335. }
  336. }
  337. </style>

   实现结果

 

 

总结、

  该甘特图任务是个人项目时所写,可能存在精度等问题。只是一个实现gantt的思路和经验,可以在此基础上进行拓展和设计,希望可以提供给大家有用的帮助。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/499545
推荐阅读
相关标签
  

闽ICP备14008679号