项目结构:
<template>
<div class="bing-map-market">
<!-- 加载遮罩层 -->
<div class="loading-overlay" v-show="isLoading || errorMessage">
<div class="spinner-container">
<div class="spinner-border text-primary" role="status"></div>
<p>{{ isLoading ? '加载地图中...' : errorMessage }}</p>
</div>
</div>
<!-- 调试信息 -->
<div class="debug-info" v-show="debugMode">
<p>isLoading: {{ isLoading }}</p>
<p>mapLoaded: {{ mapLoaded }}</p>
<p>mapSize: {{ mapSize.width }} x {{ mapSize.height }}</p>
<p>error: {{ errorMessage }}</p>
</div>
<div class="container">
<div class="stats">
<div class="stat-card">
<h3><i class="fa fa-map-marker text-primary"></i> 总位置数</h3>
<p class="stat-value">{{ locations.length }}</p>
</div>
<div class="stat-card">
<h3><i class="fa fa-users text-success"></i> 覆盖人群</h3>
<p class="stat-value">1,250,000+</p>
</div>
<div class="stat-card">
<h3><i class="fa fa-line-chart text-warning"></i> 转化率</h3>
<p class="stat-value">8.2%</p>
</div>
<div class="stat-card">
<h3><i class="fa fa-calendar text-info"></i> 更新日期</h3>
<p class="stat-value">2025-06-01</p>
</div>
</div>
<!-- 使用固定高度容器,防止尺寸变化 -->
<div ref="mapContainer" class="map-container"></div>
<div class="chart-container">
<h3>区域表现分析</h3>
<canvas id="performanceChart" height="100"></canvas>
</div>
<div class="location-list">
<h3>重点关注位置</h3>
<div v-if="!locations.length" class="text-center text-muted py-5">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p>加载位置数据中...</p>
</div>
<div
v-for="location in locations"
:key="location.name"
class="location-item"
@click="focusOnLocation(location)"
>
<h4><i :class="getLocationIconClass(location)"></i> {{ location.name }}</h4>
<p>{{ location.address || '未提供地址' }}</p>
<div class="location-stats">
<span :class="getTrafficBadgeClass(location.traffic)">人流量: {{ location.traffic }}</span>
<span :class="getConversionBadgeClass(location.conversionRate)">转化率: {{ location.conversionRate }}%</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import locationsData from '@/data/city.json';
// 类型定义
interface Location {
lat: number;
lng: number;
name: string;
category: 'office' | 'store';
traffic: '极低' | '低' | '中' | '高' | '极高';
conversionRate: string;
address?: string;
population?: string;
hours?: string;
phone?: string;
}
// 状态管理
const isLoading = ref(true);
const errorMessage = ref('');
const locations = ref<Location[]>([]);
const map = ref<any>(null);
const infoBox = ref<any>(null);
const mapContainer = ref<HTMLElement | null>(null);
const mapLoaded = ref(false);
const mapInitialized = ref(false);
const mapSize = ref({ width: 0, height: 0 });
const debugMode = ref(true);
const resizeObserver = ref<ResizeObserver | null>(null);
const mapResizeHandler = ref<() => void | null>(null);
// 全局API加载Promise
let bingMapsApiPromise: Promise<void> | null = null;
// 加载Bing Maps API
const loadBingMapsApi = () => {
if (bingMapsApiPromise) {
return bingMapsApiPromise;
}
bingMapsApiPromise = new Promise<void>((resolve, reject) => {
console.log('开始加载 Bing Maps API...');
const script = document.createElement('script');
script.src = 'https://www.bing.com/api/maps/mapcontrol?callback=bingMapsCallback&mkt=zh-cn';
script.async = true;
script.defer = true;
window.bingMapsCallback = () => {
console.log('Bing Maps API 加载完成');
if (!window.Microsoft || !Microsoft.Maps) {
reject(new Error('Bing Maps API 加载但未正确初始化'));
return;
}
resolve();
};
script.onerror = () => reject(new Error('Bing Maps API 加载失败'));
document.head.appendChild(script);
// 设置超时
setTimeout(() => {
if (!window.Microsoft || !Microsoft.Maps) {
reject(new Error('Bing Maps API 加载超时'));
}
}, 10000);
});
return bingMapsApiPromise;
};
// 初始化地图
const initializeMap = async () => {
try {
if (!mapContainer.value) {
throw new Error('地图容器不存在');
}
// 确保API已加载
await loadBingMapsApi();
// 创建地图实例
map.value = new Microsoft.Maps.Map(mapContainer.value, {
credentials: '你的KYE',
center: new Microsoft.Maps.Location(35.8617, 104.1954), // 中国中心点
zoom: 4,
culture: 'zh-CN',
region: 'cn',
mapTypeId: Microsoft.Maps.MapTypeId.road,
showMapTypeSelector: true,
enableSearchLogo: false,
showBreadcrumb: false,
animate: false, // 禁用初始动画
// 防止地图自动调整视图
suppressInfoWindows: true,
disableBirdseye: true,
showScalebar: false
});
mapInitialized.value = true;
console.log('地图实例已创建');
// 记录地图容器尺寸
updateMapSize();
// 添加地图加载完成事件
await new Promise((resolve) => {
if (!map.value) {
resolve(null);
return;
}
// 快速检测
if (map.value.getRootElement()) {
console.log('地图已加载(快速检测)');
mapLoaded.value = true;
resolve(null);
return;
}
// 事件监听
Microsoft.Maps.Events.addHandler(map.value, 'load', () => {
console.log('地图加载完成(事件触发)');
mapLoaded.value = true;
resolve(null);
});
// 超时处理
setTimeout(() => {
console.log('地图加载超时,使用备用方案');
mapLoaded.value = true;
resolve(null);
}, 5000);
});
// 添加位置点并调整视野
addLocationsToMap();
// 初始化图表
initializeChart();
// 添加容器尺寸变化监听
setupResizeObserver();
// 隐藏加载状态
isLoading.value = false;
} catch (error: any) {
console.error('初始化地图时出错:', error);
errorMessage.value = error.message || '地图初始化失败';
isLoading.value = false;
}
};
// 添加位置到地图
const addLocationsToMap = () => {
if (!map.value || !locations.value.length) {
console.warn('地图未初始化或位置数据为空');
return;
}
try {
const layer = new Microsoft.Maps.Layer();
if (!layer || typeof layer.add !== 'function') {
throw new Error('无法创建地图图层');
}
map.value.layers.insert(layer);
locations.value.forEach((location) => {
try {
const pin = new Microsoft.Maps.Pushpin(
new Microsoft.Maps.Location(location.lat, location.lng),
{
title: location.name,
subTitle: location.category === "office" ? "办公地点" : "零售门店",
//color: location.category === "office" ? "#0066cc" : "#cc0000", //颜色标记
icon: location.category === "office" ? '3.png':'21.png', //自定义图片
text: location.category === "office" ? "公" : "店",
textOffset: new Microsoft.Maps.Point(0, 5),
anchor: new Microsoft.Maps.Point(12, 39),
enableClickedStyle: true
}
);
if (!pin) {
console.error('无法创建标记:', location.name);
return;
}
(pin as any).locationData = location;
if (Microsoft.Maps.Events && typeof Microsoft.Maps.Events.addHandler === 'function') {
Microsoft.Maps.Events.addHandler(pin, 'click', (e: any) => {
const locationData = (e.target as any).locationData;
if (locationData) {
showInfoWindow(locationData);
}
});
}
layer.add(pin);
} catch (error) {
console.error(`添加位置 ${location.name} 时出错:`, error);
}
});
console.log(`成功添加 ${locations.value.length} 个标记`);
// 延迟调整视野,避免闪烁
setTimeout(() => {
adjustMapView();
}, 1000);
} catch (error) {
console.error('添加位置到地图时出错:', error);
errorMessage.value = '地图标记加载失败';
}
};
// 调整地图视野
const adjustMapView = () => {
if (!map.value || !locations.value.length) return;
try {
const locationsArray = locations.value.map(loc =>
new Microsoft.Maps.Location(loc.lat, loc.lng)
);
const minLat = Math.min(...locationsArray.map(loc => loc.latitude));
const maxLat = Math.max(...locationsArray.map(loc => loc.latitude));
const minLng = Math.min(...locationsArray.map(loc => loc.longitude));
const maxLng = Math.max(...locationsArray.map(loc => loc.longitude));
const latRange = maxLat - minLat;
const lngRange = maxLng - minLng;
const paddedMinLat = Math.max(minLat - latRange * 0.2, -85);
const paddedMaxLat = Math.min(maxLat + latRange * 0.2, 85);
const paddedMinLng = minLng - lngRange * 0.2;
const paddedMaxLng = maxLng + lngRange * 0.2;
const bounds = Microsoft.Maps.LocationRect.fromEdges(
paddedMaxLat, paddedMaxLng, paddedMinLat, paddedMinLng
);
// 仅在必要时调整视图
if (map.value && bounds) {
// 保存当前中心点和缩放级别
const currentView = map.value.getView();
// 检查新边界是否明显不同
const newCenter = bounds.getCenter();
const centerDistance = Math.sqrt(
Math.pow(currentView.center.latitude - newCenter.latitude, 2) +
Math.pow(currentView.center.longitude - newCenter.longitude, 2)
);
// 如果中心点变化超过阈值或缩放级别变化超过1级,则调整视图
if (centerDistance > 0.1 || Math.abs(currentView.zoom - bounds.getZoomLevel()) > 1) {
map.value.setView({
bounds,
animate: true,
duration: 1000
});
}
}
} catch (error) {
console.error('调整地图视野时出错:', error);
}
};
// 聚焦到特定位置
const focusOnLocation = (location: Location) => {
if (!map.value) return;
map.value.setView({
center: new Microsoft.Maps.Location(location.lat, location.lng),
zoom: 12,
animate: true
});
showInfoWindow(location);
};
// 显示信息窗口
const showInfoWindow = (location: Location) => {
if (!map.value) return;
try {
if (infoBox.value) {
map.value.entities.remove(infoBox.value);
}
infoBox.value = new Microsoft.Maps.Infobox(
new Microsoft.Maps.Location(location.lat, location.lng),
{
title: location.name,
description: `
<div class="custom-infobox">
<div class="infobox-header">${location.name}</div>
<div class="infobox-content">
<p><strong>类型:</strong> ${location.category === "office" ? "办公地点" : "零售门店"}</p>
<p><strong>人流量:</strong> <span class="${getTrafficBadgeClass(location.traffic)}">${location.traffic}</span></p>
<p><strong>转化率:</strong> ${location.conversionRate}%</p>
<p><strong>地址:</strong> ${location.address || '未提供'}</p>
<p><strong>周边人口:</strong> ${location.population || '未提供'}</p>
</div>
<div class="infobox-footer">
<button class="btn btn-primary btn-sm">查看详情</button>
</div>
</div>
`,
showCloseButton: true,
maxWidth: 350,
offset: new Microsoft.Maps.Point(0, 20)
}
);
map.value.entities.push(infoBox.value);
} catch (error) {
console.error('显示信息窗口时出错:', error);
}
};
// 更新地图尺寸
const updateMapSize = () => {
if (mapContainer.value) {
mapSize.value = {
width: mapContainer.value.offsetWidth,
height: mapContainer.value.offsetHeight
};
console.log('地图容器尺寸更新:', mapSize.value);
}
};
// 设置尺寸变化监听
const setupResizeObserver = () => {
if (!mapContainer.value || typeof ResizeObserver === 'undefined') return;
// 移除现有监听器
if (resizeObserver.value) {
resizeObserver.value.disconnect();
}
// 创建新的尺寸监听器
resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === mapContainer.value) {
updateMapSize();
// 防止地图在尺寸变化时变黑
if (map.value) {
// 延迟调整,避免频繁触发
if (mapResizeHandler.value) clearTimeout(mapResizeHandler.value);
mapResizeHandler.value = setTimeout(() => {
map.value.setView({ animate: false }); // 强制地图重绘
}, 300);
}
}
}
});
resizeObserver.value.observe(mapContainer.value);
};
// 初始化图表
const initializeChart = () => {
try {
const ctx = document.getElementById('performanceChart') as HTMLCanvasElement;
if (!ctx) return;
const cities = locations.value.slice(0, 10).map(loc => loc.name);
const trafficValues = locations.value.slice(0, 10).map(loc => {
const trafficMap = { '极低': 1, '低': 2, '中': 3, '高': 4, '极高': 5 };
return trafficMap[loc.traffic] || 3;
});
const conversionRates = locations.value.slice(0, 10).map(loc => parseFloat(loc.conversionRate));
new Chart(ctx, {
type: 'bar',
data: {
labels: cities,
datasets: [
{
label: '人流量 (相对值)',
data: trafficValues,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
},
{
label: '转化率 (%)',
data: conversionRates,
backgroundColor: 'rgba(75, 192, 192, 0.5)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1,
type: 'line',
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: '人流量' },
ticks: { callback: (value) => ['极低', '低', '中', '高', '极高'][value - 1] || value }
},
y1: {
beginAtZero: true,
position: 'right',
title: { display: true, text: '转化率 (%)' },
grid: { drawOnChartArea: false }
}
}
}
});
} catch (error) {
console.error('初始化图表时出错:', error);
}
};
// 工具方法
const getTrafficBadgeClass = (traffic: string) => {
const classes = {
'极低': 'badge bg-success',
'低': 'badge bg-info',
'中': 'badge bg-primary',
'高': 'badge bg-warning',
'极高': 'badge bg-danger'
};
return classes[traffic] || 'badge bg-secondary';
};
const getConversionBadgeClass = (conversionRate: string) => {
const rate = parseFloat(conversionRate);
return rate >= 8 ? 'badge bg-success' :
rate >= 6 ? 'badge bg-warning' : 'badge bg-danger';
};
const getLocationIconClass = (location: Location) => {
return location.category === 'office' ? 'fa fa-building' : 'fa fa-shopping-bag';
};
// 生命周期钩子
onMounted(() => {
console.log('组件已挂载,加载位置数据...');
locations.value = locationsData;
initializeMap();
});
onUnmounted(() => {
console.log('组件卸载,清理资源...');
// 清理地图资源
if (map.value) {
map.value.dispose();
map.value = null;
}
if (infoBox.value) {
infoBox.value = null;
}
// 移除尺寸监听器
if (resizeObserver.value) {
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
// 清除定时器
if (mapResizeHandler.value) {
clearTimeout(mapResizeHandler.value);
mapResizeHandler.value = null;
}
});
// 监听地图容器尺寸变化
watch(mapSize, (newSize, oldSize) => {
if (newSize.width !== oldSize.width || newSize.height !== oldSize.height) {
console.log('地图尺寸变化,重绘地图...');
if (map.value) {
map.value.setView({ animate: false });
}
}
});
</script>
<style scoped>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.stats {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
flex: 1 1 200px;
background: #ffffff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.map-container {
/* 使用固定高度,防止尺寸变化导致黑屏 */
height: 600px;
width: 100%;
background-color: #f8f9fa; /* 防止初始化黑屏 */
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
margin-bottom: 20px;
overflow: hidden;
/* 防止父容器尺寸变化影响地图 */
min-height: 600px;
}
.chart-container {
background: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.location-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.location-item {
background: #ffffff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.location-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-3px);
}
.location-stats {
display: flex;
gap: 10px;
margin-top: 10px;
}
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner-container {
text-align: center;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.spinner-border {
display: inline-block;
width: 2rem;
height: 2rem;
vertical-align: -0.125em;
border: 0.25em solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spinner-border 0.75s linear infinite;
}
@keyframes spinner-border {
to {
transform: rotate(360deg);
}
}
.debug-info {
position: fixed;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
font-size: 12px;
z-index: 1000;
max-width: 300px;
}
.custom-infobox {
font-family: Arial, sans-serif;
line-height: 1.5;
}
.infobox-header {
background-color: #0078d4;
color: white;
padding: 8px 15px;
font-size: 16px;
font-weight: bold;
border-radius: 4px 4px 0 0;
}
.infobox-content {
padding: 10px 15px;
}
.infobox-footer {
padding: 10px 15px;
border-top: 1px solid #eee;
text-align: right;
}
.btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
</style>
输出: