2025-09-23 19:00:32 +08:00

625 lines
15 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="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'
import { onLoad } from '@dcloudio/uni-app'
// 响应式数据
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.UNI_PLATFORM == "h5") {
if (process.env.NODE_ENV === 'development') {
openid = uni.getStorageSync('DEV_APPID');
} else {
openid = uni.getStorageSync('AUTH_APPID');
}
} else if(process.env.UNI_PLATFORM == "mp-weixin") {
const {
envVersion
} = uni.getAccountInfoSync().miniProgram;
if (envVersion == "release") {
openid = uni.getStorageSync('DEV_APPID');
} else {
openid = uni.getStorageSync('AUTH_APPID');
}
}else{
//app
}
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: 'none' })
// 可以跳转到支付成功页,或者查询订单状态
},
// 支付失败
(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'
}
}
onLoad((options) => {
courseId.value = options.id // 已支持 App-Plus
console.log('课程ID:', options.id)
})
// 生命周期
onMounted(() => {
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>