收藏
回答

【地图组件】addGroundOverlay手绘地图覆盖物不显示,始终为空?

使用 addGroundOverlay添加手绘地图覆盖物,API调用成功,但 groundOverlays数组始终为空

图片URL可正常访问,配置正确,bounds坐标准确

开发者工具和真机均不显示覆盖物
微信版本:8.0.48

基础库版本:3.14.3

操作系统:Windows 11 / iOS 16 / Android 13

开发者工具版本:最新稳定版

小程序AppID:wxff75d3fb22ba667a
怀疑方向:

addGroundOverlayAPI 在特定条件下不更新数据绑定

地图组件内部实现有bug

生命周期或调用时机问题

请求官方协助:

这是否是已知的bug?

是否有正确的调用示例?

需要特殊配置吗?

// pages/map/map.js
// 总调度中心 - 仅负责导入和代理调用
// 引入所有服务模块
const uiService = require('../../services/ui-service');
const mapService = require('../../services/map-service');
const audioService = require('../../services/audio-service');
const locationService = require('../../services/location-service');
const tourService = require('../../services/tour-service');
// 引入工具函数
const utils = require('../../utils/index');


Page({
  // 数据层:完整复制原始map.js的data对象
  data: {
    // 地图相关
    mapScale: 18,
    mapCenterLatitude: 30.8,
    mapCenterLongitude: 120.3,
    userLocation: null,
    mapSpots: [],
    mapImageUrl: '',
    groundOverlays: [],
    usingTencentMap: true,
    locationPermissionDenied: false,
    
    // 手绘地图相关新增字段
    handdrawSpots: [],               // 手绘地图标点数据
    mapImageSize: { width: 0, height: 0 }, // 手绘地图图片尺寸
    mapConfig: null,                 // 地图配置
    handdrawConfig: null,            // 手绘地图配置
    hasShownFallbackToast: false,    // 是否已显示降级提示
    showSpotLabels: true,            // 是否显示标点标签
    
    // 模式相关
    currentMode: 'none', // 'none', 'exclusive', 'guided_tour'
    isTourLocked: false,
    isTourPaused: false,
    globalTourLocked: false,
    
    // UI状态
    showSpotActionSheet: false,
    selectedSpot: null,
    loadingText: '',
    isRequesting: false,
    lastTapTime: 0,
    showModeActionSheet: false,
    
    // === 【优化方案新增字段 START】===
    // 伴游智能防抖与冷却相关
    lastValidLocation: null,      // 上一次有效位置 {latitude, longitude}
    lastUpdateTime: 0,            // 【新增】上一次有效位置更新的时间戳(毫秒)
    accumulatedDistance: 0,       // 累计移动距离(米)
    frontendCooldown: 300000,     // 前端冷却时间5分钟 (300000毫秒)
    lastTourRequestTime: 0,       // 上次触发时间戳
    // === 【优化方案新增字段 END】===
    
    // 伴游相关状态
    tourState: {
      state: 'idle', // 'idle', 'playing', 'paused'
      currentSpot: null,
      visitedSpotIds: [],
      lastRequestTime: 0
    },
    // 注意:lastTourRequestTime 字段已上移至新增字段区,此处data中已不存在,后续代码中所有 this.data.lastTourRequestTime 将引用新的字段
    tourLocationListener: null,
    lastTriggeredSpotId: '',
    
    // 音频播放器
    audioPlayer: {
      show: false,
      title: '',
      data: null,
      playing: false,
      paused: false,
      hasImage: false,
      imageUrl: '',
      hasVoice: false,
      answerText: '',
      friendlySource: '',
      currentSpotId: ''
    },
    innerAudioContext: null,
    
    // ====================== 【新增调试字段】======================
    debug: {
      isMapComponentReady: false,       // 地图组件自身是否已触发 ready
      mapReadyTimestamp: null,          // 地图组件就绪的时间戳
      pageReadyTimestamp: null,          // 页面 onReady 的时间戳
      lastGroundOverlaySetTime: null,   // 最后一次设置覆盖物的时间
      groundOverlaysQueue: [],          // 覆盖物设置队列(用于地图未就绪时缓存)
      logs: []                          // 存储调试日志
    }
  },


  // ====================== 生命周期函数代理 ======================
  onLoad: function (options) {
    this._addDebugLog('[生命周期] 页面 onLoad 开始');
    tourService.initTourState(this);
    locationService.getUserLocationAndInitMap(this);
  },


  onShow: function () {
    const app = getApp();
    const isLocked = app.isTourModeLocked();
    this.setData({ 
      globalTourLocked: isLocked,
      isTourLocked: isLocked && this.data.currentMode === 'guided_tour'
    });
    
    if (isLocked && this.data.currentMode === 'guided_tour' && !this.data.isTourPaused) {
      locationService.startLocationUpdateForTour(this);
    }
  },


  onUnload: function () {
    locationService.stopLocationUpdateForTour(this);
    audioService.stopCurrentVoicePlayback(this);
    audioService.closeAudioPlayer(this);
  },


  onHide: function () {
    locationService.stopLocationUpdateForTour(this);
  },


  onReady: function() {
    console.log('✅ 页面onReady');
    
    // 初始化地图上下文
    this.mapContext = wx.createMapContext('tencentMap', this);
    console.log('🗺️ 地图上下文初始化完成');
    
    // 【新增】记录页面就绪时间
    this.setData({
      'debug.pageReadyTimestamp': Date.now()
    });
    this._addDebugLog(`[生命周期] 页面 onReady 执行。地图上下文已创建。`);
    this._addDebugLog(`[状态] 当前 groundOverlays 数据: ${JSON.stringify(this.data.groundOverlays)}`);
    
    // 【修改】删除延迟检查手绘地图的代码,因为已经在loadMapAndNearbySpots中处理
    // 手绘地图加载已由 mapService.loadMapAndNearbySpots 统一处理
  },


  // ====================== 【核心修改】地图组件就绪事件处理函数 ======================
  /**
   * 地图组件就绪事件处理函数
   */
  onMapComponentReady: function() {
    const now = Date.now();
    const pageReadyTime = this.data.debug.pageReadyTimestamp;
    const timeDiff = pageReadyTime ? now - pageReadyTime : 'N/A';
    
    this._addDebugLog(`[地图组件] bindready 事件触发!当前时间戳: ${now}`);
    this._addDebugLog(`[时间线] 页面onReady 到 地图bindready 间隔: ${timeDiff}ms`);
    
    // 更新地图上下文(确保是最新的)
    this.mapContext = wx.createMapContext('tencentMap', this);
    this._addDebugLog(`[地图组件] 地图上下文重新创建完成。`);


    // 更新就绪状态
    this.setData({
      'debug.isMapComponentReady': true,
      'debug.mapReadyTimestamp': now
    });


    // 检查是否有缓存的覆盖物需要设置
    const queue = this.data.debug.groundOverlaysQueue;
    if (queue && queue.length > 0) {
      this._addDebugLog(`[队列] 发现 ${queue.length} 个缓存的覆盖物,开始应用...`);
      this.setData({
        groundOverlays: queue,
        'debug.groundOverlaysQueue': [],
        'debug.lastGroundOverlaySetTime': now
      }, () => {
        this._addDebugLog(`[队列] 缓存覆盖物应用完成。当前 groundOverlays: ${JSON.stringify(this.data.groundOverlays)}`);
        this._forceMapRedrawWithLog();
      });
    } else {
      this._addDebugLog(`[队列] 覆盖物队列为空,无需应用。`);
      // 即使没有队列,也检查当前已设置的覆盖物,并尝试重绘
      if (this.data.groundOverlays && this.data.groundOverlays.length > 0) {
        this._addDebugLog(`[状态] 地图就绪时发现已存在 groundOverlays,尝试重新激活。`);
        this._reactivateGroundOverlays();
      }
    }
    
    // 输出完整的调试摘要
    this._printDebugSummary();
  },


  // ====================== 【新增调试函数】======================
  /**
   * 添加调试日志
   */
  _addDebugLog: function(message) {
    console.log(`[GroundOverlay调试] ${message}`);
    const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false, fractionalSecondDigits: 3 });
    const newLog = `${timestamp} - ${message}`;
    
    // 保持日志数组不至于过长
    const currentLogs = this.data.debug.logs || [];
    const updatedLogs = [newLog, ...currentLogs].slice(0, 50); // 保留最近50条
    
    this.setData({
      'debug.logs': updatedLogs
    });
  },


  /**
   * 重新激活已存在的覆盖物(通过先清空再设置的方式)
   */
  _reactivateGroundOverlays: function() {
    const currentOverlays = this.data.groundOverlays;
    this._addDebugLog(`[重绘] 开始重新激活 ${currentOverlays.length} 个覆盖物。`);
    
    this.setData({ groundOverlays: [] }, () => {
      setTimeout(() => {
        this.setData({ 
          groundOverlays: currentOverlays,
          'debug.lastGroundOverlaySetTime': Date.now()
        }, () => {
          this._addDebugLog(`[重绘] 覆盖物重新激活完成。`);
          this._forceMapRedrawWithLog();
        });
      }, 150);
    });
  },


  /**
   * 增强的强制地图重绘函数(带日志)
   */
  _forceMapRedrawWithLog: function() {
    this._addDebugLog(`[重绘] 开始强制地图重绘流程...`);
    const originalScale = this.data.mapScale || 18;
    const originalLat = this.data.mapCenterLatitude;
    const originalLng = this.data.mapCenterLongitude;


    if (!originalLat || !originalLng) {
      this._addDebugLog(`[重绘] ❌ 中止:地图中心坐标无效。`);
      return;
    }


    // 微调缩放级别以触发重绘
    this.setData({
      mapScale: originalScale + 0.001
    }, () => {
      setTimeout(() => {
        this.setData({
          mapScale: originalScale
        }, () => {
          this._addDebugLog(`[重绘] ✅ 通过缩放变化触发重绘完成。`);
        });
      }, 50);
    });
  },


  /**
   * 打印调试摘要
   */
  _printDebugSummary: function() {
    this._addDebugLog(`========== 调试状态摘要 ==========`);
    this._addDebugLog(`地图组件就绪: ${this.data.debug.isMapComponentReady}`);
    this._addDebugLog(`覆盖物队列长度: ${(this.data.debug.groundOverlaysQueue || []).length}`);
    this._addDebugLog(`页面Data中覆盖物长度: ${(this.data.groundOverlays || []).length}`);
    this._addDebugLog(`========== 摘要结束 ==========`);
  },


  /**
   * (可选)手动触发调试信息检查
   */
  manualDebugCheck: function() {
    wx.showModal({
      title: '手动调试',
      content: '将在控制台输出当前覆盖物和地图状态的详细信息。',
      success: (res) => {
        if (res.confirm) {
          this._printDebugSummary();
          this._addDebugLog(`[手动检查] 当前 groundOverlays 详情: ${JSON.stringify(this.data.groundOverlays, null, 2)}`);
          this._addDebugLog(`[手动检查] 地图上下文存在: ${!!this.mapContext}`);
          this._addDebugLog(`[手动检查] 地图中心: ${this.data.mapCenterLatitude}, ${this.data.mapCenterLongitude}`);
          this._addDebugLog(`[手动检查] 地图缩放: ${this.data.mapScale}`);
        }
      }
    });
  },


  // ====================== UI 控制函数代理 ======================
  showLoading: function (text) { uiService.showLoading(text, this); },
  hideLoading: function () { uiService.hideLoading(this); },
  isButtonDebouncing: function () { return uiService.isButtonDebouncing(this); },
  showModeSelect: function () { uiService.showModeSelect(this); },
  hideModeSelect: function () { uiService.hideModeSelect(this); },
  hideSpotAction: function () { uiService.hideSpotAction(this); },
  onMapZoomIn: function () { uiService.onMapZoomIn(this); },
  onMapZoomOut: function () { uiService.onMapZoomOut(this); },
  onResetMap: function () { uiService.onResetMap(this); },


  // ====================== 地图与景点函数代理 ======================
  onMapMarkerTap: function (e) { mapService.onMapMarkerTap(e, this); },
  onMapSpotTap: function (e) { mapService.onMapSpotTap(e, this); },
  loadMapAndNearbySpots: function (centerLat, centerLng) { mapService.loadMapAndNearbySpots(centerLat, centerLng, this); },
  useDefaultLocation: function () { mapService.useDefaultLocation(this); },
  parseLocationString: function(locationObj) { return mapService.parseLocationString(locationObj); },
  
  /**
   * 手绘地图图片加载完成事件
   * @param {Object} e - 图片加载事件对象
   */
  onMapImageLoad: function(e) {
    const { width, height } = e.detail;
    console.log(`手绘地图图片加载完成,尺寸: ${width}x${height}`);
    this.setData({ 
      mapImageSize: { width, height } 
    });
    
    // 触发坐标转换
    if (this.data.mapSpots.length > 0 && this.data.handdrawConfig) {
      mapService.convertSpotsToHanddraw(this);
    }
  },


  // ====================== 音频播放函数代理 ======================
  previewImage: function (e) { audioService.previewImage(e, this); },
  pauseOrResumeAudio: function () { audioService.pauseOrResumeAudio(this); },
  startAudioPlayback: function () { audioService.startAudioPlayback(this); },
  closeAudioPlayer: function () { audioService.closeAudioPlayer(this); }, // 【修复】添加缺失的关闭音频播放器代理
  onSpotExplain: function () { audioService.onSpotExplain(this); },
  onSpotNavigate: function () { audioService.onSpotNavigate(this); },


  // ====================== 定位与伴游函数代理 ======================
  handleSelectExclusiveMode: function () { tourService.handleSelectExclusiveMode(this); },
  handleSelectTourMode: function () { tourService.handleSelectTourMode(this); },
  pauseTourMode: function () { tourService.pauseTourMode(this); },
  resumeTourMode: function () { tourService.resumeTourMode(this); },
  onExitTourMode: function () { tourService.onExitTourMode(this); },
  exitTourModeCleanup: function () { tourService.exitTourModeCleanup(this); },
  onLocationChangeForTour: function (res) { tourService.onLocationChangeForTour(res, this); },


  // ====================== 工具函数直接导入 ======================
  calculateDistance: utils.calculateDistance,
  
  // ====================== 【新增】地图相关函数 ======================
  
  /**
   * 【修改】检查手绘地图
   * 【重要修改】手绘地图的加载应该由 map-service.js 统一处理
   * 这个函数现在只用于调试目的,不实际加载手绘地图
   */
  checkHanddrawMap: function() {
    console.log('🔍 检查手绘地图状态');
    console.log('当前地图配置:', this.data.mapConfig);
    console.log('当前groundOverlays数量:', this.data.groundOverlays.length);
    console.log('groundOverlays详情:', this.data.groundOverlays);
    
    // 【修改】不再手动加载手绘地图,因为已经在 loadMapAndNearbySpots 中处理
    // 手绘地图的加载统一由 mapService.loadMapAndNearbySpots 处理
  },


  /**
   * 【修改】地图更新完成事件
   * 只用于状态检查和调试,不触发任何加载操作
   */
  onMapUpdated: function(e) {
    console.log('✅ 地图组件updated事件触发');
    console.log('当前groundOverlays数量:', this.data.groundOverlays.length);
    console.log('groundOverlays详情:', this.data.groundOverlays);
    
    if (this.data.groundOverlays.length > 0) {
      console.log('🎉 groundOverlays应该已显示在地图上');
      
      // 检查图片是否加载成功
      const overlay = this.data.groundOverlays[0];
      console.log('图片URL:', overlay.src);
      console.log('边界范围:', overlay.bounds);
      console.log('透明度:', overlay.opacity);
    } else {
      console.log('❌ groundOverlays为空');
      // 【重要】这里不再调用 checkHanddrawMap 或其他加载函数
      // 手绘地图的加载已由 map-service.js 统一处理
    }
  },


  /**
   * 【保留】直接测试groundOverlays
   * 这个函数用于调试,可以手动调用
   */
  testGroundOverlays: function() {
    console.log('🧪 开始测试groundOverlays显示');
    
    // 使用一个绝对简单的测试图片和边界
    const testOverlay = {
      id: 999,  // 使用大数字,避免冲突
      src: 'https://img2.baidu.com/it/u=3040262610,2207575815&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
      bounds: {
        southwest: { latitude: 30.74, longitude: 120.15 },
        northeast: { latitude: 30.76, longitude: 120.17 }
      },
      opacity: 0.8,  // 设置透明度为0.8,更容易看到
      zIndex: 9999
    };
    
    console.log('📋 测试groundOverlay对象:', testOverlay);
    
    // 先清空
    this.setData({ groundOverlays: [] }, () => {
      setTimeout(() => {
        // 再设置
        this.setData({ 
          groundOverlays: [testOverlay],
          mapCenterLatitude: 30.75,
          mapCenterLongitude: 120.16,
          mapScale: 18
        }, () => {
          console.log('✅ 测试groundOverlays已设置');
          console.log('当前groundOverlays:', this.data.groundOverlays);
          
          // 强制重绘
          this.forceMapRedraw();
        });
      }, 100);
    });
  },


  /**
   * 【保留】强制地图重绘
   */
  forceMapRedraw: function() {
    console.log('🔄 手动触发地图重绘');
    
    const originalScale = this.data.mapScale;
    const originalLat = this.data.mapCenterLatitude;
    const originalLng = this.data.mapCenterLongitude;
    
    if (!originalLat || !originalLng) {
      console.warn('❌ 地图中心坐标无效,无法重绘');
      return;
    }
    
    // 轻微改变地图参数
    this.setData({
      mapScale: originalScale + 0.001
    }, () => {
      setTimeout(() => {
        this.setData({
          mapScale: originalScale
        }, () => {
          console.log('✅ 手动重绘完成');
          
          // 通过地图上下文再次触发
          if (this.mapContext) {
            this.mapContext.moveToLocation({
              latitude: originalLat + 0.00001,
              longitude: originalLng
            });
            
            setTimeout(() => {
              this.mapContext.moveToLocation({
                latitude: originalLat,
                longitude: originalLng
              });
            }, 100);
          }
        });
      }, 100);
    });
  },


  // ====================== 【新增】统一权限校验与相关功能入口修改 ======================


  /**
   * 【新增】统一的定位权限检查与申请函数
   * @param {string} actionDesc - 触发申请的功能描述(用于提示用户),例如:"智能伴游"、"路线导航"
   * @param {Function} successCallback - 授权成功并获取位置后的回调函数
   * @param {Function} failCallback - 用户拒绝授权或最终失败后的回调函数(可选)
   */
  checkAndRequestLocation: function (actionDesc, successCallback, failCallback) {
    const that = this;
    console.log(`[权限检查] 功能: ${actionDesc}, 当前状态: locationPermissionDenied=${that.data.locationPermissionDenied}, userLocation=${that.data.userLocation ? '有' : '无'}`);


    // 情景1:已有精确定位,直接执行成功回调
    if (this.data.userLocation && !this.data.locationPermissionDenied) {
      console.log('[权限检查] 已有定位权限和位置,直接执行功能');
      typeof successCallback === 'function' && successCallback();
      return;
    }


    // 情景2:权限曾被拒绝,需引导用户前往设置页手动开启
    if (this.data.locationPermissionDenied) {
      wx.showModal({
        title: `开启${actionDesc}`,
        content: `此功能需要获取您的实时位置。您之前已拒绝过定位权限,需要前往小程序设置页手动开启。`,
        confirmText: '去设置',
        cancelText: '取消',
        success: (modalRes) => {
          if (modalRes.confirm) {
            // 用户确认,打开小程序设置页
            console.log(`[权限检查] 用户同意为【${actionDesc}】打开设置页`);
            wx.openSetting({
              success: (settingRes) => {
                console.log('[权限检查] 用户从设置页返回', settingRes);
                // 无论用户在设置页是否操作,返回后都尝试重新获取位置
                wx.showLoading({ title: '获取位置中...' });
                locationService.getUserLocationAndInitMap(that);
                // 等待定位结果
                that._waitForLocationUpdate(actionDesc, successCallback, failCallback);
              },
              fail: (err) => {
                console.error('[权限检查] 打开设置页失败', err);
                wx.showToast({ title: '打开设置失败', icon: 'none' });
                typeof failCallback === 'function' && failCallback();
              }
            });
          } else {
            // 用户取消,不前往设置
            console.log(`[权限检查] 用户取消为【${actionDesc}】打开设置页`);
            typeof failCallback === 'function' && failCallback();
          }
        }
      });
      return;
    }


    // 情景3:首次使用或无位置,但未明确拒绝 (例如网络失败、首次触发)
    console.log(`[权限检查] 首次为【${actionDesc}】申请定位权限`);
    wx.showLoading({ title: '获取位置中...' });
    locationService.getUserLocationAndInitMap(this);
    // 等待定位结果
    that._waitForLocationUpdate(actionDesc, successCallback, failCallback);
  },


  /**
   * 【新增】私有方法:等待位置更新完成
   * @param {string} actionDesc - 功能描述
   * @param {Function} successCallback - 成功回调
   * @param {Function} failCallback - 失败回调
   */
  _waitForLocationUpdate: function (actionDesc, successCallback, failCallback) {
    const that = this;
    let checkCount = 0;
    const maxCheckCount = 20; // 最多检查10秒 (20 * 500ms)
    const checkInterval = setInterval(() => {
      checkCount++;
      // 检查条件:获取到了用户位置 且 权限未被拒绝
      if (that.data.userLocation && !that.data.locationPermissionDenied) {
        clearInterval(checkInterval);
        wx.hideLoading();
        console.log(`[权限检查] 【${actionDesc}】定位授权成功,位置已获取`);
        typeof successCallback === 'function' && successCallback();
      } 
      // 检查条件:权限被拒绝(可能在这次申请中再次被拒)
      else if (that.data.locationPermissionDenied) {
        clearInterval(checkInterval);
        wx.hideLoading();
        console.log(`[权限检查] 【${actionDesc}】定位授权被拒绝`);
        wx.showToast({ title: '需要位置权限', icon: 'none' });
        typeof failCallback === 'function' && failCallback();
      } 
      // 检查条件:超时
      else if (checkCount > maxCheckCount) {
        clearInterval(checkInterval);
        wx.hideLoading();
        console.warn(`[权限检查] 【${actionDesc}】等待位置更新超时`);
        wx.showToast({ title: '获取位置超时', icon: 'none' });
        typeof failCallback === 'function' && failCallback();
      }
      // 否则继续等待
    }, 500);
  },


  /**
   * 【修改】智能伴游模式入口 - 加入权限检查
   */
  handleSelectTourMode: function () {
    this.checkAndRequestLocation(
      '智能伴游模式',
      () => {
        // 授权成功后的回调:执行原有的伴游模式启动逻辑
        console.log('[handleSelectTourMode] 定位就绪,开始执行伴游模式逻辑');
        tourService.handleSelectTourMode(this);
      },
      () => {
        // 授权失败后的回调
        wx.showToast({ title: '需要位置权限才能开始伴游', icon: 'none' });
      }
    );
  },


  /**
   * 【修改】地图复位功能入口 - 加入权限检查
   */
  onResetMap: function () {
    const app = getApp(); 
    // 如果已有用户位置,直接使用它复位
    if (this.data.userLocation && !this.data.locationPermissionDenied) {
      uiService.onResetMap(this);
      return;
    }
    // 如果没有位置或权限被拒,触发权限检查和获取
    this.checkAndRequestLocation(
      '重置地图视野',
      () => {
        // 授权并获取位置成功后,执行复位逻辑
        console.log('[onResetMap] 定位就绪,执行地图复位');
        uiService.onResetMap(this);
      },
      () => {
        // 用户最终拒绝授权,降级到使用默认位置复位
        console.log('[onResetMap] 定位授权失败,使用默认位置复位');
        mapService.useDefaultLocation(this);
        // 给用户一个提示
        wx.showToast({ title: '已使用默认位置', icon: 'none' });
      }
    );
  },


  /**
   * 【修改】景点导航功能入口 - 加入权限检查
   */
  onSpotNavigate: function () {
    this.checkAndRequestLocation(
      '路线导航',
      () => {
        // 授权成功后的回调:执行原有的导航逻辑
        console.log('[onSpotNavigate] 定位就绪,开始执行导航逻辑');
        audioService.onSpotNavigate(this);
      },
      () => {
        // 授权失败后的回调
        wx.showToast({ title: '需要位置权限才能导航', icon: 'none' });
      }
    );
  }

})

