Files
-----/src/App.vue
2025-12-19 16:15:15 +08:00

2136 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<!-- 顶部导航栏 -->
<header class="top-header">
<div class="header-left">
<span class="demo-tag">演示界面电脑端</span>
<!-- <span class="region-name">傲来国</span> -->
</div>
<div class="header-center">
<h1 class="app-title">天枢视界</h1>
</div>
<div class="header-right">
<span class="datetime">{{ currentDateTime }}</span>
</div>
</header>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 地图容器全屏 -->
<div class="map-container">
<div id="map-container" ref="mapContainer"></div>
<!-- 左侧信息面板覆盖层 -->
<aside class="left-panel">
<!-- 实时画面 -->
<div class="info-panel">
<!-- <div class="panel-header">
<span class="panel-title">实时画面</span>
<span class="panel-count">35</span>
</div>
<div class="video-grid">
<div v-for="i in 12" :key="i" class="video-item">
<div class="video-placeholder">
<span class="video-label">{{ i }}</span>
</div>
</div>
</div> -->
</div>
<!-- 巡检航线 -->
<div class="info-panel">
<!-- <div class="panel-header">
<span class="panel-title">巡检航线</span>
<span class="panel-count">35</span>
</div>
<div class="status-list">
<div class="status-item">
<img src="./assets/image/Completed.png" alt="已完成" class="status-icon" />
<span class="status-number">13</span>
<span class="status-text">已完成</span>
</div>
<div class="status-item">
<img src="./assets/image/in_progress.png" alt="进行中" class="status-icon" />
<span class="status-number">23</span>
<span class="status-text">进行中</span>
</div>
<div class="status-item">
<img src="./assets/image/to_be_started.png" alt="待开始" class="status-icon" />
<span class="status-number">11</span>
<span class="status-text">待开始</span>
</div>
</div> -->
</div>
<!-- 识别场景 -->
<div class="info-panel">
<!-- <div class="panel-header">
<span class="panel-title">识别场景</span>
<span class="panel-count">35</span>
</div>
<div class="status-list">
<div class="status-item">
<img src="./assets/image/fire.png" alt="火点" class="status-icon" />
<span class="status-number">13</span>
<span class="status-text">火点</span>
</div>
<div class="status-item">
<img src="./assets/image/personnel.png" alt="人员" class="status-icon" />
<span class="status-number">23</span>
<span class="status-text">人员</span>
</div>
<div class="status-item">
<img src="./assets/image/smoke.png" alt="烟雾" class="status-icon" />
<span class="status-number">11</span>
<span class="status-text">烟雾</span>
</div>
</div> -->
</div>
</aside>
<!-- 控制面板 -->
<div class="control-panel" v-show="showControlPanel">
<div class="panel-title">电子围栏控制</div>
<div>
<!-- 地图类型切换 -->
<div class="control-group">
<div class="draw-type-title">地图类型</div>
<div class="draw-buttons">
<el-button @click="switchMapType('normal')" :type="mapType === 'normal' ? 'primary' : 'default'"
size="default">
普通地图
</el-button>
<el-button @click="switchMapType('satellite')" :type="mapType === 'satellite' ? 'primary' : 'default'"
size="default">
卫星地图
</el-button>
</div>
</div>
<!-- 绘制类型选择 -->
<div class="control-group">
<div class="draw-type-title">选择绘制类型</div>
<div class="draw-buttons">
<el-button @click="startDrawPolygon" :type="currentDrawType === 'polygon' ? 'primary' : 'default'"
size="default">
多边形
</el-button>
<el-button @click="startDrawRectangle" :type="currentDrawType === 'rectangle' ? 'primary' : 'default'"
size="default">
矩形
</el-button>
<el-button @click="startDrawCircle" :type="currentDrawType === 'circle' ? 'primary' : 'default'"
size="default">
圆形
</el-button>
</div>
</div>
<!-- 无人机状态面板 -->
<div class="drone-status-panel">
<div class="status-title">无人机状态</div>
<div class="status-item">
<span class="status-label">连接状态:</span>
<el-tag :type="wsConnected ? 'success' : 'danger'" size="small">
{{ wsConnected ? '已连接' : '未连接' }}
</el-tag>
</div>
<div class="status-item" v-if="droneData">
<span class="status-label">位置:</span>
<span class="status-value">{{ droneData.latitude?.toFixed(6) }}, {{ droneData.longitude?.toFixed(6)
}}</span>
</div>
<div class="status-item" v-if="droneData">
<span class="status-label">高度:</span>
<span class="status-value">{{ droneData.altitude?.toFixed(1) }}m</span>
</div>
<div class="status-item" v-if="droneData">
<span class="status-label">速度:</span>
<span class="status-value">{{ droneData.speed?.toFixed(1) }}m/s</span>
</div>
<div class="status-item" v-if="droneData">
<span class="status-label">电量:</span>
<span class="status-value">{{ droneData.battery?.toFixed(0) }}%</span>
</div>
<div class="status-item">
<span class="status-label">围栏状态:</span>
<el-tag :type="isInFence ? 'success' : 'danger'" size="small">
{{ isInFence ? '围栏内' : '围栏外' }}
</el-tag>
</div>
<el-button @click="handleTakeoff" :type="isFlying ? 'warning' : 'success'" size="small"
style="width: 100%; margin-top: 8px;" :disabled="isFlying">
{{ isFlying ? '飞行中' : '起飞' }}
</el-button>
<el-button @click="toggleWebSocket" :type="wsConnected ? 'danger' : 'primary'" size="small"
style="width: 100%; margin-top: 8px;margin-left: 0;">
{{ wsConnected ? '断开连接' : '开始连接' }}
</el-button>
</div>
<!-- 围栏列表 -->
<div class="fence-list" v-if="fences.length > 0">
<div class="list-title">已绘制围栏 ({{ fences.length }})</div>
<div v-for="fence in fences" :key="fence.id" class="fence-item">
<span class="fence-info">
<span class="fence-name">{{ fence.name || '未命名围栏' }}</span>
</span>
<div class="fence-actions">
<el-button @click="uploadFence(fence.id)" type="primary" size="small">
上传
</el-button>
<el-button @click="removeFence(fence.id)" type="danger" size="small">
删除
</el-button>
</div>
</div>
<el-button @click="clearAllFences" type="danger" size="default" style="width: 100%; margin-top: 8px;">
清除所有围栏
</el-button>
</div>
</div>
</div>
<!-- 右侧信息面板覆盖层 -->
<aside class="right-panel">
<!-- 区域管理 -->
<div class="info-panel">
<!-- <div class="panel-header">
<span class="panel-title">区域管理</span>
<span class="panel-count">12</span>
</div>
<div class="area-preview">
<img src="./assets/image/regional_management.png" alt="区域管理" class="area-image" />
</div> -->
</div>
<!-- 出勤设备 -->
<div class="info-panel">
<!-- <div class="panel-header">
<span class="panel-title">出勤设备</span>
<span class="panel-count">35</span>
</div>
<div class="status-list">
<div class="status-item">
<div class="device-icon device-standby"></div>
<span class="status-number">25</span>
<span class="status-text">待机</span>
</div>
<div class="status-item">
<div class="device-icon device-duty"></div>
<span class="status-number">13</span>
<span class="status-text">在勤</span>
</div>
<div class="status-item">
<div class="device-icon device-charging"></div>
<span class="status-number">12</span>
<span class="status-text">充电</span>
</div>
</div> -->
</div>
<!-- 执行任务 -->
<div class="info-panel">
<!-- <div class="panel-header">
<span class="panel-title">执行任务</span>
<span class="panel-count">12</span>
</div>
<div class="task-progress">
<div class="progress-item">
<div class="progress-bar">
<div class="progress-fill progress-completed" style="width: 13%"></div>
</div>
<span class="progress-text">13% 已完成</span>
</div>
<div class="progress-item">
<div class="progress-bar">
<div class="progress-fill progress-in-progress" style="width: 48%"></div>
</div>
<span class="progress-text">48% 进行中</span>
</div>
<div class="progress-item">
<div class="progress-bar">
<div class="progress-fill progress-pending" style="width: 60%"></div>
</div>
<span class="progress-text">60% 待开始</span>
</div>
</div> -->
</div>
</aside>
</div>
</div>
<!-- 底部导航栏 -->
<footer class="bottom-footer">
<div class="footer-center">
<div class="footer-icons">
<img src="./assets/image/d1.png" class="arrow-img" />
<img src="./assets/image/i1.png" class="icon-img" />
<img src="./assets/image/i2.png" class="icon-img" />
<img src="./assets/image/i3.png" class="icon-img" @click="handleFooterIconClick(4)"/>
<img src="./assets/image/i4.png" class="icon-img" />
<img src="./assets/image/i5.png" class="icon-img" />
<img src="./assets/image/d2.png" class="arrow-img" />
</div>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import { ElMessage, ElMessageBox } from 'element-plus'
import { WebSocketClient, MockWebSocketDataGenerator } from './utils/websocket.js'
import droneImage from './assets/image/wrj.png'
import icon1 from './assets/image/1.png'
import icon2 from './assets/image/2.png'
import icon3 from './assets/image/3.png'
import icon4 from './assets/image/4.png'
import icon5 from './assets/image/5.png'
import icon6 from './assets/image/6.png'
import icon7 from './assets/image/7.png'
import { getConfig } from ".//utils/request";
import gcoord from 'gcoord';
import DroneCtrl, {
UploadAreaReq
} from 'DroneCtrl';
const ctrl = new DroneCtrl(getConfig());
// 当前日期时间
const currentDateTime = ref('')
const updateDateTime = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
currentDateTime.value = `${year}${month}${day}${hours}:${minutes}`
}
// 地图相关
const mapContainer = ref(null)
let map = null
let AMap = null
let mouseTool = null
const mapType = ref('satellite') // 'normal' | 'satellite'
const dronePath = ref([]) // 无人机轨迹点集合
let droneTrackLine = null
const drone2Path = ref([]) // 无人机2轨迹点集合
let drone2TrackLine = null
const MAX_TRACK_POINTS = 2000
// 围栏相关状态
const isDrawing = ref(false)
const currentDrawType = ref(null) // 'polygon' | 'rectangle' | 'circle'
const fences = ref([])
const showControlPanel = ref(true) // 控制面板显示/隐藏
// 无人机相关状态
const wsConnected = ref(false)
const droneData = ref(null)
const isFlying = ref(false)
const droneMarker = ref(null)
const drone2Marker = ref(null)
let wsClient = null
let mockDataGenerator = null
const isInFence = ref(true)
const warningShown = ref(false) // 防止重复警告
// 高德地图配置
const mapKey = '430d442aef846341695d0ceba4d4793b'
// 初始化地图
const initMap = async () => {
try {
// 加载高德地图API需要加载MouseTool插件
AMap = await AMapLoader.load({
key: mapKey,
version: '2.0',
plugins: ['AMap.MouseTool', 'AMap.Polygon', 'AMap.Rectangle', 'AMap.Circle', 'AMap.Marker', 'AMap.Icon', 'AMap.Text', 'AMap.TileLayer', 'AMap.TileLayer.Satellite', 'AMap.Polyline']
})
// 创建地图实例
map = new AMap.Map('map-container', {
viewMode: '3D',
zoom: 13,
// center: [116.397428, 39.90923], // 默认北京天安门
mapStyle: 'amap://styles/normal' // 默认普通地图
})
// 初始化卫星图层默认显示因为mapType默认是satellite
const satelliteLayer = new AMap.TileLayer.Satellite()
map.add(satelliteLayer)
satelliteLayer.show() // 默认显示卫星图层
// 保存图层引用以便切换
window.satelliteLayer = satelliteLayer
// 初始化无人机轨迹图层
initDroneTrack()
AMap.plugin('AMap.Geolocation', function () {
// 插件加载完成后才能使用
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
convert: true,
showButton: false
});
map.addControl(geolocation);
// 获取当前位置
geolocation.getCurrentPosition(function (status, result) {
if (status === 'complete') {
console.log('定位成功:', result.position.lng, result.position.lat);
} else {
console.error('定位失败:', result);
}
});
});
// 创建鼠标工具实例
mouseTool = new AMap.MouseTool(map)
// 监听绘制完成事件
mouseTool.on('draw', (event) => {
handleDrawComplete(event)
})
// 地图加载完成事件
map.on('complete', () => {
console.log('地图加载完成')
// 地图加载完成后初始化WebSocket
// initWebSocket()
})
console.log('地图初始化成功')
} catch (error) {
console.error('地图初始化失败:', error)
}
}
// 切换地图类型
const switchMapType = (type) => {
if (!map) return
mapType.value = type
if (type === 'satellite') {
// 切换到卫星地图
map.setMapStyle('amap://styles/normal')
// 显示卫星图层
if (window.satelliteLayer) {
window.satelliteLayer.show()
} else {
const satelliteLayer = new AMap.TileLayer.Satellite()
map.add(satelliteLayer)
window.satelliteLayer = satelliteLayer
}
ElMessage.success('已切换到卫星地图')
} else if (type === 'normal') {
// 切换到普通地图
map.setMapStyle('amap://styles/normal')
// 隐藏卫星图层
if (window.satelliteLayer) {
window.satelliteLayer.hide()
}
ElMessage.success('已切换到普通地图')
}
}
// 切换控制面板显示/隐藏
const toggleControlPanel = () => {
showControlPanel.value = !showControlPanel.value
}
// 处理底部图标点击
const handleFooterIconClick = (index) => {
if (index === 4) {
// 第4个图标显示/隐藏控制面板
toggleControlPanel()
}
// 其他图标的功能可以在这里添加
console.log('点击了图标:', index)
}
// 性能优化:节流处理数据更新
let lastUpdateTime = 0
const UPDATE_INTERVAL = 1000 // 3秒更新一次大幅降低更新频率
let conn = null;
// 初始化WebSocket连接
const initWebSocket = () => {
// 使用模拟数据生成器因为目前没有真实WebSocket服务器
// mockDataGenerator = new MockWebSocketDataGenerator({
// interval: 500, // 内部500ms生成一次数据用于平滑
// startLat: 39.90923, // 北京天安门附近
// startLng: 116.397428,
// speed: 0.00005 // 移动速度
// })
// // 监听数据(使用节流,大幅降低更新频率)
// mockDataGenerator.start((data) => {
// const now = Date.now()
// // 只更新数据状态,不触发地图更新
// droneData.value = data
// // 节流:降低地图更新频率
// if (now - lastUpdateTime >= UPDATE_INTERVAL) {
// lastUpdateTime = now
// handleDroneData(data)
// }
// })
wsConnected.value = true
ElMessage.success('无人机数据连接成功')
}
// 起飞处理
const handleTakeoff = () => {
// 检查是否已有围栏
// if (fences.value.length === 0) {
// ElMessage.warning('请先绘制电子围栏后再起飞')
// return
// }
// // 如果未连接,先连接
// if (!wsConnected.value) {
// initWebSocket()
// }
ctrl.TakeoffAndAutoExecTask({});
// 设置飞行状态
isFlying.value = true
ElMessage.success('无人机已起飞')
}
// 切换WebSocket连接
const toggleWebSocket = () => {
if (wsConnected.value) {
// 断开连接
if (mockDataGenerator) {
mockDataGenerator.stop()
mockDataGenerator = null
}
if (wsClient) {
wsClient.close()
wsClient = null
}
wsConnected.value = false
isFlying.value = false // 断开连接时重置飞行状态
clearDroneTrack()
ElMessage.info('已断开连接')
conn.close();
} else {
// 连接
initWebSocket()
conn = ctrl.Message();
conn.onDroneGPS = (data) => {
// console.log(data);
const drone1 = data.drone_1;
const drone2 = data.drone_2;
const point = gcoord.transform(
[drone1.longitude, drone1.latitude],
gcoord.WGS84, // 当前坐标系
gcoord.GCJ02, // 目标坐标系
);
const point2 = gcoord.transform(
[drone2.longitude, drone2.latitude],
gcoord.WGS84, // 当前坐标系
gcoord.GCJ02, // 目标坐标系
);
console.log(point, point2);
updateDroneMarker(point[0], point[1], point2[0], point2[1], drone1.heading);
// updateDrone2Marker(point2[0], point2[1], drone2.heading);
};
}
}
// 处理无人机数据(性能优化版本)
const handleDroneData = (data) => {
if (data.type !== 'drone_position') return
// 使用requestAnimationFrame优化渲染避免阻塞
if (typeof requestAnimationFrame !== 'undefined') {
requestAnimationFrame(() => {
updateDroneMarker(data.longitude, data.latitude, data.heading)
// 检测是否在围栏内(如果正在绘制,跳过检测,避免干扰绘制操作)
if (!isDrawing.value) {
checkFenceStatus(data.longitude, data.latitude)
}
})
} else {
// 降级方案
updateDroneMarker(data.longitude, data.latitude, data.heading)
if (!isDrawing.value) {
checkFenceStatus(data.longitude, data.latitude)
}
}
}
// 更新无人机marker
const updateDroneMarker = (lng, lat, lng2, lat2, heading = 0) => {
if (!map || !AMap) return
if (!droneMarker.value) {
// 创建无人机图标
const icon = new AMap.Icon({
image: droneImage,
size: new AMap.Size(0, 100),
imageSize: new AMap.Size(100, 100),
imageOffset: new AMap.Pixel(0, 0)
})
// 创建marker
droneMarker.value = new AMap.Marker({
position: [lng, lat],
icon: icon,
zIndex: 100,
title: '侦查机',
offset: new AMap.Pixel(-20, -20),
// 优化:禁用动画,减少性能消耗
animation: 'AMAP_ANIMATION_NONE'
})
map.add(droneMarker.value)
drone2Marker.value = new AMap.Marker({
position: [lng, lat],
icon: icon,
zIndex: 100,
title: '投弹机',
offset: new AMap.Pixel(-20, -20),
// 优化:禁用动画,减少性能消耗
animation: 'AMAP_ANIMATION_NONE'
})
map.add(drone2Marker.value)
} else {
// 更新位置
droneMarker.value.setPosition([lng, lat])
drone2Marker.value.setPosition([lng2, lat2])
// 更新旋转角度如果有heading数据
if (heading !== undefined) {
droneMarker.value.setAngle(heading)
drone2Marker.value.setAngle(heading)
}
}
// 更新飞行轨迹
updateDroneTrack(lng, lat)
updateDrone2Track(lng2, lat2)
// 将地图中心移动到无人机位置(可选,可以注释掉)
map.setCenter([lng, lat])
}
// 初始化无人机轨迹线
const initDroneTrack = () => {
if (!map || !AMap) return
// 如果已有轨迹线,先移除
if (droneTrackLine) {
map.remove(droneTrackLine)
}
droneTrackLine = new AMap.Polyline({
map,
path: [],
showDir: true,
isOutline: true,
outlineColor: 'rgba(0,0,0,0.3)',
borderWeight: 2,
strokeColor: '#00e0ff',
strokeOpacity: 0.8,
strokeWeight: 4,
lineJoin: 'round',
lineCap: 'round',
zIndex: 90
})
}
const initDrone2Track = () => {
if (!map || !AMap) return
// 如果已有轨迹线,先移除
if (drone2TrackLine) {
map.remove(drone2TrackLine)
}
drone2TrackLine = new AMap.Polyline({
map,
path: [],
showDir: true,
isOutline: true,
outlineColor: 'rgba(0,0,0,0.3)',
borderWeight: 2,
strokeColor: '#00e0ff',
strokeOpacity: 0.8,
strokeWeight: 4,
lineJoin: 'round',
lineCap: 'round',
zIndex: 90
})
}
// 更新无人机轨迹
const updateDroneTrack = (lng, lat) => {
if (!map || !AMap) return
if (!droneTrackLine) {
initDroneTrack()
}
dronePath.value.push([lng, lat])
if (dronePath.value.length > MAX_TRACK_POINTS) {
dronePath.value.shift()
}
if (droneTrackLine) {
droneTrackLine.setPath(dronePath.value)
}
}
const updateDrone2Track = (lng, lat) => {
if (!map || !AMap) return
if (!drone2TrackLine) {
initDrone2Track()
}
drone2Path.value.push([lng, lat])
if (drone2Path.value.length > MAX_TRACK_POINTS) {
drone2Path.value.shift()
}
if (drone2TrackLine) {
drone2TrackLine.setPath(drone2Path.value)
}
}
// 清空无人机轨迹
const clearDroneTrack = () => {
dronePath.value = []
if (droneTrackLine) {
droneTrackLine.setPath([])
}
}
const clearDrone2Track = () => {
drone2Path.value = []
if (drone2TrackLine) {
drone2TrackLine.setPath([])
}
}
// 检测点是否在多边形内(射线法)
const isPointInPolygon = (point, polygon) => {
const [x, y] = point
let inside = false
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, yi] = polygon[i]
const [xj, yj] = polygon[j]
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
if (intersect) inside = !inside
}
return inside
}
// 检测点是否在矩形内
const isPointInRectangle = (point, bounds) => {
const [lng, lat] = point
const sw = bounds.getSouthWest()
const ne = bounds.getNorthEast()
return lng >= sw.getLng() && lng <= ne.getLng() &&
lat >= sw.getLat() && lat <= ne.getLat()
}
// 检测点是否在圆形内
const isPointInCircle = (point, center, radius) => {
const [lng, lat] = point
const centerLng = center.getLng ? center.getLng() : (center.lng || center[0])
const centerLat = center.getLat ? center.getLat() : (center.lat || center[1])
// 计算距离(使用简单的欧几里得距离,对于小范围足够准确)
const distance = Math.sqrt(
Math.pow((lng - centerLng) * 111320, 2) +
Math.pow((lat - centerLat) * 110540, 2)
)
return distance <= radius
}
// 检测围栏状态
const checkFenceStatus = (lng, lat) => {
if (fences.value.length === 0) {
// 没有围栏时,默认认为在围栏内
isInFence.value = true
warningShown.value = false
return
}
let insideAnyFence = false
// 检查是否在任何围栏内
for (const fence of fences.value) {
let inside = false
if (fence.type === 'polygon') {
// 多边形检测
const path = fence.shape.getPath ? fence.shape.getPath() : fence.path
const points = path.map(p => {
if (Array.isArray(p)) return p
if (p.getLng && p.getLat) return [p.getLng(), p.getLat()]
return [p.lng || p[0], p.lat || p[1]]
})
inside = isPointInPolygon([lng, lat], points)
} else if (fence.type === 'rectangle') {
// 矩形检测
const bounds = fence.shape.getBounds ? fence.shape.getBounds() : fence.bounds
if (bounds) {
inside = isPointInRectangle([lng, lat], bounds)
}
} else if (fence.type === 'circle') {
// 圆形检测
const center = fence.shape.getCenter ? fence.shape.getCenter() : fence.center
const radius = fence.shape.getRadius ? fence.shape.getRadius() : fence.radius
if (center && radius) {
inside = isPointInCircle([lng, lat], center, radius)
}
}
if (inside) {
insideAnyFence = true
break
}
}
const wasInFence = isInFence.value
isInFence.value = insideAnyFence
// 如果从围栏内移动到围栏外,显示警告(异步执行,避免阻塞)
if (wasInFence && !insideAnyFence && !warningShown.value) {
// 使用setTimeout异步执行避免阻塞UI和绘制操作
setTimeout(() => {
showFenceWarning()
warningShown.value = true
}, 0)
} else if (insideAnyFence) {
// 回到围栏内,重置警告状态
warningShown.value = false
}
}
// 显示围栏警告(优化版本:使用非阻塞提示)
let lastWarningTime = 0
const WARNING_INTERVAL = 10000 // 10秒内只显示一次警告避免频繁弹窗
const showFenceWarning = () => {
const now = Date.now()
// 防抖:避免频繁警告
if (now - lastWarningTime < WARNING_INTERVAL) {
return
}
lastWarningTime = now
// 使用非阻塞的消息提示不阻塞UI
ElMessage({
message: '警告:无人机已飞出围栏范围,请立即采取措施!',
type: 'error',
duration: 8000, // 显示8秒
showClose: true,
dangerouslyUseHTMLString: false,
// 避免重复消息
grouping: true
})
// 在控制台输出详细警告(可选)
console.warn('⚠️ 无人机已飞出围栏范围!', {
timestamp: new Date().toLocaleString(),
position: droneData.value ? {
lng: droneData.value.longitude,
lat: droneData.value.latitude
} : null
})
}
// 处理绘制完成事件
const handleDrawComplete = (event) => {
const obj = event.obj
let fence = null
// 根据绘制类型创建围栏对象
if (obj instanceof AMap.Polygon) {
// 多边形
const path = obj.getPath()
fence = {
id: `fence_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
type: 'polygon',
shape: obj,
path: path,
createdAt: new Date()
}
} else if (obj instanceof AMap.Rectangle) {
// 矩形
const bounds = obj.getBounds()
const path = [
bounds.getSouthWest(), // 西南角
[bounds.getNorthEast().getLng(), bounds.getSouthWest().getLat()], // 东南角
bounds.getNorthEast(), // 东北角
[bounds.getSouthWest().getLng(), bounds.getNorthEast().getLat()] // 西北角
]
fence = {
id: `fence_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
type: 'rectangle',
shape: obj,
path: path,
bounds: bounds,
createdAt: new Date()
}
} else if (obj instanceof AMap.Circle) {
// 圆形
const center = obj.getCenter()
const radius = obj.getRadius()
fence = {
id: `fence_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
type: 'circle',
shape: obj,
center: center,
radius: radius,
createdAt: new Date()
}
}
if (fence) {
// 注意使用MouseTool绘制的对象已经自动添加到地图上
// 如果对象还没有添加到地图,则添加它
if (!fence.shape.getMap()) {
map.add(fence.shape)
}
// 先设置默认名称,确保列表能立即显示
const defaultName = `${getTypeName(fence.type)}_${fences.value.length + 1}`
fence.name = defaultName
// 保存围栏到列表(立即显示)
fences.value.push(fence)
// 关闭绘制工具,确保不能再继续绘制
// 注意使用close()而不是close(true)因为true会删除已绘制的图形
if (mouseTool) {
mouseTool.close() // 关闭绘制工具,但不删除已绘制的图形
}
// 先更新样式和标签(使用默认名称)
updateFenceStyleAndLabel(fence)
// 获取当前围栏在数组中的索引,用于后续更新
const fenceIndex = fences.value.length - 1
// 弹出输入框让用户输入围栏名称(异步,不阻塞列表显示)
ElMessageBox.prompt('请输入围栏名称', '围栏命名', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入围栏名称',
inputValue: defaultName,
inputValidator: (value) => {
if (!value || value.trim() === '') {
return '围栏名称不能为空'
}
return true
}
}).then(({ value }) => {
// 用户输入了名称通过数组索引更新确保Vue响应式系统能检测到变化
const newName = value.trim()
if (fences.value[fenceIndex]) {
fences.value[fenceIndex].name = newName
// 更新标签
addFenceLabel(fences.value[fenceIndex])
}
}).catch(() => {
// 用户取消,清除刚刚添加的围栏
const cancelledFence = fences.value[fenceIndex]
if (cancelledFence) {
// 从地图上移除围栏和标签
if (cancelledFence.shape) {
try {
const currentMap = cancelledFence.shape.getMap ? cancelledFence.shape.getMap() : null
if (currentMap) {
currentMap.remove(cancelledFence.shape)
if (cancelledFence.label) {
currentMap.remove(cancelledFence.label)
}
}
} catch (error) {
console.error('清除围栏时出错:', error)
}
}
// 从列表中移除
fences.value.splice(fenceIndex, 1)
}
})
// 重置绘制状态
isDrawing.value = false
currentDrawType.value = null
console.log('围栏绘制完成:', fence)
}
}
// 更新围栏样式和标签
const updateFenceStyleAndLabel = (fence) => {
// 设置围栏样式
if (fence.shape.setOptions) {
fence.shape.setOptions({
strokeColor: '#0066FF',
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: '#0066FF',
fillOpacity: 0.15,
zIndex: 50,
visible: true
})
}
// 确保在地图上
if (!fence.shape.getMap()) {
map.add(fence.shape)
}
// 添加围栏名称标签到地图上
addFenceLabel(fence)
}
// 添加围栏名称标签
const addFenceLabel = (fence) => {
if (!map || !AMap || !fence.name) return
// 移除旧的标签(如果存在)
if (fence.label) {
map.remove(fence.label)
}
// 计算标签位置
let labelPosition = null
if (fence.type === 'polygon') {
// 多边形使用bounds的右上角东北角
const bounds = fence.shape.getBounds ? fence.shape.getBounds() : null
if (bounds) {
const ne = bounds.getNorthEast()
labelPosition = [ne.getLng(), ne.getLat()]
} else {
// 如果没有bounds使用第一个顶点
const path = fence.shape.getPath ? fence.shape.getPath() : fence.path
if (path && path.length > 0) {
const firstPoint = path[0]
if (Array.isArray(firstPoint)) {
labelPosition = firstPoint
} else if (firstPoint.getLng && firstPoint.getLat) {
labelPosition = [firstPoint.getLng(), firstPoint.getLat()]
} else {
labelPosition = [firstPoint.lng || firstPoint[0], firstPoint.lat || firstPoint[1]]
}
}
}
} else if (fence.type === 'rectangle') {
// 矩形使用bounds的右上角东北角
const bounds = fence.shape.getBounds ? fence.shape.getBounds() : fence.bounds
if (bounds) {
const ne = bounds.getNorthEast()
labelPosition = [ne.getLng(), ne.getLat()]
}
} else if (fence.type === 'circle') {
// 圆形:使用圆心,然后向上偏移到右上角位置
const center = fence.shape.getCenter ? fence.shape.getCenter() : fence.center
const radius = fence.shape.getRadius ? fence.shape.getRadius() : fence.radius
if (center) {
let centerLng, centerLat
if (center.getLng && center.getLat) {
centerLng = center.getLng()
centerLat = center.getLat()
} else {
centerLng = center.lng || center[0]
centerLat = center.lat || center[1]
}
// 计算右上角位置(圆心 + 半径的偏移)
const offset = radius / 111320 // 转换为经纬度偏移
labelPosition = [centerLng + offset, centerLat + offset]
}
}
if (labelPosition) {
// 创建文本标签,位置在右上角
const label = new AMap.Text({
text: fence.name,
position: labelPosition,
offset: new AMap.Pixel(-10, -10), // 偏移到右上角
style: {
padding: '4px 8px',
backgroundColor: 'rgba(0, 102, 255, 0.6)', // 半透明背景
border: '1px solid rgba(0, 102, 255, 0.8)',
borderRadius: '4px',
fontSize: '12px',
color: '#fff',
fontWeight: 'bold',
whiteSpace: 'nowrap'
},
zIndex: 100
})
map.add(label)
fence.label = label
}
}
// 开始绘制多边形
const startDrawPolygon = () => {
if (!mouseTool) return
// 如果已经在绘制,先关闭之前的绘制工具
if (isDrawing.value && mouseTool) {
mouseTool.close(true)
}
isDrawing.value = true
currentDrawType.value = 'polygon'
// 使用高德地图内置工具绘制多边形
mouseTool.polygon({
strokeColor: '#FF0000',
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: '#FF0000',
fillOpacity: 0.2
})
}
// 开始绘制矩形
const startDrawRectangle = () => {
if (!mouseTool) return
// 如果已经在绘制,先关闭之前的绘制工具
if (isDrawing.value && mouseTool) {
mouseTool.close(true)
}
isDrawing.value = true
currentDrawType.value = 'rectangle'
// 使用高德地图内置工具绘制矩形
mouseTool.rectangle({
strokeColor: '#FF0000',
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: '#FF0000',
fillOpacity: 0.2
})
}
// 开始绘制圆形
const startDrawCircle = () => {
if (!mouseTool) return
// 如果已经在绘制,先关闭之前的绘制工具
if (isDrawing.value && mouseTool) {
mouseTool.close(true)
}
isDrawing.value = true
currentDrawType.value = 'circle'
// 使用高德地图内置工具绘制圆形
mouseTool.circle({
strokeColor: '#FF0000',
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: '#FF0000',
fillOpacity: 0.2
})
}
// 取消绘制功能已移除
// 获取类型名称
const getTypeName = (type) => {
const typeMap = {
polygon: '多边形',
rectangle: '矩形',
circle: '圆形'
}
return typeMap[type] || type
}
// 上传围栏 - 获取当前绘制的点
const uploadFence = (fenceId) => {
const fence = fences.value.find(f => f.id === fenceId)
if (!fence) {
console.error('未找到围栏:', fenceId)
return
}
let pointsData = null
try {
// 根据围栏类型获取坐标点数据
if (fence.type === 'polygon') {
// 多边形:获取所有顶点坐标
const path = fence.shape.getPath ? fence.shape.getPath() : fence.path
pointsData = {
type: 'polygon',
id: fence.id,
points: path.map(point => {
// 处理坐标格式,确保是[lng, lat]格式
if (Array.isArray(point)) {
return [point[0], point[1]]
} else if (point.getLng && point.getLat) {
return [point.getLng(), point.getLat()]
} else if (point.lng !== undefined && point.lat !== undefined) {
return [point.lng, point.lat]
}
return point
}),
pointCount: path.length
}
} else if (fence.type === 'rectangle') {
// 矩形:获取四个顶点坐标
const bounds = fence.shape.getBounds ? fence.shape.getBounds() : fence.bounds
if (bounds) {
const sw = bounds.getSouthWest() // 西南角
const ne = bounds.getNorthEast() // 东北角
pointsData = {
type: 'rectangle',
id: fence.id,
points: [
[sw.getLng(), sw.getLat()], // 西南角
[ne.getLng(), sw.getLat()], // 东南角
[ne.getLng(), ne.getLat()], // 东北角
[sw.getLng(), ne.getLat()] // 西北角
],
bounds: {
southWest: [sw.getLng(), sw.getLat()],
northEast: [ne.getLng(), ne.getLat()]
},
pointCount: 4
}
} else if (fence.path) {
// 如果bounds不可用使用保存的path
pointsData = {
type: 'rectangle',
id: fence.id,
points: fence.path.map(point => {
if (Array.isArray(point)) {
return [point[0], point[1]]
} else if (point.getLng && point.getLat) {
return [point.getLng(), point.getLat()]
}
return point
}),
pointCount: fence.path.length
}
}
} else if (fence.type === 'circle') {
// 圆形:获取圆心和半径
const center = fence.shape.getCenter ? fence.shape.getCenter() : fence.center
const radius = fence.shape.getRadius ? fence.shape.getRadius() : fence.radius
if (center) {
const centerLng = center.getLng ? center.getLng() : (center.lng || center[0])
const centerLat = center.getLat ? center.getLat() : (center.lat || center[1])
pointsData = {
type: 'circle',
id: fence.id,
center: [centerLng, centerLat],
radius: radius,
// 生成圆形边界点(可选,用于可视化)
boundaryPoints: generateCirclePoints(centerLng, centerLat, radius, 32)
}
}
}
if (pointsData) {
const points = pointsData.points.map(point => {
const wgsPoint = gcoord.transform(
point, // 原始坐标
gcoord.GCJ02, // 当前坐标系
gcoord.WGS84 // 目标坐标系
);
return wgsPoint;
});
console.log(points)
ctrl.UploadArea(new UploadAreaReq(points.map(point => ({
longitude: point[0],
latitude: point[1],
}))));
// 输出到控制台
console.log('围栏坐标数据:', pointsData)
console.log('围栏坐标数据(JSON):', JSON.stringify(pointsData, null, 2))
// 可以在这里添加实际的上传逻辑,比如发送到服务器
// await uploadToServer(pointsData)
// 提示用户
const message = pointsData.type === 'circle'
? `围栏坐标数据已获取!\n类型: ${getTypeName(pointsData.type)}\n圆心: [${pointsData.center[0]}, ${pointsData.center[1]}]\n半径: ${pointsData.radius}`
: `围栏坐标数据已获取!\n类型: ${getTypeName(pointsData.type)}\n顶点数量: ${pointsData.pointCount}\n请查看控制台获取完整数据`
ElMessage.success({
message: message,
duration: 3000,
showClose: true
})
return pointsData
} else {
console.error('无法获取围栏坐标数据')
ElMessage.error('获取围栏坐标数据失败')
}
} catch (error) {
console.error('上传围栏时出错:', error)
ElMessage.error('获取围栏坐标数据时出错: ' + error.message)
}
}
// 生成圆形边界点(辅助函数)
const generateCirclePoints = (centerLng, centerLat, radius, segments = 32) => {
const points = []
for (let i = 0; i <= segments; i++) {
const angle = (i * 360) / segments
const radian = (angle * Math.PI) / 180
// 使用简单的近似计算(对于小范围更准确)
const lng = centerLng + (radius / 111320) * Math.cos(radian)
const lat = centerLat + (radius / 110540) * Math.sin(radian)
points.push([lng, lat])
}
return points
}
// 删除围栏(性能优化版本:直接同步删除,快速响应)
const removeFence = (fenceId) => {
const index = fences.value.findIndex(f => f.id === fenceId)
if (index === -1) return
const fence = fences.value[index]
// 直接从地图上移除(同步操作,快速完成)
if (fence && fence.shape) {
try {
const currentMap = fence.shape.getMap ? fence.shape.getMap() : null
if (currentMap) {
currentMap.remove(fence.shape)
// 同时移除标签
if (fence.label) {
currentMap.remove(fence.label)
}
}
} catch (error) {
console.error('删除围栏时出错:', error)
}
}
ctrl.ClearArea({});
// 从列表中移除
fences.value.splice(index, 1)
console.log('围栏已删除:', fenceId, '剩余围栏数量:', fences.value.length)
}
// 从地图上移除围栏(优化版本:只使用最有效的方法)
const removeFenceFromMap = (fence) => {
if (!fence || !fence.shape) return
try {
// 只使用最有效的方法map.remove
const currentMap = fence.shape.getMap ? fence.shape.getMap() : null
if (currentMap) {
currentMap.remove(fence.shape)
} else if (fence.shape.setMap) {
// 如果不在地图上尝试setMap(null)
fence.shape.setMap(null)
}
} catch (error) {
console.error('删除围栏时出错:', error)
}
}
// 清除所有围栏
const clearAllFences = async () => {
try {
await ElMessageBox.confirm(
'确定要清除所有围栏吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
await ctrl.ClearArea({});
// 先清除地图上的所有图形
const fencesToRemove = [...fences.value] // 创建副本避免遍历时修改数组
fencesToRemove.forEach(fence => {
if (fence && fence.shape) {
try {
const currentMap = fence.shape.getMap ? fence.shape.getMap() : null
if (currentMap) {
currentMap.remove(fence.shape)
// 同时移除标签
if (fence.label) {
currentMap.remove(fence.label)
}
}
} catch (error) {
console.error('清除围栏时出错:', error)
}
}
})
// 清空围栏列表
fences.value = []
// 清除无人机轨迹
clearDroneTrack()
console.log('所有围栏已清除,当前围栏数量:', fences.value.length)
ElMessage.success('所有围栏已清除')
} catch (error) {
// 用户点击取消
if (error !== 'cancel') {
console.error('清除围栏时出错:', error)
ElMessage.error('清除围栏时出错')
}
}
}
// watch已移除无人机位置实时更新时会自动检测围栏状态不需要监听围栏变化
// 日期时间更新定时器
let dateTimeTimer = null
// 组件挂载时初始化地图
onMounted(() => {
initMap()
updateDateTime()
// 每秒更新一次时间
dateTimeTimer = setInterval(updateDateTime, 1000)
})
// 组件卸载时清理
onUnmounted(() => {
// 清除日期时间定时器
if (dateTimeTimer) {
clearInterval(dateTimeTimer)
dateTimeTimer = null
}
// 停止WebSocket连接
if (mockDataGenerator) {
mockDataGenerator.stop()
mockDataGenerator = null
}
if (wsClient) {
wsClient.close()
wsClient = null
}
if (conn) {
conn.close()
conn = null
}
// 清理地图
if (droneMarker.value) {
map?.remove(droneMarker.value)
droneMarker.value = null
}
if (drone2Marker.value) {
map?.remove(drone2Marker.value)
drone2Marker.value = null
}
if (mouseTool) {
mouseTool.close()
}
if (map) {
map.destroy()
}
})
</script>
<style scoped>
/* 全局容器 */
.app-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
/* background: #0a0e27; */
color: #ffffff;
overflow: hidden;
}
/* 顶部导航栏 */
.top-header {
height: 60px;
background: #0a0e27;
backdrop-filter: blur(15px);
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
z-index: 1000;
position: relative;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.demo-tag {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
padding: 4px 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.region-name {
font-size: 16px;
color: #ffffff;
font-weight: 500;
}
.header-center {
flex: 1;
text-align: center;
}
.app-title {
font-size: 28px;
font-weight: bold;
color: #ffffff;
margin: 0;
text-shadow: 0 2px 10px rgba(0, 150, 255, 0.5);
}
.header-right {
display: flex;
align-items: center;
}
.datetime {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
font-family: 'Courier New', monospace;
}
/* 主内容区域 */
.main-content {
flex: 1;
position: relative;
overflow: hidden;
}
/* 地图容器 */
.map-container {
width: 100%;
height: 100%;
position: relative;
}
/* 左侧面板(覆盖层) */
.left-panel {
/* background: url('./assets/image/z1.png') no-repeat center center; */
position: absolute;
left: 0;
top: 0;
bottom: 0;
padding: 20px;
overflow-y: auto;
z-index: 100;
display: flex;
flex-direction: column;
justify-content: center;
:nth-child(1) {
background: url('./assets/image/z1.png') no-repeat center center;
width: 320px;
height: 270px;
}
:nth-child(2) {
background: url('./assets/image/z2.png') no-repeat center center;
width: 320px;
height: 270px;
}
:nth-child(3) {
background: url('./assets/image/z3.png') no-repeat center center;
width: 320px;
height: 270px;
}
}
/* 右侧面板(覆盖层) */
.right-panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
padding: 20px;
overflow-y: auto;
z-index: 100;
display: flex;
flex-direction: column;
justify-content: center;
:nth-child(1) {
background: url('./assets/image/y1.png') no-repeat center center;
width: 320px;
height: 270px;
}
:nth-child(2) {
background: url('./assets/image/y2.png') no-repeat center center;
width: 320px;
height: 270px;
}
:nth-child(3) {
background: url('./assets/image/y3.png') no-repeat center center;
width: 320px;
height: 270px;
}
}
/* 信息面板 */
.info-panel {
background: rgba(36, 39, 56, 0.7);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-title {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
}
.panel-count {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
/* 视频网格 */
.video-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.video-item {
aspect-ratio: 16/9;
position: relative;
}
.video-placeholder {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.video-placeholder::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(0, 150, 255, 0.1) 0%, rgba(0, 0, 0, 0.3) 100%);
}
.video-label {
position: relative;
z-index: 1;
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
}
/* 状态列表 */
.status-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
background: rgba(0, 0, 0, 0.15);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.status-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.status-number {
font-size: 18px;
font-weight: bold;
color: #ffffff;
min-width: 30px;
}
.status-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
flex: 1;
}
/* 区域预览 */
.area-preview {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, 0.15);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.area-image {
max-width: 100%;
height: auto;
opacity: 0.8;
}
/* 设备图标 */
.device-icon {
width: 24px;
height: 24px;
border-radius: 4px;
border: 2px solid;
position: relative;
flex-shrink: 0;
}
.device-icon::before {
content: '✈';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
}
.device-standby {
background: transparent;
border-color: rgba(255, 255, 255, 0.5);
}
.device-standby::before {
color: rgba(255, 255, 255, 0.5);
}
.device-duty {
background: rgba(255, 165, 0, 0.3);
border-color: #ffa500;
}
.device-duty::before {
color: #ffa500;
}
.device-charging {
background: rgba(0, 255, 0, 0.3);
border-color: #00ff00;
}
.device-charging::before {
color: #00ff00;
}
/* 任务进度 */
.task-progress {
display: flex;
flex-direction: column;
gap: 12px;
}
.progress-item {
display: flex;
align-items: center;
gap: 12px;
}
.progress-bar {
flex: 1;
height: 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-completed {
background: rgba(255, 255, 255, 0.8);
}
.progress-in-progress {
background: rgba(255, 165, 0, 0.8);
}
.progress-pending {
background: rgba(0, 255, 0, 0.8);
}
.progress-text {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
min-width: 80px;
text-align: right;
}
#map-container {
width: 100%;
height: 100%;
}
/* 控制面板 */
.control-panel {
position: absolute;
top: 80px;
right: 340px;
background: rgba(36, 39, 56, 0.7);
border-radius: 8px;
padding: 16px;
min-width: 280px;
max-width: 320px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.15);
z-index: 1000;
max-height: calc(100vh - 160px);
overflow-y: auto;
color: #ffffff;
}
.control-panel .panel-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
color: rgba(255, 255, 255, 0.9);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 8px;
}
.control-group {
margin-bottom: 16px;
}
.draw-type-title {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 8px;
font-weight: 500;
}
.draw-buttons {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.draw-buttons .el-button {
flex: 1;
min-width: 80px;
}
.fence-list {
margin-top: 16px;
border-top: 1px solid #eee;
padding-top: 12px;
}
.list-title {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 8px;
}
.fence-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
margin-bottom: 8px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.fence-info {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.fence-actions {
display: flex;
gap: 6px;
align-items: center;
}
.btn-upload {
background-color: #1890ff;
color: white;
}
.btn-upload:hover {
background-color: #40a9ff;
}
.fence-type {
display: inline-block;
padding: 2px 6px;
background-color: #e6f7ff;
color: #1890ff;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
}
.fence-name {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
margin-left: 4px;
}
.drone-status-panel {
margin-top: 16px;
margin-bottom: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 12px;
}
.drone-status-panel .status-title {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 12px;
}
.drone-status-panel .status-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
}
.drone-status-panel .status-label {
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
}
.drone-status-panel .status-value {
color: rgba(255, 255, 255, 0.9);
font-family: monospace;
}
/* 底部导航栏 */
.bottom-footer {
background: #0a0e27;
backdrop-filter: blur(15px);
border-top: 1px solid rgba(255, 255, 255, 0.15);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 1000;
position: relative;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.footer-left {
display: flex;
align-items: center;
gap: 8px;
}
.storage-info {
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
font-family: 'Courier New', monospace;
font-weight: 500;
padding: 4px 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.resolution-info {
position: relative;
}
.resolution-text {
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
background: rgba(0, 0, 0, 0.25);
padding: 3px 10px;
border-radius: 4px;
font-family: 'Courier New', monospace;
letter-spacing: 0.5px;
}
.footer-icons {
display: flex;
gap: 16px;
align-items: center;
padding: 4px 0;
}
.footer-icon {
font-size: 56px;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.3s ease;
padding: 6px;
border-radius: 4px;
}
.footer-icon:hover {
color: rgba(0, 150, 255, 1);
background: rgba(0, 150, 255, 0.1);
transform: translateY(-2px);
}
.footer-icon-img {
width: 48px;
height: 48px;
cursor: pointer;
transition: all 0.3s ease;
padding: 4px;
border-radius: 4px;
object-fit: contain;
opacity: 0.8;
}
.footer-icon-img:hover {
opacity: 1;
background: rgba(0, 150, 255, 0.1);
transform: translateY(-2px);
}
.footer-icon-img.active {
opacity: 1;
background: rgba(0, 150, 255, 0.2);
border: 1px solid rgba(0, 150, 255, 0.5);
}
.footer-right {
display: flex;
align-items: center;
gap: 8px;
}
.uptime-info {
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
font-family: 'Courier New', monospace;
font-weight: 500;
padding: 4px 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* 滚动条样式 */
.control-panel::-webkit-scrollbar,
.left-panel::-webkit-scrollbar,
.right-panel::-webkit-scrollbar {
width: 6px;
}
.control-panel::-webkit-scrollbar-track,
.left-panel::-webkit-scrollbar-track,
.right-panel::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.control-panel::-webkit-scrollbar-thumb,
.left-panel::-webkit-scrollbar-thumb,
.right-panel::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.control-panel::-webkit-scrollbar-thumb:hover,
.left-panel::-webkit-scrollbar-thumb:hover,
.right-panel::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>