Files
integral-shop/single_uniapp22miao/pages/integral/confirm.vue

1100 lines
28 KiB
Vue
Raw Normal View History

<template>
<view class="confirm-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="nav-back" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">提交订单</text>
<view class="nav-placeholder"></view>
</view>
<!-- 主内容区 -->
<view class="main-content">
<!-- 收货地址 -->
<view class="section-card address-section" @click="onAddress">
<view class="section-header">
<text class="section-title">收货地址</text>
</view>
<view class="address-row">
<view v-if="addressInfo.id" class="address-content">
<view class="address-info">
<text class="receiver-name">{{ addressInfo.realName }}</text>
<text class="receiver-phone">{{ addressInfo.phone }}</text>
</view>
<view class="address-detail-row">
<!-- <text v-if="addressInfo.isDefault" class="default-tag">[默认]</text> -->
<text class="address-detail">{{ fullAddress }}</text>
</view>
</view>
<view v-else class="no-address">
<text>设置收货地址</text>
</view>
<!-- <view class="arrow-icon">
<text class="iconfont icon-jiantou"></text>
</view> -->
</view>
</view>
<!-- 商品信息 -->
<view class="section-card goods-section">
<view class="section-header">
<text class="section-title-bold">共计商品</text>
</view>
<view v-for="(item, index) in cartInfo" :key="index" class="goods-item">
<image :src="item.image" class="goods-image" mode="aspectFill" />
<view class="goods-info">
<text class="goods-name">{{ item.name || item.storeName }}</text>
<!-- <view class="goods-tags">
<text class="goods-tag">积分购买兑换</text>
</view> -->
<view class="goods-bottom">
<text class="goods-points">{{ item.points || item.integral }}积分</text>
<text class="goods-quantity">x {{ item.quantity || item.cartNum }}</text>
</view>
</view>
</view>
</view>
<!-- 使用积分 -->
<view class="section-card points-section" @click="toggleUseIntegral">
<view class="option-row">
<text class="option-label">使用积分</text>
<view class="option-value">
<text class="points-text">约为积分{{ totalIntegral }}</text>
<view class="checkbox" :class="{ checked: useIntegral }">
<text v-if="useIntegral" class="check-icon"></text>
</view>
</view>
</view>
</view>
<!-- 快递费用 -->
<view class="section-card express-section">
<view class="option-row">
<text class="option-label">快递费用</text>
<text class="express-free">免运费</text>
</view>
</view>
<!-- 商品数量 -->
<view class="section-card quantity-section">
<view class="option-row">
<text class="option-label">商品数量</text>
<text class="quantity-value">{{ orderProNum }}</text>
</view>
</view>
</view>
<!-- 底部提交栏 -->
<view class="submit-bar safe-area-inset-bottom">
<view class="total-section">
<text class="total-label">合计</text>
<view class="total-price-row">
<text class="total-price">{{ formatNumber(totalIntegral) }}积分</text>
<text class="balance-text">余额: {{ formatNumber(userIntegral) }}积分</text>
</view>
</view>
<view
class="submit-btn"
:class="{ disabled: submitting || !canSubmit }"
@click="SubOrder"
>
<text v-if="submitting">提交中...</text>
<text v-else>立即兑换</text>
</view>
</view>
</view>
</template>
<script>
import {
getCartList,
preOrderApi,
orderCreate,
orderPay,
loadPreOrderApi
} from '@/api/order.js'
import {
getAddressDetail,
getAddressDefault,
getAddressList,
getUserInfo
} from '@/api/user.js'
import { getProductDetail } from '@/api/store.js'
import { mapGetters } from "vuex"
import { toLogin } from '@/libs/login.js'
export default {
data() {
return {
// 页面来源
from: '',
// 商品ID
goodsId: 0,
// 购物车ID列表从URL参数获取
cartIds: '',
// 商品数量
quantity: 1,
// 商品列表
cartInfo: [],
// 商品总数
orderProNum: 0,
// 地址信息
addressInfo: {},
// 地址ID
addressId: 0,
// 地址变更ID从地址选择页返回
addressChangeId: 0,
// 用户积分余额
userIntegral: 0,
// 是否使用积分
useIntegral: true,
// 最大数量
maxQuantity: 50,
// 备注信息
mark: '',
// 提交中
submitting: false,
// 预下单订单号
preOrderNo: '',
// 支付渠道
payChannel: ''
}
},
computed: {
...mapGetters(['isLogin', 'userInfo']),
// 完整地址
fullAddress() {
if (!this.addressInfo.id) return ''
return `${this.addressInfo.province || ''}${this.addressInfo.city || ''}${this.addressInfo.district || ''}${this.addressInfo.detail || ''}`
},
// 总积分
totalIntegral() {
return this.cartInfo.reduce((sum, item) => {
const points = item.points || item.integral || 0
const num = item.quantity || item.cartNum || 1
return sum + points * num
}, 0)
},
// 是否可以提交
canSubmit() {
return this.addressInfo.id &&
this.cartInfo.length > 0 &&
this.userIntegral >= this.totalIntegral
}
},
watch: {
isLogin: {
handler: function(newV, oldV) {
if (newV) {
this.initPage()
}
},
deep: true
}
},
onLoad(options) {
// 设置支付渠道
// #ifdef H5
this.payChannel = this.$wechat && this.$wechat.isWeixin() ? 'public' : 'weixinh5'
// #endif
// #ifdef MP
this.payChannel = 'routine'
// #endif
// #ifdef APP-PLUS
this.payChannel = 'weixinAppAndroid'
// #endif
this.from = options.from || ''
this.goodsId = options.id || 0
this.cartIds = options.cartIds || '' // 获取购物车ID列表
this.quantity = parseInt(options.quantity) || 1
this.addressChangeId = options.addressId || 0
if (this.isLogin) {
this.initPage()
} else {
toLogin()
}
},
onShow() {
// 从地址选择页返回时更新地址
uni.$on('bindSelectAddress', (res) => {
if (res && res.addressId) {
this.addressId = res.addressId
this.getAddressInfo()
}
uni.$off('bindSelectAddress')
})
// 兼容 storage 方式
const selectedAddress = uni.getStorageSync('selected_address')
if (selectedAddress) {
this.addressInfo = selectedAddress
this.addressId = selectedAddress.id
uni.removeStorageSync('selected_address')
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
},
// 格式化数字(添加千分位)
formatNumber(num) {
if (!num && num !== 0) return '0'
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
},
// 初始化页面
async initPage() {
// 加载商品数据
await this.loadGoodsData()
// 获取用户积分余额
this.getUserIntegral()
// 获取地址信息
if (this.addressChangeId) {
this.addressId = this.addressChangeId
this.getAddressInfo()
} else {
this.getDefaultAddress()
}
},
// 获取用户积分余额(从用户信息中获取)
async getUserIntegral() {
try {
// 优先从 vuex store 获取
if (this.userInfo && this.userInfo.integral !== undefined) {
this.userIntegral = this.userInfo.integral || 0
}
// 刷新用户信息获取最新积分
const res = await getUserInfo()
if (res.data) {
// 更新 vuex store
this.$store.commit('UPDATE_USERINFO', res.data)
this.userIntegral = res.data.integral || 0
}
} catch (error) {
console.error('获取用户积分失败:', error)
// 降级处理:尝试从本地存储获取
const userInfoCache = uni.getStorageSync('USER_INFO')
if (userInfoCache) {
const info = typeof userInfoCache === 'string' ? JSON.parse(userInfoCache) : userInfoCache
if (info && info.integral !== undefined) {
this.userIntegral = info.integral || 0
}
}
}
},
// 加载商品数据
async loadGoodsData() {
if (this.from === 'cart') {
// 从购物车来,读取选中的商品
await this.loadCartItems()
} else {
// 单个商品购买包括有goodsId或有buy_now_goods缓存的情况
await this.loadSingleItem()
}
},
// 加载购物车商品
async loadCartItems() {
// 解析选中的购物车ID列表
const selectedIds = this.cartIds ? this.cartIds.split(',').map(id => parseInt(id)) : []
console.log('选中的购物车ID:', selectedIds)
if (selectedIds.length === 0) {
// 尝试从缓存读取
const checkoutItems = uni.getStorageSync('checkout_items')
if (checkoutItems) {
this.parseCartItems(checkoutItems)
return
}
this.showTips('请选择商品')
setTimeout(() => uni.navigateBack(), 1500)
return
}
try {
uni.showLoading({ title: '加载中...' })
// 从API获取购物车列表
const res = await getCartList({
page: 1,
limit: 50,
isValid: true
})
console.log('购物车列表响应:', res)
if (res.data && res.data.list) {
const allItems = res.data.list || []
// 筛选出选中的商品
const selectedItems = allItems.filter(item => selectedIds.includes(item.id))
console.log('筛选后的商品:', selectedItems)
if (selectedItems.length > 0) {
this.parseCartItems(selectedItems)
} else {
this.showTips('未找到选中的商品')
setTimeout(() => uni.navigateBack(), 1500)
}
} else {
this.showTips('获取购物车失败')
}
} catch (error) {
console.error('加载购物车商品失败:', error)
this.showTips('加载商品失败')
} finally {
uni.hideLoading()
}
},
// 解析购物车商品数据
parseCartItems(items) {
const itemList = typeof items === 'string' ? JSON.parse(items) : items
this.cartInfo = itemList.map(item => {
// 积分字段优先级vipPrice > price > integral > points
const pointsValue = item.vipPrice || item.price || item.integral || item.points || 0
return {
id: item.id,
goodsId: item.productId || item.goodsId || item.id,
name: item.storeName || item.name,
image: item.image || item.pic,
points: pointsValue,
integral: pointsValue,
quantity: item.cartNum || item.quantity || 1,
cartNum: item.cartNum || item.quantity || 1,
attrValueId: item.productAttrUnique || item.attrValueId || item.unique || ''
}
})
// 计算商品总数
this.orderProNum = this.cartInfo.reduce((sum, item) => {
return sum + (item.quantity || item.cartNum || 1)
}, 0)
console.log('解析后的商品列表:', this.cartInfo)
console.log('商品总数:', this.orderProNum)
},
// 加载单个商品
async loadSingleItem() {
// 从详情页传递的商品信息
const goodsInfo = uni.getStorageSync('buy_now_goods')
if (goodsInfo) {
try {
const goods = typeof goodsInfo === 'string' ? JSON.parse(goodsInfo) : goodsInfo
let attrValueId = goods.attrValueId || goods.unique || goods.productAttrUnique || ''
// 如果没有规格信息,获取商品默认规格
if (!attrValueId && (goods.id || this.goodsId)) {
attrValueId = await this.getDefaultAttrValueId(goods.id || this.goodsId)
}
this.cartInfo = [{
id: goods.id || this.goodsId,
goodsId: goods.id || this.goodsId,
name: goods.name || goods.storeName,
image: goods.image || goods.pic,
points: goods.points || goods.integral,
integral: goods.integral || goods.points,
quantity: this.quantity,
cartNum: this.quantity,
attrValueId: attrValueId
}]
this.orderProNum = this.quantity
// 使用后删除缓存
uni.removeStorageSync('buy_now_goods')
} catch (error) {
console.error('解析商品信息失败:', error)
this.showTips('商品信息加载失败')
}
} else if (this.goodsId) {
// 只有商品ID需要获取商品详情
await this.fetchGoodsDetailAndSetCart()
} else {
// 没有商品信息,提示错误
this.showTips('请选择要购买的商品')
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
// 获取商品默认规格ID
async getDefaultAttrValueId(productId) {
try {
const res = await getProductDetail(productId, 1)
if (res.data && res.data.productValue) {
const productValue = res.data.productValue
// 找到第一个有库存的规格
for (let key in productValue) {
if (productValue[key].stock > 0) {
return productValue[key].id || ''
}
}
// 如果都没库存,返回第一个规格
const firstKey = Object.keys(productValue)[0]
if (firstKey && productValue[firstKey]) {
return productValue[firstKey].id || ''
}
}
} catch (error) {
console.error('获取商品规格失败:', error)
}
return ''
},
// 获取商品详情并设置购物车
async fetchGoodsDetailAndSetCart() {
try {
uni.showLoading({ title: '加载中...' })
const res = await getProductDetail(this.goodsId, 1)
uni.hideLoading()
if (res.data && res.data.productInfo) {
const productInfo = res.data.productInfo
const productValue = res.data.productValue || {}
// 获取默认规格
let attrValueId = ''
let price = productInfo.price
let image = productInfo.image
for (let key in productValue) {
if (productValue[key].stock > 0) {
attrValueId = productValue[key].id || ''
price = productValue[key].price || price
image = productValue[key].image || image
break
}
}
// 如果没找到有库存的,用第一个
if (!attrValueId) {
const firstKey = Object.keys(productValue)[0]
if (firstKey && productValue[firstKey]) {
attrValueId = productValue[firstKey].id || ''
price = productValue[firstKey].price || price
}
}
this.cartInfo = [{
id: this.goodsId,
goodsId: this.goodsId,
name: productInfo.storeName,
image: image,
points: price,
integral: price,
quantity: this.quantity,
cartNum: this.quantity,
attrValueId: attrValueId
}]
this.orderProNum = this.quantity
} else {
throw new Error('商品信息不存在')
}
} catch (error) {
uni.hideLoading()
console.error('获取商品详情失败:', error)
this.showTips('商品信息加载失败')
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
},
// 获取默认地址
async getDefaultAddress() {
try {
const res = await getAddressDefault()
if (res.data && res.data.id) {
this.addressInfo = res.data
this.addressId = res.data.id
} else {
// 如果没有默认地址,尝试获取地址列表中的第一个
const listRes = await getAddressList()
if (listRes.data && listRes.data.length > 0) {
const firstAddr = listRes.data[0]
this.addressInfo = firstAddr
this.addressId = firstAddr.id
}
}
} catch (error) {
console.error('获取默认地址失败:', error)
}
},
// 根据地址ID获取地址信息
async getAddressInfo() {
if (!this.addressId) return
try {
const res = await getAddressDetail(this.addressId)
if (res.data) {
this.addressInfo = res.data
}
} catch (error) {
console.error('获取地址详情失败:', error)
}
},
// 跳转选择地址
onAddress() {
uni.navigateTo({
url: '/pages/sub-pages/address/index?from=integral&cartId=' + (this.goodsId || '')
})
},
// 切换使用积分
toggleUseIntegral() {
this.useIntegral = !this.useIntegral
},
// 提交订单(参考 order_confirm 的 SubOrder 方法)
SubOrder() {
let that = this
// 验证收货地址
if (!that.addressId && !that.addressInfo.id) {
return that.showTips('请选择收货地址')
}
// 验证商品
if (that.cartInfo.length === 0) {
return that.showTips('请选择商品')
}
// 验证积分余额
if (that.userIntegral < that.totalIntegral) {
return that.showTips('积分余额不足')
}
// 防止重复提交
if (that.submitting) return
// 开始创建订单
that.createOrder()
},
// 创建订单(参考 order_confirm 使用 @api/order.js 接口)
async createOrder() {
this.submitting = true
uni.showLoading({ title: '提交中...', mask: true })
try {
// 第一步:创建预下单
const preOrderData = {
preOrderType: 'buyNow',
orderDetails: this.cartInfo.map(item => ({
productId: item.goodsId || item.id,
attrValueId: item.attrValueId || '',
productNum: item.quantity || item.cartNum || 1
}))
}
// 如果是从购物车来的使用购物车ID
if (this.from === 'cart' && this.cartIds) {
preOrderData.preOrderType = 'shoppingCart'
preOrderData.orderDetails = this.cartInfo.map(item => ({
shoppingCartId: item.id,
productId: item.goodsId || item.id,
attrValueId: item.attrValueId || '',
productNum: item.quantity || item.cartNum || 1
}))
}
console.log('预下单数据:', preOrderData)
const preOrderRes = await preOrderApi(preOrderData)
console.log('预下单响应:', preOrderRes)
if (!preOrderRes.data || !preOrderRes.data.preOrderNo) {
throw new Error(preOrderRes.msg || '预下单失败')
}
this.preOrderNo = preOrderRes.data.preOrderNo
// 第二步:创建订单
const orderData = {
preOrderNo: this.preOrderNo,
addressId: this.addressId || this.addressInfo.id,
couponId: 0,
useIntegral: this.useIntegral, // 使用积分
mark: this.mark,
shippingType: 1, // 1=快递配送
storeId: 0
}
console.log('创建订单数据:', orderData)
const orderRes = await orderCreate(orderData)
console.log('创建订单响应:', orderRes)
if (!orderRes.data || !orderRes.data.orderNo) {
throw new Error(orderRes.msg || '创建订单失败')
}
const orderNo = orderRes.data.orderNo
// 第三步:使用积分支付(余额支付方式)
await this.payWithIntegral(orderNo)
} catch (error) {
uni.hideLoading()
console.error('创建订单失败:', error)
this.showTips(error.message || error.msg || '提交失败,请重试')
this.submitting = false
}
},
// 使用积分支付
async payWithIntegral(orderNo) {
try {
const payData = {
orderNo: orderNo,
payChannel: this.payChannel || 'public',
payType: 'yue', // 余额支付(积分支付)
scene: 0
}
console.log('支付数据:', payData)
const payRes = await orderPay(payData)
console.log('支付响应:', payRes)
uni.hideLoading()
// 清除购物车缓存
if (this.from === 'cart') {
uni.removeStorageSync('checkout_items')
}
uni.removeStorageSync('buy_now_goods')
// 判断支付结果
if (payRes.data && payRes.data.payType === 'yue') {
// 余额/积分支付成功
uni.showToast({
title: '兑换成功',
icon: 'success'
})
// 跳转到订单详情或订单列表
setTimeout(() => {
uni.redirectTo({
url: `/pages/integral/orders?status=-99`
})
}, 1500)
} else {
throw new Error(payRes.msg || '支付失败')
}
} catch (error) {
uni.hideLoading()
console.error('支付失败:', error)
this.showTips(error.message || error.msg || '支付失败,请重试')
} finally {
this.submitting = false
}
},
// 显示提示
showTips(title) {
if (this.$util && this.$util.Tips) {
return this.$util.Tips({ title: title })
} else {
uni.showToast({
title: title,
icon: 'none'
})
}
}
}
}
</script>
<style lang="scss" scoped>
// 主题色变量
$primary-color: #F54900;
$text-primary: #0A0A0A;
$text-secondary: #4A5565;
$text-tertiary: #6A7282;
$text-muted: #99A1AF;
$bg-color: #F9FAFB;
$card-bg: #FFFFFF;
$border-color: rgba(0, 0, 0, 0.1);
.confirm-page {
min-height: 100vh;
background-color: $card-bg;
display: flex;
flex-direction: column;
}
// 自定义导航栏
.custom-navbar {
background-color: $card-bg;
height: 112rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid $border-color;
flex-shrink: 0;
}
.nav-back {
width: 72rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
.back-icon {
font-size: 56rpx;
color: $text-primary;
font-weight: 300;
line-height: 1;
}
}
.nav-title {
flex: 1;
font-size: 32rpx;
color: $text-primary;
font-weight: 400;
text-align: center;
}
.nav-placeholder {
width: 72rpx;
}
// 主内容区
.main-content {
flex: 1;
background-color: $bg-color;
padding: 0 32rpx;
padding-bottom: 180rpx;
}
// 卡片通用样式
.section-card {
margin-top: 24rpx;
padding: 32rpx;
background-color: $card-bg;
border-radius: 20rpx;
&:first-child {
margin-top: 24rpx;
}
}
.section-header {
margin-bottom: 0;
}
.section-title {
font-size: 32rpx;
font-weight: 400;
color: $text-primary;
}
.section-title-bold {
font-size: 36rpx;
font-weight: 500;
color: $text-primary;
}
// 收货地址
.address-section {
padding: 32rpx;
}
.address-row {
display: flex;
align-items: flex-start;
margin-top: 24rpx;
}
.address-content {
flex: 1;
}
.address-info {
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 8rpx;
.receiver-name {
font-size: 30rpx;
font-weight: 500;
color: $text-primary;
}
.receiver-phone {
font-size: 28rpx;
color: $text-secondary;
}
}
.address-detail-row {
display: flex;
align-items: flex-start;
.default-tag {
font-size: 24rpx;
color: $primary-color;
margin-right: 12rpx;
flex-shrink: 0;
}
}
.address-detail {
font-size: 28rpx;
color: $text-secondary;
line-height: 1.5;
}
.no-address {
flex: 1;
text {
font-size: 28rpx;
color: $text-tertiary;
}
}
.arrow-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-top: 8rpx;
.iconfont, .icon-jiantou {
font-size: 36rpx;
color: $text-tertiary;
font-weight: 300;
}
}
// 商品信息
.goods-section {
padding: 32rpx;
}
.goods-item {
display: flex;
gap: 24rpx;
margin-top: 24rpx;
&:first-child {
margin-top: 24rpx;
}
}
.goods-image {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
background-color: #F5F5F5;
flex-shrink: 0;
}
.goods-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
min-height: 160rpx;
}
.goods-name {
font-size: 28rpx;
color: $text-primary;
font-weight: 500;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
overflow: hidden;
}
.goods-tags {
margin-top: 0;
}
.goods-tag {
font-size: 28rpx;
color: $text-tertiary;
}
.goods-bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.goods-points {
font-size: 32rpx;
font-weight: 400;
color: $primary-color;
}
.goods-quantity {
font-size: 32rpx;
color: $text-tertiary;
}
// 使用积分
.points-section {
padding: 32rpx;
}
.option-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.option-label {
font-size: 32rpx;
font-weight: 400;
color: $text-primary;
}
.option-value {
display: flex;
align-items: center;
gap: 16rpx;
}
.points-text {
font-size: 32rpx;
color: $primary-color;
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 3rpx solid $primary-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.checked {
background-color: transparent;
border-color: $primary-color;
}
.check-icon {
color: $primary-color;
font-size: 24rpx;
font-weight: 600;
}
}
// 快递费用
.express-section {
padding: 32rpx;
}
.express-free {
font-size: 32rpx;
}
// 商品数量
.quantity-section {
padding: 32rpx;
}
.quantity-value {
font-size: 32rpx;
font-weight: 400;
color: $text-primary;
}
// 底部提交栏
.submit-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: $card-bg;
border-top: 1rpx solid $border-color;
padding: 24rpx 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
padding-bottom: calc(24rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
}
.total-section {
display: flex;
flex-direction: column;
}
.total-label {
font-size: 28rpx;
color: $text-tertiary;
line-height: 1.4;
}
.total-price-row {
display: flex;
align-items: baseline;
gap: 16rpx;
}
.total-price {
font-size: 40rpx;
font-weight: 500;
color: $primary-color;
line-height: 1.4;
}
.balance-text {
font-size: 24rpx;
color: $text-muted;
}
.submit-btn {
background-color: $primary-color;
color: $card-bg;
font-size: 28rpx;
font-weight: 500;
padding: 24rpx 64rpx;
border-radius: 16rpx;
&.disabled {
background-color: #CCCCCC;
}
&:active {
opacity: 0.9;
}
}
</style>