610 lines
15 KiB
Vue
610 lines
15 KiB
Vue
<template>
|
||
<view class="payment-page">
|
||
<!-- 自定义导航栏 -->
|
||
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="nav-content">
|
||
<view class="nav-left" @click="goBack">
|
||
<uni-icons type="left" size="24" color="#333"></uni-icons>
|
||
</view>
|
||
<view class="nav-title">课程支付</view>
|
||
<view class="nav-right"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 页面内容 -->
|
||
<view class="page-content" :style="{ paddingTop: navBarHeight + 'px' }">
|
||
<!-- 课程信息卡片 -->
|
||
<view class="course-card">
|
||
<view class="course-left">
|
||
<image :src="courseInfo.image" mode="aspectFill" class="course-image"></image>
|
||
</view>
|
||
<view class="course-right">
|
||
<text class="course-title">{{ courseInfo.title }}</text>
|
||
<view class="course-tags">
|
||
<view class="tag-item" v-for="(tag, index) in courseInfo.tags" :key="index">
|
||
<text class="tag-text">{{ tag }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="course-price">¥{{ courseInfo.price.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 支付方式选择 -->
|
||
<view class="payment-methods">
|
||
<view class="method-item" :class="{ disabled: pointsValueYuan <= 0 }" @click="togglePaymentMethod('points')">
|
||
<view class="method-info">
|
||
<text class="method-label">账户积分({{ userInfo.points }}积分)</text>
|
||
<text class="method-desc" v-if="userInfo.points > 0">约 ¥{{ (userInfo.points / userInfo.pointsExchangeRate).toFixed(2) }} 元</text>
|
||
</view>
|
||
<view class="radio-button" :class="{ active: usePoints }">
|
||
<view class="radio-inner" v-if="usePoints"></view>
|
||
</view>
|
||
</view>
|
||
<view class="method-item" :class="{ disabled: balanceYuan <= 0 }" @click="togglePaymentMethod('balance')">
|
||
<view class="method-info">
|
||
<text class="method-label">账户余额(¥{{ (userInfo.balance/100).toFixed(2) }} 元)</text>
|
||
</view>
|
||
<view class="radio-button" :class="{ active: useBalance }">
|
||
<view class="radio-inner" v-if="useBalance"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 支付明细 -->
|
||
<view class="payment-summary">
|
||
<view class="summary-item">
|
||
<text class="summary-label">课程总金额</text>
|
||
<text class="summary-value">¥{{ courseInfo.price.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="summary-item" v-if="usePoints && pointsUsedYuan > 0">
|
||
<text class="summary-label">使用积分</text>
|
||
<text class="summary-value">-¥{{ pointsUsedYuan.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="summary-item" v-if="useBalance && balanceUsedYuan > 0">
|
||
<text class="summary-label">使用余额</text>
|
||
<text class="summary-value">-¥{{ balanceUsedYuan.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="summary-item total">
|
||
<text class="summary-label">合计:</text>
|
||
<text class="summary-value total-price">¥{{ finalPrice.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部支付栏 -->
|
||
<view class="bottom-payment">
|
||
<view class="payment-left">
|
||
<text class="final-price">¥{{ finalPrice.toFixed(2) }}</text>
|
||
<!-- <text class="payment-method" v-if="selectedMethod === 'points'">使用积分支付</text>
|
||
<text class="payment-method" v-if="selectedMethod === 'balance'">使用余额支付</text> -->
|
||
</view>
|
||
<view class="payment-right">
|
||
<button class="pay-button" @click="confirmPayment" :disabled="!canPay">实际支付</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import course_api from "@/api/course_api.js"
|
||
import { requestPayment } from '@/utils/payment.js'
|
||
|
||
// 响应式数据
|
||
const statusBarHeight = ref(0)
|
||
const navBarHeight = ref(88)
|
||
const selectedMethod = ref('points') // 默认选择积分支付(兼容旧逻辑)
|
||
const usePoints = ref(true) // 是否使用积分
|
||
const useBalance = ref(false) // 是否使用余额
|
||
const courseInfo = ref({
|
||
title: '找到已购买课程即学习找到已购买课程即可学习',
|
||
price: 39.00,
|
||
tags: ['佑安医院', '临床思维', '陈煜'],
|
||
image: '/static/icon_home_my_patient.png'
|
||
})
|
||
const userInfo = ref({
|
||
points: 0,
|
||
balance: 0.00,
|
||
pointsExchangeRate: 50 // 积分兑换比例,50积分=1元
|
||
})
|
||
|
||
// 计算属性
|
||
// 金额与抵扣(单位元)
|
||
const pointsValueYuan = computed(() => {
|
||
const rate = userInfo.value.pointsExchangeRate || 50
|
||
if (!rate) return 0
|
||
return (userInfo.value.points || 0) / rate
|
||
})
|
||
const balanceYuan = computed(() => {
|
||
// 后端余额单位为分
|
||
return (userInfo.value.balance || 0) / 100
|
||
})
|
||
|
||
// 组合抵扣:先用积分再用余额
|
||
const pointsUsedYuan = computed(() => {
|
||
const price = courseInfo.value.price || 0
|
||
if (!usePoints.value || price <= 0) return 0
|
||
return Math.min(pointsValueYuan.value, price)
|
||
})
|
||
const balanceUsedYuan = computed(() => {
|
||
const price = courseInfo.value.price || 0
|
||
if (!useBalance.value || price <= 0) return 0
|
||
const remainAfterPoints = Math.max(0, price - pointsUsedYuan.value)
|
||
return Math.min(balanceYuan.value, remainAfterPoints)
|
||
})
|
||
const finalPrice = computed(() => {
|
||
const price = courseInfo.value.price || 0
|
||
const remain = Math.max(0, price - pointsUsedYuan.value - balanceUsedYuan.value)
|
||
return Number(remain.toFixed(2))
|
||
})
|
||
const canPay = computed(() => {
|
||
const price = courseInfo.value.price || 0
|
||
if (price === 0) return true // 免费
|
||
if (finalPrice.value > 0) return true // 第三方可支付剩余
|
||
return (usePoints.value || useBalance.value)
|
||
})
|
||
|
||
// 方法
|
||
const getSystemInfo = () => {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
statusBarHeight.value = systemInfo.statusBarHeight
|
||
// #ifdef MP-WEIXIN
|
||
navBarHeight.value = systemInfo.statusBarHeight + 44
|
||
// #endif
|
||
// #ifdef APP-PLUS
|
||
navBarHeight.value = systemInfo.statusBarHeight + 44
|
||
// #endif
|
||
}
|
||
|
||
const goBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
const togglePaymentMethod = (method) => {
|
||
if (method === 'points') {
|
||
usePoints.value = !usePoints.value
|
||
} else if (method === 'balance') {
|
||
useBalance.value = !useBalance.value
|
||
}
|
||
}
|
||
|
||
const order_pay_type = ref(0)
|
||
|
||
// 组合支付类型编码:
|
||
// 0免费;1余额;2积分;3余额+积分;4第三方;5余额+第三方;6积分+第三方;7余额+积分+第三方
|
||
const computeOrderPayType = () => {
|
||
const price = courseInfo.value.price || 0
|
||
// 免费
|
||
if (price === 0) return 0
|
||
|
||
const useP = usePoints.value && pointsUsedYuan.value > 0
|
||
const useB = useBalance.value && balanceUsedYuan.value > 0
|
||
const useThirdParty = finalPrice.value > 0
|
||
|
||
if (useThirdParty && useB && useP) return 7
|
||
if (useThirdParty && useB) return 5
|
||
if (useThirdParty && useP) return 6
|
||
if (useThirdParty) return 4
|
||
if (useB && useP) return 3
|
||
if (useB) return 1
|
||
if (useP) return 2
|
||
// 回退:如果都没选但也不免费,则第三方
|
||
return 4
|
||
}
|
||
|
||
const confirmPayment = () => {
|
||
if (!canPay.value) {
|
||
uni.showToast({
|
||
title: '积分/余额不足以完成支付',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
|
||
order_pay_type.value = computeOrderPayType()
|
||
console.log('确认支付,支付方式:', { usePoints: usePoints.value, useBalance: useBalance.value, order_pay_type: order_pay_type.value })
|
||
let openid = ""
|
||
if (process.env.NODE_ENV === 'development') {
|
||
openid = uni.getStorageSync('DEV_APPID');
|
||
} else {
|
||
openid = uni.getStorageSync('AUTH_APPID');
|
||
}
|
||
course_api.createExcellencourseMixedOrder(courseId.value, order_pay_type.value, openid).then(res => {
|
||
console.log('创建订单:', res)
|
||
if (res.code == 200) {
|
||
|
||
const payParams = res.data.order
|
||
|
||
console.log(payParams)
|
||
requestPayment(
|
||
payParams,
|
||
// 支付成功
|
||
(res) => {
|
||
uni.showToast({ title: '支付成功', icon: 'success' })
|
||
// 可以跳转到支付成功页,或者查询订单状态
|
||
},
|
||
// 支付失败
|
||
(err) => {
|
||
console.error('支付失败', err)
|
||
uni.showToast({ title: '支付失败或取消', icon: 'none' })
|
||
}
|
||
)
|
||
// uni.redirectTo({
|
||
// url: '/pages_course/course_detail/course_detail?id='+courseId.value
|
||
// })
|
||
}else{
|
||
uni.showToast({
|
||
title: res.msg,
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
|
||
}
|
||
|
||
const courseId = ref(0)
|
||
const getCourseInfo = () => {
|
||
// 获取课程详情
|
||
course_api.excellencourseDetail(courseId.value).then(res => {
|
||
console.log('课程详情:', res)
|
||
if (res.code == 200) {
|
||
const data = res.data
|
||
// 根据优惠类型计算价格(单位:分)
|
||
const now = Date.now()
|
||
const beginTs = data.discount_begin_date ? new Date(data.discount_begin_date).getTime() : null
|
||
const endTs = data.discount_end_date ? new Date(data.discount_end_date).getTime() : null
|
||
let finalPriceCents = data.account || 0
|
||
if (data.discount_type === 1 && data.discount_price > 0) {
|
||
// 永久优惠
|
||
finalPriceCents = data.discount_price
|
||
} else if (data.discount_type === 2 && data.discount_price > 0) {
|
||
// 限时优惠:仅在有效期内生效
|
||
const inWindow = (!beginTs || beginTs <= now) && (!!endTs && endTs > now)
|
||
if (inWindow) finalPriceCents = data.discount_price
|
||
}
|
||
courseInfo.value = {
|
||
title: data.title || '课程标题',
|
||
price: (finalPriceCents || 0) / 100,
|
||
tags: [data.special_type_name || '精品课程'],
|
||
image: data.index_img || '/static/icon_home_my_patient.png'
|
||
}
|
||
}
|
||
}).catch(err => {
|
||
console.error('获取课程详情失败:', err)
|
||
})
|
||
|
||
// 获取用户账户信息
|
||
course_api.excellencoursePayPage(courseId.value).then(res => {
|
||
console.log('用户账户信息:', res)
|
||
if (res.code == 200) {
|
||
const data = res.data
|
||
userInfo.value = {
|
||
points: data.totalPoints || 0,
|
||
balance: data.availableBalance || 0,
|
||
pointsExchangeRate: parseInt(data.points_price_exchange) || 50
|
||
}
|
||
|
||
// 根据可用余额和积分自动选择支付方式
|
||
updatePaymentMethod()
|
||
}
|
||
}).catch(err => {
|
||
console.error('获取用户账户信息失败:', err)
|
||
})
|
||
}
|
||
|
||
// 更新支付方式选择
|
||
const updatePaymentMethod = () => {
|
||
const coursePrice = courseInfo.value.price
|
||
const pointsValue = userInfo.value.points / userInfo.value.pointsExchangeRate
|
||
const balance = userInfo.value.balance
|
||
|
||
// 如果积分足够,优先选择积分支付
|
||
if (pointsValue >= coursePrice) {
|
||
selectedMethod.value = 'points'
|
||
}
|
||
// 如果余额足够,选择余额支付
|
||
else if (balance >= coursePrice) {
|
||
selectedMethod.value = 'balance'
|
||
}
|
||
// 如果都不够,默认选择积分支付
|
||
else {
|
||
selectedMethod.value = 'points'
|
||
}
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
// 获取页面参数
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
const options = currentPage.options || {}
|
||
|
||
courseId.value = options.id
|
||
console.log('课程ID:', options.id)
|
||
getSystemInfo()
|
||
getCourseInfo()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.payment-page {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
padding-bottom: 120rpx;
|
||
}
|
||
|
||
// 导航栏
|
||
.navbar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 999;
|
||
background-color: #fff;
|
||
border-bottom: 1rpx solid #e5e5e5;
|
||
|
||
.nav-content {
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 32rpx;
|
||
|
||
.nav-left, .nav-right {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.nav-title {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #FF4757; // 红色标题
|
||
}
|
||
}
|
||
}
|
||
|
||
.page-content {
|
||
padding: 24rpx;
|
||
}
|
||
|
||
// 课程信息卡片
|
||
.course-card {
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 24rpx;
|
||
display: flex;
|
||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||
|
||
.course-left {
|
||
margin-right: 24rpx;
|
||
|
||
.course-image {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 12rpx;
|
||
}
|
||
}
|
||
|
||
.course-right {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
|
||
.course-title {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
margin-bottom: 16rpx;
|
||
// 文字溢出处理
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
|
||
.course-tags {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
margin-bottom: 16rpx;
|
||
|
||
.tag-item {
|
||
.tag-text {
|
||
border: 1rpx solid #e0e0e0;
|
||
color: #333;
|
||
font-size: 20rpx;
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 8rpx;
|
||
background-color: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.course-price {
|
||
font-size: 36rpx;
|
||
color: #FF4757;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 支付方式选择
|
||
.payment-methods {
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||
|
||
.method-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
transition: all 0.3s;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&.disabled {
|
||
opacity: 0.5;
|
||
background-color: #f8f9fa;
|
||
|
||
.method-label,
|
||
.method-value,
|
||
.method-desc {
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.method-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4rpx;
|
||
|
||
.method-label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.method-value {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.method-desc {
|
||
font-size: 20rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.radio-button {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s;
|
||
|
||
&.active {
|
||
border-color: #FF4757;
|
||
background-color: #FF4757;
|
||
}
|
||
|
||
.radio-inner {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
background-color: #fff;
|
||
border-radius: 50%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 支付明细
|
||
.payment-summary {
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||
|
||
.summary-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
&.total {
|
||
border-top: 1rpx solid #f0f0f0;
|
||
padding-top: 20rpx;
|
||
margin-top: 20rpx;
|
||
|
||
.summary-label {
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.total-price {
|
||
color: #FF4757;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.summary-label {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 底部支付栏
|
||
.bottom-payment {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 120rpx;
|
||
background-color: #fff;
|
||
border-top: 1rpx solid #e5e5e5;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 24rpx;
|
||
z-index: 999;
|
||
|
||
.payment-left {
|
||
.final-price {
|
||
font-size: 40rpx;
|
||
color: #FF4757;
|
||
font-weight: 600;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.payment-method {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.payment-right {
|
||
.pay-button {
|
||
background-color: #FF4757;
|
||
color: #fff;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
padding: 20rpx 60rpx;
|
||
border-radius: 44rpx;
|
||
border: none;
|
||
box-shadow: 0 4rpx 16rpx rgba(255, 71, 87, 0.3);
|
||
transition: all 0.3s;
|
||
|
||
&:disabled {
|
||
background-color: #ccc;
|
||
box-shadow: none;
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|