Vue3 使用 echarts 甘特图(GanttChart)
Documentation - Apache ECharts
可自定义设置以下属性
- 甘特图数据(ganttData),类型:Gantt[],必传,默认 []
- 容器宽度(width),类型:number | string,默认 ‘100%’
- 容器高度(height),类型:number | string,默认 ‘100%’
- 主题色(themeColor),类型: string,默认 ‘#1677ff’
- 状态映射表(statusMap),类型: Status[],默认 []
type Gantt
- 名称(name),类型:string,必传
- 开始时间(start),类型:string | number | Date,必传
- 结束时间(end),类型:string | number | Date,必传
- 状态值(status),类型:number | string,用于着色
type Status
- 状态值(value),类型:number | string,必传
- 状态名(label),类型:string,必传
- 状态颜色(color),类型:string,必传
效果如下图:echarts@6.0.0
在线预览
安装插件
pnpm add echarts
创建甘特图组件GanttChart.vue
<script setup lang="ts">
import { ref, useTemplateRef, onMounted, onBeforeUnmount, watch, computed } from 'vue'
import { useResizeObserver } from '../utils'
import * as echarts from 'echarts/core'
import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
import { CustomChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'echarts.use([TooltipComponent, GridComponent, LegendComponent, CustomChart, CanvasRenderer])const chartRef = useTemplateRef('chartRef')
const myChart = ref<any>()
let option: anyinterface Gantt {name: string // 名称start: string | number | Date // 开始时间end: string | number | Date // 结束时间status: number | string // 状态值,用于着色
}
interface Status {value: number | string // 状态值label: string // 状态名color: string // 状态颜色
}
interface Props {ganttData?: Gantt[] // 数据width?: string | number // 容器宽度height?: string | number // 容器高度themeColor?: string // 主题色statusMap?: Status[] // 状态映射表
}
const props = withDefaults(defineProps<Props>(), {ganttData: () => [],width: '100%',height: '100%',themeColor: '#1677FF',statusMap: () => []
})
const chartWidth = computed(() => {if (typeof props.width === 'number') {return `${props.width}px`}return props.width
})
const chartHeight = computed(() => {if (typeof props.height === 'number') {return `${props.height}px`}return props.height
})
watch(() => props.ganttData,() => {myChart.value && myChart.value.setOption(buildOption(), true)},{ deep: true }
)
// 统一转换为时间戳格式
function toTimestamp(value: string | number | Date): number {if (typeof value === 'number') return valueif (value instanceof Date) return value.getTime()// 替换空格为 'T' 以提升跨浏览器解析稳定性const normalized = value.replace(' ', 'T')const t = Date.parse(normalized)return isNaN(t) ? new Date(value).getTime() : t
}
// 处理数据:输出 y 轴分类与自定义系列的数据
function getSeriesData() {const yAxisData = props.ganttData.map((item) => item.name)const data = props.ganttData.map((item, index) => {return {value: [index, // 类别索引toTimestamp(item.start), // 开始时间戳toTimestamp(item.end), // 结束时间戳item.status],name: item.name}})return { yAxisData, data }
}
// 根据状态值获取对应的颜色
function getStatusColor(value: string | number): string {const statusItem = props.statusMap.find((status) => String(status.value) === String(value))const color = statusItem ? statusItem.color : '#5470c6' // 默认颜色return color
}
function renderGanttBar(params: any, api: any) {const categoryIndex = api.value(0)const startCoord = api.coord([api.value(1), categoryIndex])const endCoord = api.coord([api.value(2), categoryIndex])const barHeight = Math.max(api.size([0, 1])[1] * 0.6, 2)const rectShape = {x: startCoord[0],y: startCoord[1] - barHeight / 2,width: Math.max(endCoord[0] - startCoord[0], 0),height: barHeight}const shape = echarts.graphic.clipRectByRect(rectShape, {x: params.coordSys.x,y: params.coordSys.y,width: params.coordSys.width,height: params.coordSys.height})if (shape) {// 为矩形添加圆角;(shape as any).r = 2}// 获取状态值const statusValue = api.value(3)// 获取对应的颜色const color = getStatusColor(statusValue)return { // 返回自定义矩形元素type: 'rect',shape,style: {fill: color,stroke: color}}
}
// 构建图表配置项 option
function buildOption() {const { yAxisData, data } = getSeriesData()option = {grid: {top: 0,left: 0,right: 0,bottom: 42,containLabel: true},legend: {orient: 'horizontal',left: 'center',bottom: 0,itemGap: 18,textStyle: {fontWeight: 400,fontSize: 14,color: '#333',lineHeight: 22},data: props.statusMap.map((status: Status) => {return status.label})},tooltip: {trigger: 'item',formatter: (params: any) => {// console.log('params', params)const start = params.value[1]const end = params.value[2]const format = (timestamp: number) => {return echarts.time.format(timestamp, '{HH}:{mm}:{ss}', false)}const piecesMap = new Map(props.statusMap.map((status: Status) => [String(status.value), status.label]))const statusLabel = piecesMap.get(String(params.value[3])) || ''return `${params.name}<br/>${format(start)} - ${format(end)}${statusLabel ? `<br/>状态:${params.marker} ${statusLabel}` : ''}`}},xAxis: {type: 'time',position: 'top',splitLine: {show: true,lineStyle: {type: 'dashed',color: '#ccc'}},axisLabel: {color: '#333',fontWeight: 400,fontSize: 14,lineHeight: 22,align: 'center',showMinLabel: true,showMaxLabel: true,formatter: (value: number) => {// return echarts.time.format(value, '{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}', false)return echarts.time.format(value, '{HH}:{mm}', false)}}},yAxis: {type: 'category',inverse: true,data: yAxisData,axisLabel: {color: 'rgba(0, 0, 0, 0.88)',fontWeight: 500,fontSize: 14,lineHeight: 22}},series: props.statusMap.map((status) => ({name: status.label,type: 'custom',renderItem: renderGanttBar, // 以 Function 形式提供图形渲染的逻辑encode: { x: [1, 2], y: 0 }, // 定义 data 的哪个维度被编码成什么itemStyle: { // 图形样式color: getStatusColor(status.value)},data: data.filter((item) => String(item.value[3]) === String(status.value))}))}return option
}
function initChart() {myChart.value = echarts.init(chartRef.value)myChart.value.setOption(buildOption())
}
function showLoading (config: any) {myChart.value && myChart.value.showLoading('default', { text: '', color: props.themeColor, ...config }) // 显示加载动画效果
}
function hideLoading () {myChart.value && myChart.value.hideLoading() // 隐藏动画加载效果
}
// 监听图表容器尺寸变化,重新初始化图表
useResizeObserver(chartRef, () => {requestAnimationFrame(() => {myChart.value && myChart.value.resize()})
})
onMounted(() => {initChart()
})
onBeforeUnmount(() => {myChart.value && myChart.value.dispose() // 销毁图表实例
})
defineExpose({showLoading,hideLoading
})
</script>
<template><divclass="chart-container"ref="chartRef":style="`--chart-width: ${chartWidth}; --chart-height: ${chartHeight};`"></div>
</template>
<style lang="less" scoped>
.chart-container {width: var(--chart-width);height: var(--chart-height);
}
</style>
在要使用的页面引入
<script setup lang="ts">
import GanttChart from './GanttChart.vue'
import { useTemplateRef, ref, onMounted } from 'vue'
const ganttRef = useTemplateRef('ganttRef')
const ganttData = ref<any[]>([])
onMounted(() => {getGanttData()
})
function getGanttData () { // 模拟接口调用ganttRef.value.showLoading()setTimeout(() => {ganttData.value.push({name: '任务一',start: '2025-08-12 00:00:00',end: '2025-08-12 04:00:00',status: '1'},{name: '任务二',start: '2025-08-12 04:00:00',end: '2025-08-12 05:00:00',status: '2'},{name: '任务三',start: '2025-08-12 05:00:00',end: '2025-08-12 06:00:00',status: '3'},{name: '任务四',start: '2025-08-12 06:00:00',end: '2025-08-12 08:00:00',status: '4'},{name: '任务五',start: '2025-08-12 08:00:00',end: '2025-08-12 13:00:00',status: '5'})ganttRef.value.hideLoading()}, 1500)
}
const statusMap = [{ value: '1', label: '启动', color: '#5470c6' },{ value: '2', label: '运行', color: '#91cc75' },{ value: '3', label: '等待', color: '#fac858' },{ value: '4', label: '成功', color: '#ee6666' },{ value: '5', label: '失败', color: '#73c0de' },{ value: '6', label: '停止', color: '#3ba272' }
]
</script>
<template><div><h1>GaugeChart 参考文档</h1><ul class="m-list"><li><a class="u-file" href="https://echarts.apache.org/handbook/zh/get-started" target="_blank">使用手册</a></li><li><a class="u-file" href="https://echarts.apache.org/handbook/zh/basics/import" target="_blank">在项目中引入 ECharts</a></li><li><a class="u-file" href="https://echarts.apache.org/zh/builder.html" target="_blank">ECharts 在线定制</a></li></ul><GanttChartref="ganttRef":gantt-data="ganttData":status-map="statusMap":height="500"/></div>
</template>