vue3: bingmap using typescript
项目结构:
<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>
输出:
哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)