当前位置:   article > 正文

vue 实现项目进度甘特图

vue 实现项目进度甘特图

 项目需求:

实现以1天、7天、30天为周期(周期根据筛选条件选择),展示每个项目不同里程碑任务进度。

项目在Vue-Gantt-chart: 使用Vue做数据控制的Gantt图表基础上进行了改造。

有需要的小伙伴也可以直接引入插件,自己修改。

 我是直接把甘特图封装成了组件,结构如下图:

 

 首先,安装插件

npm install v-gantt-chart

引入插件(我是全局引入的)

  1. import vGanttChart from 'v-gantt-chart';
  2. Vue.use(vGanttChart);

 代码如下:

index.js

  1. <template>
  2. <div class="container">
  3. <v-gantt-chart
  4. :startTime="times[0]"
  5. :endTime="times[1]"
  6. :cellWidth="cellWidth"
  7. :cellHeight="cellHeight"
  8. :timeLines="timeLines"
  9. :titleHeight="titleHeight"
  10. :scale="Number(1440 * scale)"
  11. :titleWidth="titleWidth"
  12. showCurrentTime
  13. :hideHeader="hideHeader"
  14. :dataKey="dataKey"
  15. :arrayKeys="arrayKeys"
  16. :scrollToTime="scrollToTime"
  17. :scrollToPostion="positionA"
  18. @scrollLeft="scrollLeftA"
  19. customGenerateBlocks
  20. :datas="ganttData"
  21. >
  22. <template
  23. v-slot:block="{
  24. data,
  25. getPositonOffset,
  26. getWidthAbout2Times,
  27. isInRenderingTimeRange,
  28. startTimeOfRenderArea,
  29. endTimeOfRenderArea,
  30. isAcrossRenderingTimeRange
  31. }"
  32. >
  33. <div
  34. class="gantt-block-item"
  35. v-for="(item, index) in data.gtArray"
  36. v-if="
  37. isInRenderingTimeRange(item.start) ||
  38. isInRenderingTimeRange(item.end) ||
  39. isAcrossRenderingTimeRange(item.start, item.end)
  40. "
  41. :key="item.id"
  42. :style="{
  43. left: getPositonOffset(item.start) + 'px',
  44. width: getWidthAbout2Times(item.start, item.end) + 'px',
  45. height: judgeTime(data.gtArray) ? '50%' : '100%',
  46. top: !judgeTime(data.gtArray)
  47. ? ''
  48. : index % 2 !== 1
  49. ? '0px'
  50. : '22px'
  51. }"
  52. >
  53. <Test
  54. :data="data"
  55. :updateTimeLines="updateTimeLines"
  56. :cellHeight="cellHeight"
  57. :currentTime="currentTime"
  58. :item="item"
  59. @nodeEvent="nodeEvent"
  60. ></Test>
  61. </div>
  62. </template>
  63. <template v-slot:left="{ data }">
  64. <TestLeft :data="data" @panelDb="panelDb"></TestLeft>
  65. </template>
  66. <!-- <template v-slot:timeline="{ day , getTimeScales }">
  67. <TestTimeline :day="day" :getTimeScales="getTimeScales"></TestTimeline>
  68. </template> -->
  69. <template v-slot:title>
  70. <div class="title">名称</div>
  71. </template>
  72. </v-gantt-chart>
  73. </div>
  74. </template>
  75. <script>
  76. import moment from 'moment';
  77. import Test from './components/test.vue';
  78. import TestLeft from './components/test-left.vue';
  79. import TestTimeline from './components/test-timeline.vue';
  80. import TestMarkline from './components/test-markline.vue';
  81. import dayjs from 'dayjs';
  82. export default {
  83. name: '',
  84. components: { Test, TestLeft, TestTimeline, TestMarkline },
  85. props: {
  86. ganttData: {
  87. type: Array,
  88. default: () => []
  89. },
  90. scaleData: {
  91. type: Number,
  92. default: 10080
  93. },
  94. scrollToTime: {
  95. type: String,
  96. default: moment().subtract(4, 'days').format('YYYY-MM-DD')
  97. }
  98. },
  99. data() {
  100. return {
  101. timeLines: [],
  102. currentTime: dayjs(),
  103. cellWidth: 100,
  104. cellHeight: 50,
  105. titleHeight: 50,
  106. titleWidth: 250,
  107. // scale: 1440 * 30,
  108. startDate: moment().startOf('year'),
  109. endDate: moment().endOf('year'),
  110. times: [
  111. moment().subtract(1, 'year').format('YYYY-MM-DD hh:mm:ss'),
  112. moment().add(6, 'months').format('YYYY-MM-DD hh:mm:ss')
  113. ],
  114. rowNum: 100,
  115. colNum: 10,
  116. datasB: [],
  117. dataKey: 'projectId',
  118. // scrollToTime: moment().subtract(14, 'days').format('YYYY-MM-DD'),
  119. // scrollToTime: moment().subtract(4, 'days').format('YYYY-MM-DD'),
  120. scrollToPostion: { x: 10000, y: 10000 },
  121. hideHeader: false,
  122. hideSecondGantt: false,
  123. arrayKeys: ['gtArray'],
  124. scrollToY: 0,
  125. positionB: {},
  126. positionA: {}
  127. };
  128. },
  129. watch: {
  130. scrollToY(val) {
  131. this.positionA = { x: val };
  132. },
  133. ganttData(newVal, oldVal) {
  134. console.log('newVal===', newVal);
  135. console.log('oldVal===', oldVal);
  136. }
  137. },
  138. computed: {
  139. scale() {
  140. console.log(this.scaleData);
  141. return this.scaleData / 1440;
  142. }
  143. },
  144. methods: {
  145. judgeTime(arr) {
  146. let startTimeArr = [];
  147. let endTimeArr = [];
  148. arr.map(function (item) {
  149. startTimeArr.push(
  150. item.startDate ? new Date(item.startDate).getTime() : ''
  151. );
  152. endTimeArr.push(
  153. item.delayDate
  154. ? new Date(item.delayDate).getTime()
  155. : item.endDate
  156. ? new Date(item.endDate).getTime()
  157. : ''
  158. );
  159. });
  160. let allStartTime = startTimeArr.sort(); // 排序
  161. let allEndTime = endTimeArr.sort();
  162. let result = 0; // 判断时间是否有重复区间
  163. for (let k = 0; k < allStartTime.length; k++) {
  164. if (k > 0) {
  165. if (allStartTime[k] <= allEndTime[k - 1]) {
  166. result += 1;
  167. }
  168. }
  169. }
  170. return result > 0;
  171. },
  172. nodeEvent(item) {
  173. this.$emit('nodeEventClick', item);
  174. },
  175. panelDb(item) {
  176. this.$emit('panelDbClick', item);
  177. },
  178. updateTimeLines(timeA, timeB) {
  179. this.timeLines = [
  180. {
  181. time: timeA,
  182. text: '自定义'
  183. },
  184. {
  185. time: timeB,
  186. text: '测试',
  187. color: '#747e80'
  188. }
  189. ];
  190. },
  191. scrollLeftA(val) {
  192. this.positionB = { x: val };
  193. }
  194. }
  195. };
  196. </script>
  197. <style lang="scss" scoped>
  198. .container {
  199. height: 82vh;
  200. background-color: #f5faff;
  201. }
  202. .title {
  203. width: 100%;
  204. height: 100%;
  205. color: #96aaca;
  206. background: #f5faff;
  207. }
  208. :deep(.gantt-leftbar-wrapper) {
  209. border-right: 1px solid #c6d8ee !important;
  210. }
  211. </style>

