使用 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;
