Files
uni-fans-score/pages/device/detail.vue
T

1014 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="device-header">
<view class="device-status-card" :class="deviceStatus.class">
<view class="status-indicator"></view>
<text class="status-text">{{ deviceStatus.text }}</text>
</view>
<view class="device-title">
<text class="name">共享风扇</text>
<view class="device-meta">
<text class="id-label">设备号</text>
<text class="id-value">{{ deviceId }}</text>
</view>
</view>
</view>
<!-- 设备信息卡片 -->
<view class="card device-info-card">
<view class="card-row">
<view class="card-item">
<view class="item-icon location-icon">
<!-- <uni-icons type="location" size="24" color="#fff"></uni-icons> -->
<image src="/static/images/location-map.svg" mode="aspectFill"
style="width: 45rpx;height: 45rpx;"></image>
</view>
<view class="item-content">
<text class="item-label">当前位置</text>
<text class="item-value">{{ deviceLocation }}</text>
</view>
</view>
<view class="card-item">
<view class="item-icon battery-icon" :class="{ 'battery-low': batteryLevel < 20 }">
<image src="/static/images/Electricity.svg" mode="aspectFill"
style="width: 45rpx;height: 45rpx;"></image>
</view>
<view class="item-content">
<text class="item-label">电池电量</text>
<text class="item-value">{{ batteryLevel }}%</text>
</view>
</view>
</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">¥{{deviceFeeConfig.maxHourPrice}}</text>
<text class="unit">/小时</text>
</view>
<text class="cap-price">封顶 ¥{{deviceInfo.depositAmount}}</text>
</view>
<view class="pricing-rules">
<!-- <view class="rule-item">
<view class="rule-dot"></view>
<text class="rule-text">前15分钟内归还<text class="highlight">免费</text></text>
</view> -->
<view class="rule-item">
<view class="rule-dot"></view>
<text class="rule-text">不足60分钟按60分钟计费</text>
</view>
<view class="rule-item">
<view class="rule-dot"></view>
<text class="rule-text">持续计费至99元视为买断</text>
</view>
</view>
</view>
<!-- 手机号输入 -->
<!-- <view class="card phone-card" v-if="!hasActiveOrder">
<view class="card-header">
<text class="card-title">联系方式</text>
</view>
<view class="phone-input-container">
<view class="input-wrapper">
<text class="prefix">+86</text>
<input type="number" class="phone-input" maxlength="11" placeholder="请输入手机号码"
v-model="phoneNumber" />
</view>
</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>
<view class="credit-text">
<text>微信支付分</text>
<text class="credit-divider">|</text>
<text class="credit-score">550分及以上优享</text>
</view>
</view>
<view class="" style="align-items: center;align-content: center;text-align: center;line-height: 50rpx;"
@click="handleRent('wx-pay')">
无法免押点这里></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
} from '@dcloudio/uni-app'
import {
getDeviceInfo,
rentPowerBank,
getOrderByOrderNoScore,
getOrderByOrderNoScorePayStatus,
getOrderByOrderNo,
updateOrderPackage,
cancelOrder
} from '@/config/user.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((options) => {
deviceId.value = options.deviceNo
checkOrderStatus()
console.log(options.deviceNo)
fetchDeviceInfo()
})
onMounted(() => {
checkUserPhone()
})
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'
}
}
console.log('feeconfig',deviceInfo.value.feeConfig);
deviceFeeConfig.value = JSON.parse(deviceInfo.value.feeConfig)[0] || {}
}
}
// 显示登录提示
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 {
uni.showLoading({
title: '处理中'
})
// 调用设备租借接口
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value)
if (rentResult.code !== 200) {
throw new Error(rentResult.msg || '设备租借失败')
}
// 获取后端返回的订单信息
const order = rentResult.data
// --- 统一:先更新订单套餐信息 ---
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(deviceInfo.value.depositAmount);
const packagePrice = parseFloat(selectedPkg.price);
const totalAmount = (deposit + packagePrice).toFixed(2);
// --- 计算结束 ---
uni.hideLoading()
// 跳转到订单支付页面,传递订单ID、套餐信息和总金额
uni.redirectTo({
url: `/pages/order/payment?orderId=${order.orderId}&packageTimeHours=${selectedPkg.time.replace('小时', '')}&packagePrice=${selectedPkg.price}&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);
// 支付成功后的逻辑处理 - 可以根据业务需求决定是否跳转或刷新页面
if (payResult.errCode == '0') {
const res = await getOrderByOrderNoScorePayStatus(order.orderNo);
console.log(res.data.orderStatus);
if (res.data.orderStatus == 'in_used') {
// 用户完成了支付流程,可以查询订单状态或跳转到订单页
uni.showToast({
title: '设备租借成功',
icon: 'success'
});
setTimeout(() => {
// 延迟跳转到租用中页面或订单页
uni.redirectTo({
url: '/pages/order/index'
});
}, 1500);
} else if (res.data.orderStatus == 'waiting_for_payment') {
uni.showToast({
title: '设备租借失败,订单已取消',
icon: 'error'
});
await cancelOrder({
orderId: order.orderNo
});
// 延迟跳转到租用中页面或订单页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
});
}, 1500)
}
// 用户取消等其他情况,不做特殊处理
}
// 用户取消等其他情况,不做特殊处理
} catch (payError) {
uni.showToast({
title: '支付分调用失败,请重试',
icon: 'none'
});
}
} 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;
}
// 顶部设备信息
.device-header {
display: flex;
flex-direction: column;
margin-bottom: 30rpx;
.device-status-card {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.status-indicator {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
margin-right: 10rpx;
}
&.available {
.status-indicator {
background-color: #10c469;
box-shadow: 0 0 10rpx rgba(16, 196, 105, 0.5);
}
.status-text {
color: #10c469;
}
}
&.offline {
.status-indicator {
background-color: #9a9a9a;
}
.status-text {
color: #9a9a9a;
}
}
.status-text {
font-size: 28rpx;
font-weight: 500;
}
}
.device-title {
.name {
font-size: 48rpx;
font-weight: bold;
color: #333;
}
.device-meta {
margin-top: 10rpx;
display: flex;
align-items: center;
.id-label {
font-size: 26rpx;
color: #999;
}
.id-value {
font-size: 26rpx;
color: #666;
}
}
}
}
// 卡片通用样式
.card {
background-color: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
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 {
.card-row {
display: flex;
justify-content: space-between;
}
.card-item {
display: flex;
align-items: center;
flex: 1;
.item-icon {
width: 60rpx;
height: 60rpx;
border-radius: 12rpx;
margin-right: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #fff;
&.location-icon {
background: linear-gradient(135deg, #40c9ff, #32a5ff);
// &::before {
// content: "\e900"; // 使用字体图标,需要自行替换
// }
}
&.battery-icon {
background: linear-gradient(135deg, #33db92, #10c469);
// &::before {
// content: "\e901"; // 使用字体图标,需要自行替换
// }
&.battery-low {
background: linear-gradient(135deg, #ff7676, #f54f4f);
}
}
}
.item-content {
display: flex;
flex-direction: column;
.item-label {
font-size: 26rpx;
color: #999;
margin-bottom: 4rpx;
}
.item-value {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
}
}
}
// 计费规则
.pricing-card {
.pricing-banner {
background: linear-gradient(to right, #f8f9ff, #e8f0ff);
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
.pricing-main {
display: flex;
align-items: flex-end;
.price {
font-size: 60rpx;
font-weight: bold;
color: #ff6b6b;
}
.unit {
font-size: 28rpx;
color: #999;
margin-left: 4rpx;
margin-bottom: 10rpx;
}
}
.cap-price {
margin-top: 10rpx;
font-size: 26rpx;
color: #666;
background-color: rgba(255, 107, 107, 0.1);
padding: 6rpx 20rpx;
border-radius: 20rpx;
}
}
.pricing-rules {
.rule-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.rule-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #ff6b6b;
margin-right: 16rpx;
}
.rule-text {
font-size: 28rpx;
color: #666;
.highlight {
color: #ff6b6b;
font-weight: bold;
}
}
}
}
}
// 手机号输入
.phone-card {
.phone-input-container {
.input-wrapper {
display: flex;
align-items: center;
height: 88rpx;
background-color: #f5f7fa;
border-radius: 16rpx;
padding: 0 24rpx;
.prefix {
font-size: 28rpx;
color: #666;
margin-right: 16rpx;
padding-right: 16rpx;
border-right: 1px solid #e0e0e0;
}
.phone-input {
flex: 1;
height: 88rpx;
font-size: 28rpx;
color: #333;
padding-left: 10rpx;
}
}
.phone-tip {
font-size: 24rpx;
color: #999;
margin-top: 16rpx;
display: block;
}
}
}
// 使用须知
.notice-card {
.notice-items {
.notice-item {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.notice-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #32a5ff;
margin-right: 16rpx;
margin-top: 12rpx;
}
.notice-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
}
}
// 底部操作区
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.05);
z-index: 100;
display: flex;
flex-direction: column;
// 添加一个变量来保存footer高度,方便管理和确保一致性
--footer-height: 180rpx;
.wechat-credit {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10rpx;
.wx-icon {
width: 50rpx;
height: 40rpx;
margin-right: 10rpx;
}
.credit-text {
font-size: 24rpx;
color: #07c160;
display: flex;
align-items: center;
.credit-divider {
margin: 0 10rpx;
}
.credit-score {
font-weight: 500;
}
}
}
.rent-button {
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;
width: 90%;
&.return-button {
background: linear-gradient(135deg, #FF9800, #FFB74D);
}
&:active {
transform: scale(0.98);
opacity: 0.9;
}
}
}
/* 手机号授权弹窗样式 */
.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>