749 lines
18 KiB
Vue
749 lines
18 KiB
Vue
<template>
|
||
<view class="detail-container">
|
||
<!-- 设备状态卡片 -->
|
||
<view class="device-card">
|
||
<view class="device-header">
|
||
<view class="device-title">
|
||
<text class="name">共享风扇</text>
|
||
<text class="id">设备号:{{ deviceId }}</text>
|
||
</view>
|
||
<view class="status" :class="deviceStatus.class">{{ deviceStatus.text }}</view>
|
||
</view>
|
||
<view class="device-info">
|
||
<view class="info-item">
|
||
<text class="label">设备位置</text>
|
||
<text class="value">{{ deviceLocation }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">电池电量</text>
|
||
<text class="value">{{ batteryLevel }}%</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 套餐选择 -->
|
||
<view class="package-section" v-if="!hasActiveOrder">
|
||
<view class="section-title">选择套餐</view>
|
||
<view class="package-list">
|
||
<view v-for="(pkg, index) in packages" :key="index" class="package-item"
|
||
:class="{ active: selectedPackage === index }" @click="selectPackage(index)">
|
||
<view class="package-content">
|
||
<text class="time">{{ pkg.time }}</text>
|
||
<text class="price">¥{{ pkg.price }}</text>
|
||
</view>
|
||
<text class="unit-price">约{{ pkg.unitPrice }}元/小时</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 手机号输入 -->
|
||
<view class="phone-section" v-if="!hasActiveOrder">
|
||
<view class="section-title">联系方式</view>
|
||
<view class="phone-input-wrap">
|
||
<input
|
||
type="number"
|
||
class="phone-input"
|
||
maxlength="11"
|
||
placeholder="请输入手机号码"
|
||
v-model="phoneNumber"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 使用说明 -->
|
||
<view class="notice-section">
|
||
<view class="section-title">使用说明</view>
|
||
<view class="notice-list">
|
||
<view class="notice-item">
|
||
<view class="dot"></view>
|
||
<text>请在使用前检查设备是否完好</text>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="dot"></view>
|
||
<text>超出使用时间将自动按小时计费</text>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="dot"></view>
|
||
<text>请在指定区域内使用设备</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<view class="bottom-bar">
|
||
<view class="price-info" v-if="!hasActiveOrder">
|
||
<text class="deposit-text">押金:</text>
|
||
<text class="deposit-amount">¥{{ depositAmount }}</text>
|
||
</view>
|
||
<button class="action-btn" :class="hasActiveOrder ? 'return' : 'rent'" @click="handleRent">
|
||
{{ hasActiveOrder ? '归还设备' : '立即租借' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
getDeviceInfo,
|
||
rentPowerBank,
|
||
updateOrderPackage
|
||
} from '@/config/user.js'
|
||
export default {
|
||
data() {
|
||
return {
|
||
deviceInfo: {},
|
||
deviceId: '',
|
||
deviceLocation: '一号教学楼大厅',
|
||
batteryLevel: 95,
|
||
hasActiveOrder: false,
|
||
deviceStatus: {
|
||
text: '可使用',
|
||
class: 'available'
|
||
},
|
||
selectedPackage: 1,
|
||
packages: [],
|
||
depositAmount: "99.00", // 默认押金金额
|
||
isLoggedIn: true,
|
||
phoneNumber: ''
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
// console.log(options);
|
||
this.deviceId = options.deviceNo
|
||
|
||
// 如果URL中包含了feeConfig参数,直接使用它
|
||
if (options.feeConfig) {
|
||
try {
|
||
console.log('从URL获取到feeConfig:', options.feeConfig)
|
||
const feeConfigStr = decodeURIComponent(options.feeConfig)
|
||
// 存储到设备信息中,这样后续处理逻辑可以保持不变
|
||
this.deviceInfo = { ...this.deviceInfo, feeConfig: feeConfigStr }
|
||
// 马上解析feeConfig并生成套餐选项
|
||
this.parseFeeConfig()
|
||
} catch (e) {
|
||
console.error('解析URL中的feeConfig失败:', e)
|
||
// 如果解析失败,继续正常流程获取设备信息
|
||
this.checkOrderStatus()
|
||
this.getDeviceInfo()
|
||
}
|
||
} else {
|
||
// 正常流程
|
||
this.checkOrderStatus() // 新增状态检查
|
||
console.log(options.deviceNo);
|
||
this.getDeviceInfo()
|
||
}
|
||
},
|
||
methods: {
|
||
// 检查登录状态和订单
|
||
async getDeviceInfo() {
|
||
const res = await getDeviceInfo(this.deviceId);
|
||
if (res.code == 200) {
|
||
this.deviceInfo = res.data.device || {};
|
||
|
||
// 更新设备位置信息
|
||
if (this.deviceInfo.deviceLocation) {
|
||
this.deviceLocation = this.deviceInfo.deviceLocation;
|
||
} else if (res.data.position && res.data.position.name) {
|
||
this.deviceLocation = res.data.position.name;
|
||
}
|
||
|
||
// 获取押金金额
|
||
if (this.deviceInfo.depositAmount) {
|
||
this.depositAmount = this.deviceInfo.depositAmount;
|
||
}
|
||
|
||
// 更新设备状态
|
||
if (this.deviceInfo.status) {
|
||
if (this.deviceInfo.status === 'online') {
|
||
this.deviceStatus = {
|
||
text: '可使用',
|
||
class: 'available'
|
||
};
|
||
} else if (this.deviceInfo.status === 'offline') {
|
||
this.deviceStatus = {
|
||
text: '离线',
|
||
class: 'offline'
|
||
};
|
||
}
|
||
}
|
||
|
||
// 使用抽取的方法解析feeConfig并生成套餐选项
|
||
this.parseFeeConfig();
|
||
}
|
||
},
|
||
|
||
// 显示登录提示
|
||
showLoginTip() {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '请先登录后再操作',
|
||
confirmText: '去登录',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateTo({
|
||
url: '/pages/login/index'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
selectPackage(index) {
|
||
this.selectedPackage = index;
|
||
},
|
||
|
||
// 检查订单状态
|
||
async checkOrderStatus() {
|
||
try {
|
||
// 调用接口检查是否有进行中的订单
|
||
const result = await this.$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=${this.deviceId}`
|
||
})
|
||
}else if(order.status === 'in_used'){
|
||
// 如果有正在进行的订单,跳转到归还页面,带上设备ID
|
||
uni.redirectTo({
|
||
url: `/pages/device/return?deviceId=${this.deviceId}`
|
||
})
|
||
}
|
||
|
||
|
||
}
|
||
} catch (error) {
|
||
uni.showToast({
|
||
title: '订单状态查询失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
|
||
// 处理租借操作
|
||
handleRent() {
|
||
if (!this.isLoggedIn) {
|
||
this.showLoginTip()
|
||
return
|
||
}
|
||
|
||
// 添加手机号验证
|
||
if (!this.phoneNumber) {
|
||
uni.showToast({
|
||
title: '请输入手机号码',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 验证手机号格式
|
||
if (!/^1[3-9]\d{9}$/.test(this.phoneNumber)) {
|
||
uni.showToast({
|
||
title: '请输入正确的手机号码',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 直接提交订单,不显示确认对话框
|
||
this.submitRentOrder()
|
||
},
|
||
|
||
// 提交租借订单
|
||
async submitRentOrder() {
|
||
try {
|
||
uni.showLoading({
|
||
title: '处理中'
|
||
})
|
||
|
||
// 获取选中的套餐信息
|
||
const selectedPkg = this.packages[this.selectedPackage]
|
||
|
||
// 调用设备租借接口
|
||
const rentResult = await rentPowerBank(this.deviceId, this.phoneNumber)
|
||
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);
|
||
// 即使更新失败,也继续尝试跳转支付,让用户完成支付
|
||
}
|
||
// --- 更新结束 ---
|
||
|
||
// --- 新增:计算总金额 ---
|
||
const deposit = parseFloat(this.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=${this.depositAmount}${this.deviceInfo && this.deviceInfo.feeConfig ? '&feeConfig=' + encodeURIComponent(this.deviceInfo.feeConfig) : ''}`
|
||
})
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: error.message || '租借失败,请重试',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
|
||
// 单独抽取解析feeConfig的逻辑
|
||
parseFeeConfig() {
|
||
if (this.deviceInfo.feeConfig) {
|
||
try {
|
||
const feeConfig = JSON.parse(this.deviceInfo.feeConfig);
|
||
|
||
// 检查是否为新格式 [{"hour":1,"timesPrice":4},{"hour":3,"timesPrice":10},{"hour":5,"timesPrice":15}]
|
||
if (feeConfig.length > 0 && 'hour' in feeConfig[0] && 'timesPrice' in feeConfig[0]) {
|
||
// 新格式处理 - 直接使用所有套餐
|
||
this.packages = feeConfig.map(pkg => {
|
||
const hour = pkg.hour;
|
||
const price = pkg.timesPrice;
|
||
const unitPrice = (price / hour).toFixed(2);
|
||
|
||
return {
|
||
time: `${hour}小时`,
|
||
price: price.toFixed(2),
|
||
unitPrice: unitPrice,
|
||
hour: hour // 添加小时信息,用于后续处理
|
||
};
|
||
});
|
||
|
||
// 按小时排序
|
||
this.packages.sort((a, b) => a.hour - b.hour);
|
||
} else {
|
||
// 旧格式处理
|
||
// 通常使用common规格的配置
|
||
const commonConfig = feeConfig.find(item => item.specCode === 'common') || feeConfig[0];
|
||
|
||
if (commonConfig) {
|
||
// 根据收费类型生成套餐
|
||
if (this.deviceInfo.feeType === 'hour') {
|
||
// 按小时收费
|
||
const hourPrice = commonConfig.hourPrice > 0 ? commonConfig.hourPrice : commonConfig.timesPrice / 6;
|
||
|
||
this.packages = [
|
||
{
|
||
time: '1小时',
|
||
price: hourPrice.toFixed(2),
|
||
unitPrice: hourPrice.toFixed(2),
|
||
hour: 1
|
||
},
|
||
{
|
||
time: '6小时',
|
||
price: (hourPrice * 6).toFixed(2),
|
||
unitPrice: hourPrice.toFixed(2),
|
||
hour: 6
|
||
},
|
||
{
|
||
time: '12小时',
|
||
price: (hourPrice * 12).toFixed(2),
|
||
unitPrice: hourPrice.toFixed(2),
|
||
hour: 12
|
||
}
|
||
];
|
||
} else if (this.deviceInfo.feeType === 'times') {
|
||
// 按次收费
|
||
const timesPrice = commonConfig.timesPrice;
|
||
this.packages = [
|
||
{
|
||
time: '1次',
|
||
price: timesPrice.toFixed(2),
|
||
unitPrice: timesPrice.toFixed(2),
|
||
hour: 1
|
||
}
|
||
];
|
||
} else {
|
||
// 默认套餐
|
||
this.packages = [
|
||
{
|
||
time: '1小时',
|
||
price: '2.00',
|
||
unitPrice: '2.00',
|
||
hour: 1
|
||
},
|
||
{
|
||
time: '6小时',
|
||
price: '10.00',
|
||
unitPrice: '1.67',
|
||
hour: 6
|
||
},
|
||
{
|
||
time: '12小时',
|
||
price: '15.00',
|
||
unitPrice: '1.25',
|
||
hour: 12
|
||
}
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 默认选中中间套餐
|
||
this.selectedPackage = Math.min(1, this.packages.length - 1);
|
||
} catch (e) {
|
||
console.error('解析设备费用配置失败:', e);
|
||
// 使用默认套餐
|
||
this.packages = [
|
||
{
|
||
time: '1小时',
|
||
price: '2.00',
|
||
unitPrice: '2.00',
|
||
hour: 1
|
||
},
|
||
{
|
||
time: '6小时',
|
||
price: '10.00',
|
||
unitPrice: '1.67',
|
||
hour: 6
|
||
},
|
||
{
|
||
time: '12小时',
|
||
price: '15.00',
|
||
unitPrice: '1.25',
|
||
hour: 12
|
||
}
|
||
];
|
||
|
||
// 默认选中中间套餐
|
||
this.selectedPackage = 1;
|
||
}
|
||
} else {
|
||
// 如果没有feeConfig,使用默认套餐
|
||
this.packages = [
|
||
{
|
||
time: '1小时',
|
||
price: '2.00',
|
||
unitPrice: '2.00',
|
||
hour: 1
|
||
},
|
||
{
|
||
time: '6小时',
|
||
price: '10.00',
|
||
unitPrice: '1.67',
|
||
hour: 6
|
||
},
|
||
{
|
||
time: '12小时',
|
||
price: '15.00',
|
||
unitPrice: '1.25',
|
||
hour: 12
|
||
}
|
||
];
|
||
|
||
// 默认选中中间套餐
|
||
this.selectedPackage = 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.detail-container {
|
||
min-height: 100vh;
|
||
background: #f8f8f8;
|
||
padding: 30rpx;
|
||
padding-bottom: 180rpx;
|
||
box-sizing: border-box;
|
||
|
||
.device-card {
|
||
background: #fff;
|
||
border-radius: 24rpx;
|
||
padding: 30rpx;
|
||
margin-bottom: 30rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||
|
||
.device-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30rpx;
|
||
|
||
.device-title {
|
||
.name {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.id {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.status {
|
||
padding: 8rpx 24rpx;
|
||
border-radius: 24rpx;
|
||
font-size: 24rpx;
|
||
|
||
&.available {
|
||
background: #E8F5E9;
|
||
color: #4CAF50;
|
||
}
|
||
|
||
&.in-use {
|
||
background: #E3F2FD;
|
||
color: #1976D2;
|
||
}
|
||
}
|
||
}
|
||
|
||
.device-info {
|
||
.info-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 16rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.label {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.value {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.package-section {
|
||
background: #fff;
|
||
border-radius: 24rpx;
|
||
padding: 30rpx;
|
||
margin-bottom: 30rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.package-list {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
|
||
.package-item {
|
||
flex: 1;
|
||
margin: 0 10rpx;
|
||
padding: 24rpx;
|
||
background: #F5F5F5;
|
||
border-radius: 16rpx;
|
||
text-align: center;
|
||
transition: all 0.3s;
|
||
|
||
&:first-child {
|
||
margin-left: 0;
|
||
}
|
||
|
||
&:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
&.active {
|
||
background: #E3F2FD;
|
||
border: 2rpx solid #1976D2;
|
||
|
||
.package-content {
|
||
|
||
.time,
|
||
.price {
|
||
color: #1976D2;
|
||
}
|
||
}
|
||
}
|
||
|
||
.package-content {
|
||
margin-bottom: 8rpx;
|
||
|
||
.time {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-bottom: 8rpx;
|
||
display: block;
|
||
}
|
||
|
||
.price {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.unit-price {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.phone-section {
|
||
background: #fff;
|
||
border-radius: 24rpx;
|
||
padding: 30rpx;
|
||
margin-bottom: 30rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.phone-input-wrap {
|
||
.phone-input {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: #F5F5F5;
|
||
border-radius: 16rpx;
|
||
padding: 0 30rpx;
|
||
font-size: 28rpx;
|
||
box-sizing: border-box;
|
||
|
||
&::placeholder {
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.notice-section {
|
||
background: #fff;
|
||
border-radius: 24rpx;
|
||
padding: 30rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.notice-list {
|
||
.notice-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.dot {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
background: #1976D2;
|
||
border-radius: 50%;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.bottom-bar {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: #fff;
|
||
padding: 20rpx 30rpx;
|
||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||
|
||
.price-info {
|
||
.deposit-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.deposit-amount {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #FF9800;
|
||
}
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
margin-left: 30rpx;
|
||
height: 88rpx;
|
||
border-radius: 44rpx;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
|
||
&.rent {
|
||
background: linear-gradient(135deg, #1976D2, #42A5F5);
|
||
color: #fff;
|
||
}
|
||
|
||
&.return {
|
||
background: linear-gradient(135deg, #FF9800, #FFB74D);
|
||
color: #fff;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |