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>

输出:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值