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