370 lines
10 KiB
Vue
370 lines
10 KiB
Vue
<script lang="ts" setup>
|
||
import AMapLoader from '@amap/amap-jsapi-loader';
|
||
import { getVillageByIdAPI } from '@/api/system/community';
|
||
import { onUnmounted, ref, watch, onMounted, nextTick } from 'vue';
|
||
import { LngLat } from '@/components/Map/type';
|
||
|
||
// 密钥配置
|
||
const autoloader = import.meta.env.VITE_GCJ02_KEY_GD;
|
||
const VITE_GCJ02_JS_CODE_GD = import.meta.env.VITE_GCJ02_JS_CODE_GD;
|
||
window._AMapSecurityConfig = { securityJsCode: VITE_GCJ02_JS_CODE_GD };
|
||
|
||
// 全局实例
|
||
const AMAP = ref<any>(null);
|
||
const map = ref<any>(null);
|
||
const mapReady = ref(false);
|
||
const mapLoading = ref(false);
|
||
const markers = ref<any[]>([]);
|
||
const mouseTool = ref<any>(null);
|
||
const AddMarkered = new Map<string, any>();
|
||
let drawnPolylines: any[] = [];
|
||
|
||
// Props & Emits
|
||
const props = defineProps({
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
clickOrDraw: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
smallScroll: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
});
|
||
const emits = defineEmits<{
|
||
clickPoint: [value: LngLat];
|
||
}>();
|
||
|
||
// 对外暴露方法
|
||
defineExpose({
|
||
addMarker_only,
|
||
clearLocation,
|
||
addMarker_more,
|
||
clearAll,
|
||
getAllDrawLines,
|
||
renderSavedPath,
|
||
clearMapMarker
|
||
});
|
||
|
||
const village = localStorage.getItem('villageid');
|
||
|
||
// ===================== 工具函数 =====================
|
||
/** 地图完整可用校验 */
|
||
function isMapAvailable() {
|
||
return mapReady.value && map.value && AMAP.value;
|
||
}
|
||
|
||
/** 统一适配视野,无动画 + 留白 + 最大缩放限制 */
|
||
function fitMapView(overlays: any[]) {
|
||
if (!isMapAvailable() || overlays.length === 0) return;
|
||
// 第二个参数true关闭动画,留白80px,最大缩放18级
|
||
map.value.setFitView(overlays, props.smallScroll);
|
||
}
|
||
|
||
// ===================== 渲染等待队列(响应式) =====================
|
||
type WaitMarkerItem = { angular: number[]; pointName: string };
|
||
type WaitMoreMarkerItem = { angular: number[]; pointName: string; pointId: string };
|
||
const waitQueue = ref<{
|
||
onlyPoints: WaitMarkerItem[];
|
||
point: WaitMoreMarkerItem[];
|
||
routes: string[];
|
||
clear: boolean;
|
||
}>({
|
||
onlyPoints: [],
|
||
point: [],
|
||
routes: [],
|
||
clear: false
|
||
});
|
||
|
||
// 地图就绪后批量执行队列任务
|
||
watch(mapReady, async (ready) => {
|
||
if (!ready || !isMapAvailable()) return;
|
||
await nextTick();
|
||
|
||
// 先执行清空
|
||
if (waitQueue.value.clear) {
|
||
map.value.clearMap();
|
||
AddMarkered.clear();
|
||
markers.value = [];
|
||
drawnPolylines = [];
|
||
waitQueue.value.clear = false;
|
||
}
|
||
|
||
// 批量多点标记
|
||
waitQueue.value.point.forEach((item) => addMarker_more(item.angular, item.pointName, item.pointId));
|
||
waitQueue.value.point = [];
|
||
|
||
// 单点标记
|
||
waitQueue.value.onlyPoints.forEach((item) => addMarker_only(item.angular, item.pointName));
|
||
waitQueue.value.onlyPoints = [];
|
||
|
||
// 路线回显
|
||
waitQueue.value.routes.forEach((pathStr) => renderSavedPath(pathStr));
|
||
waitQueue.value.routes = [];
|
||
});
|
||
|
||
// ===================== 单点标记 =====================
|
||
function addMarker_only(angular: number[], pointName: string) {
|
||
if (!isMapAvailable()) {
|
||
waitQueue.value.onlyPoints.push({ angular, pointName });
|
||
return;
|
||
}
|
||
clearLocation();
|
||
const marker = new AMAP.value.Marker({
|
||
position: angular,
|
||
map: map.value,
|
||
title: pointName || '巡检点',
|
||
content: `
|
||
<div style="display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:rgba(26,117,255,0.3);">
|
||
<div style="width:20px;height:20px;border-radius:50%;line-height:20px;background:#1a75ff;color:#fff;font-size:20px;text-align:center;">1</div>
|
||
</div>`
|
||
});
|
||
markers.value = [marker];
|
||
fitMapView(markers.value);
|
||
return marker;
|
||
}
|
||
|
||
/** 清空单点标记 */
|
||
function clearLocation() {
|
||
if (!isMapAvailable() || markers.value.length === 0) return;
|
||
markers.value.forEach((item) => map.value.remove(item));
|
||
markers.value = [];
|
||
}
|
||
|
||
let lastFitTime = 0;
|
||
let fitTimer: ReturnType<typeof setTimeout> | null = null;
|
||
const FIT_THROTTLE_MS = 100; // 节流间隔(ms)
|
||
// === 新增函数 ===
|
||
function throttledFitView() {
|
||
const now = Date.now();
|
||
if (now - lastFitTime >= FIT_THROTTLE_MS) {
|
||
// 距离上次调用已超过阈值,立即执行
|
||
if (fitTimer) {
|
||
clearTimeout(fitTimer);
|
||
fitTimer = null;
|
||
}
|
||
fitMapView(Array.from(AddMarkered.values()));
|
||
lastFitTime = now;
|
||
} else if (!fitTimer) {
|
||
// 未超过阈值,延迟到阈值时再执行
|
||
fitTimer = setTimeout(() => {
|
||
fitMapView(Array.from(AddMarkered.values()));
|
||
lastFitTime = Date.now();
|
||
fitTimer = null;
|
||
}, FIT_THROTTLE_MS);
|
||
}
|
||
// 如果已经有定时器在等,不重复创建
|
||
}
|
||
|
||
// ===================== 多点标记(可增删) =====================
|
||
function addMarker_more(angular: number[], pointName: string, pointId: string) {
|
||
if (!isMapAvailable()) {
|
||
waitQueue.value.point.push({ angular, pointName, pointId });
|
||
return;
|
||
}
|
||
// 存在则删除
|
||
if (AddMarkered.has(pointId)) {
|
||
map.value.remove(AddMarkered.get(pointId));
|
||
AddMarkered.delete(pointId);
|
||
// 删除后重新适配视野
|
||
// fitMapView(Array.from(AddMarkered.values()));
|
||
throttledFitView();
|
||
return;
|
||
}
|
||
|
||
const index = AddMarkered.size + 1;
|
||
const marker = new AMAP.value.Marker({
|
||
position: angular,
|
||
map: map.value,
|
||
title: pointName || '巡检点',
|
||
content: `
|
||
<div style="display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;background:rgba(26,117,255,0.3);">
|
||
<div style="width:20px;height:20px;border-radius:50%;line-height:20px;background:#1a75ff;color:#fff;font-size:20px;text-align:center;">${index}</div>
|
||
</div>`
|
||
});
|
||
AddMarkered.set(pointId, marker);
|
||
markers.value.push(marker);
|
||
// fitMapView(Array.from(AddMarkered.values()));
|
||
throttledFitView();
|
||
return marker;
|
||
}
|
||
|
||
// ===================== 绘制路线 =====================
|
||
function MapDrawLine() {
|
||
if (!isMapAvailable()) return;
|
||
mouseTool.value = new AMAP.value.MouseTool(map.value);
|
||
mouseTool.value.on('draw', (e: any) => {
|
||
if (!e.obj) return;
|
||
// 只保留一条线
|
||
if (drawnPolylines.length > 0) map.value.remove(drawnPolylines[0]);
|
||
drawnPolylines = [e.obj];
|
||
initDrawTool();
|
||
});
|
||
initDrawTool();
|
||
}
|
||
|
||
function initDrawTool() {
|
||
if (!mouseTool.value || !isMapAvailable()) return;
|
||
mouseTool.value.close(false);
|
||
mouseTool.value.polyline({
|
||
strokeColor: '#3366FF',
|
||
strokeOpacity: 1,
|
||
strokeWeight: 3,
|
||
strokeStyle: 'solid'
|
||
});
|
||
}
|
||
|
||
/** 获取路线坐标数组 */
|
||
function getAllDrawLines(): LngLat[] | [] {
|
||
if (!isMapAvailable() || drawnPolylines.length === 0) return [];
|
||
const path = drawnPolylines[0].getPath();
|
||
return path.map((p: any) => [p.lng, p.lat]);
|
||
}
|
||
|
||
/** 回显历史路线 */
|
||
function renderSavedPath(pathStr: string) {
|
||
if (!isMapAvailable()) {
|
||
waitQueue.value.routes.push(pathStr);
|
||
return;
|
||
}
|
||
try {
|
||
const path = JSON.parse(pathStr);
|
||
if (!Array.isArray(path) || path.length === 0) return;
|
||
// 清空旧线
|
||
drawnPolylines.forEach((line) => map.value.remove(line));
|
||
drawnPolylines = [];
|
||
|
||
const polyline = new AMAP.value.Polyline({
|
||
path,
|
||
strokeColor: '#3366FF',
|
||
strokeOpacity: 1,
|
||
strokeWeight: 3,
|
||
strokeStyle: 'solid',
|
||
map: map.value
|
||
});
|
||
drawnPolylines.push(polyline);
|
||
fitMapView([polyline]);
|
||
} catch (err) {
|
||
console.error('路线解析失败', err);
|
||
}
|
||
}
|
||
|
||
/** 清空绘图工具+路线 */
|
||
function clearAll() {
|
||
if (!isMapAvailable() || !mouseTool.value) return;
|
||
mouseTool.value.close(true);
|
||
drawnPolylines.forEach((line) => map.value.removeOverlay(line));
|
||
drawnPolylines = [];
|
||
initDrawTool();
|
||
}
|
||
|
||
/** 清空地图所有覆盖物(标记+路线) */
|
||
function clearMapMarker() {
|
||
if (isMapAvailable()) {
|
||
map.value.clearMap();
|
||
AddMarkered.clear();
|
||
markers.value = [];
|
||
drawnPolylines = [];
|
||
} else {
|
||
waitQueue.value.clear = true;
|
||
}
|
||
}
|
||
|
||
/** 地图点击拾取坐标 */
|
||
function handleMap() {
|
||
if (!isMapAvailable()) return;
|
||
map.value.on('click', (e: any) => {
|
||
const { lng, lat } = e.lnglat;
|
||
emits('clickPoint', [lng, lat]);
|
||
});
|
||
}
|
||
|
||
// ===================== 地图初始化 =====================
|
||
onMounted(async () => {
|
||
mapLoading.value = true;
|
||
try {
|
||
const AMap = await AMapLoader.load({
|
||
key: autoloader,
|
||
version: '2.0',
|
||
plugins: ['AMap.Scale', 'AMap.Marker', 'AMap.Geocoder', 'AMap.MouseTool']
|
||
});
|
||
AMAP.value = AMap;
|
||
|
||
// 创建地图,全局关闭动画,解决移动缓慢
|
||
map.value = new AMap.Map('GD-Map', {
|
||
viewMode: '3D',
|
||
zoom: 11,
|
||
center: [116.397428, 39.90923]
|
||
});
|
||
|
||
try {
|
||
// 获取村庄城市定位
|
||
const villageInfo = await getVillageByIdAPI(village);
|
||
const city = villageInfo.data.city.replaceAll('/', '');
|
||
// 城市地理编码定位
|
||
const geocoder = new AMap.Geocoder({ city });
|
||
geocoder.getLocation(city, (status: string, result: any) => {
|
||
if (status === 'complete' && result.info === 'OK') {
|
||
map.value.setCenter(result.geocodes[0].location.toArray());
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.error(e);
|
||
map.value.setCenter([116.397428, 39.90923]);
|
||
}
|
||
|
||
mapReady.value = true;
|
||
|
||
// 禁用状态不开启绘制/点击
|
||
if (!props.disabled) {
|
||
props.clickOrDraw ? handleMap() : MapDrawLine();
|
||
}
|
||
} catch (err) {
|
||
console.error('高德地图加载失败', err);
|
||
} finally {
|
||
mapLoading.value = false;
|
||
}
|
||
});
|
||
|
||
// ===================== 销毁清理 =====================
|
||
onUnmounted(() => {
|
||
// 销毁地图实例
|
||
map.value?.destroy();
|
||
// 清空所有缓存队列
|
||
waitQueue.value.onlyPoints = [];
|
||
waitQueue.value.point = [];
|
||
waitQueue.value.routes = [];
|
||
waitQueue.value.clear = false;
|
||
// 清空覆盖物缓存
|
||
markers.value = [];
|
||
drawnPolylines = [];
|
||
AddMarkered.clear();
|
||
mouseTool.value = null;
|
||
AMAP.value = null;
|
||
map.value = null;
|
||
mapReady.value = false;
|
||
if (fitTimer) {
|
||
clearTimeout(fitTimer);
|
||
fitTimer = null;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div id="GD-Map" v-loading="mapLoading" class="map"></div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.map {
|
||
width: 100%;
|
||
min-height: 450px;
|
||
background-color: #f0f0f0;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
margin-top: 10px;
|
||
}
|
||
</style>
|