Browse Source

甘特图

lbj
sd 1 month ago
parent
commit
48fb8045a0
  1. 7
      ruoyi-ui/src/lang/en.js
  2. 7
      ruoyi-ui/src/lang/zh.js
  3. 5
      ruoyi-ui/src/router/index.js
  4. 11
      ruoyi-ui/src/views/cesiumMap/index.vue
  5. 4
      ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue
  6. 81
      ruoyi-ui/src/views/childRoom/BottomTimeline.vue
  7. 5
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  8. 14
      ruoyi-ui/src/views/childRoom/index.vue
  9. 540
      ruoyi-ui/src/views/ganttChart/index.vue

7
ruoyi-ui/src/lang/en.js

@ -71,6 +71,13 @@ export default {
systemDescription: 'System Description',
language: 'Language'
},
tools: {
routeCalculation: 'Route Calculation',
conflictDisplay: 'Conflict Display',
dataMaterials: 'Data Materials',
coordinateConversion: 'Coordinate Conversion',
generateGanttChart: 'Generate Gantt Chart'
},
favorites: {
layerFavorites: 'Layer Favorites',
routeFavorites: 'Route Favorites'

7
ruoyi-ui/src/lang/zh.js

@ -71,6 +71,13 @@ export default {
systemDescription: '系统说明',
language: '语言'
},
tools: {
routeCalculation: '航线计算',
conflictDisplay: '冲突显示',
dataMaterials: '数据资料',
coordinateConversion: '坐标转换',
generateGanttChart: '生成甘特图'
},
favorites: {
layerFavorites: '图层收藏',
routeFavorites: '航线收藏'

5
ruoyi-ui/src/router/index.js

@ -70,6 +70,11 @@ export const constantRoutes = [
hidden: true
},
{
path: '/ganttChart',
component: () => import('@/views/ganttChart'),
hidden: true
},
{
path: '/register',
component: () => import('@/views/register'),
hidden: true

11
ruoyi-ui/src/views/cesiumMap/index.vue

@ -62,7 +62,7 @@
/>
<!-- 地图右下角比例尺 + 经纬度 -->
<div class="map-info-panel">
<div class="map-info-panel" :class="{ 'panel-raised': bottomPanelVisible }">
<div class="scale-bar" @click="handleScaleClick">
<span class="scale-bar-text">{{ scaleBarText }}</span>
<div class="scale-bar-line" :style="{ width: scaleBarWidthPx + 'px' }">
@ -119,6 +119,10 @@ export default {
coordinateFormat: {
type: String,
default: 'dms' // 'decimal' 'dms'
},
bottomPanelVisible: {
type: Boolean,
default: false
}
},
watch: {
@ -5241,6 +5245,11 @@ this.viewer.scene.postProcessStages.fxaa.enabled = true
gap: 6px;
z-index: 1000;
pointer-events: none;
transition: bottom 0.3s ease;
}
.map-info-panel.panel-raised {
bottom: 65px;
}
/* 比例尺:高德风格,浅色底、圆角、刻度线 */

4
ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue

@ -75,19 +75,23 @@ export default {
this.$refs.timeline.isVisible = true
this.isExpanded = false
this.showPanel = false
this.$emit('bottom-panel-visible', true)
}
},
onTimelineHidden() {
this.showPanel = true
this.$emit('bottom-panel-visible', false)
},
showSixSteps() {
this.showSixStepsBar = true
this.isExpanded = false
this.showPanel = false
this.$emit('bottom-panel-visible', true)
},
hideSixStepsBar() {
this.showSixStepsBar = false
this.showPanel = true
this.$emit('bottom-panel-visible', false)
},
selectStep(index) {
const clickedStep = this.sixStepsData[index]

81
ruoyi-ui/src/views/childRoom/BottomTimeline.vue

@ -137,28 +137,6 @@
<el-form-item label="描述">
<el-input v-model="segmentForm.description" type="textarea" :rows="3" placeholder="输入描述" />
</el-form-item>
<el-form-item label="提醒方式">
<el-checkbox-group v-model="segmentForm.reminderTypes">
<el-checkbox label="popup">弹窗提醒</el-checkbox>
<el-checkbox label="sound">声音提醒</el-checkbox>
<el-checkbox label="message">消息提醒</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="分配任务">
<el-select
v-model="segmentForm.assignedTask"
placeholder="选择任务"
style="width: 100%"
clearable
>
<el-option
v-for="task in availableTasks"
:key="task.id"
:label="task.name"
:value="task.id"
/>
</el-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showSegmentDialog = false">取消</el-button>
@ -189,18 +167,6 @@
<span>{{ currentSegment.description }}</span>
</div>
<div class="detail-row">
<label>分配任务</label>
<span>{{ getTaskName(currentSegment.assignedTask) }}</span>
</div>
<div class="detail-row">
<label>提醒方式</label>
<div class="reminder-tags">
<el-tag v-if="currentSegment.reminderTypes.includes('popup')" size="small">弹窗</el-tag>
<el-tag v-if="currentSegment.reminderTypes.includes('sound')" size="small">声音</el-tag>
<el-tag v-if="currentSegment.reminderTypes.includes('message')" size="small">消息</el-tag>
</div>
</div>
<div class="detail-row">
<label>状态</label>
<el-tag :type="currentSegment.passed ? 'success' : (currentSegment.active ? 'warning' : 'info')" size="small">
{{ currentSegment.passed ? '已到达' : (currentSegment.active ? '即将到达' : '未到达') }}
@ -236,23 +202,11 @@ export default {
segmentForm: {
time: null,
name: '',
description: '',
reminderTypes: ['popup', 'message'],
assignedTask: null
description: ''
},
timer: null,
audio: null,
currentTimeStr: '',
availableTasks: [
{ id: 1, name: '任务准备' },
{ id: 2, name: '资源调配' },
{ id: 3, name: '任务执行' },
{ id: 4, name: '任务监控' },
{ id: 5, name: '任务完成' },
{ id: 6, name: '数据收集' },
{ id: 7, name: '分析评估' },
{ id: 8, name: '报告生成' }
]
currentTimeStr: ''
}
},
mounted() {
@ -279,9 +233,7 @@ export default {
this.segmentForm = {
time: null,
name: '',
description: '',
reminderTypes: ['popup', 'message'],
assignedTask: null
description: ''
}
this.showSegmentDialog = true
},
@ -312,8 +264,6 @@ export default {
description: '开始准备任务所需的资源和设备',
active: false,
passed: false,
reminderTypes: ['popup', 'message'],
assignedTask: 1,
position: 0,
triggered: false
},
@ -323,8 +273,6 @@ export default {
description: '完成资源的调配和分配',
active: false,
passed: false,
reminderTypes: ['popup', 'message'],
assignedTask: 2,
position: 0,
triggered: false
},
@ -334,8 +282,6 @@ export default {
description: '开始执行主要任务',
active: false,
passed: false,
reminderTypes: ['popup', 'sound', 'message'],
assignedTask: 3,
position: 0,
triggered: false
},
@ -345,8 +291,6 @@ export default {
description: '监控任务执行进度',
active: false,
passed: false,
reminderTypes: ['popup', 'message'],
assignedTask: 4,
position: 0,
triggered: false
},
@ -356,8 +300,6 @@ export default {
description: '任务完成,进行总结',
active: false,
passed: false,
reminderTypes: ['popup', 'sound', 'message'],
assignedTask: 5,
position: 0,
triggered: false
}
@ -426,26 +368,20 @@ export default {
},
triggerReminder(segment) {
if (segment.reminderTypes.includes('popup')) {
this.$notify({
title: '时间提醒',
message: `${segment.time} - ${segment.name}`,
type: 'warning',
duration: 5000
})
}
if (segment.reminderTypes.includes('sound')) {
this.playSound()
}
if (segment.reminderTypes.includes('message')) {
this.$message({
message: `时间提醒:${segment.time} - ${segment.name}`,
type: 'warning',
duration: 5000
})
}
},
playSound() {
@ -492,9 +428,7 @@ export default {
this.segmentForm = {
time: this.parseTime(segment.time),
name: segment.name,
description: segment.description,
reminderTypes: [...segment.reminderTypes],
assignedTask: segment.assignedTask
description: segment.description
}
this.showSegmentDialog = true
},
@ -509,8 +443,6 @@ export default {
time: this.formatTime(this.segmentForm.time),
name: this.segmentForm.name,
description: this.segmentForm.description,
reminderTypes: [...this.segmentForm.reminderTypes],
assignedTask: this.segmentForm.assignedTask,
active: false,
passed: false,
triggered: false,
@ -544,11 +476,6 @@ export default {
}).catch(() => {})
},
getTaskName(taskId) {
const task = this.availableTasks.find(t => t.id === taskId)
return task ? task.name : '未分配'
},
formatTime(date) {
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')

5
ruoyi-ui/src/views/childRoom/TopHeader.vue

@ -156,6 +156,7 @@
<el-dropdown-item @click.native="conflictDisplay">{{ $t('topHeader.tools.conflictDisplay') }}</el-dropdown-item>
<el-dropdown-item @click.native="dataMaterials">{{ $t('topHeader.tools.dataMaterials') }}</el-dropdown-item>
<el-dropdown-item @click.native="coordinateConversion">{{ $t('topHeader.tools.coordinateConversion') }}</el-dropdown-item>
<el-dropdown-item @click.native="generateGanttChart">{{ $t('topHeader.tools.generateGanttChart') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@ -574,6 +575,10 @@ export default {
this.$emit('toggle-route', this.isRouteVisible)
},
generateGanttChart() {
this.$emit('generate-gantt-chart')
},
systemDescription() {
this.$emit('system-description')
},

14
ruoyi-ui/src/views/childRoom/index.vue

@ -13,6 +13,7 @@
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
:scaleConfig="scaleConfig"
:coordinateFormat="coordinateFormat"
:bottomPanelVisible="bottomPanelVisible"
@draw-complete="handleMapDrawComplete"
@drawing-points-update="missionDrawingPointsCount = $event"
@open-waypoint-dialog="handleOpenWaypointEdit"
@ -126,6 +127,7 @@
@toggle-airport="toggleAirport"
@toggle-landmark="toggleLandmark"
@toggle-route="toggleRoute"
@generate-gantt-chart="generateGanttChart"
@system-description="systemDescription"
@layer-favorites="layerFavorites"
@route-favorites="routeFavorites"
@ -190,7 +192,7 @@
@open-import-dialog="showImportDialog = true"
/>
<!-- 左下角工具面板 -->
<bottom-left-panel v-show="!screenshotMode" />
<bottom-left-panel v-show="!screenshotMode" @bottom-panel-visible="handleBottomPanelVisible" />
<!-- 底部时间轴最初版本的样式- 蓝色主题 -->
<div
v-show="!screenshotMode"
@ -471,6 +473,8 @@ export default {
platformIconSaveTimer: null,
//
showImportDialog: false,
// /
bottomPanelVisible: false,
//
screenshotMode: false,
@ -709,6 +713,9 @@ export default {
if (this.currentRoomId) this.getRoomDetail();
},
methods: {
handleBottomPanelVisible(visible) {
this.bottomPanelVisible = visible
},
//
async handleOpenWaypointEdit(wpId, routeId) {
console.log(`>>> [父组件接收] 航点 ID: ${wpId}, 所属航线 ID: ${routeId}`);
@ -1867,6 +1874,11 @@ export default {
this.$message.success(this.showRoute ? '显示航线' : '隐藏航线');
},
generateGanttChart() {
const url = this.$router.resolve('/ganttChart').href
window.open(url, '_blank')
},
systemDescription() {
this.$message.success('系统说明');
//

540
ruoyi-ui/src/views/ganttChart/index.vue

@ -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…
Cancel
Save