wx-shop/pages/order/create.vue
2025-12-09 22:57:49 +08:00

785 lines
16 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="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"> 0.00</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>