Compare commits

...

5 Commits
master ... lbj

  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. 6
      ruoyi-ui/babel.config.js
  8. 6
      ruoyi-ui/package.json
  9. 691
      ruoyi-ui/public/dataCard.html
  10. 1
      ruoyi-ui/public/element-ui.css
  11. BIN
      ruoyi-ui/public/fonts/element-icons.woff
  12. BIN
      ruoyi-ui/public/logo.png
  13. BIN
      ruoyi-ui/public/logo2.jpg
  14. 252
      ruoyi-ui/public/stepEditor.html
  15. 53
      ruoyi-ui/src/api/system/timeline.js
  16. 1
      ruoyi-ui/src/assets/icons/svg/shujukapian.svg
  17. 7
      ruoyi-ui/src/lang/en.js
  18. 7
      ruoyi-ui/src/lang/zh.js
  19. 5
      ruoyi-ui/src/router/index.js
  20. 45
      ruoyi-ui/src/views/cesiumMap/index.vue
  21. 20
      ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue
  22. 550
      ruoyi-ui/src/views/childRoom/BottomTimeline.vue
  23. 5
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  24. 18
      ruoyi-ui/src/views/childRoom/index.vue
  25. 1005
      ruoyi-ui/src/views/ganttChart/index.vue
  26. 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>

6
ruoyi-ui/babel.config.js

@ -1,12 +1,12 @@
module.exports = {
presets: [
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
'@vue/cli-plugin-babel/preset'
],
plugins: [
'@babel/plugin-proposal-object-rest-spread'
],
'env': {
'development': {
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
'plugins': ['dynamic-import-node']
}
}

6
ruoyi-ui/package.json

@ -35,9 +35,11 @@
"file-saver": "2.0.5",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
"html2canvas": "^1.4.1",
"js-beautify": "1.13.0",
"js-cookie": "3.0.1",
"jsencrypt": "3.0.0-rc.1",
"jspdf": "^2.5.1",
"mammoth": "^1.11.0",
"nprogress": "0.2.0",
"quill": "2.0.2",
@ -50,9 +52,11 @@
"vue-i18n": "^8.28.2",
"vue-router": "3.4.9",
"vuedraggable": "2.24.3",
"vuex": "3.6.0"
"vuex": "3.6.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
"@babel/plugin-transform-optional-chaining": "^7.28.6",
"@open-wc/webpack-import-meta-loader": "^0.4.7",

691
ruoyi-ui/public/dataCard.html

@ -0,0 +1,691 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据卡</title>
<link rel="stylesheet" href="./element-ui.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #fafafa;
}
#app {
width: 100%;
height: 100vh;
}
.data-card-container {
display: flex;
flex-direction: column;
height: 100vh;
background: #fafafa;
}
.data-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 32px;
height: 56px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.back-btn {
padding: 8px 12px;
font-size: 14px;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.back-btn:hover {
color: #409EFF;
}
.data-card-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.data-card-toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 32px;
background: #fff;
border-bottom: 1px solid #e8e8e8;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-label {
font-size: 14px;
color: #666;
}
.data-card-content {
flex: 1;
padding: 24px 32px;
overflow: auto;
}
.table-container {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 24px;
display: inline-block;
}
.data-table {
border-collapse: collapse;
table-layout: fixed;
}
.data-table td {
border: 1px solid #ddd;
min-width: 60px;
height: 30px;
position: relative;
cursor: cell;
transition: background-color 0.2s;
}
.data-table td:hover {
background-color: #f5f7fa;
}
.data-table td.selected {
background-color: #ecf5ff;
border-color: #409EFF;
}
.resize-handle-col {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
background: transparent;
z-index: 10;
}
.resize-handle-col:hover {
background: #409EFF;
}
.resize-handle-row {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
cursor: row-resize;
background: transparent;
z-index: 10;
}
.resize-handle-row:hover {
background: #409EFF;
}
.cell-input {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 4px 8px;
font-size: 14px;
background: transparent;
}
.color-picker-panel {
display: flex;
flex-wrap: wrap;
gap: 4px;
width: 180px;
}
.color-option {
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
}
.color-option:hover {
border-color: #409EFF;
}
.color-option.selected {
border-color: #409EFF;
}
</style>
</head>
<body>
<div id="app">
<div class="data-card-container">
<div class="data-card-header">
<div class="header-left">
<div class="back-btn" @click="goBack">
<i class="el-icon-arrow-left"></i>
返回
</div>
<div class="data-card-title">数据卡</div>
</div>
<div class="header-right">
<el-button type="primary" size="small" @click="exportImage">
<i class="el-icon-picture-outline"></i>
导出图片
</el-button>
</div>
</div>
<div class="data-card-toolbar">
<div class="toolbar-group">
<span class="toolbar-label">行数:</span>
<el-input-number v-model="rows" :min="1" :max="50" size="small" @change="updateTableSize"></el-input-number>
</div>
<div class="toolbar-group">
<span class="toolbar-label">列数:</span>
<el-input-number v-model="cols" :min="1" :max="20" size="small" @change="updateTableSize"></el-input-number>
</div>
<el-divider direction="vertical"></el-divider>
<div class="toolbar-group">
<span class="toolbar-label">单元格背景色:</span>
<el-popover
placement="bottom"
width="200"
trigger="click"
v-model="colorPopoverVisible">
<div class="color-picker-panel">
<div
v-for="color in colors"
:key="color"
class="color-option"
:style="{ backgroundColor: color }"
:class="{ selected: selectedColor === color }"
@click="selectColor(color)">
</div>
</div>
<el-button slot="reference" size="small">
<span :style="{ display: 'inline-block', width: '16px', height: '16px', backgroundColor: selectedColor, marginRight: '8px', verticalAlign: 'middle', borderRadius: '2px' }"></span>
选择颜色
</el-button>
</el-popover>
</div>
<el-divider direction="vertical"></el-divider>
<div class="toolbar-group">
<el-button size="small" @click="undo">撤回</el-button>
<el-button size="small" @click="mergeCells">合并单元格</el-button>
<el-button type="primary" size="small" @click="saveTable">保存</el-button>
<el-button type="danger" size="small" @click="clearCell">清空选中</el-button>
</div>
</div>
<div class="data-card-content">
<div class="table-container" ref="tableContainer">
<table class="data-table" ref="dataTable">
<tbody>
<tr v-for="(row, rowIndex) in tableData" :key="rowIndex">
<td
v-for="(cell, colIndex) in row"
:key="colIndex"
:class="{ selected: selectedCells.some(c => c.row === rowIndex && c.col === colIndex) }"
:rowspan="getRowspan(rowIndex, colIndex)"
:colspan="getColspan(rowIndex, colIndex)"
:style="getCellStyle(rowIndex, colIndex)"
@click="editCell(rowIndex, colIndex)"
@mousedown="startSelect($event, rowIndex, colIndex)"
@mouseover="continueSelect($event, rowIndex, colIndex)"
@mouseup="endSelect">
<div class="resize-handle-col" @mousedown.stop="startResizeCol($event, colIndex)"></div>
<div class="resize-handle-row" @mousedown.stop="startResizeRow($event, rowIndex)"></div>
<span v-if="!editingCell || editingCell.row !== rowIndex || editingCell.col !== colIndex">{{ cell.content }}</span>
<input
v-else
class="cell-input"
v-model="tableData[rowIndex][colIndex].content"
@blur="finishEditing"
@keyup.enter="finishEditing"
ref="cellInput"
autofocus>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@2.6.12/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
rows: 10,
cols: 8,
tableData: [],
cellWidths: [],
cellHeights: [],
selectedCells: [],
isSelecting: false,
startCell: null,
editingCell: null,
selectedColor: '#ffffff',
colorPopoverVisible: false,
colors: [
'#ffffff', '#f5f7fa', '#e6a23c', '#67c23a', '#409eff',
'#909399', '#f0f9eb', '#fdf6ec', '#ecf5ff', '#f4f4f5',
'#ff6b6b', '#51cf66', '#339af0', '#cc5de8', '#ff922b',
'#ffd43b', '#20c997', '#22b8cf', '#748ffc', '#be4bdb'
],
resizingCol: null,
resizingRow: null,
startX: 0,
startY: 0,
startWidth: 0,
startHeight: 0,
mergedCells: [],
history: [],
historyIndex: -1
};
},
mounted() {
this.loadTable();
if (this.tableData.length === 0) {
this.generateTable();
// 初始化历史记录
this.saveState();
}
document.addEventListener('mouseup', this.endSelect);
document.addEventListener('mousemove', this.handleResize);
document.addEventListener('mouseup', this.endResize);
},
beforeDestroy() {
document.removeEventListener('mouseup', this.endSelect);
document.removeEventListener('mousemove', this.handleResize);
document.removeEventListener('mouseup', this.endResize);
},
methods: {
goBack() {
window.close();
},
generateTable() {
this.tableData = [];
this.cellWidths = [];
this.cellHeights = [];
this.mergedCells = [];
for (let i = 0; i < this.rows; i++) {
const row = [];
for (let j = 0; j < this.cols; j++) {
row.push({ content: '', bgColor: '#ffffff' });
}
this.tableData.push(row);
this.cellHeights.push(30);
}
for (let j = 0; j < this.cols; j++) {
this.cellWidths.push(100);
}
this.selectedCells = [];
// 重置历史记录
this.history = [];
this.historyIndex = -1;
},
updateTableSize() {
const oldRows = this.tableData.length;
const oldCols = oldRows > 0 ? this.tableData[0].length : 0;
if (this.rows !== oldRows || this.cols !== oldCols) {
// 保存当前状态
this.saveState();
// 处理行的增减
if (this.rows > oldRows) {
// 增加行
for (let i = oldRows; i < this.rows; i++) {
const row = [];
for (let j = 0; j < this.cols; j++) {
row.push({ content: '', bgColor: '#ffffff' });
}
this.tableData.push(row);
this.cellHeights.push(30);
}
} else if (this.rows < oldRows) {
// 减少行
this.tableData.splice(this.rows);
this.cellHeights.splice(this.rows);
}
// 处理列的增减
if (this.cols > oldCols) {
// 增加列
for (let i = 0; i < this.tableData.length; i++) {
for (let j = oldCols; j < this.cols; j++) {
this.tableData[i].push({ content: '', bgColor: '#ffffff' });
}
}
for (let j = oldCols; j < this.cols; j++) {
this.cellWidths.push(100);
}
} else if (this.cols < oldCols) {
// 减少列
for (let i = 0; i < this.tableData.length; i++) {
this.tableData[i].splice(this.cols);
}
this.cellWidths.splice(this.cols);
}
}
},
mergeCells() {
if (this.selectedCells.length < 2) {
this.$message.warning('请至少选择两个单元格进行合并');
return;
}
// 保存当前状态
this.saveState();
// 计算合并区域的最小和最大行、列
const minRow = Math.min(...this.selectedCells.map(c => c.row));
const maxRow = Math.max(...this.selectedCells.map(c => c.row));
const minCol = Math.min(...this.selectedCells.map(c => c.col));
const maxCol = Math.max(...this.selectedCells.map(c => c.col));
// 获取左上角单元格的内容和背景色
const firstCell = this.tableData[minRow][minCol];
const content = firstCell.content;
const bgColor = firstCell.bgColor;
// 清除其他单元格的内容和背景色
for (let r = minRow; r <= maxRow; r++) {
for (let c = minCol; c <= maxCol; c++) {
if (r !== minRow || c !== minCol) {
this.tableData[r][c].content = '';
this.tableData[r][c].bgColor = '#ffffff';
}
}
}
// 记录合并信息
this.mergedCells.push({
startRow: minRow,
startCol: minCol,
endRow: maxRow,
endCol: maxCol
});
this.$message.success('单元格合并成功');
},
saveTable() {
const tableData = {
rows: this.rows,
cols: this.cols,
tableData: this.tableData,
cellWidths: this.cellWidths,
cellHeights: this.cellHeights,
mergedCells: this.mergedCells
};
localStorage.setItem('dataCardTable', JSON.stringify(tableData));
this.$message.success('表格保存成功!');
},
loadTable() {
const savedData = localStorage.getItem('dataCardTable');
if (savedData) {
try {
const data = JSON.parse(savedData);
this.rows = data.rows;
this.cols = data.cols;
this.tableData = data.tableData;
this.cellWidths = data.cellWidths;
this.cellHeights = data.cellHeights;
this.mergedCells = data.mergedCells || [];
this.$message.success('表格加载成功!');
// 初始化历史记录
this.saveState();
} catch (e) {
console.error('加载表格失败:', e);
}
}
},
saveState() {
// 保存当前状态到历史记录
const state = {
rows: this.rows,
cols: this.cols,
tableData: JSON.parse(JSON.stringify(this.tableData)),
cellWidths: [...this.cellWidths],
cellHeights: [...this.cellHeights],
mergedCells: JSON.parse(JSON.stringify(this.mergedCells))
};
// 如果当前不是在历史记录的最后,删除后面的历史记录
if (this.historyIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.historyIndex + 1);
}
// 添加新状态到历史记录
this.history.push(state);
// 限制历史记录长度,防止内存占用过大
if (this.history.length > 50) {
this.history.shift();
// 调整历史记录索引
if (this.historyIndex > 0) {
this.historyIndex--;
}
} else {
this.historyIndex++;
}
},
undo() {
if (this.historyIndex > 0) {
this.historyIndex--;
const state = this.history[this.historyIndex];
this.rows = state.rows;
this.cols = state.cols;
this.tableData = state.tableData;
this.cellWidths = state.cellWidths;
this.cellHeights = state.cellHeights;
this.mergedCells = state.mergedCells;
this.$message.success('操作已撤回');
} else {
this.$message.info('没有可撤回的操作');
}
},
startSelect(event, row, col) {
if (event.button !== 0) return;
// 只有在按下鼠标并移动时才开始选择
this.isSelecting = true;
this.startCell = { row, col };
this.selectedCells = [{ row, col }];
},
continueSelect(event, row, col) {
if (!this.isSelecting || !this.startCell) return;
const minRow = Math.min(this.startCell.row, row);
const maxRow = Math.max(this.startCell.row, row);
const minCol = Math.min(this.startCell.col, col);
const maxCol = Math.max(this.startCell.col, col);
this.selectedCells = [];
for (let r = minRow; r <= maxRow; r++) {
for (let c = minCol; c <= maxCol; c++) {
this.selectedCells.push({ row: r, col: c });
}
}
},
endSelect() {
this.isSelecting = false;
},
editCell(row, col) {
// 停止任何正在进行的选择
this.isSelecting = false;
this.selectedCells = [{ row, col }];
// 进入编辑模式
this.editingCell = { row, col };
this.$nextTick(() => {
const cellInputs = this.$refs.cellInput;
if (cellInputs) {
// 确保输入框获得焦点
if (Array.isArray(cellInputs)) {
cellInputs.forEach(input => {
if (input) input.focus();
});
} else if (cellInputs) {
cellInputs.focus();
}
}
});
},
finishEditing() {
// 保存当前状态
this.saveState();
this.editingCell = null;
},
selectColor(color) {
if (this.selectedCells.length === 0) return;
// 保存当前状态
this.saveState();
this.selectedColor = color;
this.colorPopoverVisible = false;
this.selectedCells.forEach(cell => {
this.tableData[cell.row][cell.col].bgColor = color;
});
},
clearCell() {
if (this.selectedCells.length === 0) return;
// 保存当前状态
this.saveState();
this.selectedCells.forEach(cell => {
this.tableData[cell.row][cell.col].content = '';
this.tableData[cell.row][cell.col].bgColor = '#ffffff';
});
},
startResizeCol(event, col) {
this.resizingCol = col;
this.startX = event.clientX;
this.startWidth = this.cellWidths[col];
},
startResizeRow(event, row) {
this.resizingRow = row;
this.startY = event.clientY;
this.startHeight = this.cellHeights[row];
},
handleResize(event) {
if (this.resizingCol !== null) {
const deltaX = event.clientX - this.startX;
const newWidth = Math.max(60, this.startWidth + deltaX);
this.$set(this.cellWidths, this.resizingCol, newWidth);
}
if (this.resizingRow !== null) {
const deltaY = event.clientY - this.startY;
const newHeight = Math.max(30, this.startHeight + deltaY);
this.$set(this.cellHeights, this.resizingRow, newHeight);
}
},
endResize() {
this.resizingCol = null;
this.resizingRow = null;
},
getRowspan(row, col) {
// 检查当前单元格是否是合并单元格的起始单元格
const mergedCell = this.mergedCells.find(mc => mc.startRow === row && mc.startCol === col);
return mergedCell ? mergedCell.endRow - mergedCell.startRow + 1 : 1;
},
getColspan(row, col) {
// 检查当前单元格是否是合并单元格的起始单元格
const mergedCell = this.mergedCells.find(mc => mc.startRow === row && mc.startCol === col);
return mergedCell ? mergedCell.endCol - mergedCell.startCol + 1 : 1;
},
getCellStyle(row, col) {
// 检查当前单元格是否是合并单元格的一部分
const mergedCell = this.mergedCells.find(mc =>
row >= mc.startRow && row <= mc.endRow &&
col >= mc.startCol && col <= mc.endCol
);
// 如果是合并单元格的非起始单元格,隐藏它
if (mergedCell && (row !== mergedCell.startRow || col !== mergedCell.startCol)) {
return {
display: 'none',
width: this.cellWidths[col] + 'px',
height: this.cellHeights[row] + 'px'
};
}
// 正常单元格的样式
return {
width: this.cellWidths[col] + 'px',
height: this.cellHeights[row] + 'px',
backgroundColor: this.tableData[row][col].bgColor || '#fff'
};
},
exportImage() {
const container = this.$refs.tableContainer;
html2canvas(container, {
backgroundColor: '#ffffff',
scale: 2,
useCORS: true
}).then(canvas => {
const link = document.createElement('a');
link.download = '数据卡.png';
link.href = canvas.toDataURL('image/png');
link.click();
this.$message.success('图片导出成功!');
}).catch(err => {
this.$message.error('图片导出失败:' + err.message);
});
}
}
});
</script>
</body>
</html>

