Files
taimed-international-app/components/order/Confirm.vue

796 lines
18 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="confirm-order-page">
<!-- 商品列表区域 -->
<view class="common-section">
<view class="section-title">{{ $t('order.goodsInfo') }}</view>
<slot name="goodsList" />
</view>
<!-- 订单备注区域 -->
<view class="remark-section common-section" @click="showRemarkPopup = true">
<view class="remark-row">
<text class="remark-label">{{ $t('order.remark') }}</text>
<view class="remark-value">
<text :class="remark ? 'remark-text' : 'remark-placeholder'">
{{ remark || $t('order.remarkPlaceholder') }}
</text>
<image src="/static/icon/icon_right.png" class="arrow-icon" />
</view>
</view>
</view>
<!-- 价格明细区域 -->
<view class="price-section common-section">
<view class="section-title">{{ $t('order.priceDetail') }}</view>
<!-- 商品总价 -->
<view class="price-item">
<text class="label">{{ $t('order.totalPrice') }}</text>
<text class="value">{{ totalPrice.toFixed(2) }} 天医币</text>
</view>
<!-- 优惠券 -->
<!-- <coupon v-model="selectedCoupon" /> -->
<!-- 活动立减 -->
<view v-if="hasActivityDiscount" class="price-item">
<text class="label">{{ $t('order.activityDiscount') }}</text>
<text class="discount-value">-{{ activityDiscountAmount.toFixed(2) }}</text>
</view>
<!-- VIP专享立减 -->
<view v-if="vipPrice > 0" class="price-item">
<view class="label-row">
<text class="vip-icon">VIP</text>
<text class="label">{{ $t('order.vipDiscount') }}</text>
</view>
<text class="discount-value">-{{ vipPrice.toFixed(2) }}</text>
</view>
<!-- 地区优惠 -->
<view v-if="districtAmount > 0" class="price-item">
<text class="label">{{ $t('order.districtDiscount') }}</text>
<text class="discount-value">-{{ districtAmount.toFixed(2) }}</text>
</view>
<!-- 积分 -->
<view v-if="allowPointPay && userInfo.jf > 0" class="price-item">
<view class="label-row">
<image src="/static/icon/jifen.png" class="icon-img" />
<text class="label">{{ $t('order.points') }}</text>
<text class="points-total">
({{ $t('order.allPoints') }}{{ userInfo.jf }})
</text>
</view>
<text class="discount-value">-{{ jfNumber.toFixed(2) }}</text>
</view>
<!-- 积分输入 -->
<view v-if="allowPointPay && userInfo.jf > 0" class="points-input-section">
<text class="points-label">
{{ $t('order.maxPoints', { max: jfNumberMax }) }}
</text>
<view class="points-input-box">
<input
v-model="jfNumber"
type="number"
clearable
:placeholder="$t('order.pointsPlaceholder')"
class="text-right"
@input="handlePointsInput"
@clear="handlePointsClear"
/>
</view>
</view>
</view>
<!-- 支付方式区域 -->
<view class="common-section">
<view class="section-title">{{ $t('order.paymentMethod') }}</view>
<PayWay :peanutCoin="userInfo?.peanutCoin || 0" />
</view>
<!-- 底部汇总栏 -->
<view class="bottom-bar">
<view class="total-info">
<text class="label">{{ $t('order.total') }}</text>
<text class="amount">{{ actualPayment.toFixed(2) }} 天医币</text>
</view>
<wd-button type="primary" :loading="submitting" @click="handleSubmit">
{{ $t('order.submit') }}
</wd-button>
</view>
<!-- 订单备注弹窗 -->
<wd-popup
v-model="showRemarkPopup"
position="bottom"
>
<view class="remark-popup">
<view class="popup-header">{{ $t('order.remarkTitle') }}</view>
<view class="remark-content">
<wd-textarea
v-model="remark"
:placeholder="$t('order.remarkPlaceholder')"
:maxlength="200"
show-word-limit
class="pb-0!"
/>
</view>
<view class="popup-footer">
<wd-button type="primary" block @click="handleRemarkConfirm">{{ $t('global.ok') }}</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { orderApi } from '@/api/modules/order'
import { t } from '@/utils/i18n'
import type { IOrderGoods } from '@/types/order'
import PayWay from '@/components/order/PayWay.vue'
// 使用页面传参
interface Props {
goodsList: IOrderGoods[],
userInfo: object
}
const props = withDefaults(defineProps<Props>(), {
goodsList: () => [],
userInfo: () => ({}),
allowPointPay: () => true
})
// 价格相关
const totalPrice = ref(0)
const vipPrice = ref(0)
const districtAmount = ref(0)
const actualPayment = ref(0)
// 积分相关
const jfNumber = ref(0)
const jfNumberMax = ref(0)
// 优惠券相关
// 订单备注
const remark = ref('')
const showRemarkPopup = ref(false)
// 支付相关
// UI状态
const submitting = ref(false)
watch(() => props.goodsList, () => {
// 初始化价格
calculateAllPrices()
})
/**
* 是否有活动优惠
*/
const hasActivityDiscount = computed(() => {
return props.goodsList.some(item => item.activityPrice && item.activityPrice > 0)
})
/**
* 活动优惠金额
*/
const activityDiscountAmount = computed(() => {
return props.goodsList.reduce((sum, item) => {
if (item.activityPrice && item.activityPrice > 0) {
return sum + (item.price - item.activityPrice) * item.productAmount
}
return sum
}, 0)
})
/**
* 处理商品数量变化
*/
const handleQuantityChange = async (item: IOrderGoods, value: number) => {
try {
// 更新购物车
await orderApi.updateCart({
productId: item.productId,
productAmount: value,
price: item.price
})
// 重新获取商品列表和计算价格
// await getGoodsList()
} catch (error) {
console.error('更新数量失败:', error)
uni.showToast({
title: '更新失败',
icon: 'none'
})
}
}
/**
* 计算所有价格
*/
const calculateAllPrices = async () => {
try {
// 计算商品总价
calculateTotalPrice()
// 获取VIP优惠
// await calculateVipDiscount()
// 获取地区优惠
// await calculateDistrictDiscount()
// 获取优惠券列表
// await getAvailableCoupons()
// 计算最终价格
calculateFinalPrice()
} catch (error) {
console.error('计算价格失败:', error)
}
}
/**
* 计算商品总价
*/
const calculateTotalPrice = () => {
console.log('商品列表:', props.goodsList)
totalPrice.value = props.goodsList.reduce((sum, item) => {
// return sum + (item.price * item.productAmount)
return sum + (item.price * 1)
}, 0)
}
/**
* 计算VIP优惠
*/
const calculateVipDiscount = async () => {
try {
const productList = props.goodsList.map(item => ({
productId: item.productId,
quantity: item.productAmount
}))
const res = await orderApi.getVipDiscountAmount(productList)
if (res.code === 0 && res.data) {
vipPrice.value = res.data.discountAmount
}
} catch (error) {
console.error('获取VIP优惠失败:', error)
}
}
/**
* 计算活动优惠
*/
const calculateDistrictDiscount = async () => {
try {
const productList = props.goodsList.map(item => ({
productId: item.productId,
quantity: item.productAmount
}))
const res = await orderApi.getDistrictAmount(productList)
if (res.code === 0 && res.data) {
districtAmount.value = res.data.districtAmount
}
} catch (error) {
console.error('获取地区优惠失败:', error)
}
}
/**
* 获取可用优惠券
*/
const getAvailableCoupons = async () => {
try {
const shopProductInfos = props.goodsList.map(item => {
const price = item.activityPrice && item.activityPrice > 0 ? item.activityPrice : item.price
return `${item.productId}:${price}:${item.productAmount}`
}).join(',')
const res = await orderApi.getCouponListPayment(shopProductInfos)
if (res.code === 0 && res.data) {
couponList.value = res.data.couponHistoryList || []
// 如果当前选中的优惠券不可用,清除选择
if (selectedCoupon.value) {
const stillAvailable = couponList.value.find(
c => c.couponId === selectedCoupon.value?.couponId && c.canUse === 1
)
if (!stillAvailable) {
selectedCoupon.value = null
}
}
}
} catch (error) {
console.error('获取优惠券失败:', error)
}
}
/**
* 计算最终价格
*/
const calculateFinalPrice = () => {
const couponAmount = selectedCoupon.value?.couponEntity.couponAmount || 0
// 计算最大可用积分
const orderAmountAfterDiscount = totalPrice.value - districtAmount.value - vipPrice.value
jfNumberMax.value = Math.min(
props.userInfo.jf || 0,
Math.floor(orderAmountAfterDiscount - couponAmount)
)
// 限制当前积分不超过最大值
if (jfNumber.value > jfNumberMax.value) {
jfNumber.value = jfNumberMax.value
}
// 计算实付款
actualPayment.value = Math.max(
0,
totalPrice.value - couponAmount - jfNumber.value - districtAmount.value - vipPrice.value
)
}
/**
* 处理积分输入
*/
const handlePointsInput = (value: any) => {
let val = String(value.detail.value)
// 只允许数字字符,去掉小数点
val = val.replace(/[^0-9]/g, '')
if (val === '0' || val === '') {
jfNumber.value = 0
} else {
let numericValue = parseInt(val, 10)
if (numericValue < 0 || isNaN(numericValue)) {
numericValue = 0
}
// 确保不超过最大值
if (numericValue >= jfNumberMax.value) {
numericValue = jfNumberMax.value
}
jfNumber.value = numericValue
}
// 重新计算实付款
calculateFinalPrice()
}
/**
* 清除积分输入
*/
const handlePointsClear = () => {
jfNumber.value = 0
calculateFinalPrice()
}
/**
* 跳转到充值页面
*/
const goToRecharge = () => {
uni.navigateTo({
url: '/pages/user/wallet/recharge/index?source=order'
})
}
/**
* 确认备注
*/
const handleRemarkConfirm = () => {
showRemarkPopup.value = false
}
/**
* 验证订单
*/
const validateOrder = (): boolean => {
// 验证天医币余额
if (actualPayment.value > props.userInfo.peanutCoin) {
uni.showToast({
title: t('order.insufficientBalance'),
icon: 'none'
})
return false
}
return true
}
/**
* 提交订单
*/
const handleSubmit = async () => {
if (submitting.value) {
uni.showToast({
title: t('order.tooFrequent'),
icon: 'none'
})
return
}
// 验证订单
if (!validateOrder()) {
return
}
try {
submitting.value = true
// 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
uni.showToast({
title: t('order.orderSuccess'),
icon: 'success'
})
} catch (error) {
console.error('提交订单失败:', error)
uni.showToast({
title: t('order.orderFailed'),
icon: 'none'
})
} finally {
submitting.value = false
}
}
/**
* 创建订单
*/
const createOrder = async (): Promise<string | null> => {
try {
const orderParams = {
userId: props.userInfo.id,
paymentMethod: 4, // 天医币
orderMoney: totalPrice.value,
realMoney: actualPayment.value,
jfDeduction: jfNumber.value,
// couponId: selectedCoupon.value?.id,
// couponName: selectedCoupon.value?.couponEntity.couponName,
// vipDiscountAmount: vipPrice.value,
// districtMoney: districtAmount.value,
remark: remark.value,
productList: props.goodsList.map(item => ({
productId: item.productId,
quantity: item.productAmount || 1
})),
orderType: 'order',
come: 10
}
const res = await orderApi.placeCourseOrder(orderParams)
if (res.code === 0 && res.orderSn) {
return res.orderSn
}
return null
} catch (error) {
console.error('创建订单失败:', error)
throw error
}
}
</script>
<style lang="scss" scoped>
.confirm-order-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 60px;
}
.common-section {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.price-section {
.price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
font-size: 28rpx;
.label-row {
display: flex;
align-items: center;
gap: 10rpx;
.label {
color: #666;
}
.vip-icon {
padding: 2rpx 8rpx;
background-color: #f94f04;
color: #fff;
font-size: 20rpx;
border-radius: 4rpx;
font-weight: bold;
}
.icon-img {
width: 40rpx;
height: 40rpx;
}
.points-total {
font-size: 24rpx;
color: #aaa;
}
}
.label {
color: #666;
}
.value {
color: #333;
font-weight: 500;
}
.value-row {
display: flex;
align-items: center;
gap: 10rpx;
}
.discount-value {
color: #fe6035;
font-weight: 500;
}
}
.points-input-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15rpx 0;
margin-top: 10rpx;
background-color: #f5f5f5;
border-radius: 20rpx;
padding: 20rpx;
.points-label {
font-size: 28rpx;
color: #7dc1f0;
font-weight: 600;
}
.points-input-box {
width: 320rpx;
}
}
}
.goods-section {
.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;
}
}
}
}
}
.remark-section {
.remark-row {
display: flex;
justify-content: space-between;
align-items: center;
.remark-label {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.remark-value {
display: flex;
align-items: center;
gap: 10rpx;
max-width: 500rpx;
.remark-text {
font-size: 24rpx;
color: #333;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remark-placeholder {
font-size: 24rpx;
color: #b0b0b0;
}
.arrow-icon {
width: 24rpx;
height: 24rpx;
}
}
}
}
.remark-popup {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #f0f0f0;
.popup-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
}
.remark-content {
min-height: 300rpx;
}
.popup-footer {
padding: 20rpx;
border-top: 1px solid #f0f0f0;
}
}
.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>