<!-- pages/map/map.wxml -->

<view class="page-container">

  <!-- 顶部状态栏 -->

  <view class="status-bar">

    <view class="mode-indicator" wx:if="{{currentMode !== 'none'}}">

      <text class="mode-text">{{currentMode === 'exclusive' ? '专属导游' : '智能伴游'}}</text>

      <text class="exit-btn" wx:if="{{currentMode === 'guided_tour' && isTourLocked}}" bindtap="onExitTourMode">退出</text>

    </view>

    <view class="mode-selector-btn {{currentMode !== 'none' ? 'active' : ''}}" bindtap="showModeSelect">

      <text class="btn-icon">📍</text>

      <text class="btn-text">{{currentMode === 'none' ? '选择模式' : '切换模式'}}</text>

    </view>

  </view>


  <!-- 地图区域 - 统一使用腾讯地图组件(包含地面覆盖物) -->

  <view class="map-area">

    <!-- 腾讯地图组件(始终显示,支持地面覆盖物显示手绘地图) -->

    <map

      id="tencentMap"

      class="tencent-map"

      latitude="{{mapCenterLatitude}}"

      longitude="{{mapCenterLongitude}}"

      scale="{{mapScale}}"

      markers="{{mapSpots}}"

      ground-overlays="{{groundOverlays}}"

      bindmarkertap="onMapMarkerTap"

      bindupdated="onMapUpdated"

      bindready="onMapComponentReady"  

      show-location

    >

    </map>

   

    <!-- 用户位置标记(伴游模式) -->

    <view wx:if="{{userLocation && currentMode === 'guided_tour'}}" class="user-location-marker"

          style="top: 50%; left: 50%; transform: translate(-50%, -50%);">

      <view class="pulse-dot"></view>

    </view>

   

    <!-- 【修复点】将 catchtouchmove="preventDefault" 改为 catchtouchmove -->

    <view wx:if="{{globalTourLocked}}" class="tour-lock-overlay" >

      <view class="lock-message">

        <text class="lock-icon">🔒</text>

        <text class="lock-text">智能伴游模式运行中</text>

        <text class="lock-tip">屏幕已锁定,请专注于语音讲解</text>

        <text class="lock-exit-tip">点击顶部"退出"按钮结束伴游</text>

       

        <!-- ================= 【新增】伴游模式暂停/恢复控制按钮 ================= -->

        <view class="tour-control-buttons">

          <button

            wx:if="{{!isTourPaused}}"

            class="tour-control-btn btn-pause"

            bindtap="pauseTourMode"

          >

            ⏸️ 暂停伴游

          </button>

          <button

            wx:if="{{isTourPaused}}"

            class="tour-control-btn btn-resume"

            bindtap="resumeTourMode"

          >

            ▶️ 恢复伴游

          </button>

        </view>

      </view>

    </view>

   

    <!-- 【新增】测试按钮,完成后可删除 -->

    <view class="test-groundoverlay-btn" bindtap="testGroundOverlays">

      <text>🧪 测试GroundOverlays</text>

    </view>

  </view>


  <!-- 地图控制按钮 (右下角) -->

  <view class="map-controls">

    <view class="control-btn zoom-in" bindtap="onMapZoomIn">+</view>

    <view class="control-btn zoom-out" bindtap="onMapZoomOut">-</view>

    <view class="control-btn reset" bindtap="onResetMap"></view>

  </view>


  <!-- 模式选择 ActionSheet -->

  <view class="action-sheet-overlay {{showModeActionSheet ? 'show' : ''}}" catchtap="hideModeSelect">

    <view class="action-sheet-content" catchtap="stopPropagation">

      <view class="action-sheet-title">选择导游模式</view>

      <view class="action-sheet-item exclusive" bindtap="handleSelectExclusiveMode">

        <view class="item-icon">🎤</view>

        <view class="item-text">

          <view class="item-title">专属导游模式</view>

          <view class="item-desc">点击地图景点,获取深度讲解和导航</view>

        </view>

      </view>

      <view class="action-sheet-item tour" bindtap="handleSelectTourMode">

        <view class="item-icon">🚶</view>

        <view class="item-text">

          <view class="item-title">智能伴游模式</view>

          <view class="item-desc">走到哪讲到哪,自动讲解附近景点</view>

        </view>

      </view>

      <view class="action-sheet-item cancel" bindtap="hideModeSelect">取消</view>

    </view>

  </view>


  <!-- 景点操作 ActionSheet -->

  <view class="action-sheet-overlay {{showSpotActionSheet ? 'show' : ''}}" catchtap="hideSpotAction">

    <view class="action-sheet-content" catchtap="stopPropagation">

      <view class="action-sheet-title">{{selectedSpot ? selectedSpot.name : ''}}</view>

     

      <!-- 【修改点】根据当前模式动态显示讲解按钮文本 -->

      <view class="action-sheet-item explain" bindtap="onSpotExplain">

        <view class="item-icon">🔊</view>

        <view class="item-text">

          {{currentMode === 'exclusive' ? '景点讲解' : '重新讲解'}}

        </view>

      </view>

     

      <view class="action-sheet-item navigate" bindtap="onSpotNavigate">

        <view class="item-icon">🧭</view>

        <view class="item-text">路线导航</view>

      </view>

      <view class="action-sheet-item cancel" bindtap="hideSpotAction">取消</view>

    </view>

  </view>


  <!-- 全局加载遮罩 -->

  <view wx:if="{{loadingText}}" class="loading-overlay">

    <view class="loading-content">

      <view class="loading-spinner"></view>

      <text class="loading-text">{{loadingText}}</text>

    </view>

  </view>


  <!-- ================= 【新增修复2】自定义答案模态框 ================= -->

  <view wx:if="{{showAnswerModal}}" class="answer-modal">

    <view class="modal-mask" bindtap="hideAnswerModal"></view>

    <view class="modal-content">

      <view class="modal-header">

        <text class="modal-title">{{answerModalTitle}}</text>

        <view class="modal-close" bindtap="hideAnswerModal">×</view>

      </view>

      <view class="modal-body">

        <text class="answer-text">{{answerModalContent}}</text>

       

        <!-- 语音播放按钮区域 -->

        <view class="voice-action-section">

          <button

            class="voice-play-button"

            bindtap="playAnswerVoice"

          >

            🔊 播放语音讲解

          </button>

          <text class="voice-tip">点击播放语音讲解</text>

        </view>

      </view>

      <view class="modal-footer">

        <button class="modal-btn confirm" bindtap="hideAnswerModal">关闭</button>

      </view>

    </view>

  </view>


  <!-- ================= 【新增修复3】语音播放器 ================= -->

  <view class="voice-player" wx:if="{{showVoicePlayer}}">

    <view class="voice-player-header">

      <text class="voice-player-title">{{voiceTitle}}</text>

      <view class="voice-player-close" bindtap="closeVoicePlayer">×</view>

    </view>

   

    <view class="voice-player-content">

      <view class="voice-text">{{voiceText}}</view>

     

      <view class="voice-controls">

        <view class="voice-progress">

          <view class="voice-progress-bar" style="width: {{voiceProgress}}%"></view>

        </view>

       

        <view class="voice-buttons">

          <button

            class="voice-btn play-btn {{isPlaying ? 'playing' : ''}}"

            bindtap="toggleVoicePlayback"

          >

            {{isPlaying ? '暂停' : '播放'}}

          </button>

         

          <button

            class="voice-btn stop-btn"

            bindtap="stopVoicePlayback"

            wx:if="{{isPlaying}}"

          >

            停止

          </button>

        </view>

       

        <view class="voice-time">

          <text>{{currentTime}} / {{totalTime}}</text>

        </view>

      </view>

    </view>

  </view>


  <!-- ================= 【重构】多媒体讲解器 (图片+文字+语音) ================= -->

  <view wx:if="{{audioPlayer.show}}" class="multimedia-player-container">

    <!-- 遮罩层 -->

    <view class="player-mask" bindtap="closeAudioPlayer"></view>

   

    <!-- 播放器主体 -->

    <view class="multimedia-player">

      <!-- 头部:标题和关闭按钮 -->

      <view class="player-header">

        <!-- 【修改点】将标题和友好来源组合显示 -->

        <text class="player-title">{{audioPlayer.title}} · {{audioPlayer.friendlySource}}</text>

        <view class="player-close-btn" bindtap="closeAudioPlayer">×</view>

      </view>

     

      <!-- 内容区域 -->

      <view class="player-content">

       

        <!-- 图片区域 (有图片时显示) -->

        <view wx:if="{{audioPlayer.hasImage}}" class="image-section">

          <image

            src="{{audioPlayer.imageUrl}}"

            mode="aspectFit"

            class="spot-image"

            bindtap="previewImage"

            data-src="{{audioPlayer.imageUrl}}"

          />

          <view class="image-tip">点击图片可放大查看</view>

        </view>

       

        <!-- 文字讲解区域 -->

        <view wx:if="{{audioPlayer.answerText}}" class="text-section">

          <view class="section-title">文字讲解</view>

          <view class="answer-text">{{audioPlayer.answerText}}</view>

        </view>

       

        <!-- 语音控制区域 (有语音时显示) -->

        <view wx:if="{{audioPlayer.hasVoice}}" class="voice-control-section">

          <view class="section-title">语音讲解</view>

         

          <!-- 播放控制按钮 -->

          <view class="voice-controls">

            <button

              wx:if="{{!audioPlayer.playing && !audioPlayer.paused}}"

              class="btn-control btn-play"

              bindtap="startAudioPlayback"

              size="mini"

            >

              🔊 开始播放

            </button>

            <button

              wx:if="{{audioPlayer.playing || audioPlayer.paused}}"

              class="btn-control btn-pause-resume"

              bindtap="pauseOrResumeAudio"

              size="mini"

            >

              {{audioPlayer.paused ? '▶ 继续播放' : '⏸ 暂停'}}

            </button>

            <button

              class="btn-control btn-close"

              bindtap="closeAudioPlayer"

              size="mini"

            >

              关闭

            </button>

          </view>

         

          <!-- 播放状态提示 -->

          <view class="voice-status">

            <text wx:if="{{audioPlayer.playing}}">▶ 正在播放语音讲解...</text>

            <text wx:if="{{audioPlayer.paused}}">⏸ 语音已暂停</text>

          </view>

        </view>

       

        <!-- 无语音提示 (有图片或文字但无语音时显示) -->

        <view wx:if="{{!audioPlayer.hasVoice && (audioPlayer.hasImage || audioPlayer.answerText)}}" class="no-voice-section">

          <view class="no-voice-tip">

            <text>🎵 暂无语音讲解,请阅读上方文字介绍</text>

          </view>

          <button

            class="btn-control btn-close-full"

            bindtap="closeAudioPlayer"

          >

            关闭讲解

          </button>

        </view>

       

      </view>

    </view>

  </view>

</view>

// services/map-service.js

// 地图显示与景点数据服务 (单例模式)

const utils = require('../utils/index');


// 默认地图配置 - 当后端配置获取失败时使用

const DEFAULT_MAP_CONFIG = {

  activeMode: 'tencent',

  handdraw: {

    enabled: false,

    imageUrl: '',

    bounds: { north: null, south: null, east: null, west: null }

  },

  markerIcons: {

    default: 'https://your-bucket.cos.ap-shanghai.myqcloud.com/icons/spot_handdraw.png'

  }

};


// 保底常量 - 当所有配置都失效时使用

const FALLBACK_CONSTANTS = {

  TENCENT_MAP_FALLBACK: true,

  DEFAULT_MARKER_ICON: '/images/default-marker.png'

};


const mapService = {

  /**

   * 地图标记点点击事件

   * @param {Object} e - 事件对象

   * @param {Object} page - 页面实例

   */

  onMapMarkerTap: function (e, page) {

    const markerId = e.markerId;

    console.log('【地图页】标记点被点击,id:', markerId);

   

    const app = getApp();

    if (!app.isInteractionAllowed()) {

      wx.showToast({ title: '伴游模式运行中,此功能不可用', icon: 'none' });

      return;

    }

   

    if (page.data.currentMode !== 'exclusive' || page.isButtonDebouncing()) return;

   

    const spot = page.data.mapSpots.find(s => s.id === markerId);

    if (!spot) return;

    page.setData({ selectedSpot: spot, showSpotActionSheet: true });

  },


  /**

   * 地图景点组件点击事件

   * @param {Object} e - 事件对象

   * @param {Object} page - 页面实例

   */

  onMapSpotTap: function (e, page) {

    const app = getApp();

    if (!app.isInteractionAllowed()) {

      wx.showToast({ title: '伴游模式运行中,此功能不可用', icon: 'none' });

      return;

    }

   

    if (page.data.currentMode !== 'exclusive' || page.isButtonDebouncing()) return;

    const spotId = e.currentTarget.dataset.spotId;

    const spot = page.data.mapSpots.find(s => s.id === spotId);

    if (!spot) return;

    page.setData({ selectedSpot: spot, showSpotActionSheet: true });

  },


  /**

   * 获取地图配置

   * @param {Object} page - 页面实例

   * @returns {Promise<Object>} 地图配置对象

   */

  async getMapConfig(page) {

    const app = getApp();

    try {

      const res = await app.callGateway('getMapConfig', {});

      console.log('【getMapConfig-调试】原始接口响应:', res);

      console.log('【getMapConfig-调试】响应code:', res.code);

      console.log('【getMapConfig-调试】响应data:', res.data);

      console.log('【getMapConfig-调试】响应data.markerIcons:', res.data?.markerIcons);

      console.log('【getMapConfig-调试】响应data.markerIcons.default:', res.data?.markerIcons?.default);

     

      if (res.code === 200) {

        console.log('✅ 地图配置获取成功:', res.data);

        return res.data;

      } else {

        console.warn('获取地图配置返回非200状态码:', res.code, res.message);

      }

    } catch (err) {

      console.warn('获取地图配置失败:', err);

    }

    console.log('【getMapConfig-调试】使用默认地图配置:', DEFAULT_MAP_CONFIG);

    return DEFAULT_MAP_CONFIG;

  },


  /**

   * 判断是否应使用手绘地图

   * @param {Object} config - 地图配置

   * @returns {boolean} 是否使用手绘地图

   */

  shouldUseHanddrawMap(config) {

    // 必须满足所有条件

    return config.activeMode === 'handdraw' &&

           config.handdraw?.enabled === true &&

           config.handdraw?.imageUrl &&

           config.handdraw?.imageUrl.trim() &&

           this.hasValidBounds(config.handdraw.bounds);

  },


  /**

   * 检查边界是否有效

   * @param {Object} bounds - 边界对象

   * @returns {boolean} 边界是否有效

   */

  hasValidBounds(bounds) {

    if (!bounds) return false;

    const { north, south, east, west } = bounds;

    return north !== null && south !== null &&

           east !== null && west !== null &&

           north > south && east > west; // 基本地理合理性检查

  },


  /**

   * 【核心修改】尝试加载手绘地图

   * @param {Object} handdrawConfig - 手绘地图配置

   * @param {Object} page - 页面实例

   * @returns {Promise<boolean>} 是否加载成功

   */

  tryLoadHanddrawMap: function(handdrawConfig, page) {

    // 【新增调试】记录开始

    page._addDebugLog && page._addDebugLog(`[服务] tryLoadHanddrawMap 被调用,配置: ${JSON.stringify(handdrawConfig)}`);

    console.log('🔄 开始加载手绘地图为groundOverlays');


    if (!handdrawConfig || !handdrawConfig.imageUrl || !handdrawConfig.bounds) {

      const errorMsg = '❌ 手绘地图配置无效';

      page._addDebugLog && page._addDebugLog(`[服务] ${errorMsg}`);

      console.warn(errorMsg);

      return false;

    }


    const bounds = handdrawConfig.bounds;

    // 【新增】详细记录配置

    page._addDebugLog && page._addDebugLog(`[服务] 图片URL: ${handdrawConfig.imageUrl}, 边界: ${JSON.stringify(bounds)}`);


    // 【关键】验证边界数值

    if (!this.hasValidBounds(bounds)) {

      const errorMsg = '❌ 手绘地图边界数据无效,无法创建覆盖物';

      page._addDebugLog && page._addDebugLog(`[服务] ${errorMsg}`);

      console.error(errorMsg);

      return false;

    }


    const groundOverlay = {

      id: 0,  // 必须为数字

      src: handdrawConfig.imageUrl,

      bounds: {

        southwest: {

          latitude: parseFloat(bounds.south),  // 确保是数字

          longitude: parseFloat(bounds.west)

        },

        northeast: {

          latitude: parseFloat(bounds.north),

          longitude: parseFloat(bounds.east)

        }

      },

      opacity: 1.0,  // 必须是1.0,不是1

      zIndex: 9999

    };


    page._addDebugLog && page._addDebugLog(`[服务] 构造的覆盖物对象: ${JSON.stringify(groundOverlay)}`);

    console.log('✅ 创建groundOverlay对象:', groundOverlay);


    // 【核心逻辑】检查地图组件就绪状态,决定是直接应用还是加入队列

    if (page.data.debug && page.data.debug.isMapComponentReady) {

      page._addDebugLog && page._addDebugLog(`[服务] 地图组件已就绪,立即应用覆盖物。`);

      this._applyGroundOverlayWithLog(groundOverlay, page, handdrawConfig);

    } else {

      page._addDebugLog && page._addDebugLog(`[服务] 地图组件未就绪,将覆盖物加入等待队列。`);

      // 存入队列,等待地图就绪事件处理

      const currentQueue = page.data.debug.groundOverlaysQueue || [];

      page.setData({

        'debug.groundOverlaysQueue': [groundOverlay, ...currentQueue].slice(0, 5), // 队列只保留最新的5个

        handdrawConfig: handdrawConfig

      });

    }

    return true;

  },


  /**

   * 【新增函数】带详细日志的覆盖物应用函数

   */

  _applyGroundOverlayWithLog: function(groundOverlay, page, handdrawConfig) {

    const applyStartTime = Date.now();

    page._addDebugLog && page._addDebugLog(`[服务] 开始应用覆盖物到地图...`);


    // 先清空现有覆盖物

    page.setData({

      groundOverlays: []

    }, () => {

      page._addDebugLog && page._addDebugLog(`[服务] 已清空现有覆盖物。`);

      // 短暂延迟后设置新覆盖物

      setTimeout(() => {

        page.setData({

          groundOverlays: [groundOverlay],

          handdrawConfig: handdrawConfig,

          'debug.lastGroundOverlaySetTime': Date.now()

        }, () => {

          const applyEndTime = Date.now();

          page._addDebugLog && page._addDebugLog(`[服务] ✅ 覆盖物已设置到页面Data,耗时 ${applyEndTime - applyStartTime}ms。`);

          page._addDebugLog && page._addDebugLog(`[服务] 设置后 groundOverlays: ${JSON.stringify(page.data.groundOverlays)}`);


          // 调整地图视野到覆盖物中心

          this._adjustMapToShowOverlayWithLog(page, groundOverlay);

          // 尝试强制重绘

          this.forceMapRedraw(page);

        });

      }, 150); // 清空和设置之间的延迟

    });

  },


  /**

   * 【新增函数】带日志的地图视野调整

   */

  _adjustMapToShowOverlayWithLog: function(page, overlay) {

    if (!overlay || !overlay.bounds) {

      page._addDebugLog && page._addDebugLog(`[服务] ❌ 无法调整视野:覆盖物或边界无效。`);

      return;

    }

    const bounds = overlay.bounds;

    const centerLat = (bounds.southwest.latitude + bounds.northeast.latitude) / 2;

    const centerLng = (bounds.southwest.longitude + bounds.northeast.longitude) / 2;

    const latRange = bounds.northeast.latitude - bounds.southwest.latitude;

    const lngRange = bounds.northeast.longitude - bounds.southwest.longitude;


    page._addDebugLog && page._addDebugLog(`[服务] 覆盖物中心计算: (${centerLat}, ${centerLng}), 纬度范围: ${latRange}, 经度范围: ${lngRange}`);


    // 计算合适的缩放级别

    const maxRange = Math.max(latRange, lngRange);

    let zoomLevel = 15; // 默认

    if (maxRange < 0.001) zoomLevel = 19;

    else if (maxRange < 0.005) zoomLevel = 18;

    else if (maxRange < 0.01) zoomLevel = 17;

    else if (maxRange < 0.05) zoomLevel = 16;

    else if (maxRange < 0.1) zoomLevel = 15;

    else if (maxRange < 0.2) zoomLevel = 14;

    else zoomLevel = 13;


    page.setData({

      mapCenterLatitude: centerLat,

      mapCenterLongitude: centerLng,

      mapScale: zoomLevel

    }, () => {

      page._addDebugLog && page._addDebugLog(`[服务] 地图视野已调整至中心(${centerLat}, ${centerLng}),缩放等级 ${zoomLevel}`);

    });

  },


  /**

   * 【新增函数】强制地图重绘

   */

  forceMapRedraw: function(page) {

    console.log('🔄 强制地图重绘groundOverlays');

    page._addDebugLog && page._addDebugLog(`[服务] 强制地图重绘被调用`);

   

    if (!page || !page.data) {

      console.error('❌ page对象无效');

      page._addDebugLog && page._addDebugLog(`[服务] ❌ page对象无效,无法重绘`);

      return;

    }

   

    const originalScale = page.data.mapScale || 18;

    const originalLat = page.data.mapCenterLatitude;

    const originalLng = page.data.mapCenterLongitude;

   

    if (!originalLat || !originalLng) {

      console.warn('❌ 地图中心坐标无效,无法重绘');

      page._addDebugLog && page._addDebugLog(`[服务] ❌ 地图中心坐标无效,无法重绘`);

      return;

    }

   

    // 方法1:通过改变地图参数触发重绘

    page.setData({

      mapScale: originalScale + 0.001

    }, () => {

      // 立即改回

      setTimeout(() => {

        page.setData({

          mapScale: originalScale

        }, () => {

          console.log('✅ 地图缩放重绘完成');

          page._addDebugLog && page._addDebugLog(`[服务] ✅ 地图缩放重绘完成`);

         

          // 方法2:通过地图上下文再次触发

          this.triggerMapContextRedraw(page, originalLat, originalLng);

        });

      }, 50);

    });

  },


  /**

   * 【新增函数】通过地图上下文触发重绘

   */

  triggerMapContextRedraw: function(page, lat, lng) {

    try {

      const mapCtx = wx.createMapContext('tencentMap', page);

      if (mapCtx && mapCtx.moveToLocation) {

        console.log('🔄 通过地图上下文强制重绘');

        page._addDebugLog && page._addDebugLog(`[服务] 通过地图上下文强制重绘`);

       

        // 先移动到旁边

        mapCtx.moveToLocation({

          latitude: lat + 0.00001,

          longitude: lng

        });

       

        // 延迟后再移回来

        setTimeout(() => {

          mapCtx.moveToLocation({

            latitude: lat,

            longitude: lng

          });

          console.log('✅ 地图上下文重绘完成');

          page._addDebugLog && page._addDebugLog(`[服务] ✅ 地图上下文重绘完成`);

        }, 100);

      } else {

        console.warn('⚠️ 地图上下文创建失败');

        page._addDebugLog && page._addDebugLog(`[服务] ⚠️ 地图上下文创建失败`);

      }

    } catch (err) {

      console.error('❌ 地图上下文操作失败:', err);

      page._addDebugLog && page._addDebugLog(`[服务] ❌ 地图上下文操作失败: ${err.message}`);

    }

  },


  /**

   * 降级到腾讯地图

   * @param {Object} page - 页面实例

   * @param {string} reason - 降级原因

   */

  fallbackToTencentMap(page, reason) {

    console.log(`【地图降级】原因: ${reason}`);

    page.setData({

      usingTencentMap: true,

      groundOverlays: []  // 清除地面覆盖物

    });

   

    // 只显示一次提示,避免频繁打扰

    if (!page.data.hasShownFallbackToast) {

      wx.showToast({

        title: '已使用腾讯地图',

        icon: 'none',

        duration: 1500

      });

      page.setData({ hasShownFallbackToast: true });

    }

  },


  /**

   * 获取标点图标路径(多级降级策略)

   * @param {Object} mapConfig - 地图配置

   * @returns {string} 标点图标路径

   */

  getMarkerIconPath(mapConfig) {

    console.log('【getMarkerIconPath-调试】开始图标决策');

    console.log('【getMarkerIconPath-调试】输入mapConfig:', mapConfig);

    console.log('【getMarkerIconPath-调试】输入mapConfig.markerIcons:', mapConfig?.markerIcons);

    console.log('【getMarkerIconPath-调试】输入mapConfig.markerIcons.default:', mapConfig?.markerIcons?.default);

    console.log('【getMarkerIconPath-调试】DEFAULT_MAP_CONFIG.markerIcons.default:', DEFAULT_MAP_CONFIG.markerIcons.default);

    console.log('【getMarkerIconPath-调试】FALLBACK_CONSTANTS.DEFAULT_MARKER_ICON:', FALLBACK_CONSTANTS.DEFAULT_MARKER_ICON);

   

    // 1. 优先使用配置中的图标

    if (mapConfig?.markerIcons?.default) {

      console.log('✅ 【getMarkerIconPath-决策】使用配置中的图标:', mapConfig.markerIcons.default);

      return mapConfig.markerIcons.default;

    } else {

      console.log('❌ 【getMarkerIconPath-决策】配置中没有图标,尝试默认配置');

    }

   

    // 2. 使用默认配置的图标

    if (DEFAULT_MAP_CONFIG.markerIcons.default) {

      console.log('✅ 【getMarkerIconPath-决策】使用默认配置图标:', DEFAULT_MAP_CONFIG.markerIcons.default);

      return DEFAULT_MAP_CONFIG.markerIcons.default;

    } else {

      console.log('❌ 【getMarkerIconPath-决策】默认配置中也没有图标,尝试保底图标');

    }

   

    // 3. 使用保底本地图标

    console.log('✅ 【getMarkerIconPath-决策】使用保底本地图标:', FALLBACK_CONSTANTS.DEFAULT_MARKER_ICON);

    return FALLBACK_CONSTANTS.DEFAULT_MARKER_ICON;

  },


  /**

   * 坐标转换计算

   * @param {number} lng - 经度

   * @param {number} lat - 纬度

   * @param {Object} bounds - 边界

   * @param {Object} imageSize - 图片尺寸

   * @returns {Object|null} 像素坐标

   */

  calculatePixelPosition(lng, lat, bounds, imageSize) {

    const { north, south, east, west } = bounds;

    const { width, height } = imageSize;

   

    // 检查是否在地图范围内

    if (lng < west || lng > east || lat < south || lat > north) {

      console.warn(`坐标(${lng}, ${lat})不在手绘地图范围内`);

      return null;

    }

   

    // 线性映射

    const x = ((lng - west) / (east - west)) * width;

    const y = ((north - lat) / (north - south)) * height;

   

    return { x, y };

  },


  /**

   * 将标点坐标转换为手绘地图像素坐标

   * @param {Object} page - 页面实例

   */

  convertSpotsToHanddraw(page) {

    const { mapSpots, handdrawConfig, mapImageSize } = page.data;

   

    if (!mapSpots || !handdrawConfig || !mapImageSize.width) {

      console.warn('坐标转换条件不满足');

      return;

    }

   

    const bounds = handdrawConfig.bounds;

    const imageSize = mapImageSize;

   

    // 验证边界有效性

    if (!this.hasValidBounds(bounds)) {

      console.warn('手绘地图边界无效,无法转换坐标');

      return;

    }

   

    const handdrawSpots = mapSpots.map(spot => {

      const pixelPos = this.calculatePixelPosition(

        spot.longitude,

        spot.latitude,

        bounds,

        imageSize

      );

     

      if (pixelPos) {

        return {

          ...spot,

          pixelX: pixelPos.x,

          pixelY: pixelPos.y

        };

      }

      return null;

    }).filter(spot => spot !== null);

   

    page.setData({ handdrawSpots });

    console.log(`坐标转换完成,有效标点: ${handdrawSpots.length}/${mapSpots.length}`);

  },


  /**

   * 加载地图和附近景点

   * @param {number} centerLat - 中心纬度

   * @param {number} centerLng - 中心经度

   * @param {Object} page - 页面实例

   */

  async loadMapAndNearbySpots(centerLat, centerLng, page) {

    const app = getApp();

    page.showLoading('加载地图资源中...');

   

    try {

      console.log('【loadMapAndNearbySpots-调试】开始加载地图配置');

     

      // 1. 获取地图配置

      let mapConfig = DEFAULT_MAP_CONFIG;

      try {

        const configRes = await this.getMapConfig(page);

        console.log('【loadMapAndNearbySpots-调试】getMapConfig返回结果:', configRes);

        console.log('【loadMapAndNearbySpots-调试】configRes.activeMode:', configRes?.activeMode);

       

        if (configRes && configRes.activeMode) {

          mapConfig = { ...DEFAULT_MAP_CONFIG, ...configRes };

          console.log('✅ 使用后端地图配置');

          console.log('【loadMapAndNearbySpots-调试】合并后的mapConfig:', mapConfig);

          console.log('【loadMapAndNearbySpots-调试】合并后的mapConfig.markerIcons:', mapConfig.markerIcons);

          console.log('【loadMapAndNearbySpots-调试】合并后的mapConfig.markerIcons.default:', mapConfig.markerIcons.default);

        } else {

          console.warn('后端配置无效,使用默认配置');

        }

      } catch (err) {

        console.warn('获取地图配置失败,使用默认配置:', err);

      }

     

      // 保存配置到页面

      page.setData({ mapConfig });

      console.log('【loadMapAndNearbySpots-调试】已保存mapConfig到页面data');

     

      // 2. 决策地图模式

      const shouldUseHanddraw = this.shouldUseHanddrawMap(mapConfig);

      console.log('【loadMapAndNearbySpots-调试】是否应使用手绘地图:', shouldUseHanddraw);

      console.log('【loadMapAndNearbySpots-调试】mapConfig.activeMode:', mapConfig.activeMode);

      console.log('【loadMapAndNearbySpots-调试】mapConfig.handdraw?.enabled:', mapConfig.handdraw?.enabled);

      console.log('【loadMapAndNearbySpots-调试】mapConfig.handdraw?.imageUrl:', mapConfig.handdraw?.imageUrl);

      console.log('【loadMapAndNearbySpots-调试】hasValidBounds:', this.hasValidBounds(mapConfig.handdraw?.bounds));

     

      if (shouldUseHanddraw) {

        // 3. 尝试手绘地图模式(作为地面覆盖物)

        console.log('【loadMapAndNearbySpots-调试】尝试加载手绘地图为覆盖物');

        const handdrawSuccess = await this.tryLoadHanddrawMap(mapConfig.handdraw, page);

       

        if (!handdrawSuccess) {

          // 手绘地图失败,降级到标准腾讯地图

          console.warn('手绘地图覆盖物加载失败,使用标准腾讯地图');

          this.fallbackToTencentMap(page, '手绘地图加载失败');

         

          // 设置地图中心为用户位置

          page.setData({

            mapCenterLatitude: centerLat,

            mapCenterLongitude: centerLng,

            mapScale: 18

          });

        } else {

          console.log('✅ 手绘地图覆盖物加载成功');

        }

      } else {

        // 4. 使用腾讯地图(默认或配置指定)

        console.log('使用标准腾讯地图模式');

        this.fallbackToTencentMap(page, '配置为腾讯地图模式');

       

        // 设置地图中心为用户位置

        page.setData({

          mapCenterLatitude: centerLat,

          mapCenterLongitude: centerLng,

          mapScale: 18

        });

      }

     

      // 5. 加载景点数据

      console.log('【loadMapAndNearbySpots-调试】开始加载景点数据');

      await this.loadNearbySpotsFromCloud(centerLat, centerLng, page, mapConfig);

     

    } catch (err) {

      console.error('地图加载异常,强制使用腾讯地图:', err);

      this.fallbackToTencentMap(page, '加载异常');

     

      // 设置地图中心为用户位置

      page.setData({

        mapCenterLatitude: centerLat,

        mapCenterLongitude: centerLng,

        mapScale: 18

      });

     

      await this.loadNearbySpotsFromCloud(centerLat, centerLng, page, DEFAULT_MAP_CONFIG);

    } finally {

      page.hideLoading();

    }

  },


  /**

   * 使用默认位置

   * @param {Object} page - 页面实例

   */

  useDefaultLocation: function (page) {

    const defaultLat = 30.8;

    const defaultLng = 120.3;

    page.setData({

      mapCenterLatitude: defaultLat,

      mapCenterLongitude: defaultLng

    });

    this.loadMapAndNearbySpots(defaultLat, defaultLng, page);

    wx.showToast({

      title: '已使用默认位置,部分功能受限',

      icon: 'none',

      duration: 2000

    });

  },


  /**

   * 解析位置字符串 (从原始 map.js 中迁移的工具函数)

   * @param {*} locationObj - 位置对象

   * @returns {Object} 解析结果

   */

  parseLocationString: function(locationObj) {

    // 这里直接调用已提取到 utils 中的函数

    // 注意:此函数是工具函数,不应直接操作 page,因此不传入 page 参数

    // 实际实现应已在 utils/content-utils.js 中

    // 此处仅为保持函数签名一致,实际调用 utils 中的版本

    return utils.parseLocationString(locationObj);

  },


  /**

   * 从云端加载附近景点

   * @param {number} centerLat - 中心纬度

   * @param {number} centerLng - 中心经度

   * @param {Object} page - 页面实例

   * @param {Object} mapConfig - 地图配置

   */

  loadNearbySpotsFromCloud: function (centerLat, centerLng, page, mapConfig) {

    const app = getApp();

    console.log('【地图页-调试】开始以用户位置为中心加载附近景点,中心点:', centerLat, centerLng);

    console.log('【loadNearbySpotsFromCloud-调试】传入的mapConfig:', mapConfig);

    console.log('【loadNearbySpotsFromCloud-调试】传入的mapConfig.markerIcons:', mapConfig?.markerIcons);

    console.log('【loadNearbySpotsFromCloud-调试】传入的mapConfig.markerIcons.default:', mapConfig?.markerIcons?.default);

   

    app.callGateway('getMapSpots', {

      centerLatitude: centerLat,

      centerLongitude: centerLng,

    })

      .then(res => {

        console.log('✅ 附近景点数据加载结果:', res);

        if (res.code === 200) {

          const spotsData = Array.isArray(res.data) ? res.data : [];

          console.log('【地图页-调试】转换后景点数组 spotsData:', spotsData);

          console.log('【地图页-调试】数组长度:', spotsData.length);

         

          if (spotsData.length > 0) {

            console.log(`✅ 加载云端景点数据成功,数量: ${spotsData.length}`);

           

            // 获取zhu合集图标路径(第二优先级)

            const globalDefaultIcon = this.getMarkerIconPath(mapConfig || DEFAULT_MAP_CONFIG);

            console.log('【图标优先级-全局】zhu合集图标路径:', globalDefaultIcon);

           

            const spots = spotsData.map((item, index) => {

              let finalLatitude = null;

              let finalLongitude = null;

             

              if (item.latitude !== undefined && item.longitude !== undefined) {

                finalLatitude = parseFloat(item.latitude);

                finalLongitude = parseFloat(item.longitude);

                console.log(`【前端解析】使用后端解析的坐标: ${finalLatitude}, ${finalLongitude}`);

              }

              else if (item.location) {

                const parsed = this.parseLocationString(item.location);

                if (parsed.success) {

                  finalLatitude = parsed.latitude;

                  finalLongitude = parsed.longitude;

                } else {

                  console.warn(`❌ 【前端解析】景点"${item.name}"的location解析失败`);

                  return null;

                }

              }

              else {

                console.warn(`❌ 【前端解析】景点"${item.name}"无location信息`);

                return null;

              }

             

              if (finalLatitude === null || finalLongitude === null ||

                  isNaN(finalLatitude) || isNaN(finalLongitude) ||

                  Math.abs(finalLatitude) > 90 || Math.abs(finalLongitude) > 180) {

                console.warn(`❌ 【前端解析】景点"${item.name}"坐标无效: ${finalLatitude}, ${finalLongitude}`);

                return null;

              }

             

              // ========== 【核心修改】图标优先级逻辑 ==========

              // 替换原有的图标优先级判断逻辑

let finalIconPath = '';

const spotIcon = (item.iconUrl || item.icon || '').toString().trim();


if (spotIcon) {

  finalIconPath = spotIcon;

  console.log(`✅ 【图标优先级】景点"${item.name}"使用自身图标: ${finalIconPath}`);

} else if (globalDefaultIcon && globalDefaultIcon.trim()) {

  finalIconPath = globalDefaultIcon.trim();

  console.log(`✅ 【图标优先级】景点"${item.name}"使用zhu合集图标: ${finalIconPath}`);

} else {

  finalIconPath = FALLBACK_CONSTANTS.DEFAULT_MARKER_ICON;

  console.log(`⚠️ 【图标优先级】景点"${item.name}"使用保底本地图标: ${finalIconPath}`);

}

              console.log(`【图标调试】景点"${item.name}"最终图标: ${finalIconPath}`);

              // ========== 【核心修改结束】 ==========

             

              return {

                id: item._id || index,

                name: item.name || `景点_${item._id || index}`,

                latitude: finalLatitude,

                longitude: finalLongitude,

                width: 40,

                height: 40,

                iconPath: finalIconPath,  // 使用最终确定的图标路径

                rawData: item

              };

            }).filter(spot => spot !== null);

           

            console.log('【地图页-调试】转换后的地图标点数组 (mapSpots):', spots);

            console.log(`✅ 【前端解析】成功解析 ${spots.length}/${spotsData.length} 个景点`);

           

            if (spots.length > 0) {

              page.setData({ mapSpots: spots });

              console.log('【loadNearbySpotsFromCloud-调试】已设置mapSpots到页面data');

              console.log('【loadNearbySpotsFromCloud-调试】第一个标点的iconPath:', spots[0]?.iconPath);

             

              // 注意:现在手绘地图是覆盖物,不需要转换坐标

              // 景点会直接显示在腾讯地图上,手绘地图图片会覆盖在指定区域

            } else {

              console.warn('❌ 所有景点数据解析失败,地图无标点');

              page.setData({ mapSpots: [] });

              wx.showToast({

                title: '景点坐标解析失败,请检查数据格式',

                icon: 'none',

                duration: 3000

              });

            }

          } else {

            console.warn('云端返回的景点数据为空');

            page.setData({ mapSpots: [] });

            wx.showToast({ title: '当前景区暂无景点数据', icon: 'none' });

          }

        } else {

          console.error('加载云端景点数据失败,状态码非200:', res.message || '未知错误');

          page.setData({ mapSpots: [] });

          wx.showToast({ title: '加载景点数据失败,请稍后重试', icon: 'none' });

        }

      }).catch(err => {

        console.error('调用云端接口失败:', err);

        page.setData({ mapSpots: [] });

        wx.showToast({ title: '网络请求失败', icon: 'none' });

      }).finally(() => {

        page.hideLoading();

      });

  }

};


// 导出单例

module.exports = mapService;


回答关注问题邀请回答
收藏
登录 后发表内容