2136 lines
56 KiB
Vue
2136 lines
56 KiB
Vue
<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>
|