airport-qingdao-vue3/src/components/map/controls/LayerSwitcher.vue
2025-07-15 11:49:27 +08:00

862 lines
23 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="layer-switcher">
<div class="layer-icon" @click="toggleLayerPanel"></div>
<!-- 图层面板 -->
<div class="layer-panel" v-if="showPanel">
<!-- 标签页切换 -->
<div class="panel-tabs">
<div
class="tab"
:class="{ active: activeTab === 'icon' }"
@click="activeTab = 'icon'"
>
图标
</div>
<div
class="tab"
:class="{ active: activeTab === 'text' }"
@click="activeTab = 'text'"
>
文本
</div>
<div
class="tab"
:class="{ active: activeTab === 'road' }"
@click="activeTab = 'road'"
>
道路
</div>
</div>
<!-- 图标tab页 -->
<div class="panel-content" v-if="activeTab === 'icon'">
<div class="layer-group">
<!-- <div class="group-title">图标显示控制</div> -->
<div class="layer-grid">
<div class="layer-item" v-for="(cat, type) in categories" :key="type">
<label class="checkbox-container">
<input type="checkbox" v-model="cat.visible" @change="emitSet(type)">
<span class="checkmark"></span>
<span class="layer-name">{{ cat.name }}</span>
<!-- <img :src="cat.icon" class="layer-icon-preview" /> -->
</label>
</div>
</div>
</div>
</div>
<!-- 文本tab页 -->
<div class="panel-content" v-else-if="activeTab === 'text'">
<div class="layer-group">
<!-- <div class="group-title">标签显示控制</div> -->
<div class="layer-grid">
<div class="layer-item" v-for="(cat, type) in categories" :key="type">
<label class="checkbox-container">
<input type="checkbox" v-model="cat.showLabel" @change="emitSet(type)">
<span class="checkmark"></span>
<span class="layer-name">{{ cat.name }}</span>
</label>
</div>
</div>
</div>
</div>
<div class="panel-content" v-else>
<div class="layer-group">
<!-- <div class="group-title">道路图层</div> -->
<div class="layer-grid-full">
<div class="layer-item">
<label class="checkbox-container">
<input type="checkbox" :checked="!hideRoadLayer" @change="toggleHideRoadLayer" />
<span class="checkmark"></span>
<span class="layer-name">电子围栏</span>
<!-- <svg class="layer-icon-preview" width="24" height="24" viewBox="0 0 1024 1024"><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286z" fill="#409eff"/></svg> -->
</label>
</div>
<!-- 添加自定义路线图层选项 -->
<div class="layer-item">
<label class="checkbox-container">
<input type="checkbox" v-model="showCustomRoadLayer" />
<span class="checkmark"></span>
<span class="layer-name">路线图</span>
<!-- <svg class="layer-icon-preview" width="24" height="24" viewBox="0 0 1024 1024"><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286z" fill="#FF5722"/></svg> -->
</label>
</div>
<!-- 添加区域1图层选项 -->
<div class="layer-item">
<label class="checkbox-container">
<input type="checkbox" v-model="showArea1Layer" />
<span class="checkmark"></span>
<span class="layer-name">测试区域1</span>
</label>
</div>
<!-- 添加区域2图层选项 -->
<div class="layer-item">
<label class="checkbox-container">
<input type="checkbox" v-model="showArea2Layer" />
<span class="checkmark"></span>
<span class="layer-name">测试区域2</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed, onBeforeMount, onUnmounted } from 'vue';
import { Vector as VectorSource } from 'ol/source';
import { Vector as VectorLayer } from 'ol/layer';
import { Style, Icon, Stroke, Fill } from 'ol/style';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import GeoJSON from 'ol/format/GeoJSON';
import Overlay from 'ol/Overlay';
import labelBg from '../../../assets/images/label_bg.png';
// 导入车辆图标
import car1Icon from '../../../assets/images/Aircraft.png'; //滑入航空器
import car1Icon1 from '../../../assets/images/Aircraft1.png'; //滑出航空器
import car2Icon from '../../../assets/images/noPeopleCar.png';
import noPeopleCarIcon from '../../../assets/images/noPeopleCar.png';
import airportBg from '../../../assets/images/airport_bg.png'; // 蓝色背景
import airportOutBg from '../../../assets/images/airport_out.png'; // 黄色背景
// 定义props接收地图实例
const props = defineProps({
map: Object,
categories: Object
});
// 响应式状态
const showPanel = ref(false);
const activeTab = ref('icon'); // 默认选中图标标签页
const selectedTextStyle = ref('white'); // 默认选中白色样式
// 新增道路tab相关状态
const hideRoadLayer = ref(false); // 默认不隐藏(即显示)
const showRoadLayer = computed(() => !hideRoadLayer.value); // 计算属性与hideRoadLayer相反
let roadVectorLayer = null;
// 新增:自定义路线图层状态
const showCustomRoadLayer = ref(true); // 默认显示
let customRoadVectorLayer = null;
// 新增区域1图层状态
const showArea1Layer = ref(true); // 默认显示
let area1VectorLayer = null;
// 新增区域2图层状态
const showArea2Layer = ref(true); // 默认显示
let area2VectorLayer = null;
// 向父组件发出事件
const emit = defineEmits(['layerChange', 'setCategoryVisibility']);
// 切换图层面板显示
function toggleLayerPanel() {
showPanel.value = !showPanel.value;
}
// 切换电子围栏显示状态
function toggleHideRoadLayer() {
hideRoadLayer.value = !hideRoadLayer.value;
console.log('toggleHideRoadLayer: hideRoadLayer =', hideRoadLayer.value, 'showRoadLayer =', showRoadLayer.value);
if (showRoadLayer.value) {
addRoadLayer();
} else {
removeRoadLayer();
}
}
// 组件挂载时初始化
onMounted(() => {
if (props.map) {
// 初始化自定义路线图层
loadCustomRoadLayer();
// 初始化电子围栏图层 - 默认显示
if (showRoadLayer.value) {
addRoadLayer();
}
// 初始化区域1图层
loadArea1Layer();
// 初始化区域2图层
loadArea2Layer();
}
});
// 监听地图实例变化
watch(() => props.map, (newMap) => {
if (newMap) {
// 初始化自定义路线图层
loadCustomRoadLayer();
// 初始化电子围栏图层
if (showRoadLayer.value) {
addRoadLayer();
}
// 初始化区域1图层
loadArea1Layer();
// 初始化区域2图层
loadArea2Layer();
}
});
// 向外暴露方法
defineExpose({
setLayerVisibility(typeKey, visible) {
emit('setCategoryVisibility', typeKey, {
visible,
showLabel: props.categories[typeKey]?.showLabel || true
});
}
});
async function loadCustomRoadLayer() {
// 先移除已存在的自定义路线图层,避免重复
removeCustomRoadLayer();
if (!props.map) return;
try {
// 使用相对路径加载自定义路线文件
const res = await fetch('./roadTest.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
customRoadVectorLayer = new VectorLayer({
source,
style: new Style({
stroke: new Stroke({ color: '#C9C9C9', width: 1 })
}),
zIndex: 2, // 确保在标准道路图层之上
visible: showCustomRoadLayer.value
});
props.map.addLayer(customRoadVectorLayer);
console.log('loadCustomRoadLayer: 已添加自定义路线图层', customRoadVectorLayer);
} catch (e) {
console.error('loadCustomRoadLayer: 加载或添加自定义路线图层失败', e);
}
}
function removeCustomRoadLayer() {
if (!props.map) return;
let removed = false;
// 获取所有图层,查找类型为 VectorLayer 且 zIndex 为 2 的(即自定义路线图层)
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 2) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeCustomRoadLayer: 已移除自定义路线图层', lyr);
}
}
customRoadVectorLayer = null;
if (!removed) {
console.log('removeCustomRoadLayer: 没有找到可移除的自定义路线图层');
}
}
// 监听自定义路线显示状态变化
watch(showCustomRoadLayer, (val) => {
console.log('showCustomRoadLayer变化:', val);
if (val) {
if (customRoadVectorLayer) {
customRoadVectorLayer.setVisible(true);
} else {
loadCustomRoadLayer();
}
} else if (customRoadVectorLayer) {
customRoadVectorLayer.setVisible(false);
}
});
async function addRoadLayer() {
// 先移除已存在的道路图层,避免重复
removeRoadLayer();
if (!props.map) return;
try {
// 使用相对路径,确保在任何部署环境下都能正确加载
const res = await fetch('./dianziweilan.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
// 创建两个图层样式 - 一个用于普通状态,一个用于闪烁状态
const normalStyle = new Style({
stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.4)', width: 1 }),
fill: new Fill({ color: 'rgba(0, 0, 0, 0.2)' })
});
const flashStyle = new Style({
stroke: new Stroke({ color: 'rgba(255, 0, 0, 0.8)', width: 1 }),
fill: new Fill({ color: 'rgba(255, 0, 0, 0.3)' })
});
roadVectorLayer = new VectorLayer({
source,
style: normalStyle,
zIndex: 1,
className: 'fence-layer'
});
props.map.addLayer(roadVectorLayer);
console.log('addRoadLayer: 已添加电子围栏图层', roadVectorLayer);
// 开始闪烁效果
startFenceFlashing(roadVectorLayer, normalStyle, flashStyle);
} catch (e) {
console.error('addRoadLayer: 加载或添加电子围栏图层失败', e);
}
}
let flashingInterval = null;
// 启动闪烁效果
function startFenceFlashing(layer, normalStyle, flashStyle) {
// 清除可能存在的旧定时器
if (flashingInterval) {
clearInterval(flashingInterval);
}
// 闪烁状态标志
let isFlashing = false;
// 定时切换样式
flashingInterval = setInterval(() => {
if (!layer) return;
layer.setStyle(isFlashing ? normalStyle : flashStyle);
isFlashing = !isFlashing;
}, 800); // 每800毫秒切换一次样式
}
// 停止闪烁效果
function stopFenceFlashing() {
if (flashingInterval) {
clearInterval(flashingInterval);
flashingInterval = null;
}
}
// 移除电子围栏图层时也停止闪烁
function removeRoadLayer() {
if (!props.map) return;
stopFenceFlashing();
let removed = false;
// 获取所有图层,查找类型为 VectorLayer 且 zIndex 为 1 的(即道路图层)
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 1) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeRoadLayer: 已移除道路图层', lyr);
}
}
roadVectorLayer = null;
if (!removed) {
console.log('removeRoadLayer: 没有找到可移除的道路图层');
}
}
watch(showRoadLayer, (val) => {
console.log('showRoadLayer变化:', val);
if (val) addRoadLayer();
else removeRoadLayer();
});
// 加载区域1图层
async function loadArea1Layer() {
// 先移除已存在的区域1图层避免重复
removeArea1Layer();
if (!props.map) return;
try {
// 使用相对路径加载区域1文件
const res = await fetch('./quyu1.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
area1VectorLayer = new VectorLayer({
source,
style: new Style({
stroke: new Stroke({ color: '#FF5733', width: 2 }),
fill: new Fill({ color: 'rgba(255, 87, 51, 0.3)' })
}),
zIndex: 3, // 确保在其他图层之上
visible: showArea1Layer.value
});
props.map.addLayer(area1VectorLayer);
console.log('loadArea1Layer: 已添加区域1图层', area1VectorLayer);
} catch (e) {
console.error('loadArea1Layer: 加载或添加区域1图层失败', e);
}
}
// 移除区域1图层
function removeArea1Layer() {
if (!props.map) return;
let removed = false;
// 获取所有图层,查找类型为 VectorLayer 且 zIndex 为 3 的即区域1图层
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 3) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeArea1Layer: 已移除区域1图层', lyr);
}
}
area1VectorLayer = null;
if (!removed) {
console.log('removeArea1Layer: 没有找到可移除的区域1图层');
}
}
// 加载区域2图层
async function loadArea2Layer() {
// 先移除已存在的区域2图层避免重复
removeArea2Layer();
if (!props.map) return;
try {
// 使用相对路径加载区域2文件
const res = await fetch('./quyu2.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
area2VectorLayer = new VectorLayer({
source,
style: new Style({
stroke: new Stroke({ color: '#3374FF', width: 2 }),
fill: new Fill({ color: 'rgba(51, 116, 255, 0.3)' })
}),
zIndex: 4, // 确保在区域1图层之上
visible: showArea2Layer.value
});
props.map.addLayer(area2VectorLayer);
console.log('loadArea2Layer: 已添加区域2图层', area2VectorLayer);
} catch (e) {
console.error('loadArea2Layer: 加载或添加区域2图层失败', e);
}
}
// 移除区域2图层
function removeArea2Layer() {
if (!props.map) return;
let removed = false;
// 获取所有图层,查找类型为 VectorLayer 且 zIndex 为 4 的即区域2图层
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 4) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeArea2Layer: 已移除区域2图层', lyr);
}
}
area2VectorLayer = null;
if (!removed) {
console.log('removeArea2Layer: 没有找到可移除的区域2图层');
}
}
// 监听区域1显示状态变化
watch(showArea1Layer, (val) => {
console.log('showArea1Layer变化:', val);
if (val) {
if (area1VectorLayer) {
area1VectorLayer.setVisible(true);
} else {
loadArea1Layer();
}
} else if (area1VectorLayer) {
area1VectorLayer.setVisible(false);
}
});
// 监听区域2显示状态变化
watch(showArea2Layer, (val) => {
console.log('showArea2Layer变化:', val);
if (val) {
if (area2VectorLayer) {
area2VectorLayer.setVisible(true);
} else {
loadArea2Layer();
}
} else if (area2VectorLayer) {
area2VectorLayer.setVisible(false);
}
});
onUnmounted(() => {
removeRoadLayer();
removeCustomRoadLayer();
removeArea1Layer();
removeArea2Layer();
stopFenceFlashing();
});
// 发送分类可见性设置到父组件
function emitSet(type) {
emit('setCategoryVisibility', type, {
visible: props.categories[type].visible,
showLabel: props.categories[type].showLabel
});
}
</script>
<style scoped>
.layer-switcher {
position: relative;
}
.layer-icon {
width: 32px;
height: 32px;
cursor: pointer;
}
.layer-panel {
position: absolute;
top: 10px;
left: 50px;
width:397px;
background-color: #424851;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
z-index: 3100;
overflow: hidden;
color: #ffffff;
}
.panel-tabs {
display: flex;
border-bottom: 1px solid #303850;
}
.tab {
padding: 0 20px;
height: 40px;
line-height: 40px;
font-size: 14px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
color: #F0F0F0;
position: relative;
}
.tab:hover {
color: #409eff;
}
.tab.active {
color: #409eff;
}
.tab.active::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 2px;
background-color: #409eff;
}
.panel-content {
padding: 10px;
max-height: 400px;
overflow-y: auto;
}
.layer-group {
margin-bottom: 15px;
}
.group-title {
font-weight: bold;
margin-bottom: 8px;
padding-bottom: 5px;
border-bottom: 1px solid #999999;
color: #ffffff;
}
.layer-grid {
display: grid;
grid-template-columns: auto auto auto;
gap: 5px;
}
.layer-grid-full {
display: grid;
grid-template-columns: 1fr;
gap: 5px;
}
.layer-item {
padding: 6px 0;
}
/* 自定义复选框样式 */
.checkbox-container {
display: flex;
align-items: center;
position: relative;
padding-left: 30px;
cursor: pointer;
font-size: 14px;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
left: 0;
height: 18px;
width: 18px;
background-color: transparent;
border: 1px solid #999999;
border-radius: 3px;
}
.checkbox-container:hover input ~ .checkmark {
background-color: #666666;
}
.checkbox-container input:checked ~ .checkmark {
background-color: #0096ff;
border-color: #0078cc;
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkbox-container .checkmark:after {
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.layer-name {
margin-left: 5px;
color: #ffffff;
}
/* 图层图标预览 */
.layer-icon-preview {
width: 20px;
height: 20px;
margin-left: 8px;
object-fit: contain;
}
/* 滚动条样式 */
.panel-content::-webkit-scrollbar {
width: 6px;
}
.panel-content::-webkit-scrollbar-track {
background: #555555;
}
.panel-content::-webkit-scrollbar-thumb {
background: #999999;
border-radius: 3px;
}
.panel-content::-webkit-scrollbar-thumb:hover {
background: #bbbbbb;
}
/* 文本样式选择器 */
.style-selector {
display: flex;
flex-direction: column;
gap: 15px;
padding: 10px 0;
}
.style-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
}
.style-label {
font-size: 14px;
color: #ffffff;
}
.radio-box {
width: 20px;
height: 20px;
border: 2px solid #999999;
border-radius: 3px;
cursor: pointer;
position: relative;
}
.radio-box:hover {
border-color: #bbbbbb;
}
.radio-box.active {
border-color: #0096ff;
}
.radio-box.active::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
background-color: #0096ff;
border-radius: 1px;
}
.radio-box.blue {
border-color: #0096ff;
}
.radio-box.blue.active::after {
background-color: #0096ff;
}
.radio-box.white {
border-color: #ffffff;
}
.radio-box.white.active::after {
background-color: #ffffff;
}
/* 道路json样式 */
.road-json {
padding: 10px;
background-color: #333;
border-radius: 4px;
margin-top: 10px;
}
.road-json pre {
margin: 0;
padding: 0;
white-space: pre-wrap;
word-break: break-all;
}
/* 自定义标签样式 */
.custom-label {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
min-width: 120px;
height: 28px;
padding: 0 10px;
font-size: 12px;
font-weight: bold;
border-radius: 4px;
border: 1px solid;
color: #fff;
box-sizing: border-box;
white-space: nowrap;
z-index: 1000;
pointer-events: none;
transform: translateX(-50%);
}
/* 滑入航空器 - 黄色背景,黑色文字 */
.label-aircraft-in {
background-color: rgba(245, 231, 79, 0.7);
border-color: #E4CB0D;
color: #333;
}
/* 滑出航空器 - 蓝色背景,白色文字 */
.label-aircraft-out {
background-color: rgba(52, 122, 226, 0.7);
border-color: #347AE2;
color: #fff;
}
/* 无人车 */
.label-car {
background-color: rgba(37, 37, 37, 0.7);
border-color: #484848;
color: #fff;
}
/* 文本样式类 */
.custom-label.style-default {
/* 默认样式在各个类型中已定义 */
}
.custom-label.style-blue {
background-color: rgba(52, 122, 226, 0.7) !important;
border-color: #347AE2 !important;
color: #fff !important;
}
.custom-label.style-white {
background-color: rgba(255, 255, 255, 0.7) !important;
border-color: #ffffff !important;
color: #333 !important;
}
</style>