| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | |
| | | <div |
| | | class="gantt-chart" |
| | | @wheel="wheelHandle" |
| | | > |
| | | <div |
| | | class="gantt-container" |
| | | :style="{height:`calc(100% - ${scrollXBarHeight}px)`,width:`calc(100% - ${scrollYBarWidth}px)`}" |
| | | > |
| | | <!-- <div class="gantt-container"> --> |
| | | <div |
| | | v-show="!hideHeader" |
| | | class="gantt-header" |
| | | :style="{width:`calc(100% + ${scrollYBarWidth}px)`}" |
| | | > |
| | | <div |
| | | class="gantt-header-title" |
| | | :style="{'line-height':titleHeight+'px',height:titleHeight+'px','width':titleWidth+'px'}" |
| | | > |
| | | <slot name="title">welcome v-gantt-chart</slot> |
| | | </div> |
| | | <div |
| | | ref="headerTimeline" |
| | | class="gantt-header-timeline" |
| | | > |
| | | <div |
| | | class="gantt-timeline-wrapper" |
| | | :style="{width:(totalWidth+scrollYBarWidth)+'px'}" |
| | | > |
| | | <timeline |
| | | :start="start" |
| | | :end="end" |
| | | :cell-width="cellWidth" |
| | | :title-height="titleHeight" |
| | | :scale="scale" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | class="gantt-body" |
| | | :style="{height:`calc(100% - ${actualHeaderHeight}px)`}" |
| | | > |
| | | <!-- <div class="gantt-body" style="height:500px"> --> |
| | | <div class="gantt-table"> |
| | | <div |
| | | ref="marklineArea" |
| | | :style="{marginLeft:(titleWidth+50)+'px'}" |
| | | class="gantt-markline-area" |
| | | > |
| | | <!-- <CurrentTime v-if="showCurrentTime" |
| | | :getPositonOffset="getPositonOffset" /> --> |
| | | <!-- <mark-line v-for="(times,index) in timeLines" |
| | | :key="index" |
| | | :markLineTime="times.time" |
| | | :getPositonOffset="getPositonOffset" |
| | | :color="times.color"></mark-line> --> |
| | | </div> |
| | | <div |
| | | ref="leftbarWrapper" |
| | | class="gantt-leftbar-wrapper" |
| | | :style="{'width':(titleWidth)+'px',height:`calc(100% + ${scrollXBarHeight}px)`}" |
| | | > |
| | | <LeftBar |
| | | :datas="datas" |
| | | :data-key="dataKey" |
| | | :scroll-top="scrollTop" |
| | | :height-of-render-aera="heightOfRenderAera" |
| | | :width-of-render-aera="widthOfRenderAera" |
| | | :cell-height="cellHeight" |
| | | :style="{height:(totalHeight+scrollXBarHeight)+'px'}" |
| | | > |
| | | <template slot-scope="{data}"> |
| | | <slot |
| | | name="left" |
| | | :data="data" |
| | | /> |
| | | </template> |
| | | </LeftBar> |
| | | </div> |
| | | <div |
| | | ref="blocksWrapper" |
| | | class="gantt-blocks-wrapper" |
| | | > |
| | | <blocks |
| | | :scroll-top="scrollTop" |
| | | :scroll-left="scrollLeft" |
| | | :height-of-render-aera="heightOfRenderAera" |
| | | :width-of-render-aera="widthOfRenderAera" |
| | | :array-keys="arrayKeys" |
| | | :item-key="itemKey" |
| | | :data-key="dataKey" |
| | | :datas="datas" |
| | | :cell-width="cellWidth" |
| | | :cell-height="cellHeight" |
| | | :scale="scale" |
| | | :get-positon-offset="getPositonOffset" |
| | | :get-width-about2times="getWidthAbout2Times" |
| | | :custom-generate-blocks="customGenerateBlocks" |
| | | :start-time-of-render-area="startTimeOfRenderArea" |
| | | :end-time-of-render-area="endTimeOfRenderArea" |
| | | :style="{width:totalWidth+'px'}" |
| | | > |
| | | |
| | | <!-- <template slot-scope="{data,item}"> |
| | | <slot name="block" |
| | | :data="data" |
| | | :item="item"> |
| | | </slot> |
| | | </template> --> |
| | | |
| | | <template |
| | | slot-scope="{data,item,getPositonOffset,getWidthAbout2Times,isInRenderingTimeRange}" |
| | | > |
| | | <slot |
| | | name="block" |
| | | :data="data" |
| | | :item="item" |
| | | :getPositonOffset="getPositonOffset" |
| | | :getWidthAbout2Times="getWidthAbout2Times" |
| | | :isInRenderingTimeRange="isInRenderingTimeRange" |
| | | :startTimeOfRenderArea="startTimeOfRenderArea" |
| | | :endTimeOfRenderArea="endTimeOfRenderArea" |
| | | /> |
| | | </template> |
| | | |
| | | </blocks> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | ref="scrollYBar" |
| | | class="gantt-scroll-y" |
| | | :style="{width:`${scrollYBarWidth}px`, |
| | | height:`calc(100% - ${actualHeaderHeight}px`,marginTop:`${actualHeaderHeight}px`}" |
| | | @scroll="syncScrollY" |
| | | > |
| | | <div :style="{height:totalHeight+'px'}" /> |
| | | </div> |
| | | |
| | | <div |
| | | ref="scrollXBar" |
| | | class="gantt-scroll-x" |
| | | :style="{height:`${scrollXBarHeight}px`, |
| | | width:`calc(100% - ${titleWidth}px )`,marginLeft:titleWidth+'px'}" |
| | | @scroll="syncScrollX" |
| | | > |
| | | <div :style="{width:totalWidth+'px'}" /> |
| | | </div> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import dayjs from 'dayjs' |
| | | import ResizeObserver from 'resize-observer-polyfill' |
| | | import { |
| | | scaleList, |
| | | getBeginTimeOfTimeLine, |
| | | calcScalesAbout2Times |
| | | } from './utils/timeLineUtils.js' |
| | | import { isDef, warn } from './utils/tool.js' |
| | | import { |
| | | getPositonOffset as _getPositonOffset, |
| | | getWidthAbout2Times as _getWidthAbout2Times |
| | | } from './utils/gtUtils.js' |
| | | import throttle from './utils/throttle.js' |
| | | import Timeline from './components/time-line/index.vue' |
| | | import CurrentTime from './components/mark-line/current-time.vue' |
| | | import LeftBar from './components/left-bar/index.vue' |
| | | import Blocks from './components/blocks/index.vue' |
| | | import MarkLine from './components/mark-line/index.vue' |
| | | |
| | | export default { |
| | | name: 'Gantt', |
| | | |
| | | components: { Timeline, LeftBar, Blocks, MarkLine, CurrentTime }, |
| | | |
| | | props: { |
| | | startTime: { |
| | | default: () => dayjs(), |
| | | validator(date) { |
| | | const ok = dayjs(date).isValid() |
| | | if (!ok) warn(`éæ³çå¼å§æ¶é´ ${date}`) |
| | | return ok |
| | | } |
| | | }, |
| | | endTime: { |
| | | default: () => dayjs(), |
| | | validator(date) { |
| | | const ok = dayjs(date).isValid() |
| | | if (!ok) warn(`éæ³çç»ææ¶é´ ${date}`) |
| | | return ok |
| | | } |
| | | }, |
| | | cellWidth: { |
| | | type: Number, |
| | | default: 50 |
| | | }, |
| | | cellHeight: { |
| | | type: Number, |
| | | default: 20 |
| | | }, |
| | | titleHeight: { |
| | | type: Number, |
| | | default: 40 |
| | | }, |
| | | titleWidth: { |
| | | type: Number, |
| | | default: 200 |
| | | }, |
| | | scale: { |
| | | type: Number, |
| | | default: 60, |
| | | validator(value) { |
| | | return scaleList.includes(value) |
| | | } |
| | | }, |
| | | datas: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | dataKey: { |
| | | type: String, |
| | | default: undefined |
| | | }, |
| | | itemKey: { |
| | | type: String, |
| | | default: undefined |
| | | }, |
| | | arrayKeys: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | showCurrentTime: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | timeLines: { |
| | | type: Array |
| | | }, |
| | | scrollToTime: { |
| | | validator(date) { |
| | | return dayjs(date).isValid() |
| | | } |
| | | }, |
| | | scrollToPostion: { |
| | | validator(obj) { |
| | | const validX = isDef(obj.x) ? !Number.isNaN(obj.x) : true |
| | | const validY = isDef(obj.y) ? !Number.isNaN(obj.y) : true |
| | | if (!validX && !validY) { |
| | | warn('scrollToPostion xæy æå¼ä¸ºéNumberç±»å') |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | }, |
| | | hideHeader: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | hideXScrollBar: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | hideYScrollBar: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | customGenerateBlocks: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }, |
| | | |
| | | data() { |
| | | return { |
| | | // ç¼åèç¹ |
| | | selector: { |
| | | gantt_leftbar: {}, |
| | | gantt_table: {}, |
| | | gantt_scroll_y: {}, |
| | | gantt_timeline: {}, |
| | | gantt_scroll_x: {}, |
| | | gantt_markArea: {} |
| | | }, |
| | | scrollTop: 0, |
| | | scrollLeft: 0, |
| | | // block åºåéè¦æ¸²æçèå´ |
| | | // å
渲æåºç©ºæ¡æ¶ï¼å¨mountedååå¾å°çå®ç渲æèå´ï¼ç¶å卿 ¹æ®èå´æ¸²ææ°æ®ï¼æ¯ä¹å设置ä¸ä¸ªé»è®¤é«åº¦å®½åº¦ï¼é¢å¤çæ¸²ææµªè´¹æ´å°äº |
| | | heightOfRenderAera: 0, |
| | | widthOfRenderAera: 0, |
| | | startTimeOfRenderArea: null, |
| | | endTimeOfRenderArea: null, |
| | | scrollBarWitdh: 17 |
| | | } |
| | | }, |
| | | |
| | | computed: { |
| | | start() { |
| | | return dayjs(this.startTime) |
| | | }, |
| | | end() { |
| | | const { start, widthOfRenderAera, scale, cellWidth } = this |
| | | let end = dayjs(this.endTime) |
| | | const totalWidth = calcScalesAbout2Times(start, end, scale) * cellWidth |
| | | if (start.isAfter(end) || totalWidth <= widthOfRenderAera) { |
| | | end = start.add((widthOfRenderAera / cellWidth) * scale, 'minute') |
| | | } |
| | | return end |
| | | }, |
| | | totalWidth() { |
| | | const { cellWidth, totalScales } = this |
| | | return cellWidth * totalScales |
| | | }, |
| | | totalScales() { |
| | | const { start, end, scale } = this |
| | | return calcScalesAbout2Times(start, end, scale) |
| | | }, |
| | | totalHeight() { |
| | | const { datas, cellHeight } = this |
| | | return datas.length * cellHeight |
| | | }, |
| | | beginTimeOfTimeLine() { |
| | | const value = getBeginTimeOfTimeLine(this.start, this.scale) |
| | | return value |
| | | }, |
| | | beginTimeOfTimeLineToString() { |
| | | return this.beginTimeOfTimeLine.toString() |
| | | }, |
| | | avialableScrollLeft() { |
| | | // ä¸åè¿ä¸ª1ï¼æ»å¨å°æ¶é´è½´å°½å¤´åç»§ç»æ»å¨ä¼æ
¢æ
¢çæº¢åº |
| | | const { totalWidth, widthOfRenderAera } = this |
| | | return totalWidth - widthOfRenderAera - 1 |
| | | }, |
| | | avialableScrollTop() { |
| | | const { totalHeight, heightOfRenderAera } = this |
| | | return totalHeight - heightOfRenderAera - 1 |
| | | }, |
| | | scrollXBarHeight() { |
| | | return this.hideXScrollBar ? 0 : this.scrollBarWitdh |
| | | }, |
| | | scrollYBarWidth() { |
| | | return this.hideYScrollBar ? 0 : this.scrollBarWitdh |
| | | }, |
| | | actualHeaderHeight() { |
| | | return this.hideHeader ? 0 : this.titleHeight |
| | | } |
| | | }, |
| | | |
| | | watch: { |
| | | scrollLeft() { |
| | | this.getTimeRange() |
| | | }, |
| | | widthOfRenderAera() { |
| | | this.getTimeRange() |
| | | }, |
| | | cellWidth() { |
| | | this.getTimeRange() |
| | | }, |
| | | scrollToTime: { |
| | | handler(newV) { |
| | | if (!newV) { |
| | | return |
| | | } |
| | | const { start, end } = this |
| | | const time = dayjs(newV) |
| | | if (!(time.isAfter(start) && time.isBefore(end))) { |
| | | warn(`å½åæ»å¨è³${newV}ä¸å¨${start}å${end}çèå´ä¹å
`) |
| | | return |
| | | } |
| | | |
| | | const offset = this.getPositonOffset(newV) |
| | | // immediate ä¼é ædom è¿æ²¡ææè½½æ¶å°±è¿è¡æä½ï¼æ
éè¦å»¶è¿æ§è¡ |
| | | this.$nextTick(() => |
| | | this.syncScrollX( |
| | | { |
| | | target: { |
| | | scrollLeft: offset |
| | | } |
| | | }, |
| | | true |
| | | ) |
| | | ) |
| | | }, |
| | | immediate: true |
| | | }, |
| | | scrollToPostion: { |
| | | handler(newV) { |
| | | if (!newV) { |
| | | return |
| | | } |
| | | const x = Number.isNaN(newV.x) ? undefined : newV.x |
| | | const y = Number.isNaN(newV.y) ? undefined : newV.y |
| | | this.$nextTick(() => { |
| | | if (isDef(x) && x !== this.scrollLeft) { |
| | | this.syncScrollX({ target: { scrollLeft: x }}, true) |
| | | } |
| | | if (isDef(y) && y !== this.scrollTop) { |
| | | this.syncScrollY({ target: { scrollTop: y }}, true) |
| | | } |
| | | }) |
| | | }, |
| | | immediate: true |
| | | } |
| | | }, |
| | | |
| | | mounted() { |
| | | this.getSelector() |
| | | // 计ç®åç¡®çæ¸²æåºåèå´ |
| | | const observeContainer = throttle(entries => { |
| | | entries.forEach(entry => { |
| | | const cr = entry.contentRect |
| | | this.heightOfRenderAera = cr.height |
| | | this.widthOfRenderAera = cr.width |
| | | }) |
| | | }) |
| | | const observer = new ResizeObserver(observeContainer) |
| | | observer.observe(this.$refs.blocksWrapper) |
| | | }, |
| | | |
| | | methods: { |
| | | /** |
| | | * 计ç®éè¦æ¸²æçæ¶é´èå´ |
| | | * |
| | | */ |
| | | getTimeRange() { |
| | | if (this.heightOfRenderAera === 0) { |
| | | return |
| | | } |
| | | |
| | | const { |
| | | beginTimeOfTimeLine, |
| | | scrollLeft, |
| | | cellWidth, |
| | | scale, |
| | | widthOfRenderAera |
| | | } = this |
| | | |
| | | this.startTimeOfRenderArea = beginTimeOfTimeLine |
| | | .add((scrollLeft / cellWidth) * scale, 'minute') |
| | | .toDate() |
| | | .getTime() |
| | | this.endTimeOfRenderArea = beginTimeOfTimeLine |
| | | .add(((scrollLeft + widthOfRenderAera) / cellWidth) * scale, 'minute') |
| | | .toDate() |
| | | .getTime() |
| | | }, |
| | | getWidthAbout2Times(start, end) { |
| | | const options = { |
| | | scale: this.scale, |
| | | cellWidth: this.cellWidth |
| | | } |
| | | return _getWidthAbout2Times(start, end, options) |
| | | }, |
| | | /** |
| | | * 为æ¶é´çº¿è®¡ç®åç§» |
| | | */ |
| | | getPositonOffset(date) { |
| | | const options = { |
| | | scale: this.scale, |
| | | cellWidth: this.cellWidth |
| | | } |
| | | |
| | | return _getPositonOffset(date, this.beginTimeOfTimeLineToString, options) |
| | | }, |
| | | // ç¼åèç¹ |
| | | getSelector() { |
| | | this.selector.gantt_leftbar = this.$refs.leftbarWrapper |
| | | this.selector.gantt_table = this.$refs.blocksWrapper |
| | | this.selector.gantt_scroll_y = this.$refs.scrollYBar |
| | | this.selector.gantt_timeline = this.$refs.headerTimeline |
| | | this.selector.gantt_scroll_x = this.$refs.scrollXBar |
| | | this.selector.gantt_markArea = this.$refs.marklineArea |
| | | }, |
| | | wheelHandle(event) { |
| | | const { deltaX, deltaY } = event |
| | | this.$nextTick(() => { |
| | | const { |
| | | scrollTop, |
| | | scrollLeft, |
| | | avialableScrollLeft, |
| | | avialableScrollTop |
| | | } = this |
| | | |
| | | if (deltaY !== 0) { |
| | | if ( |
| | | scrollTop + deltaY >= avialableScrollTop && |
| | | scrollTop !== avialableScrollTop |
| | | ) { |
| | | this.syncScrollY( |
| | | { target: { scrollTop: avialableScrollTop }}, |
| | | true |
| | | ) |
| | | } else if ( |
| | | scrollTop + deltaY < 0 && |
| | | scrollTop !== 0 /* æ»å¨ä¸º0éå¶*/ |
| | | ) { |
| | | this.syncScrollY({ target: { scrollTop: 0 }}, true) |
| | | } else { |
| | | this.syncScrollY( |
| | | { target: { scrollTop: scrollTop + deltaY }}, |
| | | true |
| | | ) |
| | | } |
| | | } |
| | | if (deltaX !== 0) { |
| | | if ( |
| | | scrollLeft + deltaX >= avialableScrollLeft && |
| | | scrollLeft !== avialableScrollLeft |
| | | ) { |
| | | this.syncScrollX( |
| | | { target: { scrollLeft: avialableScrollLeft }}, |
| | | true |
| | | ) |
| | | } else if ( |
| | | scrollLeft + deltaX < 0 && |
| | | scrollLeft !== 0 /* æ»å¨ä¸º0éå¶*/ |
| | | ) { |
| | | this.syncScrollX({ target: { scrollLeft: 0 }}, true) |
| | | } else { |
| | | this.syncScrollX( |
| | | { target: { scrollLeft: scrollLeft + deltaX }}, |
| | | true |
| | | ) |
| | | } |
| | | } |
| | | }) |
| | | }, |
| | | // 忥fixleftåblockçæ»å¨ |
| | | syncScrollY(event, fake = false) { |
| | | const { gantt_leftbar, gantt_table, gantt_scroll_y } = this.selector |
| | | const topValue = event.target.scrollTop |
| | | if (fake) { |
| | | // ä¼è§¦å䏿¬¡ççæ»å¨äºä»¶event, åé¢ç代ç ä¼å¨ç¬¬äºä¸ªäºä»¶ä¸æ§è¡ |
| | | gantt_scroll_y.scrollTop = topValue |
| | | return |
| | | } |
| | | gantt_leftbar.scrollTop = topValue |
| | | gantt_table.scrollTop = topValue |
| | | this.scrollTop = topValue |
| | | this.$emit('scrollTop', topValue) |
| | | }, |
| | | syncScrollX(event, fake = false) { |
| | | const { |
| | | gantt_table, |
| | | gantt_timeline, |
| | | gantt_markArea, |
| | | gantt_scroll_x |
| | | } = this.selector |
| | | const leftValue = event.target.scrollLeft |
| | | if (fake) { |
| | | // ä¼è§¦å䏿¬¡ççæ»å¨äºä»¶event, åé¢ç代ç ä¼å¨ç¬¬äºä¸ªäºä»¶ä¸æ§è¡ |
| | | gantt_scroll_x.scrollLeft = leftValue |
| | | return |
| | | } |
| | | gantt_table.scrollLeft = leftValue |
| | | gantt_timeline.scrollLeft = leftValue |
| | | gantt_markArea.style.left = '-' + leftValue + 'px' |
| | | this.scrollLeft = leftValue |
| | | this.$emit('scrollLeft', leftValue) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss"> |
| | | @import "./gantt.scss"; |
| | | .gantt-leftbar-wrapper{ |
| | | // overflow-y: scroll; |
| | | // overflow: visible; |
| | | } |
| | | .gantt-body{ |
| | | // overflow-y: scroll; |
| | | } |
| | | .gantt-leftbar{ |
| | | // overflow-y: scroll; |
| | | } |
| | | </style> |