1
ruoyi-ui/public/element-ui.css

File diff suppressed because one or more lines are too long

BIN
ruoyi-ui/public/fonts/element-icons.woff

Binary file not shown.

BIN
ruoyi-ui/public/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
ruoyi-ui/public/logo2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

252
ruoyi-ui/public/stepEditor.html

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>六步法编辑器</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<link rel="stylesheet" href="./element-ui.css">
<style>
* {
margin: 0;
@ -36,7 +36,6 @@
padding: 0 32px;
height: 56px;
background: #fff;
border-bottom: 1px solid #e8e8e8;
}
.header-left {
@ -45,9 +44,14 @@
gap: 16px;
}
.header-right {
display: flex;
gap: 12px;
}
.back-btn {
padding: 8px 12px;
font-size: 14px;
font-size: 16px;
color: #666;
}
@ -56,14 +60,20 @@
}
.step-title {
font-size: 16px;
font-weight: 500;
font-size: 18px;
font-weight: bold;
color: #333;
}
.save-btn {
padding: 8px 24px;
font-size: 14px;
font-size: 16px;
border-radius: 4px;
}
.export-btn {
padding: 8px 24px;
font-size: 16px;
border-radius: 4px;
}
@ -135,18 +145,18 @@
.editor-content {
flex: 1;
overflow: hidden;
padding: 32px;
padding: 16px;
display: flex;
justify-content: center;
}
.editor-area {
width: 100%;
max-width: 840px;
max-width: 100%;
height: 100%;
background: #fff;
border-radius: 8px;
padding: 40px;
padding: 24px;
overflow-y: auto;
outline: none;
font-size: 16px;
@ -201,6 +211,10 @@
<i class="el-icon-check"></i>
保存
</el-button>
<el-button type="success" @click="exportContent" class="export-btn">
<i class="el-icon-download"></i>
导出
</el-button>
</div>
</div>
@ -231,11 +245,6 @@
<i class="el-icon-picture"></i>
</el-button>
</el-tooltip>
<el-tooltip content="插入链接" placement="top">
<el-button type="text" @click="insertLink" class="toolbar-btn">
<i class="el-icon-link"></i>
</el-button>
</el-tooltip>
</div>
<div class="toolbar-divider"></div>
@ -501,17 +510,6 @@
}
reader.readAsDataURL(file)
},
insertLink() {
this.$prompt('请输入链接地址', '插入链接', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(({ value }) => {
if (value) {
const link = `<a href="${value}" target="_blank" style="color: #409EFF; text-decoration: underline;">${value}</a>`
this.insertHTML(link)
}
})
},
insertText(text) {
const editor = this.$refs.editor
const selection = window.getSelection()
@ -612,6 +610,88 @@
localStorage.setItem('stepEditorContent', JSON.stringify(savedData))
this.$message.success('保存成功')
},
async exportContent() {
this.content = this.$refs.editor.innerHTML
let contentWithBase64 = this.content
try {
const base64 = await this.imageToBase64('./logo2.jpg')
contentWithBase64 = this.content.replace(/<img src="\.\/logo2\.jpg"[^>]*>/g,
`<img src="${base64}" style="height: 50px; width: auto;">`)
} catch (e) {
console.log('Logo conversion skipped:', e)
}
const completeHtml = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${this.stepTitle}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
font-size: 32px;
font-weight: bold;
margin: 20px 0;
text-align: center;
}
h2 {
font-size: 24px;
font-weight: bold;
margin: 20px 0;
}
p {
margin: 10px 0;
}
.editor-content {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="editor-content">
${contentWithBase64}
</div>
</body>
</html>
`
const blob = new Blob([completeHtml], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${this.stepTitle}_${new Date().toLocaleString().replace(/[/:]/g, '-')}.html`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
this.$message.success('导出成功')
},
imageToBase64(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
resolve(canvas.toDataURL('image/jpeg'))
}
img.onerror = reject
img.src = url
})
},
loadSavedContent() {
const savedData = JSON.parse(localStorage.getItem('stepEditorContent') || '{}')
const stepData = savedData[`step_${this.stepIndex}`]
@ -621,7 +701,129 @@
this.content = stepData.content
this.updateWordCount()
})
} else {
this.setDefaultContent()
}
},
setDefaultContent() {
this.$nextTick(() => {
if (this.stepIndex === 0) {
this.$refs.editor.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 20px;">
<img src="./logo2.jpg" alt="logo" style="height: 50px; width: auto; justify-self: start;">
<h1 style="font-size: 32px; font-weight: bold; margin: 0; text-align: center;">点名</h1>
<div></div>
</div>
<div style="display: flex; justify-content: center; gap: 580px; margin-top: 30px; text-align: center;">
<div>
<div style="margin-bottom: 80px;"><h2 style="font-size: 18px; font-weight: bold; display: inline-block;">XXX组:</h2><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></div>
<div style="margin-bottom: 80px;"><h2 style="font-size: 18px; font-weight: bold; display: inline-block;">XXX组:</h2><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></div>
</div>
<div>
<div style="margin-bottom: 80px;"><h2 style="font-size: 18px; font-weight: bold; display: inline-block;">XXX组:</h2><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></div>
<div style="margin-bottom: 80px;"><h2 style="font-size: 18px; font-weight: bold; display: inline-block;">XXX组:</h2><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></div>
</div>
<div>
<div style="margin-bottom: 80px;"><h2 style="font-size: 18px; font-weight: bold; display: inline-block;">XXX组:</h2><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></div>
<div style="margin-bottom: 80px;"><h2 style="font-size: 18px; font-weight: bold; display: inline-block;">XXX组:</h2><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p></div>
</div>
</div>
`
} else if (this.stepIndex === 1) {
this.$refs.editor.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 20px;">
<img src="./logo2.jpg" alt="logo" style="height: 50px; width: auto; justify-self: start;">
<h1 style="font-size: 32px; font-weight: bold; margin: 0; text-align: center;">集体协同</h1>
<div></div>
</div>
<p style="font-size: 18px; font-weight: bold; margin: 20px 0; text-align: center;">任务主要目标、次要目标及风险等级</p>
<div style="margin-top: 30px;">
<p style="font-size: 16px; font-weight: bold; margin-bottom: 10px;">主要目标:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">次要目标:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">风险等级:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
`
} else if (this.stepIndex === 2) {
this.$refs.editor.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 20px;">
<img src="./logo2.jpg" alt="logo" style="height: 50px; width: auto; justify-self: start;">
<h1 style="font-size: 32px; font-weight: bold; margin: 0; text-align: center;">战术规划</h1>
<div></div>
</div>
<h2 style="font-size: 24px; font-weight: bold; margin: 20px 0;">整体战术:</h2>
`
} else if (this.stepIndex === 3) {
this.$refs.editor.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 20px;">
<img src="./logo2.jpg" alt="logo" style="height: 50px; width: auto; justify-self: start;">
<h1 style="font-size: 32px; font-weight: bold; margin: 0; text-align: center;">资源准备</h1>
<div></div>
</div>
<p style="font-size: 18px; font-weight: bold; margin: 20px 0; text-align: center;">资源准备、风险识别及应对措施</p>
<div style="margin-top: 30px;">
<p style="font-size: 16px; font-weight: bold; margin-bottom: 10px;">资源准备:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">风险识别:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">应对措施:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
`
} else if (this.stepIndex === 4) {
this.$refs.editor.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 20px;">
<img src="./logo2.jpg" alt="logo" style="height: 50px; width: auto; justify-self: start;">
<h1 style="font-size: 32px; font-weight: bold; margin: 0; text-align: center;">指挥演练</h1>
<div></div>
</div>
<p style="font-size: 18px; font-weight: bold; margin: 20px 0; text-align: center;">任务分工、时间节点及实时监控</p>
<div style="margin-top: 30px;">
<p style="font-size: 16px; font-weight: bold; margin-bottom: 10px;">任务分工:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">时间节点:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">实时监控:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
`
} else if (this.stepIndex === 5) {
this.$refs.editor.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 20px;">
<img src="./logo2.jpg" alt="logo" style="height: 50px; width: auto; justify-self: start;">
<h1 style="font-size: 32px; font-weight: bold; margin: 0; text-align: center;">总结评估</h1>
<div></div>
</div>
<p style="font-size: 18px; font-weight: bold; margin: 20px 0; text-align: center;">任务完成情况、效果评估及总结改进</p>
<div style="margin-top: 30px;">
<p style="font-size: 16px; font-weight: bold; margin-bottom: 10px;">完成情况:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">效果评估:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p style="font-size: 16px; font-weight: bold; margin: 20px 0 10px 0;">总结改进:</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
`
} else {
this.$refs.editor.innerHTML = ''
}
this.content = this.$refs.editor.innerHTML
this.updateWordCount()
})
}
}
})

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'
})
}

1
ruoyi-ui/src/assets/icons/svg/shujukapian.svg

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772159974776" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1624" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M880.96 64h-579.84c-43.648 0-79.04 35.392-79.04 79.04v105.408a79.04 79.04 0 0 0-79.04 79.04v105.408h-26.368A52.736 52.736 0 0 0 64 485.632v421.696c0 29.056 23.616 52.672 52.672 52.672h632.512a52.672 52.672 0 0 0 52.736-52.672v-131.84c43.648 0 79.04-35.328 79.04-79.04V591.104A79.04 79.04 0 0 0 960 512V143.04A79.04 79.04 0 0 0 880.96 64zM116.672 485.632h632.512v112.576H116.672V485.632z m0 421.696V650.88h632.512v256.448H116.672z m711.488-210.816a26.368 26.368 0 0 1-26.304 26.304V485.632a52.736 52.736 0 0 0-52.672-52.736H195.776V327.488c0-14.528 11.776-26.304 26.304-26.304h579.84c14.528 0 26.304 11.776 26.368 26.304v369.024h-0.128zM907.328 512a26.368 26.368 0 0 1-26.368 26.368V327.488c0-43.648-35.328-79.04-78.976-79.04H274.88V143.104c0-14.592 11.776-26.368 26.304-26.368h579.712c14.592 0 26.432 11.776 26.432 26.368V512h-0.064z" fill="#000000" p-id="1625"></path><path d="M448 768a32 32 0 1 0 64 0 32 32 0 0 0-64 0z m96 0a32 32 0 1 0 64 0 32 32 0 0 0-64 0z m96 0a32 32 0 1 0 64 0 32 32 0 0 0-64 0z" fill="#000000" p-id="1626"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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

45
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: {
@ -183,8 +187,8 @@ export default {
routeLabelVisible: {},
//
defaultStyles: {
point: { color: '#FF0000', size: 12 },
line: { color: '#00FF00', width: 3 },
point: { color: '#808080', size: 8 },
line: { color: '#000000', width: 1 },
polygon: { color: '#0000FF', opacity: 0, width: 4 },
rectangle: { color: '#FFA500', opacity: 0, width: 4 },
circle: { color: '#800080', opacity: 0, width: 4 },
@ -2127,7 +2131,7 @@ this.viewer.scene.postProcessStages.fxaa.enabled = true
// 使
this.hoverTooltip = {
visible: true,
content: `${(cumulativeLength / 1000).toFixed(1)}km ,${bearingType === 'magnetic' ? '磁' : '真'}${bearing.toFixed(1)}°`,
content: `${(cumulativeLength / 1000).toFixed(1)}km ,${bearing.toFixed(1)}°`,
position: {
x: movement.endPosition.x + 10,
y: movement.endPosition.y - 10
@ -2468,7 +2472,7 @@ this.viewer.scene.postProcessStages.fxaa.enabled = true
// 使
this.hoverTooltip = {
visible: true,
content: `${(length / 1000).toFixed(1)}km ,真:${bearing.toFixed(1)}°`,
content: `${(length / 1000).toFixed(1)}km ,${bearing.toFixed(1)}°`,
position: {
x: movement.endPosition.x + 10,
y: movement.endPosition.y - 10
@ -2496,9 +2500,16 @@ this.viewer.scene.postProcessStages.fxaa.enabled = true
const position = this.getClickPosition(click.position);
if (position) {
this.drawingPoints.push(position);
//
let cumulativeDistance = 0;
if (this.drawingPoints.length > 1) {
cumulativeDistance = this.calculateLineLength(this.drawingPoints);
}
//
this.entityCounter++;
const pointId = `point_${this.entityCounter}`;
//
const isStartPoint = this.drawingPoints.length === 1;
const pointEntity = this.viewer.entities.add({
id: pointId,
position: position,
@ -2507,6 +2518,17 @@ this.viewer.scene.postProcessStages.fxaa.enabled = true
color: Cesium.Color.fromCssColorString(this.defaultStyles.point.color),
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
label: {
text: isStartPoint ? '起点' : `${(cumulativeDistance / 1000).toFixed(2)}km`,
font: '14px Microsoft YaHei, sans-serif',
fillColor: Cesium.Color.BLACK,
backgroundColor: Cesium.Color.WHITE.withAlpha(0.8),
showBackground: true,
horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
pixelOffset: new Cesium.Cartesian2(15, 0),
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
this.drawingPointEntities.push(pointEntity);
@ -2581,6 +2603,14 @@ this.viewer.scene.postProcessStages.fxaa.enabled = true
this.viewer.entities.remove(this.tempPreviewEntity);
this.tempPreviewEntity = null;
}
// ""
if (this.drawingPointEntities.length > 0) {
const lastPointEntity = this.drawingPointEntities[this.drawingPointEntities.length - 1];
const length = this.calculateLineLength([...this.drawingPoints]);
if (lastPointEntity.label) {
lastPointEntity.label.text = `${(length / 1000).toFixed(2)}km`;
}
}
// 线
const entity = this.addLineEntity([...this.drawingPoints], [...this.drawingPointEntities]);
//
@ -5241,6 +5271,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: 75px;
}
/* 比例尺:高德风格,浅色底、圆角、刻度线 */

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

@ -1,6 +1,6 @@
<template>
<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="panel-toggle" @click="togglePanel" :title="isExpanded ? '收起' : '展开'">
@ -51,14 +51,20 @@ export default {
components: {
BottomTimeline
},
props: {
roomId: {
type: Number,
default: null
}
},
data() {
return {
isExpanded: false,
showPanel: true,
showSixStepsBar: false,
sixStepsData: [
{ title: '理解', desc: '明确任务目标和要求', active: true, completed: true },
{ title: '判断', desc: '评估可用资源和能力', active: false, completed: true },
{ title: '理解', desc: '明确任务目标和要求', active: false, completed: false },
{ title: '判断', desc: '评估可用资源和能力', active: false, completed: false },
{ title: '规划', desc: '制定详细执行方案', active: false, completed: false },
{ title: '准备', desc: '识别和评估潜在风险', active: false, completed: false },
{ title: '执行', desc: '实时监控执行过程', active: false, completed: false },
@ -75,19 +81,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]
@ -205,7 +215,7 @@ export default {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.98) 100%);
backdrop-filter: blur(20px);
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.08);
padding: 5px 60px 5px 60px;
padding: 2px 60px 4px 60px;
z-index: 1000;
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border-top: 1px solid rgba(0, 0, 0, 0.05);
@ -249,7 +259,7 @@ export default {
.steps-bar-container {
display: flex;
gap: 12px;
height: 40px;
height: 62px;
align-items: center;
}

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

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

18
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" :room-id="currentRoomId" />
<!-- 底部时间轴最初版本的样式- 蓝色主题 -->
<div
v-show="!screenshotMode"
@ -471,6 +473,8 @@ export default {
platformIconSaveTimer: null,
//
showImportDialog: false,
// /
bottomPanelVisible: false,
//
screenshotMode: false,
@ -502,6 +506,7 @@ export default {
{ id: 'modify', name: '测距', icon: 'cj' },
{ id: 'refresh', name: '截图', icon: 'screenshot', action: 'refresh' },
{ id: 'basemap', name: '底图', icon: 'dt' },
{ id: 'datacard', name: '数据卡', icon: 'shujukapian' },
{ id: 'save', name: '保存', icon: 'el-icon-document-checked' },
{ id: 'import', name: '导入', icon: 'el-icon-upload2' },
{ id: 'export', name: '导出', icon: 'el-icon-download' }
@ -709,6 +714,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 +1875,11 @@ export default {
this.$message.success(this.showRoute ? '显示航线' : '隐藏航线');
},
generateGanttChart() {
const url = this.$router.resolve('/ganttChart').href
window.open(url, '_blank')
},
systemDescription() {
this.$message.success('系统说明');
//
@ -1948,6 +1961,9 @@ export default {
//
this.drawDom = false;
this.airspaceDrawDom = false;
} else if (item.id === 'datacard') {
//
window.open('/dataCard.html', '_blank');
} else {
//
this.isRightPanelHidden = true;

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

File diff suppressed because it is too large

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