支付宝兼容

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
+71 -21
View File
@@ -3,40 +3,88 @@
<!-- 地图容器 --> <!-- 地图容器 -->
<view class="map-wrapper"> <view class="map-wrapper">
<!-- 使用小程序原生地图组件 --> <!-- 使用小程序原生地图组件 -->
<map id="map" class="native-map" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude" <map
:markers="mapMarkers" :scale="mapZoom" :show-location="false" @regionchange="onMapRegionChange" id="map"
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated" @error="onMapError"> class="native-map"
<!-- 覆盖在地图上的广告轮播使用 cover-view 以兼容小程序原生组件层级 --> :longitude="mapCenter.longitude"
<cover-view class="index-swiper" v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"> :latitude="mapCenter.latitude"
<cover-image :src="currentBannerImage" class="index-swiper-img" mode="aspectFill" @tap="handleBannerTap"></cover-image> :markers="mapMarkers"
<!-- 轮播指示器 --> :scale="mapZoom"
<cover-view class="banner-indicators" v-if="props.bannerImages.length > 1"> :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 <cover-view
v-for="(img, idx) in props.bannerImages" class="banner-indicators"
:key="idx" v-if="props.bannerImages.length > 1"
class="indicator-dot" >
:class="{ active: idx === currentBannerIndex }"> <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>
</cover-view>
<!-- 地图中心固定定位图标 --> <!-- 地图中心固定定位图标 -->
<cover-view class="center-location-marker" v-if="!props.hideMapOverlays"> <cover-view
<cover-image src="/static/location-icon.png" class="center-marker-icon"></cover-image> 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>
<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-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>
<cover-view class="side-btn locate" @tap="handleRelocate"> <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>
<cover-view class="side-btn search" @tap="handleSearch"> <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>
<cover-view class="side-btn service" @tap="handleService"> <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>
</cover-view> </cover-view>
</map> </map>
@@ -510,13 +558,14 @@ const handleSearch = () => {
flex-direction: column; flex-direction: column;
// #ifdef H5 // #ifdef H5
height:78vh; height: 78vh;
// #endif // #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
height: 72vh; height: 72vh;
// #endif // #endif
&.full-width { &.full-width {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -682,6 +731,7 @@ const handleSearch = () => {
justify-content: center; justify-content: center;
gap: 8rpx; gap: 8rpx;
z-index: 2; z-index: 2;
pointer-events: none;
.indicator-dot { .indicator-dot {
width: 12rpx; 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'"> <view class="payment-badge member" v-else-if="order.payWay == 'wx_member_pay'">
<text class="badge-text">{{ $t('order.memberOrder') }}</text> <text class="badge-text">{{ $t('order.memberOrder') }}</text>
</view> </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> <view class="payment-badge deposit" v-else>
<text class="badge-text">{{ $t('order.wxPay') }}</text> <text class="badge-text">{{ $t('order.wxPay') }}</text>
<text class="divider">|</text> <text class="divider">|</text>
+4 -2
View File
@@ -9,12 +9,14 @@ export const getCouponsByPosition = (positionId) => {
} }
// 创建优惠券支付订单 // 创建优惠券支付订单
export const createCouponPayment = (couponId) => { export const createCouponPayment = (couponId, paymentPlatform) => {
return request({ return request({
url: '/app/coupon/pay', url: '/app/coupon/pay',
method: 'post', method: 'post',
data: { data: {
couponId couponId,
// 支付平台类型:WECHAT / ALIPAY / ANTOM(不传则后端默认 WECHAT
...(paymentPlatform ? { paymentPlatform } : {})
} }
}) })
} }
+4 -2
View File
@@ -1,12 +1,14 @@
import request from '../http' import request from '../http'
// 创建会员卡支付订单 // 创建会员卡支付订单
export const createMemberCardPayment = (memberCardId) => { export const createMemberCardPayment = (memberCardId, paymentPlatform) => {
return request({ return request({
url: '/app/member/pay', url: '/app/member/pay',
method: 'post', method: 'post',
data: { data: {
memberCardId memberCardId,
// 支付平台类型:WECHAT / ALIPAY / ANTOM(不传则后端默认 WECHAT
...(paymentPlatform ? { paymentPlatform } : {})
} }
}) })
} }
+18
View File
@@ -126,6 +126,15 @@ export const createWxPayment = (orderNo) => {
}) })
} }
// 创建支付宝支付订单(租借押金 H5 支付)
// 对应文档《支付宝接口文档》:GET /app/ali-payment/create/{orderNo}
export const createAliPayment = (orderNo) => {
return request({
url: `/app/ali-payment/create/${orderNo}`,
method: 'get'
})
}
// 获取正在使用中的订单 // 获取正在使用中的订单
export const getInUseOrder = () => { export const getInUseOrder = () => {
return request({ return request({
@@ -150,6 +159,15 @@ export const getWxPaymentStatus = (orderNo) => {
}) })
} }
// 查询支付宝支付状态
// 对应文档:GET /app/ali-payment/status/{orderNo}
export const getAliPaymentStatus = (orderNo) => {
return request({
url: `/app/ali-payment/status/${orderNo}`,
method: 'get'
})
}
// ==================== Antom 支付相关接口 ==================== // ==================== Antom 支付相关接口 ====================
// 创建 Antom H5 支付订单 // 创建 Antom H5 支付订单
+4 -8
View File
@@ -33,14 +33,10 @@ export const getProductDetail = (id) => {
} }
/** /**
* 创建商品支付订单 * 创建商品支付订单(多支付平台)
* @param {Object} data - 订单数据 * 对应《商品购买多支付平台方案》:
* @param {Array} data.items - 订单项列表 [{skuId, quantity}] * paymentPlatform: WECHAT / ALIPAY / ANTOM
* @param {string} data.receiverName - 收件人姓名 * 其他字段见文档
* @param {string} data.receiverPhone - 收件人手机号
* @param {string} data.receiverAddress - 收件人详细地址
* @param {string} data.remark - 用户备注(可选)
* @returns {Promise} 微信支付参数
*/ */
export const createProductOrder = (data) => { export const createProductOrder = (data) => {
return request({ return request({
+27 -1
View File
@@ -1,7 +1,7 @@
import request from '../http' import request from '../http'
import { URL, appid } from '../url' import { URL, appid } from '../url'
// 用户登录 // 旧登录接口(兼容保留,后端将逐步废弃)
export const login = (data) => { export const login = (data) => {
return request({ return request({
url: '/app/user/login', url: '/app/user/login',
@@ -10,6 +10,22 @@ export const login = (data) => {
}) })
} }
// 统一快捷登录接口 /app/user/quickLogin
// 对应文档《快捷登录最终方案》中的 QuickLoginDto
// loginType: WECHAT / ALIPAY / SMS
// appid: 平台应用ID
// openId: 第三方 openId(微信必传)
// code: 授权码(微信手机号授权码 / 支付宝 authCode
// phonenumber: 短信登录手机号
// smsCode: 短信验证码
export const quickLogin = (data) => {
return request({
url: '/app/user/quickLogin',
method: 'post',
data
})
}
// 发送验证码 // 发送验证码
export const sendVerifyCode = (phonenumber) => { export const sendVerifyCode = (phonenumber) => {
return request({ return request({
@@ -144,3 +160,13 @@ export const getWxUserPhoneNumber = (data) => {
}) })
} }
// 获取支付宝用户手机号(复用同一后端接口,由后端按 appid / 参数结构区分平台)
// 期望后端返回:{ code:200, data:{ phoneNumber: 'xxx' } }
export const getAliUserPhoneNumber = (data) => {
return request({
url: '/app/user/alipay/getPhone',
method: 'post',
data
})
}
+1 -1
View File
@@ -6,7 +6,7 @@
// 配置项:true 表示打印日志,false 表示不打印日志 // 配置项:true 表示打印日志,false 表示不打印日志
export const CONSOLE_CONFIG = { export const CONSOLE_CONFIG = {
// 是否启用 console.log // 是否启用 console.log
enableLog: false, enableLog: true,
// 是否启用 console.warn // 是否启用 console.warn
enableWarn: false, enableWarn: false,
// 是否启用 console.error // 是否启用 console.error
+9 -2
View File
@@ -1,8 +1,15 @@
import { import {
URL, URL,
appid appid,
ZFBappid
} from './url' } from './url'
// 根据运行平台选择正确的小程序 appid(后端通常依赖该 header 做平台识别)
let platformAppid = appid
// #ifdef MP-ALIPAY
platformAppid = ZFBappid
// #endif
// 获取多语言翻译文本 // 获取多语言翻译文本
const getLoadingText = () => { const getLoadingText = () => {
try { try {
@@ -31,7 +38,7 @@ const request = (option) => {
header: { header: {
"Content-Type": option.headers && option.headers["Content-Type"] ? option.headers["Content-Type"] : (option.method && option.method.toUpperCase() === 'POST' ? 'application/json' : 'application/x-www-form-urlencoded'), "Content-Type": option.headers && option.headers["Content-Type"] ? option.headers["Content-Type"] : (option.method && option.method.toUpperCase() === 'POST' ? 'application/json' : 'application/x-www-form-urlencoded'),
...option.headers, ...option.headers,
'appid': appid, 'appid': platformAppid,
'Authorization': "Bearer " + uni.getStorageSync('token'), 'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id'), 'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_') 'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
+1 -1
View File
@@ -1,7 +1,7 @@
// export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用 // export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用
// export const URL = "https://manager.fdzpower.com/api" //正式服务器 // export const URL = "https://manager.fdzpower.com/api" //正式服务器
export const URL = "https://fansdev.gxfs123.com/api" //测试服务器 export const URL = "https://fansdev.gxfs123.com/api" //测试服务器
// export const URL = "http://192.168.5.123:8080" //本地调试 // export const URL = "http://192.168.5.64:8080" //本地调试
// export const URL = "http://127.0.0.1:8080" //本地调试 // export const URL = "http://127.0.0.1:8080" //本地调试
export const appid = "wx2165f0be356ae7a9" //微信小程序appid export const appid = "wx2165f0be356ae7a9" //微信小程序appid
+4
View File
@@ -211,6 +211,8 @@ export default {
depositFree: 'Deposit-free', depositFree: 'Deposit-free',
whitelistOrder: 'Whitelist Order', whitelistOrder: 'Whitelist Order',
memberOrder: 'Member Order', memberOrder: 'Member Order',
aliPay: 'AliPay',
antomPay: 'Antom Pay',
wxPay: 'WeChat Pay', wxPay: 'WeChat Pay',
depositPay: 'Deposit Pay', depositPay: 'Deposit Pay',
paymentInProgress: 'Payment in Progress', paymentInProgress: 'Payment in Progress',
@@ -307,6 +309,8 @@ export default {
cooperation: 'Partner', cooperation: 'Partner',
settings: 'Settings', settings: 'Settings',
userAgreement: 'Terms', userAgreement: 'Terms',
settinguserAgreement:'Terms',
settinguserprivacyPolicy:'Privacy',
privacyPolicy: 'Privacy', privacyPolicy: 'Privacy',
version: 'v', version: 'v',
logout: 'Logout', logout: 'Logout',
+4
View File
@@ -211,6 +211,8 @@ export default {
depositFree: 'Sewa Tanpa Deposit', depositFree: 'Sewa Tanpa Deposit',
whitelistOrder: 'Pesanan Whitelist', whitelistOrder: 'Pesanan Whitelist',
memberOrder: 'Pesanan Anggota', memberOrder: 'Pesanan Anggota',
aliPay: 'Alipay',
antomPay: 'Antom Pay',
wxPay: 'Pembayaran WeChat', wxPay: 'Pembayaran WeChat',
depositPay: 'Sewa dengan Deposit', depositPay: 'Sewa dengan Deposit',
paymentInProgress: 'Sedang membayar', paymentInProgress: 'Sedang membayar',
@@ -307,6 +309,8 @@ export default {
cooperation: 'Kerja Sama dan Keanggotaan', cooperation: 'Kerja Sama dan Keanggotaan',
settings: 'Pengaturan', settings: 'Pengaturan',
userAgreement: '《Perjanjian Pengguna》', userAgreement: '《Perjanjian Pengguna》',
settinguserAgreement:'Perjanjian Pengguna',
settinguserprivacyPolicy:'Kebijakan Privasi',
privacyPolicy: '《Kebijakan Privasi》', privacyPolicy: '《Kebijakan Privasi》',
version: 'v', version: 'v',
logout: 'Keluar', logout: 'Keluar',
+5 -1
View File
@@ -84,7 +84,7 @@ export default {
scanToUse: '扫码使用', scanToUse: '扫码使用',
personalCenter: '个人中心', personalCenter: '个人中心',
useGuide: '使用指南', useGuide: '使用指南',
buyDevice: '充电宝定制', buyDevice: '产品定制',
navigate: '导航', navigate: '导航',
relocate: '重新定位', relocate: '重新定位',
search: '搜索', search: '搜索',
@@ -210,6 +210,8 @@ export default {
depositFree: '免押租借', depositFree: '免押租借',
whitelistOrder: '白名单订单', whitelistOrder: '白名单订单',
memberOrder: '会员订单', memberOrder: '会员订单',
aliPay: '支付宝支付',
antomPay: '海外支付',
wxPay: '微信支付', wxPay: '微信支付',
depositPay: '押金租借', depositPay: '押金租借',
paymentInProgress: '支付中', paymentInProgress: '支付中',
@@ -306,7 +308,9 @@ export default {
cooperation: '合作加盟', cooperation: '合作加盟',
settings: '设置', settings: '设置',
userAgreement: '《用户协议》', userAgreement: '《用户协议》',
settinguserAgreement: '用户协议',
privacyPolicy: '《隐私政策》', privacyPolicy: '《隐私政策》',
settinguserprivacyPolicy: '隐私政策',
version: 'v', version: 'v',
logout: '退出登录', logout: '退出登录',
confirmLogout: '确认退出登录?', confirmLogout: '确认退出登录?',
+3 -6
View File
@@ -69,12 +69,9 @@
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ] "requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
}, },
"mp-alipay" : { "mp-alipay" : {
"component2":true, "component2" : true,
"transpile":[ "transpile" : [ "uview-ui", "vue-i18n" ],
"uview-ui", "skia" : true,
"vue-i18n"
],
"skia":true,
"usingComponents" : true, "usingComponents" : true,
"appid" : "2021006117693332", "appid" : "2021006117693332",
"unipush" : { "unipush" : {
+35 -6
View File
@@ -91,7 +91,7 @@
<!-- 底部操作区 --> <!-- 底部操作区 -->
<view class="footer"> <view class="footer">
<view class="rent-button" :class="{ 'return-button': hasActiveOrder }" <view class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent(isWechatMiniProgram ? 'wx-score-pay' : 'wx-pay')"> @click="handleRent">
<text>{{ hasActiveOrder ? $t('order.returnDevice') : getRentButtonText() }}</text> <text>{{ hasActiveOrder ? $t('order.returnDevice') : getRentButtonText() }}</text>
</view> </view>
<!-- 微信支付分标识仅在微信小程序环境显示 --> <!-- 微信支付分标识仅在微信小程序环境显示 -->
@@ -176,6 +176,8 @@
const phoneNumber = ref('') const phoneNumber = ref('')
const showPhoneAuthPopup = ref(false) const showPhoneAuthPopup = ref(false)
const isWechatMiniProgram = ref(false) const isWechatMiniProgram = ref(false)
const isAlipayMiniProgram = ref(false)
const isH5 = ref(false)
// 生命周期 onLoad 钩子 // 生命周期 onLoad 钩子
onLoad(async (options) => { onLoad(async (options) => {
@@ -193,13 +195,27 @@
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: t('device.deviceInfo') title: t('device.deviceInfo')
}) })
// 检测当前运行环境 // 检测当前运行环境:微信小程序 / 支付宝小程序 / H5
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
isWechatMiniProgram.value = true isWechatMiniProgram.value = true
isAlipayMiniProgram.value = false
isH5.value = false
// #endif
// #ifdef MP-ALIPAY
isWechatMiniProgram.value = false
isAlipayMiniProgram.value = true
isH5.value = false
// #endif // #endif
// #ifdef H5 // #ifdef H5
isWechatMiniProgram.value = false isWechatMiniProgram.value = false
isAlipayMiniProgram.value = false
isH5.value = true
// #endif // #endif
console.log('当前运行环境:', {
isWechatMiniProgram: isWechatMiniProgram.value,
isAlipayMiniProgram: isAlipayMiniProgram.value,
isH5: isH5.value
})
await checkUserPhone() await checkUserPhone()
await fetchDeviceInfo() await fetchDeviceInfo()
}) })
@@ -437,7 +453,7 @@
} }
// 处理租借操作 // 处理租借操作
const handleRent = (payWay) => { const handleRent = () => {
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
showLoginTip() showLoginTip()
return return
@@ -449,8 +465,21 @@
return return
} }
// 提交订单 // 根据运行环境选择不同的租借/支付流程
submitRentOrder(payWay) // 微信小程序:走微信支付分免押租借
if (isWechatMiniProgram.value) {
submitRentOrder('wx-score-pay')
return
}
// 支付宝小程序:走押金租借,后续在支付页内调起支付宝支付
if (isAlipayMiniProgram.value) {
submitRentOrder('wx-pay')
return
}
// H5 等其他环境:统一走押金租借,支付页内根据平台选择支付方式(Antom 等)
submitRentOrder('wx-pay')
} }
// 获取价格单位文本 // 获取价格单位文本
@@ -603,7 +632,7 @@
// 跳转到订单支付页面 // 跳转到订单支付页面
uni.redirectTo({ uni.redirectTo({
url: `/pages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}` url: `/subPackages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}`
}) })
} else if (payWay == 'wx-score-pay') { } else if (payWay == 'wx-score-pay') {
+192 -13
View File
@@ -1,14 +1,14 @@
<template> <template>
<view class="container fullscreen"> <view class="container fullscreen">
<!-- 自定义导航栏 --> <!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }"> <view class="custom-navbar" :style="navbarStyle">
<view class="navbar-content" :style="{ height: navBarHeight + 'px' }"> <view class="navbar-content" :style="{ height: navBarHeight + 'px' }">
<text class="navbar-title">{{ $t('home.title') }}</text> <text class="navbar-title">{{ $t('home.title') }}</text>
</view> </view>
</view> </view>
<!-- 顶部信息区域通知招商等 --> <!-- 顶部信息区域通知招商等 -->
<view class="top-info-section" :style="{ top: (statusBarHeight + navBarHeight) + 'px' }"> <view class="top-info-section" :style="topInfoSectionStyle">
<!-- 通知栏 --> <!-- 通知栏 -->
<view class="notice-wrapper" v-if="noticeText" @click="openNoticePopup"> <view class="notice-wrapper" v-if="noticeText" @click="openNoticePopup">
<uv-notice-bar :text="noticeText" :speed="50" :show-icon="true" color="#07c160" bg-color="#E8F8EF" <uv-notice-bar :text="noticeText" :speed="50" :show-icon="true" color="#07c160" bg-color="#E8F8EF"
@@ -17,14 +17,31 @@
</view> </view>
<!-- 内容区域 --> <!-- 内容区域 -->
<!-- #ifdef MP-ALIPAY -->
<view class="main-content" :style="{ paddingTop: (navBarHeight + noticeHeight) + 'px' }">
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<view class="main-content" :style="{ paddingTop: (statusBarHeight + navBarHeight + noticeHeight) + 'px' }"> <view class="main-content" :style="{ paddingTop: (statusBarHeight + navBarHeight + noticeHeight) + 'px' }">
<!-- #endif -->
<!-- 全屏地图组件 --> <!-- 全屏地图组件 -->
<!-- 支付宝小程序使用专用组件 -->
<!-- #ifdef MP-ALIPAY -->
<MapComponentAlipay v-if="!isLoading && userLocation" ref="mapRef" :userLocation="userLocation"
:positionList="positionList" :filteredPositions="filteredPositions" :searchKeyword="searchKeyword"
:enableMarkers="true" :bannerImages="bannerImages"
:hideMapOverlays="showGuidePopup || showNoticePopup || showActivityPopup" @relocate="handleRelocate"
@scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
@mapCenterChange="onMapCenterChange" @bannerClick="handleBannerClick" @guide="openGuidePopup" />
<!-- #endif -->
<!-- 非支付宝小程序使用通用组件 -->
<!-- #ifndef MP-ALIPAY -->
<MapComponent v-if="!isLoading && userLocation" ref="mapRef" :userLocation="userLocation" <MapComponent v-if="!isLoading && userLocation" ref="mapRef" :userLocation="userLocation"
:positionList="positionList" :filteredPositions="filteredPositions" :searchKeyword="searchKeyword" :positionList="positionList" :filteredPositions="filteredPositions" :searchKeyword="searchKeyword"
:enableMarkers="true" :bannerImages="bannerImages" :enableMarkers="true" :bannerImages="bannerImages"
:hideMapOverlays="showGuidePopup || showNoticePopup || showActivityPopup" @relocate="handleRelocate" :hideMapOverlays="showGuidePopup || showNoticePopup || showActivityPopup" @relocate="handleRelocate"
@scan="handleScan" @showList="showLocationList" @markerTap="selectPosition" @scan="handleScan" @showList="showLocationList" @markerTap="selectPosition"
@mapCenterChange="onMapCenterChange" @bannerClick="handleBannerClick" @guide="openGuidePopup" /> @mapCenterChange="onMapCenterChange" @bannerClick="handleBannerClick" @guide="openGuidePopup" />
<!-- #endif -->
<!-- 地图加载状态 --> <!-- 地图加载状态 -->
<view v-if="isLoading || !userLocation" class="map-loading-placeholder"> <view v-if="isLoading || !userLocation" class="map-loading-placeholder">
@@ -79,7 +96,7 @@
</view> </view>
</view> --> </view> -->
<!-- 手机号授权弹窗 --> <!-- 手机号校验/授权弹窗扫码前校验 -->
<view class="phone-auth-popup" v-if="showPhoneAuthPopup"> <view class="phone-auth-popup" v-if="showPhoneAuthPopup">
<view class="popup-mask" @click.stop="showPhoneAuthPopup = false"></view> <view class="popup-mask" @click.stop="showPhoneAuthPopup = false"></view>
<view class="popup-content"> <view class="popup-content">
@@ -90,9 +107,25 @@
<view class="auth-desc"> <view class="auth-desc">
<text>{{ $t('auth.authDesc') }}</text> <text>{{ $t('auth.authDesc') }}</text>
</view> </view>
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber"> <!-- 微信获取手机号 code 并上报后端绑定 -->
<!-- #ifdef MP-WEIXIN -->
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onWxGetPhoneNumber">
<text>{{ $t('auth.getPhoneNumber') }}</text> <text>{{ $t('auth.getPhoneNumber') }}</text>
</button> </button>
<!-- #endif -->
<!-- 支付宝先授权 phoneNumber再调用 my.getPhoneNumber上报后端解密并校验一致性 -->
<!-- #ifdef MP-ALIPAY -->
<button
class="auth-btn"
open-type="getAuthorize"
scope="phoneNumber"
@getAuthorize="onAliAuthorizePhoneNumber"
@error="onAliAuthorizePhoneNumberError"
>
<text>{{ $t('auth.getPhoneNumber') }}</text>
</button>
<!-- #endif -->
<view class="auth-cancel" @click="showPhoneAuthPopup = false"> <view class="auth-cancel" @click="showPhoneAuthPopup = false">
<text>{{ $t('auth.notNow') }}</text> <text>{{ $t('auth.notNow') }}</text>
</view> </view>
@@ -187,7 +220,10 @@
} from 'vue' } from 'vue'
import { import {
getQueryString, getQueryString,
wxLogin wxLogin,
getUserInfo,
getUserPhoneNumber,
getAlipayUserPhoneNumber
} from '../../util/index.js' } from '../../util/index.js'
import { import {
URL URL
@@ -219,6 +255,9 @@
// 同样需要使用相对路径引入组件 // 同样需要使用相对路径引入组件
// 注意:从 pages/index/ 目录访问 components/ 需要使用 ../../components/ 路径 // 注意:从 pages/index/ 目录访问 components/ 需要使用 ../../components/ 路径
import MapComponent from '../../components/MapComponent.vue' import MapComponent from '../../components/MapComponent.vue'
// #ifdef MP-ALIPAY
import MapComponentAlipay from '../../components/MapComponentAlipay.vue'
// #endif
import LocationListSheet from '../../components/LocationListSheet.vue' import LocationListSheet from '../../components/LocationListSheet.vue'
import { import {
useI18n useI18n
@@ -244,6 +283,8 @@
const isExpanded = ref(false) const isExpanded = ref(false)
const isLoading = ref(false) const isLoading = ref(false)
const showPhoneAuthPopup = ref(false) const showPhoneAuthPopup = ref(false)
const pendingScan = ref(false) // 是否是扫码流程触发的手机号校验
const expectedUserPhone = ref('') // 当前登录态下绑定的手机号(用于支付宝一致性校验)
const isLocationInitialized = ref(false) const isLocationInitialized = ref(false)
const showLocationPopup = ref(false) const showLocationPopup = ref(false)
const isRelocating = ref(false) // 防抖标志:是否正在重新定位 const isRelocating = ref(false) // 防抖标志:是否正在重新定位
@@ -259,6 +300,30 @@
const navBarHeight = ref(44) // 默认导航栏内容高度 const navBarHeight = ref(44) // 默认导航栏内容高度
const noticeHeight = ref(0) // 通知栏高度 const noticeHeight = ref(0) // 通知栏高度
// 导航栏样式:支付宝小程序不设置 paddingTop
const navbarStyle = computed(() => {
// #ifdef MP-ALIPAY
// 支付宝小程序:不设置 paddingTop
return { paddingTop: '0px' }
// #endif
// #ifndef MP-ALIPAY
// 非支付宝小程序:设置 statusBarHeight
return { paddingTop: statusBarHeight.value + 'px' }
// #endif
})
// 顶部信息区域样式:支付宝小程序使用 paddingTop,非支付宝小程序使用 top
const topInfoSectionStyle = computed(() => {
// #ifdef MP-ALIPAY
// 支付宝小程序:使用 paddingTop
return { top: ( navBarHeight.value) + 'px' }
// #endif
// #ifndef MP-ALIPAY
// 非支付宝小程序:使用 top
return { top: (statusBarHeight.value + navBarHeight.value) + 'px' }
// #endif
})
// 使用指南步骤 // 使用指南步骤
const guideSteps = ref([{ const guideSteps = ref([{
title: '扫码使用', title: '扫码使用',
@@ -474,11 +539,13 @@
// 距离格式化函数 // 距离格式化函数
const formatDistance = (distanceInMeters) => { const formatDistance = (distanceInMeters) => {
if (distanceInMeters < 1000) { // 支付宝小程序等环境下,可能传入 String/BigInt/异常对象,导致 toFixed 不存在
return `${Math.round(distanceInMeters)}m` let meters = distanceInMeters
} else { if (typeof meters === 'bigint') meters = Number(meters)
return `${(distanceInMeters / 1000).toFixed(1)}km` meters = Number(meters)
} if (!Number.isFinite(meters) || meters < 0) return ''
if (meters < 1000) return `${Math.round(meters)}m`
return `${(meters / 1000).toFixed(1)}km`
} }
@@ -964,7 +1031,58 @@
} }
} }
const handleScan = async () => { const normalizePhone = (p) => {
if (!p) return ''
return String(p).replace(/\s+/g, '').replace(/^\+?86/, '')
}
const isLoggedIn = () => {
const token = uni.getStorageSync('token')
return !!token
}
const precheckBeforeScan = async () => {
if (!isLoggedIn()) {
redirectToLogin()
return false
}
try {
const userInfoRes = await getUserInfo()
console.log('userInfoRes', userInfoRes.data.phone);
const phone = userInfoRes.data.phone || ''
expectedUserPhone.value = phone
console.log('expectedUserPhone', expectedUserPhone.value);
// 没有手机号:弹窗引导获取/绑定
if (!phone) {
pendingScan.value = true
showPhoneAuthPopup.value = true
return false
}
// // 支付宝:即使已有手机号,也要求扫码前做一次一致性校验(或命中缓存)
// // #ifdef MP-ALIPAY
// const cached = uni.getStorageSync('alipay_phone_verified')
// const cachedPhone = cached && cached.phone ? cached.phone : ''
// if (cachedPhone && normalizePhone(cachedPhone) === normalizePhone(phone)) {
// return true
// }
// pendingScan.value = true
// showPhoneAuthPopup.value = true
// return false
// // #endif
return true
} catch (e) {
console.error('扫码前检查用户信息失败:', e)
return true // 不阻断,避免影响主流程;后续租借仍会二次校验
}
}
const doScan = async () => {
// #ifdef H5 // #ifdef H5
uni.navigateTo({ uni.navigateTo({
url: '/pages/scan/index' url: '/pages/scan/index'
@@ -987,6 +1105,12 @@
} }
} }
const handleScan = async () => {
const ok = await precheckBeforeScan()
if (!ok) return
await doScan()
}
const processScanResult = async (scanResult) => { const processScanResult = async (scanResult) => {
try { try {
console.log('===== 处理扫码结果 ====='); console.log('===== 处理扫码结果 =====');
@@ -1162,12 +1286,67 @@
showLocationPopup.value = false showLocationPopup.value = false
} }
const onGetPhoneNumber = (e) => { // 微信:绑定手机号后继续扫码
if (e.detail.errMsg === 'getPhoneNumber:ok') { const onWxGetPhoneNumber = async (e) => {
try {
if (!e || e.detail.errMsg !== 'getPhoneNumber:ok') {
return
}
await getUserPhoneNumber(e.detail.code)
showPhoneAuthPopup.value = false showPhoneAuthPopup.value = false
if (pendingScan.value) {
pendingScan.value = false
await doScan()
}
} catch (err) {
console.error('微信绑定手机号失败:', err)
} }
} }
// 支付宝:获取手机号(后端解密)并与 userInfo.phone 对比,一致才允许继续扫码
const onAliAuthorizePhoneNumber = async (e) => {
// #ifdef MP-ALIPAY
try {
console.log('[ALIPAY] getAuthorize(phoneNumber) event:', e)
uni.showLoading({ title: t('common.processing') })
const res = await getAlipayUserPhoneNumber()
const aliPhone = res?.data?.phoneNumber || res?.data?.phone || res?.phoneNumber || ''
if (!aliPhone) {
throw new Error('未获取到手机号')
}
const expected = expectedUserPhone.value
if (expected && normalizePhone(aliPhone) !== normalizePhone(expected)) {
uni.showModal({
title: t('common.tips'),
content: '当前支付宝授权手机号与账号绑定手机号不一致,无法扫码租借,请更换账号或联系管理员。',
showCancel: false
})
return
}
// 缓存本次校验结果(避免每次扫码都弹)
uni.setStorageSync('alipay_phone_verified', { phone: aliPhone, ts: Date.now() })
showPhoneAuthPopup.value = false
if (pendingScan.value) {
pendingScan.value = false
await doScan()
}
} catch (err) {
console.error('支付宝手机号校验失败:', err)
} finally {
uni.hideLoading()
}
// #endif
}
const onAliAuthorizePhoneNumberError = (e) => {
// #ifdef MP-ALIPAY
console.error('支付宝手机号授权失败:', e)
// #endif
}
// 使用指南弹窗控制 // 使用指南弹窗控制
const openPopup = () => { const openPopup = () => {
uni.navigateTo({ uni.navigateTo({
+7 -2
View File
@@ -134,8 +134,13 @@
} }
const formatDistance = (meters) => { const formatDistance = (meters) => {
if (meters < 1000) return `${Math.round(meters)}m` // 兼容支付宝小程序等环境:保证始终对 Number 调用 toFixed
return `${(meters / 1000).toFixed(1)}km` let m = meters
if (typeof m === 'bigint') m = Number(m)
m = Number(m)
if (!Number.isFinite(m) || m < 0) return ''
if (m < 1000) return `${Math.round(m)}m`
return `${(m / 1000).toFixed(1)}km`
} }
const setTab = (name) => { const setTab = (name) => {
+175 -17
View File
@@ -176,6 +176,8 @@
<view class="form-item"> <view class="form-item">
<text class="form-label">收货地区</text> <text class="form-label">收货地区</text>
<!-- 非支付宝小程序:使用多列 picker -->
<!-- #ifndef MP-ALIPAY -->
<picker mode="multiSelector" :range="regionColumns" range-key="name" :value="regionIndexes" <picker mode="multiSelector" :range="regionColumns" range-key="name" :value="regionIndexes"
@change="onRegionChange" @columnchange="onRegionColumnChange"> @change="onRegionChange" @columnchange="onRegionColumnChange">
<view class="form-input region-selector"> <view class="form-input region-selector">
@@ -187,6 +189,19 @@
<text class="arrow-icon"></text> <text class="arrow-icon"></text>
</view> </view>
</picker> </picker>
<!-- #endif -->
<!-- 支付宝小程序:使用自定义弹窗 + picker-view -->
<!-- #ifdef MP-ALIPAY -->
<view class="form-input region-selector" @click="showRegionPicker = true">
<text v-if="addressForm.province && addressForm.city && addressForm.district"
class="region-text">
{{ addressForm.province }} {{ addressForm.city }} {{ addressForm.district }}
</text>
<text v-else class="input-placeholder">请选择省市区</text>
<text class="arrow-icon"></text>
</view>
<!-- #endif -->
</view> </view>
<view class="form-item"> <view class="form-item">
@@ -254,6 +269,49 @@
</view> </view>
</view> </view>
<!-- 支付宝小程序地区选择弹窗 -->
<!-- #ifdef MP-ALIPAY -->
<view class="popup-mask" v-if="showRegionPicker" @click="closeRegionPicker">
<view class="popup-container" @click.stop>
<view class="address-popup">
<view class="popup-header">
<text class="popup-title">选择省市区</text>
<view class="close-btn" @click="closeRegionPicker">
<text class="close-icon">✕</text>
</view>
</view>
<view class="form-section">
<picker-view :value="regionIndexes" @change="onAliRegionChange"
indicator-style="height: 50px;">
<picker-view-column>
<view v-for="item in regionColumns[0]" :key="item.code">
{{ item.name }}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in regionColumns[1]" :key="item.code">
{{ item.name }}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in regionColumns[2]" :key="item.code">
{{ item.name }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="popup-footer">
<view class="confirm-btn" @click="confirmAliRegion">
<text>确定</text>
</view>
</view>
</view>
</view>
</view>
<!-- #endif -->
</view> </view>
</template> </template>
@@ -340,6 +398,7 @@
const showSkuPopup = ref(false) const showSkuPopup = ref(false)
const showAddressPopup = ref(false) const showAddressPopup = ref(false)
const showAddressDisplay = ref(false) // 地址展示弹窗 const showAddressDisplay = ref(false) // 地址展示弹窗
const showRegionPicker = ref(false) // 支付宝地区选择弹窗
// 计算是否已有地址 // 计算是否已有地址
const hasAddress = computed(() => { const hasAddress = computed(() => {
@@ -489,6 +548,45 @@
addressForm.value.districtCode = district.code addressForm.value.districtCode = district.code
} }
// 支付宝小程序 picker-view 列变化
const onAliRegionChange = (e) => {
const newVal = e.detail.value || []
const oldVal = regionIndexes.value || []
// 找出发生变化的列
let column = -1
for (let i = 0; i < newVal.length; i++) {
if (newVal[i] !== oldVal[i]) {
column = i
break
}
}
if (column !== -1) {
onRegionColumnChange({
detail: {
column,
value: newVal[column]
}
})
}
}
// 关闭支付宝地区弹窗
const closeRegionPicker = () => {
showRegionPicker.value = false
}
// 支付宝地区选择“确定”
const confirmAliRegion = () => {
onRegionChange({
detail: {
value: regionIndexes.value
}
})
closeRegionPicker()
}
// 获取用户收货地址 // 获取用户收货地址
const fetchUserAddress = async () => { const fetchUserAddress = async () => {
try { try {
@@ -767,7 +865,7 @@
}) })
} }
// 创建订单并支付 // 创建订单并支付(接入商品多支付平台方案:微信 / 支付宝,小程序端)
const createOrder = async () => { const createOrder = async () => {
try { try {
uni.showLoading({ uni.showLoading({
@@ -782,13 +880,24 @@
savedAddress.value.receiverAddress : savedAddress.value.receiverAddress :
`${addressForm.value.province}${addressForm.value.city}${addressForm.value.district}${addressForm.value.receiverAddress}` `${addressForm.value.province}${addressForm.value.city}${addressForm.value.district}${addressForm.value.receiverAddress}`
// 根据当前运行环境确定支付平台
let paymentPlatform = 'WECHAT' // 默认微信
// #ifdef MP-ALIPAY
paymentPlatform = 'ALIPAY'
// #endif
// #ifdef H5
// H5 预留 Antom 支付,这里暂时仍按微信处理,如需接入可改为 ANTOM 并补充 paymentType / osType
paymentPlatform = 'WECHAT'
// #endif
const orderData = { const orderData = {
skuId: selectedSku.value.skuId, skuId: selectedSku.value.skuId,
quantity:quantity.value, quantity: quantity.value,
receiverName: addressData.receiverName, receiverName: addressData.receiverName,
receiverPhone: addressData.receiverPhone, receiverPhone: addressData.receiverPhone,
receiverAddress: fullAddress, // 传递完整地址(省市区+详细地址) receiverAddress: fullAddress, // 传递完整地址(省市区+详细地址)
remark: addressForm.value.remark || '' remark: addressForm.value.remark || '',
paymentPlatform // WECHAT / ALIPAY /(预留)ANTOM
} }
console.log('创建订单数据:', orderData) console.log('创建订单数据:', orderData)
@@ -800,11 +909,12 @@
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
uni.hideLoading() uni.hideLoading()
// 调用微信支付 // 统一获取平台订单号(商品统一支付订单号)
const payParams = res.data const outOrderNo = res.data.OutOrderNo || res.data.outOrderNo
// 保存订单ID,用于取消订单
const orderId = payParams.OutOrderNo || res.data.OutOrderNo
// ====================== 微信小程序支付 ======================
// #ifdef MP-WEIXIN
const payParams = res.data
uni.requestPayment({ uni.requestPayment({
timeStamp: payParams.timeStamp, timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr, nonceStr: payParams.nonceStr,
@@ -834,23 +944,15 @@
// 判断是用户取消还是支付失败 // 判断是用户取消还是支付失败
if (payErr.errMsg && payErr.errMsg.includes('cancel')) { if (payErr.errMsg && payErr.errMsg.includes('cancel')) {
// 用户取消支付,调用取消订单接口 // 用户取消支付,这里预留调用取消订单接口
try { try {
// uni.showLoading({ // await cancelProductOrder(outOrderNo)
// title: '正在取消订单...',
// mask: true
// })
// await cancelProductOrder(orderId)
uni.hideLoading()
uni.showToast({ uni.showToast({
title: '支付已取消', title: '支付已取消',
icon: 'none' icon: 'none'
}) })
} catch (cancelError) { } catch (cancelError) {
console.error('取消订单失败:', cancelError) console.error('取消订单失败:', cancelError)
uni.hideLoading()
uni.showToast({ uni.showToast({
title: '支付已取消', title: '支付已取消',
icon: 'none' icon: 'none'
@@ -865,6 +967,62 @@
} }
} }
}) })
// #endif
// ====================== 支付宝小程序支付 ======================
// #ifdef MP-ALIPAY
console.log(res.data,'支付宝支付参数');
const tradeNO = res.data.tradeNo
if (!tradeNO) {
uni.showToast({
title: '未获取到支付宝支付参数',
icon: 'none'
})
return
}
my.tradePay({
tradeNO,
success: (payRes) => {
console.log('支付宝支付结果:', payRes)
if (payRes.resultCode === '9000') {
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
resetForm()
setTimeout(() => {
uni.switchTab({
url: '/subPackages/business/device-orderList'
})
}, 2000)
} else {
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
})
}
},
fail: () => {
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
})
}
})
// #endif
// ====================== H5 环境(预留 Antom 支付) ======================
// #ifdef H5
uni.showToast({
title: '当前环境暂不支持购买,请使用微信或支付宝小程序',
icon: 'none'
})
// #endif
} else { } else {
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
+84 -16
View File
@@ -335,25 +335,40 @@
navigateToOrderDetail(order); navigateToOrderDetail(order);
}; };
// 立即支付 // 立即支付(对齐 device-goods.vue,多平台支付)
const handlePayment = async (order) => { const handlePayment = async (order) => {
try { try {
uni.showLoading({ uni.showLoading({
title: '正在创建支付...', title: '正在创建支付...',
mask: true mask: true
}); });
console.log(order); console.log('订单列表立即支付,订单信息:', order);
// 调用后端创建微信支付订单接口(使用订单号 // 根据当前运行环境确定支付平台(与 device-goods.vue 保持一致
// const res = await createWxPayment(order.orderNo); let paymentPlatform = 'WECHAT'; // 默认微信
const res = await createProductOrder({orderNo:order.orderNo}); // #ifdef MP-ALIPAY
paymentPlatform = 'ALIPAY';
// #endif
// #ifdef H5
// H5 预留 Antom 支付,这里暂时仍按微信处理,如需接入可改为 ANTOM 并补充 paymentType / osType
paymentPlatform = 'WECHAT';
// #endif
// 使用商品多支付平台统一下单接口,对已有订单进行支付
const res = await createProductOrder({
orderNo: order.orderNo,
paymentPlatform
});
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
uni.hideLoading(); uni.hideLoading();
const payParams = res.data; // 统一获取平台订单号(商品统一支付订单号)
const outOrderNo = res.data.OutOrderNo || res.data.outOrderNo;
// 调用微信支付 // ====================== 微信小程序支付 ======================
// #ifdef MP-WEIXIN
const payParams = res.data;
uni.requestPayment({ uni.requestPayment({
timeStamp: payParams.timeStamp, timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr, nonceStr: payParams.nonceStr,
@@ -373,18 +388,18 @@
const statusValue = statusArray.length === 0 ? undefined : statusArray[0]; const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue); await loadOrderList(statusValue);
}, },
fail: async (err) => { fail: async (payErr) => {
console.error('支付失败:', err); console.error('支付失败:', payErr);
// 判断是用户取消还是支付失败 // 判断是用户取消还是支付失败
if (err.errMsg && err.errMsg.includes('cancel')) { if (payErr.errMsg && payErr.errMsg.includes('cancel')) {
// 用户取消支付,调用取消订单接口 // 用户取消支付,这里预留调用取消订单接口
try { try {
// await cancelProductOrder(order.id || order.orderId); // await cancelProductOrder(outOrderNo || order.orderNo);
// uni.showToast({ uni.showToast({
// title: '支付已取消', title: '支付已取消',
// icon: 'none' icon: 'none'
// }); });
// 刷新订单列表 // 刷新订单列表
const statusArray = orderStatusTabs[currentTab.value].status; const statusArray = orderStatusTabs[currentTab.value].status;
@@ -406,6 +421,59 @@
} }
} }
}); });
// #endif
// ====================== 支付宝小程序支付 ======================
// #ifdef MP-ALIPAY
console.log(res.data, '支付宝支付参数');
const tradeNO = res.data.tradeNo || res.data.tradeNO;
if (!tradeNO) {
uni.showToast({
title: '未获取到支付宝支付参数',
icon: 'none'
});
return;
}
my.tradePay({
tradeNO,
success: async (payRes) => {
console.log('支付宝支付结果:', payRes);
if (payRes.resultCode === '9000') {
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
});
// 支付成功后刷新订单列表
const statusArray = orderStatusTabs[currentTab.value].status;
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
} else {
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
});
}
},
fail: () => {
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
});
}
});
// #endif
// ====================== H5 环境(预留 Antom 支付) ======================
// #ifdef H5
uni.showToast({
title: '当前环境暂不支持购买,请使用微信或支付宝小程序',
icon: 'none'
});
// #endif
} else { } else {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
+178 -93
View File
@@ -176,9 +176,6 @@ import {
createCouponPayment, createCouponPayment,
cancelCouponPayment cancelCouponPayment
} from '@/config/api/coupon.js' } from '@/config/api/coupon.js'
// import {
// cancelMemberCardPayment
// } from '@/config/api/member.js'
import { import {
createMemberCardPayment, createMemberCardPayment,
getMemberCardsByPosition, getMemberCardsByPosition,
@@ -353,7 +350,48 @@ const selectProduct = (product) => {
const orderNo = ref('') const orderNo = ref('')
// 处理购买 // 获取当前支付平台(前端维度)
const getPaymentPlatform = () => {
// 小程序环境
// #ifdef MP-WEIXIN
return 'WECHAT'
// #endif
// #ifdef MP-ALIPAY
return 'ALIPAY'
// #endif
// H5 默认使用 ANTOM
// #ifdef H5
return 'ANTOM'
// #endif
return 'WECHAT'
}
// 支付宝 tradePay 兼容:优先 tradeNO,其次 orderStr
const alipayTradePay = ({ tradeNo, orderStr, onSuccess, onFail }) => {
// #ifdef MP-ALIPAY
const tradeNO = tradeNo
if (tradeNO) {
my.tradePay({
tradeNO,
success: onSuccess,
fail: onFail
})
return
}
if (orderStr) {
my.tradePay({
orderStr,
success: onSuccess,
fail: onFail
})
return
}
// #endif
onFail && onFail(new Error('未获取到支付宝支付参数'))
}
// 处理购买(会员卡 / 优惠券),内部使用商品多支付平台方案下的统一思路
const handleBuy = async () => { const handleBuy = async () => {
if (!selectedProduct.value) { if (!selectedProduct.value) {
uni.showToast({ uni.showToast({
@@ -363,63 +401,89 @@ const handleBuy = async () => {
return return
} }
// 会员卡购买 const paymentPlatform = getPaymentPlatform()
// 会员卡购买(按接口文档:POST /app/member/pay
if (currentTab.value === 'card') { if (currentTab.value === 'card') {
try { try {
uni.showLoading({ uni.showLoading({
title: '正在创建订单...' title: '正在创建订单...'
}) })
const res = await createMemberCardPayment(selectedProduct.value.id) const res = await createMemberCardPayment(selectedProduct.value.id, paymentPlatform)
uni.hideLoading() uni.hideLoading()
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// 不同平台分别发起支付(字段按文档)
orderNo.value = res.data.OutOrderNo; // 微信小程序
// 调起微信支付 // #ifdef MP-WEIXIN
uni.requestPayment({ if (paymentPlatform === 'WECHAT') {
timeStamp: res.data.timeStamp, // 会员卡订单号:OutOrderNo(文档)
nonceStr: res.data.nonceStr, orderNo.value = res.data.OutOrderNo
package: res.data.packageValue || res.data.package, uni.requestPayment({
signType: res.data.signType || 'MD5', timeStamp: res.data.timeStamp,
paySign: res.data.paySign, nonceStr: res.data.nonceStr,
success: (payRes) => { package: res.data.package,
uni.showToast({ signType: res.data.signType || 'MD5',
title: '支付成功', paySign: res.data.paySign,
icon: 'success' success: () => {
}) uni.showToast({ title: '支付成功', icon: 'success' })
// 支付成功后,跳转到我的会员卡页面 setTimeout(() => {
setTimeout(() => { uni.navigateTo({ url: '/subPackages/business/my-card' })
uni.navigateTo({ }, 1500)
url: '/subPackages/business/my-card' },
}) fail: (err) => {
}, 1500) console.error('支付失败:', err)
}, if (err.errMsg && err.errMsg.includes('cancel')) {
fail: (err) => { // 取消支付(本项目取消接口走 device 侧 cancel
console.error('支付失败:', err) orderNo.value && cancelMemberCardPayment(orderNo.value).catch(() => {})
console.log('支付失败详细信息:', err.errMsg.includes('cancel')); uni.showToast({ title: '已取消支付', icon: 'none' })
if (err.errMsg && err.errMsg.includes('cancel')) { } else {
if (orderNo.value) { uni.showToast({ title: '支付失败', icon: 'none' })
cancelMemberCardPayment(orderNo.value)
.then(cancelRes => {
console.log('取消支付订单成功:', cancelRes);
})
.catch(cancelErr => {
console.error('取消支付订单失败:', cancelErr);
});
} }
uni.showToast({
title: '已取消支付',
icon: 'none'
})
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
})
} }
})
return
}
// #endif
// 支付宝小程序
// #ifdef MP-ALIPAY
if (paymentPlatform === 'ALIPAY') {
// 文档返回:tradeNo / outTradeNo;也兼容 orderStr
const tradeNo = res.data.tradeNo || res.data.tradeNO
const payForm = res.data.payForm || res.data.orderStr
alipayTradePay({
tradeNo,
orderStr: payForm,
onSuccess: (payRes) => {
if (payRes.resultCode === '9000') {
uni.showToast({ title: '支付成功', icon: 'success' })
setTimeout(() => {
uni.navigateTo({ url: '/subPackages/business/my-card' })
}, 1500)
} else {
uni.showToast({ title: '支付失败', icon: 'none' })
}
},
onFail: () => {
uni.showToast({ title: '支付失败', icon: 'none' })
}
})
return
}
// #endif
// H5 + Antom(文档里 Antom 预留,当前后端可能返回 cashierUrl / h5Url 之一)
// #ifdef H5
if (paymentPlatform === 'ANTOM') {
const cashierUrl = res.data.cashierUrl || res.data.h5Url
if (cashierUrl) {
window.location.href = cashierUrl
return
} }
}) }
// #endif
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg || '创建订单失败', title: res.msg || '创建订单失败',
@@ -437,62 +501,83 @@ const handleBuy = async () => {
return return
} }
// 优惠券购买 // 优惠券购买(按接口文档:POST /app/coupon/pay
if (currentTab.value === 'coupon') { if (currentTab.value === 'coupon') {
try { try {
uni.showLoading({ uni.showLoading({
title: '正在创建订单...' title: '正在创建订单...'
}) })
const res = await createCouponPayment(selectedProduct.value.couponId) const res = await createCouponPayment(selectedProduct.value.couponId, paymentPlatform)
uni.hideLoading() uni.hideLoading()
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// 调起微信支付 // 微信小程序
uni.requestPayment({ // #ifdef MP-WEIXIN
timeStamp: res.data.timeStamp, if (paymentPlatform === 'WECHAT') {
nonceStr: res.data.nonceStr, uni.requestPayment({
package: res.data.packageValue || res.data.package, timeStamp: res.data.timeStamp,
signType: res.data.signType || 'MD5', nonceStr: res.data.nonceStr,
paySign: res.data.paySign, package: res.data.package,
success: (payRes) => { signType: res.data.signType || 'MD5',
uni.showToast({ paySign: res.data.paySign,
title: '支付成功', success: () => {
icon: 'success' uni.showToast({ title: '支付成功', icon: 'success' })
}) setTimeout(() => {
// 支付成功后,跳转到我的优惠券页面 uni.navigateTo({ url: '/subPackages/business/my-coupon' })
setTimeout(() => { }, 1500)
uni.navigateTo({ },
url: '/subPackages/business/my-coupon' fail: (err) => {
}) console.error('支付失败:', err)
}, 1500) if (err.errMsg && err.errMsg.includes('cancel')) {
}, const outOrderNo = res.data.OutOrderNo || res.data.outOrderNo
fail: (err) => { outOrderNo && cancelCouponPayment(outOrderNo).catch(() => {})
console.error('支付失败:', err) uni.showToast({ title: '已取消支付', icon: 'none' })
if (err.errMsg && err.errMsg.includes('cancel')) { } else {
// 用户取消支付,调用取消接口 uni.showToast({ title: '支付失败', icon: 'none' })
const orderNo = res.data.OutOrderNo;
if (orderNo) {
cancelCouponPayment(orderNo)
.then(cancelRes => {
console.log('取消支付订单成功:', cancelRes);
})
.catch(cancelErr => {
console.error('取消支付订单失败:', cancelErr);
});
} }
uni.showToast({
title: '已取消支付',
icon: 'none'
})
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
})
} }
})
return
}
// #endif
// 支付宝小程序
// #ifdef MP-ALIPAY
if (paymentPlatform === 'ALIPAY') {
const tradeNo = res.data.tradeNo || res.data.tradeNO
const payForm = res.data.payForm || res.data.orderStr
alipayTradePay({
tradeNo,
orderStr: payForm,
onSuccess: (payRes) => {
if (payRes.resultCode === '9000') {
uni.showToast({ title: '支付成功', icon: 'success' })
setTimeout(() => {
uni.navigateTo({ url: '/subPackages/business/my-coupon' })
}, 1500)
} else {
uni.showToast({ title: '支付失败', icon: 'none' })
}
},
onFail: () => {
uni.showToast({ title: '支付失败', icon: 'none' })
}
})
return
}
// #endif
// H5 + Antom
// #ifdef H5
if (paymentPlatform === 'ANTOM') {
const cashierUrl = res.data.cashierUrl || res.data.h5Url
if (cashierUrl) {
window.location.href = cashierUrl
return
} }
}) }
// #endif
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg || '创建订单失败', title: res.msg || '创建订单失败',
+183 -51
View File
@@ -78,6 +78,10 @@
} from '@dcloudio/uni-app' } from '@dcloudio/uni-app'
import { import {
queryById, queryById,
createWxPayment,
getWxPaymentStatus,
createAliPayment,
getAliPaymentStatus,
createAntomPayment, createAntomPayment,
getAntomPaymentMethods, getAntomPaymentMethods,
getAntomPaymentStatus getAntomPaymentStatus
@@ -107,9 +111,9 @@
const countdown = ref(15 * 60) // 15分钟 = 900秒 const countdown = ref(15 * 60) // 15分钟 = 900秒
let countdownTimer = null let countdownTimer = null
// 支付方式相关 // 支付方式相关(微信/支付宝/H5-Antom 多平台)
const paymentMethods = ref([]) const paymentMethods = ref([])
const selectedPaymentMethod = ref('ALIPAY') // 默认选择支付宝 const selectedPaymentMethod = ref('WECHAT') // 默认微信
// 地点名称(可以从设备信息中获取,这里先用默认值) // 地点名称(可以从设备信息中获取,这里先用默认值)
const locationName = ref('澎创办公室') const locationName = ref('澎创办公室')
@@ -197,11 +201,11 @@
deviceNo.value = orderData.deviceNo; deviceNo.value = orderData.deviceNo;
await loadDeviceInfo(); await loadDeviceInfo();
await loadPaymentMethods(); await loadPaymentMethods();
// #ifdef H5 // 如果订单状态是等待支付,启动相应的支付状态轮询
if(orderInfo.value.orderStatus=='waiting_for_payment'){ if(orderInfo.value.orderStatus=='waiting_for_payment'){
// 使用当前选中的支付方式类型进行轮询
startPaymentStatusPolling(); startPaymentStatusPolling();
} }
// #endif
} else { } else {
throw new Error(t('order.getOrderFailed')) throw new Error(t('order.getOrderFailed'))
} }
@@ -253,35 +257,46 @@
} }
} }
// 加载支付方式列表 // 加载支付方式列表(根据平台决定可选项)
const loadPaymentMethods = async () => { const loadPaymentMethods = async () => {
if (!orderInfo.value.orderNo) return; const methods = []
try { // 小程序环境下:微信 / 支付宝
const osType = getOsType(); // #ifdef MP-WEIXIN
console.log('当前系统类型:', osType); methods.push({ paymentMethodType: 'WECHAT', paymentMethodName: '微信支付' })
// #endif
// #ifdef MP-ALIPAY
methods.push({ paymentMethodType: 'ALIPAY', paymentMethodName: '支付宝支付' })
// #endif
const res = await getAntomPaymentMethods(orderInfo.value.orderNo, osType); // H5 环境:使用 Antom 聚合支付(多通道)
if (res.code === 200 && res.data && res.data.paymentOptions) { // #ifdef H5
paymentMethods.value = res.data.paymentOptions; if (orderInfo.value.orderNo) {
console.log('支付方式列表:', paymentMethods.value); try {
// 如果有支付方式,默认选择第一个 const osType = getOsType();
if (paymentMethods.value.length > 0) { const res = await getAntomPaymentMethods(orderInfo.value.orderNo, osType);
selectedPaymentMethod.value = paymentMethods.value[0].paymentMethodType; if (res.code === 200 && res.data && res.data.paymentOptions) {
res.data.paymentOptions.forEach(item => {
methods.push({
paymentMethodType: item.paymentMethodType,
paymentMethodName: item.paymentMethodName || item.paymentMethodType
});
});
} }
} catch (error) {
console.error('获取 Antom 支付方式失败:', error);
} }
} catch (error) { }
console.error('获取支付方式失败:', error); // #endif
// 如果获取失败,使用默认支付方式
paymentMethods.value = [{ // 兜底:至少保留一个微信
paymentMethodType: 'ALIPAY', if (!methods.length) {
paymentMethodName: '支付' methods.push({ paymentMethodType: 'WECHAT', paymentMethodName: '微信支付' })
}, }
{
paymentMethodType: 'WECHATPAY', paymentMethods.value = methods
paymentMethodName: '微信支付' if (paymentMethods.value.length > 0) {
} selectedPaymentMethod.value = paymentMethods.value[0].paymentMethodType
];
} }
} }
@@ -315,8 +330,85 @@
title: t('common.processing') title: t('common.processing')
}) })
const method = selectedPaymentMethod.value
// 微信小程序支付押金
// #ifdef MP-WEIXIN
if (method === 'WECHAT') {
const wxRes = await createWxPayment(orderInfo.value.orderNo)
if (wxRes.code === 200 && wxRes.data) {
const payData = wxRes.data
await new Promise((resolve, reject) => {
wx.requestPayment({
...payData,
success: resolve,
fail: reject
})
})
// 支付成功后轮询微信支付状态
startPaymentStatusPolling('WECHAT')
return
} else {
throw new Error(wxRes.msg || t('payment.createPayOrderFailed'))
}
}
// #endif
// 支付宝小程序支付押金
// #ifdef MP-ALIPAY
if (method === 'ALIPAY') {
const aliRes = await createAliPayment(orderInfo.value.orderNo)
if (aliRes.code === 200 && aliRes.data) {
// 后端当前实际返回结构示例:
// { code:200, msg:'操作成功', data:{ tradeNo:'xxx', outTradeNo:'yyy' } }
const tradeNO = aliRes.data.tradeNo || aliRes.data.outTradeNo
const payForm = aliRes.data.payForm || aliRes.data.orderStr
if (!tradeNO && !payForm) {
throw new Error('未获取到支付宝支付参数')
}
// 优先使用 tradeNO 方式,其次兼容老的 orderStr 方式
if (tradeNO) {
my.tradePay({
tradeNO,
success: (res) => {
if (res.resultCode === '9000') {
startPaymentStatusPolling('ALIPAY')
} else {
uni.showToast({ title: t('payment.paymentFailed'), icon: 'none' })
}
},
fail: () => {
uni.showToast({ title: t('payment.paymentFailed'), icon: 'none' })
}
})
} else {
my.tradePay({
orderStr: payForm,
success: (res) => {
if (res.resultCode === '9000') {
startPaymentStatusPolling('ALIPAY')
} else {
uni.showToast({ title: t('payment.paymentFailed'), icon: 'none' })
}
},
fail: () => {
uni.showToast({ title: t('payment.paymentFailed'), icon: 'none' })
}
})
}
return
} else {
throw new Error(aliRes.msg || t('payment.createPayOrderFailed'))
}
}
// #endif
// H5 + Antom 聚合支付
// #ifdef H5
const osType = getOsType(); const osType = getOsType();
const res = await createAntomPayment(orderInfo.value.orderNo, selectedPaymentMethod.value, osType) const res = await createAntomPayment(orderInfo.value.orderNo, method, osType)
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
const paymentUrl = res.data.h5Url; const paymentUrl = res.data.h5Url;
@@ -326,21 +418,16 @@
} }
uni.hideLoading(); uni.hideLoading();
// #ifdef H5
uni.setStorageSync('pendingPaymentNo', orderId.value); uni.setStorageSync('pendingPaymentNo', orderId.value);
// 跳转到支付页面
// uni.navigateTo({
// url: `/pages/webview/index?url=${encodeURIComponent(paymentUrl)}&title=支付`
// });
window.open(paymentUrl); window.open(paymentUrl);
// #endif
// 开始轮询支付状态
startPaymentStatusPolling();
// 开始轮询支付状态(传入当前选中的支付方式类型)
startPaymentStatusPolling(method);
return
} else { } else {
throw new Error(res?.msg || t('payment.createPayOrderFailed')) throw new Error(res?.msg || t('payment.createPayOrderFailed'))
} }
// #endif
} catch (error) { } catch (error) {
console.error('支付失败:', error) console.error('支付失败:', error)
uni.showToast({ uni.showToast({
@@ -352,14 +439,22 @@
} }
} }
// 轮询支付状态 // 轮询定时器
let pollingTimer = null; let pollingTimer = null;
const startPaymentStatusPolling = () => {
/**
* 统一的支付状态轮询方法
* @param {string} paymentMethodType - 支付方式类型,如 'WECHAT' | 'ALIPAY' | 'WECHATPAY' 等,与 selectedPaymentMethod 保持一致
*/
const startPaymentStatusPolling = (paymentMethodType = null) => {
// 清除之前的定时器 // 清除之前的定时器
if (pollingTimer) { if (pollingTimer) {
clearInterval(pollingTimer); clearInterval(pollingTimer);
} }
// 如果没有传入支付方式类型,使用当前选中的支付方式
const methodType = paymentMethodType || selectedPaymentMethod.value;
let pollCount = 0; let pollCount = 0;
const maxPollCount = 60; // 最多轮询60次(5分钟) const maxPollCount = 60; // 最多轮询60次(5分钟)
@@ -376,32 +471,64 @@
} }
try { try {
let res;
let status, successStatus, failStatuses;
// #ifdef H5
// H5 环境统一使用 Antom 聚合支付 API
const osType = getOsType(); const osType = getOsType();
const res = await getAntomPaymentStatus(orderInfo.value.orderNo, osType); res = await getAntomPaymentStatus(orderInfo.value.orderNo, osType);
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
const paymentStatus = res.data.paymentStatus; status = res.data.paymentStatus;
successStatus = 'SUCCESS';
failStatuses = ['FAIL', 'CANCELLED'];
}
// #endif
if (paymentStatus === 'SUCCESS') { // #ifdef MP-WEIXIN
// 微信小程序:根据支付方式类型判断
if (methodType === 'WECHAT' || methodType === 'WECHATPAY') {
res = await getWxPaymentStatus(orderInfo.value.orderNo);
if (res && res.code === 200 && res.data) {
status = res.data.tradeStatus;
successStatus = 'SUCCESS';
failStatuses = ['FAIL', 'CANCELLED'];
}
}
// #endif
// #ifdef MP-ALIPAY
// 支付宝小程序
if (methodType === 'ALIPAY') {
res = await getAliPaymentStatus(orderInfo.value.orderNo);
if (res && res.code === 200 && res.data) {
status = res.data.tradeStatus;
successStatus = 'TRADE_SUCCESS';
failStatuses = ['TRADE_FAIL', 'TRADE_CANCELLED'];
}
}
// #endif
// 处理支付状态结果
if (res && res.code === 200 && res.data && status) {
// 支付成功
if (status === successStatus) {
clearInterval(pollingTimer); clearInterval(pollingTimer);
// uni.showToast({
// title: t('payment.paymentSuccess'),
// icon: 'success'
// });
try { try {
await updateUserBalance(orderId.value); await updateUserBalance(orderId.value);
} catch (error) { } catch (error) {
console.warn('更新用户余额失败:', error); console.warn('更新用户余额失败:', error);
} }
console.log(orderInfo);
setTimeout(() => { setTimeout(() => {
uni.redirectTo({ uni.redirectTo({
url: `/pages/order/success?orderId=${orderId.value}&deviceId=${orderInfo.value.deviceNo}` url: `/pages/order/success?orderId=${orderId.value}&deviceId=${orderInfo.value.deviceNo}`
}); });
}, 1500); }, 1500);
} else if (paymentStatus === 'FAIL' || paymentStatus === 'CANCELLED') { }
// 支付失败
else if (failStatuses && failStatuses.includes(status)) {
clearInterval(pollingTimer); clearInterval(pollingTimer);
uni.showToast({ uni.showToast({
title: '支付失败,请重新支付', title: '支付失败,请重新支付',
@@ -410,11 +537,16 @@
} }
} }
} catch (error) { } catch (error) {
console.error('查询支付状态失败:', error); const errorMsg = methodType === 'ALIPAY' ? '支付宝' : (methodType === 'WECHAT' || methodType === 'WECHATPAY') ? '微信' : '支付';
console.error(`查询${errorMsg}支付状态失败:`, error);
} }
}, 5000); // 每5秒查询一次 }, 5000); // 每5秒查询一次
} }
// 兼容性方法:保持原有函数名,内部调用统一方法
const startWxPaymentStatusPolling = () => startPaymentStatusPolling('WECHAT');
const startAliPaymentStatusPolling = () => startPaymentStatusPolling('ALIPAY');
// 更新导航栏倒计时 // 更新导航栏倒计时
const updateNavBarCountdown = () => { const updateNavBarCountdown = () => {
const minutes = Math.floor(countdown.value / 60).toString().padStart(2, '0') const minutes = Math.floor(countdown.value / 60).toString().padStart(2, '0')
+1 -1
View File
@@ -86,7 +86,7 @@
// 跳转到投诉记录列表 // 跳转到投诉记录列表
const navigateToRecord = () => { const navigateToRecord = () => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/feedback/list' url: '/subPackages/service/feedback/list'
}) })
} }
+1 -1
View File
@@ -270,7 +270,7 @@
// 跳转到详情页 // 跳转到详情页
const navigateToDetail = (item) => { const navigateToDetail = (item) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/feedback/detail?id=${item.id || item.feedbackId}` url: `/subPackages/service/feedback/detail?id=${item.id || item.feedbackId}`
}); });
}; };
</script> </script>
+87 -17
View File
@@ -2,18 +2,27 @@
<view class="help-container"> <view class="help-container">
<!-- 常见问题 --> <!-- 常见问题 -->
<view class="faq-section"> <view class="faq-section">
<uv-collapse :border="false"> <view
<uv-collapse-item v-for="(item, index) in faqList"
v-for="(item, index) in faqList" :key="index"
:key="index" class="collapse-item"
:title="$t(item.question)" >
:name="index" <view
class="collapse-header"
@click="toggleCollapse(index)"
>
<text class="collapse-title">{{ $t(item.question) }}</text>
<text class="collapse-icon" :class="{ 'active': activeIndex === index }"></text>
</view>
<view
class="collapse-content"
:class="{ 'show': activeIndex === index }"
> >
<view class="answer-content"> <view class="answer-content">
<text class="answer-text">{{ $t(item.answer) }}</text> <text class="answer-text">{{ $t(item.answer) }}</text>
</view> </view>
</uv-collapse-item> </view>
</uv-collapse> </view>
</view> </view>
<!-- 联系客服 --> <!-- 联系客服 -->
@@ -34,7 +43,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { HELP_CONTENT } from '@/constants/help' import { HELP_CONTENT } from '@/constants/help'
import { getCustomerPhone } from '@/util/index.js' import { getCustomerPhone } from '@/util/index.js'
@@ -44,6 +53,7 @@ const { t } = useI18n()
const faqList = ref(HELP_CONTENT.FAQ_LIST) const faqList = ref(HELP_CONTENT.FAQ_LIST)
const customerPhone = ref(HELP_CONTENT.CONTACT.PHONE.VALUE) const customerPhone = ref(HELP_CONTENT.CONTACT.PHONE.VALUE)
const activeIndex = ref(null)
onLoad(() => { onLoad(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
@@ -52,6 +62,14 @@ onLoad(() => {
customerPhone.value = getCustomerPhone() customerPhone.value = getCustomerPhone()
}) })
const toggleCollapse = (index) => {
if (activeIndex.value === index) {
activeIndex.value = null
} else {
activeIndex.value = index
}
}
const makePhoneCall = () => { const makePhoneCall = () => {
uni.makePhoneCall({ uni.makePhoneCall({
phoneNumber: customerPhone.value phoneNumber: customerPhone.value
@@ -72,15 +90,67 @@ const makePhoneCall = () => {
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04); box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
overflow: hidden; overflow: hidden;
.answer-content { .collapse-item {
padding: 20rpx 30rpx 30rpx; border-bottom: 1rpx solid #f0f0f0;
background: #f9f9f9;
.answer-text { &:last-child {
font-size: 28rpx; border-bottom: none;
color: #666; }
line-height: 1.8;
display: block; .collapse-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
cursor: pointer;
transition: background-color 0.2s;
&:active {
background-color: #f5f5f5;
}
.collapse-title {
flex: 1;
font-size: 30rpx;
color: #333;
font-weight: 500;
line-height: 1.5;
}
.collapse-icon {
margin-left: 20rpx;
font-size: 24rpx;
color: #999;
transition: transform 0.3s;
transform: rotate(0deg);
&.active {
transform: rotate(180deg);
}
}
}
.collapse-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
&.show {
max-height: 2000rpx;
transition: max-height 0.3s ease-in;
}
.answer-content {
padding: 20rpx 30rpx 30rpx;
background: #f9f9f9;
.answer-text {
font-size: 28rpx;
color: #666;
line-height: 1.8;
display: block;
}
}
} }
} }
} }
+37 -8
View File
@@ -8,13 +8,27 @@
<view class="title">{{ $t('auth.loginTitle') }}</view> <view class="title">{{ $t('auth.loginTitle') }}</view>
<view class="subtitle">{{ $t('auth.loginDesc') }}</view> <view class="subtitle">{{ $t('auth.loginDesc') }}</view>
<!-- 微信一键手机号快捷登录推荐 --> <!-- 微信小程序一键手机号快捷登录 -->
<!-- #ifdef MP-WEIXIN -->
<button v-if="!isAgreed" class="btn primary" @click="handleLoginClick"> <button v-if="!isAgreed" class="btn primary" @click="handleLoginClick">
{{ $t('auth.getPhoneNumber') }} {{ $t('auth.getPhoneNumber') }}
</button> </button>
<button v-else class="btn primary" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber"> <button v-else class="btn primary" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
{{ $t('auth.getPhoneNumber') }} {{ $t('auth.getPhoneNumber') }}
</button> </button>
<!-- #endif -->
<!-- 支付宝小程序授权码快捷登录不支持 open-type=getPhoneNumber -->
<!-- #ifdef MP-ALIPAY -->
<button v-if="!isAgreed" class="btn primary" @click="handleLoginClick">
{{ $t('auth.loginBtn') }}
</button>
<button v-else class="btn primary" @click="onAlipayLogin">
{{ $t('auth.loginBtn') }}
</button>
<!-- #endif -->
<!-- H5不显示小程序快捷登录按钮 -->
<!-- 手机号验证码登录 --> <!-- 手机号验证码登录 -->
<button class="btn outline" @click="goToPhoneLogin" v-if="isHTML5"> <button class="btn outline" @click="goToPhoneLogin" v-if="isHTML5">
@@ -40,7 +54,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { wxLogin, getUserPhoneNumber, getUserInfo } from '@/util/index.js' import { wxLogin, alipayLogin, getUserPhoneNumber, getUserInfo } from '@/util/index.js'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n() const { t } = useI18n()
@@ -116,13 +130,12 @@
uni.reLaunch({ url: target }) uni.reLaunch({ url: target })
} }
// 微信快捷登录入口(备用,目前主流程使用一键手机号登录)
const onWeChatLogin = async () => { const onWeChatLogin = async () => {
try { try {
// 先检查是否同意协议
await checkAgreement() await checkAgreement()
await wxLogin()
await wxLogin() uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
await navigateAfterLogin() await navigateAfterLogin()
} catch (error) { } catch (error) {
if (error.message !== t('auth.pleaseAgreeToTerms')) { if (error.message !== t('auth.pleaseAgreeToTerms')) {
@@ -131,6 +144,22 @@
} }
} }
// 支付宝快捷登录入口(支付宝小程序)
const onAlipayLogin = async () => {
try {
await checkAgreement()
await alipayLogin()
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
await navigateAfterLogin()
} catch (error) {
if (error.message !== t('auth.pleaseAgreeToTerms')) {
uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
}
}
}
// 注意:手机号一致性校验不在登录页做;扫码/租借前校验(支付宝 my.getPhoneNumber
const onGetPhoneNumber = async (e) => { const onGetPhoneNumber = async (e) => {
if (!e || e.detail.errMsg !== 'getPhoneNumber:ok') { if (!e || e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({ title: t('auth.phoneCancelled'), icon: 'none' }) uni.showToast({ title: t('auth.phoneCancelled'), icon: 'none' })
@@ -138,9 +167,9 @@
} }
try { try {
// 先微信登录,获取 token // 先完成微信登录/app/user/quickLogin,后端建立/查询 WECHAT_MINI 用户)
await wxLogin() await wxLogin()
// 再用微信返回的临时 code 换取手机号 // 再用微信返回的临时 code 换取手机号(后端按文档更新 phone 字段)
await getUserPhoneNumber(e.detail.code) await getUserPhoneNumber(e.detail.code)
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' }) uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
await navigateAfterLogin() await navigateAfterLogin()
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<view class="my-page"> <view class="my-page">
<view class="user-card" @click="navigateTo('/pages/userProfile/index')"> <view class="user-card" @click="navigateTo('/subPackages/user/userProfile/index')">
<view class="avatar-box"> <view class="avatar-box">
<image class="avatar" v-if="userInfo.avatar" :src="userInfo.avatar" mode="aspectFill" lazy-load="true"></image> <image class="avatar" v-if="userInfo.avatar" :src="userInfo.avatar" mode="aspectFill" lazy-load="true"></image>
<image v-else class="avatar" src="@/static/head.png" mode="aspectFill" lazy-load="true"></image> <image v-else class="avatar" src="@/static/head.png" mode="aspectFill" lazy-load="true"></image>
+2 -2
View File
@@ -11,11 +11,11 @@
</view> </view>
<view class="group"> <view class="group">
<view class="item" @click="navigateTo('/subPackages/other/legal/agreement')"> <view class="item" @click="navigateTo('/subPackages/other/legal/agreement')">
<text class="label">{{ $t('user.userAgreement') }}</text> <text class="label">{{ $t('user.settinguserAgreement') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon> <uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view> </view>
<view class="item" @click="navigateTo('/subPackages/other/legal/privacy')"> <view class="item" @click="navigateTo('/subPackages/other/legal/privacy')">
<text class="label">{{ $t('user.privacyPolicy') }}</text> <text class="label">{{ $t('user.settinguserprivacyPolicy') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon> <uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view> </view>
<view class="item" @click="navigateTo('/subPackages/other/legal/terms')"> <view class="item" @click="navigateTo('/subPackages/other/legal/terms')">
+6 -1
View File
@@ -8,6 +8,9 @@
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<button class="avatar-choose-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar"></button> <button class="avatar-choose-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar"></button>
<!-- #endif --> <!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<!-- <button class="avatar-choose-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar"></button> -->
<!-- #endif -->
</view> </view>
<view class="avatar-tip">{{ $t('userProfile.clickToChange') }}</view> <view class="avatar-tip">{{ $t('userProfile.clickToChange') }}</view>
</view> </view>
@@ -129,6 +132,8 @@ const redirectToLogin = () => {
// 小程序原生选择头像回调 // 小程序原生选择头像回调
const onChooseAvatar = async (e) => { const onChooseAvatar = async (e) => {
console.log(e.detail.avatarUrl,'获取头像详情');
try { try {
const token = uni.getStorageSync('token'); const token = uni.getStorageSync('token');
if (!token) { if (!token) {
@@ -282,7 +287,7 @@ function maskPhone(phone) {
} }
/* 仅小程序端存在,此按钮覆盖在头像上捕获点击以触发选择头像 */ /* 仅小程序端存在,此按钮覆盖在头像上捕获点击以触发选择头像 */
/* #ifdef MP-WEIXIN */ /* #ifdef MP-WEIXIN || MP-ALIPAY */
.avatar-choose-btn { .avatar-choose-btn {
position: absolute; position: absolute;
left: 0; left: 0;
+150 -18
View File
@@ -1,37 +1,48 @@
import { import {
login, login,
quickLogin,
getMyIndexInfo, getMyIndexInfo,
getWxUserPhoneNumber getWxUserPhoneNumber,
getAliUserPhoneNumber
} from "../config/api/user" } from "../config/api/user"
import { import {
URL, URL,
appid appid,
ZFBappid
} from "@/config/url.js" } from "@/config/url.js"
import { getCommonByBrand } from "@/config/api/system" import { getCommonByBrand } from "@/config/api/system"
import { HELP_CONTENT } from "@/constants/help" import { HELP_CONTENT } from "@/constants/help"
// import { GET_PHONE_NUMBER_URL } from "../config/url" // import { GET_PHONE_NUMBER_URL } from "../config/url"
// 微信登录方法 // 统一快捷登录 - 微信小程序登录(使用 /app/user/quickLogin
export const wxLogin = () => { export const wxLogin = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 1. 获取微信登录凭证 // 1. 获取微信登录凭证code -> 后端换 openId
uni.login({ uni.login({
provider: 'weixin', provider: 'weixin',
success: async (loginRes) => { success: async (loginRes) => {
try { try {
if (loginRes.code) { if (!loginRes.code) {
// 2. 发送 code 到后端换取 token throw new Error('获取微信登录凭证失败')
const result = await login({ }
code: loginRes.code,
appid: "wx2165f0be356ae7a9"
})
if (result.code === 200) { // 2. 调用统一快捷登录接口,后端根据 code 获取 openId,并按文档用 openId+userSource 处理
// 3. 保存token和用户信息 const result = await quickLogin({
loginType: 'WECHAT',
appid: appid,
code: loginRes.code
})
uni.setStorageSync('token', result.data.LoginWxVo.access_token) if (result.code === 200 && result.data) {
uni.setStorageSync('client_id', result.data.LoginWxVo.client_id) // 3. 兼容多种返回 token 结构
const token = result.data.access_token || result.data.token || result.token
const clientId = result.data.client_id || result.data.clientId || result.client_id
if (token) {
uni.setStorageSync('token', token)
if (clientId) {
uni.setStorageSync('client_id', clientId)
}
// 4. 登录成功后获取并缓存客服电话 // 4. 登录成功后获取并缓存客服电话
fetchAndCacheCustomerPhone().catch(err => { fetchAndCacheCustomerPhone().catch(err => {
@@ -40,13 +51,12 @@ export const wxLogin = () => {
resolve(result.data) resolve(result.data)
} else { } else {
throw new Error(result.message || '登录失败') throw new Error(result.msg || '登录失败,未返回令牌')
} }
} else { } else {
throw new Error('获取微信登录凭证失败') throw new Error(result.msg || result.message || '登录失败')
} }
} catch (error) { } catch (error) {
uni.showToast({ uni.showToast({
title: error.message || '登录失败', title: error.message || '登录失败',
icon: 'none' icon: 'none'
@@ -65,6 +75,79 @@ export const wxLogin = () => {
}) })
} }
// 统一快捷登录 - 支付宝小程序登录
export const alipayLogin = () => {
return new Promise((resolve, reject) => {
// 仅在支付宝小程序环境可用
// #ifdef MP-ALIPAY
my.getAuthCode({
scopes: ['auth_user'],
success: async (res) => {
try {
if (!res.authCode) {
throw new Error('获取支付宝授权码失败')
}
const result = await quickLogin({
loginType: 'ALIPAY',
appid: ZFBappid,
code: res.authCode
})
if (result.code === 200 && result.data) {
const token = result.data.access_token || result.data.token || result.token
const clientId = result.data.client_id || result.data.clientId || result.client_id
if (token) {
uni.setStorageSync('token', token)
if (clientId) {
uni.setStorageSync('client_id', clientId)
}
fetchAndCacheCustomerPhone().catch(err => {
console.error('获取客服电话失败,但不影响登录', err)
})
resolve(result.data)
} else {
throw new Error(result.msg || '登录失败,未返回令牌')
}
} else {
throw new Error(result.msg || result.message || '登录失败')
}
} catch (error) {
uni.showToast({
title: error.message || '登录失败',
icon: 'none'
})
reject(error)
}
},
fail: (error) => {
uni.showToast({
title: '支付宝登录失败',
icon: 'none'
})
reject(error)
}
})
// #endif
// #ifndef MP-ALIPAY
reject(new Error('当前环境不支持支付宝登录'))
// #endif
})
}
// 统一快捷登录 - 短信验证码登录(主要用于 H5/海外)
export const smsQuickLogin = (phonenumber, smsCode) => {
return quickLogin({
loginType: 'SMS',
phonenumber,
smsCode
})
}
// 检查登录状态 // 检查登录状态
// export const checkLogin = () => { // export const checkLogin = () => {
// const token = uni.getStorageSync('token') // const token = uni.getStorageSync('token')
@@ -106,6 +189,55 @@ export const getUserPhoneNumber = (code) => {
}) })
} }
// 支付宝:在已授权 phoneNumber 的前提下调用 my.getPhoneNumber,再提交后端解密/绑定,返回手机号
// 官方 API https://opendocs.alipay.com/mini/01f46f19_my.getPhoneNumber?pathHash=a67c2790
export const getAlipayUserPhoneNumber = () => {
return new Promise((resolve, reject) => {
// #ifdef MP-ALIPAY
if (typeof my === 'undefined' || typeof my.getPhoneNumber !== 'function') {
reject(new Error('当前环境不支持支付宝获取手机号'))
return
}
my.getPhoneNumber({
success: async (res) => {
try {
// res.response 实际是一个 JSON 字符串,形如:
// {"response":"...","sign":"...","sign_type":"RSA2","encrypt_type":"AES","charset":"UTF-8"}
// 这里先做 JSON 解析,再把字段按 JSON 结构传给后端
let parsed = {}
if (res && res.response) {
try {
parsed = typeof res.response === 'string' ? JSON.parse(res.response) : res.response
} catch (parseErr) {
console.error('解析支付宝手机号报文失败:', parseErr, res.response)
throw parseErr
}
}
const payload = {
appid: ZFBappid,
...parsed
}
const result = await getAliUserPhoneNumber(payload)
resolve(result)
} catch (e) {
reject(e)
}
},
fail: (err) => {
reject(new Error(err?.errorMessage || err?.message || '支付宝获取手机号失败'))
}
})
// #endif
// #ifndef MP-ALIPAY
reject(new Error('当前环境不支持支付宝获取手机号'))
// #endif
})
}
// 调用微信支付分接口 // 调用微信支付分接口
export const initiateWeChatScorePayment = (paymentData) => { export const initiateWeChatScorePayment = (paymentData) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
+386 -6
View File
@@ -36,6 +36,206 @@ const getPermissionText = (key) => {
return texts[key] || permissionTexts[DEFAULT_LOCALE][key] || '' return texts[key] || permissionTexts[DEFAULT_LOCALE][key] || ''
} }
// 兼容多端:部分平台(如支付宝小程序)经纬度可能返回字符串
// 统一先转为 Number 再做 toFixed,避免 "toFixed is not a function"
const toFixedNumber = (value, digits = 5) => {
const n = Number(value)
if (!Number.isFinite(n)) return null
return Number(n.toFixed(digits))
}
// =============================
// 支付宝小程序:高德地图 WebService(需要 key + 安全密钥签名)
// 说明:支付宝小程序下仅使用高德 key(你提供的 key/secret),避免腾讯地图 key 不可用的问题
// =============================
// #ifdef MP-ALIPAY
const AMAP_KEY = '1c6df40606891377b33576e7876af6ac'
const AMAP_SECRET = '00ea790d0b24190174c598199b183750'
const AMAP_BASE_URL = 'https://restapi.amap.com/v3/'
// 轻量 MD5RFC1321)实现:用于高德 WebService 的 sig 参数
// 仅在 MP-ALIPAY 编译进包,避免影响其它端体积
function md5(str) {
/* eslint-disable */
function cmn(q, a, b, x, s, t) {
a = add32(add32(a, q), add32(x, t));
return add32((a << s) | (a >>> (32 - s)), b);
}
function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t); }
function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t); }
function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); }
function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t); }
function md5cycle(x, k) {
let a = x[0], b = x[1], c = x[2], d = x[3];
a = ff(a, b, c, d, k[0], 7, -680876936);
d = ff(d, a, b, c, k[1], 12, -389564586);
c = ff(c, d, a, b, k[2], 17, 606105819);
b = ff(b, c, d, a, k[3], 22, -1044525330);
a = ff(a, b, c, d, k[4], 7, -176418897);
d = ff(d, a, b, c, k[5], 12, 1200080426);
c = ff(c, d, a, b, k[6], 17, -1473231341);
b = ff(b, c, d, a, k[7], 22, -45705983);
a = ff(a, b, c, d, k[8], 7, 1770035416);
d = ff(d, a, b, c, k[9], 12, -1958414417);
c = ff(c, d, a, b, k[10], 17, -42063);
b = ff(b, c, d, a, k[11], 22, -1990404162);
a = ff(a, b, c, d, k[12], 7, 1804603682);
d = ff(d, a, b, c, k[13], 12, -40341101);
c = ff(c, d, a, b, k[14], 17, -1502002290);
b = ff(b, c, d, a, k[15], 22, 1236535329);
a = gg(a, b, c, d, k[1], 5, -165796510);
d = gg(d, a, b, c, k[6], 9, -1069501632);
c = gg(c, d, a, b, k[11], 14, 643717713);
b = gg(b, c, d, a, k[0], 20, -373897302);
a = gg(a, b, c, d, k[5], 5, -701558691);
d = gg(d, a, b, c, k[10], 9, 38016083);
c = gg(c, d, a, b, k[15], 14, -660478335);
b = gg(b, c, d, a, k[4], 20, -405537848);
a = gg(a, b, c, d, k[9], 5, 568446438);
d = gg(d, a, b, c, k[14], 9, -1019803690);
c = gg(c, d, a, b, k[3], 14, -187363961);
b = gg(b, c, d, a, k[8], 20, 1163531501);
a = gg(a, b, c, d, k[13], 5, -1444681467);
d = gg(d, a, b, c, k[2], 9, -51403784);
c = gg(c, d, a, b, k[7], 14, 1735328473);
b = gg(b, c, d, a, k[12], 20, -1926607734);
a = hh(a, b, c, d, k[5], 4, -378558);
d = hh(d, a, b, c, k[8], 11, -2022574463);
c = hh(c, d, a, b, k[11], 16, 1839030562);
b = hh(b, c, d, a, k[14], 23, -35309556);
a = hh(a, b, c, d, k[1], 4, -1530992060);
d = hh(d, a, b, c, k[4], 11, 1272893353);
c = hh(c, d, a, b, k[7], 16, -155497632);
b = hh(b, c, d, a, k[10], 23, -1094730640);
a = hh(a, b, c, d, k[13], 4, 681279174);
d = hh(d, a, b, c, k[0], 11, -358537222);
c = hh(c, d, a, b, k[3], 16, -722521979);
b = hh(b, c, d, a, k[6], 23, 76029189);
a = hh(a, b, c, d, k[9], 4, -640364487);
d = hh(d, a, b, c, k[12], 11, -421815835);
c = hh(c, d, a, b, k[15], 16, 530742520);
b = hh(b, c, d, a, k[2], 23, -995338651);
a = ii(a, b, c, d, k[0], 6, -198630844);
d = ii(d, a, b, c, k[7], 10, 1126891415);
c = ii(c, d, a, b, k[14], 15, -1416354905);
b = ii(b, c, d, a, k[5], 21, -57434055);
a = ii(a, b, c, d, k[12], 6, 1700485571);
d = ii(d, a, b, c, k[3], 10, -1894986606);
c = ii(c, d, a, b, k[10], 15, -1051523);
b = ii(b, c, d, a, k[1], 21, -2054922799);
a = ii(a, b, c, d, k[8], 6, 1873313359);
d = ii(d, a, b, c, k[15], 10, -30611744);
c = ii(c, d, a, b, k[6], 15, -1560198380);
b = ii(b, c, d, a, k[13], 21, 1309151649);
a = ii(a, b, c, d, k[4], 6, -145523070);
d = ii(d, a, b, c, k[11], 10, -1120210379);
c = ii(c, d, a, b, k[2], 15, 718787259);
b = ii(b, c, d, a, k[9], 21, -343485551);
x[0] = add32(a, x[0]);
x[1] = add32(b, x[1]);
x[2] = add32(c, x[2]);
x[3] = add32(d, x[3]);
}
function md5blk(s) {
const md5blks = [];
for (let i = 0; i < 64; i += 4) {
md5blks[i >> 2] = s.charCodeAt(i) +
(s.charCodeAt(i + 1) << 8) +
(s.charCodeAt(i + 2) << 16) +
(s.charCodeAt(i + 3) << 24);
}
return md5blks;
}
function md5blk_array(a) {
const md5blks = [];
for (let i = 0; i < 64; i += 4) {
md5blks[i >> 2] = a[i] +
(a[i + 1] << 8) +
(a[i + 2] << 16) +
(a[i + 3] << 24);
}
return md5blks;
}
function md51(s) {
let n = s.length;
let state = [1732584193, -271733879, -1732584194, 271733878];
let i;
for (i = 64; i <= n; i += 64) {
md5cycle(state, md5blk(s.substring(i - 64, i)));
}
s = s.substring(i - 64);
const tail = new Array(64).fill(0);
for (i = 0; i < s.length; i++) tail[i] = s.charCodeAt(i);
tail[i] = 0x80;
if (i > 55) {
md5cycle(state, md5blk_array(tail));
for (i = 0; i < 64; i++) tail[i] = 0;
}
const tmp = n * 8;
tail[56] = tmp & 0xFF;
tail[57] = (tmp >>> 8) & 0xFF;
tail[58] = (tmp >>> 16) & 0xFF;
tail[59] = (tmp >>> 24) & 0xFF;
md5cycle(state, md5blk_array(tail));
return state;
}
function rhex(n) {
const s = '0123456789abcdef';
let j, out = '';
for (j = 0; j < 4; j++) {
out += s.charAt((n >> (j * 8 + 4)) & 0x0F) + s.charAt((n >> (j * 8)) & 0x0F);
}
return out;
}
function hex(x) { return x.map(rhex).join(''); }
function add32(a, b) { return (a + b) & 0xFFFFFFFF; }
return hex(md51(str));
/* eslint-enable */
}
function buildAmapQuery(params) {
const clean = {}
Object.keys(params || {}).forEach((k) => {
const v = params[k]
if (v === undefined || v === null || v === '') return
clean[k] = String(v)
})
// 必须包含 key
if (!clean.key) clean.key = AMAP_KEY
const keys = Object.keys(clean).sort()
const query = keys.map((k) => `${k}=${encodeURIComponent(clean[k])}`).join('&')
const sig = md5(query + AMAP_SECRET)
return `${query}&sig=${sig}`
}
function amapGet(path, params) {
return new Promise((resolve, reject) => {
const query = buildAmapQuery(params)
uni.request({
url: `${AMAP_BASE_URL}${path}?${query}`,
method: 'GET',
header: { 'content-type': 'application/json' },
success: (res) => {
const data = res && res.data
// 高德 WebServicestatus '1' 表示成功
if (data && String(data.status) === '1') {
resolve(data)
} else {
reject(data || { status: '0', info: '请求失败' })
}
},
fail: (err) => reject(err)
})
})
}
// #endif
// 腾讯地图Key // 腾讯地图Key
const QQMAP_KEY = const QQMAP_KEY =
// #ifdef H5 // #ifdef H5
@@ -566,8 +766,15 @@ function getUserLocation() {
wx.getLocation({ wx.getLocation({
type: 'gcj02', type: 'gcj02',
success: (res) => { success: (res) => {
const longitude = parseFloat(res.longitude.toFixed(5)); const longitude = toFixedNumber(res.longitude, 5)
const latitude = parseFloat(res.latitude.toFixed(5)); const latitude = toFixedNumber(res.latitude, 5)
if (longitude === null || latitude === null) {
reject({
code: 'INVALID_COORD',
errMsg: 'invalid longitude/latitude from getLocation'
})
return
}
console.log('地址获取成功'); console.log('地址获取成功');
resolve({ resolve({
longitude, longitude,
@@ -612,8 +819,15 @@ function getUserLocation() {
wx.getLocation({ wx.getLocation({
type: 'gcj02', type: 'gcj02',
success: (res) => { success: (res) => {
const longitude = parseFloat(res.longitude.toFixed(5)); const longitude = toFixedNumber(res.longitude, 5)
const latitude = parseFloat(res.latitude.toFixed(5)); const latitude = toFixedNumber(res.latitude, 5)
if (longitude === null || latitude === null) {
reject({
code: 'INVALID_COORD',
errMsg: 'invalid longitude/latitude from getLocation'
})
return
}
console.log('地址获取成功'); console.log('地址获取成功');
resolve({ resolve({
longitude, longitude,
@@ -635,8 +849,15 @@ function getUserLocation() {
uni.getLocation({ uni.getLocation({
type: 'gcj02', type: 'gcj02',
success: (res) => { success: (res) => {
const longitude = parseFloat(res.longitude.toFixed(5)); const longitude = toFixedNumber(res.longitude, 5)
const latitude = parseFloat(res.latitude.toFixed(5)); const latitude = toFixedNumber(res.latitude, 5)
if (longitude === null || latitude === null) {
reject({
code: 'INVALID_COORD',
errMsg: 'invalid longitude/latitude from getLocation'
})
return
}
console.log('地址获取成功'); console.log('地址获取成功');
resolve({ resolve({
longitude, longitude,
@@ -665,6 +886,41 @@ function getUserLocation() {
// 逆地理编码 - 根据经纬度获取地址信息 // 逆地理编码 - 根据经纬度获取地址信息
function getRegeo(longitude, latitude) { function getRegeo(longitude, latitude) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// #ifdef MP-ALIPAY
// 支付宝小程序:高德逆地理编码
const lng = toFixedNumber(longitude, 6)
const lat = toFixedNumber(latitude, 6)
if (lng === null || lat === null) {
reject({ success: false, message: '无效经纬度' })
return
}
amapGet('geocode/regeo', {
location: `${lng},${lat}`,
radius: 1000,
extensions: 'base'
}).then((data) => {
const regeocode = data.regeocode || {}
const ac = regeocode.addressComponent || {}
resolve({
success: true,
data: {
formatted_address: regeocode.formatted_address || '',
addressComponent: {
city: Array.isArray(ac.city) ? '' : (ac.city || ''),
district: ac.district || '',
province: ac.province || '',
street: (ac.streetNumber && ac.streetNumber.street) || '',
street_number: (ac.streetNumber && ac.streetNumber.number) || ''
}
}
})
}).catch((err) => {
console.error('支付宝-高德逆地理编码失败:', err)
reject({ success: false, message: err.info || err.message || '逆地理编码失败' })
})
return
// #endif
// #ifdef H5 // #ifdef H5
// H5环境:使用JSONP方式调用腾讯地图API,避免跨域问题 // H5环境:使用JSONP方式调用腾讯地图API,避免跨域问题
const callbackName = `qqmap_geocoder_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const callbackName = `qqmap_geocoder_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -756,6 +1012,50 @@ function getRegeo(longitude, latitude) {
// 搜索周边POI // 搜索周边POI
function getPoiAround(longitude, latitude, keyword = '', radius = 1000) { function getPoiAround(longitude, latitude, keyword = '', radius = 1000) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// #ifdef MP-ALIPAY
// 支付宝小程序:高德周边检索
const lng = toFixedNumber(longitude, 6)
const lat = toFixedNumber(latitude, 6)
if (lng === null || lat === null) {
reject({ success: false, message: '无效经纬度' })
return
}
amapGet('place/around', {
location: `${lng},${lat}`,
keywords: keyword || '',
radius: radius || 1000,
sortrule: 'distance',
offset: 10,
page: 1,
extensions: 'base'
}).then((data) => {
const pois = Array.isArray(data.pois) ? data.pois : []
const list = pois.map((p) => {
const loc = (p.location || '').split(',')
const plng = toFixedNumber(loc[0], 6)
const plat = toFixedNumber(loc[1], 6)
return {
id: p.id || null,
title: p.name || null,
latitude: plat,
longitude: plng,
address: p.address || null,
category: p.type || null,
tel: p.tel || null,
adcode: p.adcode || null,
city: p.cityname || null,
district: p.adname || null,
province: p.pname || null
}
})
resolve({ success: true, data: list })
}).catch((err) => {
console.error('支付宝-高德搜索POI失败:', err)
reject({ success: false, message: err.info || err.message || '搜索POI失败' })
})
return
// #endif
// #ifdef H5 // #ifdef H5
// H5环境:使用JSONP方式调用腾讯地图API // H5环境:使用JSONP方式调用腾讯地图API
const callbackName = `qqmap_search_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const callbackName = `qqmap_search_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -843,6 +1143,48 @@ function getPoiAround(longitude, latitude, keyword = '', radius = 1000) {
// 计算距离(异步) // 计算距离(异步)
function calculateDistance(from, to) { function calculateDistance(from, to) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// #ifdef MP-ALIPAY
// 支付宝小程序:高德距离计算(支持多个目的地)
if (!from || !to) {
reject({ success: false, message: '参数缺失' })
return
}
const fromLng = toFixedNumber((from && (from.longitude !== undefined ? from.longitude : from.lng)), 6)
const fromLat = toFixedNumber((from && (from.latitude !== undefined ? from.latitude : from.lat)), 6)
if (fromLng === null || fromLat === null) {
reject({ success: false, message: '无效起点坐标' })
return
}
const toArr = Array.isArray(to) ? to : [to]
const origins = toArr.map((p) => {
const lng = toFixedNumber((p && (p.longitude !== undefined ? p.longitude : p.lng)), 6)
const lat = toFixedNumber((p && (p.latitude !== undefined ? p.latitude : p.lat)), 6)
return (lng === null || lat === null) ? null : `${lng},${lat}`
}).filter(Boolean)
if (!origins.length) {
reject({ success: false, message: '无效终点坐标' })
return
}
// 高德接口:origins(多个) + destination(单个)
amapGet('distance', {
origins: origins.join('|'),
destination: `${fromLng},${fromLat}`,
type: 0 // 0:驾车距离;步行/骑行需要其它接口,这里保持与原来“直线/近似”用途一致
}).then((data) => {
const results = Array.isArray(data.results) ? data.results : []
const distances = results.map((r) => Number(r.distance)).filter((n) => Number.isFinite(n))
// 保持与原实现一致:始终返回数组(即使只传了一个目的地)
resolve({ success: true, data: distances })
}).catch((err) => {
console.error('支付宝-高德计算距离失败:', err)
reject({ success: false, message: err.info || err.message || '计算距离失败' })
})
return
// #endif
// #ifdef H5 // #ifdef H5
// H5环境:使用JSONP方式调用腾讯地图API // H5环境:使用JSONP方式调用腾讯地图API
const callbackName = `qqmap_distance_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const callbackName = `qqmap_distance_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -935,6 +1277,44 @@ function calculateDistanceSync(lat1, lng1, lat2, lng2) {
// 关键词提示 // 关键词提示
function getSuggestion(keyword, region = '全国') { function getSuggestion(keyword, region = '全国') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// #ifdef MP-ALIPAY
// 支付宝小程序:高德输入提示
if (!keyword) {
resolve({ success: true, data: [] })
return
}
amapGet('assistant/inputtips', {
keywords: keyword,
city: region && region !== '全国' ? region : '',
citylimit: region && region !== '全国' ? 1 : 0
}).then((data) => {
const tips = Array.isArray(data.tips) ? data.tips : []
const list = tips.map((t) => {
const loc = (t.location || '').split(',')
const lng = toFixedNumber(loc[0], 6)
const lat = toFixedNumber(loc[1], 6)
return {
adcode: t.adcode || null,
address: t.address || null,
category: t.type || null,
city: t.cityname || null,
district: t.district || null,
id: t.id || null,
latitude: lat,
longitude: lng,
province: t.pname || null,
title: t.name || null,
type: t.type || null
}
})
resolve({ success: true, data: list })
}).catch((err) => {
console.error('支付宝-高德关键词提示失败:', err)
reject({ success: false, message: err.info || err.message || '关键词提示失败' })
})
return
// #endif
// #ifdef H5 // #ifdef H5
// H5环境:使用JSONP方式调用腾讯地图API // H5环境:使用JSONP方式调用腾讯地图API
const callbackName = `qqmap_suggestion_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const callbackName = `qqmap_suggestion_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;