146 lines
4.1 KiB
Vue
146 lines
4.1 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="gantt-container">
|
|||
|
|
<!-- 顶部分组栏(维度选择) -->
|
|||
|
|
<dimension-panel :dimensionOptions="dimensionOptions" @switch="onSwitchDimension" class="gantt-dimension-panel" />
|
|||
|
|
<!-- 图例组件 -->
|
|||
|
|
<legend :groups="taskGroups" :dimensionName="getCurrentDimensionName" />
|
|||
|
|
<view class="gantt-body">
|
|||
|
|
<!-- 右侧横向滚动区 -->
|
|||
|
|
<scroll-view scroll-x class="gantt-scroll-x" @scroll="onScroll">
|
|||
|
|
<view class="gantt-right-area">
|
|||
|
|
<!-- 上方时间轴 -->
|
|||
|
|
<timeline-header :config="timelineConfig" class="gantt-timeline-header" />
|
|||
|
|
<!-- 下方任务条 -->
|
|||
|
|
<gantt-canvas :tasks="visibleTasks" :layout="taskLayout" @drag="onTaskDrag" @task-click="onTaskClick" class="gantt-canvas-area" >
|
|||
|
|
<template #task-bar="slotProps">
|
|||
|
|
<slot name="task-bar" v-bind="slotProps" />
|
|||
|
|
</template>
|
|||
|
|
</gantt-canvas>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
<script>
|
|||
|
|
import DataManager from '../core/DataManager'
|
|||
|
|
import TimeCalculator from '../core/TimeCalculator'
|
|||
|
|
import Interaction from '../core/Interaction'
|
|||
|
|
import Layout from '../core/Layout'
|
|||
|
|
import TimelineHeader from './TimelineHeader.vue'
|
|||
|
|
import DimensionPanel from './DimensionPanel.vue'
|
|||
|
|
import GanttCanvas from './GanttCanvas.vue'
|
|||
|
|
import LegendCom from './Legend.vue'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'Gantt',
|
|||
|
|
components: { TimelineHeader, DimensionPanel, GanttCanvas, LegendCom },
|
|||
|
|
props: {
|
|||
|
|
tasks: {
|
|||
|
|
type: Array,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
config: {
|
|||
|
|
type: Object,
|
|||
|
|
required: true
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
dataManager: null,
|
|||
|
|
timeCalculator: null,
|
|||
|
|
interaction: null,
|
|||
|
|
layout: null,
|
|||
|
|
taskGroups: [],
|
|||
|
|
visibleTasks: [],
|
|||
|
|
taskLayout: [],
|
|||
|
|
timelineConfig: {},
|
|||
|
|
dimensionOptions: this.config.visibleDimensions || [],
|
|||
|
|
groupBy: this.config.groupBy // 新增响应式分组依据
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
getCurrentDimensionName() {
|
|||
|
|
const dim = this.dimensionOptions.find(d => d.prop === this.groupBy)
|
|||
|
|
return dim ? dim.label || dim.name || dim.prop : ''
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
created() {
|
|||
|
|
this.dataManager = new DataManager(this.tasks, this.config.visibleDimensions)
|
|||
|
|
this.timeCalculator = new TimeCalculator(this.config)
|
|||
|
|
this.interaction = new Interaction(this.dataManager, this.timeCalculator)
|
|||
|
|
this.layout = new Layout(this.tasks, this.config)
|
|||
|
|
this.refresh()
|
|||
|
|
this.dataManager.onChange(this.refresh)
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
refresh() {
|
|||
|
|
this.taskGroups = this.dataManager.groupByDimension(this.groupBy)
|
|||
|
|
this.visibleTasks = this.taskGroups.flatMap(g => g.tasks)
|
|||
|
|
this.layout = new Layout(this.visibleTasks, this.config)
|
|||
|
|
this.taskLayout = this.layout.computeTaskLayout()
|
|||
|
|
const ticks = this.timeCalculator.getTimelineTicks().map(tick => {
|
|||
|
|
return {
|
|||
|
|
...tick,
|
|||
|
|
left: this.timeCalculator.dateToPixel(tick.date)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
this.timelineConfig = { ticks }
|
|||
|
|
},
|
|||
|
|
onSwitchDimension(dim) {
|
|||
|
|
this.groupBy = dim.prop
|
|||
|
|
this.refresh()
|
|||
|
|
},
|
|||
|
|
onTaskDrag({ taskId, deltaX }) {
|
|||
|
|
const task = this.tasks.find(t => t.id === taskId)
|
|||
|
|
if (!task) return
|
|||
|
|
const startPixel = this.timeCalculator.dateToPixel(task.start)
|
|||
|
|
const newPixel = startPixel + deltaX
|
|||
|
|
const newDate = this.timeCalculator.pixelToDate(newPixel)
|
|||
|
|
this.interaction.handleDragUpdate(taskId, newDate.toISOString().slice(0, 10))
|
|||
|
|
},
|
|||
|
|
onTaskClick(task) {
|
|||
|
|
this.$emit('task-click', task)
|
|||
|
|
},
|
|||
|
|
onScroll(e) {
|
|||
|
|
// 不做 scrollLeft 回写,避免抖动
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
<style scoped>
|
|||
|
|
.gantt-container {
|
|||
|
|
width: 100%;
|
|||
|
|
background: #f5f6fa;
|
|||
|
|
}
|
|||
|
|
.gantt-body {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
height: 500px;
|
|||
|
|
}
|
|||
|
|
.gantt-dimension-panel {
|
|||
|
|
width: 100%;
|
|||
|
|
border-right: none;
|
|||
|
|
background: #fafbfc;
|
|||
|
|
z-index: 2;
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
.gantt-scroll-x {
|
|||
|
|
flex: 1;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
.gantt-right-area {
|
|||
|
|
min-width: 800px;
|
|||
|
|
}
|
|||
|
|
.gantt-timeline-header {
|
|||
|
|
height: 32px;
|
|||
|
|
background: #f7f7f7;
|
|||
|
|
border-bottom: 1px solid #eee;
|
|||
|
|
position: sticky;
|
|||
|
|
top: 0;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
.gantt-canvas-area {
|
|||
|
|
min-height: 400px;
|
|||
|
|
}
|
|||
|
|
</style>
|