Browse Source

Merge branch 'lbj' of http://124.70.32.114:3100/woka/cesium-map-object

# Conflicts:
#	ruoyi-ui/src/views/cesiumMap/index.vue
#	ruoyi-ui/src/views/childRoom/index.vue
mh
ctw 1 month ago
parent
commit
5a092bd197
  1. 81
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTimelineSegmentController.java
  2. 89
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysTimelineSegment.java
  3. 22
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTimelineSegmentMapper.java
  4. 21
      ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTimelineSegmentService.java
  5. 57
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTimelineSegmentServiceImpl.java
  6. 77
      ruoyi-system/src/main/resources/mapper/system/SysTimelineSegmentMapper.xml
  7. 53
      ruoyi-ui/src/api/system/timeline.js
  8. 7
      ruoyi-ui/src/lang/en.js
  9. 7
      ruoyi-ui/src/lang/zh.js
  10. 5
      ruoyi-ui/src/router/index.js
  11. 49
      ruoyi-ui/src/views/cesiumMap/index.vue
  12. 16
      ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue
  13. 550
      ruoyi-ui/src/views/childRoom/BottomTimeline.vue
  14. 5
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  15. 14
      ruoyi-ui/src/views/childRoom/index.vue
  16. 540
      ruoyi-ui/src/views/ganttChart/index.vue
  17. 22
      sql/sys_timeline_segment.sql

81
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysTimelineSegmentController.java

@ -0,0 +1,81 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysTimelineSegment;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.service.ISysTimelineSegmentService;
@RestController
@RequestMapping("/system/timeline")
public class SysTimelineSegmentController extends BaseController
{
@Autowired
private ISysTimelineSegmentService segmentService;
@PreAuthorize("@ss.hasPermi('system:timeline:list')")
@GetMapping("/list")
public AjaxResult list(SysTimelineSegment segment)
{
List<SysTimelineSegment> list = segmentService.selectSegmentList(segment);
return success(list);
}
@GetMapping("/listByRoomId/{roomId}")
public AjaxResult listByRoomId(@PathVariable Long roomId)
{
List<SysTimelineSegment> list = segmentService.selectSegmentListByRoomId(roomId);
return success(list);
}
@PreAuthorize("@ss.hasPermi('system:timeline:query')")
@GetMapping(value = "/{segmentId}")
public AjaxResult getInfo(@PathVariable Long segmentId)
{
return success(segmentService.selectSegmentById(segmentId));
}
@PreAuthorize("@ss.hasPermi('system:timeline:add')")
@Log(title = "时间轴管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysTimelineSegment segment)
{
return toAjax(segmentService.insertSegment(segment));
}
@PreAuthorize("@ss.hasPermi('system:timeline:edit')")
@Log(title = "时间轴管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysTimelineSegment segment)
{
return toAjax(segmentService.updateSegment(segment));
}
@PreAuthorize("@ss.hasPermi('system:timeline:remove')")
@Log(title = "时间轴管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{segmentId}")
public AjaxResult remove(@PathVariable Long segmentId)
{
return toAjax(segmentService.deleteSegmentById(segmentId));
}
@PreAuthorize("@ss.hasPermi('system:timeline:remove')")
@Log(title = "时间轴管理", businessType = BusinessType.DELETE)
@DeleteMapping("/room/{roomId}")
public AjaxResult removeByRoomId(@PathVariable Long roomId)
{
return toAjax(segmentService.deleteSegmentByRoomId(roomId));
}
}

89
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysTimelineSegment.java

@ -0,0 +1,89 @@
package com.ruoyi.common.core.domain.entity;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class SysTimelineSegment
{
private static final long serialVersionUID = 1L;
private Long segmentId;
private Long roomId;
@NotBlank(message = "时间点不能为空")
@Size(min = 0, max = 8, message = "时间点长度不能超过8个字符")
private String segmentTime;
@NotBlank(message = "时间段名称不能为空")
@Size(min = 0, max = 50, message = "时间段名称长度不能超过50个字符")
private String segmentName;
@Size(max = 500, message = "时间段描述长度不能超过500个字符")
private String segmentDesc;
public Long getSegmentId()
{
return segmentId;
}
public void setSegmentId(Long segmentId)
{
this.segmentId = segmentId;
}
@NotNull(message = "房间ID不能为空")
public Long getRoomId()
{
return roomId;
}
public void setRoomId(Long roomId)
{
this.roomId = roomId;
}
public String getSegmentTime()
{
return segmentTime;
}
public void setSegmentTime(String segmentTime)
{
this.segmentTime = segmentTime;
}
public String getSegmentName()
{
return segmentName;
}
public void setSegmentName(String segmentName)
{
this.segmentName = segmentName;
}
public String getSegmentDesc()
{
return segmentDesc;
}
public void setSegmentDesc(String segmentDesc)
{
this.segmentDesc = segmentDesc;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("segmentId", getSegmentId())
.append("roomId", getRoomId())
.append("segmentTime", getSegmentTime())
.append("segmentName", getSegmentName())
.append("segmentDesc", getSegmentDesc())
.toString();
}
}

22
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTimelineSegmentMapper.java

@ -0,0 +1,22 @@
package com.ruoyi.system.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.common.core.domain.entity.SysTimelineSegment;
public interface SysTimelineSegmentMapper
{
public List<SysTimelineSegment> selectSegmentList(SysTimelineSegment segment);
public List<SysTimelineSegment> selectSegmentListByRoomId(@Param("roomId") Long roomId);
public SysTimelineSegment selectSegmentById(Long segmentId);
public int insertSegment(SysTimelineSegment segment);
public int updateSegment(SysTimelineSegment segment);
public int deleteSegmentById(Long segmentId);
public int deleteSegmentByRoomId(@Param("roomId") Long roomId);
}

21
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTimelineSegmentService.java

@ -0,0 +1,21 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.common.core.domain.entity.SysTimelineSegment;
public interface ISysTimelineSegmentService
{
public List<SysTimelineSegment> selectSegmentList(SysTimelineSegment segment);
public List<SysTimelineSegment> selectSegmentListByRoomId(Long roomId);
public SysTimelineSegment selectSegmentById(Long segmentId);
public int insertSegment(SysTimelineSegment segment);
public int updateSegment(SysTimelineSegment segment);
public int deleteSegmentById(Long segmentId);
public int deleteSegmentByRoomId(Long roomId);
}

57
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTimelineSegmentServiceImpl.java

@ -0,0 +1,57 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.domain.entity.SysTimelineSegment;
import com.ruoyi.system.mapper.SysTimelineSegmentMapper;
import com.ruoyi.system.service.ISysTimelineSegmentService;
@Service
public class SysTimelineSegmentServiceImpl implements ISysTimelineSegmentService
{
@Autowired
private SysTimelineSegmentMapper segmentMapper;
@Override
public List<SysTimelineSegment> selectSegmentList(SysTimelineSegment segment)
{
return segmentMapper.selectSegmentList(segment);
}
@Override
public List<SysTimelineSegment> selectSegmentListByRoomId(Long roomId)
{
return segmentMapper.selectSegmentListByRoomId(roomId);
}
@Override
public SysTimelineSegment selectSegmentById(Long segmentId)
{
return segmentMapper.selectSegmentById(segmentId);
}
@Override
public int insertSegment(SysTimelineSegment segment)
{
return segmentMapper.insertSegment(segment);
}
@Override
public int updateSegment(SysTimelineSegment segment)
{
return segmentMapper.updateSegment(segment);
}
@Override
public int deleteSegmentById(Long segmentId)
{
return segmentMapper.deleteSegmentById(segmentId);
}
@Override
public int deleteSegmentByRoomId(Long roomId)
{
return segmentMapper.deleteSegmentByRoomId(roomId);
}
}

77
ruoyi-system/src/main/resources/mapper/system/SysTimelineSegmentMapper.xml

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysTimelineSegmentMapper">
<resultMap type="SysTimelineSegment" id="SysTimelineSegmentResult">
<id property="segmentId" column="segment_id" />
<result property="roomId" column="room_id" />
<result property="segmentTime" column="segment_time" />
<result property="segmentName" column="segment_name" />
<result property="segmentDesc" column="segment_desc" />
</resultMap>
<sql id="selectSegmentVo">
select s.segment_id, s.room_id, s.segment_time, s.segment_name, s.segment_desc
from sys_timeline_segment s
</sql>
<select id="selectSegmentList" parameterType="SysTimelineSegment" resultMap="SysTimelineSegmentResult">
<include refid="selectSegmentVo"/>
where 1=1
<if test="roomId != null and roomId != 0">
AND room_id = #{roomId}
</if>
<if test="segmentName != null and segmentName != ''">
AND segment_name like concat('%', #{segmentName}, '%')
</if>
order by s.segment_time
</select>
<select id="selectSegmentListByRoomId" parameterType="Long" resultMap="SysTimelineSegmentResult">
<include refid="selectSegmentVo"/>
where room_id = #{roomId}
order by s.segment_time
</select>
<select id="selectSegmentById" parameterType="Long" resultMap="SysTimelineSegmentResult">
<include refid="selectSegmentVo"/>
where segment_id = #{segmentId}
</select>
<insert id="insertSegment" parameterType="SysTimelineSegment">
insert into sys_timeline_segment(
room_id,
segment_time,
segment_name,
<if test="segmentDesc != null and segmentDesc != ''">segment_desc,</if>
segment_id
)values(
#{roomId},
#{segmentTime},
#{segmentName},
<if test="segmentDesc != null and segmentDesc != ''">#{segmentDesc},</if>
#{segmentId}
)
</insert>
<update id="updateSegment" parameterType="SysTimelineSegment">
update sys_timeline_segment
<set>
<if test="segmentTime != null and segmentTime != ''">segment_time = #{segmentTime},</if>
<if test="segmentName != null and segmentName != ''">segment_name = #{segmentName},</if>
<if test="segmentDesc != null">segment_desc = #{segmentDesc},</if>
</set>
where segment_id = #{segmentId}
</update>
<delete id="deleteSegmentById" parameterType="Long">
delete from sys_timeline_segment where segment_id = #{segmentId}
</delete>
<delete id="deleteSegmentByRoomId" parameterType="Long">
delete from sys_timeline_segment where room_id = #{roomId}
</delete>
</mapper>

53
ruoyi-ui/src/api/system/timeline.js

@ -0,0 +1,53 @@
import request from '@/utils/request'
export function listTimelineSegments(query) {
return request({
url: '/system/timeline/list',
method: 'get',
params: query
})
}
export function getTimelineSegmentsByRoomId(roomId) {
return request({
url: '/system/timeline/listByRoomId/' + roomId,
method: 'get'
})
}
export function getTimelineSegment(id) {
return request({
url: '/system/timeline/' + id,
method: 'get'
})
}
export function addTimelineSegment(data) {
return request({
url: '/system/timeline',
method: 'post',
data: data
})
}
export function updateTimelineSegment(data) {
return request({
url: '/system/timeline',
method: 'put',
data: data
})
}
export function delTimelineSegment(id) {
return request({
url: '/system/timeline/' + id,
method: 'delete'
})
}
export function delTimelineSegmentsByRoomId(roomId) {
return request({
url: '/system/timeline/room/' + roomId,
method: 'delete'
})
}

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

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

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

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

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

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

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

@ -100,7 +100,7 @@
<el-input-number <el-input-number
v-model="editPlatformForm.iconSize" v-model="editPlatformForm.iconSize"
:min="48" :min="48"
:max="256" :max="256"
controls-position="right" controls-position="right"
style="width: 100%;" style="width: 100%;"
/> />
@ -144,7 +144,7 @@
</el-dialog> </el-dialog>
<!-- 地图右下角比例尺 + 经纬度 --> <!-- 地图右下角比例尺 + 经纬度 -->
<div class="map-info-panel"> <div class="map-info-panel" :class="{ 'panel-raised': bottomPanelVisible }">
<div class="scale-bar" @click="handleScaleClick"> <div class="scale-bar" @click="handleScaleClick">
<span class="scale-bar-text">{{ scaleBarText }}</span> <span class="scale-bar-text">{{ scaleBarText }}</span>
<div class="scale-bar-line" :style="{ width: scaleBarWidthPx + 'px' }"> <div class="scale-bar-line" :style="{ width: scaleBarWidthPx + 'px' }">
@ -204,6 +204,10 @@ export default {
type: String, type: String,
default: 'dms' // 'decimal' 'dms' default: 'dms' // 'decimal' 'dms'
}, },
bottomPanelVisible: {
type: Boolean,
default: false
},
/** 航线上锁状态(由父组件维护,与右侧列表锁图标同步) */ /** 航线上锁状态(由父组件维护,与右侧列表锁图标同步) */
routeLocked: { routeLocked: {
type: Object, type: Object,
@ -1643,9 +1647,9 @@ export default {
id: platformBillboardId, id: platformBillboardId,
name: (platform && platform.name) || '平台', name: (platform && platform.name) || '平台',
position: originalPositions[0], position: originalPositions[0],
properties: { properties: {
routeId: routeId, routeId: routeId,
platformId: (platform && platform.id) || 0 platformId: (platform && platform.id) || 0
}, },
billboard: { billboard: {
image: fullUrl, image: fullUrl,
@ -1666,9 +1670,9 @@ export default {
if (target && target.billboard) { if (target && target.billboard) {
target.billboard.image = whiteImage; target.billboard.image = whiteImage;
// target.billboard.color BLACK线 // target.billboard.color BLACK线
// Redis applyRedisPlatformStyles // Redis applyRedisPlatformStyles
if (this.platformCustomStyles && this.platformCustomStyles[routeId]) { if (this.platformCustomStyles && this.platformCustomStyles[routeId]) {
const style = this.platformCustomStyles[routeId]; const style = this.platformCustomStyles[routeId];
if (style.platformColor) { if (style.platformColor) {
@ -2372,7 +2376,7 @@ export default {
// 使 Redis 使 routeLabelStyles // 使 Redis 使 routeLabelStyles
const customStyle = this.platformCustomStyles[routeId]; const customStyle = this.platformCustomStyles[routeId];
let style = { fontSize: 16, fontColor: '#000000' }; let style = { fontSize: 16, fontColor: '#000000' };
if (customStyle) { if (customStyle) {
style.fontSize = customStyle.labelFontSize || 16; style.fontSize = customStyle.labelFontSize || 16;
style.fontColor = customStyle.labelFontColor || '#000000'; style.fontColor = customStyle.labelFontColor || '#000000';
@ -5343,7 +5347,7 @@ export default {
let iconSize = 144 let iconSize = 144
let iconColor = '#ffffff' let iconColor = '#ffffff'
let platformName = '平台' // let platformName = '平台' //
// //
if (labelEntity && labelEntity.label && labelEntity.label.text) { if (labelEntity && labelEntity.label && labelEntity.label.text) {
const now = Cesium.JulianDate.now() const now = Cesium.JulianDate.now()
@ -5374,7 +5378,7 @@ export default {
this.editPlatformForm.iconSize = iconSize this.editPlatformForm.iconSize = iconSize
this.editPlatformForm.iconColor = iconColor this.editPlatformForm.iconColor = iconColor
this.editPlatformForm.platformName = platformName this.editPlatformForm.platformName = platformName
this.editPlatformForm.platformId = ed.platformId || 0 this.editPlatformForm.platformId = ed.platformId || 0
// 线 platformId platformName // 线 platformId platformName
if (routeId) { if (routeId) {
@ -5448,7 +5452,7 @@ export default {
platformEntity.billboard.height = size platformEntity.billboard.height = size
platformEntity.billboard.color = Cesium.Color.fromCssColorString(color) platformEntity.billboard.color = Cesium.Color.fromCssColorString(color)
} }
// //
this.$set(this.platformCustomStyles, routeId, { this.$set(this.platformCustomStyles, routeId, {
labelFontSize: fontSize, labelFontSize: fontSize,
@ -5575,14 +5579,14 @@ export default {
const idStr = entity.id || ''; const idStr = entity.id || '';
if (idStr.startsWith('route-platform-') && !idStr.startsWith('route-platform-label-')) { if (idStr.startsWith('route-platform-') && !idStr.startsWith('route-platform-label-')) {
const routeId = idStr.replace('route-platform-', ''); const routeId = idStr.replace('route-platform-', '');
// platformId // platformId
let platformId = 0; let platformId = 0;
if (entity.properties && entity.properties.platformId) { if (entity.properties && entity.properties.platformId) {
const now = Cesium.JulianDate.now(); const now = Cesium.JulianDate.now();
platformId = entity.properties.platformId.getValue ? entity.properties.platformId.getValue(now) : entity.properties.platformId; platformId = entity.properties.platformId.getValue ? entity.properties.platformId.getValue(now) : entity.properties.platformId;
} }
// platformId // platformId
if (Number(platformId) > 0) { if (Number(platformId) > 0) {
getPlatformStyle({ getPlatformStyle({
@ -5592,7 +5596,7 @@ export default {
}).then(res => { }).then(res => {
if (res.data) { if (res.data) {
const style = res.data; const style = res.data;
// //
this.$set(this.platformCustomStyles, routeId, { this.$set(this.platformCustomStyles, routeId, {
labelFontSize: style.labelFontSize, labelFontSize: style.labelFontSize,
@ -5600,15 +5604,15 @@ export default {
platformSize: style.platformSize, platformSize: style.platformSize,
platformColor: style.platformColor platformColor: style.platformColor
}); });
// //
const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`); const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`);
if (labelEntity) { if (labelEntity) {
// updatePlatformPosition // updatePlatformPosition
// updatePlatformPosition 使 // updatePlatformPosition 使
this.$set(this.routeLabelStyles, routeId, { this.$set(this.routeLabelStyles, routeId, {
fontSize: style.labelFontSize || 16, fontSize: style.labelFontSize || 16,
fontColor: style.labelFontColor || '#333333' fontColor: style.labelFontColor || '#333333'
}); });
// //
if (labelEntity.labelDataCache) { if (labelEntity.labelDataCache) {
@ -5617,7 +5621,7 @@ export default {
this.updatePlatformPosition(routeId, entity.position.getValue(Cesium.JulianDate.now()), null, labelEntity.labelDataCache); this.updatePlatformPosition(routeId, entity.position.getValue(Cesium.JulianDate.now()), null, labelEntity.labelDataCache);
} }
} }
// //
if (entity.billboard) { if (entity.billboard) {
const size = Math.max(48, Math.min(256, Number(style.platformSize) || 144)); const size = Math.max(48, Math.min(256, Number(style.platformSize) || 144));
@ -5626,7 +5630,7 @@ export default {
entity.billboard.height = size; entity.billboard.height = size;
entity.billboard.color = Cesium.Color.fromCssColorString(color); entity.billboard.color = Cesium.Color.fromCssColorString(color);
} }
if (this.viewer.scene.requestRenderMode) { if (this.viewer.scene.requestRenderMode) {
this.viewer.scene.requestRender(); this.viewer.scene.requestRender();
} }
@ -7112,6 +7116,11 @@ export default {
gap: 6px; gap: 6px;
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
transition: bottom 0.3s ease;
}
.map-info-panel.panel-raised {
bottom: 75px;
} }
/* 比例尺:高德风格,浅色底、圆角、刻度线 */ /* 比例尺:高德风格,浅色底、圆角、刻度线 */

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

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<bottom-timeline ref="timeline" @timeline-hidden="onTimelineHidden" /> <bottom-timeline ref="timeline" @timeline-hidden="onTimelineHidden" :room-id="roomId" />
<div class="bottom-left-panel" v-show="showPanel"> <div class="bottom-left-panel" v-show="showPanel">
<div class="panel-toggle" @click="togglePanel" :title="isExpanded ? '收起' : '展开'"> <div class="panel-toggle" @click="togglePanel" :title="isExpanded ? '收起' : '展开'">
@ -51,6 +51,12 @@ export default {
components: { components: {
BottomTimeline BottomTimeline
}, },
props: {
roomId: {
type: Number,
default: null
}
},
data() { data() {
return { return {
isExpanded: false, isExpanded: false,
@ -75,19 +81,23 @@ export default {
this.$refs.timeline.isVisible = true this.$refs.timeline.isVisible = true
this.isExpanded = false this.isExpanded = false
this.showPanel = false this.showPanel = false
this.$emit('bottom-panel-visible', true)
} }
}, },
onTimelineHidden() { onTimelineHidden() {
this.showPanel = true this.showPanel = true
this.$emit('bottom-panel-visible', false)
}, },
showSixSteps() { showSixSteps() {
this.showSixStepsBar = true this.showSixStepsBar = true
this.isExpanded = false this.isExpanded = false
this.showPanel = false this.showPanel = false
this.$emit('bottom-panel-visible', true)
}, },
hideSixStepsBar() { hideSixStepsBar() {
this.showSixStepsBar = false this.showSixStepsBar = false
this.showPanel = true this.showPanel = true
this.$emit('bottom-panel-visible', false)
}, },
selectStep(index) { selectStep(index) {
const clickedStep = this.sixStepsData[index] const clickedStep = this.sixStepsData[index]
@ -205,7 +215,7 @@ export default {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.98) 100%); background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.98) 100%);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.08); box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.08);
padding: 5px 60px 5px 60px; padding: 2px 60px 4px 60px;
z-index: 1000; z-index: 1000;
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1); animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid rgba(0, 0, 0, 0.05);
@ -249,7 +259,7 @@ export default {
.steps-bar-container { .steps-bar-container {
display: flex; display: flex;
gap: 12px; gap: 12px;
height: 40px; height: 62px;
align-items: center; align-items: center;
} }

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

@ -1,11 +1,18 @@
<template> <template>
<div class="bottom-timeline" v-if="isVisible"> <div class="bottom-timeline" v-if="isVisible">
<div class="timeline-header"> <div class="timeline-header">
<el-button type="text" size="mini" @click="openSettingsDialog" icon="el-icon-setting" title="时间轴设置"> <div class="timeline-title">
时间轴设置 <i class="el-icon-time"></i>
</el-button> <span>任务时间轴</span>
<el-button type="text" size="mini" @click="closeTimeline" icon="el-icon-close" title="关闭时间轴"> </div>
</el-button> <div class="header-actions">
<el-button type="text" size="mini" @click="openSettingsDialog" title="时间轴设置">
<i class="el-icon-setting"></i>
<span>设置</span>
</el-button>
<el-button type="text" size="mini" @click="closeTimeline" icon="el-icon-close" title="关闭时间轴">
</el-button>
</div>
</div> </div>
<div class="timeline-bar-container"> <div class="timeline-bar-container">
@ -22,6 +29,10 @@
:title="segment.time + ' - ' + segment.name" :title="segment.time + ' - ' + segment.name"
> >
<div class="marker-dot"></div> <div class="marker-dot"></div>
<div class="marker-tooltip">
<div class="tooltip-time">{{ segment.time }}</div>
<div class="tooltip-name">{{ segment.name }}</div>
</div>
</div> </div>
</div> </div>
<div class="timeline-current" :style="{ left: progressWidth + '%' }"> <div class="timeline-current" :style="{ left: progressWidth + '%' }">
@ -137,28 +148,6 @@
<el-form-item label="描述"> <el-form-item label="描述">
<el-input v-model="segmentForm.description" type="textarea" :rows="3" placeholder="输入描述" /> <el-input v-model="segmentForm.description" type="textarea" :rows="3" placeholder="输入描述" />
</el-form-item> </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> </el-form>
<span slot="footer"> <span slot="footer">
<el-button @click="showSegmentDialog = false">取消</el-button> <el-button @click="showSegmentDialog = false">取消</el-button>
@ -189,18 +178,6 @@
<span>{{ currentSegment.description }}</span> <span>{{ currentSegment.description }}</span>
</div> </div>
<div class="detail-row"> <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> <label>状态</label>
<el-tag :type="currentSegment.passed ? 'success' : (currentSegment.active ? 'warning' : 'info')" size="small"> <el-tag :type="currentSegment.passed ? 'success' : (currentSegment.active ? 'warning' : 'info')" size="small">
{{ currentSegment.passed ? '已到达' : (currentSegment.active ? '即将到达' : '未到达') }} {{ currentSegment.passed ? '已到达' : (currentSegment.active ? '即将到达' : '未到达') }}
@ -216,8 +193,16 @@
</template> </template>
<script> <script>
import { getTimelineSegmentsByRoomId, addTimelineSegment, updateTimelineSegment, delTimelineSegment } from '@/api/system/timeline'
export default { export default {
name: 'BottomTimeline', name: 'BottomTimeline',
props: {
roomId: {
type: Number,
default: null
}
},
data() { data() {
return { return {
isVisible: false, isVisible: false,
@ -236,34 +221,33 @@ export default {
segmentForm: { segmentForm: {
time: null, time: null,
name: '', name: '',
description: '', description: ''
reminderTypes: ['popup', 'message'],
assignedTask: null
}, },
timer: null, timer: null,
audio: null, audio: null,
currentTimeStr: '', currentTimeStr: '',
availableTasks: [ isLoading: false
{ id: 1, name: '任务准备' },
{ id: 2, name: '资源调配' },
{ id: 3, name: '任务执行' },
{ id: 4, name: '任务监控' },
{ id: 5, name: '任务完成' },
{ id: 6, name: '数据收集' },
{ id: 7, name: '分析评估' },
{ id: 8, name: '报告生成' }
]
} }
}, },
mounted() { mounted() {
this.initDefaultTime() this.initDefaultTime()
this.initDefaultSegments() this.loadTimelineSegments()
this.startTimer() this.startTimer()
this.initAudio() this.initAudio()
}, },
beforeDestroy() { beforeDestroy() {
this.stopTimer() this.stopTimer()
}, },
watch: {
roomId: {
handler(newVal) {
if (newVal) {
this.loadTimelineSegments()
}
},
immediate: true
}
},
methods: { methods: {
closeTimeline() { closeTimeline() {
this.isVisible = false this.isVisible = false
@ -279,9 +263,7 @@ export default {
this.segmentForm = { this.segmentForm = {
time: null, time: null,
name: '', name: '',
description: '', description: ''
reminderTypes: ['popup', 'message'],
assignedTask: null
} }
this.showSegmentDialog = true this.showSegmentDialog = true
}, },
@ -296,6 +278,38 @@ export default {
this.editSegment(this.currentSegment) this.editSegment(this.currentSegment)
}, },
async loadTimelineSegments() {
if (!this.roomId) {
this.initDefaultSegments()
return
}
try {
this.isLoading = true
const response = await getTimelineSegmentsByRoomId(this.roomId)
if (response.code === 200 && response.data && response.data.length > 0) {
this.timelineSegments = response.data.map(item => ({
segmentId: item.segmentId,
time: item.segmentTime,
name: item.segmentName,
description: item.segmentDesc,
active: false,
passed: false,
position: 0,
triggered: false
}))
this.updateTimeline()
} else {
this.initDefaultSegments()
}
} catch (error) {
console.error('加载时间轴数据失败:', error)
this.initDefaultSegments()
} finally {
this.isLoading = false
}
},
initDefaultTime() { initDefaultTime() {
const now = new Date() const now = new Date()
this.startTime = new Date(now) this.startTime = new Date(now)
@ -312,8 +326,6 @@ export default {
description: '开始准备任务所需的资源和设备', description: '开始准备任务所需的资源和设备',
active: false, active: false,
passed: false, passed: false,
reminderTypes: ['popup', 'message'],
assignedTask: 1,
position: 0, position: 0,
triggered: false triggered: false
}, },
@ -323,8 +335,6 @@ export default {
description: '完成资源的调配和分配', description: '完成资源的调配和分配',
active: false, active: false,
passed: false, passed: false,
reminderTypes: ['popup', 'message'],
assignedTask: 2,
position: 0, position: 0,
triggered: false triggered: false
}, },
@ -334,8 +344,6 @@ export default {
description: '开始执行主要任务', description: '开始执行主要任务',
active: false, active: false,
passed: false, passed: false,
reminderTypes: ['popup', 'sound', 'message'],
assignedTask: 3,
position: 0, position: 0,
triggered: false triggered: false
}, },
@ -345,8 +353,6 @@ export default {
description: '监控任务执行进度', description: '监控任务执行进度',
active: false, active: false,
passed: false, passed: false,
reminderTypes: ['popup', 'message'],
assignedTask: 4,
position: 0, position: 0,
triggered: false triggered: false
}, },
@ -356,8 +362,6 @@ export default {
description: '任务完成,进行总结', description: '任务完成,进行总结',
active: false, active: false,
passed: false, passed: false,
reminderTypes: ['popup', 'sound', 'message'],
assignedTask: 5,
position: 0, position: 0,
triggered: false triggered: false
} }
@ -426,26 +430,20 @@ export default {
}, },
triggerReminder(segment) { triggerReminder(segment) {
if (segment.reminderTypes.includes('popup')) { this.$notify({
this.$notify({ title: '时间提醒',
title: '时间提醒', message: `${segment.time} - ${segment.name}`,
message: `${segment.time} - ${segment.name}`, type: 'warning',
type: 'warning', duration: 5000
duration: 5000 })
})
}
if (segment.reminderTypes.includes('sound')) { this.playSound()
this.playSound()
}
if (segment.reminderTypes.includes('message')) { this.$message({
this.$message({ message: `时间提醒:${segment.time} - ${segment.name}`,
message: `时间提醒:${segment.time} - ${segment.name}`, type: 'warning',
type: 'warning', duration: 5000
duration: 5000 })
})
}
}, },
playSound() { playSound() {
@ -492,63 +490,84 @@ export default {
this.segmentForm = { this.segmentForm = {
time: this.parseTime(segment.time), time: this.parseTime(segment.time),
name: segment.name, name: segment.name,
description: segment.description, description: segment.description
reminderTypes: [...segment.reminderTypes],
assignedTask: segment.assignedTask
} }
this.showSegmentDialog = true this.showSegmentDialog = true
}, },
saveSegment() { async saveSegment() {
if (!this.segmentForm.time || !this.segmentForm.name) { if (!this.segmentForm.time || !this.segmentForm.name) {
this.$message.warning('请填写完整信息') this.$message.warning('请填写完整信息')
return return
} }
const segment = { const segmentData = {
time: this.formatTime(this.segmentForm.time), roomId: this.roomId,
name: this.segmentForm.name, segmentTime: this.formatTime(this.segmentForm.time),
description: this.segmentForm.description, segmentName: this.segmentForm.name,
reminderTypes: [...this.segmentForm.reminderTypes], segmentDesc: this.segmentForm.description
assignedTask: this.segmentForm.assignedTask,
active: false,
passed: false,
triggered: false,
position: 0
} }
if (this.isEditMode) { try {
this.timelineSegments[this.editIndex] = segment if (this.isEditMode && this.timelineSegments[this.editIndex].segmentId) {
} else { segmentData.segmentId = this.timelineSegments[this.editIndex].segmentId
this.timelineSegments.push(segment) await updateTimelineSegment(segmentData)
} } else {
const response = await addTimelineSegment(segmentData)
segmentData.segmentId = response.data
}
this.timelineSegments.sort((a, b) => { const segment = {
return this.timeToSeconds(a.time) - this.timeToSeconds(b.time) segmentId: segmentData.segmentId,
}) time: segmentData.segmentTime,
name: segmentData.segmentName,
description: segmentData.segmentDesc,
active: false,
passed: false,
triggered: false,
position: 0
}
this.updateTimeline() if (this.isEditMode) {
this.showSegmentDialog = false this.timelineSegments[this.editIndex] = segment
this.$message.success(this.isEditMode ? '编辑成功' : '添加成功') } else {
this.timelineSegments.push(segment)
}
this.timelineSegments.sort((a, b) => {
return this.timeToSeconds(a.time) - this.timeToSeconds(b.time)
})
this.updateTimeline()
this.showSegmentDialog = false
this.$message.success(this.isEditMode ? '编辑成功' : '添加成功')
} catch (error) {
console.error('保存时间段失败:', error)
this.$message.error('保存失败,请重试')
}
}, },
deleteSegment(index) { async deleteSegment(index) {
this.$confirm('确定要删除这个时间段吗?', '提示', { this.$confirm('确定要删除这个时间段吗?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(async () => {
this.timelineSegments.splice(index, 1) try {
this.updateTimeline() const segment = this.timelineSegments[index]
this.$message.success('删除成功') if (segment.segmentId && this.roomId) {
await delTimelineSegment(segment.segmentId)
}
this.timelineSegments.splice(index, 1)
this.updateTimeline()
this.$message.success('删除成功')
} catch (error) {
console.error('删除时间段失败:', error)
this.$message.error('删除失败,请重试')
}
}).catch(() => {}) }).catch(() => {})
}, },
getTaskName(taskId) {
const task = this.availableTasks.find(t => t.id === taskId)
return task ? task.name : '未分配'
},
formatTime(date) { formatTime(date) {
const hours = date.getHours().toString().padStart(2, '0') const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0') const minutes = date.getMinutes().toString().padStart(2, '0')
@ -582,49 +601,89 @@ export default {
left: 0; left: 0;
right: 0; right: 0;
z-index: 1000; z-index: 1000;
background: rgba(255, 255, 255, 0.95); background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
backdrop-filter: blur(10px); backdrop-filter: blur(12px);
border-top: 1px solid #e4e7ed; border-top: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
padding: 4px 12px 6px 12px; padding: 2px 20px 4px;
border-radius: 12px 12px 0 0;
overflow: visible;
} }
.timeline-header { .timeline-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 16px; min-height: 24px;
margin-bottom: 3px; margin-bottom: 2px;
}
.timeline-title {
font-size: 15px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
gap: 8px;
}
.timeline-title i {
color: #008aff;
font-size: 16px;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.header-actions .el-button {
color: #008aff;
transition: all 0.3s ease;
font-size: 15px;
padding: 4px 8px;
}
.header-actions .el-button:hover {
color: #0078e5;
transform: translateY(-1px);
} }
.timeline-bar { .timeline-bar {
position: relative; position: relative;
height: 8px; height: 14px;
background: #f5f7fa; background: rgba(255, 255, 255, 0.6);
border-radius: 4px; border-radius: 8px;
overflow: hidden; overflow: visible;
border: 1px solid #e4e7ed; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
} }
.timeline-bar-container { .timeline-bar-container {
position: relative; position: relative;
margin-bottom: 0; margin-bottom: 2px;
overflow: visible;
} }
.timeline-scale { .timeline-scale {
position: relative; position: relative;
height: 14px; height: 20px;
margin-top: 3px; margin-top: 2px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.scale-label { .scale-label {
position: absolute; position: absolute;
font-size: 12px; font-size: 13px;
color: #909399; color: #64748b;
transform: translateX(-50%); transform: translateX(-50%);
white-space: nowrap; white-space: nowrap;
font-weight: 500;
padding: 3px 8px;
background: rgba(255, 255, 255, 0.9);
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
} }
.timeline-progress { .timeline-progress {
@ -632,9 +691,10 @@ export default {
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
background: linear-gradient(90deg, #409EFF 0%, #67c23a 100%); background: linear-gradient(90deg, #008aff 0%, #66b8ff 100%);
transition: width 0.5s ease; transition: width 0.5s ease-in-out;
border-radius: 6px; border-radius: 8px;
box-shadow: 0 0 8px rgba(0, 138, 255, 0.3);
} }
.timeline-markers { .timeline-markers {
@ -647,102 +707,132 @@ export default {
.timeline-marker { .timeline-marker {
position: absolute; position: absolute;
top: 0; top: 50%;
transform: translateX(-50%); transform: translate(-50%, -50%);
cursor: pointer; cursor: pointer;
z-index: 10; z-index: 10;
height: 100%; transition: all 0.3s ease;
display: flex; }
align-items: center;
.timeline-marker:hover {
transform: translate(-50%, -50%) translateY(-3px);
} }
.timeline-marker:hover .marker-dot { .timeline-marker:hover .marker-dot {
transform: scale(1.3); transform: scale(1.5);
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3); box-shadow: 0 0 0 6px rgba(0, 138, 255, 0.2);
}
.timeline-marker:hover .marker-tooltip {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-12px);
} }
.timeline-marker.active .marker-dot { .timeline-marker.active .marker-dot {
background: #E6A23C; background: #f59e0b;
box-shadow: 0 0 0 3px rgba(230, 162, 60, 0.4); box-shadow: 0 0 0 6px rgba(245, 158, 11, 0.3);
animation: pulse 1.5s infinite; animation: pulse 2s infinite ease-in-out;
} }
.timeline-marker.passed .marker-dot { .timeline-marker.passed .marker-dot {
background: #67c23a; background: #10b981;
box-shadow: 0 0 0 2px rgba(103, 194, 58, 0.3); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
} }
.marker-dot { .marker-dot {
width: 6px; width: 12px;
height: 6px; height: 12px;
background: #409EFF; background: #008aff;
border-radius: 50%; border-radius: 50%;
border: 1px solid white; border: 2px solid white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1;
}
.marker-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(0);
background: white;
border-radius: 8px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
padding: 8px 12px;
min-width: 140px;
opacity: 0;
visibility: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 20;
border: 1px solid #e2e8f0;
}
.tooltip-time {
font-size: 16px;
color: #008aff;
font-weight: 600;
margin-bottom: 4px;
}
.tooltip-name {
font-size: 15px;
color: #1e293b;
font-weight: 500;
} }
.timeline-current { .timeline-current {
position: absolute; position: absolute;
top: 0; top: 0;
width: 2px; width: 4px;
height: 100%; height: 100%;
background: #F56C6C; background: #ef4444;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 20; z-index: 20;
transition: left 0.5s ease; transition: left 0.5s ease-in-out;
border-radius: 4px;
} }
.current-indicator { .current-indicator {
position: absolute; position: absolute;
top: 0; top: 50%;
left: -4px; left: -6px;
width: 8px; width: 14px;
height: 8px; height: 14px;
background: #F56C6C; background: #ef4444;
border-radius: 50%; border-radius: 50%;
border: 1px solid white; border: 2px solid white;
box-shadow: 0 1px 3px rgba(245, 108, 108, 0.4); box-shadow: 0 0 12px rgba(239, 68, 68, 0.5);
animation: currentPulse 2s infinite; transform: translateY(-50%);
animation: currentPulse 1.5s infinite ease-in-out;
} }
.current-time { .current-time {
position: absolute; position: absolute;
top: -14px; top: -26px;
left: 0; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
font-size: 12px; font-size: 13px;
color: #F56C6C; color: #ef4444;
white-space: nowrap; white-space: nowrap;
font-weight: 500; font-weight: 600;
z-index: 25; z-index: 25;
padding: 4px 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.2);
} }
@keyframes pulse { @keyframes pulse {
0% { 0% { transform: scale(1); opacity: 1; }
transform: scale(1); 50% { transform: scale(1.4); opacity: 0.8; }
opacity: 1; 100% { transform: scale(1); opacity: 1; }
}
50% {
transform: scale(1.2);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
} }
@keyframes currentPulse { @keyframes currentPulse {
0% { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
box-shadow: 0 1px 3px rgba(245, 108, 108, 0.4); 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
} 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
50% {
box-shadow: 0 1px 6px rgba(245, 108, 108, 0.6);
}
100% {
box-shadow: 0 1px 3px rgba(245, 108, 108, 0.4);
}
} }
.settings-content { .settings-content {
@ -752,8 +842,9 @@ export default {
.time-settings { .time-settings {
margin-bottom: 20px; margin-bottom: 20px;
padding: 15px; padding: 15px;
background: #f5f7fa; background: #f8fafc;
border-radius: 6px; border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
} }
.setting-row { .setting-row {
@ -771,7 +862,7 @@ export default {
.setting-label { .setting-label {
font-size: 13px; font-size: 13px;
color: #606266; color: #334155;
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
min-width: 60px; min-width: 60px;
@ -787,13 +878,13 @@ export default {
align-items: center; align-items: center;
margin-bottom: 15px; margin-bottom: 15px;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid #dcdfe6; border-bottom: 1px solid #e2e8f0;
} }
.section-header h4 { .section-header h4 {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
color: #303133; color: #1e293b;
font-weight: 600; font-weight: 600;
} }
@ -805,31 +896,32 @@ export default {
.segment-item { .segment-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px; padding: 12px 16px;
background: white; background: white;
border: 1px solid #dcdfe6; border: 1px solid #e2e8f0;
border-radius: 6px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 8px;
transition: all 0.3s; transition: all 0.3s ease;
} }
.segment-item:hover { .segment-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.08);
transform: translateY(-1px);
} }
.segment-item.active { .segment-item.active {
background: linear-gradient(135deg, rgba(230, 162, 60, 0.05) 0%, rgba(230, 162, 60, 0.1) 100%); background: linear-gradient(135deg, rgba(245,158,11,0.05) 0%, rgba(245,158,11,0.1) 100%);
border-color: #E6A23C; border-color: #f59e0b;
} }
.segment-item.passed { .segment-item.passed {
background: linear-gradient(135deg, rgba(103, 194, 58, 0.05) 0%, rgba(103, 194, 58, 0.1) 100%); background: linear-gradient(135deg, rgba(16,185,129,0.05) 0%, rgba(16,185,129,0.1) 100%);
border-color: #67c23a; border-color: #10b981;
} }
.segment-time { .segment-time {
font-size: 13px; font-size: 13px;
color: #409EFF; color: #008aff;
font-weight: 700; font-weight: 700;
min-width: 80px; min-width: 80px;
} }
@ -842,13 +934,13 @@ export default {
.segment-name { .segment-name {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #303133; color: #1e293b;
margin-bottom: 3px; margin-bottom: 3px;
} }
.segment-desc { .segment-desc {
font-size: 12px; font-size: 12px;
color: #909399; color: #64748b;
} }
.segment-status { .segment-status {
@ -872,13 +964,13 @@ export default {
.detail-row label { .detail-row label {
font-weight: 600; font-weight: 600;
color: #303133; color: #1e293b;
min-width: 80px; min-width: 80px;
flex-shrink: 0; flex-shrink: 0;
} }
.detail-row span { .detail-row span {
color: #606266; color: #334155;
line-height: 1.5; line-height: 1.5;
} }
@ -896,35 +988,49 @@ export default {
left: 50% !important; left: 50% !important;
transform: translate(-50%, -50%) !important; transform: translate(-50%, -50%) !important;
margin: 0 !important; margin: 0 !important;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
border: none;
} }
.bottom-timeline .el-dialog__header { .bottom-timeline .el-dialog__header {
background: #409EFF; background: linear-gradient(90deg, #008aff 0%, #66b8ff 100%);
color: white; color: white;
padding: 12px 20px;
} }
.bottom-timeline .el-dialog__title { .bottom-timeline .el-dialog__title {
color: white; color: white;
font-size: 14px; font-size: 15px;
font-weight: 600; font-weight: 600;
} }
.bottom-timeline .el-dialog__headerbtn .el-dialog__close { .bottom-timeline .el-dialog__headerbtn .el-dialog__close {
color: white; color: white;
font-size: 16px;
transition: all 0.3s ease;
}
.bottom-timeline .el-dialog__headerbtn .el-dialog__close:hover {
color: #e0e7ff;
transform: rotate(90deg);
} }
.bottom-timeline .el-dialog__body { .bottom-timeline .el-dialog__body {
padding: 20px; padding: 24px;
background: #f8fafc;
} }
.bottom-timeline .el-dialog__footer { .bottom-timeline .el-dialog__footer {
padding: 15px 20px; padding: 16px 24px;
border-top: 1px solid #dcdfe6; border-top: 1px solid #e2e8f0;
background: white;
} }
.timeline-settings-dialog.el-dialog { .timeline-settings-dialog.el-dialog {
position: fixed !important; position: fixed !important;
bottom: 60px !important; bottom: 130px !important;
top: auto !important; top: auto !important;
left: 50% !important; left: 50% !important;
transform: translateX(-50%) !important; transform: translateX(-50%) !important;
@ -934,7 +1040,7 @@ export default {
.timeline-segment-dialog.el-dialog { .timeline-segment-dialog.el-dialog {
position: fixed !important; position: fixed !important;
bottom: 60px !important; bottom: 130px !important;
top: auto !important; top: auto !important;
left: 50% !important; left: 50% !important;
transform: translateX(-50%) !important; transform: translateX(-50%) !important;
@ -944,7 +1050,7 @@ export default {
.timeline-detail-dialog.el-dialog { .timeline-detail-dialog.el-dialog {
position: fixed !important; position: fixed !important;
bottom: 60px !important; bottom: 130px !important;
top: auto !important; top: auto !important;
left: 50% !important; left: 50% !important;
transform: translateX(-50%) !important; transform: translateX(-50%) !important;

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="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="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="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-menu>
</el-dropdown> </el-dropdown>
@ -574,6 +575,10 @@ export default {
this.$emit('toggle-route', this.isRouteVisible) this.$emit('toggle-route', this.isRouteVisible)
}, },
generateGanttChart() {
this.$emit('generate-gantt-chart')
},
systemDescription() { systemDescription() {
this.$emit('system-description') this.$emit('system-description')
}, },

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

@ -13,6 +13,7 @@
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')" :tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
:scaleConfig="scaleConfig" :scaleConfig="scaleConfig"
:coordinateFormat="coordinateFormat" :coordinateFormat="coordinateFormat"
:bottomPanelVisible="bottomPanelVisible"
:route-locked="routeLocked" :route-locked="routeLocked"
@draw-complete="handleMapDrawComplete" @draw-complete="handleMapDrawComplete"
@route-lock-changed="handleRouteLockChanged" @route-lock-changed="handleRouteLockChanged"
@ -141,6 +142,7 @@
@toggle-airport="toggleAirport" @toggle-airport="toggleAirport"
@toggle-landmark="toggleLandmark" @toggle-landmark="toggleLandmark"
@toggle-route="toggleRoute" @toggle-route="toggleRoute"
@generate-gantt-chart="generateGanttChart"
@system-description="systemDescription" @system-description="systemDescription"
@layer-favorites="layerFavorites" @layer-favorites="layerFavorites"
@route-favorites="routeFavorites" @route-favorites="routeFavorites"
@ -207,7 +209,7 @@
@open-import-dialog="showImportDialog = true" @open-import-dialog="showImportDialog = true"
/> />
<!-- 左下角工具面板 --> <!-- 左下角工具面板 -->
<bottom-left-panel v-show="!screenshotMode" /> <bottom-left-panel v-show="!screenshotMode" @bottom-panel-visible="handleBottomPanelVisible" :room-id="currentRoomId" />
<!-- 底部时间轴最初版本的样式- 蓝色主题 --> <!-- 底部时间轴最初版本的样式- 蓝色主题 -->
<div <div
v-show="!screenshotMode" v-show="!screenshotMode"
@ -483,6 +485,8 @@ export default {
platformIconSaveTimer: null, platformIconSaveTimer: null,
// //
showImportDialog: false, showImportDialog: false,
// /
bottomPanelVisible: false,
// //
screenshotMode: false, screenshotMode: false,
@ -707,6 +711,9 @@ export default {
if (this.currentRoomId) this.getRoomDetail(); if (this.currentRoomId) this.getRoomDetail();
}, },
methods: { methods: {
handleBottomPanelVisible(visible) {
this.bottomPanelVisible = visible
},
// //
async handleOpenWaypointEdit(wpId, routeId) { async handleOpenWaypointEdit(wpId, routeId) {
console.log(`>>> [父组件接收] 航点 ID: ${wpId}, 所属航线 ID: ${routeId}`); console.log(`>>> [父组件接收] 航点 ID: ${wpId}, 所属航线 ID: ${routeId}`);
@ -2055,6 +2062,11 @@ export default {
this.$message.success(this.showRoute ? '显示航线' : '隐藏航线'); this.$message.success(this.showRoute ? '显示航线' : '隐藏航线');
}, },
generateGanttChart() {
const url = this.$router.resolve('/ganttChart').href
window.open(url, '_blank')
},
systemDescription() { systemDescription() {
this.$message.success('系统说明'); 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>

22
sql/sys_timeline_segment.sql

@ -0,0 +1,22 @@
-- ----------------------------
-- 时间轴信息表
-- ----------------------------
drop table if exists sys_timeline_segment;
create table sys_timeline_segment (
segment_id bigint(20) not null auto_increment comment '时间段ID',
room_id bigint(20) not null comment '房间ID',
segment_time varchar(8) not null comment '时间点(HH:mm:ss)',
segment_name varchar(50) not null comment '时间段名称',
segment_desc varchar(500) default '' comment '时间段描述',
primary key (segment_id),
key idx_room_id (room_id)
) engine=innodb auto_increment=100 comment = '时间轴信息表';
-- ----------------------------
-- 初始化时间轴信息表数据(示例数据)
-- ----------------------------
insert into sys_timeline_segment values(1, 1, '08:00:00', '任务准备', '开始准备任务所需的资源和设备');
insert into sys_timeline_segment values(2, 1, '10:00:00', '资源调配', '完成资源的调配和分配');
insert into sys_timeline_segment values(3, 1, '12:00:00', '任务执行', '开始执行主要任务');
insert into sys_timeline_segment values(4, 1, '14:00:00', '任务监控', '监控任务执行进度');
insert into sys_timeline_segment values(5, 1, '16:00:00', '任务完成', '任务完成,进行总结');
Loading…
Cancel
Save