Files
uni-fans-score/pages/device/detail.vue
T
2025-10-18 09:24:35 +08:00

951 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<!-- 设备信息卡片 -->
<view class="card device-info-card">
<view class="device-location">
<view class="location-left">
<image src="/static/images/location-map.svg" mode="aspectFit" class="location-icon"></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">设备号</text>
<text class="id-value">{{ deviceId }}</text>
</view>
</view>
<!-- 计费规则 -->
<view class="card pricing-card">
<view class="card-header">
<text class="card-title">计费规则</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">/小时</text>
</view>
<view class="cap-badge">
<text class="cap-text">{{ deviceInfo.depositAmount || '99' }}元封顶</text>
</view>
</view>
<view class="pricing-info">
<view class="info-icon">
<text class="icon-text"></text>
</view>
<text class="info-text">5/小时45/24小时</text>
</view>
<view class="pricing-info">
<view class="info-icon">
<text class="icon-text"></text>
</view>
<text class="info-text">前15分钟内归还免费不足60分钟按60分钟计费点总封顶99元持续计费至99元视为买断</text>
</view>
</view>
<!-- 使用说明 -->
<view class="card notice-card">
<view class="card-header">
<text class="card-title">使用说明</text>
</view>
<view class="notice-items">
<view class="notice-item">
<view class="notice-dot"></view>
<text class="notice-text">请在使用前检查设备是否完好</text>
</view>
<view class="notice-item">
<view class="notice-dot"></view>
<text class="notice-text">超出使用时间将自动按小时计费</text>
</view>
<view class="notice-item">
<view class="notice-dot"></view>
<text class="notice-text">请在指定区域内使用设备</text>
</view>
</view>
</view>
<!-- 底部操作区 -->
<view class="footer">
<button class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent('wx-score-pay')">
<text>{{ hasActiveOrder ? '归还设备' : '免押金租借' }}</text>
</button>
<view class="wechat-credit">
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="wx-icon"></image>
<text class="credit-text">微信支付分 <text class="divider">|</text> 550分以上优享</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">授权获取手机号</text>
</view>
<view class="popup-body">
<view class="auth-desc">
<text>为了提供更好的服务需要授权获取您的手机号</text>
</view>
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
一键获取手机号
</button>
<view class="auth-cancel" @click="showPhoneAuthPopup = false">
<text>暂不授权</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted
} from 'vue'
import {
onLoad,
onShow
} from '@dcloudio/uni-app'
import {
getDeviceInfo,
rentPowerBank
} from '@/config/api/device.js'
import {
getOrderByOrderNoScore,
getOrderByOrderNoScorePayStatus,
getOrderByOrderNo,
updateOrderPackage,
cancelOrder
} from '@/config/api/order.js'
import {
URL
} from "@/config/url.js"
import {
initiateWeChatScorePayment,
getUserInfo,
getUserPhoneNumber
} from '@/util/index.js'
// 响应式状态
const deviceInfo = ref({})
const deviceId = ref('')
const deviceFeeConfig = ref({})
const deviceLocation = ref('一号教学楼大厅')
const batteryLevel = ref(95)
const hasActiveOrder = ref(false)
const deviceStatus = reactive({
text: '可使用',
class: 'available'
})
const isLoggedIn = ref(true)
const phoneNumber = ref('')
const showPhoneAuthPopup = ref(false)
// 生命周期 onLoad 钩子
onLoad(async (options) => {
if (options.deviceNo != uni.getStorageSync('deviceId') || !uni.getStorageSync('deviceId')) {
deviceId.value = options.deviceNo
uni.setStorageSync('deviceId', options.deviceNo)
} else {
deviceId.value = uni.getStorageSync('deviceId')
// uni.removeStorageSync('deviceId')
}
await checkOrderStatus()
await fetchDeviceInfo()
})
onMounted(async () => {
await checkUserPhone()
await fetchDeviceInfo()
})
// onShow(async () => {
// await fetchDeviceInfo()
// })
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: '需要授权手机号才能使用设备',
icon: 'none'
})
return
}
// 获取到授权code
if (e.detail.code) {
uni.showLoading({
title: '获取中...'
})
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: '手机号获取成功',
icon: 'success'
})
} else {
// 记录详细信息,不抛出错误
console.warn('获取手机号响应异常:', res.msg || '未知错误')
uni.showModal({
title: '获取手机号异常',
content: `状态码: ${res.code}, 消息: ${res.msg || '无'}`,
showCancel: false
})
}
})
.catch(err => {
uni.hideLoading()
console.error('获取手机号码失败(catch):', err)
// 显示更详细的错误信息
let errMsg = err.message || err.toString()
uni.showModal({
title: '获取手机号失败',
content: '错误信息: ' + errMsg,
showCancel: false
})
})
} catch (outerError) {
uni.hideLoading()
console.error('获取手机号外部错误:', outerError)
uni.showModal({
title: '意外错误',
content: '处理过程发生异常: ' + (outerError.message || outerError),
showCancel: false
})
}
} else {
uni.showToast({
title: '获取授权码失败',
icon: 'none'
})
}
}
// 检查登录状态和订单
const fetchDeviceInfo = async () => {
const res = await getDeviceInfo(deviceId.value)
if (res.code == 200) {
deviceInfo.value = res.data.device || {}
// 更新设备位置信息
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 = '可使用'
deviceStatus.class = 'available'
} else if (deviceInfo.value.status === 'offline') {
deviceStatus.text = '离线'
deviceStatus.class = 'offline'
}
}
if (deviceInfo.value.feeConfig) {
deviceFeeConfig.value = JSON.parse(deviceInfo.value.feeConfig)[0] || {}
} else {
deviceFeeConfig.value = {
maxHourPrice: '5.00',
}
discount.value = '99.00'
}
}
}
// 显示登录提示
const showLoginTip = () => {
uni.showModal({
title: '提示',
content: '请先登录后再操作',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/login/index'
})
}
}
})
}
// 检查订单状态
const checkOrderStatus = async () => {
try {
// 调用接口检查是否有进行中的订单
const result = await uni.$api.checkActiveOrder()
if (result.hasOrder) {
const order = result.order // 假设后端返回 order 对象
// 检查订单状态
if (order.status === 'waiting_for_payment') {
// 跳转支付页面,带上订单ID
uni.redirectTo({
url: `/pages/order/payment?orderId=${order.orderId}&deviceId=${deviceId.value}`
})
} else if (order.status === 'in_used') {
// 如果有正在进行的订单,跳转到归还页面,带上设备ID
uni.redirectTo({
url: `/pages/device/return?deviceId=${deviceId.value}`
})
}
}
} catch (error) {
uni.showToast({
title: '订单状态查询失败',
icon: 'none'
})
}
}
// 处理租借操作
const handleRent = (payWay) => {
if (!isLoggedIn.value) {
showLoginTip()
return
}
// 检查是否有手机号,如果没有则提示授权
if (!phoneNumber.value) {
showPhoneAuthPopup.value = true
return
}
// 提交订单
submitRentOrder(payWay)
}
const selectedPkg = reactive({
time: '1小时',
price: '5.00'
})
const depositAmount = ref('99.00')
// 提交租借订单
const submitRentOrder = async (payWay) => {
try {
// --- 第一步:先请求订阅消息(必须在用户点击的同步上下文中)---
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);
}
}
// --- 订阅消息请求完成 ---
uni.showLoading({
title: '处理中'
})
console.log(deviceId.value);
// 调用设备租借接口
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value)
if (rentResult.code !== 200) {
throw new Error(rentResult.msg || '设备租借失败')
}
// 获取后端返回的订单信息
const order = rentResult.data
console.log('订单信息', order);
// // --- 统一:先更新订单套餐信息 ---
// try {
// let packageTimeMinutes = 0;
// if (selectedPkg.time.includes('小时')) {
// packageTimeMinutes = parseInt(selectedPkg.time) * 60;
// } else if (selectedPkg.time.includes('分钟')) {
// packageTimeMinutes = parseInt(selectedPkg.time);
// } else {
// packageTimeMinutes = parseInt(selectedPkg.time) * 60; // 默认按小时处理
// }
// const updateRes = await updateOrderPackage({
// orderId: order.orderId,
// packageTime: packageTimeMinutes,
// packagePrice: parseFloat(selectedPkg.price)
// });
// if (updateRes.code !== 200) {
// console.warn("更新订单套餐信息失败:", updateRes.msg);
// // 这里可以选择是否提示用户或阻止流程,当前不阻止
// } else {
// console.log("订单套餐信息已提前更新");
// }
// } catch (updateError) {
// console.error("更新订单套餐信息时出错:", updateError);
// // 即使更新失败,也继续流程
// }
// --- 套餐信息更新结束 ---
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.hideLoading()
// 跳转到订单支付页面,传递订单ID、套餐信息和总金额
uni.redirectTo({
url: `/pages/order/payment?orderId=${order.orderId}&packageTimeHours=${selectedPkg.time.replace('小时', '')}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${depositAmount.value}${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}&deviceId=${deviceId.value}`
});
return;
} else {
console.log('支付分未完成授权或用户取消,extraData:', payResult.extraData);
// 用户取消授权,需要取消订单
try {
uni.showLoading({
title: '取消订单中'
});
const cancelRes = await cancelOrder({ orderId: order.orderNo });
console.log('订单取消结果:', cancelRes);
uni.hideLoading();
uni.showToast({
title: '已取消订单',
icon: 'none',
duration: 2000
});
// 延迟返回首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
});
}, 2000);
} catch (cancelError) {
console.error('取消订单失败:', cancelError);
uni.hideLoading();
uni.showToast({
title: '取消订单失败,请联系客服',
icon: 'none'
});
}
return;
}
} catch (payError) {
console.error('支付分调用异常:', payError);
// 支付分调用异常,也需要取消订单
try {
uni.showLoading({
title: '取消订单中'
});
const cancelRes = await cancelOrder({ orderId: order.orderNo });
console.log('订单取消结果:', cancelRes);
uni.hideLoading();
} catch (cancelError) {
console.error('取消订单失败:', cancelError);
uni.hideLoading();
}
uni.showToast({
title: '支付分调用失败,订单已取消',
icon: 'none'
});
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
});
}, 2000);
}
} else {
uni.showToast({
title: res?.msg || '获取支付参数失败',
icon: 'none'
});
}
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '租借失败,请重试',
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: 40rpx;
height: 40rpx;
margin-right: 12rpx;
background-color: #10d673;
border-radius: 50%;
}
.location-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
}
.device-status {
padding: 8rpx 24rpx;
border-radius: 30rpx;
font-size: 24rpx;
&.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;
.id-label {
font-size: 28rpx;
color: #999;
}
.id-value {
font-size: 28rpx;
color: #666;
}
}
}
// 计费规则卡片
.pricing-card {
.pricing-banner {
background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
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: 48rpx;
font-weight: bold;
color: #07c160;
margin-right: 4rpx;
}
.price {
font-size: 80rpx;
font-weight: bold;
color: #07c160;
line-height: 1;
}
.unit {
font-size: 32rpx;
color: #07c160;
margin-left: 8rpx;
}
}
.cap-badge {
background-color: #07c160;
padding: 10rpx 32rpx;
border-radius: 30rpx;
.cap-text {
font-size: 26rpx;
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;
}
}
}
}
// 底部操作区
.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;
.divider {
margin: 0 8rpx;
color: #ddd;
}
}
}
}
/* 手机号授权弹窗样式 */
.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>