修复:修复下单可多次点击支付按钮问题;允许积分支付小数;

This commit is contained in:
2025-12-22 17:52:02 +08:00
parent 55455fa4f2
commit cafb86cc9d
9 changed files with 283 additions and 132 deletions

View File

@@ -7,8 +7,8 @@ export const ENV = process.env.NODE_ENV || 'development';
*/
const BASE_URL_MAP = {
development: {
MAIN: 'http://192.168.110.100:9300/pb/', // 张川川
// MAIN: 'https://global.nuttyreading.com/', // 线上
// MAIN: 'http://192.168.110.100:9300/pb/', // 张川川
MAIN: 'https://global.nuttyreading.com/', // 线上
// PAYMENT: 'https://dev-pay.example.com', // 暂时用不到
// CDN: 'https://cdn-dev.example.com', // 暂时用不到
},

View File

@@ -2,8 +2,7 @@
import { mainClient } from '@/api/clients/main'
import type { IApiResponse } from '@/api/types'
import type {
ICreateOrderParams,
IGooglePayVerifyParams,
ICreateOrderParams,
ICreateOrderResponse,
IOrderGoods,
ICoupon,

View File

@@ -67,7 +67,7 @@
<view class="points-input-box">
<input
v-model="pointsDiscounted"
type="number"
type="digit"
clearable
:placeholder="$t('order.pointsPlaceholder')"
class="text-right"
@@ -90,7 +90,7 @@
<text class="label">{{ $t('order.total') }}</text>
<text class="amount">{{ finalAmount }} {{ t('global.coin') }}</text>
</view>
<wd-button type="primary" @click="handleSubmit">
<wd-button type="primary" :loading="submitLoading" @click="handleSubmit">
{{ $t('order.submit') }}
</wd-button>
</view>
@@ -128,6 +128,7 @@ import { getUserInfo } from '@/api/modules/user'
import { useUserStore } from '@/stores/user'
import { t } from '@/utils/i18n'
import type { IGoods, IGoodsDiscountParams } from '@/types/order'
import type { IUserInfo } from '@/types/user'
import PayWay from '@/components/order/PayWay.vue'
const userStore = useUserStore()
@@ -135,14 +136,13 @@ const userStore = useUserStore()
// 使用页面传参
interface Props {
goodsList: IGoods[],
userInfo: object,
userInfo: IUserInfo,
allowPointPay?: boolean,
orderType?: string,
backStep?: number // 购买完成后返回几层页面
}
const props = withDefaults(defineProps<Props>(), {
goodsList: () => [],
userInfo: () => ({}),
allowPointPay: true,
orderType: 'order',
backStep: 1
@@ -270,13 +270,24 @@ const calculatePromotionDiscounted = async () => {
const handlePointsInput = (value: any) => {
let val = String(value.detail.value)
// 允许数字字符,去掉小数点
val = val.replace(/[^0-9]/g, '')
// 允许数字字符小数点
val = val.replace(/[^0-9.]/g, '')
if (val === '0' || val === '') {
// 确保只有一个小数点
const dotIndex = val.indexOf('.')
if (dotIndex !== -1) {
// 限制小数点后最多两位
val = val.substring(0, dotIndex + 1) + val.substring(dotIndex + 1).replace(/\./g, '')
const decimalPart = val.substring(dotIndex + 1)
if (decimalPart.length > 2) {
val = val.substring(0, dotIndex + 1) + decimalPart.substring(0, 2)
}
}
if (val === '' || val === '.') {
pointsDiscounted.value = 0
} else {
let numericValue = parseInt(val, 10)
let numericValue = parseFloat(val)
if (numericValue < 0 || isNaN(numericValue)) {
numericValue = 0
}
@@ -316,7 +327,7 @@ const calculateFinalPrice = () => {
const orderAmountAfterDiscount = totalAmount.value - promotionDiscounted.value - vipDiscounted.value - couponAmount
pointsUsableMax.value = Math.min(
props?.userInfo?.jf || 0,
Math.floor(props.allowPointPay ? orderAmountAfterDiscount : 0)
props.allowPointPay ? orderAmountAfterDiscount : 0
)
pointsDiscounted.value = pointsUsableMax.value
@@ -362,28 +373,37 @@ const validateOrder = (): boolean => {
/**
* 提交订单
*/
const handleSubmit = async () => {
const submitLoading = ref<boolean>(false)
const handleSubmit = async () => {
// 验证订单
if (!validateOrder()) return
submitLoading.value = true
// 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
try {
// 创建订单 此app用天医币支付创建订单成功即支付成功
await createOrder()
// 重新获取用户信息更新store和本地缓存
const res = await getUserInfo()
userStore.setUserInfo(res.result)
// 重新获取用户信息更新store和本地缓存
const res = await getUserInfo()
userStore.setUserInfo(res.result)
uni.showToast({
title: t('order.orderSuccess'),
icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack({
delta: props.backStep
uni.showToast({
title: t('order.orderSuccess'),
icon: 'success'
})
}, 500)
// 返回上一页
setTimeout(() => {
uni.navigateBack({
delta: props.backStep
})
}, 500)
} catch (error) {
console.error('提交订单失败:', error)
} finally {
// 无论成功还是失败都要重置loading状态
submitLoading.value = false
}
}
/**

View File

@@ -1,5 +1,11 @@
<template>
<view class="home-page">
<scroll-view
class="home-page"
scroll-y
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="handleRefresh"
>
<!-- 顶部背景区域 -->
<view class="home-bg" :style="{ paddingTop: getNotchHeight() + 'px' }">
<wd-search
@@ -244,7 +250,7 @@
</Skeleton>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="ts">
@@ -427,12 +433,38 @@ const getPrompt = () => {
}
}
// 下拉刷新状态
const isRefreshing = ref(false)
/**
* 处理下拉刷新
*/
const handleRefresh = async () => {
isRefreshing.value = true
try {
// 刷新所有数据
await Promise.all([
myBookSkeleton.value?.reload(),
recommendBooksSkeleton.value?.reload(),
categoryLevel1LabelSkeleton.value?.reload()
])
} catch (error) {
console.error('刷新数据失败:', error)
} finally {
// 延迟关闭刷新状态,避免闪烁
setTimeout(() => {
isRefreshing.value = false
}, 500)
}
}
/**
* 页面显示
*/
onShow(() => {
// 刷新数据
myBookSkeleton.value?.reload()
categoryLevel1LabelSkeleton.value.reload()
})
</script>
@@ -469,6 +501,7 @@ onShow(() => {
.content-wrapper {
padding-bottom: 40rpx;
height: calc(100vh - 240px);
}
.mine-block {

View File

@@ -1,5 +1,11 @@
<template>
<view class="course-home-page">
<scroll-view
class="course-home-page"
scroll-y
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="handleRefresh"
>
<!-- 头部区域 -->
<view class="home-bg" :style="{ paddingTop: getNotchHeight() + 'px' }">
<wd-search
@@ -215,7 +221,7 @@
</template>
</Skeleton>
</view>
</view>
</scroll-view>
</template>
<script setup lang="ts">
@@ -234,6 +240,9 @@ const userStore = useUserStore()
// 系统信息
const scrollTop = ref<number>(0) // 滚动位置
// 下拉刷新状态
const isRefreshing = ref(false)
/**
* 处理搜索点击
*/
@@ -451,6 +460,31 @@ const requestAll = async () => {
}
getTryListenList()
getNewsList()
// 刷新分类数据
if (selectedFirstLevel.value === '医学') {
medicineMenuSkeletonRef.value?.reload()
} else {
menuSkeletonRef.value?.reload()
}
}
/**
* 处理下拉刷新
*/
const handleRefresh = async () => {
isRefreshing.value = true
try {
// 刷新所有数据
await requestAll()
} catch (error) {
console.error('刷新数据失败:', error)
} finally {
// 延迟关闭刷新状态,避免闪烁
setTimeout(() => {
isRefreshing.value = false
}, 500)
}
}
/**
@@ -526,7 +560,7 @@ $text-placeholder: #999999;
$border-color: #eeeeee;
.course-home-page {
min-height: 100vh;
height: 100vh;
background-color: $bg-color;
font-size: 28upx;
}

View File

@@ -4,7 +4,7 @@
<nav-bar :title="$t('order.confirmTitle')" />
<!-- 确认订单组件 -->
<Confirm :goodsList="goodsList" :userInfo="userInfo" :orderType="orderType">
<Confirm :goodsList="goodsList" :userInfo="userInfo" :orderType="orderType" :allow-point-pay="allowPointPay">
<template #goodsList>
<!-- 商品列表内容 -->
<view
@@ -86,12 +86,15 @@ const isRelearn = ref<boolean>(false)
const orderType = computed(() => {
return isRelearn.value ? 'relearn' : 'order'
})
// 是否允许积分支付
const allowPointPay = ref<boolean>(false)
/**
* 页面加载
*/
onLoad(async (options: any) => {
try {
allowPointPay.value = options.allowPointPay !== '0'
if (options.isRelearn == 1) {
uni.$on('selectedGoods', async (data: IOrderGoods) => {
// 获取用户信息

View File

@@ -1,83 +1,91 @@
<template>
<view class="user-page" :style="{ paddingTop: getNotchHeight() + 30 + 'px' }" v-if="tokenState">
<!-- 设置图标 -->
<view class="settings-icon" :style="{ top: getNotchHeight() + 30 + 'px' }" @click="goSettings">
<wd-icon name="setting1" size="24px" color="#666" />
<text>{{ $t('user.settings') }}</text>
</view>
<template>
<scroll-view
scroll-y
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="handleRefresh"
v-if="tokenState"
>
<view class="user-page" :style="{ paddingTop: getNotchHeight() + 30 + 'px' }" v-if="tokenState">
<!-- 设置图标 -->
<view class="settings-icon" :style="{ top: getNotchHeight() + 30 + 'px' }" @click="goSettings">
<wd-icon name="setting1" size="24px" color="#666" />
<text>{{ $t('user.settings') }}</text>
</view>
<!-- 用户信息区域 -->
<view class="user-info-section">
<view class="user-info">
<image :src="userInfo.avatar || defaultAvatar" class="avatar" @click="goProfile" />
<view class="user-details">
<text class="nickname">{{ userInfo.nickname || $t('user.notSet') }}</text>
<text v-if="userInfo.email" class="email">{{ userInfo.email }}</text>
<!-- 用户信息区域 -->
<view class="user-info-section">
<view class="user-info">
<image :src="userInfo.avatar || defaultAvatar" class="avatar" @click="goProfile" />
<view class="user-details">
<text class="nickname">{{ userInfo.nickname || $t('user.notSet') }}</text>
<text v-if="userInfo.email" class="email">{{ userInfo.email }}</text>
</view>
</view>
</view>
</view>
<!-- VIP订阅卡片 -->
<view class="vip-card-section">
<view class="vip-card">
<view class="vip-card-title">{{ $t('user.vip') }}</view>
<view class="vip-card-content">
<view class="vip-item-list">
<view v-if="vipInfo?.length > 0" v-for="vip in vipInfo">
{{ vipTypeDict[vip.type] }}{{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止</view>
<view v-else>办理课程VIP畅享更多权益</view>
<!-- VIP订阅卡片 -->
<view class="vip-card-section">
<view class="vip-card">
<view class="vip-card-title">{{ $t('user.vip') }}</view>
<view class="vip-card-content">
<view class="vip-item-list">
<view v-if="vipInfo?.length > 0" v-for="vip in vipInfo">
{{ vipTypeDict[vip.type] }}{{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止</view>
<view v-else>办理课程VIP畅享更多权益</view>
</view>
<wd-button v-if="vipInfo?.length > 0" plain type="primary" size="small"
@click="goCourseVipSub">{{ $t('vip.renewal') }}</wd-button>
<wd-button v-else plain type="primary" size="small"
@click="goCourseVipSub">{{ $t('vip.openVip') }}</wd-button>
</view>
<wd-button v-if="vipInfo?.length > 0" plain type="primary" size="small"
@click="goCourseVipSub">{{ $t('vip.renewal') }}</wd-button>
<wd-button v-else plain type="primary" size="small"
@click="goCourseVipSub">{{ $t('vip.openVip') }}</wd-button>
</view>
<view class="vip-card-content">
<view class="vip-item-list">
<view v-if="vipInfoEbook?.length > 0" v-for="vip in vipInfoEbook">
电子书VIP{{ vipTypeDict[vip.type] }}{{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止</view>
<view v-else>办理电子书VIP畅享更多权益</view>
<view class="vip-card-content">
<view class="vip-item-list">
<view v-if="vipInfoEbook?.length > 0" v-for="vip in vipInfoEbook">
电子书VIP{{ vipTypeDict[vip.type] }}{{ parseTime(vip.endTime, '{y}-{m}-{d}') }} 截止</view>
<view v-else>办理电子书VIP畅享更多权益</view>
</view>
<wd-button v-if="!vipInfoEbook?.length" plain type="primary" size="small"
@click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
</view>
<wd-button v-if="!vipInfoEbook?.length" plain type="primary" size="small"
@click="goSubscribe">{{ $t('vip.openVip') }}</wd-button>
</view>
</view>
</view>
<!-- 我的资产 -->
<view class="assets-card-section wallet-section">
<view class="assets-card wallet_l">
<view class="assets">
<view @click="goVirtualList">
<view class="assets_row">{{ t('global.coin') }}</view>
<view>{{userInfo.peanutCoin ?? 1}}</view>
<!-- 我的资产 -->
<view class="assets-card-section wallet-section">
<view class="assets-card wallet_l">
<view class="assets">
<view @click="goVirtualList">
<view class="assets_row">{{ t('global.coin') }}</view>
<view>{{userInfo.peanutCoin ?? 1}}</view>
</view>
<view @click="goPointsList">
<view class="assets_row">积分</view>
<view>{{userInfo.jf ?? 1}}</view>
</view>
<!-- <view>
<view class="assets_row">优惠卷</view>
<view>0</view>
</view> -->
</view>
<view @click="goPointsList">
<view class="assets_row">积分</view>
<view>{{userInfo.jf ?? 1}}</view>
</view>
<!-- <view>
<view class="assets_row">优惠卷</view>
<view>0</view>
</view> -->
<view class="chong_btn" @click="goRecharge"> </view>
<!-- <text class="wallet_title">{{$t('my.coin')}}<uni-icons type="help" size="19" color="#666"></uni-icons></text>
<text class="wallet_count">{{userMes.peanutCoin}}</text> -->
</view>
<view class="chong_btn" @click="goRecharge"> </view>
<!-- <text class="wallet_title">{{$t('my.coin')}}<uni-icons type="help" size="19" color="#666"></uni-icons></text>
<text class="wallet_count">{{userMes.peanutCoin}}</text> -->
</view>
<!-- 功能菜单列表 -->
<view class="menu-section">
<wd-cell-group border class="menu-list">
<wd-cell v-for="item in menuItems" :key="item.id" :title="item.name" :label="item.desc" is-link
@click="handleMenuClick(item)">
<text v-if="item.hufenState" class="menu-list-hufen">{{hufenData?.total ?? 0}}<text
style="margin-left: 6rpx;">湖分</text></text>
</wd-cell>
</wd-cell-group>
</view>
</view>
<!-- 功能菜单列表 -->
<view class="menu-section">
<wd-cell-group border class="menu-list">
<wd-cell v-for="item in menuItems" :key="item.id" :title="item.name" :label="item.desc" is-link
@click="handleMenuClick(item)">
<text v-if="item.hufenState" class="menu-list-hufen">{{hufenData?.total ?? 0}}<text
style="margin-left: 6rpx;">湖分</text></text>
</wd-cell>
</wd-cell-group>
</view>
</view>
</scroll-view>
<visitor v-else></visitor>
</template>
@@ -166,6 +174,9 @@
// 湖分
const hufenData = ref()
const tokenState = ref(false)
// 下拉刷新状态
const isRefreshing = ref(false)
/**
* 获取平台信息
@@ -186,6 +197,28 @@
}
}
/**
* 处理下拉刷新
*/
const handleRefresh = async () => {
isRefreshing.value = true
try {
// 刷新所有数据
await Promise.all([
getData(),
getHufen()
])
} catch (error) {
console.error('刷新数据失败:', error)
} finally {
// 延迟关闭刷新状态,避免闪烁
setTimeout(() => {
isRefreshing.value = false
}, 500)
}
}
/**
* 获取用户湖分
*/
@@ -297,6 +330,7 @@
.user-page {
min-height: calc(100vh - 50px);
background-color: #f7faf9;
position: relative;
}
.settings-icon {

View File

@@ -212,27 +212,15 @@
max-width: 96rem;
}
}
.mt-2 {
margin-top: calc(var(--spacing) * 2);
}
.mt-2\! {
margin-top: calc(var(--spacing) * 2) !important;
}
.mt-5 {
margin-top: calc(var(--spacing) * 5);
}
.mt-5\! {
margin-top: calc(var(--spacing) * 5) !important;
}
.mt-20 {
margin-top: calc(var(--spacing) * 20);
}
.mt-\[20rpx\]\! {
margin-top: 20rpx !important;
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.mb-2\! {
margin-bottom: calc(var(--spacing) * 2) !important;
}
@@ -242,15 +230,9 @@
.mb-\[20rpx\]\! {
margin-bottom: 20rpx !important;
}
.ml-1 {
margin-left: calc(var(--spacing) * 1);
}
.ml-1\! {
margin-left: calc(var(--spacing) * 1) !important;
}
.ml-2 {
margin-left: calc(var(--spacing) * 2);
}
.ml-2\.5\! {
margin-left: calc(var(--spacing) * 2.5) !important;
}
@@ -290,9 +272,6 @@
.flex-shrink {
flex-shrink: 1;
}
.border-collapse {
border-collapse: collapse;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
@@ -312,9 +291,6 @@
.bg-white {
background-color: var(--color-white);
}
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-0\! {
padding: calc(var(--spacing) * 0) !important;
}
@@ -339,9 +315,6 @@
.pt-\[10rpx\] {
padding-top: 10rpx;
}
.pb-0 {
padding-bottom: calc(var(--spacing) * 0);
}
.pb-0\! {
padding-bottom: calc(var(--spacing) * 0) !important;
}
@@ -392,9 +365,6 @@
--tw-ordinal: ordinal;
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
}
.underline {
text-decoration-line: underline;
}
.shadow-\[0_4rpx_12rpx_rgba\(0\,0\,0\,0\.05\)\] {
--tw-shadow: 0 4rpx 12rpx var(--tw-shadow-color, rgba(0,0,0,0.05));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);

58
types/order.d.ts vendored
View File

@@ -160,3 +160,61 @@ export interface IOrderDetail {
remark?: string
[key: string]: any
}
/**
* 创建订单参数
*/
export interface ICreateOrderParams {
buyType: number // 0-商品页直接下单 1-购物车结算
userId: number
paymentMethod: number // 4-天医币
orderMoney: number // 订单金额
realMoney: number // 实收金额
jfDeduction: number // 积分抵扣
couponId?: number // 优惠券ID
couponName?: string // 优惠券名称
vipDiscountAmount: number // VIP折扣金额
districtMoney: number // 地区优惠金额
remark?: string // 备注
productList: Array<{
productId: number
quantity: number
}>
orderType: string // "order"
addressId: number // 0 for course products
appName: string // "wumen"
come: number // 2
}
/**
* 创建订单响应
*/
export interface ICreateOrderResponse {
orderSn: string
money: number
[key: string]: any
}
/**
* 订单初始化数据
*/
export interface IOrderInitData {
goodsList: IOrderGoods[]
totalPrice: number
vipPrice: number
districtAmount: number
actualPayment: number
jfNumber: number
jfNumberMax: number
jfNumberShow: string
couponList: ICoupon[]
selectedCoupon: ICoupon | null
showCouponPopup: boolean
remark: string
showRemarkPopup: boolean
payType: number
loading: boolean
submitting: boolean
buyingFlag: boolean
[key: string]: any
}