支付宝兼容

This commit is contained in:
2026-03-09 09:07:58 +08:00
parent 069677957e
commit b3836b8bf2
31 changed files with 2382 additions and 307 deletions
+73 -23
View File
@@ -3,40 +3,88 @@
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map id="map" class="native-map" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude"
:markers="mapMarkers" :scale="mapZoom" :show-location="false" @regionchange="onMapRegionChange"
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated" @error="onMapError">
<!-- 覆盖在地图上的广告轮播使用 cover-view 以兼容小程序原生组件层级 -->
<cover-view class="index-swiper" v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage">
<cover-image :src="currentBannerImage" class="index-swiper-img" mode="aspectFill" @tap="handleBannerTap"></cover-image>
<!-- 轮播指示器 -->
<cover-view class="banner-indicators" v-if="props.bannerImages.length > 1">
<cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot"
:class="{ active: idx === currentBannerIndex }">
<map
id="map"
class="native-map"
:longitude="mapCenter.longitude"
:latitude="mapCenter.latitude"
:markers="mapMarkers"
:scale="mapZoom"
:show-location="false"
@regionchange="onMapRegionChange"
@markertap="onMapMarkerTap"
@callouttap="onCalloutTap"
@updated="onMapUpdated"
@error="onMapError"
>
<!-- 覆盖在地图上的广告轮播使用 cover-view 以兼容小程序原生组件层级 -->
<cover-view
class="index-swiper"
v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"
>
<cover-image
:src="currentBannerImage"
class="index-swiper-img"
mode="aspectFill"
@tap="handleBannerTap"
></cover-image>
<!-- 轮播指示器 -->
<cover-view
class="banner-indicators"
v-if="props.bannerImages.length > 1"
>
<cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot"
:class="{ active: idx === currentBannerIndex }"
>
</cover-view>
</cover-view>
</cover-view>
</cover-view>
<!-- 地图中心固定定位图标 -->
<cover-view class="center-location-marker" v-if="!props.hideMapOverlays">
<cover-image src="/static/location-icon.png" class="center-marker-icon"></cover-image>
<cover-view
class="center-location-marker"
v-if="!props.hideMapOverlays"
>
<cover-image
src="/static/location-icon.png"
class="center-marker-icon"
></cover-image>
</cover-view>
<cover-view class="map-side-controls" v-if="!props.hideControls && !props.hideMapOverlays">
<!-- 侧边控制按钮 -->
<cover-view
class="map-side-controls"
v-if="!props.hideControls && !props.hideMapOverlays"
>
<cover-view class="side-btn guide" @tap="handleGuide">
<cover-image class="side-icon" src="/static/use_help.png" style="border-radius: 50%;"></cover-image>
<cover-image
class="side-icon"
src="/static/use_help.png"
style="border-radius: 50%;"
></cover-image>
</cover-view>
<cover-view class="side-btn locate" @tap="handleRelocate">
<cover-image class="side-icon" src="/static/location.png"></cover-image>
<cover-image
class="side-icon"
src="/static/location.png"
></cover-image>
</cover-view>
<cover-view class="side-btn search" @tap="handleSearch">
<cover-image class="side-icon" src="/static/other_device.png"></cover-image>
<cover-image
class="side-icon"
src="/static/other_device.png"
></cover-image>
</cover-view>
<cover-view class="side-btn service" @tap="handleService">
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
<cover-image
class="side-icon"
src="/static/customer-service.png"
></cover-image>
</cover-view>
</cover-view>
</map>
@@ -508,15 +556,16 @@ const handleSearch = () => {
overflow: hidden;
display: flex;
flex-direction: column;
// #ifdef H5
height:78vh;
height: 78vh;
// #endif
// #ifdef MP-WEIXIN
height: 72vh;
// #endif
&.full-width {
width: 100%;
margin: 0;
@@ -682,6 +731,7 @@ const handleSearch = () => {
justify-content: center;
gap: 8rpx;
z-index: 2;
pointer-events: none;
.indicator-dot {
width: 12rpx;
+687
View File
@@ -0,0 +1,687 @@
<template>
<view class="map-container" :class="{ 'full-width': props.fullWidth }">
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map
id="map"
class="native-map"
:longitude="mapCenter.longitude"
:latitude="mapCenter.latitude"
:markers="mapMarkers"
:scale="mapZoom"
:show-location="false"
@regionchange="onMapRegionChange"
@markertap="onMapMarkerTap"
@callouttap="onCalloutTap"
@updated="onMapUpdated"
@error="onMapError"
></map>
<!-- 支付宝小程序所有 cover-image cover-view 必须放在 map 标签外部 -->
<!-- 广告轮播直接使用 cover-image不能嵌套在 cover-view -->
<cover-image
v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"
:src="currentBannerImage"
class="index-swiper-img-alipay"
mode="aspectFill"
@tap="handleBannerTap"
></cover-image>
<!-- 轮播指示器每个点都是独立的 cover-view避免嵌套 -->
<template v-if="!props.hideControls && !props.hideMapOverlays && props.bannerImages.length > 1">
<cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot-alipay"
:class="{ active: idx === currentBannerIndex }"
:style="{
left: `calc(50% + ${(idx - (props.bannerImages.length - 1) / 2) * 20}rpx)`,
transform: 'translateX(-50%)'
}"
>
</cover-view>
</template>
<!-- 地图中心固定定位图标使用 cover-view 嵌套 cover-image -->
<cover-view
class="center-location-marker-alipay"
v-if="!props.hideMapOverlays"
>
<cover-image
src="/static/location-icon.png"
class="center-marker-icon-alipay"
></cover-image>
</cover-view>
<!-- 侧边控制按钮使用 cover-view 嵌套 cover-image -->
<cover-view
class="map-side-controls-alipay"
v-if="!props.hideControls && !props.hideMapOverlays"
>
<cover-view class="side-btn-alipay guide-alipay" @tap="handleGuide">
<cover-image
class="side-icon-alipay"
src="/static/use_help.png"
style="border-radius: 50%;"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay locate-alipay" @tap="handleRelocate">
<cover-image
class="side-icon-alipay"
src="/static/location.png"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay search-alipay" @tap="handleSearch">
<cover-image
class="side-icon-alipay"
src="/static/other_device.png"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay service-alipay" @tap="handleService">
<cover-image
class="side-icon-alipay"
src="/static/customer-service.png"
></cover-image>
</cover-view>
</cover-view>
<!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>{{ $t('common.loadingMap') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
getCurrentInstance
} from 'vue'
// 导入地图工具函数
import {
calculateDistanceSync
} from '../utils/mapUtils.js'
// 导入国际化
import { useI18n } from '../utils/i18n.js'
// 获取 i18n 实例
const { t } = useI18n()
// 引用折叠面板组件的ref
const collapseRef = ref(null)
// Props
const props = defineProps({
userLocation: {
type: Object,
default: null
},
positionList: {
type: Array,
default: () => []
},
filteredPositions: {
type: Array,
default: () => []
},
searchKeyword: {
type: String,
default: ''
},
noticeText: {
type: String,
default: ''
},
enableMarkers: {
type: Boolean,
default: false
},
customHeight: {
type: String,
default: '' // 自定义高度,如 '48vh', '400rpx' 等
},
hideControls: {
type: Boolean,
default: false // 是否隐藏侧边控制按钮
},
fullWidth: {
type: Boolean,
default: false // 是否全宽显示(去掉 margin 和固定宽度)
},
hideMapOverlays: {
type: Boolean,
default: false // 是否隐藏地图上的覆盖层元素(如中心定位图标、轮播图等)
},
bannerImages: {
type: Array,
default: () => [] // 广告图片列表
}
})
// Emits
const emit = defineEmits([
'relocate',
'scan',
'showList',
'markerTap',
'mapCenterChange',
'bannerClick',
'guide'
])
// 响应式数据
const isLoading = ref(true)
const mapCenter = ref({
longitude: 116.397128,
latitude: 39.916527
})
const mapZoom = ref(17)
const mapMarkers = ref([]) // 用于地图组件的markers
const mapContext = ref(null) // 地图上下文
const currentBannerIndex = ref(0) // 当前显示的广告索引
let bannerTimer = null // 广告轮播定时器
// 计算当前显示的广告图片
const currentBannerImage = computed(() => {
if (props.bannerImages && props.bannerImages.length > 0) {
return props.bannerImages[currentBannerIndex.value]
}
// 降级:如果没有广告,显示默认图片
return '/static/index_swiper.png'
})
// 验证坐标有效性
const isValidCoordinate = (lat, lng) => {
const latitude = parseFloat(lat)
const longitude = parseFloat(lng)
return !isNaN(latitude) && !isNaN(longitude) &&
latitude >= -90 && latitude <= 90 &&
longitude >= -180 && longitude <= 180 &&
!(latitude === 0 && longitude === 0) // 排除 0,0 这种无效坐标
}
// 防抖定时器
let regionChangeTimer = null
// 方法
const updateMapMarkers = () => {
const markers = []
// 只添加周边场地位置点,中心定位图标改用固定的cover-view显示
if (props.enableMarkers && props.filteredPositions && props.filteredPositions.length > 0) {
props.filteredPositions.forEach((pos, index) => {
if (pos.longitude && pos.latitude && isValidCoordinate(pos.latitude, pos.longitude)) {
const lat = parseFloat(pos.latitude)
const lng = parseFloat(pos.longitude)
markers.push({
id: index + 1,
latitude: lat,
longitude: lng,
iconPath: '/static/markes_fdz.png',
width: 40,
height: 40,
callout: {
content: pos.name,
fontSize: 12,
borderRadius: 8,
bgColor: '#ffffff',
padding: 8,
display: 'BYCLICK'
}
})
}
})
}
mapMarkers.value = markers
isLoading.value = false
}
// 移动地图到指定位置(直接更新地图中心坐标)
const moveToLocation = (location) => {
if (!location || !location.longitude || !location.latitude) {
return
}
const newCenter = {
longitude: Number(location.longitude),
latitude: Number(location.latitude)
}
// 直接更新地图中心,触发地图组件的视图更新
mapCenter.value = newCenter
// 更新标记点
updateMapMarkers()
}
// 监听用户位置变化
watch(() => props.userLocation, (newLocation, oldLocation) => {
if (newLocation && newLocation.longitude && newLocation.latitude) {
// 检查位置是否真的变化了(避免重复更新)
const isChanged = !oldLocation ||
oldLocation.longitude !== newLocation.longitude ||
oldLocation.latitude !== newLocation.latitude
if (isChanged) {
mapCenter.value = {
longitude: newLocation.longitude,
latitude: newLocation.latitude
}
updateMapMarkers()
}
}
}, {
immediate: true,
deep: true
})
// 监听位置列表变化
watch(() => props.filteredPositions, (newPositions) => {
updateMapMarkers()
}, {
deep: true
})
// 监听广告图片变化,启动或停止轮播
watch(() => props.bannerImages, (newImages, oldImages) => {
// 先停止旧的轮播
stopBannerRotation()
currentBannerIndex.value = 0
// 如果有多张图片,启动新的轮播
if (newImages && newImages.length > 1) {
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
startBannerRotation()
})
}
}, {
immediate: true,
deep: true
})
// 地图加载完成事件
const onMapUpdated = () => {
isLoading.value = false
}
// 地图区域变化事件(带防抖优化)
const onMapRegionChange = (e) => {
// 只处理结束事件
if (!e || (e.type !== 'end' && e.type !== 'regionchange')) {
return
}
const causedBy = e.causedBy || e.detail?.causedBy
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag' || causedBy === 'update') {
// 清除之前的定时器
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
}
// 直接从事件对象中获取最新的中心点位置
const centerLocation = e.detail?.centerLocation || e.centerLocation
if (centerLocation && centerLocation.longitude && centerLocation.latitude) {
// 防抖:500ms后执行查询
regionChangeTimer = setTimeout(() => {
const newCenter = {
longitude: Number(centerLocation.longitude),
latitude: Number(centerLocation.latitude)
}
mapCenter.value = newCenter;
// 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter)
}, 500)
} else {
// 兜底方案:如果事件中没有centerLocation,才使用API获取
regionChangeTimer = setTimeout(() => {
if (mapContext.value) {
mapContext.value.getCenterLocation({
success: (res) => {
if (res && res.longitude && res.latitude) {
const newCenter = {
longitude: res.longitude,
latitude: res.latitude
}
mapCenter.value = newCenter
emit('mapCenterChange', newCenter)
}
},
fail: (err) => {
console.error('获取地图中心失败:', err)
}
})
}
}, 500)
}
}
}
// 标记点点击事件
const onMapMarkerTap = (e) => {
const markerId = e.detail?.markerId || e.markerId
// 查找对应的场地位置信息
if (props.filteredPositions && props.filteredPositions.length > 0) {
const position = props.filteredPositions[markerId - 1]
if (position) {
emit('markerTap', position)
}
}
}
// 标记点气泡点击事件
const onCalloutTap = (e) => {
const markerId = e.markerId
const marker = mapMarkers.value.find(item => item.id === markerId)
if (marker && marker.position) {
emit('markerTap', marker.position)
}
}
// 地图错误事件
const onMapError = (error) => {
console.error('地图加载失败:', error)
isLoading.value = false
}
const handleRelocate = () => {
// 直接委托父级处理定位并移动地图,避免内部重复弹 loading
try {
emit('relocate')
} catch (e) {}
}
const handleSearch = () => {
try {
uni.navigateTo({ url: '/pages/search/index' })
} catch (e) {}
}
const handleService = () => {
uni.navigateTo({
url: '/subPackages/service/help/index'
})
}
const handleGuide = () => {
emit('guide')
}
// 处理广告点击
const handleBannerTap = () => {
// 触发父组件处理点击事件
emit('bannerClick', currentBannerIndex.value)
}
// 启动广告轮播
const startBannerRotation = () => {
// 如果只有一张或没有图片,不需要轮播
if (!props.bannerImages || props.bannerImages.length <= 1) {
return
}
// 清除旧的定时器
stopBannerRotation()
// 每3秒切换一次
bannerTimer = setInterval(() => {
const nextIndex = (currentBannerIndex.value + 1) % props.bannerImages.length
currentBannerIndex.value = nextIndex
}, 3000)
}
// 停止广告轮播
const stopBannerRotation = () => {
if (bannerTimer) {
clearInterval(bannerTimer)
bannerTimer = null
}
}
// 生命周期钩子
onMounted(() => {
// 初始化地图上下文
nextTick(() => {
// 需要使用nextTick确保地图组件已经渲染
const inst = getCurrentInstance()
const vm = (inst && (inst.proxy || inst)) || undefined
try {
mapContext.value = uni.createMapContext('map', vm)
} catch (e) {
// 兼容:如果第二参不被支持,退回单参
mapContext.value = uni.createMapContext('map')
}
updateMapMarkers()
// 初始化折叠面板
if (collapseRef.value) {
collapseRef.value.init()
}
// 初始化广告轮播
if (props.bannerImages && props.bannerImages.length > 1) {
startBannerRotation()
}
})
})
onUnmounted(() => {
// 清理工作
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
regionChangeTimer = null
}
// 停止广告轮播
stopBannerRotation()
mapContext.value = null
})
// 暴露给父组件的方法
defineExpose({
mapCenter: computed(() => mapCenter.value),
moveToLocation,
updateMapMarkers,
initCollapse: () => {
if (collapseRef.value) {
collapseRef.value.init()
}
}
})
</script>
<style lang="scss" scoped>
/* 地图容器 */
.map-container {
flex: 1;
position: relative;
left: 0;
right: 0;
bottom: 0;
width: 94vw;
margin: 20rpx;
margin-bottom: 0; /* 底部不需要边距 */
border-radius: 20rpx;
overflow: hidden;
display: flex;
flex-direction: column;
height: 72vh;
&.full-width {
width: 100%;
margin: 0;
border-radius: 0;
height: 100%;
}
.map-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: visible; /* 支付宝小程序:允许覆盖层显示在地图外部 */
border-radius: 0;
.native-map {
width: 100%;
height: 100%;
display: block;
border-radius: 0;
}
}
.map-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 8rpx solid #f3f3f3;
border-top: 8rpx solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
text {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 支付宝小程序专用样式 - 所有覆盖层元素相对于 map-wrapper 定位 */
/* 广告图片样式 */
.index-swiper-img-alipay {
position: absolute;
top: 20rpx;
left: 50%;
transform: translateX(-50%);
width: 90vw;
height: 120rpx;
border-radius: 20rpx;
z-index: 100; /* 确保在地图之上 */
}
/* 轮播指示器独立定位 */
.indicator-dot-alipay {
position: absolute;
top: 130rpx; /* 广告图片 top(20rpx) + height(120rpx) - 10rpx = 130rpx */
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.5);
transition: all 0.3s ease;
pointer-events: none;
z-index: 101; /* 确保在广告图片之上 */
&.active {
width: 24rpx;
border-radius: 6rpx;
background-color: rgba(255, 255, 255, 0.9);
}
}
/* 地图中心定位图标样式 */
.center-location-marker-alipay {
position: absolute;
left: 50%;
top: 50%;
z-index: 99; /* 确保在地图之上 */
width: 60rpx;
height: 80rpx;
margin-left: -30rpx;
margin-top: -80rpx;
pointer-events: none;
background: transparent;
.center-marker-icon-alipay {
width: 60rpx;
height: 60rpx;
display: block;
}
}
/* 侧边控制按钮容器 */
.map-side-controls-alipay {
position: absolute;
right: 20rpx;
bottom: 160rpx; /* 向上移动,避免被底部按钮遮挡 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 300rpx;
margin: auto;
gap: 12rpx;
z-index: 102; /* 确保在最上层 */
background: transparent;
.side-btn-alipay {
margin: auto;
background: rgba(255, 255, 255, 0.96);
border-radius: 24rpx;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 13rpx;
border: 2rpx solid #e0e0e0;
&:active {
transform: scale(0.95);
}
.side-icon-alipay {
width: 40rpx;
height: 40rpx;
z-index:1000;
}
}
}
</style>
+6
View File
@@ -22,6 +22,12 @@
<view class="payment-badge member" v-else-if="order.payWay == 'wx_member_pay'">
<text class="badge-text">{{ $t('order.memberOrder') }}</text>
</view>
<view class="payment-badge member" v-else-if="order.payWay == 'ali_pay'">
<text class="badge-text">{{ $t('order.aliPay') }}</text>
</view>
<view class="payment-badge member" v-else-if="order.payWay == 'antom_pay'">
<text class="badge-text">{{ $t('order.antomPay') }}</text>
</view>
<view class="payment-badge deposit" v-else>
<text class="badge-text">{{ $t('order.wxPay') }}</text>
<text class="divider">|</text>