Files
taimed-international-app/components/order/Confirm.vue
2025-12-03 14:10:27 +08:00

726 lines
17 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">{{ totalAmount.toFixed(2) }} {{ t('global.coin') }}</text>
</view>
<!-- 优惠券 -->
<!-- <coupon v-model="selectedCoupon" /> -->
<!-- 活动立减 -->
<view v-if="promotionDiscounted > 0" class="price-item">
<text class="label">{{ $t('order.promotionDiscounted') }}</text>
<text class="discount-value">-{{ promotionDiscounted.toFixed(2) }} {{ t('global.coin') }}</text>
</view>
<!-- VIP专享立减 -->
<view v-if="vipDiscounted > 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">-{{ vipDiscounted.toFixed(2) }} {{ t('global.coin') }}</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 || 0 }})
</text>
</view>
<text class="discount-value">-{{ pointsDiscounted.toFixed(2) }}</text>
</view>
<!-- 积分输入 -->
<view v-if="allowPointPay && userInfo?.jf > 0" class="points-input-section">
<text class="points-label">
{{ $t('order.maxPoints').replace('max', pointsUsableMax) }}
</text>
<view class="points-input-box">
<input
v-model="pointsDiscounted"
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">{{ finalAmount }} {{ t('global.coin') }}</text>
</view>
<wd-button type="primary" @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="showRemarkPopup = false">{{ $t('global.ok') }}</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { orderApi } from '@/api/modules/order'
import { getUserInfo } from '@/api/modules/user'
import { useUserStore } from '@/stores/user'
import { t } from '@/utils/i18n'
import type { IGoods, IGoodsDiscountParams } from '@/types/order'
import PayWay from '@/components/order/PayWay.vue'
const userStore = useUserStore()
// 使用页面传参
interface Props {
goodsList: IGoods[],
userInfo: object,
allowPointPay?: boolean,
orderType?: string,
backStep?: number // 购买完成后返回几层页面
}
const props = withDefaults(defineProps<Props>(), {
goodsList: () => [],
userInfo: () => ({}),
allowPointPay: true,
orderType: 'order',
backStep: 1
})
// 订单备注
const remark = ref('')
const showRemarkPopup = ref(false)
// 价格相关
const totalAmount = ref(0) // 商品总价
const vipDiscounted = ref(0) // VIP专享立减
const promotionDiscounted = ref(0) // 活动优惠金额
const couponDiscounted = ref(0) // 优惠券优惠金额
const finalAmount = ref<string | number>('--') // 最终支付金额
// 积分相关
const pointsDiscounted = ref(0) // 积分支付金额
const pointsUsableMax = ref(0) // 最大可用积分
// 优惠券相关
/**
* 优惠价格的请求参数
*/
const goodsListParams = ref<IGoodsDiscountParams[]>([])
const getDiscountParams = (goodsList: IGoods[]) => {
goodsListParams.value = goodsList.map(item => ({
productId: item.productId,
quantity: item.productAmount || 1
}))
}
// 监听商品列表变化,更新价格
watch(() => props.goodsList, () => {
getDiscountParams(props.goodsList)
// 初始化价格
calculateAllPrices()
})
/**
* 计算所有价格
*/
const calculateAllPrices = async () => {
try {
// 计算商品总价
calculateTotalPrice()
props.orderType === 'order' && await Promise.all([
// 获取VIP优惠
calculateVipDiscounted(),
// 获取活动优惠
calculatePromotionDiscounted()
// 获取优惠券列表
// await getAvailableCoupons()
])
// 计算最终价格
calculateFinalPrice()
} catch (error) {
console.error('计算价格失败:', error)
}
}
/**
* 计算商品总价
*/
const calculateTotalPrice = () => {
totalAmount.value = props.goodsList.reduce((sum: number, item: IGoods) => {
// return sum + (item.price * item.productAmount)
return sum + (item.price * 1)
}, 0)
}
/**
* 计算VIP优惠
*/
const calculateVipDiscounted = async () => {
const res = await orderApi.getVipDiscountAmount(goodsListParams.value)
vipDiscounted.value = res.discountAmount
}
/**
* 计算活动优惠
*/
const calculatePromotionDiscounted = async () => {
const res = await orderApi.getDistrictAmount(goodsListParams.value)
promotionDiscounted.value = res.districtAmount
}
/**
* 获取可用优惠券
*/
// 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 handlePointsInput = (value: any) => {
let val = String(value.detail.value)
// 只允许数字字符,去掉小数点
val = val.replace(/[^0-9]/g, '')
if (val === '0' || val === '') {
pointsDiscounted.value = 0
} else {
let numericValue = parseInt(val, 10)
if (numericValue < 0 || isNaN(numericValue)) {
numericValue = 0
}
// 确保不超过最大值
if (numericValue >= pointsUsableMax.value) {
numericValue = pointsUsableMax.value
}
pointsDiscounted.value = numericValue
}
// 重新计算实付款
const result = Math.max(
0,
totalAmount.value - pointsDiscounted.value - promotionDiscounted.value - vipDiscounted.value
)
finalAmount.value = result
}
/**
* 清除积分输入
*/
const handlePointsClear = () => {
pointsDiscounted.value = 0
calculateFinalPrice()
}
/**
* 计算最终价格
*/
const calculateFinalPrice = () => {
// const couponAmount = selectedCoupon.value?.couponEntity.couponAmount || 0
const couponAmount = 0
// 计算最大可用积分
const orderAmountAfterDiscount = totalAmount.value - promotionDiscounted.value - vipDiscounted.value
pointsUsableMax.value = Math.min(
props?.userInfo?.jf || 0,
Math.floor(orderAmountAfterDiscount - couponAmount)
)
pointsDiscounted.value = pointsUsableMax.value
// 限制当前积分不超过最大值
if (pointsDiscounted.value >= pointsUsableMax.value) {
pointsDiscounted.value = pointsUsableMax.value
}
// 计算实付款
const result = Math.max(
0,
totalAmount.value - couponAmount - pointsDiscounted.value - promotionDiscounted.value - vipDiscounted.value
)
finalAmount.value = result
}
/**
* 验证订单
*/
const validateOrder = (): boolean => {
// 验证实付金额是否计算完成
if (isNaN(parseFloat(finalAmount.value))) {
uni.showToast({
title: t('order.invalidPaymentAmount'),
icon: 'none'
})
return false
}
// 验证天医币余额
if (finalAmount.value > props.userInfo.peanutCoin) {
uni.showToast({
title: t('order.insufficientBalance'),
icon: 'none'
})
return false
}
return true
}
/**
* 提交订单
*/
const handleSubmit = async () => {
// 验证订单
if (!validateOrder()) return
// 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
// 重新获取用户信息更新store和本地缓存
const res = await getUserInfo()
userStore.setUserInfo(res.result)
uni.showToast({
title: t('order.orderSuccess'),
icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack({
delta: props.backStep
})
}, 500)
}
/**
* 创建订单
*/
const createOrder = async (): Promise<string | null> => {
const orderParams = {
userId: props.userInfo.id,
paymentMethod: 4, // 天医币
orderMoney: totalAmount.value,
realMoney: finalAmount.value,
jfDeduction: pointsDiscounted.value,
// couponId: selectedCoupon.value?.id,
// couponName: selectedCoupon.value?.couponEntity.couponName,
vipDiscountAmount: vipDiscounted.value,
districtMoney: promotionDiscounted.value,
remark: remark.value,
orderType: props.orderType,
productList: props.orderType === 'order' ? goodsListParams.value : null,
vipBuyConfigId: props.orderType === 'vip' ? props.goodsList[0].productId : null,
abroadVipId: props.orderType === 'abroadVip' ? props.goodsList[0].productId : null,
come: 10
}
const res = await orderApi.placeOrder(orderParams)
if (res.orderSn) {
return res.orderSn
}
return null
}
</script>
<style lang="scss" scoped>
.confirm-order-page {
min-height: calc(100vh - 60px - 40rpx);
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>