feat: 添加订单支付和成功页面,更新订单状态逻辑

新增了订单支付页面和支付成功页面,分别用于处理用户的支付流程和展示支付结果。同时,更新了订单状态的逻辑,确保在订单列表中正确显示各个状态。调整了设备租借逻辑,优化了扫码处理流程,提升用户体验。
This commit is contained in:
8vd8
2025-04-10 14:18:43 +08:00
parent aebd6c0459
commit 3fecd77739
53 changed files with 2049 additions and 702 deletions
+68 -64
View File
@@ -3,13 +3,13 @@
<!-- 状态切换 -->
<view class="tab-bar">
<view
v-for="(tab, index) in tabs"
v-for="(tab, index) in OrderStatusTabs"
:key="index"
class="tab-item"
:class="{ active: currentTab === index }"
@click="switchTab(index)"
>
{{ tab }}
{{ tab.text }}
</view>
</view>
@@ -18,7 +18,7 @@
<view class="order-item" v-for="(order, index) in orderList" :key="index">
<view class="order-header">
<text class="order-no">订单号{{ order.orderNo }}</text>
<text class="order-status" :class="order.status">{{ order.statusText }}</text>
<text class="order-status" :class="OrderStatusMap[order.status]?.class">{{ OrderStatusMap[order.status]?.text }}</text>
</view>
<view class="order-content">
<view class="device-info">
@@ -51,32 +51,16 @@
</template>
<script>
import {getOrderList, queryById} from '../../config/user.js'
import {getOrderList, queryById} from '../../config/user.js'
import { OrderStatusMap, OrderStatusTabs } from '../../constants/orderStatus.js'
export default {
data() {
return {
currentTab: 0,
tabs: ['全部', '使用中', '已完成'],
orderList: [
{
orderNo: 'ORDER202403200001',
status: 'using',
statusText: '使用中',
deviceId: 'FAN001',
startTime: '2024-03-20 15:30',
endTime: '',
amount: '2.00'
},
{
orderNo: 'ORDER202403190001',
status: 'finished',
statusText: '已完成',
deviceId: 'FAN002',
startTime: '2024-03-19 13:00',
endTime: '2024-03-19 15:00',
amount: '4.00'
}
]
OrderStatusMap,
OrderStatusTabs,
orderList: []
}
},
async onLoad(options) {
@@ -91,20 +75,22 @@ export default {
// 格式化订单数据
const formattedOrder = {
orderNo: orderData.orderId,
status: orderData.orderStatus === 2 ? 'using' : 'finished',
statusText: orderData.orderStatus === 2 ? '使用中' : '已完成',
status: orderData.orderStatus,
deviceId: orderData.deviceNo,
startTime: this.formatTime(new Date(orderData.createTime)),
endTime: orderData.endTime ? this.formatTime(new Date(orderData.endTime)) : '',
startTime: orderData.createTime,
endTime: orderData.endTime || '',
amount: orderData.amount || '0.00'
};
// 将订单添加到列表开头
this.orderList = [formattedOrder, ...this.orderList];
// 如果是使用中的订单,切换到使用中标签
if (orderData.orderStatus === 2) {
this.switchTab(1); // 切换到"使用中"
// 根据订单状态切换到对应标签
const tabIndex = this.OrderStatusTabs.findIndex(tab =>
tab.status.includes(orderData.orderStatus)
);
if (tabIndex !== -1) {
this.switchTab(tabIndex);
}
}
} catch (error) {
@@ -113,38 +99,36 @@ export default {
}
// 获取订单列表
try {
const res = await getOrderList();
console.log(res);
if (res.code === 200 && res.data && res.data.records) {
// 处理订单列表数据
this.orderList = res.data.records.map(item => ({
orderNo: item.orderId,
status: item.orderStatus === 2 ? 'using' : 'finished',
statusText: item.orderStatus === 2 ? '使用中' : '已完成',
deviceId: item.deviceNo,
startTime: item.startTime,
endTime: item.endTime ? this.formatTime(new Date(item.endTime)) : '',
amount: item.amount || '0.00'
}));
}
} catch (error) {
console.error('获取订单列表失败:', error);
}
await this.getOrderList();
},
methods: {
formatTime(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
async getOrderList(statusList = []) {
try {
const res = await getOrderList(statusList);
if (res.code === 200 && res.data && res.data.records) {
// 处理订单列表数据
this.orderList = res.data.records.map(item => ({
orderNo: item.orderId,
status: item.orderStatus,
deviceId: item.deviceNo,
startTime: item.createTime,
endTime: item.endTime || '',
amount: item.amount || '0.00'
}));
}
} catch (error) {
console.error('获取订单列表失败:', error);
uni.showToast({
title: '获取订单列表失败',
icon: 'none'
});
}
},
switchTab(index) {
this.currentTab = index
// TODO: 根据状态获取订单列表
async switchTab(index) {
this.currentTab = index;
// 根据状态获取订单列表
const statusList = this.OrderStatusTabs[index].status;
await this.getOrderList(statusList);
}
}
}
@@ -216,11 +200,31 @@ export default {
.order-status {
font-size: 26rpx;
&.using {
&.status-waiting {
color: #FF9800;
}
&.status-progress {
color: #2196F3;
}
&.status-success {
color: #4CAF50;
}
&.status-using {
color: #1976D2;
}
&.finished {
&.status-failed {
color: #F44336;
}
&.status-cancelled {
color: #9E9E9E;
}
&.status-finished {
color: #4CAF50;
}
}
+589
View File
@@ -0,0 +1,589 @@
<template>
<view class="payment-container">
<!-- 订单状态 -->
<view class="status-card">
<view class="status-icon" :class="orderStatus.class"></view>
<view class="status-text">{{ orderStatus.text }}</view>
<view class="status-desc">{{ orderStatus.desc }}</view>
</view>
<!-- 订单信息 -->
<view class="order-card">
<view class="card-title">订单信息</view>
<view class="info-item">
<text class="label">订单号</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">设备号</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">创建时间</text>
<text class="value">{{ orderInfo.createTime || '-' }}</text>
</view>
<view class="info-item">
<text class="label">联系电话</text>
<text class="value">{{ orderInfo.phone || '-' }}</text>
</view>
</view>
<!-- 费用信息 -->
<view class="price-card">
<view class="card-title">费用信息</view>
<view class="price-item">
<text class="label">押金</text>
<text class="value">{{ orderInfo.deposit || '99.00' }}</text>
</view>
<view class="price-item">
<text class="label">套餐</text>
<text class="value">{{ packageInfo.time }} ({{ packageInfo.price }})</text>
</view>
<view class="price-item">
<text class="label">租借费用</text>
<text class="value">{{ orderInfo.amount || packageInfo.price }}</text>
</view>
<view class="price-item total">
<text class="label">合计</text>
<text class="value">{{ totalAmount }}</text>
</view>
</view>
<!-- 支付方式 -->
<view class="payment-methods">
<view class="card-title">支付方式</view>
<view
v-for="(method, index) in paymentMethods"
:key="index"
class="method-item"
:class="{ active: selectedMethod === index }"
@click="selectMethod(index)"
>
<view class="method-icon" :class="method.icon"></view>
<view class="method-name">{{ method.name }}</view>
<view class="method-check"></view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="total-amount">
<text>合计</text>
<text class="amount">{{ totalAmount }}</text>
</view>
<button class="pay-btn" @click="handlePayment">立即支付</button>
</view>
<view class="back-btn" @click="navigateBack">
<text>返回设备详情</text>
</view>
methods: {
navigateBack() {
uni.redirectTo({
url: `/pages/device/detail?deviceId=${this.deviceId}`
})
}
}
</view>
</template>
<script>
import { queryById } from '@/config/user.js'
export default {
data() {
return {
orderId: null,
orderInfo: {},
packageInfo: {
time: '',
price: '0.00'
},
orderStatus: {
text: '等待支付',
desc: '请在15分钟内完成支付',
class: 'waiting'
},
paymentMethods: [
{
name: '微信支付',
icon: 'wechat'
},
{
name: '支付宝',
icon: 'alipay'
}
],
selectedMethod: 0
}
},
computed: {
totalAmount() {
const deposit = parseFloat(this.orderInfo.deposit || 99)
const amount = parseFloat(this.orderInfo.amount || this.packageInfo.price || 0)
return (deposit + amount).toFixed(2)
}
},
onLoad(options) {
if (options && options.orderId) {
this.orderId = options.orderId
// 获取传递的套餐信息
if (options.packageTime && options.packagePrice) {
this.packageInfo = {
time: options.packageTime,
price: options.packagePrice
}
}
this.loadOrderInfo()
} else {
uni.showToast({
title: '订单信息不存在',
icon: 'none'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/index/index'
})
}, 1500)
}
},
methods: {
// 加载订单信息
async loadOrderInfo() {
try {
uni.showLoading({
title: '加载中'
})
const res = await queryById(this.orderId)
if (res.code === 200 && res.data) {
const orderData = res.data
this.orderInfo = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
createTime: orderData.createTime,
phone: orderData.phone,
deposit: '99.00', // 假设押金固定为99元
amount: orderData.amount || this.packageInfo.price || '0.00'
}
// 如果订单中没有套餐信息,但URL参数中有,则使用URL参数中的套餐信息
if (!orderData.packageTime && this.packageInfo.time) {
this.orderInfo.packageTime = this.packageInfo.time
this.orderInfo.packagePrice = this.packageInfo.price
}
} else {
throw new Error('获取订单信息失败')
}
uni.hideLoading()
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '获取订单信息失败',
icon: 'none'
})
}
},
// 选择支付方式
selectMethod(index) {
this.selectedMethod = index
},
// 处理支付
async handlePayment() {
try {
uni.showLoading({
title: '处理中'
})
// 调用后端创建微信支付订单接口
const res = await uni.request({
url: `${uni.getStorageSync('baseUrl') || 'http://127.0.0.1:8080'}/app/wx-payment/create/${this.orderInfo.orderNo}`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (res.statusCode === 200 && res.data.code === 200) {
// 支付成功,跳转到支付成功页面
uni.hideLoading()
uni.redirectTo({
url: `/pages/order/success?orderId=${this.orderId}`
})
} else {
throw new Error(res.data.msg || '支付失败')
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '支付失败',
icon: 'none'
})
}
},
// 发送租借指令
async sendRentCommand() {
try {
uni.showLoading({
title: '处理中'
})
// 调用发送租借指令的接口
const res = await this.sendRentRequest()
if (res.code === 200) {
uni.hideLoading()
uni.showToast({
title: '租借成功',
icon: 'success'
})
// 跳转到订单列表页面
setTimeout(() => {
uni.redirectTo({
url: `/pages/order/index?orderId=${this.orderId}`
})
}, 1500)
} else {
throw new Error(res.msg || '租借失败')
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '租借失败',
icon: 'none'
})
}
},
// 发送租借请求
sendRentRequest() {
return new Promise((resolve, reject) => {
uni.request({
url: `${uni.getStorageSync('baseUrl') || 'http://127.0.0.1:8080'}/app/device/sendRentCommand`,
method: 'POST',
data: {
orderId: this.orderId
},
header: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
success(res) {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(new Error('请求失败'))
}
},
fail(err) {
reject(err)
}
})
})
},
// 格式化时间
formatTime(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
},
// 轮询订单状态
async pollOrderStatus() {
let retryCount = 0;
const maxRetries = 10;
const interval = 1000; // 1秒间隔
const checkStatus = async () => {
try {
const res = await uni.request({
url: `${uni.getStorageSync('baseUrl') || 'http://127.0.0.1:8080'}/app/payment/status/${this.orderInfo.orderNo}`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
});
if (res.statusCode === 200 && res.data.code === 200) {
const orderData = res.data.data;
if (orderData.orderStatus === 'IN_USED') {
// 支付成功,订单已开始使用
uni.showToast({
title: '支付成功',
icon: 'success'
});
setTimeout(() => {
uni.redirectTo({
url: `/pages/order/success?orderId=${this.orderId}`
});
}, 1500);
return;
}
}
if (retryCount < maxRetries) {
retryCount++;
setTimeout(checkStatus, interval);
} else {
throw new Error('订单状态查询超时');
}
} catch (error) {
uni.showToast({
title: error.message || '查询订单状态失败',
icon: 'none'
});
}
};
checkStatus();
},
}
}
</script>
<style lang="scss" scoped>
.payment-container {
min-height: 100vh;
background: #f8f8f8;
padding: 30rpx;
padding-bottom: 180rpx;
box-sizing: border-box;
.status-card {
background: #fff;
border-radius: 24rpx;
padding: 40rpx 30rpx;
margin-bottom: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.status-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #f5f5f5;
margin-bottom: 20rpx;
&.waiting {
background: #FFF9C4;
}
&.success {
background: #E8F5E9;
}
&.failed {
background: #FFEBEE;
}
}
.status-text {
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.status-desc {
font-size: 28rpx;
color: #999;
}
}
.order-card, .price-card {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
position: relative;
padding-left: 20rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 32rpx;
background: #1976D2;
border-radius: 4rpx;
}
}
.info-item, .price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 28rpx;
color: #666;
}
.value {
font-size: 28rpx;
color: #333;
}
&.total {
margin-top: 10rpx;
padding-top: 30rpx;
border-top: 1px solid #f5f5f5;
.label, .value {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.value {
color: #FF5722;
}
}
}
}
.payment-methods {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.method-item {
display: flex;
align-items: center;
padding: 30rpx 20rpx;
border-bottom: 1px solid #f5f5f5;
position: relative;
&:last-child {
border-bottom: none;
}
&.active {
background: #F5F5F5;
.method-check {
background: #1976D2;
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 12rpx;
height: 12rpx;
background: #fff;
border-radius: 50%;
}
}
}
.method-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
&.wechat {
background: url('../../static/images/wechat.svg') no-repeat center/contain;
}
&.alipay {
background: url('../../static/images/alipay.svg') no-repeat center/contain;
}
}
.method-name {
flex: 1;
font-size: 28rpx;
color: #333;
}
.method-check {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #ddd;
position: relative;
}
}
}
.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);
.total-amount {
font-size: 28rpx;
color: #666;
.amount {
font-size: 36rpx;
font-weight: 600;
color: #FF5722;
margin-left: 10rpx;
}
}
.pay-btn {
background: #1976D2;
color: #fff;
font-size: 32rpx;
font-weight: 600;
padding: 20rpx 60rpx;
border-radius: 100rpx;
border: none;
&:active {
opacity: 0.9;
}
}
}
}
</style>
+229
View File
@@ -0,0 +1,229 @@
<template>
<view class="success-container">
<!-- 支付成功状态 -->
<view class="status-card">
<view class="status-icon success"></view>
<view class="status-text">支付成功</view>
<view class="status-desc">您的订单已支付成功</view>
</view>
<!-- 订单信息 -->
<view class="order-card">
<view class="card-title">订单信息</view>
<view class="info-item">
<text class="label">订单号</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">设备号</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">支付金额</text>
<text class="value">{{ orderInfo.amount || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">支付时间</text>
<text class="value">{{ orderInfo.payTime || '-' }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="button-group">
<button class="primary-btn" @click="goToHome">返回首页</button>
<button class="secondary-btn" @click="goToOrderList">查看订单</button>
</view>
</view>
</template>
<script>
import { queryById } from '@/config/user.js'
export default {
data() {
return {
orderId: '',
orderInfo: {}
}
},
onLoad(options) {
if (options && options.orderId) {
this.orderId = options.orderId
this.loadOrderInfo()
} else {
uni.showToast({
title: '订单信息不存在',
icon: 'none'
})
setTimeout(() => {
this.goToHome()
}, 1500)
}
},
methods: {
async loadOrderInfo() {
try {
uni.showLoading({
title: '加载中'
})
const res = await queryById(this.orderId)
if (res.code === 200 && res.data) {
const orderData = res.data
this.orderInfo = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
amount: orderData.amount,
payTime: this.formatTime(new Date())
}
} else {
throw new Error('获取订单信息失败')
}
uni.hideLoading()
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '获取订单信息失败',
icon: 'none'
})
}
},
formatTime(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
const second = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
},
goToHome() {
uni.switchTab({
url: '/pages/index/index'
})
},
goToOrderList() {
uni.redirectTo({
url: '/pages/order/index'
})
}
}
}
</script>
<style lang="scss" scoped>
.success-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.status-card {
background-color: #fff;
border-radius: 12px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
.status-icon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
background-color: #07c160;
border-radius: 50%;
position: relative;
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 20px;
border: 3px solid #fff;
border-top: none;
border-right: none;
transform-origin: center;
transform: translate(-50%, -70%) rotate(-45deg);
}
}
.status-text {
font-size: 24px;
font-weight: bold;
color: #07c160;
margin-bottom: 8px;
}
.status-desc {
font-size: 14px;
color: #666;
}
}
.order-card {
background-color: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.label {
color: #666;
font-size: 14px;
}
.value {
color: #333;
font-size: 14px;
}
}
}
.button-group {
margin-top: 30px;
display: flex;
flex-direction: column;
gap: 16px;
.primary-btn {
background-color: #07c160;
color: #fff;
border: none;
border-radius: 24px;
padding: 12px;
font-size: 16px;
&:active {
opacity: 0.8;
}
}
.secondary-btn {
background-color: #fff;
color: #07c160;
border: 1px solid #07c160;
border-radius: 24px;
padding: 12px;
font-size: 16px;
&:active {
background-color: #f5f5f5;
}
}
}
</style>