1181 lines
29 KiB
Vue
1181 lines
29 KiB
Vue
<template>
|
||
<view class="container">
|
||
<!-- 骨架屏 -->
|
||
<DeviceDetailSkeleton v-if="loading&&!deviceInfo" />
|
||
|
||
<!-- 实际内容 -->
|
||
<view v-else>
|
||
<!-- 设备信息卡片 -->
|
||
<view class="card device-info-card">
|
||
<view class="device-location">
|
||
<view class="location-left">
|
||
<image src="/static/device_location.png" mode="aspectFit" class="location-icon" lazy-load="true"></image>
|
||
<text class="location-name">{{ deviceLocation }}</text>
|
||
</view>
|
||
<view class="device-status" :class="deviceStatus.class">
|
||
<text class="status-text">{{ deviceStatus.text }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="device-id">
|
||
<text class="id-label">{{ $t('order.deviceName') }}:</text>
|
||
<text class="id-value">{{ deviceInfo.name }}</text>
|
||
</view>
|
||
<view class="device-id">
|
||
<text class="id-label">{{ $t('device.deviceNo') }}:</text>
|
||
<text class="id-value">{{ deviceId }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 计费规则 -->
|
||
<view class="card pricing-card">
|
||
<view class="card-header">
|
||
<text class="card-title">{{ $t('device.pricingRules') }}</text>
|
||
</view>
|
||
|
||
<view class="pricing-banner">
|
||
<view class="pricing-main">
|
||
<text class="price-symbol">¥</text>
|
||
<text class="price">{{ deviceFeeConfig.maxHourPrice || '5.00' }}</text>
|
||
<text class="unit">/{{ getPriceUnit() }}</text>
|
||
</view>
|
||
<view class="cap-badge">
|
||
<text class="cap-text">{{ deviceInfo.depositAmount || '99' }}{{ $t('device.capLimit') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="pricing-info">
|
||
<view class="info-icon">
|
||
<text class="icon-text">ⓘ</text>
|
||
</view>
|
||
<text class="info-text">{{ getPricingInfoText() }}</text>
|
||
</view>
|
||
<view class="pricing-info">
|
||
<view class="info-icon">
|
||
<text class="icon-text">ⓘ</text>
|
||
</view>
|
||
<text class="info-text">{{ getDetailInfoText() }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 使用说明 -->
|
||
<view class="card notice-card">
|
||
<view class="card-header">
|
||
<text class="card-title">{{ $t('device.usageInstructions') }}</text>
|
||
</view>
|
||
<view class="notice-items">
|
||
<view class="notice-item">
|
||
<view class="notice-dot"></view>
|
||
<text class="notice-text">{{ $t('device.checkBeforeUse') }}</text>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-dot"></view>
|
||
<text class="notice-text">{{ $t('device.autoChargeOvertime') }}</text>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-dot"></view>
|
||
<text class="notice-text">{{ $t('device.useInDesignatedArea') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="promotion-tip" @click="goToPurchase">
|
||
<view class="tip-left">
|
||
<text class="tip-text">{{ $t('device.canUsePromotion') }}</text>
|
||
</view>
|
||
<view class="tip-right">
|
||
<text class="buy-text">{{ $t('device.goToBuy') }}</text>
|
||
<image src="/static/gotoBuy.png" mode="aspectFit" class="arrow-icon"></image>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部操作区 -->
|
||
<view class="footer">
|
||
<view class="rent-button" :class="{ 'return-button': hasActiveOrder }"
|
||
@click="handleRent">
|
||
<text>{{ hasActiveOrder ? $t('order.returnDevice') : getRentButtonText() }}</text>
|
||
</view>
|
||
<!-- 微信支付分标识仅在微信小程序环境显示 -->
|
||
<view class="wechat-credit" v-if="isWechatMiniProgram">
|
||
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="wx-icon" lazy-load="true"></image>
|
||
<text class="credit-text">{{ $t('device.wxPayScoreDesc') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 手机号授权弹窗 -->
|
||
<view class="phone-auth-popup" v-if="showPhoneAuthPopup">
|
||
<view class="popup-mask" @click.stop></view>
|
||
<view class="popup-content">
|
||
<view class="popup-header">
|
||
<text class="popup-title">{{ $t('auth.authTitle') }}</text>
|
||
</view>
|
||
<view class="popup-body">
|
||
<view class="auth-desc">
|
||
<text>{{ $t('auth.authDescShort') }}</text>
|
||
</view>
|
||
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
|
||
{{ $t('auth.getPhoneNumber') }}
|
||
</button>
|
||
<view class="auth-cancel" @click="showPhoneAuthPopup = false">
|
||
<text>{{ $t('auth.notNow') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
ref,
|
||
reactive,
|
||
onMounted
|
||
} from 'vue'
|
||
import {
|
||
onLoad,
|
||
onUnload
|
||
} from '@dcloudio/uni-app'
|
||
import {
|
||
getDeviceInfo,
|
||
rentPowerBank
|
||
} from '@/config/api/device.js'
|
||
import {
|
||
getOrderByOrderNoScore,
|
||
getOrderByOrderNo,
|
||
cancelOrder,
|
||
getInUseOrder,
|
||
getUnpaidOrder
|
||
} from '@/config/api/order.js'
|
||
import {
|
||
initiateWeChatScorePayment,
|
||
getUserInfo,
|
||
getUserPhoneNumber
|
||
} from '@/util/index.js'
|
||
import {
|
||
useI18n
|
||
} from '@/utils/i18n.js'
|
||
import DeviceDetailSkeleton from '@/components/DeviceDetailSkeleton.vue'
|
||
|
||
const {
|
||
t
|
||
} = useI18n()
|
||
|
||
// 响应式状态
|
||
const loading = ref(true)
|
||
const deviceInfo = ref({})
|
||
const deviceId = ref('')
|
||
const deviceFeeConfig = ref({})
|
||
const positionInfo = ref({})
|
||
const deviceLocation = ref('一号教学楼大厅')
|
||
const hasActiveOrder = ref(false)
|
||
const deviceStatus = reactive({
|
||
text: t('device.available'),
|
||
class: 'available'
|
||
})
|
||
const isLoggedIn = ref(true)
|
||
const phoneNumber = ref('')
|
||
const showPhoneAuthPopup = ref(false)
|
||
const isWechatMiniProgram = ref(false)
|
||
const isAlipayMiniProgram = ref(false)
|
||
const isH5 = ref(false)
|
||
|
||
// 生命周期 onLoad 钩子
|
||
onLoad(async (options) => {
|
||
console.log('options', options)
|
||
|
||
// 普通链接二维码进入时,参数通常在 options.q(且为编码后的完整 URL)
|
||
if (!options.deviceNo && options.q) {
|
||
const fullUrl = decodeURIComponent(options.q)
|
||
const queryStr = fullUrl.includes('?') ? fullUrl.split('?')[1] : ''
|
||
if (queryStr) {
|
||
const params = queryStr.split('&').reduce((acc, pair) => {
|
||
if (!pair) return acc
|
||
const idx = pair.indexOf('=')
|
||
const rawKey = idx >= 0 ? pair.slice(0, idx) : pair
|
||
const rawVal = idx >= 0 ? pair.slice(idx + 1) : ''
|
||
const key = decodeURIComponent(rawKey || '').trim()
|
||
const val = decodeURIComponent(rawVal || '').trim()
|
||
if (key) acc[key] = val
|
||
return acc
|
||
}, {})
|
||
if (params.deviceNo) options.deviceNo = params.deviceNo
|
||
}
|
||
}
|
||
|
||
if (options.deviceNo != uni.getStorageSync('deviceId') || !uni.getStorageSync('deviceId')) {
|
||
deviceId.value = options.deviceNo
|
||
uni.setStorageSync('deviceId', options.deviceNo)
|
||
} else {
|
||
deviceId.value = uni.getStorageSync('deviceId')
|
||
}
|
||
await checkOrderStatus()
|
||
await fetchDeviceInfo()
|
||
})
|
||
|
||
onMounted(async () => {
|
||
uni.setNavigationBarTitle({
|
||
title: t('device.deviceInfo')
|
||
})
|
||
// 检测当前运行环境:微信小程序 / 支付宝小程序 / H5
|
||
// #ifdef MP-WEIXIN
|
||
isWechatMiniProgram.value = true
|
||
isAlipayMiniProgram.value = false
|
||
isH5.value = false
|
||
// #endif
|
||
// #ifdef MP-ALIPAY
|
||
isWechatMiniProgram.value = false
|
||
isAlipayMiniProgram.value = true
|
||
isH5.value = false
|
||
// #endif
|
||
// #ifdef H5
|
||
isWechatMiniProgram.value = false
|
||
isAlipayMiniProgram.value = false
|
||
isH5.value = true
|
||
// #endif
|
||
console.log('当前运行环境:', {
|
||
isWechatMiniProgram: isWechatMiniProgram.value,
|
||
isAlipayMiniProgram: isAlipayMiniProgram.value,
|
||
isH5: isH5.value
|
||
})
|
||
await checkUserPhone()
|
||
await fetchDeviceInfo()
|
||
})
|
||
|
||
// 页面卸载时设置默认启动路径为首页(仅在非下单流程时生效)
|
||
onUnload(() => {
|
||
// 如果是下单流程跳转(在提交订单时设置了标记),则本次不设置启动路径
|
||
const skipSetLaunchPathOnce = uni.getStorageSync('skipSetLaunchPathOnce')
|
||
if (skipSetLaunchPathOnce) {
|
||
console.log('下单流程离开设备详情页,本次不设置启动路径')
|
||
uni.removeStorageSync('skipSetLaunchPathOnce')
|
||
return
|
||
}
|
||
|
||
// 正常离开设备详情页(比如返回、关闭小程序)时,记录启动路径为首页
|
||
uni.setStorageSync('launchPath', '/pages/index/index')
|
||
console.log('设备详情页卸载,已设置启动路径为首页')
|
||
})
|
||
|
||
const checkUserPhone = async () => {
|
||
try {
|
||
const userInfoRes = await getUserInfo()
|
||
console.log(userInfoRes.data.phone, 'getUserInfoPhone')
|
||
|
||
if (userInfoRes.code == 200 && userInfoRes.data && userInfoRes.data.phone) {
|
||
phoneNumber.value = userInfoRes.data.phone
|
||
} else {
|
||
// 如果没有手机号,显示授权弹窗
|
||
showPhoneAuthPopup.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户信息失败:', error)
|
||
}
|
||
}
|
||
|
||
// 处理获取手机号
|
||
const onGetPhoneNumber = (e) => {
|
||
console.log('getPhoneNumber event:', e.detail)
|
||
|
||
// 用户拒绝授权的情况
|
||
if (e.detail.errMsg && e.detail.errMsg.includes('deny')) {
|
||
uni.showToast({
|
||
title: t('auth.phoneRequired'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 获取到授权code
|
||
if (e.detail.code) {
|
||
// uni.showLoading({
|
||
// title: t('auth.getting')
|
||
// })
|
||
|
||
console.log('获取到的授权code:', e.detail.code)
|
||
|
||
// 添加 try-catch 以捕获任何 Promise 外部的错误
|
||
try {
|
||
getUserPhoneNumber(e.detail.code)
|
||
.then(res => {
|
||
console.log('获取手机号API响应原始数据:', JSON.stringify(res))
|
||
// uni.hideLoading()
|
||
|
||
// 不立即抛出错误,而是记录问题并继续处理
|
||
if (!res) {
|
||
console.error('API返回数据为空')
|
||
uni.showModal({
|
||
title: '数据异常',
|
||
content: 'API返回为空',
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
|
||
// 检查响应格式
|
||
console.log('响应code:', res.code, '响应类型:', typeof res.code)
|
||
console.log('是否有data:', !!res.data, '是否有phone:', res.data && !!res.data.phone)
|
||
|
||
if (res.code == 200 && res.data && res.data.phoneNumber) {
|
||
phoneNumber.value = res.data.phoneNumber
|
||
showPhoneAuthPopup.value = false
|
||
|
||
uni.showToast({
|
||
title: t('auth.phoneSuccess'),
|
||
icon: 'success'
|
||
})
|
||
} else {
|
||
// 记录详细信息,不抛出错误
|
||
console.warn('获取手机号响应异常:', res.msg || '未知错误')
|
||
uni.showModal({
|
||
title: t('auth.phoneError'),
|
||
content: `${t('common.statusCode')}: ${res.code}, ${t('common.message')}: ${res.msg || t('common.none')}`,
|
||
showCancel: false
|
||
})
|
||
}
|
||
})
|
||
.catch(err => {
|
||
// uni.hideLoading()
|
||
console.error('获取手机号码失败(catch):', err)
|
||
|
||
// 显示更详细的错误信息
|
||
let errMsg = err.message || err.toString()
|
||
uni.showModal({
|
||
title: t('auth.phoneGetFailed'),
|
||
content: t('common.errorInfo') + ': ' + errMsg,
|
||
showCancel: false
|
||
})
|
||
})
|
||
} catch (outerError) {
|
||
// uni.hideLoading()
|
||
console.error('获取手机号外部错误:', outerError)
|
||
uni.showModal({
|
||
title: t('common.unexpectedError'),
|
||
content: t('common.processException') + ': ' + (outerError.message || outerError),
|
||
showCancel: false
|
||
})
|
||
}
|
||
} else {
|
||
uni.showToast({
|
||
title: t('auth.authCodeFailed'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 检查登录状态和订单
|
||
const fetchDeviceInfo = async () => {
|
||
try {
|
||
loading.value = true
|
||
console.log(deviceId.value);
|
||
const res = await getDeviceInfo(deviceId.value)
|
||
if (res.code == 200) {
|
||
deviceInfo.value = res.data.device || {}
|
||
|
||
// 保存 position 信息
|
||
if (res.data.position) {
|
||
positionInfo.value = res.data.position
|
||
}
|
||
|
||
// 更新设备位置信息
|
||
if (deviceInfo.value.deviceLocation) {
|
||
deviceLocation.value = deviceInfo.value.deviceLocation
|
||
} else if (res.data.position && res.data.position.name) {
|
||
deviceLocation.value = res.data.position.name
|
||
}
|
||
|
||
|
||
// 更新设备状态
|
||
if (deviceInfo.value.status) {
|
||
if (deviceInfo.value.status === 'online') {
|
||
deviceStatus.text = t('device.available')
|
||
deviceStatus.class = 'available'
|
||
} else if (deviceInfo.value.status === 'offline') {
|
||
deviceStatus.text = t('device.offline')
|
||
deviceStatus.class = 'offline'
|
||
}
|
||
}
|
||
|
||
if (deviceInfo.value.feeConfig) {
|
||
deviceFeeConfig.value = JSON.parse(deviceInfo.value.feeConfig)[0] || {}
|
||
console.log('deviceFeeConfig', deviceFeeConfig.value);
|
||
} else {
|
||
deviceFeeConfig.value = {
|
||
maxHourPrice: '5.00',
|
||
}
|
||
}
|
||
}else{
|
||
uni.reLaunch({
|
||
url:'/pages/index/index'
|
||
})
|
||
}
|
||
}catch(error){
|
||
console.error('获取设备信息失败:', error)
|
||
}
|
||
finally {
|
||
|
||
}
|
||
}
|
||
|
||
// 显示登录提示
|
||
const showLoginTip = () => {
|
||
uni.showModal({
|
||
title: t('common.tips'),
|
||
content: t('common.loginRequired'),
|
||
confirmText: t('auth.goToLogin'),
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateTo({
|
||
url: '/subPackages/user/login/index'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 跳转到优惠专区
|
||
const goToPurchase = () => {
|
||
const positionId = positionInfo.value?.positionId || positionInfo.value?.id
|
||
uni.navigateTo({
|
||
url: `/subPackages/business/purchase/index?positionId=${positionId}`
|
||
})
|
||
}
|
||
|
||
// 检查订单状态
|
||
const checkOrderStatus = async () => {
|
||
try {
|
||
// 调用接口检查是否有进行中的订单
|
||
const inUseRes = await getInUseOrder()
|
||
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
|
||
const order = inUseRes.data
|
||
// 如果有正在进行的订单,跳转到归还页面,带上设备ID
|
||
uni.redirectTo({
|
||
url: `/subPackages/service/return/index?deviceId=${deviceId.value}`
|
||
})
|
||
return
|
||
}
|
||
|
||
// 检查是否有待支付的订单
|
||
const unpaidRes = await getUnpaidOrder()
|
||
if (unpaidRes && unpaidRes.code === 200 && unpaidRes.data) {
|
||
const order = unpaidRes.data
|
||
// 跳转支付页面,带上订单ID
|
||
uni.redirectTo({
|
||
url: `/pages/order/payment?orderId=${order.orderId}&deviceId=${deviceId.value}`
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('检查订单状态失败:', error)
|
||
uni.showToast({
|
||
title: t('order.getOrderStatusFailed'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 处理租借操作
|
||
const handleRent = () => {
|
||
if (!isLoggedIn.value) {
|
||
showLoginTip()
|
||
return
|
||
}
|
||
|
||
// 检查是否有手机号,如果没有则提示授权
|
||
if (!phoneNumber.value) {
|
||
showPhoneAuthPopup.value = true
|
||
return
|
||
}
|
||
|
||
// 根据运行环境选择不同的租借/支付流程
|
||
// 微信小程序:走微信支付分免押租借
|
||
if (isWechatMiniProgram.value) {
|
||
submitRentOrder('wx-score-pay')
|
||
return
|
||
}
|
||
|
||
// 支付宝小程序:走押金租借,后续在支付页内调起支付宝支付
|
||
if (isAlipayMiniProgram.value) {
|
||
submitRentOrder('wx-pay')
|
||
return
|
||
}
|
||
|
||
// H5 等其他环境:统一走押金租借,支付页内根据平台选择支付方式(Antom 等)
|
||
submitRentOrder('wx-pay')
|
||
}
|
||
|
||
// 获取价格单位文本
|
||
const getPriceUnit = () => {
|
||
console.log(deviceInfo.value);
|
||
// 按分钟计费
|
||
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
|
||
return '分钟'
|
||
} else if (deviceInfo.value && deviceFeeConfig.value.hourPrice == '0.5') {
|
||
return '30分钟'
|
||
}
|
||
// 按小时计费(默认)
|
||
return t('time.hour')
|
||
}
|
||
|
||
// 计算计费单位时间(分钟)
|
||
const getBillingUnitMinutes = () => {
|
||
// 按分钟计费时,单位为1分钟
|
||
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
|
||
return 1
|
||
}
|
||
// 按小时计费
|
||
if (!deviceFeeConfig.value || !deviceFeeConfig.value.hourPrice) return 60
|
||
const hourPrice = parseFloat(deviceFeeConfig.value.hourPrice)
|
||
// hourPrice 为 0.5 时表示 30 分钟,为 1 时表示 60 分钟
|
||
return hourPrice === 0.5 ? 30 : 60
|
||
}
|
||
|
||
// 计算每个计费单位的价格
|
||
const getBillingUnitPrice = () => {
|
||
if (!deviceFeeConfig.value || !deviceFeeConfig.value.maxHourPrice) return '5'
|
||
const maxHourPrice = parseFloat(deviceFeeConfig.value.maxHourPrice)
|
||
|
||
// 按分钟计费时,直接返回每分钟价格
|
||
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
|
||
return maxHourPrice.toFixed(2)
|
||
}
|
||
|
||
// 按小时计费
|
||
const hourPrice = parseFloat(deviceFeeConfig.value.hourPrice || 1)
|
||
const unitPrice = maxHourPrice
|
||
return unitPrice.toFixed(2)
|
||
}
|
||
|
||
// 获取免费时间(分钟)
|
||
const getFreeMinutes = () => {
|
||
if (!positionInfo.value || !positionInfo.value.freeRentTime) return 15
|
||
return parseInt(positionInfo.value.freeRentTime)
|
||
}
|
||
|
||
// 生成计费说明文本
|
||
const getPricingInfoText = () => {
|
||
const unitPrice = getBillingUnitPrice()
|
||
const maxHourPrice = deviceFeeConfig.value.maxHourPrice || '5'
|
||
|
||
// 按分钟计费
|
||
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
|
||
return `${unitPrice}元/分钟`
|
||
}
|
||
|
||
// 按小时计费
|
||
const unitMinutes = getBillingUnitMinutes()
|
||
return deviceInfo.value.remark;
|
||
}
|
||
|
||
// 生成详细说明文本
|
||
const getDetailInfoText = () => {
|
||
const freeMinutes = getFreeMinutes()
|
||
const unitMinutes = getBillingUnitMinutes()
|
||
const depositAmount = deviceInfo.value.depositAmount || '99'
|
||
|
||
// 按分钟计费
|
||
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
|
||
return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
|
||
}
|
||
|
||
// 按小时计费
|
||
return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
|
||
}
|
||
|
||
// 获取租借按钮文本
|
||
const getRentButtonText = () => {
|
||
if (isWechatMiniProgram.value) {
|
||
return t('device.rentDepositFree')
|
||
} else {
|
||
return '立即租借'
|
||
}
|
||
}
|
||
|
||
// 提交租借订单
|
||
const submitRentOrder = async (payWay) => {
|
||
try {
|
||
uni.showLoading({
|
||
title: t('common.processing')
|
||
})
|
||
// --- 第一步:先请求订阅消息(必须在用户点击的同步上下文中)---
|
||
if (payWay === 'wx-score-pay') {
|
||
console.log('准备请求订阅消息(在异步操作之前),时间:', new Date().toLocaleTimeString());
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
uni.requestSubscribeMessage({
|
||
tmplIds: ['o7OMTIcHnFBR7mvsggxFtdt8FfIgSl-v0swVUefGx6w'],
|
||
success: (subscribeRes) => {
|
||
console.log('订阅消息success回调,时间:', new Date()
|
||
.toLocaleTimeString(), subscribeRes);
|
||
resolve(subscribeRes);
|
||
},
|
||
fail: (subscribeErr) => {
|
||
console.log('订阅消息fail回调,时间:', new Date().toLocaleTimeString(),
|
||
subscribeErr);
|
||
// 订阅失败不影响主流程
|
||
resolve(subscribeErr);
|
||
}
|
||
});
|
||
});
|
||
console.log('订阅消息完成,时间:', new Date().toLocaleTimeString());
|
||
} catch (subscribeError) {
|
||
console.log('订阅消息异常', subscribeError);
|
||
}
|
||
}
|
||
// --- 订阅消息请求完成 ---
|
||
|
||
console.log(deviceId.value);
|
||
// 调用设备租借接口
|
||
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value,payWay.value)
|
||
if (rentResult.code !== 200) {
|
||
throw new Error(rentResult.msg || t('device.rentFailed'))
|
||
}
|
||
|
||
// 获取后端返回的订单信息
|
||
const order = rentResult.data
|
||
console.log('订单信息', order);
|
||
|
||
// 标记:本次是从设备详情页发起的下单流程,离开页面时不设置启动路径
|
||
try {
|
||
uni.setStorageSync('skipSetLaunchPathOnce', true)
|
||
} catch (e) {
|
||
console.warn('设置 skipSetLaunchPathOnce 失败:', e)
|
||
}
|
||
|
||
if (payWay == 'wx-pay') {
|
||
// 当支付方式为押金支付时
|
||
uni.hideLoading()
|
||
const res = await getOrderByOrderNo(order.orderNo);
|
||
console.log(res);
|
||
|
||
const deposit = parseFloat(order.depositAmount);
|
||
const packagePrice = parseFloat(order.unitPrice);
|
||
const totalAmount = deposit.toFixed(2);
|
||
|
||
// 跳转到订单支付页面
|
||
uni.redirectTo({
|
||
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') {
|
||
// 当支付方式为支付分支付时
|
||
uni.hideLoading()
|
||
// 获取支付分所需参数
|
||
const res = await getOrderByOrderNoScore(order.orderNo);
|
||
uni.hideLoading()
|
||
|
||
if (res && res.code === 200) {
|
||
try {
|
||
// 调用微信支付分小程序
|
||
const payResult = await initiateWeChatScorePayment(res);
|
||
console.log('支付分调用结果', payResult);
|
||
// 成功则跳转到等待页面
|
||
if (payResult.errCode == '0' && payResult.extraData && Object.keys(payResult.extraData)
|
||
.length > 0) {
|
||
console.log('支付分授权成功,准备跳转到等待页,时间:', new Date().toLocaleTimeString());
|
||
// 跳转到等待页面(订阅消息已经在前面完成了)
|
||
uni.redirectTo({
|
||
url: `/pages/waiting/index?orderNo=${order.orderNo}&orderId=${order.orderId}&deviceId=${deviceId.value}`
|
||
});
|
||
return;
|
||
} else {
|
||
console.log('支付分未完成授权或用户取消,extraData:', payResult.extraData);
|
||
// 用户取消授权,需要取消订单
|
||
try {
|
||
uni.showLoading({
|
||
title: t('order.cancelling')
|
||
});
|
||
const cancelRes = await cancelOrder({
|
||
orderId: order.orderNo
|
||
});
|
||
console.log('订单取消结果:', cancelRes);
|
||
uni.hideLoading();
|
||
|
||
uni.showToast({
|
||
title: t('order.orderCancelled'),
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
|
||
// 延迟返回首页
|
||
setTimeout(() => {
|
||
uni.switchTab({
|
||
url: '/pages/index/index'
|
||
});
|
||
}, 2000);
|
||
} catch (cancelError) {
|
||
console.error('取消订单失败:', cancelError);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: t('order.cancelFailedContactService'),
|
||
icon: 'none'
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
} catch (payError) {
|
||
console.error('支付分调用异常:', payError);
|
||
// 支付分调用异常,也需要取消订单
|
||
try {
|
||
uni.showLoading({
|
||
title: t('order.cancelling')
|
||
});
|
||
const cancelRes = await cancelOrder({
|
||
orderId: order.orderNo
|
||
});
|
||
console.log('订单取消结果:', cancelRes);
|
||
uni.hideLoading();
|
||
} catch (cancelError) {
|
||
console.error('取消订单失败:', cancelError);
|
||
uni.hideLoading();
|
||
}
|
||
|
||
uni.showToast({
|
||
title: t('device.payScoreFailedCancelled'),
|
||
icon: 'none'
|
||
});
|
||
|
||
setTimeout(() => {
|
||
uni.switchTab({
|
||
url: '/pages/index/index'
|
||
});
|
||
}, 2000);
|
||
}
|
||
} else {
|
||
uni.showToast({
|
||
title: res?.msg || t('device.getPayParamsFailed'),
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: error.message || t('device.rentFailedRetry'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.container {
|
||
min-height: 100vh;
|
||
background-color: #f5f7fa;
|
||
padding: 30rpx 30rpx 300rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
// 卡片通用样式
|
||
.card {
|
||
background-color: #fff;
|
||
border-radius: 24rpx;
|
||
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
|
||
padding: 30rpx;
|
||
margin-bottom: 30rpx;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24rpx;
|
||
|
||
.card-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 设备信息卡片
|
||
.device-info-card {
|
||
|
||
.device-location {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
|
||
.location-left {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.location-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
margin-right: 12rpx;
|
||
// background-color: #10d673;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.location-name {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.device-status {
|
||
padding: 8rpx 24rpx;
|
||
border-radius: 30rpx;
|
||
font-size: 22rpx;
|
||
|
||
&.available {
|
||
background-color: #d4f4dd;
|
||
|
||
.status-text {
|
||
color: #07c160;
|
||
}
|
||
}
|
||
|
||
&.offline {
|
||
background-color: #f0f0f0;
|
||
|
||
.status-text {
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.device-id {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 5rpx;
|
||
|
||
.id-label {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.id-value {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计费规则卡片
|
||
.pricing-card {
|
||
.pricing-banner {
|
||
background: #E6F7EC;
|
||
border-radius: 20rpx;
|
||
padding: 40rpx 30rpx;
|
||
margin-bottom: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
|
||
.pricing-main {
|
||
display: flex;
|
||
align-items: baseline;
|
||
margin-bottom: 16rpx;
|
||
|
||
.price-symbol {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #07c160;
|
||
margin-right: 4rpx;
|
||
}
|
||
|
||
.price {
|
||
font-size: 64rpx;
|
||
font-weight: bold;
|
||
color: #07c160;
|
||
line-height: 1;
|
||
}
|
||
|
||
.unit {
|
||
font-size: 28rpx;
|
||
color: #07c160;
|
||
margin-left: 8rpx;
|
||
}
|
||
}
|
||
|
||
.cap-badge {
|
||
background-color: #07c160;
|
||
padding: 10rpx 28rpx;
|
||
border-radius: 30rpx;
|
||
line-height: 1;
|
||
.cap-text {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pricing-info {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 16rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.info-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 12rpx;
|
||
margin-top: 2rpx;
|
||
|
||
.icon-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.info-text {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 使用说明卡片
|
||
.notice-card {
|
||
.notice-items {
|
||
.notice-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 20rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.notice-dot {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 50%;
|
||
background-color: #07c160;
|
||
margin-right: 16rpx;
|
||
margin-top: 10rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.notice-text {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 促销提示框
|
||
.promotion-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 30rpx;
|
||
background: rgba(255, 244, 227, 1);
|
||
border-radius: 22rpx;
|
||
margin-bottom: 30rpx;
|
||
transition: all 0.3s;
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.tip-left {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.tip-text {
|
||
font-size: 26rpx;
|
||
color: #A16300;
|
||
font-weight: 400;
|
||
}
|
||
}
|
||
|
||
.tip-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
|
||
.buy-text {
|
||
font-size: 26rpx;
|
||
color: #A16300;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.arrow-icon {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 底部操作区
|
||
.footer {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background-color: #fff;
|
||
padding: 24rpx 30rpx;
|
||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||
z-index: 100;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
|
||
.rent-button {
|
||
width: 100%;
|
||
height: 96rpx;
|
||
border-radius: 48rpx;
|
||
background: linear-gradient(135deg, #07c160, #10d673);
|
||
color: #fff;
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
box-shadow: 0 8rpx 20rpx rgba(7, 193, 96, 0.3);
|
||
|
||
&.return-button {
|
||
background: linear-gradient(135deg, #FF9800, #FFB74D);
|
||
box-shadow: 0 8rpx 20rpx rgba(255, 152, 0, 0.3);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
|
||
.wechat-credit {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 16rpx;
|
||
|
||
.wx-icon {
|
||
width: 48rpx;
|
||
height: 38rpx;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.credit-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 手机号授权弹窗样式 */
|
||
.phone-auth-popup {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.popup-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
}
|
||
|
||
.popup-content {
|
||
background-color: #fff;
|
||
border-radius: 24rpx;
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
|
||
width: 90%;
|
||
max-width: 500rpx;
|
||
padding: 40rpx 30rpx;
|
||
position: relative;
|
||
z-index: 1001;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.popup-header {
|
||
margin-bottom: 30rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.popup-body {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.auth-desc {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
text-align: center;
|
||
margin-bottom: 30rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.auth-btn {
|
||
width: 100%;
|
||
height: 92rpx;
|
||
border-radius: 46rpx;
|
||
background: linear-gradient(135deg, #07c160, #10d673);
|
||
color: #fff;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
margin-bottom: 20rpx;
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
|
||
.auth-cancel {
|
||
width: 100%;
|
||
height: 92rpx;
|
||
border-radius: 46rpx;
|
||
background-color: #f5f7fa;
|
||
color: #333;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
</style> |