YouKeChuanMei_VUE/src/views/mediaMap/index.vue
2025-09-12 17:20:26 +08:00

776 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="app-container">
<el-card class="mediaCard">
<el-row :gutter="10" class="my_row" style="padding: 0 20px;">
<el-col :span="12">
<el-form :model="queryParams" ref="queryRef" :inline="true" class="searchInputForm">
<el-form-item label="" prop="templateName">
<!-- <el-input v-model="queryParams.keyword" placeholder="请输入媒体名称/媒体编号/关键字" :prefix-icon="Search"
style="width: 400px;" /> -->
<el-select class="filterSelect" v-model="queryParams.keyword" filterable remote
reserve-keyword :remote-method="getLocaleListList" :loading="selectLoading"
@change="currentSelect" placeholder="请输入关键字" remote-show-suffix clearable
style="width: 400px">
<el-option v-for="item in localeList" :key="item.id" :label="item.name" :value="item"
class="one-text">
<div style="height: 24px; line-height: 24px;font-size: 16px;">{{ item.name }}</div>
<div style="color: #8492a6; font-size: 12px;height: 18px; line-height: 18px;">{{
item.address
}}</div>
</el-option>
</el-select>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12" style="text-align: right;">
<el-dropdown placement="bottom-start">
<el-button type="primary" class="mediaMapBtn">距离:{{ distanceLable }}</el-button>
<template #dropdown>
<el-dropdown-menu style="min-width: 122px;">
<template v-for="item in distanceLableArray" :key="item.value">
<el-dropdown-item
:class="activeLableIndex === item.value ? 'distanceItemActive' : 'distanceItem'"
@click="handleChangeDistance(item)">{{ item.label }}</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="clearBtn" @click="resetQuery">清除条件</div>
</el-col>
</el-row>
<div id="mapContainer" class="mediaMapContainer"></div>
</el-card>
</div>
</template>
<script setup name="Post">
import { onMounted, onUnmounted, ref } from 'vue';
import { Search } from '@element-plus/icons-vue'
import AMapLoader from "@amap/amap-jsapi-loader"; // 引入地图服务
import { useBackgroundStore } from '@/store/modules/background'
import otherbg from '@/assets/images/otherbg.png'
import { mediaByMap } from "@/api/mediaLibrary"
const bgStore = useBackgroundStore()
const { proxy } = getCurrentInstance()
const { apiKey, secretKey } = window._CONFIG
// map实例
const mapInstance = ref(null)
const massMarks = ref(null)
const centerMarker = ref(null)
// 当前地图模式2D/3D
const mapMode = ref('2D')
// 是否全屏状态
const isFullscreen = ref(false)
const placeSearch = ref(null)
// 热区圆实例
const circle = ref(null);
const circleRadius = ref(2000); // 实际半径(米)
const circleHandle = ref(null); // 拖拽手柄
// 选择地点展示框
const selectLoading = ref(false)
const localeList = ref([])
const selectConfig = ref(null)
// 距离显示文本
const distanceLable = ref('请选择')
// 选择的距离值
const activeLableIndex = ref(null)
const distanceLableArray = ref([
{ label: '1公里内', value: 1000 },
{ label: '2公里内', value: 2000 },
{ label: '3公里内', value: 3000 },
{ label: '5公里内', value: 5000 }
])
const points = ref([])
const data = reactive({
queryParams: {
keyword: undefined,
x: undefined, //中心点经度
y: undefined, //中心点纬度
distance: undefined,
}
})
const { queryParams } = toRefs(data)
/** 清除条件操作 */
const resetQuery = () => {
queryParams.value = {
keyword: undefined,
x: undefined, //中心点经度
y: undefined, //中心点纬度
distance: undefined,
}
// 距离显示文本
distanceLable.value = '请选择'
// 选择的距离值
activeLableIndex.value = null
circleRadius.value = 2000; // 实际半径(米)
selectConfig.value = null
// 清除现有的圆形和标记
if (centerMarker.value) {
mapInstance.value.remove(centerMarker.value);
centerMarker.value = null;
}
if (circle.value) {
mapInstance.value.remove(circle.value);
circle.value = null;
}
if (circleHandle.value) {
mapInstance.value.remove(circleHandle.value);
circleHandle.value = null;
}
// 重新渲染数据点
renderMassMarks()
}
// 调用API获取地点
const getLocaleListList = (searchValue) => {
selectLoading.value = true
if (searchValue !== "") {
placeSearch.value.search(searchValue, function (status, result) {
// 查询成功时result即对应匹配的POI信息
console.log(result.poiList, result.poiList.pois, result.poiList.pois?.length)
if (result.poiList.pois?.length) {
localeList.value = result.poiList?.pois
}
selectLoading.value = false
});
}
}
// 中心点选择
const currentSelect = (val) => {
console.log('val', val)
selectConfig.value = val
queryParams.value.keyword = val.name
queryParams.value.x = val.location.lng
queryParams.value.y = val.location.lat
queryParams.value.distance = circleRadius.value
// 距离显示文本
distanceLable.value = circleRadius.value == 2000 ? '2000米' : circleRadius.value + '米'
// 选择的距离值
activeLableIndex.value = circleRadius.value == 2000 ? 2000 : circleRadius.value
// 清除现有的圆形和标记
if (circle.value) {
mapInstance.value.remove(circle.value);
circle.value = null;
}
if (circleHandle.value) {
mapInstance.value.remove(circleHandle.value);
circleHandle.value = null;
}
createCircle(val)
addCenterMark(val)
}
// 创建中心点标记
const addCenterMark = (val) => {
if (centerMarker.value) mapInstance.value.remove(centerMarker.value);
centerMarker.value = new AMap.Marker({
position: [val.location.lng, val.location.lat],
title: val.name,
zIndex: 100,
draggable: false, // 是否可以拖拽
cursor: 'move'
});
// 将点添加到地图
mapInstance.value.add(centerMarker.value);
mapInstance.value.setFitView();
}
// 创建圆形热区
const createCircle = (val) => {
// 创建圆形覆盖物
circle.value = new AMap.Circle({
center: [val.location.lng, val.location.lat],
radius: circleRadius.value,
strokeColor: '#4e54c8',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#4e54c8',
fillOpacity: 0.15,
zIndex: 100,
bubble: true,
cursor: 'move',
draggable: false // 是否可以拖拽
});
// 将圆形添加到地图
mapInstance.value.add(circle.value);
// 创建圆形边缘拖拽手柄
createCircleHandle();
// 渲染数据点
renderMassMarks()
}
// 创建圆形边缘拖拽手柄
const createCircleHandle = () => {
// 计算手柄的初始位置(圆形右侧边缘)
const center = circle.value.getCenter();
const radius = circle.value.getRadius();
// 更精确地计算手柄位置
const handlePosition = new AMap.LngLat(
center.lng + radius / (111320 * Math.cos(center.lat * Math.PI / 180)),
center.lat
);
console.log('手柄', handlePosition)
// 创建手柄标记
circleHandle.value = new AMap.Marker({
position: [handlePosition.lng, handlePosition.lat],
icon: new AMap.Icon({
size: new AMap.Size(20, 20),
image: '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png',
imageSize: new AMap.Size(20, 20)
}),
offset: new AMap.Pixel(-10, -10),
zIndex: 101,
draggable: true,
cursor: 'pointer'
});
mapInstance.value.add(circleHandle.value);
// 监听手柄拖拽事件
circleHandle.value.on('dragging', (e) => {
const handlePos = e.lnglat;
const center = circle.value.getCenter();
const newRadius = Math.round(calculateDistance(center, handlePos));
circleRadius.value = newRadius;
distanceLable.value = `${Math.round(circleRadius.value)}米`;
queryParams.value.distance = circleRadius.value // 更改查询条件距离
if (circleRadius.value !== 1000 && circleRadius.value !== 2000 && circleRadius.value !== 3000 && circleRadius.value !== 5000) activeLableIndex.value = null
else activeLableIndex.value = circleRadius.value
circle.value.setRadius(newRadius);
});
circleHandle.value.on('dragend', () => {
// 拖拽结束后更新手柄位置
updateCircleHandlePosition();
// 重新渲染数据点
renderMassMarks()
});
}
// 更新圆形拖拽手柄位置
const updateCircleHandlePosition = () => {
if (!circle.value || !circleHandle.value) return;
const center = circle.value.getCenter();
const radius = circle.value.getRadius();
const handlePosition = new AMap.LngLat(
center.lng + radius / (111320 * Math.cos(center.lat * Math.PI / 180)),
center.lat
);
circleHandle.value.setPosition(handlePosition);
};
// 计算两点之间的距离(米)
const calculateDistance = (point1, point2) => {
const lng1 = point1.lng;
const lat1 = point1.lat;
const lng2 = point2.lng;
const lat2 = point2.lat;
const radLat1 = lat1 * Math.PI / 180.0;
const radLat2 = lat2 * Math.PI / 180.0;
const a = radLat1 - radLat2;
const b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
let distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
distance = distance * 6378.137;
distance = Math.round(distance * 10000) / 10;
return distance;
};
// 选择距离
const handleChangeDistance = (itemDistance) => {
queryParams.value.distance = itemDistance.value
activeLableIndex.value = itemDistance.value
distanceLable.value = itemDistance.value + '米'
// 更新热区
circleRadius.value = itemDistance.value;
circle.value.setRadius(itemDistance.value);
updateCircleHandlePosition();
// 重新渲染数据点
renderMassMarks()
}
// 初始化地图
const loadMap = () => {
return new Promise((resolve, reject) => {
// 设置安全密钥
window._AMapSecurityConfig = {
securityJsCode: secretKey
};
AMapLoader.load({
key: apiKey,
plugins: ["AMap.PlaceSearch"],
AMapUI: {
plugins: []
}
}).then(AMap => {
// 根据当前模式设置初始参数
const initialPitch = mapMode.value === '3D' ? 65 : 0;
const initialRotation = mapMode.value === '3D' ? -15 : 0;
mapInstance.value = new AMap.Map("mapContainer", {
resizeEnable: true,
// center: [116.397428, 39.90923], // 默认中心点
pitch: initialPitch, // 倾斜角度决定2D/3D模式
rotation: initialRotation, // 旋转角度
// buildingAnimation: true,
// expandZoomRange: true,
zoom: 16,
zooms: [3, 16],
viewMode: mapMode.value // 启用3D视图
});
// // 先添加基本控件
mapInstance.value.setZoom(16);
placeSearch.value = new AMap.PlaceSearch({
// city 指定搜索所在城市支持传入格式有城市名、citycode和adcode
city: '全国'
})
// 添加自定义控件容器
addCustomControls(AMap);
// 地图加载完成后隐藏Logo
setTimeout(() => {
hideAmapLogo();
}, 1000);
// 监听地图渲染完成事件
mapInstance.value.on('render', hideAmapLogo);
// 添加缩放变化监听
mapInstance.value.on('zoomchange', handleZoomChange);
if (selectConfig.value !== null) {
currentSelect(selectConfig.value)
} else {
// 在地图完全加载后执行点数据处理
renderMassMarks();
}
resolve();
}).catch(e => {
console.log(e, "高德地图加载失败");
reject(e);
});
});
}
// 处理地图缩放事件
const handleZoomChange = () => {
const currentZoomLevel = mapInstance.value.getZoom();
// 如果缩放级别超过16自动调整回16
if (currentZoomLevel > 16) {
mapInstance.value.setZoom(16);
return;
}
}
// 添加自定义控件
const addCustomControls = (AMap) => {
// 创建控件容器
const controlContainer = document.createElement('div');
controlContainer.className = 'custom-map-controls';
// 2D/3D切换按钮
const toggle2D3DBtn = document.createElement('div');
toggle2D3DBtn.className = mapMode.value === '2D' ? 'map-control-btn map-control-3dbtn' : 'map-control-btn map-control-2dbtn';
toggle2D3DBtn.onclick = toggle2D3DMode;
// 全屏按钮
const fullscreenBtn = document.createElement('div');
fullscreenBtn.className = isFullscreen.value === true ? 'map-control-btn map-full-screen' : 'map-control-btn map-nofull-screen';
fullscreenBtn.onclick = toggleFullscreen;
controlContainer.appendChild(toggle2D3DBtn);
controlContainer.appendChild(fullscreenBtn);
// 添加到地图容器
document.getElementById('mapContainer').appendChild(controlContainer);
}
// 切换2D/3D模式
const toggle2D3DMode = () => {
if (!mapInstance.value) return;
const currentPitch = mapInstance.value.getPitch();
if (currentPitch === 0) {
// 切换到3D模式
mapInstance.value.setPitch(65);
mapInstance.value.setRotation(-15);
mapMode.value = '3D';
loadMap()
} else {
// 切换到2D模式
mapInstance.value.setPitch(0);
mapInstance.value.setRotation(0);
mapMode.value = '2D';
loadMap()
}
}
// 切换全屏模式
const toggleFullscreen = () => {
const mapContainer = document.getElementById('mapContainer');
if (!document.fullscreenElement) {
// 进入全屏
if (mapContainer.requestFullscreen) {
mapContainer.requestFullscreen().then(() => {
isFullscreen.value = true;
const buttons = document.querySelectorAll('.map-control-btn');
buttons[1].className = 'map-control-btn map-full-screen';
}).catch(err => {
console.error('全屏模式错误:', err);
});
}
} else {
// 退出全屏
if (document.exitFullscreen) {
document.exitFullscreen().then(() => {
isFullscreen.value = false;
const buttons = document.querySelectorAll('.map-control-btn');
buttons[1].className = 'map-control-btn map-nofull-screen';
});
}
}
}
// 监听全屏变化事件
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
isFullscreen.value = false;
}
});
// 创建自定义点样式
const createPointStyle = (color, size, styleType) => {
const canvas = document.createElement('canvas');
canvas.width = size * 2;
canvas.height = size * 2;
const ctx = canvas.getContext('2d');
// 根据样式类型绘制不同的图形
switch (styleType) {
case 'circle':
ctx.beginPath();
ctx.arc(size, size, size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
break;
case 'ring':
ctx.beginPath();
ctx.arc(size, size, size, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fill();
ctx.beginPath();
ctx.arc(size, size, size * 0.7, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
break;
case 'square':
ctx.fillStyle = color;
ctx.fillRect(0, 0, size * 2, size * 2);
break;
case 'star':
ctx.fillStyle = color;
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 2 * Math.PI / 5) - Math.PI / 2;
const x = size + size * Math.cos(angle);
const y = size + size * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
const innerAngle = angle + Math.PI / 5;
const innerX = size + size * 0.4 * Math.cos(innerAngle);
const innerY = size + size * 0.4 * Math.sin(innerAngle);
ctx.lineTo(innerX, innerY);
}
ctx.closePath();
ctx.fill();
break;
}
return canvas;
}
// 渲染海量点
const renderMassMarks = () => {
// 清除现有的海量点
if (massMarks.value) {
massMarks.value.clear();
}
// 创建三种不同大小的样式
const styles = [
// 优势媒体颜色-红色
{
url: createPointStyle('#EC1B60', 18, 'circle').toDataURL(),
anchor: new AMap.Pixel(18, 18),
size: new AMap.Size(18, 18)
},
// 网络媒体颜色-蓝色
{
url: createPointStyle('#058DED', 18, 'circle').toDataURL(),
anchor: new AMap.Pixel(18, 18),
size: new AMap.Size(18, 18)
},
];
// 调用接口,获取数据点
points.value = []
mediaByMap(queryParams.value).then(res => {
if (res.code == 200) {
res.data.forEach(itemPoint => {
if (itemPoint.businessType == 1) points.value.push({ "lnglat": [itemPoint.x, itemPoint.y], "name": itemPoint.mediaId, "style": 0 })
if (itemPoint.businessType == 2) points.value.push({ "lnglat": [itemPoint.x, itemPoint.y], "name": itemPoint.mediaId, "style": 1 })
});
}
}).then(res => {
console.log('points', points.value)
// 创建MassMarks对象
massMarks.value = new AMap.MassMarks(points.value, {
opacity: 1,
zIndex: 111,
cursor: 'pointer',
style: styles
});
// 将海量点添加到地图
massMarks.value.setMap(mapInstance.value);
// 添加点击事件
massMarks.value.on('click', function (e) {
console.log('点击了节点', e.data)
});
})
}
// 隐藏Logo的函数
const hideAmapLogo = () => {
const logos = document.querySelectorAll('.amap-logo, .amap-copyright');
logos.forEach(logo => {
logo.style.display = 'none';
logo.style.visibility = 'hidden';
logo.style.opacity = '0';
});
}
onUnmounted(() => {
if (mapInstance.value) {
mapInstance.value.destroy();
}
})
// 初始化
onMounted(() => {
bgStore.setBgImage(otherbg)
loadMap()
});
</script>
<style lang="scss">
.mediaCard .el-card__body {
padding: 0px !important;
}
.mediaMapBtn {
min-width: 122px;
height: 34px;
border-radius: 4px;
background: #1a75e6;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 16px;
text-align: left;
color: #FFFFFF;
padding: 10px;
}
.mediaMapBtn>span {
align-items: center;
display: inline-flex;
width: 100%;
}
.mediaMapBtn:hover {
background: #1a75e6;
}
.distanceItem {
position: relative;
padding-left: 40px;
padding-right: 30px;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 16px;
color: #1E1E1E;
}
.distanceItem:hover {
font-weight: 600;
color: #1A75E6;
}
.distanceItemActive {
position: relative;
padding-left: 40px;
padding-right: 30px;
font-family: Microsoft YaHei;
font-weight: 600;
font-size: 16px;
color: #1A75E6;
}
.distanceItem:hover::before,
.distanceItemActive::before {
content: "√";
position: absolute;
left: 17px;
top: 6px;
width: 13px;
height: 10px;
font-family: Microsoft YaHei;
font-weight: 600;
color: #1A75E6;
}
.clearBtn {
display: inline-flex;
margin-left: 20px;
padding-left: 18px;
position: relative;
min-width: 82px;
height: 34px;
line-height: 34px;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 16px;
color: #87898E;
cursor: pointer;
}
.clearBtn::before {
content: "";
position: absolute;
left: 0px;
top: 10px;
width: 14px;
height: 14px;
background-image: url('../../assets/images/iconClearWhere.png');
background-repeat: no-repeat;
}
.mediaMapContainer {
width: 100%;
height: calc(100vh - 170px);
background: #3f8bff;
}
.custom-map-controls {
position: absolute;
top: 5px;
right: 5px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 0px;
}
.map-control-btn {
cursor: pointer;
}
.map-control-2dbtn {
width: 40px;
height: 40px;
background: url('../../assets/images/icon-2D-btn.png');
}
.map-control-2dbtn:hover {
background: url('../../assets/images/icon-2D-active-btn.png');
}
.map-control-3dbtn {
width: 40px;
height: 40px;
background: url('../../assets/images/icon-3D-btn.png');
}
.map-control-3dbtn:hover {
background: url('../../assets/images/icon-3D-active-btn.png');
}
.map-nofull-screen {
width: 40px;
height: 40px;
background: url('../../assets/images/icon-full-screen.png');
}
.map-nofull-screen:hover {
background: url('../../assets/images/icon-full-screen-active.png');
}
.map-full-screen {
width: 40px;
height: 40px;
background: url('../../assets/images/icon-map-nofull-screen.png');
}
.map-full-screen:hover {
background: url('../../assets/images/icon-map-nofull-screen-active.png');
}
/* 全屏样式 */
.mapContainer:-webkit-full-screen {
width: 100% !important;
height: 100% !important;
}
.mapContainer:-moz-full-screen {
width: 100% !important;
height: 100% !important;
}
.mapContainer:-ms-fullscreen {
width: 100% !important;
height: 100% !important;
}
.mapContainer:fullscreen {
width: 100% !important;
height: 100% !important;
}
.one-text {
height: 50px;
padding-top: 4px;
}
</style>