Files
taimed-international-app/pages/course/order.vue

673 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="course-order-page">
<!-- 自定义导航栏 -->
<nav-bar :title="$t('courseOrder.orderTitle')" />
<!-- 页面内容 -->
<view class="page-content">
<!-- 商品列表 -->
<view class="goods-section">
<view class="section-title">商品信息</view>
<view
v-for="(item, index) in goodsList"
:key="index"
class="goods-item"
>
<view class="goods-image">
<!-- VIP优惠标签 -->
<view
v-if="item.isVipPrice === 1 && item.vipPrice"
class="vip-badge"
>
VIP优惠
</view>
<image
:src="item.productImages || '/static/nobg.jpg'"
mode="aspectFit"
/>
</view>
<view class="goods-info">
<text class="goods-name">{{ item.productName }}</text>
<!-- 价格信息 -->
<view class="price-info">
<!-- VIP优惠价 -->
<view v-if="item.isVipPrice === 1 && item.vipPrice" class="price-row">
<text class="vip-price">{{ item.vipPrice.toFixed(2) }}</text>
<text class="vip-label">VIP到手价</text>
<text class="original-price">{{ item.price.toFixed(2) }}</text>
</view>
<!-- 活动价 -->
<view v-else-if="item.activityPrice && item.activityPrice > 0" class="price-row">
<text class="activity-price">{{ item.activityPrice.toFixed(2) }}</text>
<text class="activity-label">活动价</text>
<text class="original-price">{{ item.price.toFixed(2) }}</text>
</view>
<!-- 普通价格 -->
<view v-else class="price-row">
<text class="normal-price">{{ item.price.toFixed(2) }}</text>
</view>
</view>
<!-- 数量 -->
<view class="quantity">
<text>数量{{ item.productAmount || 1 }}</text>
</view>
</view>
</view>
</view>
<!-- 收货地址仅实体商品显示 -->
<view v-if="!isHideAddress" class="address-section">
<view class="section-title">收货地址</view>
<view v-if="selectedAddress" class="address-info" @click="selectAddress">
<view class="address-row">
<text class="name">{{ selectedAddress.name }}</text>
<text class="phone">{{ selectedAddress.phone }}</text>
</view>
<view class="address-detail">
{{ selectedAddress.province }} {{ selectedAddress.city }} {{ selectedAddress.district }} {{ selectedAddress.detail }}
</view>
</view>
<view v-else class="no-address" @click="selectAddress">
<wd-icon name="add-circle" size="24px" />
<text>添加收货地址</text>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info-section">
<view class="info-row">
<text class="label">商品金额</text>
<text class="value">{{ goodsAmount.toFixed(2) }}</text>
</view>
<view v-if="!isHideAddress" class="info-row">
<text class="label">运费</text>
<text class="value">{{ freight.toFixed(2) }}</text>
</view>
<view v-if="availablePoints > 0" class="info-row">
<text class="label">可用积分</text>
<text class="value">{{ availablePoints }} 可抵扣 {{ pointsDiscount.toFixed(2) }}</text>
</view>
</view>
<!-- 订单总价 -->
<view class="total-section">
<view class="total-row">
<text class="label">订单总价</text>
<text class="value">{{ totalAmount.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="total-info">
<text class="label">合计</text>
<text class="amount">{{ totalAmount.toFixed(2) }}</text>
</view>
<wd-button
type="primary"
:loading="submitting"
@click="submitOrder"
>
提交订单
</wd-button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
import NavBar from '@/components/nav-bar/nav-bar.vue'
import type { IOrderGoods, IOrderInitData } from '@/types/course'
interface OrderData {
goods: IOrderGoods[]
typeId?: number
navTitle?: string
title?: string
isFudu?: boolean
fuduId?: number
isVip?: boolean
}
// Stores
const userStore = useUserStore()
// 页面数据
const orderData = ref<OrderData>({
goods: []
})
const goodsList = ref<IOrderGoods[]>([])
const initData = ref<IOrderInitData | null>(null)
const selectedAddress = ref<any>(null)
const isHideAddress = ref(false)
const availablePoints = ref(0)
const freight = ref(0)
const submitting = ref(false)
/**
* 商品金额
*/
const goodsAmount = computed(() => {
return goodsList.value.reduce((sum, item) => {
const price = item.isVipPrice === 1 && item.vipPrice
? item.vipPrice
: item.activityPrice || item.price
return sum + price * (item.productAmount || 1)
}, 0)
})
/**
* 积分抵扣金额
*/
const pointsDiscount = computed(() => {
// 假设100积分可抵扣1元
return Math.min(availablePoints.value / 100, goodsAmount.value * 0.1)
})
/**
* 订单总价
*/
const totalAmount = computed(() => {
return Math.max(0, goodsAmount.value + freight.value - pointsDiscount.value)
})
/**
* 页面加载
*/
onLoad(async (options: any) => {
if (options.data) {
try {
orderData.value = JSON.parse(decodeURIComponent(options.data))
goodsList.value = orderData.value.goods || []
await initOrder()
} catch (error) {
console.error('解析订单数据失败:', error)
uni.showToast({
title: '订单数据错误',
icon: 'none'
})
}
}
})
/**
* 初始化订单
*/
const initOrder = async () => {
try {
uni.showLoading({ title: '加载中...' })
// 根据订单类型调用不同的初始化接口
if (orderData.value.isVip) {
// VIP订单
await initVipOrder()
} else if (orderData.value.isFudu) {
// 复读订单
await initFuduOrder()
} else {
// 普通订单
await initNormalOrder()
}
} catch (error) {
console.error('初始化订单失败:', error)
} finally {
uni.hideLoading()
}
}
/**
* 初始化普通订单
*/
const initNormalOrder = async () => {
try {
const res = await uni.request({
url: uni.getStorageSync('baseURL') + 'common/buyOrder/initPrepareOrder',
method: 'POST',
header: {
'Content-Type': 'application/json',
'token': userStore.token
},
data: {
uid: userStore.userInfo.id,
productList: goodsList.value.map(item => ({
productId: item.productId,
quantity: item.productAmount || 1
}))
}
})
const data = res.data as any
if (data.code === 0 && data.data) {
initData.value = data.data
isHideAddress.value = data.data.is_course || false
availablePoints.value = data.data.user?.jf || 0
// 如果有默认地址,设置为选中地址
if (data.data.addressList && data.data.addressList.length > 0) {
selectedAddress.value = data.data.addressList.find((addr: any) => addr.isDefault === 1)
|| data.data.addressList[0]
}
}
} catch (error) {
console.error('初始化普通订单失败:', error)
}
}
/**
* 初始化复读订单
*/
const initFuduOrder = async () => {
// 复读订单不需要地址
isHideAddress.value = true
// 获取用户信息
try {
const res = await uni.request({
url: uni.getStorageSync('baseURL') + 'common/user/getUserInfo',
method: 'POST',
header: {
'Content-Type': 'application/json',
'token': userStore.token
},
data: {}
})
const data = res.data as any
if (data.code === 0 && data.result) {
initData.value = {
user: data.result,
is_course: true
}
availablePoints.value = data.result.jf || 0
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
/**
* 初始化VIP订单
*/
const initVipOrder = async () => {
// VIP订单不需要地址
isHideAddress.value = true
// 获取用户信息
try {
const res = await uni.request({
url: uni.getStorageSync('baseURL') + 'common/user/getUserInfo',
method: 'POST',
header: {
'Content-Type': 'application/json',
'token': userStore.token
},
data: {}
})
const data = res.data as any
if (data.code === 0 && data.result) {
initData.value = {
user: data.result,
is_course: true
}
// VIP订单不使用积分
availablePoints.value = 0
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
/**
* 选择地址
*/
const selectAddress = () => {
// 跳转到地址选择页面
uni.navigateTo({
url: '/pages/user/address/list'
})
}
/**
* 提交订单
*/
const submitOrder = async () => {
// 验证
if (!isHideAddress.value && !selectedAddress.value) {
uni.showToast({
title: '请选择收货地址',
icon: 'none'
})
return
}
try {
submitting.value = true
let orderUrl = ''
let orderParams: any = {}
if (orderData.value.isVip) {
// VIP订单
orderUrl = 'common/userVip/placeVipOrder'
orderParams = {
vipConfigId: goodsList.value[0].productId,
payType: 1 // 默认支付方式
}
} else if (orderData.value.isFudu) {
// 复读订单
orderUrl = 'common/courseRelearn/relearnSave'
orderParams = {
catalogueId: orderData.value.fuduId,
productId: goodsList.value[0].productId,
quantity: 1
}
} else {
// 普通订单
orderUrl = 'book/buyOrder/placeOrder'
orderParams = {
uid: userStore.userInfo.id,
addressId: selectedAddress.value?.id,
productList: goodsList.value.map(item => ({
productId: item.productId,
quantity: item.productAmount || 1
})),
usePoints: pointsDiscount.value > 0
}
}
const res = await uni.request({
url: uni.getStorageSync('baseURL') + orderUrl,
method: 'POST',
header: {
'Content-Type': 'application/json',
'token': userStore.token
},
data: orderParams
})
const data = res.data as any
if (data.code === 0) {
uni.showToast({
title: '订单创建成功',
icon: 'success'
})
// 跳转到支付页面或订单详情
setTimeout(() => {
uni.redirectTo({
url: `/pages/user/order/index`
})
}, 1500)
} else {
uni.showToast({
title: data.errMsg || '订单创建失败',
icon: 'none'
})
}
} catch (error) {
console.error('提交订单失败:', error)
uni.showToast({
title: '提交失败',
icon: 'none'
})
} finally {
submitting.value = false
}
}
</script>
<style lang="scss" scoped>
.course-order-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.page-content {
padding: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
}
.goods-section {
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
.goods-item {
display: flex;
padding: 20rpx 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.goods-image {
position: relative;
width: 140rpx;
height: 140rpx;
flex-shrink: 0;
margin-right: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
overflow: hidden;
.vip-badge {
position: absolute;
top: 0;
left: 0;
padding: 4rpx 10rpx;
background-color: #f94f04;
color: #fff;
font-size: 20rpx;
border-radius: 8rpx 0 8rpx 0;
z-index: 1;
}
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;
line-height: 1.4;
margin-bottom: 10rpx;
}
.price-info {
.price-row {
display: flex;
align-items: baseline;
gap: 10rpx;
.vip-price,
.activity-price {
font-size: 32rpx;
font-weight: bold;
color: #e97512;
}
.normal-price {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.vip-label {
font-size: 22rpx;
color: #fa2d12;
}
.activity-label {
font-size: 22rpx;
color: #613804;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
}
}
.quantity {
font-size: 24rpx;
color: #999;
}
}
}
}
.address-section {
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
.address-info {
padding: 20rpx;
background-color: #f7f8f9;
border-radius: 8rpx;
.address-row {
display: flex;
justify-content: space-between;
margin-bottom: 10rpx;
.name {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.phone {
font-size: 28rpx;
color: #666;
}
}
.address-detail {
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
}
.no-address {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 40rpx 20rpx;
background-color: #f7f8f9;
border-radius: 8rpx;
color: #999;
font-size: 28rpx;
}
}
.order-info-section {
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
.info-row {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
font-size: 28rpx;
.label {
color: #666;
}
.value {
color: #333;
font-weight: 500;
}
}
}
.total-section {
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
.label {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.value {
font-size: 36rpx;
color: #ff4444;
font-weight: bold;
}
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background-color: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
z-index: 999;
.total-info {
flex: 1;
.label {
font-size: 28rpx;
color: #666;
}
.amount {
font-size: 36rpx;
color: #ff4444;
font-weight: bold;
margin-left: 10rpx;
}
}
}
</style>