修正布局,删改无用页面
This commit is contained in:
@@ -125,18 +125,9 @@
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { setToken } from '@/utils/request.js'
|
||||
import { loginWithPhone, oneClickLogin, sendCode } from '@/api/main.js'
|
||||
|
||||
// 测试数据
|
||||
const TEST_TOKEN = 'test_token_123456'
|
||||
const TEST_MEMBER_INFO = {
|
||||
memberId: 10001,
|
||||
memberNo: 'M001',
|
||||
phone: '13800138000',
|
||||
nickname: '测试用户',
|
||||
avatar: '/static/logo.png',
|
||||
accessToken: TEST_TOKEN,
|
||||
isNewUser: false
|
||||
}
|
||||
// 调用真实后端API,不再使用测试数据
|
||||
|
||||
const activeCard = ref('phone')
|
||||
const phone = ref('')
|
||||
@@ -230,16 +221,31 @@ async function handlePhoneLogin() {
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
setToken(TEST_TOKEN)
|
||||
uni.setStorageSync('memberInfo', TEST_MEMBER_INFO)
|
||||
uni.setStorageSync('isLogin', true)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
try {
|
||||
const res = await oneClickLogin({
|
||||
accessToken: phone.value,
|
||||
openid: 'uniapp_phone_login',
|
||||
nickname: '',
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
if (res && res.accessToken) {
|
||||
setToken(res.accessToken)
|
||||
uni.setStorageSync('memberInfo', res)
|
||||
uni.setStorageSync('isLogin', true)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
} else {
|
||||
showToast('登录失败,请重试')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('一键登录失败:', err)
|
||||
showToast(err?.message || '登录失败,请重试')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function triggerAgreementShake() {
|
||||
@@ -260,12 +266,22 @@ async function handleSendCode() {
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
countdown.value = 300
|
||||
startCountdown()
|
||||
showToast('验证码已发送')
|
||||
try {
|
||||
const res = await sendCode({ phone: smsPhone.value })
|
||||
|
||||
if (res && res.success) {
|
||||
countdown.value = 60
|
||||
startCountdown()
|
||||
showToast('验证码已发送')
|
||||
} else {
|
||||
showToast(res?.message || '验证码发送失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('发送验证码失败:', err)
|
||||
showToast(err?.message || '验证码发送失败')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
@@ -297,15 +313,29 @@ async function handleSmsLogin() {
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
setToken(TEST_TOKEN)
|
||||
uni.setStorageSync('memberInfo', TEST_MEMBER_INFO)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
try {
|
||||
const res = await loginWithPhone({
|
||||
phone: smsPhone.value,
|
||||
code: code.value
|
||||
})
|
||||
|
||||
if (res && res.accessToken) {
|
||||
setToken(res.accessToken)
|
||||
uni.setStorageSync('memberInfo', res)
|
||||
uni.setStorageSync('isLogin', true)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
} else {
|
||||
showToast('登录失败,请重试')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('验证码登录失败:', err)
|
||||
showToast(err?.message || '登录失败,请重试')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -1,64 +1,57 @@
|
||||
<template>
|
||||
<view class="member-info-page">
|
||||
<!-- 滚动内容区域 -->
|
||||
<scroll-view scroll-y class="scroll-container theme-light">
|
||||
<view class="member-scroll-wrap">
|
||||
<scroll-view scroll-y class="member-scroll">
|
||||
<view class="member-page">
|
||||
<MemberInfoHeader
|
||||
:user-info="userInfo"
|
||||
:stats="stats"
|
||||
:isLogin="isLogin"
|
||||
@user-info="goUserInfo"
|
||||
@guest-login="handleGuestLogin"
|
||||
/>
|
||||
<view class="member-page__body">
|
||||
<view class="member-page__sections">
|
||||
<MemberInfoMemberCard
|
||||
:card-info="cardInfo"
|
||||
@view-all="goMemberCard"
|
||||
@renew="onRenewCard"
|
||||
@purchase="goPurchaseCard"
|
||||
/>
|
||||
<MemberInfoQuickActions @action="onQuickAction" />
|
||||
<MemberInfoBookingList
|
||||
v-if="isLogin"
|
||||
:items="bookingPreview"
|
||||
@view-all="goBooking"
|
||||
@item-tap="goBooking"
|
||||
/>
|
||||
<MemberInfoCheckInList
|
||||
v-if="isLogin"
|
||||
:items="checkIns"
|
||||
@view-all="onCheckInViewAll"
|
||||
@item-tap="onCheckInTap"
|
||||
/>
|
||||
<MemberInfoBodyReport
|
||||
v-if="isLogin"
|
||||
:report="bodyReport"
|
||||
@view-history="onBodyReportHistory"
|
||||
@view-report="onBodyReportView"
|
||||
/>
|
||||
<MemberInfoCouponPoints
|
||||
v-if="isLogin"
|
||||
:data="couponPoints"
|
||||
@view-all="onCouponViewAll"
|
||||
@use-coupon="onUseCoupon"
|
||||
@redeem-points="onRedeemPoints"
|
||||
/>
|
||||
<MemberInfoReferral
|
||||
v-if="isLogin"
|
||||
:data="referral"
|
||||
@view-rules="onReferralRules"
|
||||
/>
|
||||
<MemberInfoSettings v-if="isLogin" @setting="onSetting" />
|
||||
<MemberInfoLogout v-if="isLogin" @logout="handleLogout" />
|
||||
</view>
|
||||
<MemberInfoHeader
|
||||
:user-info="userInfo"
|
||||
:stats="stats"
|
||||
:is-login="isLogin"
|
||||
@user-info="goUserInfo"
|
||||
@guest-login="handleGuestLogin"
|
||||
/>
|
||||
<view class="member-page__body">
|
||||
<view class="member-page__sections">
|
||||
<MemberInfoMemberCard
|
||||
:card-info="cardInfo"
|
||||
@view-all="goMemberCard"
|
||||
@renew="onRenewCard"
|
||||
@purchase="goPurchaseCard"
|
||||
/>
|
||||
<MemberInfoQuickActions @action="onQuickAction" />
|
||||
<MemberInfoBookingList
|
||||
v-if="isLogin"
|
||||
:items="bookingPreview"
|
||||
@view-all="goBooking"
|
||||
@item-tap="goBooking"
|
||||
/>
|
||||
<MemberInfoCheckInList
|
||||
v-if="isLogin"
|
||||
:items="checkIns"
|
||||
@view-all="onCheckInViewAll"
|
||||
@item-tap="onCheckInTap"
|
||||
/>
|
||||
<MemberInfoCouponPoints
|
||||
v-if="isLogin"
|
||||
:data="couponPoints"
|
||||
@view-all="onCouponViewAll"
|
||||
@use-coupon="onUseCoupon"
|
||||
@redeem-points="onRedeemPoints"
|
||||
/>
|
||||
<MemberInfoSettings v-if="isLogin" @setting="onSetting" />
|
||||
<MemberInfoLogout v-if="isLogin" @logout="handleLogout" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 固定 TabBar -->
|
||||
<view class="tabbar-fixed">
|
||||
<TabBar />
|
||||
<TabBar />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -79,9 +72,7 @@ import MemberInfoMemberCard from '@/components/memberInfo/MemberInfoMemberCard.v
|
||||
import MemberInfoQuickActions from '@/components/memberInfo/MemberInfoQuickActions.vue'
|
||||
import MemberInfoBookingList from '@/components/memberInfo/MemberInfoBookingList.vue'
|
||||
import MemberInfoCheckInList from '@/components/memberInfo/MemberInfoCheckInList.vue'
|
||||
import MemberInfoBodyReport from '@/components/memberInfo/MemberInfoBodyReport.vue'
|
||||
import MemberInfoCouponPoints from '@/components/memberInfo/MemberInfoCouponPoints.vue'
|
||||
import MemberInfoReferral from '@/components/memberInfo/MemberInfoReferral.vue'
|
||||
import MemberInfoSettings from '@/components/memberInfo/MemberInfoSettings.vue'
|
||||
import MemberInfoLogout from '@/components/memberInfo/MemberInfoLogout.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
@@ -91,9 +82,7 @@ const stats = ref({})
|
||||
const cardInfo = ref({})
|
||||
const bookingPreview = ref([])
|
||||
const checkIns = ref([])
|
||||
const bodyReport = ref({})
|
||||
const couponPoints = ref({})
|
||||
const referral = ref({})
|
||||
const loading = ref(false)
|
||||
const hasActiveCard = ref(false)
|
||||
const isLogin = ref(false)
|
||||
@@ -213,12 +202,21 @@ function refreshFromStore() {
|
||||
cardInfo.value = pageData.cardInfo
|
||||
bookingPreview.value = pageData.bookingPreview
|
||||
checkIns.value = pageData.checkIns
|
||||
bodyReport.value = pageData.bodyReport
|
||||
couponPoints.value = pageData.couponPoints
|
||||
referral.value = pageData.referral
|
||||
console.log('[memberInfo] refreshFromStore - userInfo.value:', JSON.stringify(userInfo.value))
|
||||
}
|
||||
|
||||
function goUserInfo() {
|
||||
navigateToPage(PAGE.USER_INFO)
|
||||
}
|
||||
|
||||
function handleGuestLogin() {
|
||||
console.log('[memberInfo] 用户点击登录区域')
|
||||
uni.navigateTo({
|
||||
url: '/pages/memberInfo/login'
|
||||
})
|
||||
}
|
||||
|
||||
function goMemberCard() {
|
||||
navigateToPage(PAGE.MEMBER_CARD)
|
||||
}
|
||||
@@ -231,17 +229,6 @@ function goBooking() {
|
||||
navigateToPage(PAGE.BOOKING)
|
||||
}
|
||||
|
||||
function goUserInfo() {
|
||||
navigateToPage(PAGE.USER_INFO)
|
||||
}
|
||||
|
||||
function handleGuestLogin() {
|
||||
console.log('[memberInfo] 用户点击登录区域')
|
||||
uni.navigateTo({
|
||||
url: '/pages/memberInfo/login'
|
||||
})
|
||||
}
|
||||
|
||||
function onRenewCard() {
|
||||
uni.showModal({
|
||||
title: '续费会员卡',
|
||||
@@ -258,14 +245,8 @@ function onRenewCard() {
|
||||
|
||||
function onQuickAction(type) {
|
||||
const routes = {
|
||||
booking: PAGE.COURSE_LIST,
|
||||
bodyTest: PAGE.BODY_TEST_HOME,
|
||||
bodyReport: PAGE.BODY_TEST_HISTORY,
|
||||
trainReport: PAGE.TRAIN_REPORT,
|
||||
coupon: PAGE.COUPONS,
|
||||
points: PAGE.POINTS,
|
||||
referral: PAGE.REFERRAL,
|
||||
course: PAGE.MY_COURSES
|
||||
points: PAGE.POINTS
|
||||
}
|
||||
if (routes[type]) {
|
||||
navigateToPage(routes[type])
|
||||
@@ -284,18 +265,6 @@ function onCheckInTap(item) {
|
||||
})
|
||||
}
|
||||
|
||||
function onBodyReportHistory() {
|
||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||
}
|
||||
|
||||
function onBodyReportView() {
|
||||
const id = bodyReport.value?.recordId
|
||||
const url = id
|
||||
? `${PAGE.BODY_TEST_REPORT}?id=${id}`
|
||||
: PAGE.BODY_TEST_HISTORY
|
||||
navigateToPage(url)
|
||||
}
|
||||
|
||||
function onCouponViewAll() {
|
||||
navigateToPage(PAGE.COUPONS)
|
||||
}
|
||||
@@ -308,16 +277,10 @@ function onRedeemPoints() {
|
||||
navigateToPage(PAGE.POINTS)
|
||||
}
|
||||
|
||||
function onReferralRules() {
|
||||
navigateToPage(PAGE.REFERRAL)
|
||||
}
|
||||
|
||||
function onSetting(key) {
|
||||
const labels = {
|
||||
notify: '通知设置',
|
||||
password: '修改密码',
|
||||
privacy: '隐私政策',
|
||||
nfc: 'NFC 门禁卡',
|
||||
delete: '注销账户'
|
||||
}
|
||||
if (key === 'delete') {
|
||||
@@ -361,9 +324,7 @@ function handleLogout() {
|
||||
cardInfo.value = {}
|
||||
bookingPreview.value = []
|
||||
checkIns.value = []
|
||||
bodyReport.value = {}
|
||||
couponPoints.value = {}
|
||||
referral.value = {}
|
||||
hasActiveCard.value = false
|
||||
refreshFromStore()
|
||||
uni.showToast({ title: '已退出', icon: 'success' })
|
||||
@@ -400,12 +361,29 @@ onShow(() => {
|
||||
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-body-report.css';
|
||||
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||
@import '@/common/style/memberInfo/member-info-referral.css';
|
||||
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||
|
||||
/* 页面布局:scroll-view + tabbar */
|
||||
.member-info-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--gradient-sky);
|
||||
}
|
||||
|
||||
.member-scroll-wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.member-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 固定 TabBar */
|
||||
.tabbar-fixed {
|
||||
position: fixed;
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<view class="pc-payment__methods">
|
||||
<view class="pc-payment__method pc-payment__method--selected">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/alipay.png" mode="aspectFit" class="pc-payment__icon" />
|
||||
<text class="pc-payment__name">支付宝支付</text>
|
||||
<text class="pc-payment__name">{{ isWebEnv ? '扫码支付 (Web环境)' : 'App支付' }}</text>
|
||||
<view class="pc-payment__check"></view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -88,6 +88,72 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二维码支付对话框 (Web环境) -->
|
||||
<view class="qr-payment-overlay" v-if="showQrPayment" @tap="closeQrPayment">
|
||||
<view class="qr-payment-dialog" @tap.stop>
|
||||
<view class="qr-payment-dialog__header">
|
||||
<text class="qr-payment-dialog__title">{{ qrPaymentStatus === 'pending' ? '扫码支付' : (qrPaymentStatus === 'success' ? '支付成功' : '支付失败') }}</text>
|
||||
<view class="qr-payment-dialog__close" @tap="closeQrPayment" v-if="qrPaymentStatus !== 'pending'">×</view>
|
||||
</view>
|
||||
|
||||
<view class="qr-payment-dialog__body">
|
||||
<!-- 待支付状态:显示二维码 -->
|
||||
<template v-if="qrPaymentStatus === 'pending'">
|
||||
<view class="qr-payment-dialog__order">
|
||||
<text class="qr-payment-dialog__label">订单信息</text>
|
||||
<text class="qr-payment-dialog__value">{{ selectedCard?.cardName }} × 1</text>
|
||||
</view>
|
||||
<view class="qr-payment-dialog__amount">
|
||||
<text class="qr-payment-dialog__label">支付金额</text>
|
||||
<text class="qr-payment-dialog__money">¥{{ selectedCard?.price || 0 }}</text>
|
||||
</view>
|
||||
<view class="qr-payment-dialog__qrcode">
|
||||
<image
|
||||
v-if="qrCodeUrl"
|
||||
:src="'https://api.qrserver.com/v1/create-qr-code?size=200x200&data=' + encodeURIComponent(qrCodeUrl)"
|
||||
class="qr-payment-dialog__qrcode-img"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view v-else class="qr-payment-dialog__qrcode-loading">加载中...</view>
|
||||
</view>
|
||||
<view class="qr-payment-dialog__tips">
|
||||
<text class="qr-payment-dialog__tip">请使用支付宝扫码支付</text>
|
||||
<text class="qr-payment-dialog__tip qr-payment-dialog__tip--warn" v-if="isWebEnv">⚠️ 沙箱环境测试</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 支付成功状态 -->
|
||||
<template v-else-if="qrPaymentStatus === 'success'">
|
||||
<view class="qr-payment-dialog__result">
|
||||
<text class="qr-payment-dialog__icon">✅</text>
|
||||
<text class="qr-payment-dialog__result-text">支付成功!</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 支付失败状态 -->
|
||||
<template v-else>
|
||||
<view class="qr-payment-dialog__result">
|
||||
<text class="qr-payment-dialog__icon">❌</text>
|
||||
<text class="qr-payment-dialog__result-text">支付失败</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="qr-payment-dialog__footer">
|
||||
<template v-if="qrPaymentStatus === 'pending'">
|
||||
<view class="qr-payment-dialog__btn qr-payment-dialog__btn--secondary" @tap="closeQrPayment">
|
||||
<text>取消支付</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="qr-payment-dialog__btn qr-payment-dialog__btn--primary" @tap="closeQrPayment">
|
||||
<text>确定</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -95,9 +161,27 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { onBackPress } from '@dcloudio/uni-app'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { purchaseMemberCard, createPayment, getPaymentStatus } from '@/api/main.js'
|
||||
import { purchaseMemberCard, createPayment, createQrCodePayment, getPaymentStatus } from '@/api/main.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
|
||||
// ==================== Web环境检测 ====================
|
||||
// #ifdef H5
|
||||
const isWebEnv = typeof navigator !== 'undefined' && !typeof plus !== 'undefined'
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
const isWebEnv = false
|
||||
// #endif
|
||||
|
||||
// ==================== 二维码支付相关状态 ====================
|
||||
const showQrPayment = ref(false)
|
||||
const qrPaymentStatus = ref('pending') // pending | success | fail
|
||||
const qrCodeUrl = ref('')
|
||||
let qrPaymentResolve = null
|
||||
let qrPaymentReject = null
|
||||
let currentQrOrderId = null
|
||||
let qrPollingTimer = null
|
||||
let isQrPolling = false
|
||||
|
||||
const cardTypes = ref([
|
||||
{ id: 1, cardName: '月卡', cardType: 'DURATION', validityDays: 30, price: 1, originalPrice: 399 },
|
||||
{ id: 2, cardName: '季卡', cardType: 'DURATION', validityDays: 90, price: 1, originalPrice: 999 },
|
||||
@@ -265,6 +349,103 @@ function startPolling(orderId) {
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Web环境扫码支付 ====================
|
||||
function openQrPayment(qrCode, orderId) {
|
||||
qrCodeUrl.value = qrCode
|
||||
currentQrOrderId = orderId
|
||||
qrPaymentStatus.value = 'pending'
|
||||
showQrPayment.value = true
|
||||
|
||||
// 开始轮询支付状态
|
||||
startQrPolling(orderId)
|
||||
}
|
||||
|
||||
function closeQrPayment() {
|
||||
showQrPayment.value = false
|
||||
stopQrPolling()
|
||||
|
||||
if (qrPaymentStatus.value === 'pending' && qrPaymentReject) {
|
||||
qrPaymentReject(new Error('用户取消支付'))
|
||||
}
|
||||
|
||||
qrPaymentResolve = null
|
||||
qrPaymentReject = null
|
||||
currentQrOrderId = null
|
||||
qrCodeUrl.value = ''
|
||||
}
|
||||
|
||||
// ==================== 二维码支付轮询 ====================
|
||||
function startQrPolling(orderId) {
|
||||
if (isQrPolling) return
|
||||
isQrPolling = true
|
||||
|
||||
let pollCount = 0
|
||||
const maxRetry = 300 // 5分钟超时 (300秒)
|
||||
|
||||
function doQuery() {
|
||||
if (!showQrPayment.value || qrPaymentStatus.value !== 'pending') return
|
||||
pollCount++
|
||||
|
||||
getPaymentStatus(orderId)
|
||||
.then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
const status = res.data.status || res.data.payStatus
|
||||
console.log(`[QrPoll] 第 ${pollCount} 次查询,状态:`, status)
|
||||
|
||||
if (status === 'SUCCESS') {
|
||||
qrPaymentStatus.value = 'success'
|
||||
stopQrPolling()
|
||||
if (qrPaymentResolve) {
|
||||
qrPaymentResolve(true)
|
||||
}
|
||||
return
|
||||
} else if (status === 'FAIL' || status === 'CLOSED') {
|
||||
qrPaymentStatus.value = 'fail'
|
||||
stopQrPolling()
|
||||
if (qrPaymentReject) {
|
||||
qrPaymentReject(new Error('支付失败'))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
if (pollCount < maxRetry && showQrPayment.value && qrPaymentStatus.value === 'pending') {
|
||||
qrPollingTimer = setTimeout(doQuery, 1000)
|
||||
} else if (qrPaymentStatus.value === 'pending') {
|
||||
qrPaymentStatus.value = 'fail'
|
||||
stopQrPolling()
|
||||
if (qrPaymentReject) {
|
||||
qrPaymentReject(new Error('支付超时'))
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[QrPoll] 查询异常:', err)
|
||||
if (pollCount < maxRetry && showQrPayment.value && qrPaymentStatus.value === 'pending') {
|
||||
qrPollingTimer = setTimeout(doQuery, 1000)
|
||||
} else if (qrPaymentStatus.value === 'pending') {
|
||||
qrPaymentStatus.value = 'fail'
|
||||
stopQrPolling()
|
||||
if (qrPaymentReject) {
|
||||
qrPaymentReject(new Error('支付查询失败'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 延迟 2 秒开始轮询
|
||||
qrPollingTimer = setTimeout(doQuery, 2000)
|
||||
}
|
||||
|
||||
function stopQrPolling() {
|
||||
isQrPolling = false
|
||||
if (qrPollingTimer) {
|
||||
clearTimeout(qrPollingTimer)
|
||||
qrPollingTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 核心购买逻辑 ====================
|
||||
async function handlePurchase() {
|
||||
if (!selectedCard.value) {
|
||||
@@ -278,72 +459,114 @@ async function handlePurchase() {
|
||||
try {
|
||||
const store = loadMemberStore()
|
||||
const memberId = store.memberProfile?.id || store.profile?.id
|
||||
// if (!memberId) {
|
||||
// uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
// loading.value = false
|
||||
// return
|
||||
// }
|
||||
|
||||
// 金额转换:元 -> 分
|
||||
const transAmt = String(Math.round(selectedCard.value.price * 100))
|
||||
const cardName = selectedCard.value.cardName
|
||||
|
||||
// 1. 创建支付订单
|
||||
uni.showLoading({ title: '创建订单中...' })
|
||||
const payRes = await createPayment({
|
||||
memberId: memberId,
|
||||
orderType: 'MEMBER_CARD',
|
||||
goodsDesc: `购买${cardName}`,
|
||||
transAmt: transAmt,
|
||||
tradeType: TRADE_TYPE,
|
||||
remark: `会员卡类型ID:${selectedCard.value.id}`
|
||||
})
|
||||
// 根据环境选择支付方式
|
||||
if (isWebEnv) {
|
||||
// ==================== Web环境:扫码支付 ====================
|
||||
uni.showLoading({ title: '创建订单...' })
|
||||
|
||||
if (payRes.code !== 200) {
|
||||
const payRes = await createQrCodePayment({
|
||||
memberId: memberId,
|
||||
orderType: 'MEMBER_CARD',
|
||||
goodsDesc: `购买${cardName}`,
|
||||
transAmt: transAmt,
|
||||
tradeType: 'QRCODE',
|
||||
remark: `会员卡类型ID:${selectedCard.value.id}`
|
||||
})
|
||||
|
||||
if (payRes.code !== 200) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: payRes.message || '创建支付订单失败', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const payData = payRes.data
|
||||
const orderId = payData.orderId
|
||||
const qrCode = payData.qrCode
|
||||
|
||||
if (!qrCode) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '获取二维码失败', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 显示二维码支付对话框
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: payRes.message || '创建支付订单失败', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
await new Promise((resolve, reject) => {
|
||||
qrPaymentResolve = resolve
|
||||
qrPaymentReject = reject
|
||||
openQrPayment(qrCode, orderId)
|
||||
})
|
||||
|
||||
// 支付成功后继续购买会员卡
|
||||
} else {
|
||||
// ==================== App环境:唤起支付宝App ====================
|
||||
uni.showLoading({ title: '创建订单...' })
|
||||
|
||||
const payRes = await createPayment({
|
||||
memberId: memberId,
|
||||
orderType: 'MEMBER_CARD',
|
||||
goodsDesc: `购买${cardName}`,
|
||||
transAmt: transAmt,
|
||||
tradeType: TRADE_TYPE,
|
||||
remark: `会员卡类型ID:${selectedCard.value.id}`
|
||||
})
|
||||
|
||||
if (payRes.code !== 200) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: payRes.message || '创建支付订单失败', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const payData = payRes.data
|
||||
const orderId = payData.orderId
|
||||
const payUrl = payData.payUrl || payData.payInfo || payData.h5PayUrl
|
||||
|
||||
if (!payUrl) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '获取支付链接失败', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 唤起支付宝
|
||||
uni.hideLoading()
|
||||
uni.showLoading({ title: '正在唤起支付宝...' })
|
||||
|
||||
try {
|
||||
await invokeAlipayApp(payUrl)
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: err.message || '唤起支付宝失败', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 轮询支付状态
|
||||
uni.showLoading({ title: '等待支付结果...' })
|
||||
await startPolling(orderId)
|
||||
}
|
||||
|
||||
const payData = payRes.data
|
||||
const orderId = payData.orderId
|
||||
|
||||
// 2. 获取支付链接
|
||||
const payUrl = payData.payUrl || payData.payInfo || payData.h5PayUrl
|
||||
if (!payUrl) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '获取支付链接失败', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 唤起支付宝 App
|
||||
uni.hideLoading()
|
||||
uni.showLoading({ title: '正在唤起支付宝...' })
|
||||
|
||||
try {
|
||||
await invokeAlipayApp(payUrl)
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '唤起支付宝失败,请确认已安装支付宝', icon: 'none' })
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 轮询支付状态
|
||||
uni.showLoading({ title: '等待支付结果...' })
|
||||
await startPolling(orderId)
|
||||
|
||||
// 5. 支付成功,购买会员卡
|
||||
// 支付成功,购买会员卡
|
||||
uni.hideLoading()
|
||||
const memberRes = await purchaseMemberCard({
|
||||
memberId: memberId,
|
||||
memberCardId: selectedCard.value.id,
|
||||
sourceOrderId: orderId
|
||||
sourceOrderId: currentQrOrderId || currentQrOrderId
|
||||
})
|
||||
|
||||
if (memberRes.code === 200) {
|
||||
// 关闭二维码对话框
|
||||
if (showQrPayment.value) {
|
||||
showQrPayment.value = false
|
||||
}
|
||||
uni.showToast({ title: '购买成功 🎉', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
@@ -377,6 +600,7 @@ onUnmounted(() => {
|
||||
pollingTimer.value = null
|
||||
}
|
||||
isPolling.value = false
|
||||
stopQrPolling()
|
||||
if (typeof plus !== 'undefined') {
|
||||
plus.globalEvent.removeEventListener('resume', () => {})
|
||||
}
|
||||
@@ -691,4 +915,157 @@ onUnmounted(() => {
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
// ==================== 模拟支付对话框 ====================
|
||||
.mock-payment-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.qr-payment-dialog {
|
||||
width: 320px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #E2E8F0;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1A202C;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #A0AEC0;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__order,
|
||||
.qr-payment-dialog__amount {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__label {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__value {
|
||||
font-size: 14px;
|
||||
color: #2D3748;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__money {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #E53E3E;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__qrcode {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
background: #F7FAFC;
|
||||
border-radius: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__qrcode-img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__qrcode-loading {
|
||||
font-size: 14px;
|
||||
color: #A0AEC0;
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__tip {
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
|
||||
&--warn {
|
||||
color: #ED8936;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-payment-dialog__result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__result-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2D3748;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.qr-payment-dialog__btn {
|
||||
flex: 1;
|
||||
padding: 14px 0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
&--primary {
|
||||
background: #1677FF;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: #E2E8F0;
|
||||
color: #4A5568;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user