Files
uni-fans-score/pages/device/detail.vue
T
2026-01-19 09:16:53 +08:00

974 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">{{ $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="footer">
<button class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent('alipay-score-pay')">
<text>{{ hasActiveOrder ? $t('order.returnDevice') : $t('device.rentDepositFree') }}</text>
</button>
<view class="alipay-credit">
<image src="/static/images/alipay.svg" mode="aspectFit" class="alipay-icon"></image>
<text class="credit-text">{{ $t('device.alipayScoreDesc') }}</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>
</template>
<script setup>
import {
ref,
reactive,
onMounted
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
getDeviceInfo,
rentPowerBank
} from '@/config/api/device.js'
import {
getOrderByOrderNoScore,
getOrderByOrderNo,
cancelOrder
} from '@/config/api/order.js'
import {
initiateAlipayPayment,
getUserInfo,
getUserPhoneNumber
} from '@/util/index.js'
import {
useI18n
} from '@/utils/i18n.js'
const {
t: $t
} = useI18n()
// 响应式状态
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)
// 生命周期 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')
}
await checkOrderStatus()
await fetchDeviceInfo()
})
onMounted(async () => {
uni.setNavigationBarTitle({
title: $t('device.deviceInfo')
})
await checkUserPhone()
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: $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 () => {
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',
}
}
}
}
// 显示登录提示
const showLoginTip = () => {
uni.showModal({
title: $t('common.tips'),
content: $t('common.loginRequired'),
confirmText: $t('auth.goToLogin'),
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: $t('order.getOrderStatusFailed'),
icon: 'none'
})
}
}
// 处理租借操作
const handleRent = (payWay) => {
if (!isLoggedIn.value) {
showLoginTip()
return
}
// 检查是否有手机号,如果没有则提示授权
if (!phoneNumber.value) {
showPhoneAuthPopup.value = true
return
}
// 提交订单
submitRentOrder(payWay)
}
// 获取价格单位文本
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 submitRentOrder = async (payWay) => {
try {
uni.showLoading({
title: $t('common.processing')
})
// --- 支付宝小程序不需要订阅消息,移除相关代码 ---
// 支付宝小程序使用消息推送,不需要订阅消息
console.log(deviceId.value);
// 调用设备租借接口
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value)
if (rentResult.code !== 200) {
throw new Error(rentResult.msg || $t('device.rentFailed'))
}
// 获取后端返回的订单信息
const order = rentResult.data
console.log('订单信息', order);
if (payWay == 'alipay-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: `/pages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}`
})
} else if (payWay == 'alipay-score-pay') {
// 当支付方式为支付宝信用免押支付时
uni.hideLoading()
// 获取支付宝信用免押所需参数
const res = await getOrderByOrderNoScore(order.orderNo);
uni.hideLoading()
if (res && res.code === 200) {
try {
// 调用支付宝信用免押小程序
const payResult = await initiateAlipayPayment(res);
console.log('支付宝信用免押调用结果', payResult);
// 成功则跳转到等待页面
if (payResult && payResult.success !== false) {
console.log('支付宝信用免押授权成功,准备跳转到等待页,时间:', new Date().toLocaleTimeString());
// 跳转到等待页面
uni.redirectTo({
url: `/pages/waiting/index?orderNo=${order.orderNo}&orderId=${order.orderId}&deviceId=${deviceId.value}`
});
return;
} else {
console.log('支付宝信用免押未完成授权或用户取消:', payResult);
// 用户取消授权,需要取消订单
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: 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;
}
}
.alipay-credit {
display: flex;
align-items: center;
justify-content: center;
margin-top: 16rpx;
.alipay-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>