Files
im-uniapp/components/Gantt/uniapp/Gantt.vue
2025-07-16 14:23:42 +08:00

146 lines
4.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>