项目结构:

TypeScript
<!--
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 神兽保佑 永无BUG
*
* @Author: geovindu
* @Date: 2025-05-26 14:23:15
* @LastEditors: geovindu
* @LastEditTime: 2025-05-26 14:29:48
* @FilePath: \vue\vuepdfpreview\src\viewer\tmap.vue
* @Description: geovindu
* @lib,packpage:
*
* @IDE: vscode
* @jslib: node 20 vue.js 3.0
* @OS: windows10
* @database: mysql 8.0 sql server 2019 postgreSQL 16
* Copyright (c) geovindu 2025 by [email protected], All Rights Reserved.
-->
<template>
<div id="app">
<h1>深圳酒店地图(腾讯版)</h1>
<div v-if="loading" class="loading-message">加载中...</div>
<div v-else-if="error" class="error-message">
加载失败: {{ error.message }}
</div>
<TencentMapMarker v-else :hotels="hotels" :ak="tencentMapAK" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import TencentMapMarker from '../components/TencentMapMarker.vue';
// 腾讯地图API密钥(需要替换为你自己的)
const tencentMapAK = ref('your key');
const hotels = ref<any[]>([]);
const loading = ref(true);
const error = ref<Error | null>(null);
// 从JSON文件加载酒店数据
const loadHotelData = async () => {
try {
const response = await fetch('hotels.json');
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
const data = await response.json();
hotels.value = data;
console.log('酒店数据加载成功:', data);
} catch (err: any) {
console.error('加载酒店数据失败:', err);
error.value = err;
} finally {
loading.value = false;
}
};
onMounted(() => {
loadHotelData();
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
padding: 0 20px;
}
h1 {
color: #333;
}
.loading-message, .error-message {
padding: 20px;
margin: 20px;
background-color: #f5f5f5;
border-radius: 8px;
}
.error-message {
color: #f56c6c;
}
</style>
TypeScript
<!--
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 神兽保佑 永无BUG
*
* @Author: geovindu
* @Date: 2025-05-26 14:23:15
* @LastEditors: geovindu
* @LastEditTime: 2025-05-29 17:38:46
* @FilePath: \vue\vuepdfpreview\src\components\TencentMapMarker.vue
* @Description: geovindu
* @lib,packpage:
*
* @IDE: vscode
* @jslib: node 20 vue.js 3.0
* @OS: windows10
* @database: mysql 8.0 sql server 2019 postgreSQL 16
* Copyright (c) geovindu 2025 by [email protected], All Rights Reserved.
-->
<template>
<div class="map-container" ref="mapContainer" style="height: 600px;"></div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, defineComponent } from 'vue';
interface Hotel {
name: string;
content: string;
center: string;
type: number;
icon: string;
}
const props = defineProps<{
hotels: Hotel[];
ak: string;
}>();
const mapContainer = ref<HTMLElement | null>(null);
let map: TMap.Map | null = null;
let markerLayer: TMap.MultiMarker | null = null;
let infoWindow: TMap.InfoWindow | null = null;
let currentHotel = ref<Hotel | null>(null);
// 加载腾讯地图API
const loadTMapScript = (ak: string) => {
return new Promise<void>((resolve, reject) => {
if (window.TMap) {
resolve();
return;
}
const script = document.createElement('script');
script.src = `https://map.qq.com/api/gljs?v=1.0&key=${ak}`;
script.async = true;
script.onload = () => {
if (window.TMap) {
resolve();
} else {
reject(new Error('腾讯地图API加载失败,但脚本已加载'));
}
};
script.onerror = (err) => reject(new Error('加载腾讯地图API失败,请检查网络连接或API密钥'));
document.head.appendChild(script);
});
};
// 初始化地图
const initMap = async () => {
if (!mapContainer.value || !props.ak) {
console.error('地图容器或API密钥未设置');
return;
}
try {
await loadTMapScript(props.ak);
map = new TMap.Map(mapContainer.value, {
center: new TMap.LatLng(22.543099, 114.057868),
zoom: 12
});
// 创建信息窗口(不关联map,在显示时动态关联)
infoWindow = new TMap.InfoWindow({
enableCustom: true,
map: map,
position: new TMap.LatLng(39.984104, 116.307503),
offset: { y: -70, x: -5 }
});
initMarkerLayer();
console.log('腾讯地图初始化完成');
} catch (error: any) {
console.error('地图初始化失败:', error.message);
}
};
// 初始化标记图层
const initMarkerLayer = () => {
if (!map) return;
markerLayer = new TMap.MultiMarker({
id: 'hotel-markers',
map: map,
styles: {
'hotel-marker': new TMap.MarkerStyle({
width: 32,
height: 32,
anchor: { x: 16, y: 32 },
src: 'https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png'
})
},
geometries: []
});
// 标记点击事件
markerLayer.on('click', (event) => {
const geometry = event.geometry;
if (!geometry || !geometry.position || !(geometry.position instanceof TMap.LatLng)) {
console.error('无效的标记位置:', geometry);
return;
}
if (infoWindow) {
//console.log('信息窗口是否打开:', infoWindow.isOpen());
//infoWindow.open();
// 更新当前酒店数据
currentHotel.value = {
name: geometry.properties?.name || '酒店信息',
content: geometry.properties?.content || '暂无描述',
center: '',
type: 0,
icon: ''
};
// 创建唯一ID的DOM容器
const containerId = 'info-window-content-' + Date.now();
const container = document.createElement('div');
container.id = containerId;
document.body.appendChild(container);
// 设置信息窗口内容
infoWindow.setContent(`
<div class="info_card">
<div class="title">
<span class="title_name">${currentHotel.value.name}</span>
<button class="close_btn" id="close-btn-${containerId}">×</button>
</div>
<div class="content">${currentHotel.value.content}</div>
</div>
`);
// 使用 setTimeout 确保DOM已渲染
/* */
setTimeout(() => {
// 添加关闭按钮事件监听
const closeBtn = document.getElementById(`close-btn-${containerId}`);
if (closeBtn) {
closeBtn.addEventListener('click', () => {
if (infoWindow) infoWindow.close();
});
}
// 关联地图并显示
infoWindow.setMap(map);
infoWindow.setPosition(geometry.position);
infoWindow.open();
}, 0);
}
});
updateMarkers();
};
// 更新标记
const updateMarkers = () => {
if (!markerLayer || !props.hotels || props.hotels.length === 0) return;
const geometries = props.hotels.map((hotel, index) => {
try {
const [lng, lat] = hotel.center.split(',').map(Number);
return {
id: `hotel-${index}`,
styleId: 'hotel-marker',
position: new TMap.LatLng(lat, lng),
properties: {
name: hotel.name,
content: hotel.content,
type: hotel.type
}
};
} catch (e) {
console.error(`酒店数据解析错误 (${hotel.name}):`, e);
return null;
}
}).filter(Boolean) as TMap.MultiMarkerGeometry[];
markerLayer.setGeometries(geometries);
};
onMounted(() => {
initMap();
});
watch(() => props.hotels, () => {
if (markerLayer) updateMarkers();
});
onUnmounted(() => {
if (infoWindow) {
infoWindow.close();
infoWindow.setMap(null);
}
if (markerLayer) markerLayer.setMap(null);
if (map) {
map.destroy();
map = null;
}
markerLayer = null;
infoWindow = null;
});
</script>
<style scoped>
.map-container {
width: 100%;
height: 100%;
}
/* 信息窗口样式 */
.info_card {
width: 240px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
overflow: hidden;
font-family: Arial, sans-serif;
}
.title {
height: 36px;
line-height: 36px;
background-color: #f5f5f5;
padding: 0 12px;
position: relative;
}
.title_name {
font-weight: bold;
color: #333;
font-size: 15px;
}
.close_btn {
position: absolute;
right: 10px;
top: 8px;
width: 20px;
height: 20px;
background: none;
border: none;
font-size: 18px;
color: #999;
cursor: pointer;
padding: 0;
}
.close_btn:hover {
color: #333;
}
.content {
padding: 12px;
font-size: 14px;
color: #666;
line-height: 1.5;
}
</style>
输出: