小程序地图以及讲解的使用
JS的代码
const app = getApp();
const { request } = require('../../../../../utils/request');
const API = require("../../../../../config/api");
// import { now } from "lodash-es";
import { aplus_sendEvent, aplus_sendEXP } from "../../../../../utils/dataTrack/util";
const { getLocation, getRandomLocation } = require("../../../../../utils/util");
// 引入腾讯地图 /utils/qqmap-wx-jssdk.min
const QQMapWX = require('../../../../../utils/qqmap-wx-jssdk.min');
let qqmapsdk;
const disabledTime = 300
const PLAY_TYPE = {
CLICK: 1, // 用户点击(景点 icon/名称)
AUTO: 2 // 实时定位自动播放
};
Component({
/**
* 组件的属性列表
*/
properties: {
from: {
type: String,
value: '',
},
// 讲解卡数据
item: {
type: Object,
value: {},
},
// 当前轮播图的索引
currentIndex: {
type: Number,
value: 0,
},
// 当前卡片的索引
cardIndex: {
type: Number,
value: 0,
},
// 轮播图大图显示
bigSwiperShow: {
type: Boolean,
value: false,
},
// 卡片上报场景码 卡片页:100 聊天页:300 我的收藏页:500
cardSceneCode: {
type: Number,
value: 100,
},
isHomeCard: {
type: Boolean,
value: false
}
},
/**
* 组件的初始数据
*/
data: {
isCurrentCard: true, // 是否当前卡片
preCurrentIndex: 0, // 记录currentIndex
isIOS: wx.getDeviceInfo().system.indexOf('iOS') !== -1,
// 地图相关数据
scale: 18,
markers: [],
//万松书院
// latitude: 30.227100,
// longitude: 120.161024,
// userLocation: {
// latitude: 30.227100,
// longitude: 120.161024
// },
//手摇船
// latitude: 30.251685,
// longitude: 120.142587,
// userLocation: {
// latitude: 30.251685,
// longitude: 120.142587
// },
polyline: [], // 添加多条路线支持
// 存储所有点位的边界
boundaryPoints: {
minLat: Number.MAX_VALUE,
maxLat: Number.MIN_VALUE,
minLng: Number.MAX_VALUE,
maxLng: Number.MIN_VALUE
},
totalRoutes: 0,
completedRoutes: 0,
includePoints: [],
setting: {
padding: [120, 100, 200, 100],
enableRotation: false,
showScale: false,
showLocation: false,
enableOverlooking: false
},
routeParams: null, // 存储路线参数
localAvatarPath: '', // 本地头像路径
innerAudioContext: null,
bgAudioContext: null,//https://videos.biliq.com/videos/ice_output/89_17544601861.mp3
userLocationMarker: null, //专门存用户位置的 marker
avatarUrl: null,
walkTimer: null, // 定时器
walkIndex: 0, // 当前走到第几个点
nearScale: 20, // 近距离放大级别
farScale: 18, // 正常级别
playingMarkerId: null, // 正在播放的 marker id
stayCanRecord: false, // 当前卡片是否应该记录
stayTime: null, // 时常
lastPlayedId: null, // 最后播放的markerID
lastPlayType: 2, //1:实时获取位置后推荐播放 2.:用户点击
followUserLocation: true, // 是否地图跟随用户
lastFollowLocation: null, // 上一次触发跟随的位置
activeRouteIndex: 0, // 默认选中第一条路线
markerPlaying: null, //正在讲解的marker
isCenter: 0, //是否在aoi中 1在,0不在
markerStyle: {
default: {
iconPath: '', // 默认图标
callout: {
color: '#515F63', // 默认文字颜色
fontSize: 14,
bgColor: 'transparent',
// fontWeight: 'bold',
// padding: 2,
anchorY: 60, // 调整文本垂直位置,负值表示向上移动
display: 'ALWAYS'
}
},
active: {
iconPath: 'https://images.biliq.com/images/miniprogram/paopao2.0/paopaoCard/talk-attraction.gif', // 点击态图标
// customCallout: {
// color: '#E0ED42',
// fontSize: 14,
// borderRadius: 10,
// bgColor: '#112029',
// padding: 4,
// anchorY: 0,
// display: 'ALWAYS',
// },
customCallout: {//自定义气泡
display: "ALWAYS",//显示方式,可选值BYCLICK
anchorX: 0,//横向偏移
anchorY: 0,
},
callout: {
color: '#E0ED42',
fontSize: 16,
borderRadius: 10,
bgColor: '#112029',
padding: 4,
anchorY: 0,
display: 'ALWAYS',
}
}
},
activeMarkerId: null, // 记录当前被点击的标记点ID
activeGuideName: null, // 记录当前被点击的标记点名称
activeGuideNext: null, // 记录当前被点击的标记点下一个名称
userInfo: null,
show: false,
animationData: {},
startTouchY: 0, // 记录触摸开始时的 Y 坐标
currentTouchY: 0, // 记录当前触摸的 Y 坐标
deltaY: 0, // 记录滑动的距离
swiping: false, // 是否正在滑动
chatMapAvatorHidden: false,
markerAudioPlay: true,
borderRadius: '10rpx',
cardOpacity: 1.0,//卡片透明度
cardDescOpacity: 1.0,//卡片描述透明度
overlayHide: false, // 控制遮罩是否处于收起状态(用于动画)
overlayAnimating: false, // 原有状态,保持不变
isLogin: false,
borderRadius: '5rpx',
locationTimer: null, //定时器ID(用于后续定位)
mindistance: -1,
minmarker: null,
lastGPSUpdatedTime: null,
lastAutoPlayMarkerId: null, // 上次自动播放的标记点 ID
lastAutoPlayTime: 0, // 上次自动播放的时间
autoPlayInterval: 10000, // 自动播放间隔(10 秒)
distPlay: 10, //10米
},
pageLifetimes: {
show() {
const isLogin = wx.getStorageSync('isLogin') === '1';
this.setData({
userInfo: wx.getStorageSync('loginUserInfo') || {},
isLogin,
lastGPSUpdatedTime: Date.now(),
setting: {
showLocation: !isLogin
}
});
//初始化前清理音频
this.destroyAllAudio();
// 原有逻辑:恢复播放、卡片加载上报、事件监听...
if (this.data.wasPlayingBeforeNavigate && this.data.bigSwiperShow) {
this.initAudioContext(); // 重新初始化音频(避免使用旧实例)
this.playAudio();
this.setData({ wasPlayingBeforeNavigate: false });
}
if (this.data.item?.id && (this.data.item?.id !== this.data.lastCardId)) {
this.cardLoadReport();
this.data.lastCardId = this.data.item.id;
}
app.eventBus.on('autoPauseAllCards', this.pauseAudio.bind(this));
// app.eventBus.on('lastCardInAgent', this.checkIsLastCard.bind(this));
this.startStayTimer(); // 滑入 -> 开始计时
this.startLocationTimer();//页面显示时启动「定时获取位置」任务(10秒/次)
},
hide() {
if (this.data.isPlaying) {
this.pauseAudio(); // 触发 onPause 回调,自动更新 isPlaying
this.setData({ wasPlayingBeforeNavigate: true });
}
// 【新增】讲解结束上报
const marker = this.data.markers.find(m => m.id === this.data.lastPlayedId);
if (marker) {
this.reportAudioPlayEvent(marker, this.data.lastPlayType);
}
if (this.data.isCurrentCard && this.data.isHomeCard) {
this.stopStayTimerAndReport(); // 隐藏 -> 结束并上报
}
// this.stopWatchLocation(); // 停止位置监听(原有逻辑,确保保留)
//页面隐藏时清除「定时获取位置」任务
this.stopLocationTimer();
app.eventBus.off('autoPauseAllCards', this.pauseAudio.bind(this));
// app.eventBus.off('lastCardInAgent', this.checkIsLastCard.bind(this));
}
},
lifetimes: {
// 在组件实例进入页面节点树时执行
attached() {
// 初始化地图SDK
const isLogin = wx.getStorageSync('isLogin') === '1';
this.setData({
userInfo: wx.getStorageSync('loginUserInfo') || {},
isLogin,
lastGPSUpdatedTime: Date.now(),
userLocation: this.data.userLocation,
});
qqmapsdk = new QQMapWX({
key: '6H***-UG***-LH***-ZPF***'
});
//延时1秒刷新
setTimeout(() => {
this.setData({
setting: {
showLocation: !isLogin
}
});
}, 1000);
if (this.data.item) {
try {
this.fetchAttractionData();
} catch (error) {
console.error('解析数据失败:', error);
}
} else {
console.error('未收到 URL 参数');
}
app.eventBus.on('autoPauseAllCards', this.pauseAudio.bind(this));
},
onShow() {
this.setData({ pageEnterTime: Date.now() });
},
detached() {
// 卸载时彻底清理音频
this.destroyAllAudio();
//移除事件总线监听、结束计时上报...
app.eventBus.off('autoPauseAllCards', this.pauseAudio.bind(this));
// app.eventBus.off('lastCardInAgent', this.checkIsLastCard.bind(this));
this.stopStayTimerAndReport();
// this.stopWatchLocation(); // 停止位置监听(原有逻辑,确保保留)
// 新增:组件卸载时清除定时器(双重保险)
this.stopLocationTimer();
},
},
observers: {
// 讲解卡数据
'item': function (item) {
this.setData({
isCenter: item.privData.isCenter
});
},
// 当前轮播图的索引
'currentIndex': function (currentIndex) {
if (!this.isHomePage()) {
return false;
}
this.setData({
isCurrentCard: currentIndex === this.data.cardIndex,
cardOpacity: currentIndex === this.data.cardIndex ? 1.0 : 0.5
});
if (this.data.cardIndex === 0 || currentIndex + 1 === this.data.cardIndex) {//首页音频必加载,或者预加载下一页音频
this.initAudioContext();
}
// 如果不是当前卡片,则暂停音频
if (this.data.isCurrentCard) {
if (this.data.bigSwiperShow) {
this.startStayTimer(); // 滑入 -> 开始计时
this.playAudio();
// 组件显示时,获取初始位置
this.getInitialLocation();
} else {
this.pauseAudio();
}
}
else {
if ((this.data.cardIndex - this.data.currentIndex === 1 || this.data.cardIndex - this.data.currentIndex === -1) && this.data.stayCanRecord) {
this.stopStayTimerAndReport(); // 滑出 -> 结束并上报
}
this.pauseAudio();
// this.stopWatchLocation();
//页面隐藏时清除「定时获取位置」任务
this.stopLocationTimer();
// 【新增】切换卡片时,若有正在播放的讲解,上报
const marker = this.data.markers.find(m => m.id === this.data.lastPlayedId);
if (marker) {
this.reportAudioPlayEvent(marker, this.data.lastPlayType);
}
}
// 判断左滑还是右滑 currentIndex 和 preCurrentIndex 差值大于1 或者 小于-1 且stayCanRecord为true 则上报停留时长
const isLeftSwiper = this.data.currentIndex > this.data.preCurrentIndex ? true : false;
const condition1 = !isLeftSwiper && (this.data.cardIndex - this.data.currentIndex === 1);
const condition2 = isLeftSwiper && (this.data.cardIndex - this.data.currentIndex === -1);
if ((condition1 || condition2) && this.data.stayCanRecord) {
console.log(`第${this.data.cardIndex}张讲解卡滑出`);
this.stopStayTimerAndReport(); // 滑出 -> 结束并上报
}
// 记录上一个索引
this.setData({
preCurrentIndex: this.data.currentIndex
})
},
// 轮播图大图显示
'bigSwiperShow': function (bigSwiperShow) {
if (bigSwiperShow) {
this.playAudio();
this.setData({
chatMapAvatorHidden: false
});
} else {
this.pauseAudio();
// this.stopWatchLocation();
//页面隐藏时清除「定时获取位置」任务
this.stopLocationTimer();
this.setData({
chatMapAvatorHidden: true
});
}
},
},
/**
* 组件的方法列表
*/
methods: {
// 模拟用户位置移动到第一个标记点10米内
simulateUserLocationNearMarker(index) {
const { markers } = this.data;
if (!markers || markers.length < 2) { // 需至少有1个标记点(排除用户自身标记点id=10001)
console.warn('无有效标记点,无法模拟位置');
return;
}
// 找到第一个非用户的标记点(假设第一个标记点为目标)
// const targetMarker = markers.find(marker => marker.id !== 10001);
const targetMarker = markers[index];
if (!targetMarker) {
console.warn('未找到有效目标标记点');
return;
}
// 计算目标标记点10米内的随机位置(优化:添加微小偏移,模拟真实移动)
const offset = 0.00005; // 约5米偏移(经纬度每0.00001≈1米)
const simulatedLocation = {
latitude: targetMarker.latitude + (Math.random() - 0.5) * offset,
longitude: targetMarker.longitude + (Math.random() - 0.5) * offset,
};
// 更新用户位置(触发位置变化监听)
this.setData({ userLocation: simulatedLocation });
// 手动触发一次自动讲解检测(确保立即生效)
this.checkNearbyAudio(simulatedLocation); // 新增:传入目标标记ID,用于精准更新气泡
},
// 判断是当前页面是不是 /pages/home/home
isHomePage() {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
return currentPage.route === 'pages/home/home';
},
//埋点-卡片加载
cardLoadReport() {
try {
let geoLocation = '';
const location = wx.getStorageSync('currentLocation');
if (location && location.latitude) {
// geoLocation = `${location.latitude},${location.longitude}`;
geoLocation = `${location.latitude},${location.longitude}`
}
// aplus_sendEvent('card_load', 'OTHER', {
// cardId: this.data.item.id,
// resouceId: this.data.item.resourceId,
// cardType: this.data.item.type,
// cardRank: this.data.cardIndex + 1,
// sceneCode: this.data.cardSceneCode,
// modelType: this.data.item.modelType,
// cardName: this.data.item.name,
// });
} catch (e) {
// 静默失败或可选上报
}
},
// 开始计时
startStayTimer() {
this.setData({
stayCanRecord: true,
stayTime: Date.now()
});
},
// 获取初始位置(组件挂载时执行一次)
// 新增:获取初始位置(组件挂载时执行一次)
getInitialLocation() {
return new Promise((resolve) => {
wx.getLocation({
type: 'gcj02', // 国测局坐标系(与地图SDK一致)
success: (res) => {
const initialLocation = {
latitude: res.latitude,
longitude: res.longitude
};
this.setData({
userLocation: initialLocation,
lastFollowLocation: initialLocation
});
// 初始定位成功后,立即启动定时定位任务
this.startLocationTimer();
resolve();
},
fail: (err) => {
console.error('初始位置获取失败,使用默认位置', err);
// const defaultLocation = {
// latitude: 30.251442,
// longitude: 120.136275
// };
// this.setData({
// userLocation: defaultLocation,
// lastFollowLocation: defaultLocation
// });
// 即使初始定位失败,也启动定时定位(后续可能恢复定位权限)
this.startLocationTimer();
resolve();
}
});
});
},
// 启动「定时获取位置」定时器(10秒/次)
startLocationTimer() {
// 先清除已有定时器(避免重复创建)
this.stopLocationTimer();
// 每隔10秒执行一次定位
this.data.locationTimer = setInterval(() => {
this.getLocationOnce();
}, 10000); // 10秒间隔
},
// 新增:单次获取位置(定时器回调方法)
getLocationOnce() {
wx.getLocation({
type: 'gcj02',
success: (res) => {
// 调用位置处理逻辑(复用原 onLocationChange 内部代码)
this.handleLocationUpdate(res);
},
fail: (err) => {
console.error('定时获取位置失败', err);
// 失败时可选择:1.忽略 2.使用上次位置 3.提示用户开启定位
// 此处选择忽略,避免频繁弹窗打扰用户
}
});
},
// 位置处理核心逻辑(复用原 onLocationChange 内部代码)
handleLocationUpdate(res) {
const nowTime = Date.now();
// 原有逻辑:判断自动播放间隔
if (nowTime - this.data.lastAutoPlayTime >= this.data.autoPlayInterval) {
const { latitude, longitude } = res;
const newPt = { latitude, longitude };
// -------------- 原有模拟位置代码(可保留或删除)--------------
// const { markers } = this.data;
// let simulatedLocation = { latitude, longitude }; // 默认用真实位置
// if (markers && markers.length >= 2) {
// const targetMarker = markers[0];
// const offset = 0.00005; // 约5米偏移
// simulatedLocation = {
// latitude: targetMarker.latitude + (Math.random() - 0.5) * offset,
// longitude: targetMarker.longitude + (Math.random() - 0.5) * offset,
// };
// console.log('模拟用户位置移动到:', simulatedLocation, targetMarker.callout.content);
// }
// const newPt = simulatedLocation;
// -------------- 模拟位置代码结束 --------------
// 1. 更新用户位置和标记
this.setData({ userLocation: newPt });
const userMarker = this.buildUserMarker();
const others = this.data.markers.filter(m => m.id !== 10001);
this.setData({ markers: [...others, userMarker] });
// 2. 触发自动讲解检测
this.checkNearbyAudio(newPt);
// 3. 原有地图跟随、AOI判断逻辑
const { isCenter, followUserLocation, lastFollowLocation } = this.data;
if (isCenter === 1) {
if (followUserLocation) {
this.setData({
latitude: newPt.latitude,
longitude: newPt.longitude,
lastFollowLocation: newPt
});
}
// 用户拖动后远离50米,自动切回跟随
const movedDistance = this.getDistanceBetweenPoints(lastFollowLocation, newPt);
if (!followUserLocation && movedDistance >= 50) {
this.setData({
followUserLocation: true,
lastFollowLocation: newPt,
latitude: newPt.latitude,
longitude: newPt.longitude
});
}
} else {
}
}
},
// 清除「定时获取位置」定时器
stopLocationTimer() {
if (this.data.locationTimer) {
clearInterval(this.data.locationTimer);
this.data.locationTimer = null;
}
},
checkIsLastCard() {
if (this.data.cardIndex === this.data.currentIndex) {
this.stopStayTimerAndReport();
}
},
// 结束计时 + 上报
stopStayTimerAndReport() {
if (!this.data.bigSwiperShow) return;
const finalStayTime = (Date.now() - this.data.stayTime) / 1000;
//真正滑出 埋点-卡片曝光
try {
aplus_sendEXP('card_expose', 'EXP', {
cardId: this.data.item.id,
resourceId: this.data.item.resourceId,
routeId: this.data.item.routes?.id,
cardType: this.data.item.type,
cardRank: this.data.cardIndex + 1,
stayTime: finalStayTime,
isShareable: 0,
isCollectible: 0,
isNavigable: 0,
isPurchasable: this.data.item.componentType === 'ticket' ? 1 : 0,
isReplicable: this.data.item.componentType === 'camera' ? 1 : 0,
ifCoupon: this.data.item.componentType === 'coupon' ? 1 : 0,
ifTofriend: 0,
ifCameraPackage: 0,
recReason: this.data.item.cardPersonalization,
sceneCode: this.data.cardSceneCode,
modelType: this.data.item.modelType,
cardName: this.data.item.name,
});
} catch (e) {
// 重置开始时间
this.setData({
stayCanRecord: false,
stayTime: null
});
}
},
// 获取页面实例
getPageInstance() {
const pages = getCurrentPages();
return pages[pages.length - 1];
},
// 初始化音频
initAudioContext() {
// 1. 销毁旧音频实例(避免重复创建)
if (!this.data.bgAudioContext) {
// 创建背景音频实例...
this.data.bgAudioContext = wx.createInnerAudioContext();
this.data.bgAudioContext.src = 'https://videos.biliq.com/videos/ice_output/89_17544601861.mp3';
this.data.bgAudioContext.loop = true;
this.data.bgAudioContext.volume = 0.2;
// 监听事件(注意:每次创建新实例后重新绑定监听,避免旧监听残留)
this.data.bgAudioContext.onPlay(() => {
this.setData({ isPlaying: true });
});
this.data.bgAudioContext.onPause(() => {
this.setData({ isPlaying: false });
});
this.data.bgAudioContext.onEnded(() => {
});
}
// 原有逻辑:创建二级讲解音频实例...
if (!this.data.innerAudioContext) {
const routePlan = this.data.item.privData?.routePlan;
const activeGuideNext = routePlan[0]?.name;
this.setData({
activeGuideName: this.data.item.name,
activeGuideNext: activeGuideNext
});
this.data.innerAudioContext = wx.createInnerAudioContext();
this.data.innerAudioContext.src = this.data.item.privData?.audioUrl;
this.data.innerAudioContext.onEnded(() => {
this.setData({
activeGuideName: null
});
if (this.data.bgAudioContext) {
this.data.bgAudioContext.volume = 1;
}
});
}
// 最后播放音频(确保实例创建后再播放)
// this.data.bgAudioContext.play();
// if (this.data.innerAudioContext) {
// this.data.innerAudioContext.play();
// }
},
/**
* 销毁所有音频实例并重置状态
* 作用:避免历史音频残留,防止音频混杂
*/
destroyAllAudio() {
// 1. 销毁背景音频实例(bgAudioContext)
if (this.data.bgAudioContext) {
this.data.bgAudioContext.pause(); // 先暂停
this.data.bgAudioContext.destroy(); // 销毁实例
this.data.bgAudioContext = null; // 置空,避免后续引用
}
// 2. 销毁二级讲解音频实例(innerAudioContext)
if (this.data.innerAudioContext) {
this.data.innerAudioContext.pause(); // 先暂停
this.data.innerAudioContext.destroy(); // 销毁实例
this.data.innerAudioContext = null; // 置空
}
// 4. 重置音频相关状态(关键:避免状态混乱导致的控制异常)
this.setData({
isPlaying: false, // 重置播放状态
playingMarkerId: null, // 重置正在播放的标记点ID
lastPlayedId: null, // 重置最后播放的标记点ID
stayTime: 0, // 重置播放时长
markerPlaying: null, // 重置正在讲解的marker
});
},
// 播放音频
playAudio() {
// 视频上下文不存在、正在播放、不是当前卡片,则不播放
if (!this.data.bgAudioContext || this.data.isPlaying || !this.data.isCurrentCard || !this.data.bigSwiperShow) return;
//重新加载快进智能体
if (this.data.from === 'card' && !this.data.bigSwiperShow) return;
// 确保音频已准备好
this.data.bgAudioContext.play();
if (this.data.innerAudioContext) {
this.data.innerAudioContext.play();
}
this.setData({ isPlaying: true });
},
// 暂停音频
pauseAudio() {
// 音频上下文不存在或未播放,则不执行操作
// if (!this.data.bgAudioContext || !this.data.isPlaying) return;
// try {
// this.data.bgAudioContext.pause();
// this.setData({ isPlaying: false });
// if (this.data.innerAudioContext) {
// this.data.innerAudioContext.pause();
// }
// } catch (e) {
// // 强制更新状态
// this.setData({ isPlaying: false });
// }
if (this.data.bgAudioContext) {
this.data.bgAudioContext.pause();
this.setData({ isPlaying: false });
}
if (this.data.innerAudioContext) {
this.data.innerAudioContext.pause();
}
// 【新增】暂停时,上报当前讲解的结束
const marker = this.data.markers.find(m => m.id === this.data.lastPlayedId);
if (marker) {
this.reportAudioPlayEvent(marker, this.data.lastPlayType);
}
},
togglePlay() {
if (this.data.isPlaying) {
this.pauseAudio();
this.setData({
isPlaying: false
});
// 埋点-路线讲解播放
try {
aplus_sendEvent('audioTour_play', 'OTHER', {
cardId: this.data.item.id,
resourceId: this.data.item.resourceId,
cardType: this.data.item.type
});
} catch (e) { }
} else {
// 确保音频上下文存在
if (!this.data.bgAudioContext) {
this.initAudioContext();
}
this.playAudio();
}
},
// 埋点-卡片点击
cardClick() {
try {
aplus_sendEvent('card_click', 'CLK', {
cardId: this.data.item.id,
resourceId: this.data.item.resourceId,
cardType: this.data.item.type,
modelType: this.data.item.modelType,
cardName: this.data.item.name,
recReason: this.data.item.cardPersonalization,
sceneCode: 100
});
} catch (e) { }
},
onAction() {
// 如果不是特殊景点ID,直接返回
// if (!this.isSpecialAttraction(this.data.item.attractionId)) {
// return;
// }
// 如果已禁用,则直接返回
if (this.data.isDisabled) return;
// 触发事件前禁用按钮
this.setData({
isDisabled: true,
wasPlayingBeforeNavigate: this.data.isPlaying // 记录当前播放状态
});
// 埋点-卡片点击
try {
aplus_sendEvent('card_click', 'CLK', {
cardId: this.data.item.id,
resourceId: this.data.item.resourceId,
cardType: this.data.item.type,
modelType: this.data.item.modelType,
cardName: this.data.item.name,
recReason: this.data.item.cardPersonalization,
sceneCode: 100
});
} catch (e) { }
// 埋点-去实景地图icon点击
try {
aplus_sendEvent('toRealScene_click', 'CLK', {
cardId: this.data.item.id,
resourceId: this.data.item.resourceId,
cardType: this.data.item.type
});
} catch (e) { }
// 跳转前暂停音频
this.pauseAudio();
// 1秒后恢复可点击状态(根据需求调整时间)
setTimeout(() => {
this.setData({ isDisabled: false });
}, disabledTime);
},
onRegionChange(e) {
// 用户手指离开屏幕后触发
if (e.type === 'end' && e.causedBy === 'drag') {
this.setData({ followUserLocation: false });
}
if (e.type === 'end') {
this.triggerEvent('action', {
type: 'mapDragEnd'
});
} else {
this.triggerEvent('action', {
type: 'mapDrag'
});
}
},
//创建用户中心
buildUserMarker() {
// 确保一定有值
if (!this.data.userLocation) {
this.setData({
userLocation: {
latitude: 39.948318,
longitude: 116.423990
// latitude: 30.227100,
// longitude: 120.161024
}
});
}
// if (!this.data.userLocation) return null;
return {
id: 10001,
latitude: this.data.userLocation?.latitude,
longitude: this.data.userLocation.longitude,
width: 1,
height: 1,
// iconPath: this.data.userInfo.avatarUrl,
iconPath: 'https://images.biliq.com/images/miniprogram/paopao2.0/paopaoCard/transpant.png',
customCallout: {
display: 'ALWAYS',
},
zIndex: 999
};
},
// 更新地图中心为用户位置(可选)
updateMapCenter() {
if (this.data.userLocation) {
this.setData({
latitude: this.data.userLocation?.latitude,
longitude: this.data.userLocation.longitude
});
}
},
// 展开讲解卡数据
fetchAttractionData() {
const allRoutes = [];
if (this.data.item.privData.routePlan.length > 0) {
allRoutes.push({
list: this.data.item.privData.routePlan,
});
}
if (allRoutes.length > 0) {
this.setData({
allRoutes: allRoutes,
routeParams: {
routes: allRoutes // 存储所有路线数据
}
}, () => {
this.initMultiRouteMap(allRoutes);
});
}
},
initMultiRouteMap(allRoutes) {
if (!allRoutes || !allRoutes.length) {
console.error('没有有效的路线数据');
return;
}
// 清除旧路线
this.setData({ polyline: [] });
/* 1. 处理接口点位及路线:生成所有标记点和坐标(原有逻辑保留) */
let routeMarkers = []; // 所有景点标记点
let routePoints = []; // 所有景点坐标
allRoutes.forEach((route, routeIndex) => {
const list = route.list || [];
const { default: defaultStyle } = this.data.markerStyle;
// 生成所有景点的 marker
const markers = list.map((p, idx) => {
// const id = routeMarkers.length + idx;
const id = idx;
return {
id,
latitude: Number(p.poiBase.latitude),
longitude: Number(p.poiBase.longitude),
width: 40,
height: 40,
fontSize: 14,
iconPath: p.routeIconUrl,
callout: {
...defaultStyle.callout,
content: p.name,
custom: true,
},
customData: {
audioUrl: p.explanationUrl,
attractionID: p.id,
attractionName: p.name,
routeIndex: routeIndex
}
};
});
routeMarkers.push(...markers);
routePoints.push(...markers.map(p => ({ latitude: p.latitude, longitude: p.longitude })));
});
/* 2. 添加用户位置标记点(id=10001,原有逻辑保留) */
const userMarker = this.buildUserMarker();
const finalMarkers = [...routeMarkers, userMarker];
const userPt = this.data.userLocation; // 用户当前位置
/* 3. 根据 isCenter 差异化设置地图中心、缩放、视野 */
const { isCenter } = this.data;
let mapCenter = {}; // 地图中心坐标
let mapScale = 16; // 默认中等缩放(AOI外使用)
let targetPoints = []; // 用于计算视野的目标点
if (isCenter === 1) {
// -------------------------- AOI内逻辑(isCenter=1) --------------------------
// 地图中心:用户当前位置
mapCenter = {
latitude: userPt.latitude,
longitude: userPt.longitude
};
// 缩放层级:按用户与最近1-3个标记点的距离动态调整
if (routePoints.length > 0) {
// 计算所有标记点到用户的距离,排序后取前3个最近的
const pointsWithDistance = routePoints.map(p => ({
...p,
distance: this.getDistanceBetweenPoints(userPt, p)
})).sort((a, b) => a.distance - b.distance);
const nearestPoints = pointsWithDistance.slice(0, 3); // 最多3个最近点
// 动态调整缩放:距离越近缩放越大(避免密集)
const minDistance = nearestPoints[0].distance;
if (minDistance < 50) mapScale = this.data.nearScale; // <50米:近距离(20级)
else if (minDistance < 200) mapScale = 19; // 50-200米:中等(19级)
else mapScale = this.data.farScale; // >200米:正常(18级)
// 视野范围:用户位置 + 最近1-3个标记点(至少包含用户+1个点)
targetPoints = [userPt, ...nearestPoints.map(p => ({ latitude: p.latitude, longitude: p.longitude }))];
} else {
// 极端情况:无标记点,仅显示用户位置
targetPoints = [userPt];
}
} else {
// -------------------------- AOI外逻辑(isCenter=0) --------------------------
// 地图中心:路线前两个标记点的「中心点」(若不足2个则用第一个点)
if (routePoints.length >= 2) {
const firstPt = routePoints[0];
const secondPt = routePoints[1];
// 计算两点中心点(纬度、经度分别取平均)
mapCenter = {
latitude: (firstPt.latitude + secondPt.latitude) / 2,
longitude: (firstPt.longitude + secondPt.longitude) / 2
};
// 视野范围:仅包含前两个标记点(避免显示过多导致密集)
targetPoints = [firstPt, secondPt];
} else if (routePoints.length === 1) {
// 只有1个标记点:以该点为中心
mapCenter = routePoints[0];
targetPoints = [routePoints[0]];
} else {
// 无标记点: fallback 到用户位置(避免空坐标)
mapCenter = userPt;
targetPoints = [userPt];
}
// 缩放层级:固定中等缩放(16级),避免标记点太密集
mapScale = 16;
}
/* 4. 更新地图数据并调整视野 */
this.setData({
markers: finalMarkers, // 显示所有标记点(无论AOI内外,均展示所有点)
latitude: mapCenter.latitude, // 地图中心纬度
longitude: mapCenter.longitude, // 地图中心经度
scale: mapScale, // 地图缩放层级
}, () => {
// 调整地图视野,确保目标点全部可见(padding避免边缘裁剪)
wx.createMapContext('map', this).includePoints({
padding: [80, 80, 80, 80], // 上下左右内边距,适配不同屏幕
points: targetPoints, // 需包含在视野内的点
});
});
},
// 获取两点间距离(米)
getDistanceBetweenPoints(p1, p2) {
const rad = (angle) => angle * Math.PI / 180;
const lat1 = p1?.latitude, lon1 = p1?.longitude;
const lat2 = p2?.latitude, lon2 = p2?.longitude;
const R = 6371000; // 地球半径(米)
const dLat = rad(lat2 - lat1);
const dLon = rad(lon2 - lon1);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(rad(lat1)) * Math.cos(rad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
},
// 展示导航
showNavigation(latitude, longitude, name) {
const mapCtx = wx.createMapContext('map', this);
mapCtx.openMapApp({
longitude: longitude,
latitude: latitude,
scale: 12, // 调整为12,可以看到更多周边信息
destination: name || '目的地',
success(res) {
},
fail(err) {
},
});
},
// 点击地图标记事件
markerTap(e) {
const { markerId } = e;
const { markers, markerStyle, activeMarkerId } = this.data;
const { default: defaultStyle, active: activeStyle } = markerStyle;
// 1. 遍历 markers,更新目标标记点样式,重置其他标记点
const updatedMarkers = markers.map(marker => {
// 跳过用户位置标记点(id=10001)
if (marker.id === 10001) return marker;
this.setData({ markerAudioPlay: true, borderRadius: '9rpx' })
// 目标标记点(被点击的点):切换为点击态
if (marker.id === markerId) {
this.setData({
activeMarkerId: markerId,
activeGuideName: marker.callout.content || '',
});
this.data.markerPlaying = marker;
return {
...marker,
// iconPath: activeStyle.iconPath, // 点击态图标
callout: {
...activeStyle.callout,
content: `${marker.callout.content}` // 点击态内容
},
customCallout: {
display: 'ALWAYS'
}
};
}
// 其他标记点:如果之前是点击态,重置为默认态
if (marker.id === activeMarkerId) {
delete marker.customCallout
return {
...marker,
// iconPath: defaultStyle.iconPath, // 默认图标
callout: {
...defaultStyle.callout,
content: marker.callout.content.replace('当前播放:', '') // 移除前缀,恢复原内容
}
};
}
return marker;
});
// 2. 更新页面数据:触发地图重渲染
setTimeout(() => {
this.setData({
markers: updatedMarkers,
activeMarkerId: markerId, // 记录当前点击的标记点ID
playingMarkerId: markerId // 原有逻辑:标记正在播放的音频
});
this.setData({ borderRadius: '12rpx' })
}, 10);
// 3. 原有音频播放逻辑(保留)
const marker = updatedMarkers.find(m => m.id === markerId);
if (marker && marker.customData.audioUrl) {
this.playGuideAudio(marker.customData.audioUrl, marker, false);
this.data.innerAudioContext.onPlay(() => {
const routePlan = this.data.item.privData.routePlan;
const currentIndex = routePlan.findIndex(guide => guide.name === marker.callout.content);
if (currentIndex !== -1) {
const nextGuide = routePlan[currentIndex + 1] || null;
this.setData({
activeGuideName: marker.callout.content,
activeGuideNext: nextGuide ? nextGuide.name : '暂无下一讲'
});
}
// wx.showToast({
// title: `正在播放: ${marker.callout.content.replace('当前播放:', '')}`,
// icon: 'none'
// });
});
this.data.innerAudioContext.onError((res) => {
console.error('播放失败:', res);
wx.showToast({ title: '音频播放失败', icon: 'none' });
});
this.data.innerAudioContext.onEnded(() => {
});
this.data.innerAudioContext.play();
} else if (marker) {
wx.showToast({ title: '该景点暂无讲解音频', icon: 'none' });
}
// 【新增】记录播放类型为「点击」+ 启动计时
this.setData({
lastPlayType: PLAY_TYPE.CLICK, // 标记为「用户点击」
stayTime: Date.now() // 启动计时(记录开始时间)
});
},
// 更新当前和下一个讲解信息
updateActiveGuideInfo() {
const { item, activeGuideName } = this.data;
const routePlan = item.privData.routePlan;
const currentIndex = routePlan.findIndex(guide => guide.name === activeGuideName);
if (currentIndex !== -1) {
const nextGuide = routePlan[currentIndex + 1] || null;
this.setData({
activeGuideNext: nextGuide ? nextGuide.name : '暂无下一讲'
});
}
},
//播放景点讲解
playGuideAudio(src, marker, isAutoPlay) {
// 【新增】上报「上一个讲解」的结束
const lastMarker = this.data.markers.find(m => m.id === this.data.lastPlayedId);
if (lastMarker) {
this.reportAudioPlayEvent(lastMarker, this.data.lastPlayType);
}
const { activeMarkerId, markers, markerStyle } = this.data;
const { default: defaultStyle } = markerStyle;
// 重置之前点击的标记点样式
if (activeMarkerId && activeMarkerId !== marker.id) {
const updatedMarkers = markers.map(m => {
if (m.id === activeMarkerId && m.id !== 10001) {
return {
...m,
// iconPath: defaultStyle.iconPath,
callout: {
...defaultStyle.callout,
content: m.callout.content.replace('当前播放:', '')
}
};
}
return m;
});
this.setData({ markers: updatedMarkers });
}
// 原有音频播放逻辑(保留)
const markerId = marker.id;
if (this.data.bgAudioContext) this.data.bgAudioContext.volume = 0.2;
this.setData({
playingMarkerId: markerId,
stayTime: 0,
lastPlayType: isAutoPlay ? PLAY_TYPE.AUTO : PLAY_TYPE.CLICK
});
// 4. 创建新音频实例
if (this.data.innerAudioContext) this.data.innerAudioContext.destroy();
this.data.innerAudioContext = wx.createInnerAudioContext();
this.data.innerAudioContext.src = src;
this.data.innerAudioContext.markerData = marker; // ← 关键
// 6. 设置播放结束回调
this.data.innerAudioContext.onEnded(() => {
this.handleAudioEnd();
});
// 7. 开始播放
this.data.innerAudioContext.play();
this.setData({ lastPlayedId: markerId });
},
// 处理选择事件
handleSelected(e) {
const { label, latitude, longitude, attractionId } = e.detail.selected;
this.showNavigation(
Number(latitude),
Number(longitude),
label || '目的地'
);
},
// 点击地图事件
handleMapTap(e) {
this.hideOverlay();
},
//10米内自动讲解判断
checkNearbyAudio(pt) {
const { markers, lastAutoPlayMarkerId, lastAutoPlayTime, autoPlayInterval } = this.data;
if (!markers.length) return;
let minDistance = Number.MAX_VALUE;
let minMarker = null;
// 遍历所有标记点,找到距离用户最近的标记点
markers.forEach(marker => {
// 跳过用户自身标记点(id=10001)和无音频的标记点
if (marker.id === 10001 || !marker.customData?.audioUrl) return;
const dist = this.getDistanceBetweenPoints(
{ latitude: pt.latitude, longitude: pt.longitude },
{ latitude: marker.latitude, longitude: marker.longitude }
);
if (dist < minDistance) {
minDistance = dist;
minMarker = marker;
}
});
if (!minMarker) {
return;
}
// 最小距离≤10米 + 新标记点与上次自动播放的标记点id不一致
if (minDistance <= this.data.distPlay && lastAutoPlayMarkerId === null) {//初次讲解点讲解
this.handleAutoPlayMarker(minMarker); // 统一处理自动播放的标记点样式和音频
// 更新上次自动播放的标记点 ID 和时间
this.setData({
lastAutoPlayMarkerId: minMarker.id,
lastAutoPlayTime: Date.now()
});
}
else if (minDistance <= this.data.distPlay && minMarker.id !== lastAutoPlayMarkerId) {//当前有讲解点
this.handleAutoPlayMarker(minMarker); // 统一处理自动播放的标记点样式和音频
// 更新上次自动播放的标记点 ID 和时间
this.setData({
lastAutoPlayMarkerId: minMarker.id,
lastAutoPlayTime: Date.now()
});
}
},
/**
* 统一处理自动播放标记点的样式和音频
* @param {Object} marker - 自动播放的目标标记点
*/
handleAutoPlayMarker(marker) {
const { innerAudioContext, markers, markerStyle, activeMarkerId } = this.data;
const { default: defaultStyle, active: activeStyle } = markerStyle;
// 1. 停止当前音频(若存在)
if (innerAudioContext && !innerAudioContext.paused) {
innerAudioContext.pause();
innerAudioContext.destroy(); // 销毁旧实例,避免状态残留
}
// 2. 重置之前激活的标记点样式
const updatedMarkers = markers.map(m => {
if (m.id === 10001) return m;
if (m.id === activeMarkerId) {
delete m.customCallout;
return {
...m,
callout: {
...defaultStyle.callout,
content: m.callout.content.replace('当前播放:', '')
}
};
}
if (m.id === marker.id) {
return {
...m,
callout: {
...activeStyle.callout,
content: `${m.callout.content}`
},
customCallout: { display: 'ALWAYS', anchorX: 0, anchorY: 0 }
};
}
return m;
});
// 3. 更新标记点数据
this.setData({
markers: updatedMarkers,
activeMarkerId: marker.id,
playingMarkerId: marker.id,
lastPlayedId: marker.id,
lastPlayType: PLAY_TYPE.AUTO,
stayTime: Date.now(),
borderRadius: '12rpx'
}, () => {
// 4. 播放讲解音频(创建新实例)
this.data.innerAudioContext = wx.createInnerAudioContext();
this.data.innerAudioContext.src = marker.customData.audioUrl;
this.data.innerAudioContext.markerData = marker;
// 【新增】自动播放:绑定音频结束事件,调用统一结束逻辑
this.data.innerAudioContext.onEnded(() => {
this.handleAudioEnd(); // 关键:与手动点击结束逻辑对齐
});
// 5. 播放音频
this.data.innerAudioContext.play();
// 6. 更新讲解信息
this.setData({ activeGuideName: marker.callout.content });
const routePlan = this.data.item.privData.routePlan;
const currentIndex = routePlan.findIndex(guide => guide.name === marker.callout.content);
if (currentIndex !== -1) {
const nextGuide = routePlan[currentIndex + 1] || null;
this.setData({ activeGuideNext: nextGuide ? nextGuide.name : '暂无下一讲' });
}
});
},
// 停止当前音频并上报数据
stopAndReportAudio(playType) {
const marker = this.data.markers.find(m => m.id === this.data.lastPlayedId);
if (marker && this.data.stayTime > 0) {
this.reportAudioPlayEvent(marker, playType);
}
this.setData({ stayTime: 0 });
},
// 处理音频结束
handleAudioEnd() {
const { markers, activeMarkerId, markerStyle } = this.data;
const { default: defaultStyle } = markerStyle;
// 深度重置所有标记点状态
const resetMarkers = markers.map(marker => {
if (marker.id === activeMarkerId && marker.id !== 10001) {
return {
...marker,
callout: { ...defaultStyle.callout, content: marker.callout.content.replace('当前播放:', '') },
customCallout: undefined
};
}
return marker;
});
// 立即更新状态并清除所有播放标识
this.setData({
markers: resetMarkers,
activeMarkerId: null,
playingMarkerId: null,
lastPlayedId: null,
markerAudioPlay: false,
activeGuideName: null,
}, () => {
});
// 原有埋点逻辑
const marker = this.data.innerAudioContext?.markerData;
if (marker) {
this.reportAudioPlayEvent(marker, this.data.lastPlayType);
// this.data.innerAudioContext.markerData = null;
}
},
// 埋点-讲解播放上报(通用方法:接收景点 marker、播放类型)
reportAudioPlayEvent(marker, playType) {
if (!marker || !this.data.stayTime) return; // 无景点/无计时,不上报
const finalStayTime = (Date.now() - this.data.stayTime) / 1000;
try {
aplus_sendEvent('attractionAudioTour_play', 'OTHER', {
attractionID: marker.customData.attractionID,
attractionName: marker.customData.attractionName,
playType: playType || this.data.lastPlayType, // 优先用传入类型,否则取记录的类型
stayTime: finalStayTime
});
} catch (e) {
} finally {
this.setData({ stayTime: null }); // 上报后重置计时
}
},
// 显示遮罩视图
showOverlay() {
this.setData({
show: true,
// 可以添加一些动画相关的状态
overlayAnimating: true,
cardDescOpacity: 0 // 设置 card-desc 的透明度为 0
});
// 埋点-讲解卡展开曝光
try {
aplus_sendEXP('attractionList_expose', 'EXP', {
});
} catch (e) {/* ignore */ }
// 可以在这里添加一些动画结束后的回调
setTimeout(() => {
this.setData({ overlayAnimating: false });
}, 300); // 与CSS动画时间匹配
},
// 隐藏遮罩视图
hideOverlay() {
// 1. 先添加 .hide 类,触发收起动画
this.setData({
overlayHide: true // 标记为收起状态(用于WXML绑定类)
});
// 2. 动画时长为 0.3s(与WXSS的 transition 时长一致),动画结束后重置核心状态
setTimeout(() => {
this.setData({
show: false, // 重置显示状态(控制遮罩是否渲染)
overlayHide: false, // 重置收起状态(避免下次显示时带 .hide 类)
overlayAnimating: false, // 重置动画中状态
cardDescOpacity: 1 // 设置 card-desc 的透明度为 1
});
}, 300); // 时长必须与 WXSS transition 时长匹配
},
pauseMarkerAudio(e) {
if (e.markerId === 10001) return;
// this.pauseAudio();
if (this.data.markerAudioPlay) {
this.data.innerAudioContext.pause(); // 先暂停
this.setData({ markerAudioPlay: false, borderRadius: '11rpx' })
this.setData({ borderRadius: '12rpx' })
} else {
this.data.innerAudioContext.play(); // 先暂停
this.setData({ markerAudioPlay: true, borderRadius: '12rpx' })
this.setData({ borderRadius: '10rpx' })
}
},
// 触摸开始
onTouchStart(e) {
this.setData({
startTouchY: e.touches[0].clientY,
currentTouchY: e.touches[0].clientY,
swiping: false
});
},
// 触摸移动
onTouchMove(e) {
this.setData({
currentTouchY: e.touches[0].clientY,
deltaY: this.data.currentTouchY - this.data.startTouchY
});
if (Math.abs(this.data.deltaY) > 10) { // 如果滑动距离超过10px,标记为正在滑动
this.setData({ swiping: true });
}
},
// 触摸结束
// 触摸结束(滑动收起逻辑保持不变)
onTouchEnd(e) {
if (this.data.swiping) {
if (this.data.deltaY > 100) {
// 滑动距离超过100px,调用带动画的收起方法
this.hideOverlay();
} else {
// 滑动距离不足,重置位置(保持原有逻辑)
this.setData({
deltaY: 0
});
}
}
this.setData({ swiping: false });
},
// 从气泡中切换播放状态
togglePlayFromCallout(e) {
const markerId = e.currentTarget.dataset.markerId;
const marker = this.data.markers.find(m => m.id === markerId);
if (marker && marker.customData.audioUrl) {
if (this.data.playingMarkerId === markerId && this.data.isPlaying) {
// 如果正在播放当前标记点的音频,则暂停
this.pauseAudio();
} else {
// 否则播放该标记点的音频
this.playGuideAudio(marker.customData.audioUrl, marker, false);
}
}
},
// 处理第一个特殊列表项点击
handleFirstSpotAction(e) {
const { name, audio } = e.currentTarget.dataset;
if (this.data.innerAudioContext) {
this.markerTap({ markerId: 10000 });
this.data.innerAudioContext.stop();
this.data.innerAudioContext.destroy();
const routePlan = this.data.item.privData?.routePlan;
const activeGuideNext = routePlan[0]?.name;
this.setData({
activeGuideName: this.data.item.name,
activeGuideNext: activeGuideNext
});
this.data.innerAudioContext = wx.createInnerAudioContext();
this.data.innerAudioContext.src = this.data.item.privData?.audioUrl;
this.data.bgAudioContext.volume = 0.2;
this.data.innerAudioContext.play();
this.data.innerAudioContext.onEnded(() => {
this.setData({
activeGuideName: null
});
if (this.data.bgAudioContext) {
this.data.bgAudioContext.volume = 1;
}
});
}
// 记录播放类型
this.setData({
lastPlayType: PLAY_TYPE.CLICK,
stayTime: Date.now()
});
},
// 修改原有的handleSpotActionTap方法
handleSpotActionTap(e) {
const spotName = e.currentTarget.dataset.name;
// 查找对应标记点(排除第一个特殊项)
const targetMarker = this.data.markers.find(marker => {
return marker.callout?.content === spotName && marker.id !== 10001;
});
if (targetMarker) {
this.markerTap({ markerId: targetMarker.id });
this.setData({
lastPlayType: PLAY_TYPE.CLICK,
stayTime: Date.now()
});
}
}
}
})
WXML的代码
<view class="card-wrapper" style="opacity: {{cardOpacity}};">
<!-- 用 scroll-view 包裹 map,设置横向滚动并禁用滚动条 -->
<view class="card-container">
<scroll-view class="map-scroll" scroll-x show-scrollbar="{{false}}" enhanced bindscroll="onMapScroll">
<!-- 地图宽度设为 100%,确保 scroll-view 不会产生实际滚动,仅用于拦截事件 -->
<map id="map" class="map" style="width: 100%;" enable-poi="{{false}}" controls="{{controls}}" longitude="{{longitude}}" latitude="{{latitude}}" polyline="{{polyline}}" scale="{{scale}}" include-points="{{includePoints}}" setting="{{setting}}" markers="{{markers}}" layer-style="1" subkey="6HDBZ-UGYWT-5SRX7-LHSXH-6CKN5-ZPFWG" bindtap="handleMapTap" bindmarkertap="markerTap" bindregionchange="onRegionChange" bindcallouttap="pauseMarkerAudio" enable-overlooking="{{setting.enableOverlooking}}" enable-rotate="{{setting.enableRotation}}" enable-custom-callout="true">
<!-- 地图上的覆盖物(保持不变) -->
<cover-view slot="callout" wx:if="{{!chatMapAvatorHidden}}">
<cover-view class="customCallout" marker-id="10001" wx:if="{{isLogin}}">
<!-- <cover-view class="userCorner" style="width: 80rpx; height: 80rpx; border-radius: 50%; overflow: hidden; background: white;">
<cover-image class="userAvator" src="{{userInfo.avatarUrl}}" mode="aspectFill" style="left: 6rpx; top: 6rpx; width: 70rpx; height: 70rpx; border-radius: 50%; position: absolute;"></cover-image> -->
<cover-view class="userCorner">
<cover-image class="userAvator" src="{{userInfo.avatarUrl}}" mode="aspectFill"></cover-image>
</cover-view>
<!--
<cover-image class="userdirection" src="https://images.biliq.com/images/miniprogram/paopao2.0/paopaoCard/direction.png" mode="aspectFill" style="left: 6rpx; top: 6rpx; width: 48rpx; height: 52rpx; position: absolute;"></cover-image> -->
</cover-view>
<cover-view marker-id="{{activeMarkerId}}">
<cover-view class="markerItem-style">
{{activeGuideName}}
<cover-view class="music-animation-container" style="scale: 0.9; margin-left: 10rpx;" wx:if="{{markerAudioPlay}}">
<cover-view class="music-bar bar1"></cover-view>
<cover-view class="music-bar bar2" style="margin: 0 4rpx;"></cover-view>
<cover-view class="music-bar bar3"></cover-view>
</cover-view>
<!-- <cover-image style="height: 30rpx; width: 30rpx; margin-right: 10rpx;" wx:if="{{markerAudioPlay}}" src="https://images.biliq.com/images/miniprogram/paopao2.0/paopaoCard/talk-attraction.gif"></cover-image> -->
<cover-image style="height: 30rpx; width: 30rpx;" wx:if="{{!markerAudioPlay && isIOS}}" src="https://images.biliq.com/images/miniprogram/paopao2.0/paopaoCard/jjyellowplay.png">
</cover-image>
<cover-view wx:if="{{!markerAudioPlay && !isIOS}}">
<cover-image style="height: 30rpx; width: 30rpx; margin-right: 25rpx; margin-bottom: 10rpx;" style="border-radius:{{borderRadius}}" src="https://images.biliq.com/images/miniprogram/paopao2.0/paopaoCard/jjyellowplay.png">
</cover-image>
<cover-view> </cover-view>
</cover-view>
</cover-view>
<cover-view class="triangle"></cover-view>
</cover-view>
</cover-view>
</map>
</scroll-view>
</view>
<!-- 以下内容保持不变 -->
<view class="card-desc" wx:if="{{bigSwiperShow}}" style="opacity: {{cardDescOpacity}};" bindtap="showOverlay">
<view class="top-descline" />
<image class="card-logo" src="{{item.privData.guideMapUrl}}"></image>
<text class="card-reading">{{activeGuideName === null ? '' : '正在讲解'}}</text>
<text class="card-readingcurrent">{{activeGuideName === null ? '' : activeGuideName}}</text>
<text class="card-readingnext">
{{activeGuideNext === null ? '' : '下一讲 ' + activeGuideNext}}
</text>
</view>
<view class="overlay {{overlayHide ? 'hide' : ''}}" wx:if="{{show}}" bindtouchstart="onTouchStart" bindtouchmove="onTouchMove" bindtouchend="onTouchEnd" catchtouchmove="preventTouchMove">
<view class="top-line" />
<scroll-view class="card-spotlist" scroll-y show-scrollbar="{{false}}">
<!-- 第一个特殊列表项 -->
<view class="card-spot">
<view class="spot-actionnow" wx:if="{{activeGuideName === item.name}}">
<view class="title-and-animation">
<text class="spot-titlenow">{{item.name || ''}}</text>
<view class="music-animation-container">
<view class="music-bar bar1"></view>
<view class="music-bar bar2"></view>
<view class="music-bar bar3"></view>
</view>
</view>
</view>
<view class="spot-action" wx:else bindtap="handleFirstSpotAction" data-name="{{item.name}}" data-audio="{{item.privData.audioUrl}}">
<text class="spot-title">{{item.name}}</text>
</view>
</view>
<!-- 原有景点列表内容保持不变 -->
<block wx:for="{{item.privData.routePlan}}" wx:key="index">
<view class="card-spot">
<view class="spot-actionnow" wx:if="{{activeGuideName === item.name}}">
<view class="title-and-animation">
<text class="spot-titlenow">{{item.name || ''}}</text>
<view class="music-animation-container">
<view class="music-bar bar1"></view>
<view class="music-bar bar2"></view>
<view class="music-bar bar3"></view>
</view>
</view>
</view>
<view class="spot-action" wx:else bindtap="handleSpotActionTap" data-name="{{item.name}}">
<text class="spot-title">{{item.name || ''}}</text>
</view>
</view>
</block>
</scroll-view>
</view>
<view class="page-number" wx:if="{{isCurrentCard && from==='card'}}">{{cardIndex + 1}}</view>
</view>
WXSS的代码
.card-wrapper {
width: 100%;
height: 100%;
/* display: flex;
justify-content: center;
align-items: center; */
}
.card-container {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 50rpx;
overflow: hidden;
}
/* 地图滚动容器:填充父容器,不显示滚动条 */
.map-scroll {
width: 100%;
height: 100%;
white-space: nowrap;
/* 确保内部元素不换行 */
overflow: hidden;
/* 隐藏可能的滚动溢出 */
}
/* 地图组件:宽度100%,与scroll-view匹配,无实际滚动空间 */
.map {
height: 100%;
display: inline-block;
/* 配合scroll-x横向布局 */
}
.userCorner{
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
background: white;
}
.userAvator{
position: absolute;
left: 6rpx;
top: 6rpx;
width: 70rpx;
height: 70rpx;
border-radius: 50%;
}
/* .map {
width: 100%;
height: 100%;
} */
/* 自定义气泡 */
.custom-callout {
position: absolute;
background: transparent;
pointer-events: auto;
/* 允许点击 */
}
.callout-content {
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 8px 12px;
border-radius: 6px;
}
.callout-text {
font-size: 14px;
margin-right: 8px;
}
.callout-play-icon,
.callout-pause-icon {
width: 20px;
height: 20px;
}
.callout-arrow {
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid rgba(0, 0, 0, 0.8);
/* 与背景色一致 */
}
.card-desc {
position: absolute;
display: flex;
left: 38rpx;
/* 底部超出父容器 43rpx */
bottom: -43rpx;
width: 484rpx;
height: 182rpx;
background: rgba(29, 52, 59, 0.6);
border-radius: 50rpx;
backdrop-filter: blur(32px);
z-index: 999;
transition: opacity 0.3s ease; /* 添加透明度过渡效果 */
}
.card-logo {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 14rpx;
width: 154rpx;
height: 154rpx;
border-radius: 30rpx;
}
.card-name {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 24rpx;
max-width: 130rpx;
max-height: 130rpx;
font-family: SourceHanSansCN;
/* font-weight: 800; */
font-size: 32rpx;
color: #FFFFFF;
line-height: 37rpx;
letter-spacing: 1px;
text-overflow: ellipsis;
}
.card-reading {
position: absolute;
top: 35rpx;
right: 30rpx;
width: 80rpx;
height: 29rpx;
font-family: 'SourceHanSansCN';
/* font-weight: bold; */
font-size: 20rpx;
color: #E0ED42;
line-height: 29rpx;
}
/* 针对Android设备 */
@supports not (-webkit-overflow-scrolling: touch) {
.card-reading {
font-weight: bold;
}
}
.card-readingcurrent {
position: absolute;
top: 66rpx;
right: 30rpx;
max-width: 270rpx;
font-family: 'SourceHanSansCN';
/* font-weight: bold; */
font-size: 38rpx;
color: #E0ED42;
line-height: 56rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 针对Android设备 */
@supports not (-webkit-overflow-scrolling: touch) {
.card-readingcurrent {
font-weight: bold;
}
}
.card-readingnext {
position: absolute;
bottom: 19rpx;
right: 30rpx;
max-width: 270rpx;
font-family: 'SourceHanSansCN';
/* font-weight: bold; */
font-size: 20rpx;
color: #FFFFFF;
line-height: 29rpx;
}
/* 针对Android设备 */
@supports not (-webkit-overflow-scrolling: touch) {
.card-readingnext {
font-weight: bold;
}
}
/* 蒙版容器 */
.overlay {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
bottom: -43rpx; /* 保持原有定位 */
left: 0;
right: 0;
height: 813rpx;
background: rgba(29, 52, 59, 0.6);
border-radius: 50rpx;
backdrop-filter: blur(32px);
z-index: 999;
/* 关键:动画过渡配置(时长0.3s,缓动曲线) */
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
opacity 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
/* 默认显示状态:位置正常,不透明 */
transform: translateY(0);
opacity: 1;
}
/* 新增:收起状态(通过 .hide 类控制) */
.overlay.hide {
/* 向上收起:Y轴偏移100%(完全移出可视区域) */
transform: translateY(100%);
/* 同时淡入透明 */
opacity: 0;
/* 收起后禁用点击(避免动画期间误触) */
pointer-events: none;
}
/* 顶部拖动条 */
.top-descline {
position: absolute;
top: 14rpx;
left: 50%;
transform: translateX(-50%);
width: 46rpx;
height: 6rpx;
border-radius: 3rpx;
background: #112029;
}
.top-line {
position: absolute;
align-items: center;
justify-content: center;
top: 16rpx;
width: 46rpx;
height: 6rpx;
border-radius: 3rpx;
background: #112029;
}
.card-spotlist {
position: absolute;
top: 30rpx;
bottom: 0;
width: 100%;
}
.card-spot {
display: flex;
align-items: center;
width: 100%;
height: 76rpx;
position: relative;
}
.spot-action {
width: 100%;
height: 100%;
}
.spot-title {
position: absolute;
bottom: 0;
left: 90rpx;
font-family: 'SourceHanSansCN';
/* font-weight: bold; */
font-size: 36rpx;
color: #FFFFFF;
opacity: 0.5;
line-height: 54rpx;
max-width: 400rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 针对Android设备 */
@supports not (-webkit-overflow-scrolling: touch) {
.spot-title {
font-weight: bold;
}
}
.spot-actionnow {
width: 100%;
height: 100%;
}
.title-and-animation {
display: flex;
margin-top: 14rpx;
align-items: center;
gap: 15rpx;
}
.spot-titlenow {
bottom: 0;
margin-left: 90rpx;
font-family: 'SourceHanSansCN';
/* font-weight: bold; */
font-size: 43rpx;
color: #E0ED42;
line-height: 64rpx;
max-width: 400rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 针对Android设备 */
@supports not (-webkit-overflow-scrolling: touch) {
.spot-titlenow {
font-weight: bold;
}
}
.music-animation-container {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4rpx;
/* 竖线之间的间距 */
width: 32rpx;
height: 35rpx;
}
/* 单条竖线的基础样式 */
.music-bar {
width: 8rpx;
border-radius: 4rpx;
background-color: #E0ED42;
}
/* 第一条竖线的动画:高度不规则变化 */
.bar1 {
animation: barBeat1 0.8s infinite ease-in-out;
}
@keyframes barBeat1 {
0% {
height: 12rpx;
}
25% {
height: 28rpx;
}
50% {
height: 20rpx;
}
75% {
height: 32rpx;
}
100% {
height: 12rpx;
}
}
/* 第二条竖线的动画:高度不规则变化(与第一条有差异) */
.bar2 {
animation: barBeat2 0.8s infinite ease-in-out;
}
@keyframes barBeat2 {
0% {
height: 20rpx;
}
25% {
height: 30rpx;
}
50% {
height: 15rpx;
}
75% {
height: 25rpx;
}
100% {
height: 20rpx;
}
}
/* 第三条竖线的动画:高度不规则变化(与前两条有差异) */
.bar3 {
animation: barBeat3 0.8s infinite ease-in-out;
}
@keyframes barBeat3 {
0% {
height: 15rpx;
}
25% {
height: 22rpx;
}
50% {
height: 30rpx;
}
75% {
height: 18rpx;
}
100% {
height: 15rpx;
}
}
/* 卡片页码 */
.page-number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 250rpx;
color: #FFFFFF;
font-weight: 600;
/* font-family: 'Chalet'; */
background: none;
border: none;
box-shadow: none;
z-index: 2;
text-align: center;
width: fit-content;
white-space: nowrap;
animation: fadeOut 1s ease-in-out forwards;
pointer-events: none;
}
@keyframes fadeOut {
0% {
opacity: 1;
}
80% {
opacity: 0.6;
}
100% {
opacity: 0;
}
}
/* 自定义气泡样式 */
.custom-callout {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: auto;
z-index: 9999;
}
.callout-content {
display: flex;
align-items: center;
background: rgba(17, 32, 41, 0.95);
color: #fff;
padding: 16rpx 24rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.3);
min-width: 240rpx;
max-width: 400rpx;
}
.callout-text {
font-size: 28rpx;
font-weight: bold;
margin-right: 16rpx;
color: #E0ED42;
flex: 1;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.callout-play-icon,
.callout-pause-icon {
width: 36rpx;
height: 36rpx;
}
.callout-arrow {
width: 0;
height: 0;
border-left: 12rpx solid transparent;
border-right: 12rpx solid transparent;
border-top: 12rpx solid rgba(17, 32, 41, 0.95);
margin-top: -2rpx;
}
.markerItem-style {
padding: 10rpx 20rpx;
border-radius: 20rpx;
background-color: #282C34;
color: #FFFFFF;
position: relative;
margin-bottom: 10rpx;
display: flex;
align-items: center;
font-family: 'SourceHanSansCN';
font-size: 28rpx;
color: #E0ED42;
line-height: 36rpx;
text-align: justify;
font-style: normal;
box-sizing: border-box;
}
/* 针对Android设备 */
@supports not (-webkit-overflow-scrolling: touch) {
.markerItem-style {
font-weight: bold;
}
}
/* .triangle{
position: absolute;
width: 0;
height: 0;
border-width:15rpx;
border-style:solid;
border-top-color: #282C34;
border-bottom-color: transparent;
border-left-color: transparent;
border-right-color: transparent;
left: 50%;
bottom: -19rpx;
margin-left: -20rpx;
content:''
} */