移除前端中的会员卡和支付相关,新增微信一键登录
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import request from "@/utils/request.js"
|
||||
import request from "@/utils/request.js"
|
||||
|
||||
export function getGroupCourseList(params = {}, options = {}) {
|
||||
return request.get('/groupCourse/list', params, options)
|
||||
@@ -108,7 +108,7 @@ export function getGroupCourseRecommendList(params = {}, options = { cache: fals
|
||||
* @param {number} memberId - 会员ID
|
||||
*/
|
||||
export function qrSignInGroupCourse(courseId, memberId) {
|
||||
return request.post(`/groupCourse/${courseId}/qrsignin`, { memberId })
|
||||
return request.post(`/groupCourse/signin/${memberId}`, { courseId })
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from "@/utils/request.js"
|
||||
import request from "@/utils/request.js"
|
||||
|
||||
/**
|
||||
* 微信小程序登录
|
||||
@@ -52,7 +52,7 @@ export function getQRCode(options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
|
||||
* @param {object} params - { qrcode: string, memberId: number }
|
||||
*/
|
||||
export function checkIn(params) {
|
||||
return request.post('/checkIn/scan', params)
|
||||
return request.post('/checkIn', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,167 +167,6 @@ export function updateUserInfo(params) {
|
||||
return request.put('/member/info', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买会员卡
|
||||
* @param {object} params - 购买参数
|
||||
* @param {number} params.memberId - 会员ID
|
||||
* @param {number} params.memberCardId - 会员卡类型ID
|
||||
* @param {number} [params.sourceOrderId] - 来源订单ID
|
||||
*/
|
||||
export function purchaseMemberCard(params) {
|
||||
return request.post('/member-card-records/purchase', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的会员卡列表
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {object} options - 请求选项 { cache: boolean }
|
||||
*/
|
||||
export function getMyMemberCards(memberId, options = { cache: false }) {
|
||||
return request.get(`/member-card-records/my-cards/${memberId}`, {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员的所有会员卡(支持状态筛选)
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {string} status - 状态筛选:all-全部, active-有效, expired-已失效
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function getMyMemberCardsWithStatus(memberId, status = 'all', options = { cache: false }) {
|
||||
return request.get(`/member-card-records/my-cards-with-status/${memberId}`, { status }, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员的主要有效会员卡(只返回一条)
|
||||
* 优先返回临期卡,其次返回普通有效卡
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {object} options - 请求选项 { cache: boolean }
|
||||
*/
|
||||
export function getPrimaryMemberCard(memberId, options = { cache: false }) {
|
||||
return request.get(`/member-card-records/primary/${memberId}`, {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员卡交易/流水记录(按会员ID)
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {object} options - 请求选项 { cache: boolean }
|
||||
*/
|
||||
export function getMemberCardTransactions(memberId, options = { cache: false }) {
|
||||
return request.get(`/member-card-transactions/member/${memberId}`, {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员卡交易/流水记录(按会员卡记录ID)
|
||||
* @param {number} recordId - 会员卡记录ID
|
||||
* @param {object} options - 请求选项 { cache: boolean }
|
||||
*/
|
||||
export function getMemberCardTransactionsByRecordId(recordId, options = { cache: false }) {
|
||||
return request.get(`/member-card-transactions/record/${recordId}`, {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前有效的会员卡列表
|
||||
* @param {object} options - 请求选项 { cache: boolean }
|
||||
*/
|
||||
export function getActiveMemberCards(options = { cache: false }) {
|
||||
return request.get('/member-cards/active', {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取会员卡详情
|
||||
* @param {number} cardId - 会员卡ID
|
||||
* @param {object} options - 请求选项 { cache: boolean }
|
||||
*/
|
||||
export function getMemberCardById(cardId, options = { cache: false }) {
|
||||
return request.get(`/member-cards/${cardId}`, {}, options)
|
||||
}
|
||||
|
||||
// ========== 支付密码相关API ==========
|
||||
|
||||
/**
|
||||
* 检查会员是否已设置支付密码
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function checkPayPasswordSet(memberId, options = {}) {
|
||||
return request.get(`/pay-password/check/${memberId}`, {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置支付密码
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {string} password - 支付密码
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function setPayPassword(memberId, password, options = {}) {
|
||||
return request.post(`/pay-password/set/${memberId}`, { password }, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证支付密码
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {string} password - 支付密码
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function verifyPayPassword(memberId, password, options = {}) {
|
||||
return request.post(`/pay-password/verify/${memberId}`, { password }, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置支付密码
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {string} oldPassword - 旧密码
|
||||
* @param {string} newPassword - 新密码
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function resetPayPassword(memberId, oldPassword, newPassword, options = {}) {
|
||||
return request.post(`/pay-password/reset/${memberId}`, { oldPassword, newPassword }, options)
|
||||
}
|
||||
|
||||
// ========== 储值卡相关API ==========
|
||||
|
||||
/**
|
||||
* 获取储值卡信息(包含余额、消费记录等)
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function getStoredCardInfo(memberId, options = {}) {
|
||||
return request.get(`/stored-card/${memberId}`, {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取储值卡余额
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function getStoredCardBalance(memberId, options = {}) {
|
||||
return request.get(`/stored-card/${memberId}/balance`, {}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 储值卡支付(验证密码并扣减余额)
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {string} password - 支付密码
|
||||
* @param {number} amount - 支付金额
|
||||
* @param {object} options - 请求选项
|
||||
*/
|
||||
export function payWithStoredCard(memberId, password, amount, options = {}) {
|
||||
return request.post(`/stored-card/pay/${memberId}`, {
|
||||
password,
|
||||
amount
|
||||
}, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询储值卡充值记录
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {number} page - 页码
|
||||
* @param {number} size - 每页数量
|
||||
*/
|
||||
export function getStoredCardRechargeRecords(memberId, page = 1, size = 20, options = {}) {
|
||||
return request.get(`/stored-card/${memberId}/recharges?page=${page}&size=${size}`, {}, options)
|
||||
}
|
||||
|
||||
// ========== 系统配置相关API ==========
|
||||
|
||||
/**
|
||||
@@ -339,85 +178,6 @@ export function getConfigByKey(configKey, options = {}) {
|
||||
return request.get(`/config/key/${configKey}`, {}, options)
|
||||
}
|
||||
|
||||
// ========== 支付相关API ==========
|
||||
|
||||
/**
|
||||
* 创建支付订单
|
||||
* @param {object} params - 支付参数
|
||||
* @param {number} params.memberId - 会员ID
|
||||
* @param {string} params.orderType - 订单类型 (MEMBER_CARD-会员卡, GROUP_COURSE-团课, GOODS-商品)
|
||||
* @param {string} params.goodsDesc - 商品描述
|
||||
* @param {string} params.transAmt - 交易金额(单位:分)
|
||||
* @param {string} params.tradeType - 交易类型 (WECHAT-微信, ALIPAY-支付宝)
|
||||
* @param {string} [params.remark] - 备注
|
||||
* @param {string} [params.acctId] - 账户号
|
||||
* @param {string} [params.timeExpire] - 过期时间 (yyyyMMddHHmmss)
|
||||
* @param {string} [params.delayAcctFlag] - 延迟入账标识
|
||||
* @param {number} [params.feeFlag] - 手续费标识
|
||||
* @param {string} [params.limitPayType] - 禁用支付方式
|
||||
* @param {string} [params.channelNo] - 渠道号
|
||||
* @param {string} [params.payScene] - 支付场景
|
||||
* @param {string} [params.notifyUrl] - 回调通知URL
|
||||
*/
|
||||
export function createPayment(params) {
|
||||
return request.post('/payment/create', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建扫码支付订单
|
||||
* @param {object} params - 支付参数
|
||||
*/
|
||||
export function createQrCodePayment(params) {
|
||||
return request.post('/payment/qrcode/create', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付状态(本地数据库)
|
||||
* @param {string} orderId - 订单ID
|
||||
*/
|
||||
export function getPaymentStatus(orderId) {
|
||||
return request.get(`/payment/${orderId}`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询汇付官方支付状态
|
||||
* @param {string} outOrdId - 商户订单号
|
||||
* @param {string} hfSeqId - 汇付全局流水号(可选)
|
||||
*/
|
||||
export function queryHuifuTradeByOrderId(outOrdId, hfSeqId = null) {
|
||||
let url = `/payment/huifu/query?outOrdId=${outOrdId}`
|
||||
if (hfSeqId) {
|
||||
url += `&hfSeqId=${hfSeqId}`
|
||||
}
|
||||
return request.get(url, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询待支付订单
|
||||
* @param {number} memberId - 会员ID
|
||||
* @param {string} orderType - 订单类型
|
||||
*/
|
||||
export function getPendingOrder(memberId, orderType) {
|
||||
return request.get(`/payment/pending/${memberId}?orderType=${orderType}`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭订单
|
||||
* @param {string} orderId - 订单ID
|
||||
*/
|
||||
export function closeOrder(orderId) {
|
||||
return request.post(`/payment/${orderId}/close`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
* @param {string} orderId - 订单ID
|
||||
* @param {string} refundAmt - 退款金额(单位:分)
|
||||
*/
|
||||
export function refundPayment(orderId, refundAmt) {
|
||||
return request.post(`/payment/${orderId}/refund`, { refundAmt })
|
||||
}
|
||||
|
||||
// ========== 课程相关API ==========
|
||||
|
||||
/**
|
||||
@@ -454,7 +214,6 @@ export function getGroupCoursePage(params = {}, options = { cache: true, cacheTi
|
||||
* @param {object} params - 预约参数
|
||||
* @param {number} params.courseId - 团课ID
|
||||
* @param {number} params.memberId - 会员ID
|
||||
* @param {number} params.memberCardRecordId - 会员卡记录ID
|
||||
*/
|
||||
export function bookGroupCourse(params) {
|
||||
return request.post('/groupCourse/book', params)
|
||||
@@ -522,18 +281,7 @@ export default {
|
||||
getUserInfo,
|
||||
getMemberDetail,
|
||||
updateUserInfo,
|
||||
purchaseMemberCard,
|
||||
getMyMemberCards,
|
||||
getMyMemberCardsWithStatus,
|
||||
getPrimaryMemberCard,
|
||||
getStoredCardInfo,
|
||||
getStoredCardBalance,
|
||||
payWithStoredCard,
|
||||
getStoredCardRechargeRecords,
|
||||
createPayment,
|
||||
createQrCodePayment,
|
||||
getPaymentStatus,
|
||||
refundPayment,
|
||||
getConfigByKey,
|
||||
getRecommendCourses,
|
||||
getCourseDetail,
|
||||
getGroupCoursePage,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// common/constants/routes.js
|
||||
// common/constants/routes.js
|
||||
|
||||
/** 与 pages.json 保持一致 */
|
||||
export const PAGE = {
|
||||
@@ -6,7 +6,6 @@ export const PAGE = {
|
||||
COURSE: '/pages/course/index',
|
||||
MEMBER: '/pages/memberInfo/memberInfo',
|
||||
BOOKING: '/pages/memberInfo/booking',
|
||||
MEMBER_CARD: '/pages/memberInfo/memberCard',
|
||||
USER_INFO: '/pages/memberInfo/userInfo',
|
||||
BODY_TEST_HOME: '/pages/memberInfo/bodyTestHome',
|
||||
BODY_TEST_CONNECT: '/pages/memberInfo/bodyTestConnect',
|
||||
@@ -30,10 +29,7 @@ export const PAGE = {
|
||||
POINTS: '/pages/memberInfo/points',
|
||||
REFERRAL: '/pages/memberInfo/referral',
|
||||
MY_COURSES: '/pages/memberInfo/myCourses',
|
||||
CHECK_IN_HISTORY: '/pages/memberInfo/checkInHistory',
|
||||
PURCHASE_CARD: '/pages/memberInfo/purchaseCard',
|
||||
SET_PAY_PASSWORD: '/pages/memberInfo/setPayPassword',
|
||||
RECHARGE_STORED_CARD: '/pages/memberInfo/rechargeStoredCard'
|
||||
CHECK_IN_HISTORY: '/pages/memberInfo/checkInHistory'
|
||||
}
|
||||
|
||||
/** 底部 TabBar 页面路径,顺序与 TabBar.vue 一致 */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 登录拦截Hook
|
||||
* 用于在需要登录的操作前检查用户登录状态,未登录则弹出登录框
|
||||
*
|
||||
@@ -230,8 +230,6 @@ export function loginGuardMixin() {
|
||||
*/
|
||||
export const protectedRoutes = [
|
||||
'/pages/memberInfo/memberInfo',
|
||||
'/pages/memberInfo/purchaseCard',
|
||||
'/pages/memberInfo/memberCard',
|
||||
'/pages/memberInfo/checkIn',
|
||||
'/pages/memberInfo/booking',
|
||||
'/pages/memberInfo/coupons',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 路由权限拦截器
|
||||
* 自动拦截配置好的路径,未登录时自动弹出登录弹窗
|
||||
*
|
||||
@@ -31,8 +31,6 @@ const WHITE_LIST = [
|
||||
* 匹配以下前缀的页面都需要登录
|
||||
*/
|
||||
const LOGIN_REQUIRED_PREFIXES = [
|
||||
'/pages/memberInfo/memberCard',
|
||||
'/pages/memberInfo/purchaseCard',
|
||||
'/pages/memberInfo/checkIn',
|
||||
'/pages/memberInfo/booking',
|
||||
'/pages/memberInfo/coupons',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- components/TabBar.vue -->
|
||||
<!-- components/TabBar.vue -->
|
||||
<template>
|
||||
<view v-if="shouldShowTabBar" class="tab-bar-wrapper">
|
||||
<view class="tab-bar">
|
||||
@@ -55,7 +55,6 @@ const HIDE_TABBAR_PAGES = [
|
||||
'pages/memberInfo/pointsMall',
|
||||
'pages/memberInfo/referral',
|
||||
'pages/memberInfo/userInfo',
|
||||
'pages/memberInfo/memberCard',
|
||||
]
|
||||
|
||||
function getActiveIndexFromRoute() {
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
<template>
|
||||
<view class="ppm-overlay" v-if="visible" @tap="handleCancel">
|
||||
<view class="ppm-dialog" @tap.stop>
|
||||
<!-- 头部 -->
|
||||
<view class="ppm-header">
|
||||
<text class="ppm-header__icon">🔒</text>
|
||||
<text class="ppm-header__title">{{ title }}</text>
|
||||
<text v-if="subtitle" class="ppm-header__subtitle">{{ subtitle }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 金额展示 -->
|
||||
<view v-if="amount != null" class="ppm-amount">
|
||||
<text class="ppm-amount__label">{{ amountLabel }}</text>
|
||||
<text class="ppm-amount__value">¥{{ formatAmount(amount) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 退款提示(免费次数) -->
|
||||
<view v-if="cancelNotice && cancelNotice.length" class="ppm-notice">
|
||||
<text>{{ cancelNotice }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view class="ppm-input-wrap">
|
||||
<view class="ppm-code-box">
|
||||
<view
|
||||
v-for="i in 6"
|
||||
:key="i"
|
||||
class="ppm-code-item"
|
||||
:class="{
|
||||
'ppm-code-item--focus': focusIndex === i - 1 && password.length < 6,
|
||||
'ppm-code-item--filled': password.length >= i
|
||||
}"
|
||||
>
|
||||
<text v-if="password.length >= i" class="ppm-code-dot"></text>
|
||||
<view v-if="focusIndex === i - 1 && password.length === i - 1" class="ppm-code-cursor"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 隐藏输入框 -->
|
||||
<input
|
||||
class="ppm-hidden-input"
|
||||
type="number"
|
||||
:maxlength="6"
|
||||
:focus="inputFocus"
|
||||
:value="password"
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
|
||||
<!-- 提示 -->
|
||||
<view class="ppm-tips">
|
||||
<text>请输入6位支付密码</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="ppm-buttons">
|
||||
<view class="ppm-btn ppm-btn--cancel" @tap="handleCancel">
|
||||
<text>取消</text>
|
||||
</view>
|
||||
<view
|
||||
class="ppm-btn ppm-btn--confirm"
|
||||
:class="{ 'ppm-btn--disabled': password.length < 6 || loading }"
|
||||
@tap="handleConfirm"
|
||||
>
|
||||
<text>{{ loading ? '验证中...' : '确认' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
title: { type: String, default: '支付验证' },
|
||||
subtitle: { type: String, default: '' },
|
||||
amount: { type: [Number, String], default: null },
|
||||
amountLabel: { type: String, default: '支付金额' },
|
||||
cancelNotice: { type: String, default: '' },
|
||||
errorMsg: { type: String, default: '' },
|
||||
loading: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel'])
|
||||
|
||||
const password = ref('')
|
||||
const inputFocus = ref(false)
|
||||
const focusIndex = ref(0)
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
password.value = ''
|
||||
focusIndex.value = 0
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
inputFocus.value = true
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function formatAmount(val) {
|
||||
return Number(val).toFixed(2)
|
||||
}
|
||||
|
||||
function onInput(e) {
|
||||
let val = String(e.detail.value || '').replace(/\D/g, '').slice(0, 6)
|
||||
password.value = val
|
||||
focusIndex.value = val.length
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
inputFocus.value = true
|
||||
focusIndex.value = password.value.length
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
// Keep focusIndex, just mark input as not focused
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (password.value.length < 6 || props.loading) return
|
||||
emit('confirm', password.value)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function clear() {
|
||||
password.value = ''
|
||||
focusIndex.value = 0
|
||||
}
|
||||
|
||||
defineExpose({ clear })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ppm-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: 9999;
|
||||
animation: ppm-fade-in 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes ppm-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.ppm-dialog {
|
||||
width: 600rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
padding: 48rpx 40rpx 36rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
animation: ppm-scale-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@keyframes ppm-scale-in {
|
||||
from { opacity: 0; transform: scale(0.85); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.ppm-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.ppm-header__icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.ppm-header__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1A202C;
|
||||
}
|
||||
|
||||
.ppm-header__subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #8A99B4;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.ppm-amount {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx 40rpx;
|
||||
background: linear-gradient(135deg, #FFF5F0 0%, #FFF8F5 100%);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ppm-amount__label {
|
||||
font-size: 22rpx;
|
||||
color: #8A99B4;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.ppm-amount__value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #FF6B35;
|
||||
font-family: 'DIN', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.ppm-notice {
|
||||
width: 100%;
|
||||
padding: 12rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #FFF7ED;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #FFEDD5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ppm-notice text {
|
||||
font-size: 22rpx;
|
||||
color: #C2410C;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ppm-input-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.ppm-code-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ppm-code-item {
|
||||
width: 36px;
|
||||
height: 40px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ppm-code-item--focus {
|
||||
border-color: #FF6B35;
|
||||
}
|
||||
|
||||
.ppm-code-item--filled {
|
||||
border-color: #1A202C;
|
||||
}
|
||||
|
||||
.ppm-code-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #1A202C;
|
||||
}
|
||||
|
||||
.ppm-code-cursor {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: #FF6B35;
|
||||
animation: ppm-blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes ppm-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.ppm-hidden-input {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.ppm-tips {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.ppm-tips text {
|
||||
font-size: 22rpx;
|
||||
color: #8A99B4;
|
||||
}
|
||||
|
||||
.ppm-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ppm-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ppm-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.ppm-btn--cancel {
|
||||
background: #F3F4F6;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.ppm-btn--confirm {
|
||||
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 8rpx 20rpx rgba(255, 107, 53, 0.25);
|
||||
}
|
||||
|
||||
.ppm-btn--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- 状态栏占位区(仅 App 端生效,H5/小程序无状态栏概念) -->
|
||||
<template>
|
||||
<!-- 状态栏占位区,为刘海屏和安全区域留出空间 -->
|
||||
<view class="tab-page__status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- 导航栏主体 -->
|
||||
<view class="tab-page__header">
|
||||
@@ -38,15 +38,9 @@ const isAnimating = ref(false)
|
||||
const statusBarHeight = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef APP-PLUS
|
||||
// App 端获取真实状态栏高度(px)
|
||||
// 所有平台获取状态栏高度,为刘海屏/状态栏留出安全空间
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 0
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
// H5/小程序:状态栏由浏览器或框架处理,不额外占位
|
||||
statusBarHeight.value = 0
|
||||
// #endif
|
||||
|
||||
if (props.showBack) {
|
||||
isAnimating.value = true
|
||||
@@ -65,9 +59,13 @@ function goBack() {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 状态栏占位:仅在 App 端有高度,H5/小程序为 0,背景继承页面 */
|
||||
/* 状态栏占位:适配所有平台,包含刘海屏安全区域 */
|
||||
.tab-page__status-bar {
|
||||
width: 100%;
|
||||
/* iOS 刘海屏额外安全区域 */
|
||||
padding-top: constant(safe-area-inset-top); /* iOS 11.0-11.2 */
|
||||
padding-top: env(safe-area-inset-top); /* iOS 11.2+ */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
|
||||
@@ -61,11 +61,13 @@ const qrScanSignIn = () => {
|
||||
console.log('扫码结果:', res)
|
||||
const qrContent = res.result
|
||||
|
||||
// 解析二维码内容获取 courseId
|
||||
// 解析二维码内容获取 courseId 和课程名称
|
||||
let courseId = null
|
||||
let courseName = ''
|
||||
try {
|
||||
const parsed = JSON.parse(qrContent)
|
||||
courseId = Number(parsed.courseId)
|
||||
courseId = Number(parsed.id || parsed.courseId)
|
||||
courseName = parsed.courseName || ''
|
||||
} catch {
|
||||
const num = Number(qrContent)
|
||||
if (!isNaN(num)) {
|
||||
@@ -89,7 +91,7 @@ const qrScanSignIn = () => {
|
||||
const result = await qrSignInGroupCourse(courseId, memberId)
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
uni.showToast({ title: '签到成功', icon: 'success' })
|
||||
uni.showToast({ title: courseName ? `「${courseName}」签到成功` : '签到成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: '/pages/memberInfo/myCourses' })
|
||||
}, 1200)
|
||||
@@ -99,7 +101,11 @@ const qrScanSignIn = () => {
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('团课签到失败:', e)
|
||||
const errMsg = e?.message || e?.data?.message || '签到失败'
|
||||
let errMsg = e?.message || e?.data?.message || '签到失败'
|
||||
// 附加课程名称(如果已知)
|
||||
if (courseName && !errMsg.includes(courseName)) {
|
||||
errMsg = `「${courseName}」${errMsg}`
|
||||
}
|
||||
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<view class="member-card-section">
|
||||
<view class="member-card-section__inner">
|
||||
<!-- 标题栏 -->
|
||||
<view class="member-card-section__head">
|
||||
<view class="member-card-section__head-inner">
|
||||
<text class="member-card-section__title">我的会员卡</text>
|
||||
<view class="member-card-section__count" v-if="activeCount > 0">
|
||||
<text class="member-card-section__count-num">{{ activeCount }}</text>
|
||||
<text class="member-card-section__count-text">张有效</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 三张卡展示 -->
|
||||
<view class="card-three-row">
|
||||
<!-- 储值卡 -->
|
||||
<view
|
||||
class="card-three-item"
|
||||
:class="{ 'card-three-item--inactive': !storedCard.hasCard }"
|
||||
hover-class="mi-tap-card--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="$emit('stored-card-tap')"
|
||||
>
|
||||
<view class="card-three-item__icon-wrap card-three-item__icon-wrap--stored">
|
||||
<uni-icons type="wallet" size="28rpx" :color="storedCard.hasCard ? '#667eea' : '#B0BEC5'" />
|
||||
</view>
|
||||
<text class="card-three-item__label">储值卡</text>
|
||||
<text class="card-three-item__value" v-if="storedCard.hasCard">¥{{ storedCard.balance }}</text>
|
||||
<text class="card-three-item__value card-three-item__value--empty" v-else>未购买</text>
|
||||
<text class="card-three-item__sub" v-if="storedCard.hasCard">余额</text>
|
||||
</view>
|
||||
|
||||
<!-- 次数卡 -->
|
||||
<view
|
||||
class="card-three-item"
|
||||
:class="{ 'card-three-item--inactive': !countCard.hasCard }"
|
||||
>
|
||||
<view class="card-three-item__icon-wrap card-three-item__icon-wrap--count">
|
||||
<uni-icons type="loop" size="28rpx" :color="countCard.hasCard ? '#4CAF50' : '#B0BEC5'" />
|
||||
</view>
|
||||
<text class="card-three-item__label">次数卡</text>
|
||||
<text class="card-three-item__value" v-if="countCard.hasCard">{{ countCard.remainingTimes }}次</text>
|
||||
<text class="card-three-item__value card-three-item__value--empty" v-else>未购买</text>
|
||||
<text class="card-three-item__sub" v-if="countCard.hasCard">剩余</text>
|
||||
</view>
|
||||
|
||||
<!-- 时长卡 -->
|
||||
<view
|
||||
class="card-three-item"
|
||||
:class="{ 'card-three-item--inactive': !timeCard.hasCard }"
|
||||
>
|
||||
<view class="card-three-item__icon-wrap card-three-item__icon-wrap--time">
|
||||
<uni-icons type="calendar" size="28rpx" :color="timeCard.hasCard ? '#FF6B35' : '#B0BEC5'" />
|
||||
</view>
|
||||
<text class="card-three-item__label">时长卡</text>
|
||||
<text class="card-three-item__value" v-if="timeCard.hasCard">{{ timeCard.remainingDays }}天</text>
|
||||
<text class="card-three-item__value card-three-item__value--empty" v-else>未购买</text>
|
||||
<text class="card-three-item__sub" v-if="timeCard.hasCard">剩余</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="card-actions">
|
||||
<view
|
||||
class="card-actions__btn card-actions__btn--primary"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="$emit('view-all')"
|
||||
>
|
||||
<text class="card-actions__btn-text">查看卡片</text>
|
||||
</view>
|
||||
<view
|
||||
class="card-actions__btn card-actions__btn--ghost"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="$emit('purchase')"
|
||||
>
|
||||
<text class="card-actions__btn-text card-actions__btn-text--ghost">购买新卡</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { getStoredCardInfo, getMyMemberCardsWithStatus } from '@/api/main.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
|
||||
defineEmits(['view-all', 'purchase', 'stored-card-tap'])
|
||||
|
||||
const storedCard = reactive({ hasCard: false, balance: 0 })
|
||||
const countCard = reactive({ hasCard: false, remainingTimes: 0 })
|
||||
const timeCard = reactive({ hasCard: false, remainingDays: 0 })
|
||||
const activeCount = ref(0)
|
||||
let isLoading = false
|
||||
|
||||
async function loadMemberCard() {
|
||||
if (isLoading) return
|
||||
isLoading = true
|
||||
try {
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) return
|
||||
|
||||
// 并行获取储值卡和所有有效卡
|
||||
const [storedRes, activeCardsRes] = await Promise.all([
|
||||
getStoredCardInfo(memberId).catch(() => null),
|
||||
getMyMemberCardsWithStatus(memberId, 'active').catch(() => null)
|
||||
])
|
||||
|
||||
// 解析储值卡
|
||||
if (storedRes) {
|
||||
const data = storedRes.data || storedRes.value || storedRes
|
||||
if (data && (data.balance > 0 || data.totalAmount > 0 || data.id)) {
|
||||
storedCard.hasCard = true
|
||||
storedCard.balance = data.balance || data.totalAmount || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 解析有效卡列表(排除储值卡)
|
||||
let allActive = []
|
||||
if (activeCardsRes?.value) {
|
||||
allActive = activeCardsRes.value
|
||||
} else if (Array.isArray(activeCardsRes)) {
|
||||
allActive = activeCardsRes
|
||||
} else if (activeCardsRes?.data) {
|
||||
allActive = Array.isArray(activeCardsRes.data) ? activeCardsRes.data : (activeCardsRes.data.value || [])
|
||||
}
|
||||
allActive = allActive.filter(card => card.memberCardType !== 'STORED_VALUE_CARD')
|
||||
activeCount.value = allActive.length
|
||||
|
||||
// 查找次数卡
|
||||
const countCardData = allActive.find(c => c.memberCardType === 'COUNT_CARD')
|
||||
if (countCardData) {
|
||||
countCard.hasCard = true
|
||||
countCard.remainingTimes = countCardData.remainingTimes || countCardData.remainingAmount || 0
|
||||
}
|
||||
|
||||
// 查找时长卡(优先找有效期最长的)
|
||||
const timeCards = allActive.filter(c => c.memberCardType === 'TIME_CARD')
|
||||
if (timeCards.length > 0) {
|
||||
timeCard.hasCard = true
|
||||
// 计算剩余天数
|
||||
let maxDays = 0
|
||||
timeCards.forEach(c => {
|
||||
if (c.expireTime) {
|
||||
const now = new Date()
|
||||
const expire = new Date(c.expireTime)
|
||||
const days = Math.max(0, Math.ceil((expire - now) / (1000 * 60 * 60 * 24)))
|
||||
if (days > maxDays) maxDays = days
|
||||
}
|
||||
})
|
||||
timeCard.remainingDays = maxDays
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[MemberInfoMemberCard] 加载失败:', e)
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMemberCard()
|
||||
})
|
||||
|
||||
defineExpose({ loadMemberCard })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
|
||||
// ========== 容器 ==========
|
||||
.member-card-section {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-card-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ========== 标题栏 ==========
|
||||
.member-card-section__head {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-card-section__head-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-card-section__title {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.member-card-section__count {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-card-section__count-num {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #FF6B35;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.member-card-section__count-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
color: #FF6B35;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// ========== 三张卡片行 ==========
|
||||
.card-three-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-three-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.card-three-item--inactive {
|
||||
opacity: 0.55;
|
||||
background: #F5F7FA;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card-three-item__icon-wrap {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-three-item__icon-wrap--stored {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.card-three-item__icon-wrap--count {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.card-three-item__icon-wrap--time {
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
}
|
||||
|
||||
.card-three-item--inactive .card-three-item__icon-wrap {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-three-item__label {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #5E6F8D;
|
||||
}
|
||||
|
||||
.card-three-item__value {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #1E2A3A;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card-three-item__value--empty {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: #B0BEC5;
|
||||
}
|
||||
|
||||
.card-three-item__sub {
|
||||
font-size: 20rpx;
|
||||
color: #A0AEC0;
|
||||
}
|
||||
|
||||
// ========== 操作按钮 ==========
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-actions__btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-actions__btn--primary {
|
||||
background: linear-gradient(135deg, #FF6B35, #FF8C5A);
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.card-actions__btn--ghost {
|
||||
background: #FFFFFF;
|
||||
border: 2rpx solid #FF6B35;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-actions__btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.card-actions__btn-text--ghost {
|
||||
color: #FF6B35;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,15 +12,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/purchaseCard",
|
||||
"style": {
|
||||
"navigationBarTitleText": "购买会员卡"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/login",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
@@ -47,13 +42,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/memberCard",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的会员卡"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/userInfo",
|
||||
"style": {
|
||||
@@ -201,32 +189,6 @@
|
||||
"navigationBarTitleText": "签到记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/cardDetail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "会员卡详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/confirmPayment",
|
||||
"style": {
|
||||
"navigationBarTitleText": "确认支付"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/setPayPassword",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "设置支付密码"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/rechargeStoredCard",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "储值卡充值"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/memberInfo/courseList",
|
||||
"style": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="checkin-page">
|
||||
<!-- 顶部导航栏(固定顶部) -->
|
||||
<view class="header">
|
||||
@@ -97,7 +97,8 @@ import { ref } from 'vue';
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
// 引入状态组件(路径与你保持一致)
|
||||
import QrStatus from '@/components/QRCode/StatusCard.vue'
|
||||
import { getQRCode, getMyMemberCards } from '@/api/main.js'
|
||||
import { getQRCode, checkIn as apiCheckIn } from '@/api/main.js'
|
||||
import { qrSignInGroupCourse } from '@/api/groupCourse.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
|
||||
let image = ref("")
|
||||
@@ -220,40 +221,6 @@ import { getMemberId } from '@/utils/request.js'
|
||||
throw new Error('未获取到会员ID')
|
||||
}
|
||||
|
||||
const cardRes = await getMyMemberCards(memberId)
|
||||
console.log('[签到页面] 查询会员卡结果:', JSON.stringify(cardRes, null, 2))
|
||||
|
||||
const hasCard = cardRes && cardRes.data && cardRes.data.length > 0
|
||||
console.log('[签到页面] 查询会员卡 - hasCard:', hasCard)
|
||||
|
||||
if (!hasCard) {
|
||||
isHaveCard.value = false
|
||||
uni.hideLoading()
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '您还没有会员卡,无法进行签到。是否去购买会员卡?',
|
||||
confirmText: '去购买',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/memberInfo/purchaseCard'
|
||||
})
|
||||
} else {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isHaveCard.value = true
|
||||
uni.showLoading({
|
||||
title: '生成签到二维码...',
|
||||
@@ -388,9 +355,53 @@ import { getMemberId } from '@/utils/request.js'
|
||||
})
|
||||
}
|
||||
|
||||
// 手动签到接口
|
||||
const checkIn = (qrContent) => {
|
||||
console.log(qrContent)
|
||||
// 手动签到接口(兼容到店签到和团课签到两种二维码)
|
||||
const checkIn = async (qrContent) => {
|
||||
console.log('扫码结果:', qrContent)
|
||||
|
||||
// 尝试解析为 JSON,判断是否为团课二维码(包含 id/courseName 字段)
|
||||
let parsedQr = null
|
||||
try {
|
||||
parsedQr = JSON.parse(qrContent)
|
||||
} catch (_) {
|
||||
// 非 JSON,按到店签到处理
|
||||
}
|
||||
|
||||
if (parsedQr && parsedQr.id && parsedQr.courseName) {
|
||||
// 团课签到:使用团课签到接口
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const courseId = Number(parsedQr.id)
|
||||
uni.showLoading({ title: '团课签到中...' })
|
||||
try {
|
||||
const result = await qrSignInGroupCourse(courseId, memberId)
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
status.value = 'scanned'
|
||||
errorText.value = ''
|
||||
QRStatus.value = '团课签到成功'
|
||||
isCheckIn.value = true
|
||||
setCacheData("checkInTime", QRStatus.value)
|
||||
setCacheData("isCheckIn", isCheckIn.value)
|
||||
uni.showToast({ title: '团课签到成功', icon: 'success' })
|
||||
} else {
|
||||
status.value = 'error'
|
||||
errorText.value = result.message || '团课签到失败'
|
||||
uni.showToast({ title: result.message || '签到失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
status.value = 'error'
|
||||
errorText.value = err.message || '团课签到失败'
|
||||
uni.showToast({ title: err.message || '签到失败', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 到店签到:使用原有签到接口
|
||||
apiCheckIn({ qrContent }).then(res => {
|
||||
closeWebSocket()
|
||||
console.log(res)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="course-detail-page">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader title="课程详情" subtitle="团课预约" :show-back="true" />
|
||||
@@ -188,39 +188,6 @@
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar">
|
||||
<!-- 左侧:支付方式选择(包含价格) -->
|
||||
<view v-if="hasMultiplePayment" class="payment-card">
|
||||
<!-- 左侧:滑块支付选择 -->
|
||||
<view class="payment-slider" @click="togglePayment">
|
||||
<view class="slider-bg" :class="{ right: selectedPayment === 'pointCard' }"></view>
|
||||
<view class="tab-item" :class="{ active: selectedPayment === 'storedValue' }">
|
||||
<text class="tab-label">储值卡</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: selectedPayment === 'pointCard' }">
|
||||
<text class="tab-label">次卡</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 右侧:当前选中价格 -->
|
||||
<view class="payment-price">
|
||||
<text class="price" :class="selectedPrice.type">
|
||||
<text class="currency">¥</text><text class="amount">{{ course.storedValueAmount }}</text>
|
||||
<text class="point-text"><text class="point-icon">⚡</text>{{ course.pointCardAmount }}次</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 免费课程或单一支付方式 -->
|
||||
<view v-else class="price-display">
|
||||
<text v-if="Number(course.storedValueAmount) === 0 && Number(course.pointCardAmount) === 0" class="price free">免费</text>
|
||||
<text v-else-if="Number(course.storedValueAmount) > 0" class="price">
|
||||
<text class="currency">¥</text>{{ course.storedValueAmount }}
|
||||
</text>
|
||||
<text v-else class="price">
|
||||
<text class="point-icon">⚡</text>
|
||||
<text>{{ course.pointCardAmount }}次</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧:预约按钮 -->
|
||||
<view :class="['booking-btn', { disabled: !canBook }]" @click="handleBooking">
|
||||
<text>{{ canBook ? '立即预约' : (course.currentMembers >= course.maxMembers ? '已满员' : statusText) }}</text>
|
||||
@@ -228,17 +195,6 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付密码弹窗 -->
|
||||
<PayPasswordModal
|
||||
:visible="showPayPwd"
|
||||
title="支付验证"
|
||||
:subtitle="course.courseName"
|
||||
:amount="course.storedValueAmount || 0"
|
||||
amount-label="课程金额"
|
||||
@confirm="onPayPasswordConfirm"
|
||||
@cancel="onPayPasswordCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -246,19 +202,12 @@ import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getGroupCourseDetail, bookGroupCourse } from '@/api/groupCourse.js'
|
||||
import { getCourseCoverUrl } from '@/utils/request.js'
|
||||
import { getPrimaryMemberCard } from '@/api/main.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import PayPasswordModal from '@/components/global/PayPasswordModal.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
|
||||
// 支付密码弹窗状态
|
||||
const showPayPwd = ref(false)
|
||||
const payPwdLoading = ref(false)
|
||||
let payPwdResolve = null
|
||||
|
||||
// 课程详情数据
|
||||
const course = ref({
|
||||
id: '',
|
||||
@@ -323,46 +272,6 @@ const canBook = computed(() => {
|
||||
return status === '0' && !isFull
|
||||
})
|
||||
|
||||
// 是否有多种支付方式
|
||||
const hasMultiplePayment = computed(() => {
|
||||
const storedValue = Number(course.value.storedValueAmount)
|
||||
const pointCard = Number(course.value.pointCardAmount)
|
||||
return storedValue > 0 && pointCard > 0
|
||||
})
|
||||
|
||||
// 当前选中的支付方式
|
||||
const selectedPayment = ref('storedValue')
|
||||
|
||||
// 当前选中的支付方式价格显示
|
||||
const selectedPrice = computed(() => {
|
||||
const storedValue = Number(course.value.storedValueAmount)
|
||||
const pointCard = Number(course.value.pointCardAmount)
|
||||
|
||||
if (selectedPayment.value === 'storedValue') {
|
||||
return { type: 'storedValue', amount: storedValue }
|
||||
} else if (selectedPayment.value === 'pointCard') {
|
||||
return { type: 'pointCard', amount: pointCard }
|
||||
} else if (storedValue > 0 && pointCard > 0) {
|
||||
// 默认优先显示储值卡
|
||||
return { type: 'storedValue', amount: storedValue }
|
||||
} else if (storedValue > 0) {
|
||||
return { type: 'storedValue', amount: storedValue }
|
||||
} else if (pointCard > 0) {
|
||||
return { type: 'pointCard', amount: pointCard }
|
||||
}
|
||||
return { type: 'free', amount: 0 }
|
||||
})
|
||||
|
||||
// 选择/切换支付方式
|
||||
const selectPayment = (type) => {
|
||||
selectedPayment.value = type
|
||||
}
|
||||
|
||||
// 切换支付方式(滑块点击)
|
||||
const togglePayment = () => {
|
||||
selectedPayment.value = selectedPayment.value === 'storedValue' ? 'pointCard' : 'storedValue'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
@@ -405,29 +314,15 @@ const handleBooking = async () => {
|
||||
|
||||
uni.showModal({
|
||||
title: '确认预约',
|
||||
content: `确定要预约「${course.value.courseName}」吗?${course.value.storedValueAmount > 0 ? '\n金额: ¥' + course.value.storedValueAmount : ''}`,
|
||||
content: `确定要预约「${course.value.courseName}」吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
|
||||
// 弹出支付密码输入
|
||||
const payPassword = await requestPayPassword()
|
||||
if (!payPassword) return
|
||||
|
||||
uni.showLoading({ title: '预约中...' })
|
||||
try {
|
||||
const cardRes = await getPrimaryMemberCard(memberId)
|
||||
const memberCardRecordId = cardRes?.id || cardRes?.data?.id
|
||||
if (!memberCardRecordId) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '请先购买会员卡', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const result = await bookGroupCourse({
|
||||
courseId: Number(course.value.id),
|
||||
memberId: Number(memberId),
|
||||
memberCardRecordId: Number(memberCardRecordId),
|
||||
payPassword: payPassword
|
||||
memberId: Number(memberId)
|
||||
})
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
@@ -459,24 +354,6 @@ const handleBooking = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 弹出支付密码输入框,返回 Promise<string|null>
|
||||
function requestPayPassword() {
|
||||
return new Promise((resolve) => {
|
||||
payPwdResolve = resolve
|
||||
showPayPwd.value = true
|
||||
})
|
||||
}
|
||||
|
||||
function onPayPasswordConfirm(password) {
|
||||
payPwdResolve?.(password)
|
||||
showPayPwd.value = false
|
||||
}
|
||||
|
||||
function onPayPasswordCancel() {
|
||||
payPwdResolve?.(null)
|
||||
showPayPwd.value = false
|
||||
}
|
||||
|
||||
// 获取课程详情
|
||||
const fetchCourseDetail = async (id) => {
|
||||
try {
|
||||
@@ -751,71 +628,6 @@ onLoad((options) => {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.payment-options {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.payment-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx;
|
||||
background: linear-gradient(135deg, #F9FAFB 0%, #F3F4F6 100%);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid transparent;
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||
border-color: #FF6B35;
|
||||
|
||||
.payment-label,
|
||||
.payment-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&.free {
|
||||
background: linear-gradient(135deg, #ECFDF5 0%, #D1FAE5 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-label {
|
||||
font-size: 20rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.payment-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #FF6B35;
|
||||
|
||||
.free & {
|
||||
color: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-check {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background: #059669;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 预约须知 */
|
||||
.notice-card {
|
||||
background: #ffffff;
|
||||
@@ -857,152 +669,7 @@ onLoad((options) => {
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* 支付卡片(包含选择器和价格) */
|
||||
.payment-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #FFF7F5 0%, #FFF0EC 100%);
|
||||
border-radius: 52rpx;
|
||||
padding: 8rpx;
|
||||
height: 88rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 2rpx solid rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
/* 支付滑块选择器 */
|
||||
.payment-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #F5F7FA;
|
||||
border-radius: 44rpx;
|
||||
padding: 6rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
/* 滑动背景指示器 */
|
||||
.payment-slider .slider-bg {
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
left: 6rpx;
|
||||
width: calc(50% - 6rpx);
|
||||
height: calc(100% - 12rpx);
|
||||
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||
border-radius: 38rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.payment-slider .slider-bg.right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.payment-slider .tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
border-radius: 38rpx;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.payment-slider .tab-item.active .tab-label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.payment-slider .tab-label {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #6B7280;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 支付卡片中的价格 */
|
||||
.payment-price {
|
||||
min-width: 140rpx;
|
||||
padding: 0 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.payment-price .price {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #FF6B35;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
white-space: nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.payment-price .price .currency,
|
||||
.payment-price .price .amount,
|
||||
.payment-price .price .point-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-price .price.storedValue .currency,
|
||||
.payment-price .price.storedValue .amount {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.payment-price .price.pointCard .point-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.payment-price .currency {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.payment-price .point-icon {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* 价格展示(单一支付方式或免费) */
|
||||
.price-display {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.price-display .price {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.price-display .price.free {
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.price-display .price:not(.free) {
|
||||
color: #FF6B35;
|
||||
}
|
||||
|
||||
.price-display .currency {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-display .point-icon {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 预约按钮 */
|
||||
@@ -1014,7 +681,6 @@ onLoad((options) => {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.booking-btn.disabled {
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="card-detail-page">
|
||||
<view v-if="card" class="card-detail-page__body">
|
||||
<view class="cd-hero">
|
||||
<view class="cd-hero__card">
|
||||
<view class="cd-hero__card-header">
|
||||
<text class="cd-hero__card-name">{{ card.memberCardName || card.cardName }}</text>
|
||||
<view class="cd-hero__card-tag" :class="'cd-hero__card-tag--' + getTagClass(card.memberCardType || card.cardType)">
|
||||
<text class="cd-hero__card-tag-text">{{ getCardTypeName(card.memberCardType || card.cardType) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cd-hero__card-body">
|
||||
<view class="cd-hero__card-item" v-if="card.memberCardValidityDays || card.validityDays">
|
||||
<text class="cd-hero__card-label">有效期</text>
|
||||
<text class="cd-hero__card-value">{{ card.memberCardValidityDays || card.validityDays }}天</text>
|
||||
</view>
|
||||
<view class="cd-hero__card-item" v-if="(card.memberCardType === 'COUNT_CARD' || card.cardType === 'COUNT') && (card.memberCardTotalTimes || card.totalTimes)">
|
||||
<text class="cd-hero__card-label">总次数</text>
|
||||
<text class="cd-hero__card-value">{{ card.memberCardTotalTimes || card.totalTimes }}次</text>
|
||||
</view>
|
||||
<view class="cd-hero__card-item" v-if="(card.memberCardType === 'STORED_VALUE_CARD' || card.cardType === 'BALANCE') && (card.memberCardAmount || card.amount)">
|
||||
<text class="cd-hero__card-label">储值金额</text>
|
||||
<text class="cd-hero__card-value">{{ card.memberCardAmount || card.amount }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cd-hero__card-footer">
|
||||
<text class="cd-hero__card-price">¥{{ card.memberCardPrice || card.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cd-section">
|
||||
<text class="cd-section__title">会员卡权益</text>
|
||||
<view class="cd-benefits">
|
||||
<view class="cd-benefit">
|
||||
<view class="cd-benefit__icon">💪</view>
|
||||
<view class="cd-benefit__content">
|
||||
<text class="cd-benefit__title">全场器械使用</text>
|
||||
<text class="cd-benefit__desc">免费使用所有健身器械和设备</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cd-benefit">
|
||||
<view class="cd-benefit__icon">🏋️</view>
|
||||
<view class="cd-benefit__content">
|
||||
<text class="cd-benefit__title">免费团课</text>
|
||||
<text class="cd-benefit__desc">预约参加各类团课(次卡除外)</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cd-benefit">
|
||||
<view class="cd-benefit__icon">🧴</view>
|
||||
<view class="cd-benefit__content">
|
||||
<text class="cd-benefit__title">淋浴服务</text>
|
||||
<text class="cd-benefit__desc">免费使用淋浴间和储物柜</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cd-benefit">
|
||||
<view class="cd-benefit__icon">📱</view>
|
||||
<view class="cd-benefit__content">
|
||||
<text class="cd-benefit__title">在线预约</text>
|
||||
<text class="cd-benefit__desc">APP在线预约课程和签到</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cd-section" v-if="card.extraConfig">
|
||||
<text class="cd-section__title">详细解读</text>
|
||||
<view class="cd-desc">
|
||||
<text class="cd-desc__text">{{ parseExtraConfig(card.extraConfig) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cd-section">
|
||||
<text class="cd-section__title">购买须知</text>
|
||||
<view class="cd-notes">
|
||||
<text class="cd-notes__item">1. 会员卡一经购买,非特殊情况不予退款</text>
|
||||
<text class="cd-notes__item">2. 请在有效期内使用,过期自动失效</text>
|
||||
<text class="cd-notes__item">3. 会员卡仅限本人使用,不得转借他人</text>
|
||||
<text class="cd-notes__item">4. 支付订单有效时间为15分钟,超时自动取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cd-bottom">
|
||||
<view class="cd-bottom__total">
|
||||
<text class="cd-bottom__total-label">价格</text>
|
||||
<text class="cd-bottom__total-price">¥{{ card.memberCardPrice || card.price }}</text>
|
||||
</view>
|
||||
<view class="cd-bottom__btn" @tap="handleSelectCard">
|
||||
<text class="cd-bottom__btn-text">选择该卡</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="cd-loading">
|
||||
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getMemberCardById } from '@/api/main.js'
|
||||
|
||||
const card = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
function getCardTypeName(type) {
|
||||
const map = {
|
||||
TIME_CARD: '时长卡',
|
||||
DURATION: '时长卡',
|
||||
COUNT_CARD: '次卡',
|
||||
COUNT: '次卡',
|
||||
STORED_VALUE_CARD: '储值卡',
|
||||
BALANCE: '储值卡'
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
function getTagClass(type) {
|
||||
if (type === 'TIME_CARD' || type === 'DURATION') return 'time'
|
||||
if (type === 'COUNT_CARD' || type === 'COUNT') return 'count'
|
||||
if (type === 'STORED_VALUE_CARD' || type === 'BALANCE') return 'value'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function parseExtraConfig(config) {
|
||||
if (!config) return ''
|
||||
try {
|
||||
const obj = JSON.parse(config)
|
||||
return obj.description || obj.desc || config
|
||||
} catch (e) {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectCard() {
|
||||
if (!card.value) return
|
||||
uni.navigateTo({ url: `/pages/memberInfo/purchaseCard?cardId=${card.value.memberCardId || card.value.id}` })
|
||||
}
|
||||
|
||||
async function loadCardDetail(cardId) {
|
||||
if (!cardId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMemberCardById(cardId)
|
||||
if (res && res.memberCardId) {
|
||||
card.value = res
|
||||
} else if (res && res.code === 200 && res.data) {
|
||||
card.value = res.data
|
||||
} else {
|
||||
uni.showToast({ title: res?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载会员卡详情失败:', e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = currentPage.options || {}
|
||||
|
||||
const cardId = options.cardId || options.id
|
||||
if (cardId) {
|
||||
loadCardDetail(cardId)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
||||
|
||||
.card-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.card-detail-page__body {
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
|
||||
.cd-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 60vh;
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.cd-hero {
|
||||
background: linear-gradient(135deg, #1A365D 0%, #2C5282 100%);
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.cd-hero__card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.cd-hero__card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cd-hero__card-name {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.cd-hero__card-tag {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
&--time { background: rgba(66, 153, 225, 0.3); color: #90CDF4; }
|
||||
&--count { background: rgba(237, 137, 54, 0.3); color: #FBD38D; }
|
||||
&--value { background: rgba(56, 161, 105, 0.3); color: #9AE6B4; }
|
||||
&--default { background: rgba(160, 174, 192, 0.3); color: #CBD5E0; }
|
||||
}
|
||||
|
||||
.cd-hero__card-body {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cd-hero__card-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cd-hero__card-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.cd-hero__card-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.cd-hero__card-footer {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.cd-hero__card-price {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #F6E05E;
|
||||
}
|
||||
|
||||
.cd-hero__card-original {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.cd-section {
|
||||
margin: 16px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cd-section__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1A202C;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cd-benefits {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cd-benefit {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cd-benefit__icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cd-benefit__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cd-benefit__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2D3748;
|
||||
}
|
||||
|
||||
.cd-benefit__desc {
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.cd-desc__text {
|
||||
font-size: 14px;
|
||||
color: #4A5568;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cd-notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cd-notes__item {
|
||||
font-size: 13px;
|
||||
color: #718096;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cd-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.cd-bottom__total {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cd-bottom__total-label {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.cd-bottom__total-price {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #E53E3E;
|
||||
}
|
||||
|
||||
.cd-bottom__btn {
|
||||
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
|
||||
border-radius: 12px;
|
||||
padding: 16px 48px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.cd-bottom__btn-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,61 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<!-- 自定义Toast弹窗 -->
|
||||
<view class="custom-toast" :class="{ show: toastShow }">
|
||||
<text class="toast-text">{{ toastMessage }}</text>
|
||||
</view>
|
||||
|
||||
<!-- ==================== 微信小程序登录 ==================== -->
|
||||
<template v-if="isWechatMiniProgram">
|
||||
<view class="login-header" :style="{ paddingTop: statusBarHeightPx }">
|
||||
<view class="logo-wrapper">
|
||||
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="app-name">活氧舱欢迎您</text>
|
||||
<text class="app-slogan">享受运动,拥抱健康</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="wechat-card">
|
||||
<view class="card-body wechat-login-body">
|
||||
<text class="phone-display">微信一键登录</text>
|
||||
<text class="auth-tip">授权获取您的微信信息</text>
|
||||
|
||||
<!-- 获取微信头像昵称 -->
|
||||
<button
|
||||
v-if="canUseGetUserProfile"
|
||||
class="login-btn wechat-btn"
|
||||
hover-class="wechat-btn-hover"
|
||||
@tap="handleGetUserProfile"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<text>{{ isLoading ? '登录中...' : '微信一键登录' }}</text>
|
||||
</button>
|
||||
<!-- 低版本兼容 -->
|
||||
<button
|
||||
v-else
|
||||
class="login-btn wechat-btn"
|
||||
hover-class="wechat-btn-hover"
|
||||
open-type="getUserInfo"
|
||||
@getuserinfo="handleGetUserInfo"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<text>{{ isLoading ? '登录中...' : '微信一键登录' }}</text>
|
||||
</button>
|
||||
|
||||
<view class="agreement-text" :class="{ shake: shakeAgreement }">
|
||||
<checkbox-group @change="onAgreementChange">
|
||||
<checkbox :checked="agreed" value="agreed" color="#07C160" />
|
||||
</checkbox-group>
|
||||
<text>登录即表示同意</text>
|
||||
<text class="link" @tap="showAgreement">《用户协议》</text>
|
||||
<text>和</text>
|
||||
<text class="link" @tap="showPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- ==================== App/其他平台登录(原有) ==================== -->
|
||||
<template v-else>
|
||||
<view class="login-header">
|
||||
<view class="logo-wrapper">
|
||||
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
|
||||
@@ -98,15 +149,36 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { setToken, setRefreshToken } from '@/utils/request.js'
|
||||
import { loginWithPhone, oneClickLogin, sendCode } from '@/api/main.js'
|
||||
import { loginWithPhone, oneClickLogin, sendCode, login } from '@/api/main.js'
|
||||
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
|
||||
|
||||
// ===== 平台检测 =====
|
||||
const isWechatMiniProgram = ref(false)
|
||||
const statusBarHeightPx = ref('0px')
|
||||
|
||||
onMounted(() => {
|
||||
// 获取状态栏高度,为刘海屏留出安全空间
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
statusBarHeightPx.value = (sysInfo.statusBarHeight || 0) + 'px'
|
||||
|
||||
// 检测当前是否为微信小程序环境
|
||||
// #ifdef MP-WEIXIN
|
||||
isWechatMiniProgram.value = true
|
||||
// #endif
|
||||
|
||||
if (!isWechatMiniProgram.value) {
|
||||
// 非微信小程序,保持原有默认卡片
|
||||
activeCard.value = 'phone'
|
||||
}
|
||||
})
|
||||
|
||||
const activeCard = ref('phone')
|
||||
const isLoading = ref(false)
|
||||
const agreed = ref(true)
|
||||
@@ -440,6 +512,121 @@ async function handleSmsLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 微信小程序登录 =====
|
||||
const canUseGetUserProfile = ref(false)
|
||||
|
||||
// 检查是否支持 getUserProfile(基础库 2.10.4+)
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (wx.getUserProfile) {
|
||||
canUseGetUserProfile.value = true
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
/**
|
||||
* 微信小程序登录(新版:getUserProfile)
|
||||
*/
|
||||
function handleGetUserProfile() {
|
||||
if (!agreed.value) {
|
||||
triggerAgreementShake()
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.getUserProfile({
|
||||
desc: '用于完善会员资料',
|
||||
success: (profileRes) => {
|
||||
const userInfo = profileRes.userInfo
|
||||
console.log('[微信登录] 获取用户信息成功:', userInfo)
|
||||
// 执行微信登录
|
||||
performWechatLogin(userInfo)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[微信登录] 获取用户信息失败:', err)
|
||||
isLoading.value = false
|
||||
// 用户拒绝授权时,仍然尝试静默登录
|
||||
performWechatLogin(null)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序登录(旧版:getUserInfo)
|
||||
*/
|
||||
function handleGetUserInfo(e) {
|
||||
if (!agreed.value) {
|
||||
triggerAgreementShake()
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const userInfo = e.detail.userInfo
|
||||
if (userInfo) {
|
||||
console.log('[微信登录] 获取用户信息成功:', userInfo)
|
||||
}
|
||||
performWechatLogin(userInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行微信登录:调用 uni.login 获取 code,发送到后端
|
||||
*/
|
||||
function performWechatLogin(userInfo) {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: async (loginRes) => {
|
||||
const code = loginRes.code
|
||||
console.log('[微信登录] 获取 code 成功:', code)
|
||||
|
||||
if (!code) {
|
||||
showToast('微信登录失败,请重试')
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await login({
|
||||
code: code,
|
||||
nickname: userInfo?.nickName || '',
|
||||
avatar: userInfo?.avatarUrl || ''
|
||||
})
|
||||
|
||||
console.log('[微信登录] 后端返回:', res)
|
||||
|
||||
if (res && res.accessToken) {
|
||||
setToken(res.accessToken)
|
||||
if (res.refreshToken) {
|
||||
setRefreshToken(res.refreshToken)
|
||||
}
|
||||
saveLoginInfo(res)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
} else {
|
||||
showToast('登录失败,请重试')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[微信登录] 后端调用失败:', e)
|
||||
showToast(e?.message || '登录失败,请重试')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[微信登录] uni.login 失败:', err)
|
||||
showToast('微信登录失败,请重试')
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
@@ -481,6 +668,8 @@ button::after {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
@@ -523,6 +712,53 @@ button::after {
|
||||
height: 500rpx;
|
||||
}
|
||||
|
||||
/* ===== 微信小程序登录卡片 ===== */
|
||||
.wechat-card {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: $bg-white;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.18), 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
padding: 48rpx 40rpx;
|
||||
}
|
||||
|
||||
.wechat-login-body {
|
||||
min-height: 400rpx;
|
||||
justify-content: flex-start;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.wechat-btn {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
border-radius: $radius-md;
|
||||
border: none;
|
||||
background: #07C160;
|
||||
color: #ffffff;
|
||||
font-size: 34rpx;
|
||||
font-weight: $font-weight-bold;
|
||||
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: #A0E0B0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-btn-hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: absolute;
|
||||
height: 620rpx;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<scroll-view scroll-y class="scroll-container theme-light">
|
||||
<view class="member-page">
|
||||
<!-- Header: 用户信息区 -->
|
||||
@@ -47,105 +47,6 @@
|
||||
|
||||
<view class="member-page__body">
|
||||
<view class="member-page__sections">
|
||||
<!-- 快过期会员卡提醒 -->
|
||||
<view class="expiring-cards-section" v-if="expiringCards.length > 0">
|
||||
<view class="expiring-cards__inner">
|
||||
<view class="expiring-cards__header">
|
||||
<view class="expiring-cards__header-inner">
|
||||
<view class="expiring-cards__title-row">
|
||||
<image class="expiring-cards__warn-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png" mode="aspectFit" />
|
||||
<text class="expiring-cards__title">会员卡即将到期</text>
|
||||
</view>
|
||||
<view class="expiring-cards__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goMemberCard">
|
||||
<text class="expiring-cards__link-text">查看全部</text>
|
||||
<image class="expiring-cards__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright12.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="expiring-cards__list">
|
||||
<view
|
||||
v-for="(card, index) in expiringCards"
|
||||
:key="card.id || index"
|
||||
class="expiring-card-item"
|
||||
hover-class="mi-tap-row--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goMemberCard"
|
||||
>
|
||||
<view class="expiring-card-item__inner">
|
||||
<view class="expiring-card-item__icon-wrap">
|
||||
<image class="expiring-card-item__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="expiring-card-item__info">
|
||||
<text class="expiring-card-item__name">{{ card.name }}</text>
|
||||
<text class="expiring-card-item__validity">{{ card.validity }}</text>
|
||||
</view>
|
||||
<view class="expiring-card-item__days">
|
||||
<text class="expiring-card-item__days-num">{{ computeRemainingDays(card.validityEnd) }}</text>
|
||||
<text class="expiring-card-item__days-unit">天后到期</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会员卡区域 -->
|
||||
<view class="member-card-section">
|
||||
<view class="member-card-section__inner">
|
||||
<view class="member-card-section__head">
|
||||
<view class="member-card-section__head-inner">
|
||||
<text class="member-card-section__title">我的会员卡</text>
|
||||
<view class="member-card-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goMemberCard">
|
||||
<text class="member-card-section__link-text">查看全部</text>
|
||||
<image class="member-card-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright12.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-card-preview" hover-class="mi-tap-card--hover" :hover-stay-time="150" @tap="goMemberCard">
|
||||
<view class="member-card-preview__inner">
|
||||
<view class="member-card-preview__head">
|
||||
<view class="member-card-preview__head-inner">
|
||||
<view class="member-card-preview__type-row">
|
||||
<view class="member-card-preview__icon-wrap">
|
||||
<view class="member-card-preview__icon-border">
|
||||
<view class="member-card-preview__icon-bg"></view>
|
||||
<view class="member-card-preview__icon-stroke"></view>
|
||||
</view>
|
||||
<image class="member-card-preview__icon-line" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Line_2_468.png" mode="aspectFill" />
|
||||
</view>
|
||||
<text class="member-card-preview__name">{{ cardInfo.name }}</text>
|
||||
</view>
|
||||
<view class="member-card-preview__tag">
|
||||
<text class="member-card-preview__tag-text">{{ cardInfo.detailTag || '详情' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="member-card-preview__expire">{{ cardInfo.expireDate }}</text>
|
||||
<view class="member-card-preview__footer">
|
||||
<view class="member-card-preview__footer-inner">
|
||||
<view class="member-card-preview__days">
|
||||
<text class="member-card-preview__days-num">{{ cardInfo.remainingDays }}</text>
|
||||
<text class="member-card-preview__days-unit">天剩余</text>
|
||||
</view>
|
||||
<view class="member-card-preview__renew" hover-class="mi-tap-btn--hover" :hover-stay-time="150" @tap.stop="onRenewCard">
|
||||
<text class="member-card-preview__renew-text">续费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-card-tip">
|
||||
<view class="member-card-tip__inner">
|
||||
<view class="member-card-tip__content">
|
||||
<image class="member-card-tip__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png" mode="aspectFit" />
|
||||
<text class="member-card-tip__text">{{ cardInfo.tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-card-tip__border"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作区域 -->
|
||||
<view class="quick-actions">
|
||||
<view class="quick-actions__inner">
|
||||
@@ -390,9 +291,9 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { getCenterPageData, renewMemberCard, computeRemainingDays, saveMemberStore, loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getCenterPageData, saveMemberStore, loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import { getMyMemberCards, getCheckInStats, getMemberDetail } from '@/api/main.js'
|
||||
import { getCheckInStats, getMemberDetail } from '@/api/main.js'
|
||||
import { getMemberBookings } from '@/api/groupCourse.js'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
@@ -400,8 +301,6 @@ import PageHeader from '@/components/index/PageHeader.vue'
|
||||
// 页面数据
|
||||
const userInfo = ref({})
|
||||
const stats = ref({})
|
||||
const cardInfo = ref({})
|
||||
const expiringCards = ref([])
|
||||
const bodyReport = ref({})
|
||||
const couponPoints = ref({})
|
||||
const referral = ref({})
|
||||
@@ -468,8 +367,6 @@ function refreshFromStore() {
|
||||
const pageData = getCenterPageData(store)
|
||||
userInfo.value = pageData.userInfo
|
||||
stats.value = pageData.stats
|
||||
cardInfo.value = pageData.cardInfo
|
||||
expiringCards.value = pageData.expiringCards || []
|
||||
bodyReport.value = pageData.bodyReport
|
||||
couponPoints.value = pageData.couponPoints
|
||||
referral.value = pageData.referral
|
||||
@@ -558,72 +455,10 @@ async function refreshStatsFromServer() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCardsFromServer() {
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) return
|
||||
|
||||
try {
|
||||
const res = await getMyMemberCards(memberId)
|
||||
let cards = []
|
||||
if (Array.isArray(res)) {
|
||||
cards = res
|
||||
} else if (res.code === 200 && res.data) {
|
||||
cards = res.data
|
||||
} else if (Array.isArray(res.data)) {
|
||||
cards = res.data
|
||||
}
|
||||
|
||||
if (cards.length > 0) {
|
||||
const allCards = cards.map(myCard => {
|
||||
const cardName = myCard.memberCardName || myCard.cardName || myCard.name || `会员卡#${myCard.memberCardId || myCard.id}`
|
||||
const validityEnd = myCard.validityEnd || myCard.expireTime
|
||||
const validityStart = myCard.validityStart || myCard.purchaseTime || myCard.createdAt
|
||||
return {
|
||||
id: myCard.id,
|
||||
name: cardName,
|
||||
status: myCard.status || 'active',
|
||||
validity: validityEnd ? `有效期至 ${validityEnd}` : '永久有效',
|
||||
validityEnd: validityEnd,
|
||||
validityStart: validityStart
|
||||
}
|
||||
})
|
||||
|
||||
store.cards = allCards
|
||||
|
||||
const myCard = cards[0]
|
||||
const cardName = myCard.memberCardName || myCard.cardName || myCard.name || `会员卡#${myCard.memberCardId || myCard.id}`
|
||||
const validityEnd = myCard.validityEnd || myCard.expireTime
|
||||
const validityStart = myCard.validityStart || myCard.purchaseTime || myCard.createdAt
|
||||
|
||||
store.card = {
|
||||
id: myCard.id,
|
||||
name: cardName,
|
||||
status: myCard.status || 'active',
|
||||
validity: validityEnd ? `有效期至 ${validityEnd}` : '永久有效',
|
||||
validityEnd: validityEnd,
|
||||
validityStart: validityStart
|
||||
}
|
||||
store.cardInfo = {
|
||||
expireDate: validityEnd ? `有效期至 ${validityEnd}` : '永久有效',
|
||||
remainingDays: computeRemainingDays(validityEnd)
|
||||
}
|
||||
|
||||
saveMemberStore(store)
|
||||
refreshFromStore()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[MemberCenter] 刷新会员卡数据失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function goUserInfo() {
|
||||
navigateToPage(PAGE.USER_INFO)
|
||||
}
|
||||
|
||||
function goMemberCard() {
|
||||
navigateToPage(PAGE.MEMBER_CARD)
|
||||
}
|
||||
|
||||
function goBodyTestHistory() {
|
||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||
}
|
||||
@@ -646,20 +481,6 @@ function goReferral() {
|
||||
navigateToPage(PAGE.REFERRAL)
|
||||
}
|
||||
|
||||
function onRenewCard() {
|
||||
uni.showModal({
|
||||
title: '续费会员卡',
|
||||
content: '确认续费 90 天?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const store = loadMemberStore()
|
||||
renewMemberCard(store, 90)
|
||||
refreshFromStore()
|
||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onQuickAction(type) {
|
||||
const routes = {
|
||||
booking: PAGE.COURSE_LIST,
|
||||
@@ -733,7 +554,6 @@ onMounted(() => {
|
||||
|
||||
onShow(async () => {
|
||||
await refreshStatsFromServer()
|
||||
refreshCardsFromServer()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -866,125 +686,4 @@ onShow(async () => {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.expiring-cards-section {
|
||||
margin: 0 16px 12px;
|
||||
background: linear-gradient(135deg, #FFF7E6 0%, #FFE4CC 100%);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expiring-cards__inner {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.expiring-cards__header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.expiring-cards__header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.expiring-cards__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.expiring-cards__warn-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.expiring-cards__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #D46B08;
|
||||
}
|
||||
|
||||
.expiring-cards__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.expiring-cards__link-text {
|
||||
font-size: 13px;
|
||||
color: #D46B08;
|
||||
}
|
||||
|
||||
.expiring-cards__link-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.expiring-cards__list {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 12px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.expiring-card-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.expiring-card-item__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.expiring-card-item__icon-wrap {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expiring-card-item__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.expiring-card-item__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.expiring-card-item__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1A202C;
|
||||
}
|
||||
|
||||
.expiring-card-item__validity {
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.expiring-card-item__days {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.expiring-card-item__days-num {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #E74C3C;
|
||||
}
|
||||
|
||||
.expiring-card-item__days-unit {
|
||||
font-size: 11px;
|
||||
color: #E74C3C;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,15 +21,6 @@
|
||||
/>
|
||||
<view class="member-page__body">
|
||||
<view class="member-page__sections">
|
||||
<MemberInfoMemberCard
|
||||
v-if="isLogin"
|
||||
ref="memberCardRef"
|
||||
:card-info="cardInfo"
|
||||
@view-all="goMemberCard"
|
||||
@renew="onRenewCard"
|
||||
@purchase="goPurchaseCard"
|
||||
@stored-card-tap="goRechargeStoredCard"
|
||||
/>
|
||||
<MemberInfoLogout v-if="isLogin" @logout="handleLogout" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -50,7 +41,6 @@ import { onShow } from '@dcloudio/uni-app'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import {
|
||||
getCenterPageData,
|
||||
renewMemberCard,
|
||||
saveMemberStore,
|
||||
getDefaultStore,
|
||||
loadMemberStore
|
||||
@@ -59,7 +49,6 @@ import { getMemberId } from '@/utils/request.js'
|
||||
import { login as miniappLogin, getUserInfo, getMemberDetail, getCheckInStats, getMemberBookings, updateUserInfo } from '@/api/main.js'
|
||||
import { setToken, getToken, clearToken } from '@/utils/request.js'
|
||||
import MemberInfoHeader from '@/components/memberInfo/MemberInfoHeader.vue'
|
||||
import MemberInfoMemberCard from '@/components/memberInfo/MemberInfoMemberCard.vue'
|
||||
import MemberInfoLogout from '@/components/memberInfo/MemberInfoLogout.vue'
|
||||
import MemberCenterSkeleton from '@/components/Skeleton/MemberCenterSkeleton.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
@@ -73,8 +62,6 @@ const pageReady = ref(false)
|
||||
const hasActiveCard = ref(false)
|
||||
const isLogin = ref(false)
|
||||
|
||||
const memberCardRef = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
const token = uni.getStorageSync('token')
|
||||
const loginMember = uni.getStorageSync('loginMemberInfo')
|
||||
@@ -203,29 +190,6 @@ function goUserInfo() {
|
||||
navigateToPage(PAGE.USER_INFO)
|
||||
}
|
||||
|
||||
function goMemberCard() {
|
||||
navigateToPage(PAGE.MEMBER_CARD)
|
||||
}
|
||||
|
||||
function goPurchaseCard() {
|
||||
navigateToPage(PAGE.PURCHASE_CARD)
|
||||
}
|
||||
|
||||
function goRechargeStoredCard() {
|
||||
navigateToPage(PAGE.RECHARGE_STORED_CARD)
|
||||
}
|
||||
|
||||
function onRenewCard() {
|
||||
uni.showModal({
|
||||
title: '续费会员卡',
|
||||
content: '确认续费 90 天?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
uni.showToast({ title: '续费成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
@@ -268,10 +232,6 @@ onShow(() => {
|
||||
// 强制刷新数据(忽略 loading 防重入)
|
||||
loading.value = false
|
||||
fetchMemberInfo()
|
||||
// 刷新子组件数据
|
||||
if (memberCardRef.value && typeof memberCardRef.value.loadMemberCard === 'function') {
|
||||
memberCardRef.value.loadMemberCard()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -61,60 +61,22 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付密码弹窗 -->
|
||||
<PayPasswordModal
|
||||
:visible="showPayPwd"
|
||||
:title="payPwdTitle"
|
||||
:subtitle="payPwdSubtitle"
|
||||
:cancel-notice="payPwdNotice"
|
||||
@confirm="onPayPasswordConfirm"
|
||||
@cancel="onPayPasswordCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import ListSkeleton from '@/components/Skeleton/ListSkeleton.vue'
|
||||
import PayPasswordModal from '@/components/global/PayPasswordModal.vue'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import { getCourseCoverUrl } from '@/utils/request.js'
|
||||
import { getMemberBookings, qrSignInGroupCourse } from '@/api/groupCourse.js'
|
||||
import { cancelBooking as cancelBookingApi } from '@/api/groupCourse.js'
|
||||
import { getConfigByKey } from '@/api/main.js'
|
||||
|
||||
const activeTab = ref('ongoing')
|
||||
const ongoingBookings = ref([])
|
||||
const completedBookings = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 支付密码弹窗状态
|
||||
const showPayPwd = ref(false)
|
||||
const payPwdTitle = ref('')
|
||||
const payPwdSubtitle = ref('')
|
||||
const payPwdNotice = ref('')
|
||||
let payPwdResolve = null
|
||||
let cancelTarget = null
|
||||
|
||||
// 已取消次数
|
||||
const cancelCount = ref(0)
|
||||
const freeCancelLimit = ref(3) // 默认值,从API获取
|
||||
const freeCampaignInitialized = ref(false)
|
||||
const freeCancelsLeft = computed(() => Math.max(0, freeCancelLimit.value - cancelCount.value))
|
||||
|
||||
async function fetchFreeLimit() {
|
||||
try {
|
||||
const res = await getConfigByKey('group_course.cancel_free_limit')
|
||||
const value = res?.configValue || res?.data?.configValue || '3'
|
||||
freeCancelLimit.value = parseInt(value) || 3
|
||||
freeCampaignInitialized.value = true
|
||||
console.log('[myCourses] 免费取消次数阈值:', freeCancelLimit.value, '已取消次数:', cancelCount.value, '剩余:', freeCancelsLeft.value)
|
||||
} catch (e) {
|
||||
console.warn('[myCourses] 获取免费取消次数配置失败,使用默认值3:', e)
|
||||
freeCampaignInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const displayedBookings = computed(() => {
|
||||
return activeTab.value === 'ongoing'
|
||||
? ongoingBookings.value
|
||||
@@ -139,8 +101,6 @@ async function fetchBookings() {
|
||||
console.log('[myCourses] 预约记录:', bookings.length, '条', JSON.stringify(bookings))
|
||||
|
||||
if (Array.isArray(bookings)) {
|
||||
// 统计已取消次数(status='1')
|
||||
cancelCount.value = bookings.filter(b => String(b.status) === '1').length
|
||||
const mapped = bookings.map(mapBooking)
|
||||
ongoingBookings.value = mapped.filter(b => b.status === 'ongoing')
|
||||
completedBookings.value = mapped.filter(b => b.status === 'completed')
|
||||
@@ -220,27 +180,10 @@ async function handleCancel(item) {
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
|
||||
// 弹出支付密码输入
|
||||
cancelTarget = item
|
||||
payPwdTitle.value = '取消验证'
|
||||
payPwdSubtitle.value = item.title
|
||||
// 实时获取最新免费次数阈值
|
||||
await fetchFreeLimit()
|
||||
// 设置退款提示
|
||||
if (freeCancelsLeft.value > 0) {
|
||||
payPwdNotice.value = `您还有 ${freeCancelsLeft.value} 次免手续费退款机会(超出后将扣除10%手续费)`
|
||||
} else {
|
||||
payPwdNotice.value = '本次取消将扣除10%手续费'
|
||||
}
|
||||
console.log('[myCourses] 取消弹窗: payPwdNotice=', payPwdNotice.value, 'freeCancelsLeft=', freeCancelsLeft.value, 'cancelCount=', cancelCount.value, 'freeLimit=', freeCancelLimit.value)
|
||||
const payPassword = await requestPayPassword()
|
||||
if (!payPassword) return
|
||||
|
||||
uni.showLoading({ title: '取消中...' })
|
||||
try {
|
||||
const result = await cancelBookingApi(Number(item.id), {
|
||||
memberId: getMemberId(),
|
||||
payPassword: payPassword
|
||||
memberId: getMemberId()
|
||||
})
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
@@ -259,23 +202,6 @@ async function handleCancel(item) {
|
||||
})
|
||||
}
|
||||
|
||||
function requestPayPassword() {
|
||||
return new Promise((resolve) => {
|
||||
payPwdResolve = resolve
|
||||
showPayPwd.value = true
|
||||
})
|
||||
}
|
||||
|
||||
function onPayPasswordConfirm(password) {
|
||||
payPwdResolve?.(password)
|
||||
showPayPwd.value = false
|
||||
}
|
||||
|
||||
function onPayPasswordCancel() {
|
||||
payPwdResolve?.(null)
|
||||
showPayPwd.value = false
|
||||
}
|
||||
|
||||
// ===== 扫码签到 =====
|
||||
function handleScan() {
|
||||
uni.scanCode({
|
||||
@@ -287,7 +213,7 @@ function handleScan() {
|
||||
let courseId = null
|
||||
try {
|
||||
const parsed = JSON.parse(qrContent)
|
||||
courseId = Number(parsed.courseId)
|
||||
courseId = Number(parsed.id || parsed.courseId)
|
||||
} catch {
|
||||
const num = Number(qrContent)
|
||||
if (!isNaN(num)) {
|
||||
@@ -332,7 +258,6 @@ function handleScan() {
|
||||
|
||||
onMounted(() => {
|
||||
fetchBookings()
|
||||
fetchFreeLimit()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,405 +0,0 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="recharge-page">
|
||||
<MemberInfoSubNav title="储值卡充值" @back="goBack" />
|
||||
<view class="recharge-page__body">
|
||||
<!-- 当前余额 -->
|
||||
<view class="rc-balance-card">
|
||||
<view class="rc-balance-card__inner">
|
||||
<text class="rc-balance-card__label">当前余额</text>
|
||||
<view class="rc-balance-card__amount">
|
||||
<text class="rc-balance-card__symbol">¥</text>
|
||||
<text class="rc-balance-card__num">{{ currentBalance }}</text>
|
||||
</view>
|
||||
<text class="rc-balance-card__card-name">{{ currentCardName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值档位 -->
|
||||
<view class="rc-section">
|
||||
<text class="rc-section__title">选择充值金额</text>
|
||||
<view class="rc-grid">
|
||||
<view
|
||||
v-for="(item, index) in rechargeOptions"
|
||||
:key="index"
|
||||
class="rc-grid-item"
|
||||
:class="{ 'rc-grid-item--selected': selectedIndex === index }"
|
||||
@tap="selectRecharge(index)"
|
||||
>
|
||||
<view class="rc-grid-item__top">
|
||||
<text class="rc-grid-item__amount">¥{{ item.amount }}</text>
|
||||
</view>
|
||||
<view class="rc-grid-item__bottom">
|
||||
<text class="rc-grid-item__bonus">赠¥{{ item.bonus }}</text>
|
||||
</view>
|
||||
<view class="rc-grid-item__check" v-if="selectedIndex === index">
|
||||
<text>✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值说明 -->
|
||||
<view class="rc-rules">
|
||||
<text class="rc-rules__title">充值说明</text>
|
||||
<view class="rc-rules__list">
|
||||
<view class="rc-rules__item">
|
||||
<text>1. 充值金额实时到账,赠送金额同步到账</text>
|
||||
</view>
|
||||
<view class="rc-rules__item">
|
||||
<text>2. 余额可用于购买会员卡、消费等</text>
|
||||
</view>
|
||||
<view class="rc-rules__item">
|
||||
<text>3. 充值金额一经到账,不予退还</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="rc-bottom">
|
||||
<view class="rc-bottom__info">
|
||||
<text class="rc-bottom__label">实付金额</text>
|
||||
<view class="rc-bottom__amount">
|
||||
<text class="rc-bottom__symbol">¥</text>
|
||||
<text class="rc-bottom__num">{{ selectedAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="rc-bottom__btn"
|
||||
:class="{ 'rc-bottom__btn--disabled': loading }"
|
||||
@tap="handleRecharge"
|
||||
>
|
||||
<text>{{ loading ? '处理中...' : '立即充值' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import { checkPayPasswordSet, getStoredCardInfo } from '@/api/main.js'
|
||||
import { navigateToPage, PAGE } from '@/common/constants/routes.js'
|
||||
|
||||
const currentBalance = ref('0.00')
|
||||
const selectedIndex = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const rechargeOptions = ref([
|
||||
{ amount: 100, bonus: 10, payAmount: 100 },
|
||||
{ amount: 200, bonus: 20, payAmount: 200 },
|
||||
{ amount: 300, bonus: 30, payAmount: 300 }
|
||||
])
|
||||
|
||||
const selectedAmount = computed(() => {
|
||||
return rechargeOptions.value[selectedIndex.value]?.payAmount || 0
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
loadBalance()
|
||||
})
|
||||
|
||||
async function loadBalance() {
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) return
|
||||
|
||||
try {
|
||||
const res = await getStoredCardInfo(memberId)
|
||||
const data = res.data || res
|
||||
currentBalance.value = ((data.balance || 0).toFixed(2))
|
||||
} catch (e) {
|
||||
console.error('获取储值卡余额失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function selectRecharge(index) {
|
||||
selectedIndex.value = index
|
||||
}
|
||||
|
||||
async function handleRecharge() {
|
||||
if (loading.value) return
|
||||
|
||||
const memberId = getMemberId()
|
||||
|
||||
if (!memberId) {
|
||||
uni.showToast({ title: '用户未登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查支付密码
|
||||
uni.showLoading({ title: '检查中...' })
|
||||
try {
|
||||
const checkRes = await checkPayPasswordSet(memberId)
|
||||
uni.hideLoading()
|
||||
|
||||
const isSet = checkRes.isSet || checkRes.data?.isSet || false
|
||||
|
||||
if (!isSet) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '您尚未设置支付密码,请先设置支付密码后再进行充值',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
navigateToPage(PAGE.SET_PAY_PASSWORD)
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 确认充值
|
||||
const option = rechargeOptions.value[selectedIndex.value]
|
||||
uni.showModal({
|
||||
title: '确认充值',
|
||||
content: `确认充值 ${option.amount} 元?\n到账金额:${option.amount + option.bonus} 元\n(含赠送 ${option.bonus} 元)`,
|
||||
confirmText: '确认充值',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await initiatePayment(option)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('检查支付密码失败:', e)
|
||||
uni.showToast({ title: '检查失败,请稍后重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function initiatePayment(option) {
|
||||
const amount = option.payAmount
|
||||
const desc = `储值卡充值:充${option.amount}得${option.amount + option.bonus}`
|
||||
const remark = `rechargeAmount=${option.amount}&bonus=${option.bonus}`
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/memberInfo/confirmPayment?orderType=STORED_CARD_RECHARGE&amount=${amount}&desc=${encodeURIComponent(desc)}&remark=${encodeURIComponent(remark)}&returnPage=memberCard&cardType=stored`,
|
||||
fail: (err) => {
|
||||
console.error('跳转支付页面失败:', err)
|
||||
uni.showToast({ title: '跳转支付页面失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
|
||||
.recharge-page {
|
||||
min-height: 100vh;
|
||||
background: #F5F7FA;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.recharge-page__body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.rc-balance-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.rc-balance-card__inner {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.rc-balance-card__label {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rc-balance-card__amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.rc-balance-card__symbol {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.rc-balance-card__num {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rc-balance-card__card-name {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.rc-section {
|
||||
background: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rc-section__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1A202C;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rc-grid {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rc-grid-item {
|
||||
width: calc(33.33% - 8px);
|
||||
background: #F7FAFC;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 16px 8px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--selected {
|
||||
background: #F0F4FF;
|
||||
border-color: #1677FF;
|
||||
}
|
||||
}
|
||||
|
||||
.rc-grid-item__top {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rc-grid-item__amount {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1A202C;
|
||||
}
|
||||
|
||||
.rc-grid-item__bottom {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rc-grid-item__bonus {
|
||||
font-size: 12px;
|
||||
color: #E53E3E;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rc-grid-item__check {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #1677FF;
|
||||
border-radius: 0 10px 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.rc-rules {
|
||||
background: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.rc-rules__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1A202C;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rc-rules__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rc-rules__item {
|
||||
font-size: 13px;
|
||||
color: #718096;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rc-bottom {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #FFFFFF;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.rc-bottom__info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rc-bottom__label {
|
||||
font-size: 13px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.rc-bottom__amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.rc-bottom__symbol {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #E53E3E;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.rc-bottom__num {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #E53E3E;
|
||||
}
|
||||
|
||||
.rc-bottom__btn {
|
||||
padding: 12px 32px;
|
||||
background: #1677FF;
|
||||
border-radius: 24px;
|
||||
color: #FFFFFF;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="set-pay-password-page">
|
||||
<MemberInfoSubNav :title="isReset ? '修改支付密码' : '设置支付密码'" @back="goBack" />
|
||||
<view class="set-pay-password-page__body">
|
||||
<view class="spp-intro">
|
||||
<image class="spp-intro__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/lock.png" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<VerifyCodeInput
|
||||
ref="verifyInputRef"
|
||||
v-model="password"
|
||||
:title="currentTitle"
|
||||
:desc="currentDesc"
|
||||
:length="6"
|
||||
:mask="true"
|
||||
type="box"
|
||||
:errorMsg="errorMsg"
|
||||
@complete="handleComplete"
|
||||
/>
|
||||
|
||||
<view class="spp-forgot" v-if="isReset && step === 1" @tap="goForgotPassword">
|
||||
<text>忘记密码?</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import { setPayPassword, verifyPayPassword, resetPayPassword } from '@/api/main.js'
|
||||
|
||||
const password = ref('')
|
||||
const step = ref(1)
|
||||
const firstPassword = ref('')
|
||||
const isReset = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const verifyInputRef = ref(null)
|
||||
|
||||
const currentTitle = computed(() => {
|
||||
if (isReset.value && step.value === 1) {
|
||||
return '请输入原6位支付密码'
|
||||
}
|
||||
if (step.value === 1) {
|
||||
return isReset.value ? '请输入新的6位支付密码' : '请设置6位支付密码'
|
||||
}
|
||||
return '请再次输入6位支付密码'
|
||||
})
|
||||
|
||||
const currentDesc = computed(() => {
|
||||
if (isReset.value && step.value === 1) {
|
||||
return '验证原密码后可设置新密码'
|
||||
}
|
||||
return '支付密码用于充值、消费等资金操作,请妥善保管'
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
if (options && options.reset === 'true') {
|
||||
isReset.value = true
|
||||
}
|
||||
})
|
||||
|
||||
function handleComplete(pwd) {
|
||||
errorMsg.value = ''
|
||||
|
||||
if (isReset.value && step.value === 1) {
|
||||
verifyOldPassword(pwd)
|
||||
return
|
||||
}
|
||||
|
||||
if (step.value === 1) {
|
||||
firstPassword.value = pwd
|
||||
step.value = 2
|
||||
password.value = ''
|
||||
nextTick(() => {
|
||||
verifyInputRef.value?.focusInput()
|
||||
})
|
||||
} else {
|
||||
if (pwd !== firstPassword.value) {
|
||||
errorMsg.value = '两次密码输入不一致,请重新输入'
|
||||
password.value = ''
|
||||
step.value = 1
|
||||
firstPassword.value = ''
|
||||
nextTick(() => {
|
||||
verifyInputRef.value?.focusInput()
|
||||
})
|
||||
return
|
||||
}
|
||||
submitPassword(pwd)
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyOldPassword(pwd) {
|
||||
const memberId = getMemberId()
|
||||
|
||||
if (!memberId) {
|
||||
uni.showToast({ title: '用户未登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '验证中...' })
|
||||
|
||||
try {
|
||||
const res = await verifyPayPassword(memberId, pwd)
|
||||
uni.hideLoading()
|
||||
|
||||
if (res && (res.valid === true || res.data?.valid === true)) {
|
||||
step.value = 2
|
||||
password.value = ''
|
||||
firstPassword.value = ''
|
||||
nextTick(() => {
|
||||
verifyInputRef.value?.focusInput()
|
||||
})
|
||||
} else {
|
||||
errorMsg.value = '原密码错误,请重新输入'
|
||||
password.value = ''
|
||||
nextTick(() => {
|
||||
verifyInputRef.value?.focusInput()
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
errorMsg.value = '验证失败,请稍后重试'
|
||||
password.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPassword(pwd) {
|
||||
const memberId = getMemberId()
|
||||
|
||||
if (!memberId) {
|
||||
uni.showToast({ title: '用户未登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '设置中...' })
|
||||
|
||||
try {
|
||||
let res
|
||||
if (isReset.value) {
|
||||
res = await resetPayPassword(memberId, firstPassword.value, pwd)
|
||||
} else {
|
||||
res = await setPayPassword(memberId, pwd)
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (res && (res.code === 200 || res.code === 200 || res.message === '设置成功' || res.message === '重置成功')) {
|
||||
uni.showToast({ title: isReset.value ? '修改成功' : '设置成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '设置失败', icon: 'none' })
|
||||
password.value = ''
|
||||
step.value = 1
|
||||
firstPassword.value = ''
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '设置失败,请稍后重试', icon: 'none' })
|
||||
password.value = ''
|
||||
step.value = 1
|
||||
firstPassword.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goForgotPassword() {
|
||||
uni.showToast({ title: '请联系客服重置密码', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/pages/page-reset.css';
|
||||
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
||||
|
||||
.set-pay-password-page {
|
||||
min-height: 100vh;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.set-pay-password-page__body {
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.spp-intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.spp-intro__icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.spp-forgot {
|
||||
text-align: right;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.spp-forgot text {
|
||||
font-size: 13px;
|
||||
color: #1677FF;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
// const BASE_URL = 'http://localhost:8080/api'
|
||||
// const BASE_URL = 'http://192.168.5.15:8084/api'
|
||||
const BASE_URL = 'https://www.gymmanage.xyz/api'
|
||||
// const BASE_URL = 'https://www.gymmanage.xyz/api'
|
||||
const BASE_URL = 'http://192.168.101.5:8084/api'
|
||||
|
||||
export const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// const BASE_URL = '/api'
|
||||
// const BASE_URL = 'http://192.168.5.15:8084/api'
|
||||
const BASE_URL = 'https://www.gymmanage.xyz/api'
|
||||
// const BASE_URL = '/api'
|
||||
// const BASE_URL = 'https://www.gymmanage.xyz/api'
|
||||
const BASE_URL = 'http://192.168.101.5:8084/api'
|
||||
|
||||
// OSS 静态资源基础地址(图标、通用静态文件)
|
||||
export const OSS_BASE_URL = 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/'
|
||||
|
||||
Reference in New Issue
Block a user