Files
yxe-mp/pages/order/checkout.vue
T
2026-09-29 15:17:13 +08:00

769 lines
20 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="checkout-page">
<view class="page-hero">
<AppNavbar class="page-navbar" title="订单结算" show-back/>
</view>
<view class="card block">
<view class="section-title">收货信息</view>
<view class="address" @tap="openAddressPopup">
<view>
<view class="addr-name">
<text v-if="selectedAddress">{{ selectedAddress.name }} {{ selectedAddress.phone }}</text>
<text v-else>请选择收货地址</text>
</view>
<view class="addr-detail">
<text v-if="selectedAddress">{{ getFullAddress(selectedAddress) }}</text>
<text v-else>点击选择或新增收货地址</text>
</view>
</view>
<view class="address-actions">
<view v-if="defaultSelected" class="chip">默认</view>
<view class="choose">更换</view>
</view>
</view>
</view>
<view class="card block">
<view class="section-title">商品信息</view>
<view class="item" v-for="(item, index) in goodsList" :key="index">
<view class="flex justify-start items-start">
<u-image :src="item.image" width="80rpx" height="80rpx" radius="15rpx"/>
<view class="ml-15">
<view class="item-name">{{ item.goodsName }}</view>
<view class="item-meta">x {{ item.num }}</view>
</view>
</view>
<view class="item-price">¥{{ (item.num * item.salePrice).toFixed(2) }}</view>
</view>
<!-- <view class="vendor-sum">小计:¥{{ vendor.subtotal.toFixed(2) }}</view>-->
</view>
<view class="card block">
<view class="section-title">优惠</view>
<view class="row">
<view>优惠券</view>
<view class="coupon" @tap="openCoupon">{{ couponText }}</view>
</view>
<!-- <view class="row">-->
<!-- <view>发票</view>-->
<!-- <switch :checked="invoice" color="#2563eb" @change="invoice = $event.detail.value"/>-->
<!-- </view>-->
</view>
<view class="card block">
<view class="section-title">费用明细</view>
<view class="row">
<view>商品金额</view>
<view>¥{{ goodsTotal }}</view>
</view>
<!-- <view class="row">-->
<!-- <view>运费</view>-->
<!-- <view>¥{{ freightTotal }}</view>-->
<!-- </view>-->
<view class="row">
<view>优惠</view>
<view>-¥{{ discount }}</view>
</view>
<view class="row total">
<view>应付总额</view>
<view class="pay">¥{{ payTotal }}</view>
</view>
</view>
<view class="action-bar">
<view class="sum">合计:¥{{ payTotal }}</view>
<view class="primary-btn submit" @tap="submitOrder">提交订单</view>
</view>
<view v-if="showAddressPopup" class="address-popup-mask" @tap="closeAddressPopup"></view>
<view v-if="showAddressPopup" class="address-popup card" @tap.stop>
<view class="popup-head">
<view class="section-title">选择收货地址</view>
<view class="close-btn" @tap="closeAddressPopup">关闭</view>
</view>
<view v-if="addressLoading" class="popup-empty">加载中...</view>
<view v-else-if="addressList.length === 0" class="popup-empty">
<view class="popup-empty-text">暂无地址</view>
<u-button type="primary" shape="circle" size="mini" @tap="goAddAddress">去新增</u-button>
</view>
<view v-else class="popup-list">
<view
v-for="item in addressList"
:key="item.id"
class="popup-item"
:class="{ active: isSelectedAddress(item) }"
@tap="selectAddress(item)"
>
<view class="popup-item-head">
<view class="addr-name">{{ item.name }} {{ item.phone }}</view>
<view v-if="isDefaultAddress(item)" class="chip">默认</view>
</view>
<view class="addr-detail">{{ getFullAddress(item) }}</view>
</view>
</view>
</view>
<view v-if="showCouponPopup" class="address-popup-mask" @tap="closeCouponPopup"></view>
<view v-if="showCouponPopup" class="coupon-popup card" @tap.stop>
<view class="popup-head">
<view class="section-title">选择优惠券</view>
<view class="close-btn" @tap="closeCouponPopup">关闭</view>
</view>
<view v-if="couponLoading" class="popup-empty">加载中...</view>
<view v-else-if="couponList.length === 0" class="popup-empty">
<view class="popup-empty-text">暂无可用优惠券</view>
</view>
<view v-else class="coupon-list">
<view
v-for="item in couponList"
:key="item.id"
class="coupon-item"
:class="{ active: isSelectedCoupon(item), disabled: !item.available }"
@tap="selectCoupon(item)"
>
<view class="coupon-item-main">
<!-- <view class="coupon-amount">¥{{ item.amount }}</view>-->
<view class="coupon-info">
<view class="coupon-title">{{ item.title }}</view>
<!-- <view class="coupon-desc">{{ item.desc }}</view>-->
<view class="coupon-expire">
有效期:{{ item.expireType === 1 ? item.expireEndDate : `领取后${item.expireDay}天过期` }}
</view>
</view>
</view>
<view class="coupon-right">
<view v-if="!item.available" class="coupon-status">{{ item.statusText }}</view>
<view v-else-if="isSelectedCoupon(item)" class="chip">已选</view>
</view>
</view>
</view>
<view class="coupon-footer">
<view class="text-btn" @tap="clearCoupon">不使用优惠券</view>
</view>
</view>
</view>
</template>
<script>
import {goodsListByIdsReq, orderAddReq} from "@/api/goods";
import {userAddressListReq} from "@/api/address";
import {userCouponListReq} from "@/api/coupon";
import AppNavbar from "@/components/AppNavbar.vue";
export default {
components: {AppNavbar},
data() {
return {
vendors: [],
goodsList: [],
goodsDataList: [],
invoice: false,
addressList: [],
addressLoading: false,
showAddressPopup: false,
selectedAddress: null,
couponSourceList: [],
couponList: [],
couponLoading: false,
showCouponPopup: false,
selectedCoupon: null
}
},
computed: {
goodsTotal() {
return this.goodsList.reduce((sum, v) => sum + parseFloat(v.num * v.salePrice), 0).toFixed(2)
},
freightTotal() {
return 0
},
orderAmountValue() {
return parseFloat(this.goodsTotal) + parseFloat(this.freightTotal)
},
payTotal() {
return (parseFloat(this.goodsTotal) + parseFloat(this.freightTotal) - parseFloat(this.discount)).toFixed(2)
},
defaultSelected() {
return this.selectedAddress ? this.isDefaultAddress(this.selectedAddress) : false
},
discount() {
if (!this.selectedCoupon || !this.selectedCoupon.available) return '0.00'
const orderAmount = this.orderAmountValue
const typeValue = Number(this.selectedCoupon.type)
let discountAmount = 0
if (typeValue === 1) {
let rateValue = Number(this.selectedCoupon.reduceRate || 0)
if (rateValue > 1) {
rateValue = rateValue > 10 ? rateValue / 100 : rateValue / 10
}
rateValue = Math.max(0, Math.min(rateValue, 1))
discountAmount = orderAmount * (1 - rateValue)
} else {
discountAmount = Number(this.selectedCoupon.reduceAmount || 0)
}
const maxDiscount = Math.min(discountAmount, orderAmount)
return maxDiscount.toFixed(2)
},
couponText() {
if (!this.selectedCoupon) return '选择优惠券'
const title = this.selectedCoupon.title || '优惠券'
return `${title} -¥${this.discount}`
}
},
watch: {
orderAmountValue() {
this.refreshCouponList()
}
},
async onLoad(options) {
const rawGoods = options.goodsDataList ? decodeURIComponent(options.goodsDataList) : '[]'
try {
this.goodsDataList = JSON.parse(rawGoods)
} catch (err) {
this.goodsDataList = []
}
await this.getGoodsList()
await this.loadAddressList()
},
methods: {
async getGoodsList() {
if (!this.goodsDataList.length) {
this.vendors = []
return
}
const {data} = await goodsListByIdsReq({
ids: this.goodsDataList.map(item => item.goodsId).join()
})
this.goodsList = data.map(item => {
const goodsData = this.goodsDataList.find(goodsData => goodsData.goodsId === item.goodsId || goodsData.goodsId === item.id)
return {
num: goodsData.num,
...item,
}
})
this.$forceUpdate()
console.log(this.goodsList)
},
async loadAddressList() {
this.addressLoading = true
try {
const {data} = await userAddressListReq()
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
this.addressList = list
const defaultItem = list.find(item => this.isDefaultAddress(item))
this.selectedAddress = defaultItem || list[0] || null
} catch (err) {
uni.showToast({
title: '地址加载失败',
icon: 'none'
})
} finally {
this.addressLoading = false
}
},
isDefaultAddress(item) {
return Number(item.isDefault) === 1 || Number(item.is_default) === 1 || item.is_default === true
},
openAddressPopup() {
this.showAddressPopup = true
if (!this.addressList.length) {
this.loadAddressList()
}
},
closeAddressPopup() {
this.showAddressPopup = false
},
selectAddress(item) {
this.selectedAddress = item
this.showAddressPopup = false
},
goAddAddress() {
this.showAddressPopup = false
uni.navigateTo({
url: '/packageUser/address/index'
})
},
isSelectedAddress(item) {
if (!this.selectedAddress) return false
const selectedId = this.selectedAddress.id || this.selectedAddress.addressId || this.selectedAddress.full_address
const itemId = item.id || item.addressId || item.full_address
return selectedId && itemId ? selectedId === itemId : this.getFullAddress(item) === this.getFullAddress(this.selectedAddress)
},
getFullAddress(item) {
if (!item) return ''
return item.fullAddress || item.full_address || `${item.province || ''}${item.city || ''}${item.region || ''}${item.address || ''}`
},
async loadCouponList() {
this.couponLoading = true
try {
const {data} = await userCouponListReq()
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
this.couponSourceList = list
this.refreshCouponList()
} catch (err) {
uni.showToast({
title: '优惠券加载失败',
icon: 'none'
})
} finally {
this.couponLoading = false
}
},
openCoupon() {
this.showCouponPopup = true
this.loadCouponList()
},
closeCouponPopup() {
this.showCouponPopup = false
},
selectCoupon(item) {
if (!item.available) {
uni.showToast({
title: '该优惠券不可用',
icon: 'none'
})
return
}
this.selectedCoupon = item
this.showCouponPopup = false
},
clearCoupon() {
this.selectedCoupon = null
this.showCouponPopup = false
},
isSelectedCoupon(item) {
return this.selectedCoupon && item.id && this.selectedCoupon.id === item.id
},
refreshCouponList() {
if (!this.couponSourceList.length) return
this.couponList = this.couponSourceList.map(this.normalizeUserCoupon)
if (this.selectedCoupon) {
const matched = this.couponList.find(item => item.id === this.selectedCoupon.id)
this.selectedCoupon = matched && matched.available ? matched : null
}
},
normalizeUserCoupon(item) {
const coupon = item.coupon || {}
const amount = Number(coupon.amount || item.amount || item.faceValue || item.money || 0)
const statusValue = Number(item.used || item.status)
let available = true
let statusText = '可用'
if (statusValue === 1) {
available = false
statusText = '已使用'
}
if (statusValue === 2) {
available = false
statusText = '已过期'
}
if (!Number.isFinite(statusValue)) {
statusText = '可用'
available = true
}
const startAmount = Number(coupon.startAmount || item.startAmount || 0)
const orderAmount = this.orderAmountValue
if (available && orderAmount < startAmount) {
available = false
statusText = '未满足门槛'
}
return {
id: item.id || item.userCouponId || item.couponId || item.coupon_id,
amount: amount.toFixed(2),
title: coupon.title || item.title || item.name || '优惠券',
desc: item.desc || item.description || item.useRule || '无门槛使用',
expire: item.expire || item.expireTime || item.endTime || '-',
available,
statusText,
expireType: coupon.expireType,
expireEndDate: coupon.expireEndDate,
expireDay: coupon.expireDay,
startAmount,
type: coupon.type || item.type || 0,
reduceAmount: coupon.reduceAmount || item.reduceAmount || 0,
reduceRate: coupon.reduceRate || item.reduceRate || 0
}
},
async submitOrder() {
if (!this.selectedAddress) {
uni.showToast({
title: '请先选择收货地址',
icon: 'none'
})
return
}
uni.showLoading()
const payload = {
goodsList: this.goodsDataList,
name: this.selectedAddress.name,
phone: this.selectedAddress.phone,
address: this.getFullAddress(this.selectedAddress),
}
if (this.selectedCoupon && this.selectedCoupon.id) {
payload.couponId = this.selectedCoupon.id
}
const res = await orderAddReq(payload).catch(err => {
uni.hideLoading()
})
if (res) {
console.log(res)
uni.hideLoading()
uni.requestPayment({
provider: res.data.provider, // 服务提供商
timeStamp: res.data.timeStamp, // 时间戳
nonceStr: res.data.nonceStr, // 随机字符串
package: res.data.package,
signType: res.data.signType, // 签名算法
paySign: res.data.paySign, // 签名
success: function (res) {
uni.showToast({
title: '支付成功',
icon: 'success'
});
setTimeout(function () {
uni.navigateTo({
url: '/pages/order/list'
});
}, 500);
},
fail: function (err) {
console.log('支付失败', err);
uni.navigateTo({
url: '/pages/order/list'
});
}
})
}
}
}
}
</script>
<style lang="scss" scoped>
.checkout-page {
min-height: 100vh;
//padding: 0 20rpx 100rpx;
background: #f5f5f5;
}
.page-hero {
//padding: 30rpx 6rpx 18rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat top center / 100% auto;
}
.page-navbar {
padding: 0 6rpx;
}
.intro-card {
margin-top: 18rpx;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.block {
margin: 18rpx;
}
.address {
display: flex;
justify-content: space-between;
align-items: center;
}
.addr-name {
font-weight: 700;
color: var(--text-main);
}
.addr-detail {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 4rpx;
}
.address-actions {
display: flex;
align-items: center;
gap: 12rpx;
}
.choose {
color: var(--brand-primary);
font-size: 24rpx;
}
.vendor-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6rpx;
}
.vendor-name {
font-weight: 700;
}
.freight {
color: var(--text-sub);
font-size: 24rpx;
}
.item {
display: flex;
justify-content: space-between;
padding: 10rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.item:last-child {
border-bottom: none;
}
.item-name {
font-weight: 700;
color: var(--text-main);
}
.item-meta {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 4rpx;
}
.item-price {
color: var(--brand-primary);
font-weight: 700;
}
.vendor-sum {
text-align: right;
color: var(--text-main);
font-weight: 700;
margin-top: 8rpx;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.row:last-child {
border-bottom: none;
}
.coupon {
color: #ff8354;
}
.total {
font-weight: 800;
}
.pay {
color: #ff3b30;
font-size: 32rpx;
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 24rpx;
background: #ffffff;
box-shadow: 0 -6rpx 16rpx rgba(31, 111, 232, 0.08);
}
.address-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 30;
}
.address-popup {
position: fixed;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
width: 88%;
max-height: 70vh;
overflow: auto;
z-index: 40;
}
.popup-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.popup-empty {
color: var(--text-sub);
text-align: center;
padding: 16rpx 0;
}
.popup-empty-text {
margin-bottom: 12rpx;
}
.popup-empty-btn {
display: inline-flex;
padding: 10rpx 20rpx;
height: 52rpx;
font-size: 24rpx;
}
.popup-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.popup-item {
border: 1rpx solid #e7eef8;
border-radius: 14rpx;
padding: 12rpx;
}
.popup-item.active {
border-color: var(--brand-primary);
box-shadow: 0 8rpx 18rpx rgba(31, 111, 232, 0.1);
}
.popup-item-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.coupon-popup {
position: fixed;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
width: 88%;
max-height: 70vh;
overflow: auto;
z-index: 40;
}
.coupon-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.coupon-item {
display: flex;
justify-content: space-between;
align-items: center;
border: 1rpx solid #e7eef8;
border-radius: 14rpx;
padding: 12rpx;
background: #ffffff;
}
.coupon-item.active {
border-color: var(--brand-primary);
box-shadow: 0 8rpx 18rpx rgba(31, 111, 232, 0.1);
}
.coupon-item.disabled {
opacity: 0.6;
}
.coupon-item-main {
display: flex;
align-items: center;
gap: 12rpx;
}
.coupon-amount {
font-size: 30rpx;
font-weight: 800;
color: #ff3b30;
min-width: 100rpx;
text-align: center;
}
.coupon-info {
flex: 1;
}
.coupon-title {
font-weight: 700;
color: var(--text-main);
}
.coupon-desc,
.coupon-expire {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 2rpx;
}
.coupon-right {
min-width: 70rpx;
display: flex;
justify-content: flex-end;
}
.coupon-status {
color: var(--text-sub);
font-size: 24rpx;
}
.coupon-footer {
margin-top: 12rpx;
text-align: center;
}
.text-btn {
color: var(--brand-primary);
font-size: 24rpx;
}
.close-btn {
color: var(--text-sub);
font-size: 24rpx;
}
.sum {
color: var(--text-main);
font-weight: 800;
}
.submit {
padding: 14rpx 24rpx;
}
</style>