Browse Source

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

# Conflicts:
#	ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
#	ruoyi-ui/src/views/cesiumMap/index.vue
#	ruoyi-ui/src/views/selectRoom/index.vue
master
menghao 2 months ago
parent
commit
b27a2a52f3
  1. 2
      ruoyi-admin/src/main/resources/application-druid.yml
  2. 14
      ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml
  3. BIN
      ruoyi-ui/node_modules.zip
  4. 5
      ruoyi-ui/package.json
  5. 1
      ruoyi-ui/src/permission.js
  6. 128
      ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
  7. 104
      ruoyi-ui/src/views/cesiumMap/MeasurementPanel.vue
  8. 842
      ruoyi-ui/src/views/cesiumMap/index.vue
  9. 428
      ruoyi-ui/src/views/childRoom/BottomLeftPanel.vue
  10. 139
      ruoyi-ui/src/views/childRoom/LeftMenu.vue
  11. 723
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  12. 768
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  13. 1962
      ruoyi-ui/src/views/childRoom/index.vue

2
ruoyi-admin/src/main/resources/application-druid.yml

@ -6,7 +6,7 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
url: jdbc:mysql://localhost:3306/kj?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
# 从库数据源

14
ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml

@ -13,7 +13,6 @@
<result property="path" column="path" />
<result property="component" column="component" />
<result property="query" column="query" />
<result property="routeName" column="route_name" />
<result property="isFrame" column="is_frame" />
<result property="isCache" column="is_cache" />
<result property="menuType" column="menu_type" />
@ -29,7 +28,7 @@
</resultMap>
<sql id="selectMenuVo">
select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time
select menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time
from sys_menu
</sql>
@ -50,13 +49,13 @@
</select>
<select id="selectMenuTreeAll" resultMap="SysMenuResult">
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0
order by m.parent_id, m.order_num
</select>
<select id="selectMenuListByUserId" parameterType="SysMenu" resultMap="SysMenuResult">
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
from sys_menu m
left join sys_role_menu rm on m.menu_id = rm.menu_id
left join sys_user_role ur on rm.role_id = ur.role_id
@ -75,7 +74,7 @@
</select>
<select id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult">
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
from sys_menu m
left join sys_role_menu rm on m.menu_id = rm.menu_id
left join sys_user_role ur on rm.role_id = ur.role_id
@ -135,7 +134,7 @@
<select id="selectMenusByPathOrRouteName" parameterType="SysMenu" resultMap="SysMenuResult">
<include refid="selectMenuVo"/>
where menu_type in ('M', 'C') and (path = #{path} or path = #{routeName} or route_name = #{path} or route_name = #{routeName})
where menu_type in ('M', 'C') and (path = #{path} or path = #{routeName})
</select>
<update id="updateMenu" parameterType="SysMenu">
@ -147,7 +146,6 @@
<if test="path != null and path != ''">path = #{path},</if>
<if test="component != null">component = #{component},</if>
<if test="query != null">`query` = #{query},</if>
<if test="routeName != null">route_name = #{routeName},</if>
<if test="isFrame != null and isFrame != ''">is_frame = #{isFrame},</if>
<if test="isCache != null and isCache != ''">is_cache = #{isCache},</if>
<if test="menuType != null and menuType != ''">menu_type = #{menuType},</if>
@ -171,7 +169,6 @@
<if test="path != null and path != ''">path,</if>
<if test="component != null and component != ''">component,</if>
<if test="query != null and query != ''">`query`,</if>
<if test="routeName != null">route_name,</if>
<if test="isFrame != null and isFrame != ''">is_frame,</if>
<if test="isCache != null and isCache != ''">is_cache,</if>
<if test="menuType != null and menuType != ''">menu_type,</if>
@ -190,7 +187,6 @@
<if test="path != null and path != ''">#{path},</if>
<if test="component != null and component != ''">#{component},</if>
<if test="query != null and query != ''">#{query},</if>
<if test="routeName != null">#{routeName},</if>
<if test="isFrame != null and isFrame != ''">#{isFrame},</if>
<if test="isCache != null and isCache != ''">#{isCache},</if>
<if test="menuType != null and menuType != ''">#{menuType},</if>

BIN
ruoyi-ui/node_modules.zip

Binary file not shown.

5
ruoyi-ui/package.json

@ -26,7 +26,7 @@
"dependencies": {
"@riophae/vue-treeselect": "0.4.0",
"axios": "0.28.1",
"cesium": "1.95",
"cesium": "^1.95.0",
"clipboard": "2.0.8",
"core-js": "3.37.1",
"echarts": "5.4.0",
@ -50,6 +50,8 @@
"vuex": "3.6.0"
},
"devDependencies": {
"@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",
"@vue/cli-plugin-babel": "4.4.6",
"@vue/cli-service": "4.4.6",
@ -57,7 +59,6 @@
"chalk": "4.1.0",
"compression-webpack-plugin": "6.1.2",
"connect": "3.6.6",
"copy-webpack-plugin": "^5.1.2",
"sass": "1.32.13",
"sass-loader": "10.1.1",
"script-ext-html-webpack-plugin": "2.1.5",

1
ruoyi-ui/src/permission.js

@ -10,6 +10,7 @@ import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/register', '/selectRoom', '/childRoom']
const isWhiteList = (path) => {
return whiteList.some(pattern => isPathMatch(pattern, path))
}

128
ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue

@ -0,0 +1,128 @@
<template>
<div class="drawing-toolbar" v-if="drawDomClick">
<div class="toolbar-icons">
<div
v-for="item in toolbarItems"
:key="item.id"
class="toolbar-item"
:class="{ active: drawingMode === item.id }"
@click="handleItemClick(item)"
:title="item.name"
>
<i :class="item.icon"></i>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DrawingToolbar',
props: {
drawDomClick: {
type: Boolean,
default: false
},
drawingMode: {
type: String,
default: null
},
hasEntities: {
type: Boolean,
default: false
}
},
data() {
return {
toolbarItems: [
{ id: 'point', name: '点', icon: 'el-icon-location' },
{ id: 'line', name: '线', icon: 'el-icon-edit-outline' },
{ id: 'polygon', name: '面', icon: 'el-icon-s-grid' },
{ id: 'rectangle', name: '矩形', icon: 'el-icon-s-data' },
{ id: 'circle', name: '圆形', icon: 'el-icon-circle-plus-outline' },
{ id: 'locate', name: '定位', icon: 'el-icon-aim' },
{ id: 'clear', name: '清除', icon: 'el-icon-delete' },
{ id: 'import', name: '导入', icon: 'el-icon-upload' },
{ id: 'export', name: '导出', icon: 'el-icon-download' }
]
}
},
methods: {
handleItemClick(item) {
if (item.id === 'clear') {
this.$emit('clear-all')
} else if (item.id === 'export') {
this.$emit('export-data')
} else if (item.id === 'import') {
this.$emit('import-data')
} else if (item.id === 'locate') {
this.$emit('locate')
} else {
this.$emit('toggle-drawing', item.id)
}
}
}
}
</script>
<style scoped>
.drawing-toolbar {
position: absolute;
top: 70px;
right: 20px;
width: 40px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 138, 255, 0.1);
border-radius: 8px;
z-index: 90;
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.2);
padding: 15px 5px;
transition: all 0.3s ease;
overflow: hidden;
opacity: 1;
transform: translateX(0);
}
.toolbar-icons {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 30px;
}
.toolbar-item {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
cursor: pointer;
color: #555;
font-size: 20px;
position: relative;
transition: all 0.3s;
border-radius: 4px;
padding: 0 5px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(5px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.toolbar-item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 138, 255, 0.2);
}
.toolbar-item.active {
background: rgba(0, 138, 255, 0.15);
color: #008aff;
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
}
.toolbar-item:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

104
ruoyi-ui/src/views/cesiumMap/MeasurementPanel.vue

