移除前端中的会员卡和支付相关,新增微信一键登录

This commit is contained in:
2026-07-14 18:50:35 +08:00
parent 13b99428de
commit 6cc72ae0f7
26 changed files with 331 additions and 9159 deletions
+1 -2
View File
@@ -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">&#128274;</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