test-left.vue

  1. <template>
  2. <div class="name">
  3. <div class="carId" @dblclick="onDblclick" >{{ data.projectName }}</div>
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: "TestLeft",
  9. props: {
  10. data: Object,
  11. },
  12. methods: {
  13. onDblclick() {
  14. // this.updateTimeLines(this.item.start, this.item.end);
  15. this.$emit('panelDb', this.data);
  16. }
  17. }
  18. };
  19. </script>
  20. <style scoped>
  21. .name {
  22. color: #000000;
  23. display: flex;
  24. box-sizing: border-box;
  25. overflow: hidden;
  26. height: 100%;
  27. width: 100%;
  28. padding: 10px 0;
  29. align-items: center;
  30. text-align: center;
  31. background: #f5faff;
  32. box-shadow: 2px 0px 4px 0px rgba(0, 0, 0, 0.1);
  33. }
  34. .carId {
  35. flex: 1;
  36. }
  37. .type {
  38. padding: 0 5px 0 0;
  39. font-size: 1.2rem;
  40. }
  41. </style>

test-markline.vue

  1. <template>
  2. <div
  3. class="markline"
  4. :style="{ left: getPosition() + 'px' }"
  5. >
  6. <div class="markline-label">
  7. {{timeConfig.text}}{{ dayjs(timeConfig.time).format("HH:mm:ss") }}
  8. </div>
  9. </div>
  10. </template>
  11. <script>
  12. import dayjs from "dayjs"
  13. export default {
  14. name: "TestMarkLine",
  15. props:['getPosition','timeConfig'],
  16. data(){
  17. return {
  18. dayjs
  19. }
  20. }
  21. }
  22. </script>
  23. <style lang="scss" scoped>
  24. .markline {
  25. position: absolute;
  26. z-index: 100;
  27. width: 2px;
  28. height: 100vh;
  29. background: #747e80;
  30. &-label {
  31. padding: 3px;
  32. width: 6rem;
  33. margin-left: -3rem;
  34. margin-top: 5rem;
  35. color: #fff;
  36. background: #747e80;
  37. text-align: center;
  38. border-radius: 5px;
  39. font-size: 0.7rem;
  40. }
  41. }
  42. </style>

