UNPKG

vxe-gantt

Version:
1,204 lines (1,136 loc) 72.7 kB
import { h, ref, reactive, nextTick, inject, onBeforeUnmount, provide, computed, onMounted, onUnmounted } from 'vue' import { defineVxeComponent } from '../../ui/src/comp' import { setScrollTop, setScrollLeft, removeClass, addClass, hasClass } from '../../ui/src/dom' import { VxeUI } from '@vxe-ui/core' import { getRefElem, getStandardGapTime, getTaskBarLeft, getTaskBarWidth, hasMilestoneTask, getTaskType, hasSubviewTask } from './util' import XEUtils from 'xe-utils' import GanttViewHeaderComponent from './gantt-header' import GanttViewBodyComponent from './gantt-body' import GanttViewFooterComponent from './gantt-footer' import type { VxeTableConstructor } from 'vxe-table' import type { VxeGanttViewConstructor, GanttViewReactData, GanttViewPrivateRef, VxeGanttDefines, VxeGanttViewPrivateMethods, GanttViewInternalData, VxeGanttViewMethods, GanttViewPrivateComputed, VxeGanttConstructor, VxeGanttPrivateMethods } from '../../../types' const { globalEvents } = VxeUI const sourceType = 'gantt' const minuteMs = 1000 * 60 const dayMs = minuteMs * 60 * 24 function createInternalData (): GanttViewInternalData { return { xeTable: null, visibleColumn: [], startMaps: {}, endMaps: {}, chartMaps: {}, todayDateMaps: {}, elemStore: {}, // 存放横向 X 虚拟滚动相关的信息 scrollXStore: { preloadSize: 0, offsetSize: 0, visibleSize: 0, visibleStartIndex: 0, visibleEndIndex: 0, startIndex: 0, endIndex: 0 }, // 最后滚动位置 lastScrollTop: 0, lastScrollLeft: 0 } } function createReactData (): GanttViewReactData { return { // 是否启用了横向 X 可视渲染方式加载 scrollXLoad: false, // 是否启用了纵向 Y 可视渲染方式加载 scrollYLoad: false, // 是否存在纵向滚动条 overflowY: true, // 是否存在横向滚动条 overflowX: true, // 纵向滚动条的宽度 scrollbarWidth: 0, // 横向滚动条的高度 scrollbarHeight: 0, // 最后滚动时间戳 lastScrollTime: 0, lazScrollLoading: false, scrollVMLoading: false, scrollYHeight: 0, scrollYTop: 0, isScrollYBig: false, scrollXLeft: 0, scrollXWidth: 0, isScrollXBig: false, minViewDate: null, maxViewDate: null, tableData: [], tableColumn: [], headerGroups: [], viewCellWidth: 40 } } const maxYHeight = 5e6 // const maxXWidth = 5e6 export default defineVxeComponent({ name: 'VxeGanttView', setup (props, context) { const xID = XEUtils.uniqueId() const $xeGantt = inject('$xeGantt', {} as (VxeGanttConstructor & VxeGanttPrivateMethods)) const { reactData: ganttReactData, internalData: ganttInternalData } = $xeGantt const { computeTaskOpts, computeTaskViewOpts, computeStartField, computeEndField, computeTypeField, computeScrollbarOpts, computeScrollbarXToTop, computeScrollbarYToLeft, computeScaleUnit, computeWeekScale, computeMinScale, computeTaskNowLineOpts } = $xeGantt.getComputeMaps() const refElem = ref<HTMLDivElement>() const refScrollXVirtualElem = ref<HTMLDivElement>() const refScrollYVirtualElem = ref<HTMLDivElement>() const refScrollXHandleElem = ref<HTMLDivElement>() const refScrollXLeftCornerElem = ref<HTMLDivElement>() const refScrollXRightCornerElem = ref<HTMLDivElement>() const refScrollYHandleElem = ref<HTMLDivElement>() const refScrollYTopCornerElem = ref<HTMLDivElement>() const refScrollXWrapperElem = ref<HTMLDivElement>() const refScrollYWrapperElem = ref<HTMLDivElement>() const refScrollYBottomCornerElem = ref<HTMLDivElement>() const refScrollXSpaceElem = ref<HTMLDivElement>() const refScrollYSpaceElem = ref<HTMLDivElement>() const refColInfoElem = ref<HTMLDivElement>() const reactData = reactive(createReactData()) const internalData = createInternalData() const refMaps: GanttViewPrivateRef = { refElem, refScrollXHandleElem, refScrollYHandleElem } const computeScaleDateList = computed(() => { const { minViewDate, maxViewDate } = reactData const taskViewOpts = computeTaskViewOpts.value const minScale = computeMinScale.value const { gridding } = taskViewOpts const dateList: Date[] = [] if (!minScale || !minViewDate || !maxViewDate) { return dateList } const { type, startDay } = minScale const leftSize = -(ganttReactData.currLeftSpacing + XEUtils.toNumber(gridding ? gridding.leftSpacing || 0 : 0)) const rightSize = ganttReactData.currRightSpacing + XEUtils.toNumber(gridding ? gridding.rightSpacing || 0 : 0) const currStep = 1// XEUtils.toNumber(step || 1) || 1 switch (type) { case 'year': { let currDate = XEUtils.getWhatYear(minViewDate, leftSize, 'first') const endDate = XEUtils.getWhatYear(maxViewDate, rightSize, 'first') while (currDate <= endDate) { const itemDate = currDate dateList.push(itemDate) currDate = XEUtils.getWhatYear(currDate, currStep) } break } case 'quarter': { let currDate = XEUtils.getWhatQuarter(minViewDate, leftSize, 'first') const endDate = XEUtils.getWhatQuarter(maxViewDate, rightSize, 'first') while (currDate <= endDate) { const itemDate = currDate dateList.push(itemDate) currDate = XEUtils.getWhatQuarter(currDate, currStep) } break } case 'month': { let currDate = XEUtils.getWhatMonth(minViewDate, leftSize, 'first') const endDate = XEUtils.getWhatMonth(maxViewDate, rightSize, 'first') while (currDate <= endDate) { const itemDate = currDate dateList.push(itemDate) currDate = XEUtils.getWhatMonth(currDate, currStep) } break } case 'week': { let currDate = XEUtils.getWhatWeek(minViewDate, leftSize, startDay, startDay) const endDate = XEUtils.getWhatWeek(maxViewDate, rightSize, startDay, startDay) while (currDate <= endDate) { const itemDate = currDate dateList.push(itemDate) currDate = XEUtils.getWhatWeek(currDate, currStep) } break } case 'day': case 'date': { let currDate = XEUtils.getWhatDay(minViewDate, leftSize, 'first') const endDate = XEUtils.getWhatDay(maxViewDate, rightSize, 'first') while (currDate <= endDate) { const itemDate = currDate dateList.push(itemDate) currDate = XEUtils.getWhatDay(currDate, currStep) } break } case 'hour': case 'minute': case 'second': { const gapTime = getStandardGapTime(minScale.type) * currStep let currTime = minViewDate.getTime() + (leftSize * gapTime) const endTime = maxViewDate.getTime() + (rightSize * gapTime) while (currTime <= endTime) { const itemDate = new Date(currTime) dateList.push(itemDate) currTime += gapTime } break } } return dateList }) const computeNowLineLeft = computed(() => { const ganttReactData = $xeGantt.reactData const { minViewDate, maxViewDate, viewCellWidth } = reactData const { visibleColumn, todayDateMaps } = internalData const minScale = computeMinScale.value const taskViewOpts = computeTaskViewOpts.value const taskNowLineOpts = computeTaskNowLineOpts.value const { showNowLine } = taskViewOpts const { mode } = taskNowLineOpts const { nowTime } = ganttReactData // 此刻线 let nlLeft = 0 if (showNowLine && minScale && minViewDate && maxViewDate && nowTime >= minViewDate.getTime() && nowTime <= maxViewDate.getTime()) { const todayValue = todayDateMaps[minScale.type] let currCol: VxeGanttDefines.ViewColumn | null = null let nextCol: VxeGanttDefines.ViewColumn | null = null for (let i = 0; i < visibleColumn.length; i++) { const column = visibleColumn[i] if (column.field === todayValue) { currCol = column nlLeft = i * viewCellWidth nextCol = visibleColumn[i + 1] break } } if (mode === 'progress') { if (currCol && nextCol) { const currTime = currCol.dateObj.date.getTime() const offsetTime = nowTime - currTime const nowProgress = Math.max(0, Math.min(1, offsetTime / (nextCol.dateObj.date.getTime() - currTime))) nlLeft += nowProgress * viewCellWidth } } else if (mode === 'center') { nlLeft += viewCellWidth / 2 } else if (mode === 'end') { nlLeft += viewCellWidth - 1 } } return nlLeft }) const computeMaps: GanttViewPrivateComputed = { computeScaleDateList, computeNowLineLeft } const $xeGanttView = { xID, props, context, reactData, internalData, getRefMaps: () => refMaps, getComputeMaps: () => computeMaps } as unknown as VxeGanttViewConstructor & VxeGanttViewPrivateMethods const parseStringDate = (dateValue: any) => { const taskOpts = computeTaskOpts.value const { dateFormat } = taskOpts return XEUtils.toStringDate(dateValue, dateFormat || null) } const updateTodayData = () => { const ganttReactData = $xeGantt.reactData const { taskScaleList } = ganttReactData const minScale = computeMinScale.value if (minScale) { const weekScale = taskScaleList.find(item => item.type === 'week') const isMinWeek = minScale.type === 'week' const itemDate = new Date() let [yyyy, M, MM, dd, HH, mm, ss] = XEUtils.toDateString(itemDate, 'yyyy-M-MM-dd-HH-mm-ss').split('-') const e = itemDate.getDay() const E = e + 1 const q = Math.ceil((itemDate.getMonth() + 1) / 3) const W = `${XEUtils.getYearWeek(itemDate, weekScale ? weekScale.startDay : undefined)}` if (isMinWeek && checkWeekOfsetYear(W, M)) { yyyy = `${Number(yyyy) + 1}` M = '1' MM = '0' + M } ganttReactData.nowTime = itemDate.getTime() internalData.todayDateMaps = { year: yyyy, quarter: `${yyyy}_q${q}`, month: `${yyyy}_${MM}`, week: `${yyyy}_W${W}`, day: `${yyyy}_${MM}_${dd}_E${E}`, date: `${yyyy}_${MM}_${dd}`, hour: `${yyyy}_${MM}_${dd}_${HH}`, minute: `${yyyy}_${MM}_${dd}_${HH}_${mm}`, second: `${yyyy}_${MM}_${dd}_${HH}_${mm}_${ss}` } } } const handleColumnHeader = () => { const ganttReactData = $xeGantt.reactData const { taskScaleList } = ganttReactData const scaleUnit = computeScaleUnit.value const minScale = computeMinScale.value const weekScale = computeWeekScale.value const scaleDateList = computeScaleDateList.value const fullCols: VxeGanttDefines.ViewColumn[] = [] const groupCols: VxeGanttDefines.GroupColumn[] = [] if (minScale && scaleUnit && scaleDateList.length) { const renderListMaps: Record<VxeGanttDefines.ColumnScaleType, VxeGanttDefines.ViewColumn[]> = { year: [], quarter: [], month: [], week: [], day: [], date: [], hour: [], minute: [], second: [] } const tempTypeMaps: Record<VxeGanttDefines.ColumnScaleType, Record<string, VxeGanttDefines.ViewColumn>> = { year: {}, quarter: {}, month: {}, week: {}, day: {}, date: {}, hour: {}, minute: {}, second: {} } const isMinWeek = minScale.type === 'week' const handleData = (type: VxeGanttDefines.ColumnScaleType, colMaps: Record<VxeGanttDefines.ColumnScaleType, VxeGanttDefines.ViewColumn>, minCol: VxeGanttDefines.ViewColumn) => { if (minScale.type === type) { return } const currCol = colMaps[type] const currKey = `${currCol.field}` let currGpCol = tempTypeMaps[type][currKey] if (!currGpCol) { currGpCol = currCol tempTypeMaps[type][currKey] = currGpCol renderListMaps[type].push(currGpCol) } if (currGpCol) { if (!currGpCol.children) { currGpCol.children = [] } currGpCol.children.push(minCol) } } for (let i = 0; i < scaleDateList.length; i++) { const itemDate = scaleDateList[i] let [yy, yyyy, M, MM, d, dd, H, HH, m, mm, s, ss] = XEUtils.toDateString(itemDate, 'yy-yyyy-M-MM-d-dd-H-HH-m-mm-s-ss').split('-') const e = itemDate.getDay() const E = e + 1 const q = Math.ceil((itemDate.getMonth() + 1) / 3) const W = `${XEUtils.getYearWeek(itemDate, weekScale ? weekScale.startDay : undefined)}` const WW = XEUtils.padStart(W, 2, '0') if (isMinWeek && checkWeekOfsetYear(W, M)) { yyyy = `${Number(yyyy) + 1}` M = '1' MM = '0' + M } const dateObj: VxeGanttDefines.ScaleDateObj = { date: itemDate, yy, yyyy, M, MM, d, dd, H, HH, m, mm, s, ss, q, W, WW, E, e } const colMaps: Record<VxeGanttDefines.ColumnScaleType, VxeGanttDefines.ViewColumn> = { year: { field: yyyy, title: yyyy, dateObj }, quarter: { field: `${yyyy}_q${q}`, title: `${q}`, dateObj }, month: { field: `${yyyy}_${MM}`, title: MM, dateObj }, week: { field: `${yyyy}_W${W}`, title: `${W}`, dateObj }, day: { field: `${yyyy}_${MM}_${dd}_E${E}`, title: `${E}`, dateObj }, date: { field: `${yyyy}_${MM}_${dd}`, title: dd, dateObj }, hour: { field: `${yyyy}_${MM}_${dd}_${HH}`, title: HH, dateObj }, minute: { field: `${yyyy}_${MM}_${dd}_${HH}_${mm}`, title: mm, dateObj }, second: { field: `${yyyy}_${MM}_${dd}_${HH}_${mm}_${ss}`, title: ss, dateObj } } const minCol = colMaps[minScale.type] if (minScale.level < 19) { handleData('year', colMaps, minCol) } if (minScale.level < 17) { handleData('quarter', colMaps, minCol) } if (minScale.level < 15) { handleData('month', colMaps, minCol) } if (minScale.level < 13) { handleData('week', colMaps, minCol) } if (minScale.level < 11) { handleData('day', colMaps, minCol) } if (minScale.level < 9) { handleData('date', colMaps, minCol) } if (minScale.level < 7) { handleData('hour', colMaps, minCol) } if (minScale.level < 5) { handleData('minute', colMaps, minCol) } if (minScale.level < 3) { handleData('second', colMaps, minCol) } fullCols.push(minCol) } taskScaleList.forEach(scaleItem => { if (scaleItem.type === minScale.type) { groupCols.push({ scaleItem, columns: fullCols }) return } const list = renderListMaps[scaleItem.type] || [] if (list) { list.forEach(item => { item.childCount = item.children ? item.children.length : 0 item.children = undefined }) } groupCols.push({ scaleItem, columns: list }) }) } return { fullCols, groupCols } } /** * 判断周的年份是否跨年 */ const checkWeekOfsetYear = (W: number | string, M: number | string) => { return `${W}` === '1' && `${M}` === '12' } /** * 周维度,由于年份和第几周是冲突的行为,所以需要特殊处理,判断是否跨年,例如 * '2024-12-31' 'yyyy-MM-dd W' >> '2024-12-31 1' * '2025-01-01' 'yyyy-MM-dd W' >> '2025-01-01 1' */ const parseWeekObj = (date: any, firstDay?: 0 | 5 | 1 | 2 | 3 | 4 | 6) => { const currDate = XEUtils.toStringDate(date) let yyyy = currDate.getFullYear() const month = currDate.getMonth() const weekNum = XEUtils.getYearWeek(currDate, firstDay) if (checkWeekOfsetYear(weekNum, month + 1)) { yyyy++ } return { yyyy, W: weekNum } } const createChartRender = (fullCols: VxeGanttDefines.ViewColumn[]) => { const { minViewDate } = reactData const minScale = computeMinScale.value const scaleUnit = computeScaleUnit.value const weekScale = computeWeekScale.value if (minScale) { switch (scaleUnit) { case 'year': { const indexMaps: Record<string, number> = {} fullCols.forEach(({ dateObj }, i) => { const yyyyMM = XEUtils.toDateString(dateObj.date, 'yyyy') indexMaps[yyyyMM] = i }) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) const startStr = XEUtils.toDateString(startDate, 'yyyy') const startFirstDate = XEUtils.getWhatYear(startDate, 0, 'first') const endStr = XEUtils.toDateString(endDate, 'yyyy') const endFirstDate = XEUtils.getWhatYear(endDate, 0, 'first') const dateSize = Math.floor((XEUtils.getWhatYear(endDate, 1, 'first').getTime() - endFirstDate.getTime()) / dayMs) const subtract = (startDate.getTime() - startFirstDate.getTime()) / dayMs / dateSize const addSize = Math.max(0, (endDate.getTime() - endFirstDate.getTime()) / dayMs + 1) / dateSize const offsetLeftSize = (indexMaps[startStr] || 0) + subtract return { offsetLeftSize, offsetWidthSize: (indexMaps[endStr] || 0) - offsetLeftSize + addSize } } } case 'quarter': { const indexMaps: Record<string, number> = {} fullCols.forEach(({ dateObj }, i) => { const q = XEUtils.toDateString(dateObj.date, 'yyyy-q') indexMaps[q] = i }) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) const startStr = XEUtils.toDateString(startDate, 'yyyy-q') const startFirstDate = XEUtils.getWhatQuarter(startDate, 0, 'first') const endStr = XEUtils.toDateString(endDate, 'yyyy-q') const endFirstDate = XEUtils.getWhatQuarter(endDate, 0, 'first') const dateSize = Math.floor((XEUtils.getWhatQuarter(endDate, 1, 'first').getTime() - endFirstDate.getTime()) / dayMs) const subtract = (startDate.getTime() - startFirstDate.getTime()) / dayMs / dateSize const addSize = Math.max(0, (endDate.getTime() - endFirstDate.getTime()) / dayMs + 1) / dateSize const offsetLeftSize = (indexMaps[startStr] || 0) + subtract return { offsetLeftSize, offsetWidthSize: (indexMaps[endStr] || 0) - offsetLeftSize + addSize } } } case 'month': { const indexMaps: Record<string, number> = {} fullCols.forEach(({ dateObj }, i) => { const yyyyMM = XEUtils.toDateString(dateObj.date, 'yyyy-MM') indexMaps[yyyyMM] = i }) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) const startStr = XEUtils.toDateString(startDate, 'yyyy-MM') const startFirstDate = XEUtils.getWhatMonth(startDate, 0, 'first') const endStr = XEUtils.toDateString(endDate, 'yyyy-MM') const endFirstDate = XEUtils.getWhatMonth(endDate, 0, 'first') const dateSize = Math.floor((XEUtils.getWhatMonth(endDate, 1, 'first').getTime() - endFirstDate.getTime()) / dayMs) const subtract = (startDate.getTime() - startFirstDate.getTime()) / dayMs / dateSize const addSize = Math.max(0, (endDate.getTime() - endFirstDate.getTime()) / dayMs + 1) / dateSize const offsetLeftSize = (indexMaps[startStr] || 0) + subtract return { offsetLeftSize, offsetWidthSize: (indexMaps[endStr] || 0) - offsetLeftSize + addSize } } } case 'week': { const indexMaps: Record<string, number> = {} fullCols.forEach(({ dateObj }, i) => { const yyyyW = `${dateObj.yyyy}-${dateObj.W}` indexMaps[yyyyW] = i }) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) const startWeekObj = parseWeekObj(startDate, weekScale ? weekScale.startDay : undefined) const startStr = `${startWeekObj.yyyy}-${startWeekObj.W}` const startFirstDate = XEUtils.getWhatWeek(startDate, 0, weekScale ? weekScale.startDay : undefined, weekScale ? weekScale.startDay : undefined) const endWeekObj = parseWeekObj(endDate, weekScale ? weekScale.startDay : undefined) const endStr = `${endWeekObj.yyyy}-${endWeekObj.W}` const endFirstDate = XEUtils.getWhatWeek(endDate, 0, weekScale ? weekScale.startDay : undefined, weekScale ? weekScale.startDay : undefined) const dateSize = Math.floor((XEUtils.getWhatWeek(endDate, 1, weekScale ? weekScale.startDay : undefined, weekScale ? weekScale.startDay : undefined).getTime() - endFirstDate.getTime()) / dayMs) const subtract = (startDate.getTime() - startFirstDate.getTime()) / dayMs / dateSize const addSize = Math.max(0, (endDate.getTime() - endFirstDate.getTime()) / dayMs + 1) / dateSize const offsetLeftSize = (indexMaps[startStr] || 0) + subtract return { offsetLeftSize, offsetWidthSize: (indexMaps[endStr] || 0) - offsetLeftSize + addSize } } } case 'day': case 'date': { const indexMaps: Record<string, number> = {} fullCols.forEach(({ dateObj }, i) => { const yyyyMM = XEUtils.toDateString(dateObj.date, 'yyyy-MM-dd') indexMaps[yyyyMM] = i }) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) const startStr = XEUtils.toDateString(startDate, 'yyyy-MM-dd') const startFirstDate = XEUtils.getWhatDay(startDate, 0, 'first') const endStr = XEUtils.toDateString(endDate, 'yyyy-MM-dd') const endFirstDate = XEUtils.getWhatDay(endDate, 0, 'first') const minuteSize = Math.floor((XEUtils.getWhatDay(endDate, 1, 'first').getTime() - endFirstDate.getTime()) / minuteMs) // 开始和结束时间是否存在偏移时 const startSubtract = (startDate.getTime() - startFirstDate.getTime()) / minuteMs / minuteSize const endSubtract = (endDate.getTime() - endFirstDate.getTime()) / minuteMs / minuteSize const addSize = Math.max(0, (endDate.getTime() - endFirstDate.getTime()) / minuteMs + 1) / minuteSize const offsetLeftSize = (indexMaps[startStr] || 0) + startSubtract // 如果最小轴为天,当存在时分秒时,在当前单元格内渲染维度;如果不存在,则填充满单元格 return { offsetLeftSize, offsetWidthSize: (indexMaps[endStr] || 0) - offsetLeftSize + addSize + (startSubtract || endSubtract ? 0 : 1) } } } case 'hour': { const indexMaps: Record<string, number> = {} fullCols.forEach(({ dateObj }, i) => { const yyyyMM = XEUtils.toDateString(dateObj.date, 'yyyy-MM-dd HH') indexMaps[yyyyMM] = i }) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) const startStr = XEUtils.toDateString(startDate, 'yyyy-MM-dd HH') const startFirstDate = XEUtils.getWhatHours(startDate, 0, 'first') const endStr = XEUtils.toDateString(endDate, 'yyyy-MM-dd HH') const endFirstDate = XEUtils.getWhatHours(endDate, 0, 'first') const minuteSize = Math.floor((XEUtils.getWhatHours(endDate, 1, 'first').getTime() - endFirstDate.getTime()) / minuteMs) // 开始和结束时间是否存在偏移时 const startSubtract = (startDate.getTime() - startFirstDate.getTime()) / minuteMs / minuteSize const endSubtract = (endDate.getTime() - endFirstDate.getTime()) / minuteMs / minuteSize const addSize = Math.max(0, (endDate.getTime() - endFirstDate.getTime()) / minuteMs + 1) / minuteSize const offsetLeftSize = (indexMaps[startStr] || 0) + startSubtract return { offsetLeftSize, offsetWidthSize: (indexMaps[endStr] || 0) - offsetLeftSize + addSize + (startSubtract || endSubtract ? 0 : 1) } } } case 'minute': { const indexMaps: Record<string, number> = {} fullCols.forEach(({ dateObj }, i) => { const yyyyMM = XEUtils.toDateString(dateObj.date, 'yyyy-MM-dd HH:mm') indexMaps[yyyyMM] = i }) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) const startStr = XEUtils.toDateString(startDate, 'yyyy-MM-dd HH:mm') const startFirstDate = XEUtils.getWhatMinutes(startDate, 0, 'first') const endStr = XEUtils.toDateString(endDate, 'yyyy-MM-dd HH:mm') const endFirstDate = XEUtils.getWhatMinutes(endDate, 0, 'first') const minuteSize = Math.floor((XEUtils.getWhatMinutes(endDate, 1, 'first').getTime() - endFirstDate.getTime()) / minuteMs) const subtract = (startDate.getTime() - startFirstDate.getTime()) / minuteMs / minuteSize const addSize = Math.max(0, (endDate.getTime() - endFirstDate.getTime()) / minuteMs + 1) / minuteSize const offsetLeftSize = (indexMaps[startStr] || 0) + subtract return { offsetLeftSize, offsetWidthSize: (indexMaps[endStr] || 0) - offsetLeftSize + addSize } } } case 'second': { const gapTime = getStandardGapTime(minScale.type) return (startValue: any, endValue: any) => { const startDate = parseStringDate(startValue) const endDate = parseStringDate(endValue) let offsetLeftSize = 0 let offsetWidthSize = 0 if (minViewDate) { offsetLeftSize = (startDate.getTime() - minViewDate.getTime()) / gapTime offsetWidthSize = ((endDate.getTime() - startDate.getTime()) / gapTime) } return { offsetLeftSize, offsetWidthSize } } } } } return () => { return { offsetLeftSize: 0, offsetWidthSize: 0 } } } const handleParseColumn = () => { const ganttProps = $xeGantt.props const { treeConfig } = ganttProps const { minViewDate, maxViewDate } = reactData const { fullCols, groupCols } = handleColumnHeader() if (minViewDate && maxViewDate && fullCols.length) { const $xeTable = internalData.xeTable if ($xeTable) { const startField = computeStartField.value const endField = computeEndField.value const typeField = computeTypeField.value const { computeAggregateOpts, computeTreeOpts } = $xeTable.getComputeMaps() const tableReactData = $xeTable.reactData const { isRowGroupStatus } = tableReactData const tableInternalData = $xeTable.internalData const { afterFullData, afterTreeFullData, afterGroupFullData } = tableInternalData const aggregateOpts = computeAggregateOpts.value const treeOpts = computeTreeOpts.value const { transform } = treeOpts const childrenField = treeOpts.children || treeOpts.childrenField const ctMaps: Record<string, VxeGanttDefines.RowCacheItem> = {} const renderFn = createChartRender(fullCols) const handleParseRender = (row: any) => { const rowid = $xeTable.getRowid(row) let startValue = XEUtils.get(row, startField) let endValue = XEUtils.get(row, endField) const renderTaskType = getTaskType(XEUtils.get(row, typeField)) const isMilestone = hasMilestoneTask(renderTaskType) const isSubview = hasSubviewTask(renderTaskType) if (isMilestone) { if (!startValue) { startValue = endValue } endValue = startValue } if (isSubview) { ctMaps[rowid] = { row, rowid, oLeftSize: 0, oWidthSize: 0 } } else if (startValue && endValue) { const { offsetLeftSize, offsetWidthSize } = renderFn(startValue, endValue) ctMaps[rowid] = { row, rowid, oLeftSize: offsetLeftSize, oWidthSize: offsetWidthSize } } } if (isRowGroupStatus) { // 行分组 const mapChildrenField = aggregateOpts.mapChildrenField if (mapChildrenField) { XEUtils.eachTree(afterGroupFullData, handleParseRender, { children: mapChildrenField }) } } else if (treeConfig) { // 树结构 XEUtils.eachTree(afterTreeFullData, handleParseRender, { children: transform ? treeOpts.mapChildrenField : childrenField }) } else { afterFullData.forEach(handleParseRender) } internalData.chartMaps = ctMaps } } internalData.visibleColumn = fullCols reactData.headerGroups = groupCols updateTodayData() updateScrollXStatus() handleTableColumn() } const handleUpdateData = () => { const ganttProps = $xeGantt.props const { treeConfig } = ganttProps const { scrollXStore } = internalData const $xeTable = internalData.xeTable const sdMaps: Record<string, any> = {} const edMaps: Record<string, any> = {} let minDate: Date | null = null let maxDate: Date | null = null if ($xeTable) { const startField = computeStartField.value const endField = computeEndField.value const typeField = computeTypeField.value const { computeAggregateOpts, computeTreeOpts } = $xeTable.getComputeMaps() const tableReactData = $xeTable.reactData const { isRowGroupStatus } = tableReactData const tableInternalData = $xeTable.internalData const { afterFullData, afterTreeFullData, afterGroupFullData } = tableInternalData const aggregateOpts = computeAggregateOpts.value const treeOpts = computeTreeOpts.value const { transform } = treeOpts const childrenField = treeOpts.children || treeOpts.childrenField const handleMinMaxData = (row: any) => { let startValue = XEUtils.get(row, startField) let endValue = XEUtils.get(row, endField) const typeValue = XEUtils.get(row, typeField) const isMilestone = hasMilestoneTask(typeValue) if (!startValue) { startValue = endValue } if (isMilestone || !endValue) { endValue = startValue } if (startValue) { const startDate = parseStringDate(startValue) if (!minDate || minDate.getTime() > startDate.getTime()) { minDate = startDate } } if (endValue) { const endDate = parseStringDate(endValue) if (!maxDate || maxDate.getTime() < endDate.getTime()) { maxDate = endDate } } } if (isRowGroupStatus) { // 行分组 const mapChildrenField = aggregateOpts.mapChildrenField if (mapChildrenField) { XEUtils.eachTree(afterGroupFullData, handleMinMaxData, { children: mapChildrenField }) } } else if (treeConfig) { // 树结构 XEUtils.eachTree(afterTreeFullData, handleMinMaxData, { children: transform ? treeOpts.mapChildrenField : childrenField }) } else { afterFullData.forEach(handleMinMaxData) } } scrollXStore.startIndex = 0 scrollXStore.endIndex = Math.max(1, scrollXStore.visibleSize) reactData.minViewDate = minDate reactData.maxViewDate = maxDate internalData.startMaps = sdMaps internalData.endMaps = edMaps handleParseColumn() } const calcScrollbar = () => { const { scrollXWidth, scrollYHeight } = reactData const { elemStore } = internalData const scrollbarOpts = computeScrollbarOpts.value const bodyWrapperElem = getRefElem(elemStore['main-body-wrapper']) const xHandleEl = refScrollXHandleElem.value const yHandleEl = refScrollYHandleElem.value let overflowY = false let overflowX = false if (bodyWrapperElem) { overflowY = scrollYHeight > bodyWrapperElem.clientHeight if (yHandleEl) { reactData.scrollbarWidth = scrollbarOpts.width || (yHandleEl.offsetWidth - yHandleEl.clientWidth) || 14 } reactData.overflowY = overflowY overflowX = scrollXWidth > bodyWrapperElem.clientWidth if (xHandleEl) { reactData.scrollbarHeight = scrollbarOpts.height || (xHandleEl.offsetHeight - xHandleEl.clientHeight) || 14 } reactData.overflowX = overflowX } } const handleSubTaskMinMaxSize = ($xeTable: VxeTableConstructor, list: any[]) => { const { chartMaps } = internalData const { computeTreeOpts } = $xeTable.getComputeMaps() const treeOpts = computeTreeOpts.value const childrenField = treeOpts.children || treeOpts.childrenField const typeField = computeTypeField.value let minChildLeftSize = 0 let maxChildLeftSize = 0 XEUtils.eachTree(list, childRow => { const childRowid = $xeTable.getRowid(childRow) const renderTaskType = XEUtils.get(childRow, typeField) if (hasSubviewTask(renderTaskType)) { return } const childChartRest = childRowid ? chartMaps[childRowid] : null if (childChartRest) { maxChildLeftSize = Math.max(maxChildLeftSize, childChartRest.oLeftSize + childChartRest.oWidthSize) minChildLeftSize = minChildLeftSize ? Math.min(minChildLeftSize, childChartRest.oLeftSize) : childChartRest.oLeftSize } }, { children: childrenField }) return { minSize: minChildLeftSize, maxSize: maxChildLeftSize } } const updateTaskChartStyle = () => { const { dragBarRow } = ganttInternalData const { viewCellWidth } = reactData const { elemStore, chartMaps } = internalData const $xeTable = internalData.xeTable const chartWrapper = getRefElem(elemStore['main-chart-task-wrapper']) if (chartWrapper && $xeTable) { const { computeTreeOpts } = $xeTable.getComputeMaps() const treeOpts = computeTreeOpts.value const childrenField = treeOpts.children || treeOpts.childrenField XEUtils.arrayEach(chartWrapper.children, (rowEl) => { const barEl = rowEl.children[0] as HTMLDivElement if (!barEl) { return } const rowid = rowEl.getAttribute('rowid') if (dragBarRow && $xeTable.getRowid(dragBarRow) === rowid) { return } const chartRest = rowid ? chartMaps[rowid] : null const row = chartRest ? chartRest.row : null // 子任务视图 if (hasClass(barEl, 'is--subview')) { const childWrapperEl = barEl.firstElementChild as HTMLDivElement if (childWrapperEl) { // 行内展示 if (hasClass(childWrapperEl, 'is--inline')) { XEUtils.arrayEach(childWrapperEl.children, (childRowEl) => { const childBarEl = childRowEl.children[0] as HTMLDivElement const childRowid = childBarEl.getAttribute('rowid') || '' const childChartRest = childRowid ? chartMaps[childRowid] : null if (childChartRest) { const childRow = childChartRest.row // 如果是子视图 if (hasClass(childBarEl, 'is--subview')) { const subChildren: any[] = childRow[childrenField] const { minSize: minChildLeftSize, maxSize: maxChildLeftSize } = handleSubTaskMinMaxSize($xeTable, subChildren) childBarEl.style.left = `${viewCellWidth * minChildLeftSize}px` childBarEl.style.width = `${viewCellWidth * (maxChildLeftSize - minChildLeftSize)}px` } else { childBarEl.style.left = `${getTaskBarLeft(childChartRest, viewCellWidth)}px` if (!hasClass(childBarEl, 'is--milestone')) { // 里程碑不需要宽度 childBarEl.style.width = `${getTaskBarWidth(childChartRest, viewCellWidth)}px` } } } }) } else { // 如果展开子任务 const childRowEl = childWrapperEl.children[0] as HTMLDivElement const childBarEl = childRowEl ? childRowEl.children[0] as HTMLDivElement : null if (childBarEl) { const rowChildren: any[] = row ? row[childrenField] : [] const { minSize: minChildLeftSize, maxSize: maxChildLeftSize } = handleSubTaskMinMaxSize($xeTable, rowChildren) childBarEl.style.left = `${viewCellWidth * minChildLeftSize}px` childBarEl.style.width = `${viewCellWidth * (maxChildLeftSize - minChildLeftSize)}px` } } } } else { barEl.style.left = `${getTaskBarLeft(chartRest, viewCellWidth)}px` // 里程碑不需要宽度 if (!hasClass(barEl, 'is--milestone')) { barEl.style.width = `${getTaskBarWidth(chartRest, viewCellWidth)}px` } } }) } return nextTick() } const updateStyle = () => { const { scrollbarWidth, scrollbarHeight, headerGroups, tableColumn } = reactData const { elemStore, visibleColumn } = internalData const $xeTable = internalData.xeTable const el = refElem.value if (!el) { return } if (!$xeGantt) { return } const scrollbarOpts = computeScrollbarOpts.value const scrollbarXToTop = computeScrollbarXToTop.value const scrollbarYToLeft = computeScrollbarYToLeft.value const xLeftCornerEl = refScrollXLeftCornerElem.value const xRightCornerEl = refScrollXRightCornerElem.value const scrollXVirtualEl = refScrollXVirtualElem.value let osbWidth = scrollbarWidth const osbHeight = scrollbarHeight let tbHeight = 0 let tHeaderHeight = 0 let tFooterHeight = 0 if ($xeTable) { const tableInternalData = $xeTable.internalData tbHeight = tableInternalData.tBodyHeight tHeaderHeight = tableInternalData.tHeaderHeight tFooterHeight = tableInternalData.tFooterHeight } let yScrollbarVisible = 'visible' if (scrollbarYToLeft || (scrollbarOpts.y && scrollbarOpts.y.visible === false)) { osbWidth = 0 yScrollbarVisible = 'hidden' } const headerScrollElem = getRefElem(elemStore['main-header-scroll']) if (headerScrollElem) { headerScrollElem.style.height = `${tHeaderHeight}px` headerScrollElem.style.setProperty('--vxe-ui-gantt-view-cell-height', `${tHeaderHeight / headerGroups.length}px`) } const bodyScrollElem = getRefElem(elemStore['main-body-scroll']) if (bodyScrollElem) { bodyScrollElem.style.height = `${tbHeight}px` } const footerScrollElem = getRefElem(elemStore['main-footer-scroll']) if (footerScrollElem) { footerScrollElem.style.height = `${tFooterHeight}px` } if (scrollXVirtualEl) { scrollXVirtualEl.style.height = `${osbHeight}px` scrollXVirtualEl.style.visibility = 'visible' } const xWrapperEl = refScrollXWrapperElem.value if (xWrapperEl) { xWrapperEl.style.left = scrollbarXToTop ? `${osbWidth}px` : '' xWrapperEl.style.width = `${el.clientWidth - osbWidth}px` } if (xLeftCornerEl) { xLeftCornerEl.style.width = scrollbarXToTop ? `${osbWidth}px` : '' xLeftCornerEl.style.display = scrollbarXToTop ? (osbHeight ? 'block' : '') : '' } if (xRightCornerEl) { xRightCornerEl.style.width = scrollbarXToTop ? '' : `${osbWidth}px` xRightCornerEl.style.display = scrollbarXToTop ? '' : (osbHeight ? 'block' : '') } const scrollYVirtualEl = refScrollYVirtualElem.value if (scrollYVirtualEl) { scrollYVirtualEl.style.width = `${osbWidth}px` scrollYVirtualEl.style.height = `${tbHeight + tHeaderHeight + tFooterHeight}px` scrollYVirtualEl.style.visibility = yScrollbarVisible } const yTopCornerEl = refScrollYTopCornerElem.value if (yTopCornerEl) { yTopCornerEl.style.height = `${tHeaderHeight}px` yTopCornerEl.style.display = tHeaderHeight ? 'block' : '' } const yWrapperEl = refScrollYWrapperElem.value if (yWrapperEl) { yWrapperEl.style.height = `${tbHeight}px` yWrapperEl.style.top = `${tHeaderHeight}px` } const yBottomCornerEl = refScrollYBottomCornerElem.value if (yBottomCornerEl) { yBottomCornerEl.style.height = `${tFooterHeight}px` yBottomCornerEl.style.top = `${tHeaderHeight + tbHeight}px` yBottomCornerEl.style.display = tFooterHeight ? 'block' : '' } const colInfoElem = refColInfoElem.value let viewCellWidth = 40 if (colInfoElem) { viewCellWidth = colInfoElem.clientWidth || 40 } let viewTableWidth = viewCellWidth if (visibleColumn.length) { viewTableWidth = Math.max(0, viewCellWidth * visibleColumn.length) if (bodyScrollElem) { const viewWidth = bodyScrollElem.clientWidth const remainWidth = viewWidth - viewTableWidth if (remainWidth > 0) { viewCellWidth += Math.max(0, remainWidth / visibleColumn.length) viewTableWidth = viewWidth } } } reactData.viewCellWidth = viewCellWidth const headerTableElem = getRefElem(elemStore['main-header-table']) const bodyTableElem = getRefElem(elemStore['main-body-table']) const vmTableWidth = viewCellWidth * tableColumn.length if (headerTableElem) { headerTableElem.style.width = `${viewTableWidth}px` } if (bodyTableElem) { bodyTableElem.style.width = `${vmTableWidth}px` } reactData.scrollXWidth = viewTableWidth return Promise.all([ updateTaskChartStyle(), $xeGantt.handleUpdateTaskLinkStyle ? $xeGantt.handleUpdateTaskLinkStyle($xeGanttView) : null ]) } const handleRecalculateStyle = () => { const el = refElem.value internalData.rceRunTime = Date.now() if (!el || !el.clientWidth) { return nextTick() } if (!$xeGantt) { return nextTick() } calcScrollbar() updateStyle() return computeScrollLoad() } const handleLazyRecalculate = () => { return new Promise<void>(resolve => { const { rceTimeout, rceRunTime } = internalData const $xeTable = internalData.xeTable let refreshDelay = 30 if ($xeTable) { const { computeResizeOpts } = $xeTable.getComputeMaps() const resizeOpts = computeResizeOpts.value refreshDelay = resizeOpts.refreshDelay || refreshDelay } if (rceTimeout) { clearTimeout(rceTimeout) if (rceRunTime && rceRunTime + (refreshDelay - 5) < Date.now()) { resolve( handleRecalculateStyle() ) } else { nextTick(() => { resolve() }) } } else { resolve( handleRecalculateStyle() ) } internalData.rceTimeout = setTimeout(() => { internalData.rceTimeout = undefined handleRecalculateStyle() }, refreshDelay) }) } const computeScrollLoad = () => { return nextTick().then(() => { const { scrollXLoad } = reactData const { scrollXStore } = internalData // 计算 X 逻辑 if (scrollXLoad) { const { toVisibleIndex: toXVisibleIndex, visibleSize: visibleXSize } = handleVirtualXVisible() const offsetXSize = 2 scrollXStore.preloadSize = 1 scrollXStore.offsetSize = offsetXSize scrollXStore.visibleSize = visibleXSize scrollXStore.endIndex = Math.max(scrollXStore.startIndex + scrollXStore.visibleSize + offsetXSize, scrollXStore.endIndex) scrollXStore.visibleStartIndex = Math.max(scrollXStore.startIndex, toXVisibleIndex) scrollXStore.visibleEndIndex = Math.min(scrollXStore.endIndex, toXVisibleIndex + visibleXSize) updateScrollXData().then(() => { loadScrollXData() }) } else { updateScrollXSpace() } }) } const handleVirtualXVisible = () => { const { viewCellWidth } = reactData const { elemStore } = internalData const bodyScrollElem = getRefElem(elemStore['main-body-scroll']) if (bodyScrollElem) { const clientWidth = bodyScrollElem.clientWidth const scrollLeft = bodyScrollElem.scrollLeft const toVisibleIndex = Math.floor(scrollLeft / viewCellWidth) - 1 const visibleSize = Math.ceil(clientWidth / viewCellWidth) + 1 return { toVisibleIndex: Math.max(0, toVisibleIndex), visibleSize: Math.max(1, visibleSize) } } return { toVisibleIndex: 0, visibleSize: 6 } } const loadScrollXData = () => { const { isScrollXBig } = reactData const { scrollXStore } = internalData const { preloadSize, startIndex, endIndex, offsetSize } = scrollXStore const { toVisibleIndex, visibleSize } = handleVirtualXVisible() const offsetItem = { startIndex: Math.max(0, isScrollXBig ? toVisibleIndex - 1 : toVisibleIndex - 1 - offsetSize - preloadSize), endIndex: isScrollXBig ? toVisibleIndex + visibleSize : toVisibleIndex + visibleSize + offsetSize + preloadSize } scrollXStore.visibleStartIndex = toVisibleIndex - 1 scrollXStore.visibleEndIndex = toVisibleIndex + visibleSize + 1 const { startIndex: offsetStartIndex, endIndex: offsetEndIndex } = offsetItem if (toVisibleIndex <= startIndex || toVisibleInde