This commit is contained in:
2025-11-25 17:19:25 +08:00
20 changed files with 1828 additions and 740 deletions

View File

@@ -307,7 +307,7 @@ function closePurchasePopup() {
// 处理购买
function handlePurchase() {
uni.navigateTo({
url: `/pages/book/order?id=${bookId.value}`
url: `/pages/order/goodsConfirm?goods=${bookId.value}`
})
}

View File

@@ -449,24 +449,13 @@ const closeGoodsSelector = () => {
*/
const confirmPurchase = () => {
showProtocol.value = false
uni.showToast({ icon: 'none', title: '订单及支付功能开发中' })
return false
if (!selectedGoods.value) return
showProtocol.value = false
// 跳转到确认订单页
const orderData = {
goods: [{ ...selectedGoods.value, productAmount: 1 }],
typeId: 0,
navTitle: courseDetail.value?.title,
title: courseDetail.value?.title,
isFudu: isFudu.value,
fuduId: isFudu.value ? fuduCatalogueId.value : undefined
}
uni.navigateTo({
url: `/pages/course/order?data=${encodeURIComponent(JSON.stringify(orderData))}`
url: `/pages/order/goodsConfirm?goods=${selectedGoods.value.productId}`
})
}

View File

@@ -1,672 +0,0 @@
<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>

View File

@@ -0,0 +1,239 @@
<template>
<view>
<!-- 自定义导航栏 -->
<nav-bar :title="$t('order.confirmTitle')" />
<!-- 确认订单组件 -->
<Confirm :goodsList="goodsList" :userInfo="userInfo">
<template #goodsList>
<!-- 商品列表内容 -->
<view
v-for="(item, index) in goodsList"
:key="index"
class="goods-item"
>
<!-- VIP优惠标签 -->
<wd-tag v-if="item.isVipPrice === 1 && item.vipPrice" type="danger" mark custom-class="vip-badge">{{ $t('order.vipLabel') }}</wd-tag>
<!-- 商品图片 -->
<view class="goods-image">
<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">{{ $t('order.vipPriceLabel') }}</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">{{ $t('order.activityLabel') }}</text>
<text class="original-price">{{ item.price.toFixed(2) }}</text>
</view> -->
<!-- 普通价格 -->
<view class="price-row">
<text class="normal-price">{{ item.price.toFixed(2) }} 天医币</text>
</view>
</view>
<!-- 数量 -->
<!-- <view class="quantity-row">
<text class="quantity-label">{{ $t('order.quantity') }}</text>
<view v-if="showNumber" class="quantity-control">
<wd-number-box
v-model="item.productAmount"
:min="1"
@change="handleQuantityChange(item, $event)"
/>
</view>
<text v-else>X {{ item.productAmount }}</text>
</view> -->
</view>
</view>
</template>
</Confirm>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { orderApi } from '@/api/modules/order'
import Confirm from '@/components/order/Confirm.vue';
import type { IOrderGoods } from '@/types/order'
/**
* 获取用户信息
*/
const userInfo = ref(null)
const getUserInfo = async () => {
try {
const res = await orderApi.getUserInfo()
if (res.code === 0) {
userInfo.value = res.result || {}
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
/**
* 获取商品列表
*/
const goodsIds = ref<string>('')
const goodsList = ref<IOrderGoods[]>([])
const getGoodsList = async () => {
try {
// 获取商品详情
const res = await orderApi.getShopProductListByIds(goodsIds.value)
if (res.code === 0 && res.shopProductList?.length > 0) {
goodsList.value = res.shopProductList
}
} catch (error) {
console.error('获取商品列表失败:', error)
}
}
/**
* 页面加载
*/
onLoad(async (options: any) => {
if (options.goods) {
try {
// 获取用户信息
await getUserInfo()
// 根据商品ID获取商品详细信息
goodsIds.value = options.goods || ''
getGoodsList()
} catch (error) {
console.error('解析商品数据失败:', error)
uni.showToast({
title: '商品数据错误',
icon: 'none'
})
}
}
})
</script>
<style lang="scss" scoped>
.goods-item {
position: relative;
display: flex;
padding-bottom: 20rpx;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
.vip-badge {
position: absolute;
top: 20rpx;
left: 0;
z-index: 1;
}
.goods-image {
width: 140rpx;
height: 140rpx;
flex-shrink: 0;
margin-right: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.goods-info {
flex: 1;
display: flex;
flex-direction: column;
.goods-name {
font-size: 28rpx;
color: #333;
line-height: 1.4;
margin-bottom: 10rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.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-row {
display: flex;
align-items: center;
font-size: 24rpx;
color: #999;
.quantity-label {
margin-right: 10rpx;
}
.quantity-control {
display: flex;
align-items: center;
}
}
}
}
</style>