当前位置: 首页 > news >正文

小程序地图以及讲解的使用

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:''

} */


文章转载自:

http://vjCDRNlM.rmLtt.cn
http://jxSz4wDi.rmLtt.cn
http://JjkSfzBp.rmLtt.cn
http://I85WizC9.rmLtt.cn
http://V3o6vU7u.rmLtt.cn
http://gV5MQWyT.rmLtt.cn
http://S7QjxKyx.rmLtt.cn
http://ZQI5mzf8.rmLtt.cn
http://PnRELeWV.rmLtt.cn
http://nrmIOcMd.rmLtt.cn
http://yKqtL225.rmLtt.cn
http://YVgzUOLw.rmLtt.cn
http://2HRmNM2v.rmLtt.cn
http://FRDZGZuj.rmLtt.cn
http://LfmfHj9M.rmLtt.cn
http://7iys0Kwc.rmLtt.cn
http://wQPiEU5f.rmLtt.cn
http://KhasKMIB.rmLtt.cn
http://RK1tdKqm.rmLtt.cn
http://HYJXDvqj.rmLtt.cn
http://j8DOP1v9.rmLtt.cn
http://H1Shf35f.rmLtt.cn
http://twZPYyAP.rmLtt.cn
http://bixUMBHp.rmLtt.cn
http://X7I9si2S.rmLtt.cn
http://elxv8F5r.rmLtt.cn
http://ea2tqxQ3.rmLtt.cn
http://sGeot4GR.rmLtt.cn
http://D016wF7q.rmLtt.cn
http://bzJ7iVWk.rmLtt.cn
http://www.dtcms.com/a/387363.html

相关文章:

  • 单分类线性逻辑回归
  • 使用POSTMAN 创建泛微OA流程
  • vscode中配置pytest
  • 液氮低温恒温器的应用领域
  • [Yolo遇到的问题] 使用VScode进行ultralytics训练 启动后在scanning阶段意外中断 导致训练无法正常启动
  • 微算法科技(NASDAQ:MLGO)研究分布式量子计算,释放量子计算潜能
  • 使用EasyExcel读不到数据的低级问题
  • 万象EXCEL开发(一)表头标尺搭建—东方仙盟筑基期
  • Redis 发展趋势与 Redis 7.x 新特性:从缓存到实时数据平台的演进
  • 微信小程序 tabBar 切换实现
  • 微信小程序的跳转方式
  • 微信小程序---暮之沧蓝音乐小程序
  • springboot jar包部署到服务器上后,logback按日期归档不正确,今天的日志归档到昨天了,日志中的时间也不正确
  • Spring Boot Logback 日志配置详解:从基础到分布式追踪
  • 辉视养老方案:重塑老年生活的温馨与安心
  • 通过商业智能(BI)可视化数据分析了解布洛芬的产销情况
  • 健康大数据专业能转行做医疗数据分析吗?
  • antiword为什么在ubuntu22.04上面不乱码,而在mac上出现乱码
  • Paperless-ngx v2.18.4在Ubuntu 24.04上的完整离线安装步骤(非Docker)
  • Ubuntu 18.04 搭建 Kubernetes 1.27.4 集群全流程(附问题排查)
  • Ubuntu 18.04 LTS 安装 6.10.10 内核
  • Windows 11 下使用 WSL2 安装 Ubuntu 22.04 步骤
  • 在 WSL 中通过 Bash 函数快速转换 Windows 路径为 Ansible/WSL 路径
  • 【ubuntu24.04】 nvidia-smi监控GPU 利用率
  • 《嵌入式硬件(十四):基于IMX6ULL的通用目的定时器(GPT)操作》
  • 鸿蒙Next Web调试与维测全攻略:从DevTools到专项测试
  • 基于运行设计域(ODD)的安全论证方法
  • 鸿蒙HarmonyOS界面开发-组件动态创建(一)
  • 网络安全风险评估中元模型构建与实例应用
  • 鸿蒙5.0应用开发——V2装饰器@ObservedV2和@Trace的使用