test-timeline.vue

  1. <template>
  2. <div class="test">
  3. <span v-for="i in getTimeScales(day)"> {{i.format('HH:mm')}}</span>
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: "TestLeft",
  9. props: {
  10. day: Object,
  11. getTimeScales:Function,
  12. }
  13. };
  14. </script>
  15. <style lang="scss" scoped>
  16. .test{
  17. display: flex;
  18. span{
  19. flex:1
  20. }
  21. }
  22. </style>

test.vue

  1. <template>
  2. <el-popover placement="bottom" trigger="hover">
  3. <div
  4. slot="reference"
  5. class="plan"
  6. :style="{
  7. 'background-color': statusColor,
  8. 'margin-top': 0.1 * cellHeight + 'px'
  9. }"
  10. @click="onClick"
  11. >
  12. <div class="middle">{{ item.summary }}</div>
  13. </div>
  14. <div class="detail">{{ item.summary }}</div>
  15. </el-popover>
  16. </template>
  17. <script>
  18. import dayjs from 'dayjs';
  19. export default {
  20. name: 'Test',
  21. props: {
  22. data: Object,
  23. item: Object,
  24. currentTime: dayjs,
  25. updateTimeLines: Function,
  26. cellHeight: Number,
  27. startTimeOfRenderArea: Number
  28. },
  29. data() {
  30. return {
  31. dayjs: dayjs,
  32. stateObj: {
  33. DelayStart: '#F56C6C',
  34. Normal: '#C2F1E2',
  35. NoStart: '#D9E3ED',
  36. Delay: '#F56C6C',
  37. Stop: '#D9E3ED',
  38. DelayRisk: '#FFD4C7',
  39. NoControl: '#F56C6C',
  40. Close: '#D9E3ED'
  41. }
  42. };
  43. },
  44. computed: {
  45. statusColor() {
  46. console.log('data=======', this.data);
  47. let { item } = this;
  48. return this.stateObj[item.state] || '#D9E3ED';
  49. },
  50. startToString() {
  51. return dayjs(this.item.start).format('HH:mm');
  52. },
  53. endToString() {
  54. return dayjs(this.item.end).format('HH:mm');
  55. }
  56. },
  57. methods: {
  58. onClick() {
  59. // this.updateTimeLines(this.item.start, this.item.end);
  60. this.$emit('nodeEvent', this.item);
  61. }
  62. }
  63. };
  64. </script>
  65. <style lang="scss" scoped>
  66. .middle {
  67. flex: 1;
  68. text-align: center;
  69. padding-left: 5px;
  70. text-overflow: ellipsis; /* ellipsis:显示省略符号来代表被修剪的文本 string:使用给定的字符串来代表被修剪的文本*/
  71. white-space: nowrap; /* nowrap:规定段落中的文本不进行换行 */
  72. overflow: hidden; /*超出部分隐藏*/
  73. }
  74. .runTime {
  75. display: flex;
  76. flex-direction: column;
  77. }
  78. .plan {
  79. display: flex;
  80. align-items: center;
  81. box-sizing: border-box;
  82. height: 80%;
  83. border: 1px solid #f0f0f0;
  84. border-radius: 5px;
  85. color: #333333;
  86. padding-left: 5px;
  87. font-size: 0.8rem;
  88. // opacity: 0.8;
  89. }
  90. .detail {
  91. .header {
  92. text-align: center;
  93. font-size: 1rem;
  94. }
  95. }
  96. .detail ul {
  97. list-style: none;
  98. padding: 0px;
  99. li {
  100. span {
  101. display: inline-block;
  102. width: 80px;
  103. color: #777;
  104. font-size: 0.8rem;
  105. }
  106. span:first-child {
  107. text-align: right;
  108. }
  109. span:last-child {
  110. }
  111. }
  112. }
  113. </style>

页面中使用

  1. <div>
  2. <ganttChart
  3. :ganttData="ganttArr"
  4. :scaleData="scaleData"
  5. :scrollToTime="scrollToTime"
  6. @nodeEventClick="nodeEventClick"
  7. @panelDbClick="panelDbClick"
  8. ></ganttChart>
  9. </div>
  1. import moment from 'moment';
  2. import ganttChart from './components/ganttChart/index.vue';
  3. export default {
  4. components: { ganttChart },
  5. data(){
  6. return{
  7. ganttArr: [],
  8. scaleData: 10080,
  9. scrollToTime: moment().subtract(4, 'days').format('YYYY-MM-DD'),
  10. }
  11. },
  12. methods: {
  13. // 点击甘特图node节点
  14. nodeEventClick(item) {
  15. // 执行自己的逻辑
  16. },
  17. // 双击甘特图左侧标题
  18. panelDbClick(item) {
  19. //执行自己的逻辑
  20. }
  21. }
  22. }

以上就是实现甘特图的全部过程,欢迎大佬们指教。

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

闽ICP备14008679号