785 lines
16 KiB
Vue
785 lines
16 KiB
Vue
<template>
|
||
<view class="order-page">
|
||
<!-- 收货地址 -->
|
||
<view class="address-section" @click="showAddressPicker">
|
||
<view v-if="selectedAddress" class="address-card">
|
||
<view class="address-header">
|
||
<text class="receiver-name">{{ selectedAddress.receiver_name }}</text>
|
||
<text class="receiver-phone">{{ selectedAddress.phone }}</text>
|
||
</view>
|
||
<view class="address-detail">
|
||
<text class="address-text">{{ formatAddress(selectedAddress) }}</text>
|
||
</view>
|
||
<view class="address-arrow">
|
||
<text class="iconfont icon-arrow-right">›</text>
|
||
</view>
|
||
</view>
|
||
<view v-else class="address-empty">
|
||
<text class="iconfont icon-location">📍</text>
|
||
<text class="empty-text">请选择收货地址</text>
|
||
<text class="iconfont icon-arrow-right">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 地址选择弹窗 -->
|
||
<view class="address-picker-mask" v-if="showAddressList" @click="hideAddressPicker">
|
||
<view class="address-picker" @click.stop>
|
||
<view class="picker-header">
|
||
<text class="picker-title">选择收货地址</text>
|
||
<text class="picker-close" @click="hideAddressPicker">×</text>
|
||
</view>
|
||
<scroll-view scroll-y class="picker-content">
|
||
<view class="picker-item" v-for="(item, index) in addressList" :key="item.id || index"
|
||
@click="chooseAddress(item)">
|
||
<view class="picker-item-header">
|
||
<text class="picker-receiver">{{ item.receiver_name }}</text>
|
||
<text class="picker-phone">{{ item.phone }}</text>
|
||
<view class="picker-tag" v-if="Number(item.is_default) === 1">默认</view>
|
||
</view>
|
||
<view class="picker-item-detail">
|
||
<text class="picker-address">{{ formatAddress(item) }}</text>
|
||
</view>
|
||
<view class="picker-item-check" v-if="selectedAddress && selectedAddress.id === item.id">
|
||
<text class="iconfont icon-gou"></text>
|
||
</view>
|
||
</view>
|
||
<view v-if="addressList.length === 0" class="picker-empty">
|
||
<text class="empty-text">暂无地址,请先添加</text>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="picker-footer">
|
||
<view class="picker-btn" @click="goToAddressManage">管理地址</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 商品列表 -->
|
||
<view class="goods-section">
|
||
<view class="section-title">商品信息</view>
|
||
<view class="goods-list">
|
||
<view class="goods-item" v-for="(item, index) in orderItems" :key="index">
|
||
<view class="goods-image">
|
||
<image :src="item.product_image_url" mode="aspectFill"></image>
|
||
</view>
|
||
<view class="goods-info">
|
||
<view class="goods-name">{{ item.product_name || item.name }}</view>
|
||
<view class="goods-spec" v-if="item.sku_name">{{ item.sku_name }}</view>
|
||
<view class="goods-price-row">
|
||
<view class="goods-price">
|
||
<text class="price-symbol">¥</text>
|
||
<text class="price-value">{{ formatPrice(item.price) }}</text>
|
||
</view>
|
||
<view class="goods-quantity">x{{ item.quantity }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 订单备注 -->
|
||
<view class="remark-section">
|
||
<view class="section-title">订单备注</view>
|
||
<textarea class="remark-input" v-model="orderRemark" placeholder="选填,对本次购买的说明(如:请尽快发货)" maxlength="200"
|
||
auto-height></textarea>
|
||
</view>
|
||
|
||
<!-- 价格明细 -->
|
||
<view class="price-section">
|
||
<view class="price-item">
|
||
<text class="price-label">商品总价</text>
|
||
<text class="price-value">¥{{ formatPrice(totalPrice) }}</text>
|
||
</view>
|
||
<view class="price-item">
|
||
<text class="price-label">运费</text>
|
||
<text class="price-value">¥{{ formatPrice(shippingFee) }}</text>
|
||
</view>
|
||
<view class="price-item total" v-if="discountAmount > 0">
|
||
<text class="price-label">优惠</text>
|
||
<text class="price-value discount">-¥{{ formatPrice(discountAmount) }}</text>
|
||
</view>
|
||
<view class="price-item total">
|
||
<text class="price-label">实付金额</text>
|
||
<text class="price-value total-price">¥{{ formatPrice(finalPrice) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部提交栏 -->
|
||
<view class="order-footer">
|
||
<view class="footer-total">
|
||
<text class="total-label">合计:</text>
|
||
<text class="total-price">¥{{ formatPrice(finalPrice) }}</text>
|
||
</view>
|
||
<view class="footer-btn" :class="{ disabled: !canSubmit }" @click="submitOrder">
|
||
{{ submitting ? '提交中...' : '提交订单' }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import request from '@/api/request.js';
|
||
import { fetchAddressList } from '@/api/address.js';
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
orderItems: [], // 订单商品列表
|
||
selectedAddress: null, // 选中的收货地址
|
||
addressList: [], // 地址列表
|
||
showAddressList: false, // 是否显示地址选择弹窗
|
||
orderRemark: '', // 订单备注
|
||
shippingFee: 0, // 运费(单位:分)
|
||
discountAmount: 0, // 优惠金额(单位:分)
|
||
submitting: false, // 是否正在提交
|
||
app_id: ''
|
||
};
|
||
},
|
||
computed: {
|
||
// 商品总价(单位:分)
|
||
totalPrice() {
|
||
let total = 0;
|
||
this.orderItems.forEach(item => {
|
||
const price = parseFloat(item.price || 0);
|
||
const quantity = parseInt(item.quantity || 1);
|
||
total += price * quantity;
|
||
});
|
||
return total;
|
||
},
|
||
// 最终支付金额(单位:分)
|
||
finalPrice() {
|
||
return Math.max(0, this.totalPrice + this.shippingFee - this.discountAmount);
|
||
},
|
||
// 是否可以提交订单
|
||
canSubmit() {
|
||
return this.selectedAddress && this.orderItems.length > 0 && !this.submitting;
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
// 从购物车传递过来的商品数据
|
||
if (options.items) {
|
||
try {
|
||
this.orderItems = JSON.parse(decodeURIComponent(options.items));
|
||
} catch (error) {
|
||
console.error('解析商品数据失败:', error);
|
||
uni.showToast({
|
||
title: '商品数据错误',
|
||
icon: 'none'
|
||
});
|
||
setTimeout(() => {
|
||
uni.navigateBack();
|
||
}, 1500);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 加载地址列表
|
||
this.loadAddressList();
|
||
},
|
||
methods: {
|
||
// 格式化价格(分转元)
|
||
formatPrice(value) {
|
||
if (!value && value !== 0) {
|
||
return '0.00';
|
||
}
|
||
// 分转换为元
|
||
value = value / 100;
|
||
// 保留两位小数
|
||
return value.toFixed(2);
|
||
},
|
||
|
||
// 格式化地址
|
||
formatAddress(address) {
|
||
if (!address) return '';
|
||
const region = [
|
||
address.province || '',
|
||
address.city || '',
|
||
address.district || ''
|
||
].filter(Boolean).join('');
|
||
return region + (address.detail_address || '');
|
||
},
|
||
|
||
// 加载地址列表
|
||
async loadAddressList() {
|
||
try {
|
||
this.addressList = await fetchAddressList();
|
||
// 自动选择默认地址
|
||
const defaultAddress = this.addressList.find(addr => Number(addr.is_default) === 1);
|
||
if (defaultAddress) {
|
||
this.selectedAddress = defaultAddress;
|
||
} else if (this.addressList.length > 0) {
|
||
// 如果没有默认地址,选择第一个
|
||
this.selectedAddress = this.addressList[0];
|
||
}
|
||
} catch (error) {
|
||
console.error('加载地址列表失败:', error);
|
||
}
|
||
},
|
||
|
||
// 显示地址选择弹窗
|
||
showAddressPicker() {
|
||
if (this.addressList.length === 0) {
|
||
// 如果没有地址,直接跳转到地址管理页面
|
||
this.goToAddressManage();
|
||
} else {
|
||
this.showAddressList = true;
|
||
}
|
||
},
|
||
|
||
// 隐藏地址选择弹窗
|
||
hideAddressPicker() {
|
||
this.showAddressList = false;
|
||
},
|
||
|
||
// 选择地址
|
||
chooseAddress(address) {
|
||
this.selectedAddress = address;
|
||
this.hideAddressPicker();
|
||
},
|
||
|
||
// 跳转到地址管理页面
|
||
goToAddressManage() {
|
||
uni.navigateTo({
|
||
url: '/pages/address/index'
|
||
});
|
||
},
|
||
|
||
// 提交订单
|
||
async submitOrder() {
|
||
if (!this.canSubmit) {
|
||
if (!this.selectedAddress) {
|
||
uni.showToast({
|
||
title: '请选择收货地址',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
this.submitting = true;
|
||
// 获取小程序的appid
|
||
// if (!this.app_id) {
|
||
// const accountInfo = wx.getAccountInfoSync && wx.getAccountInfoSync();
|
||
// this.app_id = accountInfo && accountInfo.miniProgram && accountInfo.miniProgram.appId
|
||
// ? accountInfo.miniProgram.appId
|
||
// : '';
|
||
// }
|
||
try {
|
||
// 构建订单数据
|
||
const orderData = {
|
||
// app_id: this.app_id,
|
||
address_id: this.selectedAddress.id,
|
||
// items: this.orderItems.map(item => ({
|
||
// cart_id: item.cart_id,
|
||
// product_id: item.product_id,
|
||
// sku_id: item.sku_id,
|
||
// quantity: item.quantity,
|
||
// price: item.price
|
||
// })),
|
||
// remark: this.orderRemark,
|
||
// total_price: this.totalPrice,
|
||
// shipping_fee: this.shippingFee,
|
||
// discount_amount: this.discountAmount,
|
||
// final_price: this.finalPrice
|
||
};
|
||
|
||
// 调用创建订单接口
|
||
const result = await request('xcx/order', 'POST', orderData);
|
||
|
||
uni.showToast({
|
||
title: '订单提交成功',
|
||
icon: 'success',
|
||
duration: 2000
|
||
});
|
||
// 发起微信支付
|
||
wx.requestPayment({
|
||
timeStamp: result.time_stamp,
|
||
nonceStr: result.nonce_str,
|
||
package: result.package,
|
||
signType: result.sign_type,
|
||
paySign: result.pay_sign,
|
||
success: () => {
|
||
// console.log('支付成功');
|
||
// // 跳转到订单详情或订单列表页面
|
||
// setTimeout(() => {
|
||
// // 根据实际路由调整
|
||
// uni.redirectTo({
|
||
// url: `/pages/order/detail?id=${result.order_id || result.id}`
|
||
// });
|
||
// }, 2000);
|
||
},
|
||
complete: () => {
|
||
console.log('支付xxx');
|
||
// 跳转到订单详情或订单列表页面
|
||
// setTimeout(() => {
|
||
// 根据实际路由调整
|
||
uni.redirectTo({
|
||
url: `/pages/order/detail?id=${result.order_id || result.id}`
|
||
});
|
||
// }, 2000);
|
||
}
|
||
|
||
});
|
||
|
||
|
||
|
||
} catch (error) {
|
||
console.error('提交订单失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '提交订单失败,请重试',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
} finally {
|
||
this.submitting = false;
|
||
}
|
||
}
|
||
},
|
||
onShow() {
|
||
// 页面显示时,重新加载地址列表(从地址管理页面返回时)
|
||
this.loadAddressList();
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.order-page {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
padding-bottom: 120rpx;
|
||
}
|
||
|
||
/* 收货地址区域 */
|
||
.address-section {
|
||
background-color: #fff;
|
||
margin-bottom: 20rpx;
|
||
padding: 30rpx 24rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.address-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.address-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.receiver-name {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
.receiver-phone {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.address-detail {
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.address-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.address-arrow {
|
||
position: absolute;
|
||
right: 24rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.address-arrow .iconfont {
|
||
font-size: 32rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.address-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.address-empty .iconfont {
|
||
font-size: 36rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.empty-text {
|
||
flex: 1;
|
||
margin-left: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
/* 商品区域 */
|
||
.goods-section {
|
||
background-color: #fff;
|
||
margin-bottom: 20rpx;
|
||
padding: 30rpx 24rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.goods-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.goods-item {
|
||
display: flex;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.goods-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.goods-image {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
margin-right: 24rpx;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.goods-image image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.goods-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.goods-name {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
|
||
.goods-spec {
|
||
margin-top: 8rpx;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.goods-price-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.goods-price {
|
||
color: #e7000b;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.price-symbol {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.price-value {
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.goods-quantity {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
/* 备注区域 */
|
||
.remark-section {
|
||
background-color: #fff;
|
||
margin-bottom: 20rpx;
|
||
padding: 30rpx 24rpx;
|
||
}
|
||
|
||
.remark-input {
|
||
width: 100%;
|
||
min-height: 120rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.6;
|
||
padding: 20rpx;
|
||
background-color: #f8f8f8;
|
||
border-radius: 12rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 价格明细 */
|
||
.price-section {
|
||
background-color: #fff;
|
||
padding: 30rpx 24rpx;
|
||
}
|
||
|
||
.price-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.price-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.price-label {
|
||
color: #666;
|
||
}
|
||
|
||
.price-value {
|
||
color: #333;
|
||
}
|
||
|
||
.price-item.total {
|
||
margin-top: 20rpx;
|
||
padding-top: 20rpx;
|
||
border-top: 2rpx solid #f5f5f5;
|
||
}
|
||
|
||
.price-item.total .price-label {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.total-price {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #e7000b;
|
||
}
|
||
|
||
.discount {
|
||
color: #e7000b;
|
||
}
|
||
|
||
/* 底部提交栏 */
|
||
.order-footer {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 100rpx;
|
||
background-color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 24rpx;
|
||
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||
z-index: 100;
|
||
}
|
||
|
||
.footer-total {
|
||
display: flex;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.total-label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.total-price {
|
||
font-size: 36rpx;
|
||
color: #e7000b;
|
||
font-weight: bold;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
.footer-btn {
|
||
min-width: 200rpx;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
text-align: center;
|
||
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||
color: #fff;
|
||
border-radius: 36rpx;
|
||
font-size: 28rpx;
|
||
padding: 0 40rpx;
|
||
}
|
||
|
||
.footer-btn.disabled {
|
||
background: #ccc;
|
||
color: #999;
|
||
}
|
||
|
||
/* 地址选择弹窗 */
|
||
.address-picker-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.address-picker {
|
||
width: 100%;
|
||
max-height: 80vh;
|
||
background-color: #fff;
|
||
border-radius: 32rpx 32rpx 0 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
animation: slideUp 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
transform: translateY(100%);
|
||
}
|
||
|
||
to {
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.picker-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 30rpx 24rpx;
|
||
border-bottom: 2rpx solid #f5f5f5;
|
||
}
|
||
|
||
.picker-title {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.picker-close {
|
||
font-size: 48rpx;
|
||
color: #999;
|
||
line-height: 1;
|
||
}
|
||
|
||
.picker-content {
|
||
flex: 1;
|
||
max-height: 60vh;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.picker-item {
|
||
position: relative;
|
||
padding: 24rpx;
|
||
border-bottom: 2rpx solid #f5f5f5;
|
||
}
|
||
|
||
.picker-item:active {
|
||
background-color: #f8f8f8;
|
||
}
|
||
|
||
.picker-item-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.picker-receiver {
|
||
font-size: 30rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
.picker-phone {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.picker-tag {
|
||
display: inline-block;
|
||
padding: 4rpx 12rpx;
|
||
background-color: #9810fa;
|
||
color: #fff;
|
||
font-size: 20rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.picker-item-detail {
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.picker-address {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.picker-item-check {
|
||
position: absolute;
|
||
right: 24rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.picker-item-check .iconfont {
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.picker-empty {
|
||
padding: 80rpx 24rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.picker-empty .empty-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.picker-footer {
|
||
padding: 24rpx;
|
||
border-top: 2rpx solid #f5f5f5;
|
||
}
|
||
|
||
.picker-btn {
|
||
width: 100%;
|
||
height: 80rpx;
|
||
line-height: 80rpx;
|
||
text-align: center;
|
||
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||
color: #fff;
|
||
border-radius: 40rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
</style>
|