@ -0,0 +1,104 @@
<template>
<div class="measurement-panel">
<div class="measurement-content">
<h5>测量结果</h5>
<div class="measurement-item" v-if="result.distance">
<span>长度</span>
<strong>{{ result.distance.toFixed(2) }} </strong>
</div>
<div class="measurement-item" v-if="result.area">
<span>面积</span>
<strong>{{ result.area.toFixed(2) }} 平方米</strong>
</div>
<div class="measurement-item" v-if="result.radius">
<span>半径</span>
<strong>{{ result.radius.toFixed(2) }} </strong>
</div>
<button @click="handleClose" class="close-btn">关闭</button>
</div>
</div>
</template>
<script>
export default {
name: 'MeasurementPanel',
props: {
result: {
type: Object,
default: null
}
},
methods: {
handleClose() {
this.$emit('close')
}
}
}
</script>
<style scoped>
.measurement-panel {
position: absolute;
bottom: 20px;
right: 80px;
z-index: 85;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 20px;
min-width: 250px;
}
.measurement-content h5 {
margin: 0 0 15px 0;
font-size: 16px;
font-weight: 600;
color: #333;
border-bottom: 2px solid #409EFF;
padding-bottom: 8px;
}
.measurement-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
color: #666;
}
.measurement-item strong {
color: #409EFF;
font-weight: 600;
}
.close-btn {
width: 100%;
padding: 10px;
background: #409EFF;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
margin-top: 15px;
}
.close-btn:hover {
background: #66b1ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
@media (max-width: 768px) {
.measurement-panel {
bottom: 10px;
right: 10px;
left: 10px;
min-width: auto;
}
}
</style>

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

@ -2,200 +2,30 @@
<div class="cesium-container">
<div id="cesiumViewer" ref="cesiumViewer"></div>
<!-- 主工具栏 -->
<div class="main-toolbar" v-if="drawDomClick" :class="{ collapsed: isToolbarCollapsed }">
<!-- 展开/收起按钮 -->
<!-- <div class="collapse-btn" @click="toggleToolbar">
<span class="collapse-icon">{{ isToolbarCollapsed ? '▶' : '◀' }}</span>
<span class="collapse-text" v-if="!isToolbarCollapsed">收起</span>
</div> -->
<!-- 工具栏内容 -->
<div class="location-panel">
<div class="input-group">
<input
type="number"
v-model.number="targetLng"
placeholder="经度 (例如 116.40)"
step="0.000001"
>
<input
type="number"
v-model.number="targetLat"
placeholder="纬度 (例如 39.90)"
step="0.000001"
>
<button @click="flyToLocation" class="tool-btn location-btn">
<span class="icon">🚀</span>
定位
</button>
</div>
</div>
<div class="toolbar-content" v-show="!isToolbarCollapsed">
<div class="toolbar-group">
<button
@click="toggleDrawing('point')"
:class="{ active: drawingMode === 'point' }"
title="点"
class="tool-btn"
>
<span class="icon">📍</span>
<span class="text">{{ drawingMode === 'point' ? '停止' : '点' }}</span>
</button>
<button
@click="toggleDrawing('line')"
:class="{ active: drawingMode === 'line' }"
title="线"
class="tool-btn"
>
<span class="icon">📏</span>
<span class="text">{{ drawingMode === 'line' ? '停止' : '线' }}</span>
</button>
<button
@click="toggleDrawing('polygon')"
:class="{ active: drawingMode === 'polygon' }"
title="面"
class="tool-btn"
>
<span class="icon">🔶</span>
<span class="text">{{ drawingMode === 'polygon' ? '停止' : '面' }}</span>
</button>
</div>
<div class="toolbar-group">
<button
@click="toggleDrawing('rectangle')"
:class="{ active: drawingMode === 'rectangle' }"
title="矩形"
class="tool-btn"
>
<span class="icon"></span>
<span class="text">{{ drawingMode === 'rectangle' ? '停止' : '矩形' }}</span>
</button>
<button
@click="toggleDrawing('circle')"
:class="{ active: drawingMode === 'circle' }"
title="圆形"
class="tool-btn"
>
<span class="icon"></span>
<span class="text">{{ drawingMode === 'circle' ? '停止' : '圆形' }}</span>
</button>
</div>
<div class="toolbar-group">
<button
@click="clearAll"
:disabled="!allEntities.length"
title="清除所有"
class="tool-btn danger"
>
<span class="icon">🗑</span>
<span class="text">清除</span>
</button>
<button
@click="exportData"
:disabled="!allEntities.length"
title="导出数据"
class="tool-btn success"
>
<span class="icon">💾</span>
<span class="text">导出</span>
</button>
</div>
</div>
<!-- 收起时的最小化工具栏 -->
<div class="toolbar-minimized" v-show="isToolbarCollapsed">
<button
@click="toggleDrawing('point')"
:class="{ active: drawingMode === 'point' }"
title="点"
class="min-tool-btn"
>
<span class="icon">📍</span>
</button>
<button
@click="toggleDrawing('line')"
:class="{ active: drawingMode === 'line' }"
title="线"
class="min-tool-btn"
>
<span class="icon">📏</span>
</button>
<button
@click="toggleDrawing('polygon')"
:class="{ active: drawingMode === 'polygon' }"
title="面"
class="min-tool-btn"
>
<span class="icon">🔶</span>
</button>
<button
@click="toggleDrawing('rectangle')"
:class="{ active: drawingMode === 'rectangle' }"
title="矩形"
class="min-tool-btn"
>
<span class="icon"></span>
</button>
<button
@click="toggleDrawing('circle')"
:class="{ active: drawingMode === 'circle' }"
title="圆形"
class="min-tool-btn"
>
<span class="icon"></span>
</button>
<div class="min-tool-separator"></div>
<button
@click="clearAll"
:disabled="!allEntities.length"
title="清除所有"
class="min-tool-btn danger"
>
<span class="icon">🗑</span>
</button>
<button
@click="exportData"
:disabled="!allEntities.length"
title="导出数据"
class="min-tool-btn success"
>
<span class="icon">💾</span>
</button>
</div>
</div>
<!-- 测量结果显示 -->
<div class="measurement-panel" v-if="measurementResult">
<div class="measurement-content">
<h5>测量结果</h5>
<div class="measurement-item" v-if="measurementResult.distance">
<span>长度</span>
<strong>{{ measurementResult.distance.toFixed(2) }} </strong>
</div>
<div class="measurement-item" v-if="measurementResult.area">
<span>面积</span>
<strong>{{ measurementResult.area.toFixed(2) }} 平方米</strong>
</div>
<div class="measurement-item" v-if="measurementResult.radius">
<span>半径</span>
<strong>{{ measurementResult.radius.toFixed(2) }} </strong>
</div>
<button @click="measurementResult = null" class="close-btn">关闭</button>
</div>
</div>
<drawing-toolbar
:draw-dom-click="drawDomClick"
:drawing-mode="drawingMode"
:has-entities="allEntities.length > 0"
@toggle-drawing="toggleDrawing"
@clear-all="clearAll"
@export-data="exportData"
@import-data="importData"
@locate="handleLocate"
/>
<measurement-panel
v-if="measurementResult"
:result="measurementResult"
@close="measurementResult = null"
/>
</div>
</template>
<script>
// import * as Cesium from 'cesium'
// import 'cesium/Build/Cesium/Widgets/widgets.css'
import * as Cesium from 'cesium'
import 'cesium/Build/Cesium/Widgets/widgets.css'
import DrawingToolbar from './DrawingToolbar.vue'
import MeasurementPanel from './MeasurementPanel.vue'
export default {
name: 'CesiumMap',
props: {
@ -227,12 +57,6 @@ export default {
return {
viewer: null,
scaleBar: null,
isToolbarCollapsed: false,
// ================== ==================
targetLng: 116.3974, // ()
targetLat: 39.9093, //
// =================================================
//
drawingMode: null, // 'point', 'line', 'polygon', 'rectangle', 'circle'
@ -261,7 +85,10 @@ export default {
}
}
},
components: {
DrawingToolbar,
MeasurementPanel
},
mounted() {
console.log(this.drawDomClick,999999)
// this.initMap()
@ -273,10 +100,6 @@ export default {
},
methods: {
// /
toggleToolbar() {
this.isToolbarCollapsed = !this.isToolbarCollapsed
},
checkCesiumLoaded() {
if (typeof Cesium === 'undefined') {
console.error('Cesium未加载,请检查CDN链接');
@ -295,7 +118,7 @@ export default {
initMap() {
try {
// Cesium
// Cesium.buildModuleUrl.setBaseUrl(window.CESIUM_BASE_URL)
Cesium.buildModuleUrl.setBaseUrl(window.CESIUM_BASE_URL)
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjN2MzMmE5OS01NGU3LTQzOGQtYjdjZi1mNGIwZTFjZjQ0NmEiLCJpZCI6MTQ0MDc2LCJpYXQiOjE2ODU3NjY1OTN9.iCmFY-5WNdvyAT-EO2j-unrFm4ZN9J6aSuB2wElQZ-I'
this.viewer = new Cesium.Viewer('cesiumViewer', {
@ -989,39 +812,6 @@ export default {
this.stopDrawing();
},
flyToLocation() {
// 1.
if (!this.targetLng || !this.targetLat) {
alert("请输入有效的经度和纬度!");
return;
}
const lng = parseFloat(this.targetLng);
const lat = parseFloat(this.targetLat);
//
if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
alert("经纬度超出有效范围!");
return;
}
// 2.
if (this.viewer) {
this.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lng, lat, 5000), // 5000
orientation: {
heading: Cesium.Math.toRadians(0.0),
pitch: Cesium.Math.toRadians(-90.0), //
roll: 0.0
},
duration: 2 //
});
//
this.addTempMarker(lng, lat);
}
},
// ================== ==================
addPointEntity(lat, lng) {
@ -1173,31 +963,34 @@ export default {
return entity
},
addCircleEntity(center, radius) {
this.entityCounter++
const id = `circle_${this.entityCounter}`
//
const validRadius = Math.max(radius, 1)
const entity = this.viewer.entities.add({
id: id,
name: `圆形 ${this.entityCounter}`,
position: center, //
// 使 ellipse ()
ellipse: {
semiMinorAxis: radius, // =
semiMajorAxis: radius, // =
material: Cesium.Color.fromCssColorString(this.defaultStyles.circle.color)
.withAlpha(this.defaultStyles.circle.opacity),
outline: true,
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.circle.color),
outlineWidth: this.defaultStyles.circle.width
}
})
this.entityCounter++
const id = `circle_${this.entityCounter}`
// entity
this.allEntities.push(entity)
const entity = this.viewer.entities.add({
id: id,
name: `圆形 ${this.entityCounter}`,
position: center, //
return entity
},
// 使 ellipse ()
ellipse: {
semiMinorAxis: validRadius, // =
semiMajorAxis: validRadius, // =
material: Cesium.Color.fromCssColorString(this.defaultStyles.circle.color)
.withAlpha(this.defaultStyles.circle.opacity),
outline: true,
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.circle.color),
outlineWidth: this.defaultStyles.circle.width
}
})
// entity
this.allEntities.push(entity)
return entity
},
// ================== ==================
@ -1465,6 +1258,245 @@ export default {
console.log('数据已导出', data)
},
importData() {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (event) => {
try {
const data = JSON.parse(event.target.result)
if (data.entities && Array.isArray(data.entities)) {
data.entities.forEach(entityData => {
this.importEntity(entityData)
})
console.log('数据已导入', data)
this.$message.success(`成功导入 ${data.entities.length} 个实体`)
} else {
this.$message.error('文件格式不正确')
}
} catch (error) {
console.error('导入失败', error)
this.$message.error('文件解析失败')
}
}
reader.readAsText(file)
}
input.click()
},
importEntity(entityData) {
let entity
const color = entityData.color || '#008aff'
switch (entityData.type) {
case 'point':
entity = this.viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(entityData.data.lng, entityData.data.lat),
point: {
pixelSize: 10,
color: Cesium.Color.fromCssColorString(color),
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
label: {
text: entityData.label || '点',
font: '14px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -10)
}
})
break
case 'line':
const linePositions = entityData.data.points.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat))
entity = this.viewer.entities.add({
polyline: {
positions: linePositions,
width: 3,
material: Cesium.Color.fromCssColorString(color),
clampToGround: true
},
label: {
text: entityData.label || '线',
font: '14px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
position: linePositions[0]
}
})
break
case 'polygon':
const polygonPositions = entityData.data.points.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat))
entity = this.viewer.entities.add({
polygon: {
hierarchy: polygonPositions,
material: Cesium.Color.fromCssColorString(color).withAlpha(0.5),
outline: true,
outlineColor: Cesium.Color.fromCssColorString(color),
outlineWidth: 2
},
label: {
text: entityData.label || '面',
font: '14px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
position: polygonPositions[0]
}
})
break
case 'rectangle':
const rectCoords = entityData.data.coordinates
entity = this.viewer.entities.add({
rectangle: {
coordinates: Cesium.Rectangle.fromDegrees(rectCoords.west, rectCoords.south, rectCoords.east, rectCoords.north),
material: Cesium.Color.fromCssColorString(color).withAlpha(0.5),
outline: true,
outlineColor: Cesium.Color.fromCssColorString(color),
outlineWidth: 2
},
label: {
text: entityData.label || '矩形',
font: '14px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
position: Cesium.Cartesian3.fromDegrees((rectCoords.west + rectCoords.east) / 2, (rectCoords.south + rectCoords.north) / 2)
}
})
break
case 'circle':
//
const radius = entityData.data.radius || 1000
if (radius <= 0) {
this.$message.error('圆形半径必须大于0')
return
}
entity = this.viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(entityData.data.center.lng, entityData.data.center.lat),
ellipse: {
semiMinorAxis: radius,
semiMajorAxis: radius,
material: Cesium.Color.fromCssColorString(color).withAlpha(0.5),
outline: true,
outlineColor: Cesium.Color.fromCssColorString(color),
outlineWidth: 2
},
label: {
text: entityData.label || '圆形',
font: '14px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -10)
}
})
break
}
if (entity) {
this.allEntities.push({
id: entity.id,
type: entityData.type,
label: entityData.label,
color: color,
...entityData.data
})
}
},
handleLocate() {
const h = this.$createElement
this.$msgbox({
title: '定位',
message: h('div', { style: 'padding: 10px 0;' }, [
h('div', { style: 'margin-bottom: 15px;' }, [
h('label', { style: 'display: block; margin-bottom: 5px; color: #606266;' }, '经度:'),
h('input', {
attrs: {
type: 'number',
placeholder: '例如 116.40',
step: '0.000001',
value: '116.3974'
},
style: 'width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px; box-sizing: border-box;',
ref: 'lngInput'
})
]),
h('div', null, [
h('label', { style: 'display: block; margin-bottom: 5px; color: #606266;' }, '纬度:'),
h('input', {
attrs: {
type: 'number',
placeholder: '例如 39.90',
step: '0.000001',
value: '39.9093'
},
style: 'width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px; box-sizing: border-box;',
ref: 'latInput'
})
])
]),
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
const lngInput = instance.$el.querySelector('input[placeholder="例如 116.40"]')
const latInput = instance.$el.querySelector('input[placeholder="例如 39.90"]')
const lng = parseFloat(lngInput.value)
const lat = parseFloat(latInput.value)
if (!lng || !lat || isNaN(lng) || isNaN(lat)) {
this.$message.error('请输入有效的经度和纬度!')
return
}
if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
this.$message.error('经纬度超出有效范围!')
return
}
if (this.viewer) {
this.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lng, lat, 5000),
orientation: {
heading: Cesium.Math.toRadians(0.0),
pitch: Cesium.Math.toRadians(-90.0),
roll: 0.0
},
duration: 2
})
this.$message.success(`已定位到经度 ${lng.toFixed(4)},纬度 ${lat.toFixed(4)}`)
}
done()
} else {
this.$message.info('已取消定位')
done()
}
}
}).catch(() => {
this.$message.info('已取消定位')
})
},
initScaleBar() {
// ...
},
@ -1498,284 +1530,6 @@ export default {
height: 100%;
}
/* 主工具栏 - 修改后 */
.main-toolbar {
position: absolute;
top: 365px; /* 增大这个数值即可整体下移,比如从100px改成150px */
left: 62px;
z-index: 1000;
display: flex;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.main-toolbar.collapsed {
background: rgba(255, 255, 255, 0.9);
}
/* 工具栏内容 - 无需修改 */
.toolbar-content {
display: flex;
flex-direction: column;
gap: 15px;
padding: 15px;
min-width: 180px;
}
/* 收起按钮 */
.collapse-btn {
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
background: #f8f9fa;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
border-right: 1px solid #e9ecef;
transition: all 0.3s ease;
user-select: none;
}
.collapse-btn:hover {
background: #e9ecef;
}
.collapse-icon {
font-size: 14px;
font-weight: bold;
color: #495057;
margin-right: 5px;
}
.collapse-text {
font-size: 12px;
color: #6c757d;
white-space: nowrap;
}
/* 工具栏组 */
.toolbar-group {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
}
.toolbar-group:last-child {
border-bottom: none;
padding-bottom: 0;
}
/* 收起时的最小化工具栏 */
.toolbar-minimized {
display: flex;
flex-direction: column;
padding: 10px 8px;
gap: 8px;
}
.min-tool-btn {
width: 36px;
height: 36px;
border: 2px solid #e9ecef;
border-radius: 8px;
background: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #495057;
transition: all 0.3s ease;
}
.min-tool-btn:hover {
background: #f8f9fa;
border-color: #4dabf7;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.min-tool-btn.active {
background: #4dabf7;
color: white;
border-color: #4dabf7;
}
.min-tool-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.min-tool-btn.danger {
color: #dc3545;
}
.min-tool-btn.danger:hover {
background: #f8d7da;
border-color: #dc3545;
}
.min-tool-btn.success {
color: #28a745;
}
.min-tool-btn.success:hover {
background: #d4edda;
border-color: #28a745;
}
.min-tool-separator {
height: 1px;
background: #e9ecef;
margin: 5px 0;
}
/* 定位面板样式 */
.location-panel {
position: absolute;
top: -80px;
right: -180px; /* 放在右上角 */
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.input-group {
display: flex;
gap: 10px;
align-items: center;
}
.input-group input {
width: 100px;
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
outline: none;
font-size: 14px;
}
.input-group input:focus {
border-color: #4dabf7;
}
.location-btn {
background: #4dabf7;
color: white;
border: none;
padding: 6px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: background 0.3s;
}
.location-btn:hover {
background: #3b8dbd;
}
/* 原来的工具按钮样式保持不变,但添加展开/收起状态的调整 */
.tool-btn {
padding: 10px 15px;
background: white;
border: 2px solid #e9ecef;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 500;
color: #495057;
transition: all 0.3s ease;
min-width: 120px;
}
.tool-btn:hover {
background: #f8f9fa;
border-color: #4dabf7;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.tool-btn.active {
background: #4dabf7;
color: white;
border-color: #4dabf7;
}
.tool-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.tool-btn.danger {
color: #dc3545;
}
.tool-btn.danger:hover {
background: #f8d7da;
border-color: #dc3545;
}
.tool-btn.success {
color: #28a745;
}
.tool-btn.success:hover {
background: #d4edda;
border-color: #28a745;
}
.tool-btn .icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.tool-btn .text {
white-space: nowrap;
}
/* 其他样式保持不变... */
/* 属性面板 */
.property-panel {
position: absolute;
top: 20px;
left: 20px;
z-index: 1000;
background: rgba(255, 255, 255, 0.98);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
min-width: 250px;
max-width: 300px;
}
/* 测量结果面板 */
.measurement-panel {
position: absolute;
bottom: 80px;
left: 20px;
z-index: 1000;
background: rgba(255, 255, 255, 0.98);
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
min-width: 200px;
}
/* 自定义比例尺样式 */
:deep(.scale-bar-container) {
user-select: none;
@ -1794,23 +1548,5 @@ export default {
:deep(.cesium-credit-textContainer) {
display: none !important;
}
/* 响应式调整 */
@media (max-width: 768px) {
.main-toolbar {
right: 10px;
top: 80px;
}
.toolbar-content {
min-width: 150px;
}
.tool-btn {
min-width: 100px;
padding: 8px 12px;
}
}
</style>

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

@ -0,0 +1,428 @@
<template>
<div class="bottom-left-panel">
<div class="panel-toggle" @click="togglePanel" :title="isExpanded ? '收起' : '展开'">
<i :class="isExpanded ? 'el-icon-s-fold' : 'el-icon-s-unfold'"></i>
<span v-if="!isExpanded" class="toggle-text">工具</span>
</div>
<div class="panel-content" :class="{ expanded: isExpanded }">
<div class="panel-item" @click="showTimeline">
<i class="el-icon-time"></i>
<span>时间线</span>
</div>
<div class="panel-item" @click="showProgress">
<i class="el-icon-s-data"></i>
<span>进度检查</span>
</div>
<div class="panel-item" @click="showSixSteps">
<i class="el-icon-s-operation"></i>
<span>六步法</span>
</div>
</div>
<el-dialog
:visible.sync="dialogVisible"
:title="dialogTitle"
width="350px"
:modal="false"
custom-class="panel-dialog"
>
<div class="dialog-content">
<div v-if="activeTab === 'timeline'" class="timeline-content">
<h3>时间线</h3>
<div class="timeline-list">
<div v-for="(item, index) in timelineData" :key="index" class="timeline-item" :class="{ current: item.current }">
<div class="timeline-dot"></div>
<div class="timeline-info">
<div class="timeline-time">{{ item.time }}</div>
<div class="timeline-event">{{ item.event }}</div>
</div>
</div>
</div>
</div>
<div v-if="activeTab === 'progress'" class="progress-content">
<h3>进度检查</h3>
<div class="progress-list">
<div v-for="(item, index) in progressData" :key="index" class="progress-item">
<div class="progress-label">{{ item.label }}</div>
<el-progress :percentage="item.percentage" :status="item.status"></el-progress>
</div>
</div>
</div>
<div v-if="activeTab === 'sixsteps'" class="sixsteps-content">
<h3>六步法</h3>
<div class="steps-container">
<div v-for="(step, index) in sixStepsData" :key="index" class="step-item" :class="{ active: step.active, completed: step.completed }">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'BottomLeftPanel',
data() {
return {
isExpanded: false,
dialogVisible: false,
dialogTitle: '',
activeTab: '',
timelineData: [
{ time: 'K-02:00', event: '任务准备阶段', current: false },
{ time: 'K-01:00', event: '资源调配', current: false },
{ time: 'K时', event: '任务执行', current: true },
{ time: 'K+01:00', event: '任务监控', current: false },
{ time: 'K+02:00', event: '任务完成', current: false }
],
progressData: [
{ label: '任务准备', percentage: 100, status: 'success' },
{ label: '资源调配', percentage: 80, status: '' },
{ label: '任务执行', percentage: 45, status: '' },
{ label: '任务监控', percentage: 20, status: 'exception' },
{ label: '任务完成', percentage: 0, status: '' }
],
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 }
]
}
},
methods: {
togglePanel() {
this.isExpanded = !this.isExpanded
},
showTimeline() {
this.activeTab = 'timeline'
this.dialogTitle = '时间线'
this.dialogVisible = true
},
showProgress() {
this.activeTab = 'progress'
this.dialogTitle = '进度检查'
this.dialogVisible = true
},
showSixSteps() {
this.activeTab = 'sixsteps'
this.dialogTitle = '六步法'
this.dialogVisible = true
}
}
}
</script>
<style scoped>
.bottom-left-panel {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
}
.panel-toggle {
width: 50px;
height: 50px;
background: rgba(0, 138, 255, 0.9);
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
color: white;
font-size: 20px;
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.4);
transition: all 0.3s;
}
.panel-toggle:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(0, 138, 255, 0.6);
}
.toggle-text {
font-size: 10px;
margin-top: 2px;
}
.panel-content {
position: absolute;
bottom: 60px;
left: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 10px;
min-width: 150px;
opacity: 0;
transform: translateY(20px);
pointer-events: none;
transition: all 0.3s;
}
.panel-content.expanded {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.panel-item {
display: flex;
align-items: center;
padding: 12px 15px;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s;
color: #333;
}
.panel-item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
transform: translateX(5px);
}
.panel-item i {
font-size: 18px;
margin-right: 10px;
color: #008aff;
}
.panel-item span {
font-size: 14px;
font-weight: 500;
}
.dialog-content {
padding: 0;
}
.timeline-content h3,
.progress-content h3,
.sixsteps-content h3 {
margin: 0 0 15px 0;
font-size: 14px;
color: #333;
border-bottom: 2px solid #409EFF;
padding-bottom: 8px;
}
.timeline-list {
max-height: 280px;
overflow-y: auto;
}
.timeline-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
position: relative;
}
.timeline-item.current {
background: rgba(64, 158, 255, 0.1);
border-radius: 6px;
padding: 8px;
}
.timeline-item.current .timeline-dot {
background: #ff6600;
box-shadow: 0 0 0 4px rgba(255, 102, 0, 0.3);
animation: pulse 2s infinite;
}
.timeline-item.current .timeline-time {
color: #ff6600;
font-weight: 700;
}
.timeline-item.current .timeline-event {
color: #333;
font-weight: 600;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.timeline-item::before {
content: '';
position: absolute;
left: 5px;
top: 15px;
bottom: -12px;
width: 2px;
background: #e0e0e0;
}
.timeline-item:last-child::before {
display: none;
}
.timeline-dot {
width: 12px;
height: 12px;
background: #409EFF;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
}
.timeline-info {
flex: 1;
}
.timeline-time {
font-size: 12px;
color: #409EFF;
font-weight: 600;
margin-bottom: 3px;
}
.timeline-event {
font-size: 12px;
color: #666;
}
.progress-list {
max-height: 280px;
overflow-y: auto;
}
.progress-item {
margin-bottom: 15px;
}
.progress-label {
font-size: 12px;
color: #333;
margin-bottom: 5px;
font-weight: 500;
}
.steps-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.step-item {
display: flex;
align-items: flex-start;
padding: 10px;
background: #f5f7fa;
border-radius: 6px;
transition: all 0.3s;
border-left: 3px solid #dcdfe6;
}
.step-item.active {
background: rgba(64, 158, 255, 0.1);
border-left-color: #409EFF;
}
.step-item.completed {
background: rgba(103, 194, 58, 0.1);
border-left-color: #67c23a;
}
.step-number {
width: 24px;
height: 24px;
background: #dcdfe6;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
font-weight: 600;
color: #909399;
margin-right: 10px;
flex-shrink: 0;
}
.step-item.active .step-number {
background: #409EFF;
color: white;
}
.step-item.completed .step-number {
background: #67c23a;
color: white;
}
.step-content {
flex: 1;
}
.step-title {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 3px;
}
.step-desc {
font-size: 11px;
color: #666;
}
</style>
<style>
.panel-dialog {
position: absolute !important;
left: 170px !important;
bottom: 20px !important;
top: auto !important;
margin: 0 !important;
max-height: 400px;
}
.panel-dialog .el-dialog__header {
padding: 12px 15px;
background: #409EFF;
color: white;
}
.panel-dialog .el-dialog__title {
color: white;
font-size: 14px;
font-weight: 600;
}
.panel-dialog .el-dialog__headerbtn .el-dialog__close {
color: white;
}
.panel-dialog .el-dialog__body {
padding: 15px;
max-height: 350px;
overflow-y: auto;
}
</style>

139
ruoyi-ui/src/views/childRoom/LeftMenu.vue

@ -0,0 +1,139 @@
<template>
<div
class="floating-left-menu"
:class="{ 'hidden': isHidden }"
@mouseenter="showTooltip = true"
@mouseleave="showTooltip = false"
>
<!-- 隐藏按钮>箭头 -->
<div class="hide-btn" @click="handleHide" title="隐藏菜单">
<i class="el-icon-arrow-left"></i>
</div>
<!-- 一级菜单 -->
<div class="menu-icons">
<div
v-for="item in menuItems"
:key="item.id"
class="menu-item"
:class="{ active: activeMenu === item.id }"
@click="handleSelectMenu(item)"
:title="item.name"
>
<i :class="item.icon"></i>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'LeftMenu',
props: {
isHidden: {
type: Boolean,
default: false
},
menuItems: {
type: Array,
default: () => []
},
activeMenu: {
type: String,
default: ''
}
},
methods: {
handleHide() {
this.$emit('hide')
},
handleSelectMenu(item) {
this.$emit('select', item)
}
}
}
</script>
<style scoped>
/* 左侧菜单栏 - 蓝色主题 。*/
.floating-left-menu {
position: absolute;
top: 70px;
left: 20px;
width: 40px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 138, 255, 0.1);
border-radius: 8px;
z-index: 90;
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.2);
padding: 15px 5px;
transition: all 0.3s ease;
overflow: hidden;
opacity: 1;
transform: translateX(0);
}
.floating-left-menu.hidden {
opacity: 0;
transform: translateX(-100%);
pointer-events: none;
}
.hide-btn {
position: absolute;
top: 15px;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: #008aff;
font-size: 16px;
transition: all 0.3s;
background: rgba(255, 255, 255, 0.5);
border-radius: 4px;
z-index: 10;
}
.menu-icons {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 30px;
}
.menu-item {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
cursor: pointer;
color: #555;
font-size: 20px;
position: relative;
transition: all 0.3s;
border-radius: 4px;
padding: 0 5px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(5px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.menu-item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 138, 255, 0.2);
}
.menu-item.active {
background: rgba(0, 138, 255, 0.15);
color: #008aff;
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
}
</style>

723
ruoyi-ui/src/views/childRoom/RightPanel.vue

@ -0,0 +1,723 @@
<template>
<div>
<!-- 右侧外部隐藏按钮 -->
<div
class="right-external-hide-btn"
:class="{ hidden: isHidden }"
@click="handleHide"
title="隐藏右侧面板"
>
<i class="el-icon-arrow-right"></i>
</div>
<!-- 右侧实体列表浮动- 蓝色主题 -->
<div
class="floating-right-panel blue-theme"
:class="{ 'hidden': isHidden }"
>
<!-- 方案内容 -->
<div v-if="activeTab === 'plan'" class="tab-content plan-content">
<div class="section">
<div class="section-title">航线列表</div>
<div class="route-list">
<div
v-for="route in routes"
:key="route.id"
class="route-item"
:class="{ selected: selectedRouteId === route.id }"
@click="handleSelectRoute(route)"
>
<i class="el-icon-map-location"></i>
<div class="route-info">
<div class="route-name">{{ route.name }}</div>
<div class="route-meta">{{ route.points }}个航点</div>
</div>
<el-tag
v-if="route.conflict"
size="mini"
type="danger"
class="conflict-tag"
>
冲突
</el-tag>
<div class="route-actions">
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenRouteDialog(route)"></i>
</div>
</div>
</div>
</div>
<div v-if="selectedRouteDetails" class="section">
<div class="section-title">航点列表</div>
<div class="waypoint-list">
<div
v-for="point in selectedRouteDetails.waypoints"
:key="point.name"
class="waypoint-item"
@click="handleOpenWaypointDialog(point)"
>
<i class="el-icon-location"></i>
<div class="waypoint-info">
<div class="waypoint-name">{{ point.name }}</div>
<div class="waypoint-meta">高度: {{ point.altitude }}m | 速度: {{ point.speed }}</div>
</div>
<div class="waypoint-actions">
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenWaypointDialog(point)"></i>
<i class="el-icon-delete" title="删除"></i>
</div>
</div>
</div>
</div>
<div class="action-buttons">
<el-button type="primary" size="mini" icon="el-icon-circle-plus" class="blue-btn" @click="handleAddWaypoint">
添加航点
</el-button>
<el-button size="mini" class="blue-btn" @click="handleCancelRoute">
取消
</el-button>
</div>
</div>
<!-- 冲突内容 -->
<div v-if="activeTab === 'conflict'" class="tab-content conflict-content">
<div v-if="conflicts.length > 0" class="conflict-list">
<div
v-for="conflict in conflicts"
:key="conflict.id"
class="conflict-item"
>
<div class="conflict-header">
<i class="el-icon-warning" style="color: #f56c6c;"></i>
<span class="conflict-title">{{ conflict.title }}</span>
<el-tag size="mini" type="danger">严重</el-tag>
</div>
<div class="conflict-details">
<div class="detail-item">
<span class="label">涉及航线</span>
<span class="value">{{ conflict.routes.join('、') }}</span>
</div>
<div class="detail-item">
<span class="label">冲突时间</span>
<span class="value">{{ conflict.time }}</span>
</div>
<div class="detail-item">
<span class="label">冲突位置</span>
<span class="value">{{ conflict.position }}</span>
</div>
</div>
<div class="conflict-actions">
<el-button type="text" size="mini" class="blue-text-btn" @click="handleViewConflict(conflict)">
查看详情
</el-button>
<el-button type="text" size="mini" class="blue-text-btn" @click="handleResolveConflict(conflict)">
解决冲突
</el-button>
</div>
</div>
</div>
<div v-else class="no-conflict">
<i class="el-icon-success" style="color: #67c23a; font-size: 24px;"></i>
<p>暂无冲突</p>
<el-button size="mini" class="blue-btn" @click="handleRunConflictCheck">
重新检测
</el-button>
</div>
</div>
<!-- 平台内容 -->
<div v-if="activeTab === 'platform'" class="tab-content platform-content">
<div class="platform-categories">
<el-tabs v-model="activePlatformTab" type="card" size="mini" class="blue-tabs">
<el-tab-pane label="空中" name="air">
<div class="platform-list">
<div
v-for="platform in airPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<i :class="platform.icon"></i>
</div>
<div class="platform-info">
<div class="platform-name">{{ platform.name }}</div>
<div class="platform-type">{{ platform.type }}</div>
</div>
<div class="platform-status">
<span class="status-dot" :class="platform.status"></span>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="海上" name="sea">
<div class="platform-list">
<div
v-for="platform in seaPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<i :class="platform.icon"></i>
</div>
<div class="platform-info">
<div class="platform-name">{{ platform.name }}</div>
<div class="platform-type">{{ platform.type }}</div>
</div>
<div class="platform-status">
<span class="status-dot" :class="platform.status"></span>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="地面" name="ground">
<div class="platform-list">
<div
v-for="platform in groundPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<i :class="platform.icon"></i>
</div>
<div class="platform-info">
<div class="platform-name">{{ platform.name }}</div>
<div class="platform-type">{{ platform.type }}</div>
</div>
<div class="platform-status">
<span class="status-dot" :class="platform.status"></span>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'RightPanel',
props: {
isHidden: {
type: Boolean,
default: false
},
activeTab: {
type: String,
default: 'plan'
},
routes: {
type: Array,
default: () => []
},
selectedRouteId: {
type: [String, Number],
default: null
},
selectedRouteDetails: {
type: Object,
default: null
},
conflicts: {
type: Array,
default: () => []
},
conflictCount: {
type: Number,
default: 0
},
airPlatforms: {
type: Array,
default: () => []
},
seaPlatforms: {
type: Array,
default: () => []
},
groundPlatforms: {
type: Array,
default: () => []
}
},
data() {
return {
activePlatformTab: 'air'
}
},
methods: {
handleHide() {
this.$emit('hide')
},
handleSelectRoute(route) {
this.$emit('select-route', route)
},
handleOpenRouteDialog(route) {
this.$emit('open-route-dialog', route)
},
handleOpenWaypointDialog(point) {
this.$emit('open-waypoint-dialog', point)
},
handleAddWaypoint() {
this.$emit('add-waypoint')
},
handleCancelRoute() {
this.$emit('cancel-route')
},
handleViewConflict(conflict) {
this.$emit('view-conflict', conflict)
},
handleResolveConflict(conflict) {
this.$emit('resolve-conflict', conflict)
},
handleRunConflictCheck() {
this.$emit('run-conflict-check')
},
handleOpenPlatformDialog(platform) {
this.$emit('open-platform-dialog', platform)
}
}
}
</script>
<style scoped>
/* 右侧外部隐藏按钮 */
.right-external-hide-btn {
position: absolute;
top: 80px;
right: 20px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #008aff;
font-size: 18px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
z-index: 85;
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
transition: all 0.3s;
backdrop-filter: blur(5px);
}
.right-external-hide-btn:hover {
color: #0066cc;
background: rgba(0, 138, 255, 0.2);
transform: scale(1.1);
}
.right-external-hide-btn.hidden {
opacity: 0;
transform: translateX(100%);
pointer-events: none;
}
/* 右侧浮动面板 - 蓝色主题 */
.floating-right-panel {
position: absolute;
top: 70px;
right: 20px;
width: 300px;
border-radius: 0 8px 8px 8px;
z-index: 90;
color: #333;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 138, 255, 0.2);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
transition: all 0.3s ease;
opacity: 1;
transform: translateX(0);
}
.floating-right-panel.hidden {
opacity: 0;
transform: translateX(100%);
pointer-events: none;
}
.tab-content {
padding: 15px;
max-height: 500px;
overflow-y: auto;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
}
.section {
margin-bottom: 20px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #008aff;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid rgba(0, 138, 255, 0.2);
}
.route-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.route-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: rgba(255, 255, 255, 0.8);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
position: relative;
}
.route-item:hover {
background: rgba(0, 138, 255, 0.1);
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
}
.route-item.selected {
background: rgba(0, 138, 255, 0.15);
border-color: rgba(0, 138, 255, 0.3);
box-shadow: 0 2px 10px rgba(0, 138, 255, 0.25);
}
.route-info {
flex: 1;
}
.route-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.route-meta {
font-size: 12px;
color: #999;
}
.route-actions {
display: flex;
gap: 8px;
}
.route-actions i {
cursor: pointer;
color: #008aff;
font-size: 14px;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.route-actions i:hover {
background: rgba(0, 138, 255, 0.1);
transform: scale(1.2);
}
.waypoint-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.waypoint-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: rgba(255, 255, 255, 0.8);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
}
.waypoint-item:hover {
background: rgba(0, 138, 255, 0.1);
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
}
.waypoint-info {
flex: 1;
}
.waypoint-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.waypoint-meta {
font-size: 12px;
color: #999;
}
.waypoint-actions {
display: flex;
gap: 8px;
}
.waypoint-actions i {
cursor: pointer;
color: #008aff;
font-size: 14px;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.waypoint-actions i:hover {
background: rgba(0, 138, 255, 0.1);
transform: scale(1.2);
}
.action-buttons {
display: flex;
gap: 10px;
padding: 10px 0;
}
.conflict-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.conflict-item {
background: rgba(255, 255, 255, 0.8);
border-radius: 6px;
padding: 12px;
border: 1px solid rgba(245, 108, 108, 0.2);
transition: all 0.3s;
}
.conflict-item:hover {
background: rgba(245, 108, 108, 0.1);
box-shadow: 0 2px 8px rgba(245, 108, 108, 0.15);
}
.conflict-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.conflict-title {
flex: 1;
font-weight: 600;
color: #f56c6c;
}
.conflict-details {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 10px;
}
.detail-item {
display: flex;
gap: 8px;
font-size: 13px;
}
.detail-item .label {
color: #999;
min-width: 70px;
}
.detail-item .value {
color: #333;
font-weight: 500;
}
.conflict-actions {
display: flex;
gap: 10px;
}
.no-conflict {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
padding: 40px 20px;
color: #999;
}
.platform-categories {
height: 100%;
}
.platform-list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 450px;
overflow-y: auto;
}
.platform-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: rgba(255, 255, 255, 0.8);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
}
.platform-item:hover {
background: rgba(0, 138, 255, 0.1);
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
}
.platform-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.platform-info {
flex: 1;
}
.platform-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.platform-type {
font-size: 12px;
color: #999;
}
.platform-status {
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-dot.online {
background: #67c23a;
box-shadow: 0 0 6px rgba(103, 194, 58, 0.6);
}
.status-dot.offline {
background: #999;
}
.status-dot.operating {
background: #008aff;
animation: pulse 2s infinite;
box-shadow: 0 0 10px rgba(0, 138, 255, 0.8);
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.blue-btn {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
border: 1px solid rgba(0, 138, 255, 0.3);
}
.blue-btn:hover {
background: rgba(0, 138, 255, 0.2);
border-color: rgba(0, 138, 255, 0.5);
}
.blue-text-btn {
color: #008aff;
}
.blue-text-btn:hover {
color: #0066cc;
}
.blue-badge {
background: rgba(245, 108, 108, 0.1);
color: #f56c6c;
border: 1px solid rgba(245, 108, 108, 0.3);
}
.blue-tabs >>> .el-tabs__item {
color: #666;
transition: all 0.3s;
}
.blue-tabs >>> .el-tabs__item:hover {
color: #008aff;
}
.blue-tabs >>> .el-tabs__item.is-active {
color: #008aff;
font-weight: 600;
}
.blue-tabs >>> .el-tabs__active-bar {
background-color: #008aff;
box-shadow: 0 0 6px rgba(0, 138, 255, 0.5);
}
.blue-tabs >>> .el-tabs__nav-wrap::after {
background-color: rgba(0, 138, 255, 0.3);
}
.blue-success {
color: #67c23a;
}
.blue-warning {
color: #e6a23c;
}
</style>

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

@ -0,0 +1,768 @@
<template>
<div class="floating-header">
<div class="header-left">
<div class="system-title">
<i class="el-icon-s-promotion mr-2 logo-icon"></i>
<span class="title-text blue-title">联合任务筹划系统</span>
</div>
<!-- 顶部导航菜单 -->
<div class="top-nav-menu">
<div
v-for="item in topNavItems"
:key="item.id"
class="top-nav-item"
:class="{ active: activeTopNav === item.id }"
@click="selectTopNav(item)"
>
<i :class="item.icon" class="nav-icon"></i>
<span class="nav-text">{{ item.name }}</span>
<!-- 文件下拉菜单 -->
<el-dropdown
v-if="item.id === 'file'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="savePlan">保存</el-dropdown-item>
<!-- 导入二级菜单 -->
<el-dropdown-item class="submenu-item">
<span>导入</span>
<el-dropdown
trigger="hover"
placement="right-start"
class="submenu-dropdown"
>
<div class="submenu-trigger"></div>
<el-dropdown-menu slot="dropdown" class="submenu">
<el-dropdown-item @click.native="importPlanFile">导入计划</el-dropdown-item>
<el-dropdown-item @click.native="importACD">导入ACD</el-dropdown-item>
<el-dropdown-item @click.native="importATO">导入ATO</el-dropdown-item>
<el-dropdown-item @click.native="importLayer">导入图层</el-dropdown-item>
<el-dropdown-item @click.native="importRoute">导入航线</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item @click.native="exportPlan">导出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 编辑下拉菜单 -->
<el-dropdown
v-if="item.id === 'edit'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="routeEdit">航线编辑</el-dropdown-item>
<el-dropdown-item @click.native="militaryMarking">军事标绘</el-dropdown-item>
<el-dropdown-item @click.native="iconEdit">图标编辑</el-dropdown-item>
<el-dropdown-item @click.native="attributeEdit">属性修改</el-dropdown-item>
<!-- 推演编辑二级菜单 -->
<el-dropdown-item class="submenu-item">
<span>推演编辑</span>
<el-dropdown
trigger="hover"
placement="right-start"
class="submenu-dropdown"
>
<div class="submenu-trigger"></div>
<el-dropdown-menu slot="dropdown" class="submenu">
<el-dropdown-item @click.native="timeSettings">时间设置</el-dropdown-item>
<el-dropdown-item @click.native="aircraftSettings">机型设置</el-dropdown-item>
<el-dropdown-item @click.native="keyEventEdit">关键事件编辑</el-dropdown-item>
<el-dropdown-item @click.native="missileLaunch">导弹发射</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 视图下拉菜单 -->
<el-dropdown
v-if="item.id === 'view'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="toggle2D3D">2D/3D切换</el-dropdown-item>
<el-dropdown-item @click.native="toggleRuler">显示/隐藏标尺</el-dropdown-item>
<el-dropdown-item @click.native="toggleGrid">网格</el-dropdown-item>
<el-dropdown-item @click.native="toggleScale">比例尺</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 地图下拉菜单 -->
<el-dropdown
v-if="item.id === 'map'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="loadTerrain">加载/切换地形</el-dropdown-item>
<el-dropdown-item @click.native="changeProjection">投影</el-dropdown-item>
<el-dropdown-item @click.native="loadAeroChart">航空图</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 空域下拉菜单 -->
<el-dropdown
v-if="item.id === 'airspace'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="powerZone">威力区</el-dropdown-item>
<el-dropdown-item @click.native="threatZone">威胁区</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 工具下拉菜单 -->
<el-dropdown
v-if="item.id === 'tools'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="routeCalculation">航线计算</el-dropdown-item>
<el-dropdown-item @click.native="conflictDisplay">冲突显示</el-dropdown-item>
<el-dropdown-item @click.native="dataMaterials">数据资料</el-dropdown-item>
<el-dropdown-item @click.native="coordinateConversion">坐标换算</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 选项下拉菜单 -->
<el-dropdown
v-if="item.id === 'options'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<!-- 设置二级菜单 -->
<el-dropdown-item class="submenu-item">
<span>设置</span>
<el-dropdown
trigger="hover"
placement="right-start"
class="submenu-dropdown"
>
<div class="submenu-trigger"></div>
<el-dropdown-menu slot="dropdown" class="submenu">
<el-dropdown-item @click.native="pageLayout">页面布局</el-dropdown-item>
<el-dropdown-item @click.native="dataStoragePath">数据存储路径</el-dropdown-item>
<el-dropdown-item @click.native="externalParams">外部参数</el-dropdown-item>
<el-dropdown-item @click.native="toggleAirport">显示/隐藏机场</el-dropdown-item>
<el-dropdown-item @click.native="toggleLandmark">显示/隐藏地标</el-dropdown-item>
<el-dropdown-item @click.native="toggleRoute">显示/隐藏航线</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item @click.native="systemDescription">系统说明</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 收藏下拉菜单 -->
<el-dropdown
v-if="item.id === 'favorites'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
>
<div class="dropdown-trigger"></div>
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="layerFavorites">图层收藏</el-dropdown-item>
<el-dropdown-item @click.native="routeFavorites">航线收藏</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
<div class="header-right">
<!-- 作战信息区域 -->
<div class="combat-info-group">
<!-- 房间编号 -->
<div class="info-box">
<i class="el-icon-office-building info-icon"></i>
<div class="info-content">
<div class="info-label">房间编号</div>
<div class="info-value">{{ roomCode }}</div>
</div>
</div>
<!-- 在线人数 -->
<div class="info-box" @click="showOnlineMembersDialog">
<i class="el-icon-user info-icon"></i>
<div class="info-content">
<div class="info-label">在线人数</div>
<div class="info-value">{{ onlineCount }}</div>
</div>
</div>
<!-- 作战时间 -->
<div class="info-box">
<i class="el-icon-timer info-icon"></i>
<div class="info-content">
<div class="info-label">作战时间</div>
<div class="info-value">{{ combatTime }}</div>
</div>
</div>
<!-- 天文时间 -->
<div class="info-box">
<i class="el-icon-sunny info-icon"></i>
<div class="info-content">
<div class="info-label">天文时间</div>
<div class="info-value">{{ astroTime }}</div>
</div>
</div>
</div>
<!-- 用户状态区域 -->
<div class="user-status-area">
<!-- 用户头像 -->
<el-avatar :size="32" :src="userAvatar" class="user-avatar" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TopHeader',
props: {
roomCode: {
type: String,
default: 'JTF-7-ALPHA'
},
onlineCount: {
type: Number,
default: 30
},
combatTime: {
type: String,
default: 'K+01:30:45'
},
astroTime: {
type: String,
default: ''
},
userAvatar: {
type: String,
default: 'https://cube.elemecdn.com/0/88dd03f9bf287d08f58fbcf58fddbf4a8c6/avatar.png'
}
},
data() {
return {
activeTopNav: 'file',
topNavItems: [
{ id: 'file', name: '文件', icon: 'el-icon-document' },
{ id: 'edit', name: '编辑', icon: 'el-icon-edit' },
{ id: 'view', name: '视图', icon: 'el-icon-view' },
{ id: 'map', name: '地图', icon: 'el-icon-map-location' },
{ id: 'airspace', name: '空域', icon: 'el-icon-s-grid' },
{ id: 'tools', name: '工具', icon: 'el-icon-setting' },
{ id: 'options', name: '选项', icon: 'el-icon-s-tools' },
{ id: 'favorites', name: '收藏', icon: 'el-icon-star-on' }
]
}
},
methods: {
selectTopNav(item) {
this.$emit('select-nav', item)
},
//
savePlan() {
this.$emit('save-plan')
},
importPlanFile() {
this.$emit('import-plan-file')
},
importACD() {
this.$emit('import-acd')
},
importATO() {
this.$emit('import-ato')
},
importLayer() {
this.$emit('import-layer')
},
importRoute() {
this.$emit('import-route')
},
exportPlan() {
this.$emit('export-plan')
},
//
routeEdit() {
this.$emit('route-edit')
},
militaryMarking() {
this.$emit('military-marking')
},
iconEdit() {
this.$emit('icon-edit')
},
attributeEdit() {
this.$emit('attribute-edit')
},
timeSettings() {
this.$emit('time-settings')
},
aircraftSettings() {
this.$emit('aircraft-settings')
},
keyEventEdit() {
this.$emit('key-event-edit')
},
missileLaunch() {
this.$emit('missile-launch')
},
//
toggle2D3D() {
this.$emit('toggle-2d-3d')
},
toggleRuler() {
this.$emit('toggle-ruler')
},
toggleGrid() {
this.$emit('toggle-grid')
},
toggleScale() {
this.$emit('toggle-scale')
},
//
loadTerrain() {
this.$emit('load-terrain')
},
changeProjection() {
this.$emit('change-projection')
},
loadAeroChart() {
this.$emit('load-aero-chart')
},
//
powerZone() {
this.$emit('power-zone')
},
threatZone() {
this.$emit('threat-zone')
},
//
routeCalculation() {
this.$emit('route-calculation')
},
conflictDisplay() {
this.$emit('conflict-display')
},
dataMaterials() {
this.$emit('data-materials')
},
coordinateConversion() {
this.$emit('coordinate-conversion')
},
//
pageLayout() {
this.$emit('page-layout')
},
dataStoragePath() {
this.$emit('data-storage-path')
},
externalParams() {
this.$emit('external-params')
},
toggleAirport() {
this.$emit('toggle-airport')
},
toggleLandmark() {
this.$emit('toggle-landmark')
},
toggleRoute() {
this.$emit('toggle-route')
},
systemDescription() {
this.$emit('system-description')
},
//
layerFavorites() {
this.$emit('layer-favorites')
},
routeFavorites() {
this.$emit('route-favorites')
},
showOnlineMembersDialog() {
this.$emit('show-online-members')
}
}
}
</script>
<style scoped>
/* 顶部导航栏 - 最终优化设计 */
.floating-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
backdrop-filter: blur(15px);
background: rgba(255, 255, 255, 0.85);
border-bottom: 1px solid rgba(0, 138, 255, 0.2);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 25px;
flex: 1;
}
.system-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
min-width: 180px;
}
.system-title i {
font-size: 24px;
color: #008aff;
}
.blue-title {
color: #008aff !important;
}
/* 顶部导航菜单 - 优化为简洁文字效果 */
.top-nav-menu {
display: flex;
gap: 0;
flex: 1;
overflow-x: auto;
max-width: 800px;
padding: 5px 0;
scrollbar-width: thin;
}
.top-nav-menu::-webkit-scrollbar {
height: 3px;
}
.top-nav-menu::-webkit-scrollbar-track {
background: rgba(0, 138, 255, 0.1);
border-radius: 2px;
}
.top-nav-menu::-webkit-scrollbar-thumb {
background: rgba(0, 138, 255, 0.3);
border-radius: 2px;
}
.top-nav-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: #333;
transition: all 0.3s;
border-radius: 4px;
white-space: nowrap;
min-width: 60px;
justify-content: center;
margin: 0 1px;
position: relative;
flex-shrink: 0;
}
.top-nav-item:hover {
color: #008aff;
background: rgba(0, 138, 255, 0.05);
}
.top-nav-item.active {
color: #008aff;
font-weight: 700;
}
/* 移除了蓝色指示条 */
.top-nav-item.active::after {
display: none;
}
.nav-icon {
font-size: 16px;
transition: all 0.3s;
}
.top-nav-item:hover .nav-icon,
.top-nav-item.active .nav-icon {
transform: scale(1.1);
}
.nav-text {
font-size: 13px;
font-weight: 600;
transition: all 0.3s;
}
/* 下拉菜单样式 */
.file-dropdown {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.dropdown-trigger {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-dropdown-menu {
margin-top: 5px;
margin-bottom: 0;
border: none;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(15px);
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 138, 255, 0.2);
padding: 0;
min-width: auto;
width: fit-content;
}
.file-dropdown-menu .el-dropdown-menu__item {
padding: 8px 16px;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
margin: 0;
}
.file-dropdown-menu .el-dropdown-menu__item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
}
.file-dropdown-menu .el-dropdown-menu__item:not(:last-child) {
border-bottom: 1px solid rgba(0, 138, 255, 0.1);
}
.file-dropdown-menu .el-dropdown-menu__item:last-child {
border-bottom: none;
}
.submenu-item {
position: relative;
}
.submenu-dropdown {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.submenu-trigger {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.submenu {
margin-left: 5px;
margin-bottom: 0;
border: none;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(15px);
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 138, 255, 0.2);
padding: 0;
min-width: auto;
width: fit-content;
}
.submenu .el-dropdown-menu__item {
padding: 8px 16px;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
margin: 0;
}
.submenu .el-dropdown-menu__item:hover {
background: rgba(0, 138, 255, 0.1);
color: #008aff;
}
.submenu .el-dropdown-menu__item:not(:last-child) {
border-bottom: 1px solid rgba(0, 138, 255, 0.1);
}
.submenu .el-dropdown-menu__item:last-child {
border-bottom: none;
}
/* 右侧信息区域 */
.header-right {
display: flex;
align-items: center;
gap: 20px;
}
.combat-info-group {
display: flex;
align-items: center;
gap: 15px;
}
.info-box {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.6);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
}
.info-box:hover {
background: rgba(0, 138, 255, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.2);
}
.combat-info-group .info-box:nth-child(3) .info-value {
color: #409EFF;
font-weight: 600;
}
.combat-info-group .info-box:nth-child(4) .info-value {
color: #67c23a;
font-weight: 600;
}
.info-icon {
font-size: 20px;
color: #008aff;
}
.info-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.info-label {
font-size: 11px;
color: #666;
}
.info-value {
font-size: 13px;
color: #333;
font-weight: 600;
}
.user-status-area {
display: flex;
align-items: center;
}
.user-avatar {
cursor: pointer;
transition: all 0.3s;
}
.user-avatar:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
</style>

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

File diff suppressed because it is too large
Loading…
Cancel
Save