9 changed files with 607 additions and 91 deletions
@ -0,0 +1,540 @@ |
|||
<template> |
|||
<div class="gantt-chart-container"> |
|||
<div class="gantt-header"> |
|||
<h1 class="gantt-title">甘特图</h1> |
|||
<div class="gantt-actions"> |
|||
<el-button type="primary" size="small" @click="refreshData">刷新</el-button> |
|||
<el-button type="default" size="small" @click="exportData">导出</el-button> |
|||
<el-button type="default" size="small" @click="closePage">关闭</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="gantt-content"> |
|||
<div class="gantt-toolbar"> |
|||
<div class="toolbar-left"> |
|||
<el-radio-group v-model="viewMode" size="small"> |
|||
<el-radio-button label="hour">小时视图</el-radio-button> |
|||
<el-radio-button label="day">日视图</el-radio-button> |
|||
</el-radio-group> |
|||
</div> |
|||
<div class="toolbar-right"> |
|||
<el-button-group size="small"> |
|||
<el-button icon="el-icon-d-arrow-left" @click="prevPeriod">上一个</el-button> |
|||
<el-button @click="today">今天</el-button> |
|||
<el-button icon="el-icon-d-arrow-right" @click="nextPeriod">下一个</el-button> |
|||
</el-button-group> |
|||
<span class="current-period">{{ currentPeriodText }}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="gantt-body"> |
|||
<div class="gantt-sidebar"> |
|||
<div class="sidebar-header">任务名称</div> |
|||
<div class="sidebar-content"> |
|||
<div v-for="item in ganttData" :key="item.id" class="sidebar-item"> |
|||
<div class="item-icon" :style="{ backgroundColor: item.color }"></div> |
|||
<div class="item-name">{{ item.name }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="gantt-timeline"> |
|||
<div class="timeline-header"> |
|||
<div v-for="(time, index) in timelineTimes" :key="index" class="timeline-time"> |
|||
<div class="time-label">{{ time }}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="timeline-content"> |
|||
<div v-for="item in ganttData" :key="item.id" class="timeline-row"> |
|||
<div |
|||
class="timeline-bar" |
|||
:style="{ |
|||
left: item.startPercent + '%', |
|||
width: item.durationPercent + '%', |
|||
backgroundColor: item.color |
|||
}" |
|||
> |
|||
<div class="bar-label">{{ item.name }}</div> |
|||
<div class="bar-time">{{ formatTime(item.startTime) }} - {{ formatTime(item.endTime) }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="gantt-legend"> |
|||
<div class="legend-title">图例:</div> |
|||
<div class="legend-items"> |
|||
<div v-for="legend in legends" :key="legend.type" class="legend-item"> |
|||
<div class="legend-color" :style="{ backgroundColor: legend.color }"></div> |
|||
<div class="legend-text">{{ legend.label }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'GanttChart', |
|||
data() { |
|||
return { |
|||
viewMode: 'day', |
|||
currentDate: new Date(), |
|||
ganttData: [ |
|||
{ |
|||
id: 1, |
|||
name: '航线一', |
|||
type: 'route', |
|||
color: '#409EFF', |
|||
startTime: new Date(2026, 1, 20, 6, 30), |
|||
endTime: new Date(2026, 1, 20, 8, 30), |
|||
startPercent: 3.33, |
|||
durationPercent: 13.33 |
|||
}, |
|||
{ |
|||
id: 2, |
|||
name: '航线二', |
|||
type: 'route', |
|||
color: '#67C23A', |
|||
startTime: new Date(2026, 1, 20, 8, 45), |
|||
endTime: new Date(2026, 1, 20, 10, 45), |
|||
startPercent: 21.67, |
|||
durationPercent: 13.33 |
|||
}, |
|||
{ |
|||
id: 3, |
|||
name: '航线三', |
|||
type: 'route', |
|||
color: '#E6A23C', |
|||
startTime: new Date(2026, 1, 20, 11, 0), |
|||
endTime: new Date(2026, 1, 20, 13, 0), |
|||
startPercent: 40, |
|||
durationPercent: 13.33 |
|||
}, |
|||
{ |
|||
id: 4, |
|||
name: '导弹发射任务', |
|||
type: 'missile', |
|||
color: '#F56C6C', |
|||
startTime: new Date(2026, 1, 20, 13, 15), |
|||
endTime: new Date(2026, 1, 20, 14, 15), |
|||
startPercent: 47.5, |
|||
durationPercent: 6.67 |
|||
}, |
|||
{ |
|||
id: 5, |
|||
name: '空中加油任务', |
|||
type: 'refuel', |
|||
color: '#909399', |
|||
startTime: new Date(2026, 1, 20, 14, 30), |
|||
endTime: new Date(2026, 1, 20, 16, 0), |
|||
startPercent: 55, |
|||
durationPercent: 10 |
|||
}, |
|||
{ |
|||
id: 6, |
|||
name: '航线四', |
|||
type: 'route', |
|||
color: '#409EFF', |
|||
startTime: new Date(2026, 1, 20, 16, 15), |
|||
endTime: new Date(2026, 1, 20, 18, 15), |
|||
startPercent: 63.33, |
|||
durationPercent: 13.33 |
|||
}, |
|||
{ |
|||
id: 7, |
|||
name: '航线五', |
|||
type: 'route', |
|||
color: '#67C23A', |
|||
startTime: new Date(2026, 1, 20, 18, 30), |
|||
endTime: new Date(2026, 1, 20, 20, 0), |
|||
startPercent: 81.67, |
|||
durationPercent: 10 |
|||
}, |
|||
{ |
|||
id: 8, |
|||
name: '侦察任务', |
|||
type: 'reconnaissance', |
|||
color: '#E6A23C', |
|||
startTime: new Date(2026, 1, 20, 9, 30), |
|||
endTime: new Date(2026, 1, 20, 11, 30), |
|||
startPercent: 30, |
|||
durationPercent: 13.33 |
|||
} |
|||
], |
|||
legends: [ |
|||
{ type: 'route', label: '航线', color: '#409EFF' }, |
|||
{ type: 'missile', label: '导弹发射', color: '#F56C6C' }, |
|||
{ type: 'refuel', label: '空中加油', color: '#909399' }, |
|||
{ type: 'reconnaissance', label: '侦察任务', color: '#E6A23C' } |
|||
] |
|||
} |
|||
}, |
|||
computed: { |
|||
currentPeriodText() { |
|||
const year = this.currentDate.getFullYear() |
|||
const month = this.currentDate.getMonth() + 1 |
|||
const day = this.currentDate.getDate() |
|||
const hour = this.currentDate.getHours() |
|||
const minute = this.currentDate.getMinutes() |
|||
|
|||
if (this.viewMode === 'hour') { |
|||
return `${year}年${month}月${day}日 ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}` |
|||
} else { |
|||
return `${year}年${month}月${day}日` |
|||
} |
|||
}, |
|||
timelineTimes() { |
|||
const times = [] |
|||
const startHour = 6 |
|||
const endHour = 20 |
|||
|
|||
for (let hour = startHour; hour <= endHour; hour++) { |
|||
times.push(`${hour.toString().padStart(2, '0')}:00`) |
|||
} |
|||
return times |
|||
} |
|||
}, |
|||
methods: { |
|||
refreshData() { |
|||
this.$message.success('数据已刷新') |
|||
}, |
|||
exportData() { |
|||
this.$message.success('数据导出中...') |
|||
}, |
|||
closePage() { |
|||
window.close() |
|||
}, |
|||
prevPeriod() { |
|||
if (this.viewMode === 'hour') { |
|||
this.currentDate.setHours(this.currentDate.getHours() - 1) |
|||
} else { |
|||
this.currentDate.setDate(this.currentDate.getDate() - 1) |
|||
} |
|||
}, |
|||
nextPeriod() { |
|||
if (this.viewMode === 'hour') { |
|||
this.currentDate.setHours(this.currentDate.getHours() + 1) |
|||
} else { |
|||
this.currentDate.setDate(this.currentDate.getDate() + 1) |
|||
} |
|||
}, |
|||
today() { |
|||
this.currentDate = new Date() |
|||
}, |
|||
formatTime(date) { |
|||
const hours = date.getHours().toString().padStart(2, '0') |
|||
const minutes = date.getMinutes().toString().padStart(2, '0') |
|||
return `${hours}:${minutes}` |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.gantt-chart-container { |
|||
width: 100%; |
|||
height: 100vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
background: #f5f5f5; |
|||
} |
|||
|
|||
.gantt-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 15px 20px; |
|||
background: #fff; |
|||
border-bottom: 1px solid #e4e7ed; |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.gantt-title { |
|||
margin: 0; |
|||
font-size: 20px; |
|||
font-weight: 500; |
|||
color: #303133; |
|||
} |
|||
|
|||
.gantt-actions { |
|||
display: flex; |
|||
gap: 10px; |
|||
} |
|||
|
|||
.gantt-content { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 20px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.gantt-toolbar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 15px; |
|||
padding: 10px 15px; |
|||
background: #fff; |
|||
border-radius: 4px; |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.toolbar-left { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15px; |
|||
} |
|||
|
|||
.toolbar-right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15px; |
|||
} |
|||
|
|||
.current-period { |
|||
font-size: 14px; |
|||
color: #606266; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.gantt-body { |
|||
flex: 1; |
|||
display: flex; |
|||
background: #fff; |
|||
border-radius: 4px; |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.gantt-sidebar { |
|||
width: 250px; |
|||
border-right: 1px solid #e4e7ed; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.sidebar-header { |
|||
height: 50px; |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 0 15px; |
|||
background: #f5f7fa; |
|||
border-bottom: 1px solid #e4e7ed; |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: #606266; |
|||
} |
|||
|
|||
.sidebar-content { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.sidebar-item { |
|||
height: 50px; |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 0 15px; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
transition: background-color 0.3s; |
|||
} |
|||
|
|||
.sidebar-item:hover { |
|||
background-color: #f5f7fa; |
|||
} |
|||
|
|||
.item-icon { |
|||
width: 12px; |
|||
height: 12px; |
|||
border-radius: 2px; |
|||
margin-right: 10px; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.item-name { |
|||
font-size: 13px; |
|||
color: #606266; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.gantt-timeline { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.timeline-header { |
|||
height: 50px; |
|||
display: flex; |
|||
background: #f5f7fa; |
|||
border-bottom: 1px solid #e4e7ed; |
|||
} |
|||
|
|||
.timeline-time { |
|||
flex: 1; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
border-right: 1px solid #e4e7ed; |
|||
} |
|||
|
|||
.time-label { |
|||
font-size: 13px; |
|||
color: #606266; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.timeline-content { |
|||
flex: 1; |
|||
position: relative; |
|||
overflow-y: auto; |
|||
background: linear-gradient(to right, |
|||
transparent 0%, |
|||
transparent 6.667%, |
|||
#f0f0f0 6.667%, |
|||
#f0f0f0 6.677%, |
|||
transparent 6.677%, |
|||
transparent 13.333%, |
|||
#f0f0f0 13.333%, |
|||
#f0f0f0 13.343%, |
|||
transparent 13.343%, |
|||
transparent 20%, |
|||
#f0f0f0 20%, |
|||
#f0f0f0 20.01%, |
|||
transparent 20.01%, |
|||
transparent 26.667%, |
|||
#f0f0f0 26.667%, |
|||
#f0f0f0 26.677%, |
|||
transparent 26.677%, |
|||
transparent 33.333%, |
|||
#f0f0f0 33.333%, |
|||
#f0f0f0 33.343%, |
|||
transparent 33.343%, |
|||
transparent 40%, |
|||
#f0f0f0 40%, |
|||
#f0f0f0 40.01%, |
|||
transparent 40.01%, |
|||
transparent 46.667%, |
|||
#f0f0f0 46.667%, |
|||
#f0f0f0 46.677%, |
|||
transparent 46.677%, |
|||
transparent 53.333%, |
|||
#f0f0f0 53.333%, |
|||
#f0f0f0 53.343%, |
|||
transparent 53.343%, |
|||
transparent 60%, |
|||
#f0f0f0 60%, |
|||
#f0f0f0 60.01%, |
|||
transparent 60.01%, |
|||
transparent 66.667%, |
|||
#f0f0f0 66.667%, |
|||
#f0f0f0 66.677%, |
|||
transparent 66.677%, |
|||
transparent 73.333%, |
|||
#f0f0f0 73.333%, |
|||
#f0f0f0 73.343%, |
|||
transparent 73.343%, |
|||
transparent 80%, |
|||
#f0f0f0 80%, |
|||
#f0f0f0 80.01%, |
|||
transparent 80.01%, |
|||
transparent 86.667%, |
|||
#f0f0f0 86.667%, |
|||
#f0f0f0 86.677%, |
|||
transparent 86.677%, |
|||
transparent 93.333%, |
|||
#f0f0f0 93.333%, |
|||
#f0f0f0 93.343%, |
|||
transparent 93.343%, |
|||
transparent 100% |
|||
); |
|||
} |
|||
|
|||
.timeline-row { |
|||
height: 50px; |
|||
position: relative; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
} |
|||
|
|||
.timeline-bar { |
|||
position: absolute; |
|||
top: 8px; |
|||
height: 36px; |
|||
border-radius: 4px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
cursor: pointer; |
|||
transition: transform 0.2s, box-shadow 0.2s; |
|||
} |
|||
|
|||
.timeline-bar:hover { |
|||
transform: translateY(-2px); |
|||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); |
|||
} |
|||
|
|||
.bar-label { |
|||
font-size: 12px; |
|||
color: #fff; |
|||
font-weight: 500; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
padding: 0 8px; |
|||
} |
|||
|
|||
.bar-time { |
|||
font-size: 10px; |
|||
color: rgba(255, 255, 255, 0.9); |
|||
margin-top: 2px; |
|||
} |
|||
|
|||
.gantt-legend { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 15px 20px; |
|||
background: #fff; |
|||
border-top: 1px solid #e4e7ed; |
|||
margin-top: 15px; |
|||
border-radius: 4px; |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.legend-title { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: #606266; |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
.legend-items { |
|||
display: flex; |
|||
gap: 20px; |
|||
} |
|||
|
|||
.legend-item { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.legend-color { |
|||
width: 16px; |
|||
height: 16px; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
.legend-text { |
|||
font-size: 13px; |
|||
color: #606266; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue