修正多处问题
This commit is contained in:
@@ -110,7 +110,7 @@ import { getMemberId } from '@/utils/request.js'
|
||||
const STQRC = ref(false)//是否扫码
|
||||
const isCheckIn = ref(false)
|
||||
const isHaveCard = ref(true)
|
||||
const webSoketURL = "ws://localhost:8084/webSocket/checkIn"
|
||||
const webSoketURL = "ws://192.168.5.15:8084/webSocket/checkIn"
|
||||
|
||||
const qrcode = ref("")
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ function handleTabActive(index) {
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
uni.hideLoading()
|
||||
// 优先显示缓存
|
||||
const hasCache = loadFromCache()
|
||||
if (!hasCache) {
|
||||
@@ -143,6 +144,7 @@ onLoad(() => {
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
uni.hideLoading()
|
||||
// 每次显示时尝试刷新数据(后台静默更新)
|
||||
if (!loading.value) {
|
||||
loadFromNetwork()
|
||||
|
||||
@@ -228,16 +228,36 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付密码弹窗 -->
|
||||
<PayPasswordModal
|
||||
:visible="showPayPwd"
|
||||
title="支付验证"
|
||||
:subtitle="course.courseName"
|
||||
:amount="course.storedValueAmount || 0"
|
||||
amount-label="课程金额"
|
||||
@confirm="onPayPasswordConfirm"
|
||||
@cancel="onPayPasswordCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getGroupCourseDetail, bookGroupCourse } from '@/api/groupCourse.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: '',
|
||||
@@ -373,48 +393,89 @@ const formatDuration = (startStr, endStr) => {
|
||||
}
|
||||
|
||||
// 预约处理
|
||||
const handleBooking = () => {
|
||||
const handleBooking = async () => {
|
||||
if (!canBook.value) return
|
||||
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认预约',
|
||||
content: `确定要预约「${course.value.courseName}」吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '预约中...' })
|
||||
bookGroupCourse({
|
||||
courseId: course.value.id,
|
||||
memberId: '1', // 模拟会员ID
|
||||
memberCardRecordId: '1' // 模拟会员卡记录ID
|
||||
}).then((result) => {
|
||||
content: `确定要预约「${course.value.courseName}」吗?${course.value.storedValueAmount > 0 ? '\n金额: ¥' + course.value.storedValueAmount : ''}`,
|
||||
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()
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '预约成功',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '预约失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}).catch((error) => {
|
||||
uni.hideLoading()
|
||||
console.error('[detail.vue] 预约失败:', error)
|
||||
uni.showToast({ title: '请先购买会员卡', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const result = await bookGroupCourse({
|
||||
courseId: Number(course.value.id),
|
||||
memberId: Number(memberId),
|
||||
memberCardRecordId: Number(memberCardRecordId),
|
||||
payPassword: payPassword
|
||||
})
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '预约失败',
|
||||
icon: 'none'
|
||||
title: '预约成功',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/memberInfo/myCourses' })
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '预约失败',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('[detail.vue] 预约失败:', error)
|
||||
const errMsg = error?.message || error?.data?.message || '预约失败'
|
||||
uni.showToast({
|
||||
title: errMsg,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 弹出支付密码输入框,返回 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 {
|
||||
@@ -439,15 +500,13 @@ const fetchCourseDetail = async (id) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 页面挂载时获取课程详情
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = currentPage.options || {}
|
||||
const courseId = options.id || '1'
|
||||
|
||||
console.log('[detail.vue] 页面挂载,课程ID:', courseId)
|
||||
fetchCourseDetail(courseId)
|
||||
// 页面加载时获取课程详情
|
||||
onLoad((options) => {
|
||||
const courseId = options.id
|
||||
console.log('[detail.vue] onLoad,课程ID:', courseId)
|
||||
if (courseId) {
|
||||
fetchCourseDetail(courseId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -963,7 +1022,10 @@ onMounted(() => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.booking-btn.disabled text {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
padding-bottom: calc(140rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
|
||||
@@ -32,6 +32,18 @@
|
||||
@change="onTimePeriodChange"
|
||||
ref="timePeriodRef"
|
||||
/>
|
||||
|
||||
<!-- 常态化团课筛选按钮 -->
|
||||
<view class="recurring-filter">
|
||||
<view
|
||||
class="recurring-btn"
|
||||
:class="{ 'recurring-btn--active': isRecurring }"
|
||||
@tap="toggleRecurring"
|
||||
>
|
||||
<text class="recurring-icon">{{ isRecurring ? '✓' : '' }}</text>
|
||||
<text class="recurring-text">常态化团课</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 团课列表 -->
|
||||
@@ -46,6 +58,7 @@
|
||||
:key="course.id"
|
||||
:course="course"
|
||||
:courseTypeLabels="getCourseLabels(course)"
|
||||
:booked="isCourseBooked(course.id)"
|
||||
@booking="handleBooking"
|
||||
@detail="goDetail"
|
||||
></GroupCourseCard>
|
||||
@@ -74,6 +87,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import GroupCourseCard from '@/components/groupCourse/CourseCard.vue'
|
||||
import SearchBar from '@/components/groupCourse/SearchBar.vue'
|
||||
@@ -82,7 +96,8 @@ import TimePeriodSelector from '@/components/groupCourse/TimePeriodSelector.vue'
|
||||
import TimeRangePicker from '@/components/groupCourse/TimeRangePicker.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import { useGroupCourseList } from '@/composables/useGroupCourseList.js'
|
||||
import { getTypeLabels, getGroupCourseTypes } from '@/api/groupCourse.js'
|
||||
import { getTypeLabels, getGroupCourseTypes, getMemberBookings } from '@/api/groupCourse.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
|
||||
// 组件引用
|
||||
const searchBarRef = ref(null)
|
||||
@@ -93,6 +108,9 @@ const timeRangePickerRef = ref(null)
|
||||
// 课程类型标签缓存
|
||||
const courseTypeLabelsCache = ref({})
|
||||
|
||||
// 已预约课程ID集合
|
||||
const bookedCourseIds = ref(new Set())
|
||||
|
||||
// 使用组合式函数
|
||||
const {
|
||||
// 状态
|
||||
@@ -106,6 +124,7 @@ const {
|
||||
sortIndex,
|
||||
timePeriodOptions,
|
||||
timePeriodIndex,
|
||||
isRecurring,
|
||||
showTimePicker,
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -126,13 +145,20 @@ const {
|
||||
onScrollToLower
|
||||
} = useGroupCourseList()
|
||||
|
||||
// 切换常态化团课筛选
|
||||
const toggleRecurring = () => {
|
||||
isRecurring.value = !isRecurring.value
|
||||
fetchCourseList()
|
||||
}
|
||||
|
||||
// 组件挂载时调用接口获取团课列表
|
||||
onMounted(async () => {
|
||||
console.log('[list.vue] 页面组件已挂载,开始获取团课列表')
|
||||
// 并行获取课程类型列表和课程列表
|
||||
// 并行获取课程类型列表、课程列表和已预约课程
|
||||
await Promise.all([
|
||||
fetchCourseTypes(),
|
||||
fetchCourseList()
|
||||
fetchCourseList(),
|
||||
fetchBookedCourses()
|
||||
])
|
||||
// 获取所有团课的类型标签
|
||||
await fetchAllCourseTypeLabels()
|
||||
@@ -144,6 +170,12 @@ onMounted(async () => {
|
||||
console.log(' - getAllSearchParams() 获取所有参数')
|
||||
})
|
||||
|
||||
// 页面显示时刷新已预约状态(从其他页面返回时)
|
||||
onShow(async () => {
|
||||
console.log('[list.vue] onShow 刷新已预约状态')
|
||||
await fetchBookedCourses()
|
||||
})
|
||||
|
||||
const fetchCourseTypes = async () => {
|
||||
try {
|
||||
const result = await getGroupCourseTypes()
|
||||
@@ -159,6 +191,33 @@ const fetchCourseTypes = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchBookedCourses = async () => {
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) {
|
||||
console.log('[list.vue] 未登录,跳过获取已预约课程')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getMemberBookings(memberId)
|
||||
const bookings = Array.isArray(res) ? res : (res?.data || res?.content || [])
|
||||
if (Array.isArray(bookings)) {
|
||||
const ids = new Set()
|
||||
bookings.forEach(b => {
|
||||
// 只统计状态为正常的预约(status='0'),排除已取消(status='1')
|
||||
if (b.courseId && String(b.status) === '0') ids.add(Number(b.courseId))
|
||||
})
|
||||
bookedCourseIds.value = ids
|
||||
console.log('[list.vue] 已预约课程ID:', [...ids])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[list.vue] 获取已预约课程失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const isCourseBooked = (courseId) => {
|
||||
return bookedCourseIds.value.has(Number(courseId))
|
||||
}
|
||||
|
||||
const fetchAllCourseTypeLabels = async () => {
|
||||
const types = [...new Set(filteredCourseList.value.map(c => c.courseType))]
|
||||
for (const type of types) {
|
||||
@@ -227,4 +286,34 @@ defineExpose({
|
||||
.load-more-text {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 常态化团课筛选按钮 */
|
||||
.recurring-filter {
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.recurring-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
background: #fff;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.recurring-btn--active {
|
||||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||
border-color: #66bb6a;
|
||||
color: #2e7d32;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.recurring-icon {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<!-- 固定白色块(滚动时显示) -->
|
||||
<view class="hand" :style="{height : handHeight + 50 + 'rpx',lineHeight: handHeight + 70 + 'rpx'}" v-show="isShow">活氧舱</view>
|
||||
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader title="活氧舱" subtitle="科学训练 · 遇见更好的自己" />
|
||||
|
||||
<!-- 滚动内容区域 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
refresher-default-style="none"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scroll="handleScroll"
|
||||
class="scroll-container"
|
||||
>
|
||||
@@ -18,8 +14,8 @@
|
||||
<template v-else>
|
||||
<BannerSwiper />
|
||||
<QuickEntry />
|
||||
<RecommendCourses />
|
||||
<TodayRecommend />
|
||||
<RecommendCourses ref="recommendCoursesRef" />
|
||||
<TodayRecommend ref="todayRecommendRef" />
|
||||
<view class="bottom-placeholder"></view>
|
||||
</template>
|
||||
</view>
|
||||
@@ -40,60 +36,19 @@ import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
||||
import TodayRecommend from '@/components/index/TodayRecommend.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import HomeSkeleton from '@/components/Skeleton/HomeSkeleton.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const isShow = ref(false)
|
||||
const handHeight = ref(0)
|
||||
const scrollDistance = ref(0)
|
||||
const isRefreshing = ref(false)
|
||||
const recommendCoursesRef = ref(null)
|
||||
const todayRecommendRef = ref(null)
|
||||
|
||||
// 获取可视窗口高度
|
||||
const windowHeight = ref(0)
|
||||
// 获取整个滚动内容的高度
|
||||
const scrollContentHeight = ref(0)
|
||||
// 滚动距离
|
||||
const scrollDistance = ref(0)
|
||||
|
||||
// 滚动监听
|
||||
const handleScroll = (e) => {
|
||||
const distance = e.detail.scrollTop
|
||||
scrollDistance.value = distance
|
||||
|
||||
console.log(`滚动距离: ${distance}`)
|
||||
|
||||
// 当滚动超过一定距离时显示白色块(200px = 400rpx)
|
||||
const SHOW_THRESHOLD = 50 // 滚动超过200px时显示白条
|
||||
isShow.value = distance > SHOW_THRESHOLD
|
||||
}
|
||||
|
||||
// 下拉刷新处理
|
||||
const onRefresh = async () => {
|
||||
console.log('开始下拉刷新')
|
||||
isRefreshing.value = true
|
||||
|
||||
try {
|
||||
await refreshData()
|
||||
isRefreshing.value = false
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('刷新失败', error)
|
||||
isRefreshing.value = false
|
||||
uni.showToast({
|
||||
title: '刷新失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log('数据已刷新')
|
||||
resolve()
|
||||
}, 1500)
|
||||
})
|
||||
scrollDistance.value = e.detail.scrollTop
|
||||
console.log(`滚动距离: ${scrollDistance.value}`)
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
@@ -114,41 +69,6 @@ onMounted(() => {
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
}, 1500)
|
||||
|
||||
// 获取系统信息
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
windowHeight.value = res.windowHeight
|
||||
console.log('可视窗口高度:', windowHeight.value)
|
||||
console.log('平台:', res.platform)
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序使用胶囊按钮高度(保持原有逻辑)
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
const navTotalHeight = menuButtonInfo.top + menuButtonInfo.height
|
||||
handHeight.value = navTotalHeight * 2.5
|
||||
console.log('微信小程序胶囊按钮高度:', handHeight.value)
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// H5和安卓App只显示状态栏高度
|
||||
const statusBarHeight = res.statusBarHeight || 0
|
||||
handHeight.value = statusBarHeight * 2 // 转换为rpx(1px = 2rpx)
|
||||
console.log('非微信小程序状态栏高度:', handHeight.value, 'rpx')
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
// 延迟获取滚动内容高度(确保DOM已渲染)
|
||||
setTimeout(() => {
|
||||
const query = uni.createSelectorQuery().in(this)
|
||||
query.select('.home-page').boundingClientRect(data => {
|
||||
if (data) {
|
||||
scrollContentHeight.value = data.height
|
||||
console.log('内容总高度:', scrollContentHeight.value)
|
||||
}
|
||||
}).exec()
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -167,18 +87,6 @@ onMounted(() => {
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
/* 固定白色块 */
|
||||
.hand {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
/* 固定 TabBar */
|
||||
.tabbar-fixed {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="booking-page">
|
||||
<MemberInfoSubNav title="我的预约" @back="goBack" />
|
||||
<PageHeader title="我的预约" subtitle="" :show-back="true" />
|
||||
|
||||
<view class="booking-page__tabs">
|
||||
<view
|
||||
@@ -11,7 +11,7 @@
|
||||
:class="{ 'booking-page__tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="setActiveTab(tab.key)"
|
||||
@tap="activeTab = tab.key"
|
||||
>
|
||||
<text
|
||||
class="booking-page__tab-text"
|
||||
@@ -26,31 +26,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bt-page__action-bar bt-page__action-bar--end">
|
||||
<text
|
||||
class="bt-page__action-link bt-page__action-link--primary"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goCourseList"
|
||||
>
|
||||
预约课程
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="booking-page__body">
|
||||
<view
|
||||
v-if="activeTab === 'ongoing' && upcomingAlert"
|
||||
class="booking-page__alert"
|
||||
>
|
||||
<image
|
||||
class="booking-page__alert-icon"
|
||||
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="booking-page__alert-text">{{ upcomingAlert }}</text>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="!displayedBookings.length && !loading" class="booking-page__empty">
|
||||
<text class="booking-page__empty-text">
|
||||
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
<view v-else
|
||||
v-for="item in displayedBookings"
|
||||
:key="item.id"
|
||||
class="bk-card"
|
||||
@@ -59,7 +43,7 @@
|
||||
>
|
||||
<image
|
||||
class="bk-card__banner"
|
||||
:src="item.banner"
|
||||
:src="item.coverImage || item.banner || 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/default-course.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="bk-card__content">
|
||||
@@ -97,7 +81,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bk-card__footer">
|
||||
<view class="bk-card__footer" v-if="item.footerText">
|
||||
<text class="bk-card__footer-info">{{ item.footerText }}</text>
|
||||
<view class="bk-card__actions">
|
||||
<view
|
||||
@@ -122,33 +106,25 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!displayedBookings.length" class="booking-page__empty">
|
||||
<text class="booking-page__empty-text">
|
||||
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
|
||||
import {
|
||||
cancelOngoingBooking,
|
||||
formatUpcomingAlert
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import { canCancelBooking, canSigninCourse } from '@/common/memberInfo/bookingStore.js'
|
||||
import { signinGroupCourse, cancelBooking as cancelBookingApi } from '@/api/main.js'
|
||||
import { getMemberBookings, signinGroupCourse, cancelBooking as cancelBookingApi } from '@/api/main.js'
|
||||
|
||||
const tabs = ref([])
|
||||
const tabs = ref([
|
||||
{ key: 'ongoing', label: '进行中' },
|
||||
{ key: 'history', label: '历史记录' }
|
||||
])
|
||||
const activeTab = ref('ongoing')
|
||||
const ongoingBookings = ref([])
|
||||
const historyBookings = ref([])
|
||||
const activeTab = ref('ongoing')
|
||||
const loading = ref(false)
|
||||
|
||||
const displayedBookings = computed(() => {
|
||||
return activeTab.value === 'ongoing'
|
||||
@@ -156,30 +132,97 @@ const displayedBookings = computed(() => {
|
||||
: historyBookings.value
|
||||
})
|
||||
|
||||
const upcomingAlert = computed(() => {
|
||||
return formatUpcomingAlert(ongoingBookings.value[0])
|
||||
})
|
||||
|
||||
function refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
ongoingBookings.value = store.ongoingBookings.map((item) => ({
|
||||
...item,
|
||||
canCancel: canCancelBooking(item),
|
||||
canSignin: canSigninCourse(item)
|
||||
}))
|
||||
historyBookings.value = store.historyBookings.map((item) => ({ ...item }))
|
||||
async function fetchBookings() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
console.log('[booking] 开始获取预约记录')
|
||||
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) {
|
||||
console.warn('[booking] 未登录,无法获取预约记录')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMemberBookings(memberId)
|
||||
console.log('[booking] getMemberBookings 返回:', JSON.stringify(res, null, 2))
|
||||
const bookings = res?.data || res?.content || []
|
||||
|
||||
if (Array.isArray(bookings) && bookings.length > 0) {
|
||||
const mapped = bookings.map(mapBooking)
|
||||
ongoingBookings.value = mapped.filter((b) => b.status === 'ongoing')
|
||||
historyBookings.value = mapped.filter((b) => b.status !== 'ongoing')
|
||||
} else {
|
||||
ongoingBookings.value = []
|
||||
historyBookings.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[booking] 获取预约记录失败:', e)
|
||||
ongoingBookings.value = []
|
||||
historyBookings.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
backToMemberCenter()
|
||||
function mapBooking(b) {
|
||||
const status = getBookingStatus(b)
|
||||
return {
|
||||
id: b.id,
|
||||
courseId: b.courseId,
|
||||
title: b.courseName || b.courseTitle || '预约课程',
|
||||
status: status,
|
||||
statusLabel: getBookingStatusLabel(b),
|
||||
schedule: formatBookingTime(b.courseStartTime || b.startTime || b.bookingTime),
|
||||
banner: b.coverImage || '',
|
||||
coach: b.coachName || b.coach || '教练',
|
||||
footerText: status === 'ongoing' ? '请准时参加课程' : '',
|
||||
canSignin: status === 'ongoing',
|
||||
canCancel: status === 'ongoing'
|
||||
}
|
||||
}
|
||||
|
||||
function goCourseList() {
|
||||
navigateToPage(PAGE.COURSE_LIST)
|
||||
function getBookingStatus(b) {
|
||||
if (b.status) {
|
||||
const s = String(b.status).toLowerCase()
|
||||
if (s === 'cancelled' || s === 'canceled') return 'cancelled'
|
||||
if (s === 'completed' || s === 'finished') return 'completed'
|
||||
}
|
||||
const startTime = b.courseStartTime || b.startTime
|
||||
if (startTime) {
|
||||
const now = new Date()
|
||||
const start = new Date(startTime)
|
||||
if (now > start) return 'completed'
|
||||
}
|
||||
return 'ongoing'
|
||||
}
|
||||
|
||||
function setActiveTab(tab) {
|
||||
activeTab.value = tab
|
||||
function getBookingStatusLabel(b) {
|
||||
if (b.status) {
|
||||
const s = String(b.status).toLowerCase()
|
||||
if (s === 'cancelled' || s === 'canceled') return '已取消'
|
||||
if (s === 'completed' || s === 'finished') return '已完成'
|
||||
if (s === 'ongoing' || s === 'confirmed') return '进行中'
|
||||
}
|
||||
const status = getBookingStatus(b)
|
||||
if (status === 'cancelled') return '已取消'
|
||||
if (status === 'completed') return '已完成'
|
||||
return '进行中'
|
||||
}
|
||||
|
||||
function formatBookingTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
try {
|
||||
const d = new Date(timeStr)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const hour = d.getHours().toString().padStart(2, '0')
|
||||
const minute = d.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}月${day}日 ${hour}:${minute}`
|
||||
} catch {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
// 签到团课
|
||||
@@ -203,8 +246,7 @@ async function signinCourse(item) {
|
||||
|
||||
if (result.code === 200 || result.success) {
|
||||
uni.showToast({ title: '签到成功', icon: 'success' })
|
||||
// 更新本地状态
|
||||
refreshFromStore()
|
||||
fetchBookings()
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '签到失败', icon: 'none' })
|
||||
}
|
||||
@@ -219,10 +261,6 @@ async function signinCourse(item) {
|
||||
|
||||
// 取消预约
|
||||
async function cancelBooking(item) {
|
||||
if (!canCancelBooking(item)) {
|
||||
uni.showToast({ title: '距开课不足2小时,无法取消', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '取消预约',
|
||||
content: `确定要取消「${item.title}」吗?`,
|
||||
@@ -231,18 +269,12 @@ async function cancelBooking(item) {
|
||||
|
||||
uni.showLoading({ title: '取消中...', mask: true })
|
||||
try {
|
||||
// 调用后端API取消预约
|
||||
const result = await cancelBookingApi(Number(item.id))
|
||||
uni.hideLoading()
|
||||
|
||||
if (result.code === 200 || result.success) {
|
||||
// 更新本地状态
|
||||
const store = loadMemberStore()
|
||||
const localResult = cancelOngoingBooking(store, item.id)
|
||||
if (localResult.ok) {
|
||||
refreshFromStore()
|
||||
}
|
||||
uni.showToast({ title: '已取消', icon: 'success' })
|
||||
fetchBookings()
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '取消失败', icon: 'none' })
|
||||
}
|
||||
@@ -255,7 +287,9 @@ async function cancelBooking(item) {
|
||||
})
|
||||
}
|
||||
|
||||
refreshFromStore()
|
||||
onMounted(() => {
|
||||
fetchBookings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -263,7 +297,6 @@ refreshFromStore()
|
||||
@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';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/booking-page.css';
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="签到记录" @back="goBack" />
|
||||
<PageHeader title="签到记录" subtitle="" :show-back="true" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
@@ -17,7 +17,7 @@
|
||||
</view>
|
||||
<view class="bt-page__body">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">共 {{ filteredList.length }} 条记录</text>
|
||||
<text class="bt-card__title" v-if="filteredList.length > 0">共 {{ filteredList.length }} 条记录</text>
|
||||
<view
|
||||
v-for="item in filteredList"
|
||||
:key="item.id"
|
||||
@@ -37,7 +37,7 @@
|
||||
<text class="mi-mod-checkin-row__tag-text">{{ item.tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!filteredList.length" class="bt-empty">
|
||||
<view v-if="filteredList.length === 0 && !loading" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无签到记录</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -47,44 +47,89 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { getCheckInHistory } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { backToMemberCenter } from '@/common/constants/routes.js'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import { getCheckInRecords } from '@/api/main.js'
|
||||
|
||||
const tabs = ref([
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'signin', label: '签到' },
|
||||
{ key: 'trial', label: '体验' },
|
||||
{ key: 'event', label: '活动' }
|
||||
{ key: 'signin', label: '签到' }
|
||||
])
|
||||
const activeFilter = ref('all')
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (activeFilter.value === 'all') return list.value
|
||||
// "签到" tab: 筛选普通签到记录(非团课/私教类)
|
||||
if (activeFilter.value === 'signin') {
|
||||
return list.value.filter((i) => i.tagTheme === 'default')
|
||||
}
|
||||
return list.value.filter((i) => i.tagTheme === activeFilter.value)
|
||||
})
|
||||
|
||||
function refresh() {
|
||||
const store = loadMemberStore()
|
||||
list.value = getCheckInHistory(store, 'all')
|
||||
async function fetchCheckInHistory() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
console.log('[checkInHistory] 开始获取签到记录')
|
||||
try {
|
||||
const res = await getCheckInRecords({ page: 0, size: 50 })
|
||||
console.log('[checkInHistory] getCheckInRecords 返回:', JSON.stringify(res, null, 2))
|
||||
const records = res?.data?.content || res?.content || []
|
||||
if (Array.isArray(records) && records.length > 0) {
|
||||
list.value = records.map((record) => ({
|
||||
id: record.id,
|
||||
title: record.courseName || record.name || '签到',
|
||||
time: formatCheckInTime(record.checkInTime || record.createTime),
|
||||
location: record.location || record.address || '',
|
||||
tag: record.courseType || record.type || '签到',
|
||||
tagTheme: getTagTheme(record.courseType || record.type)
|
||||
}))
|
||||
} else {
|
||||
list.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[checkInHistory] 获取签到记录失败:', e)
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
backToMemberCenter()
|
||||
function formatCheckInTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
try {
|
||||
const d = new Date(timeStr)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const hour = d.getHours().toString().padStart(2, '0')
|
||||
const minute = d.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}月${day}日 ${hour}:${minute}`
|
||||
} catch {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
function getTagTheme(type) {
|
||||
if (!type) return 'default'
|
||||
const t = type.toLowerCase()
|
||||
if (t.includes('团课') || t.includes('group')) return 'primary'
|
||||
if (t.includes('私教') || t.includes('personal')) return 'accent'
|
||||
if (t.includes('线上') || t.includes('online')) return 'info'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function showDetail(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.time}\n地点:${item.location}\n类型:${item.tag}`,
|
||||
content: `${item.time}\n${item.location ? '地点:' + item.location + '\n' : ''}类型:${item.tag}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
refresh()
|
||||
onMounted(() => {
|
||||
fetchCheckInHistory()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -92,7 +137,6 @@ refresh()
|
||||
@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';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="member-card-page">
|
||||
<MemberInfoSubNav title="我的会员卡" @back="goBack" />
|
||||
<view class="member-card-page__body">
|
||||
<view class="member-card-page">
|
||||
<!-- 固定页面头部 -->
|
||||
<view class="member-card-page-header">
|
||||
<PageHeader title="我的会员卡" subtitle="" :show-back="true" />
|
||||
</view>
|
||||
|
||||
<!-- 滚动内容区域 -->
|
||||
<view class="member-card-scroll-wrap">
|
||||
<scroll-view scroll-y class="member-card-scroll">
|
||||
<view class="member-card-page__body">
|
||||
<!-- 卡类型切换 -->
|
||||
<view class="mc-type-tabs">
|
||||
<view
|
||||
@@ -10,7 +16,6 @@
|
||||
:class="{ 'mc-type-tab--active': currentCardType === 'stored' }"
|
||||
@tap="switchCardType('stored')"
|
||||
>
|
||||
<image class="mc-type-tab__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/wallet.png" mode="aspectFit" />
|
||||
<text>储值卡</text>
|
||||
</view>
|
||||
<view
|
||||
@@ -18,7 +23,6 @@
|
||||
:class="{ 'mc-type-tab--active': currentCardType === 'other' }"
|
||||
@tap="switchCardType('other')"
|
||||
>
|
||||
<image class="mc-type-tab__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png" mode="aspectFit" />
|
||||
<text>非储值卡</text>
|
||||
<text class="mc-type-tab__count" v-if="otherCards.length > 0">{{ otherCards.length }}</text>
|
||||
</view>
|
||||
@@ -31,11 +35,7 @@
|
||||
<view class="mc-stored-hero__inner">
|
||||
<view class="mc-stored-hero__header">
|
||||
<view class="mc-stored-hero__title-row">
|
||||
<image
|
||||
class="mc-stored-hero__crown"
|
||||
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<uni-icons type="star-filled" size="40rpx" color="#FFFFFF" />
|
||||
<text class="mc-stored-hero__name">{{ displayStoredCard.name }}</text>
|
||||
</view>
|
||||
<view class="mc-stored-hero__badge">
|
||||
@@ -50,19 +50,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="mc-stored-hero__footer">
|
||||
<view
|
||||
class="mc-stored-hero__recharge"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="goRecharge"
|
||||
>
|
||||
<image
|
||||
class="mc-stored-hero__recharge-icon"
|
||||
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/plus.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>充值</text>
|
||||
</view>
|
||||
<view class="mc-stored-hero__recharge" hover-class="mi-tap-btn--hover" :hover-stay-time="150" @tap="goRecharge">充值</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -348,18 +336,8 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="mc-rules">
|
||||
<text class="mc-rules__title">使用规则</text>
|
||||
<view
|
||||
v-for="(rule, index) in rules"
|
||||
:key="index"
|
||||
class="mc-rules__item"
|
||||
>
|
||||
<view class="mc-rules__bullet"></view>
|
||||
<text class="mc-rules__text">{{ rule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -367,14 +345,14 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { onShow, onLoad } from '@dcloudio/uni-app'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import {
|
||||
loadMemberStore,
|
||||
computeRemainingDays,
|
||||
renewMemberCard,
|
||||
saveMemberStore
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import { backToMemberCenter, PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { getMyMemberCards, getMyMemberCardsWithStatus, getMemberCardTransactions, getMemberCardTransactionsByRecordId, checkPayPasswordSet, createPayment, getStoredCardInfo, getStoredCardRechargeRecords } from '@/api/main.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
|
||||
@@ -394,7 +372,6 @@ const recordTabs = ref([
|
||||
{ key: 'CHECK_IN', label: '到店健身' }
|
||||
])
|
||||
const records = ref([])
|
||||
const rules = ref([])
|
||||
const activeFilter = ref('all')
|
||||
const activeSection = ref('records')
|
||||
const transactions = ref([])
|
||||
@@ -866,10 +843,6 @@ async function refreshFromServer() {
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
backToMemberCenter()
|
||||
}
|
||||
|
||||
function switchFilter(filter) {
|
||||
activeFilter.value = filter
|
||||
}
|
||||
@@ -1055,21 +1028,50 @@ onShow(() => {
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/common/style/base.css';
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.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';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/member-card-page.css';
|
||||
|
||||
// 卡类型切换样式
|
||||
// ================== 页面布局 ==================
|
||||
.member-card-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.member-card-page-header {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.member-card-scroll-wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.member-card-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.member-card-page__body {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
// ================== 卡类型切换 ==================
|
||||
.mc-type-tabs {
|
||||
display: flex;
|
||||
margin: 0 16px 16px;
|
||||
margin-bottom: 24rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
gap: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.mc-type-tab {
|
||||
@@ -1077,51 +1079,43 @@ onShow(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 12px 0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
gap: 8rpx;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #718096;
|
||||
background: #F7FAFC;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--active {
|
||||
background: #1677FF;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.mc-type-tab__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
.mc-type-tab--active {
|
||||
background: linear-gradient(135deg, #1677FF, #4096FF);
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mc-type-tab__count {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.mc-type-tab--active .mc-type-tab__count {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
// 储值卡区域样式
|
||||
// ================== 储值卡 ==================
|
||||
.mc-stored-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mc-stored-hero {
|
||||
margin: 0 16px;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.mc-stored-hero__inner {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.mc-stored-hero__header {
|
||||
@@ -1137,11 +1131,6 @@ onShow(() => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mc-stored-hero__crown {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.mc-stored-hero__name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
@@ -1182,7 +1171,7 @@ onShow(() => {
|
||||
.mc-stored-hero__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mc-stored-hero__validity {
|
||||
@@ -1201,20 +1190,14 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.mc-stored-hero__recharge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
padding: 16rpx 36rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #FFFFFF;
|
||||
color: #667eea;
|
||||
font-size: 14px;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mc-stored-hero__recharge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #667eea;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// 无储值卡提示
|
||||
@@ -1223,10 +1206,10 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 16px;
|
||||
margin: 0 16px;
|
||||
padding: 64rpx 32rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.mc-stored-empty__icon {
|
||||
@@ -1305,11 +1288,12 @@ onShow(() => {
|
||||
// 筛选条件样式
|
||||
.mc-filter-tabs {
|
||||
display: flex;
|
||||
margin: 0 16px 16px;
|
||||
margin-bottom: 24rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
gap: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.mc-filter-tab {
|
||||
@@ -1345,8 +1329,8 @@ onShow(() => {
|
||||
|
||||
// 卡片轮播样式
|
||||
.mc-swiper-cards {
|
||||
margin: 0 16px 20px;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.mc-swiper {
|
||||
@@ -1584,52 +1568,56 @@ onShow(() => {
|
||||
|
||||
.mc-section-tabs {
|
||||
display: flex;
|
||||
margin: 0 16px;
|
||||
margin-bottom: 24rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
gap: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.mc-section-tab {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
padding: 20rpx 0;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #718096;
|
||||
background: #F7FAFC;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #1677FF;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mc-section-tab--active {
|
||||
background: linear-gradient(135deg, #1677FF, #4096FF);
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mc-transactions {
|
||||
margin: 0 16px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.mc-transactions__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 16px;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mc-transactions__title {
|
||||
font-size: 17px;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1A202C;
|
||||
color: #1E2A3A;
|
||||
}
|
||||
|
||||
.mc-transactions__refresh {
|
||||
font-size: 13px;
|
||||
font-size: 24rpx;
|
||||
color: #1677FF;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.mc-transactions__scroll {
|
||||
@@ -1642,91 +1630,91 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.mc-transactions__divider {
|
||||
height: 1px;
|
||||
height: 1rpx;
|
||||
background: #F0F0F0;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.mc-transactions__item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
padding: 24rpx 0;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.mc-transactions__icon-wrap {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--purchase {
|
||||
background: #F0FFF4;
|
||||
}
|
||||
&--deduct {
|
||||
background: #FFF5F5;
|
||||
}
|
||||
&--renew {
|
||||
background: #EBF8FF;
|
||||
}
|
||||
&--refund {
|
||||
background: #FEF3E2;
|
||||
}
|
||||
&--expire {
|
||||
background: #F7FAFC;
|
||||
}
|
||||
&--default {
|
||||
background: #F7FAFC;
|
||||
}
|
||||
.mc-transactions__icon-wrap--purchase {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
.mc-transactions__icon-wrap--deduct {
|
||||
background: rgba(242, 153, 74, 0.1);
|
||||
}
|
||||
.mc-transactions__icon-wrap--renew {
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
.mc-transactions__icon-wrap--refund {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
.mc-transactions__icon-wrap--expire {
|
||||
background: rgba(158, 158, 158, 0.1);
|
||||
}
|
||||
.mc-transactions__icon-wrap--default {
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
.mc-transactions__icon-text {
|
||||
font-size: 24px;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.mc-transactions__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mc-transactions__item-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1A202C;
|
||||
font-size: 28rpx;
|
||||
color: #1E2A3A;
|
||||
display: block;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.mc-transactions__item-time {
|
||||
font-size: 12px;
|
||||
color: #A0AEC0;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.mc-transactions__value {
|
||||
font-size: 16px;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--positive {
|
||||
color: #059669;
|
||||
}
|
||||
&--negative {
|
||||
color: #DC2626;
|
||||
}
|
||||
.mc-transactions__value--positive {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.mc-transactions__value--negative {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.mc-transactions__empty {
|
||||
padding: 64rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.mc-transactions__empty-text {
|
||||
font-size: 14px;
|
||||
color: #8A99B4;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.mc-transactions__loading {
|
||||
@@ -1736,27 +1724,142 @@ onShow(() => {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.mc-records {
|
||||
background: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.mc-records__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mc-records__title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1E2A3A;
|
||||
}
|
||||
|
||||
.mc-records__tabs {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.mc-records__tab {
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #718096;
|
||||
background: #F7FAFC;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mc-records__tab--active {
|
||||
background: #1677FF;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.mc-records__divider {
|
||||
height: 1rpx;
|
||||
background: #F0F0F0;
|
||||
}
|
||||
|
||||
.mc-records__scroll {
|
||||
height: 400rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mc-records__value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
.mc-records__item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
&--positive {
|
||||
color: #059669;
|
||||
}
|
||||
&--negative {
|
||||
color: #DC2626;
|
||||
}
|
||||
&--pending {
|
||||
color: #F59E0B;
|
||||
}
|
||||
.mc-records__icon-wrap {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-records__icon-wrap--orange {
|
||||
background: rgba(242, 153, 74, 0.1);
|
||||
}
|
||||
|
||||
.mc-records__icon-wrap--green {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.mc-records__icon-wrap--purple {
|
||||
background: rgba(156, 39, 176, 0.1);
|
||||
}
|
||||
|
||||
.mc-records__icon-wrap--default {
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
.mc-records__icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.mc-records__icon-text {
|
||||
font-size: 24px;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.mc-records__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mc-records__item-title {
|
||||
font-size: 28rpx;
|
||||
color: #1E2A3A;
|
||||
display: block;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.mc-records__item-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.mc-records__value {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-records__value--positive {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.mc-records__value--negative {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.mc-records__value--pending {
|
||||
color: #F2994A;
|
||||
}
|
||||
|
||||
.mc-records__empty {
|
||||
padding: 64rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mc-records__empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,85 +3,43 @@
|
||||
<view class="member-page">
|
||||
<!-- Header: 用户信息区 -->
|
||||
<view class="profile-header">
|
||||
<view class="profile-header__toolbar" :style="toolbarStyle">
|
||||
<view class="profile-header__nav">
|
||||
<view class="profile-header__nav-left">
|
||||
<image
|
||||
class="profile-header__icon-bell"
|
||||
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/bell.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image
|
||||
class="profile-header__icon-settings"
|
||||
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/settings.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="profile-header__title">个人中心</text>
|
||||
<view class="profile-header__nav-right" :style="navRightStyle"></view>
|
||||
</view>
|
||||
<!-- 使用公共页面头部,背景色 #F5F7FA -->
|
||||
<view class="member-center-page-header">
|
||||
<PageHeader title="个人中心" subtitle="" />
|
||||
</view>
|
||||
<view class="profile-header__toolbar-spacer" :style="toolbarSpacerStyle"></view>
|
||||
<view class="profile-header__hero">
|
||||
<view class="profile-header__inner">
|
||||
<view class="profile-header__user" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goUserInfo">
|
||||
<view class="profile-header__user-inner">
|
||||
<view class="profile-header__avatar-wrap">
|
||||
<view class="profile-header__avatar-ring">
|
||||
<image
|
||||
class="profile-header__avatar"
|
||||
:key="userInfo.avatar"
|
||||
:src="displayAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="profile-header__avatar-badge">
|
||||
<image
|
||||
class="profile-header__avatar-badge-icon"
|
||||
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/camera.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-header__user-meta">
|
||||
<view class="profile-header__user-meta-inner">
|
||||
<text class="profile-header__name">{{ userInfo.name }}</text>
|
||||
<text class="profile-header__phone">{{ userInfo.phone }}</text>
|
||||
<view class="profile-header__badge">
|
||||
<image
|
||||
class="profile-header__badge-icon"
|
||||
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown0.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="profile-header__level">{{ userInfo.memberLevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-card" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goUserInfo">
|
||||
<view class="profile-card__main">
|
||||
<view class="profile-avatar">
|
||||
<image
|
||||
class="profile-avatar__img"
|
||||
:key="userInfo.avatar"
|
||||
:src="displayAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="profile-header__stats">
|
||||
<view class="profile-header__stats-inner">
|
||||
<view class="profile-header__stat">
|
||||
<view class="profile-header__stat-inner">
|
||||
<text class="profile-header__stat-value">{{ stats.checkInCount }}</text>
|
||||
<text class="profile-header__stat-label">累计签到</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-header__stat-divider"></view>
|
||||
<view class="profile-header__stat">
|
||||
<view class="profile-header__stat-inner">
|
||||
<text class="profile-header__stat-value">{{ stats.trainingHours }}</text>
|
||||
<text class="profile-header__stat-label">训练时长</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-header__stat-divider"></view>
|
||||
<view class="profile-header__stat">
|
||||
<view class="profile-header__stat-inner">
|
||||
<text class="profile-header__stat-value">{{ stats.pointsBalance }}</text>
|
||||
<text class="profile-header__stat-label">累计积分</text>
|
||||
</view>
|
||||
<view class="profile-info">
|
||||
<view class="profile-info__row">
|
||||
<text class="profile-info__name">{{ userInfo.name }}</text>
|
||||
<view class="profile-info__badge">
|
||||
<image class="profile-info__badge-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/crown0.png" mode="aspectFit" />
|
||||
<text class="profile-info__level">{{ userInfo.memberLevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="profile-info__phone">{{ userInfo.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-card__stats">
|
||||
<view class="profile-stat">
|
||||
<text class="profile-stat__value">{{ stats.checkInCount ?? 0 }}</text>
|
||||
<text class="profile-stat__label">累计签到</text>
|
||||
</view>
|
||||
<view class="profile-stat">
|
||||
<text class="profile-stat__value">{{ stats.courseCount ?? 0 }}</text>
|
||||
<text class="profile-stat__label">报课次数</text>
|
||||
</view>
|
||||
<view class="profile-stat">
|
||||
<text class="profile-stat__value">{{ stats.pointsBalance ?? 0 }}</text>
|
||||
<text class="profile-stat__label">累计积分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -233,88 +191,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预约课程区域 -->
|
||||
<view class="booking-section">
|
||||
<view class="booking-section__inner">
|
||||
<view class="booking-section__header">
|
||||
<view class="booking-section__header-inner">
|
||||
<text class="booking-section__title">我的预约</text>
|
||||
<view class="booking-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goBooking">
|
||||
<text class="booking-section__view-all">预约记录</text>
|
||||
<image class="booking-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright4.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="item in bookingPreview" :key="item.id" class="booking-section__item" hover-class="mi-tap-row--hover" :hover-stay-time="150" @tap="goBooking">
|
||||
<view class="booking-section__item-inner">
|
||||
<view class="booking-section__date">
|
||||
<view class="booking-section__date-inner">
|
||||
<text class="booking-section__num">{{ item.dateDay }}</text>
|
||||
<text class="booking-section__date-sub">{{ item.dateMonth }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="booking-section__content">
|
||||
<view class="booking-section__content-inner">
|
||||
<text class="booking-section__desc">{{ item.desc }}</text>
|
||||
<view class="booking-section__meta">
|
||||
<view class="booking-section__meta-inner">
|
||||
<image class="booking-section__icon-coach" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png" mode="aspectFit" />
|
||||
<text class="booking-section__coach">教练:{{ item.coach }}</text>
|
||||
<image class="booking-section__icon-location" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin1.png" mode="aspectFit" />
|
||||
<text class="booking-section__text">{{ item.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="booking-section__status-wrap">
|
||||
<view class="booking-section__status-badge" :class="'booking-section__status-badge--' + item.status">
|
||||
<text class="booking-section__status-text" :class="{ 'booking-section__status-text--pending': item.status === 'pending' }">{{ item.statusLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!bookingPreview.length" class="booking-section__empty">
|
||||
<text class="booking-section__empty-text">暂无进行中的预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签到记录区域 -->
|
||||
<view class="checkin-section">
|
||||
<view class="checkin-section__inner">
|
||||
<view class="checkin-section__header">
|
||||
<view class="checkin-section__header-inner">
|
||||
<text class="checkin-section__title">签到记录</text>
|
||||
<view class="checkin-section__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goCheckInHistory">
|
||||
<text class="checkin-section__view-all">查看全部</text>
|
||||
<image class="checkin-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright2.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="checkin-section__list">
|
||||
<view class="checkin-section__list-inner">
|
||||
<view v-for="(item, index) in checkIns" :key="item.id" class="checkin-section__row">
|
||||
<view v-if="index > 0" class="checkin-section__divider"></view>
|
||||
<view class="checkin-section__item" hover-class="mi-tap-row--hover" :hover-stay-time="150" @tap="onCheckInTap(item)">
|
||||
<view class="checkin-section__item-inner">
|
||||
<view class="checkin-section__dot" :class="'checkin-section__dot--' + item.tagTheme"></view>
|
||||
<view class="checkin-section__content">
|
||||
<view class="checkin-section__content-inner">
|
||||
<text class="checkin-section__desc">{{ item.title }}</text>
|
||||
<text class="checkin-section__text">{{ item.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="checkin-section__tag-badge" :class="'checkin-section__tag-badge--' + item.tagTheme">
|
||||
<text class="checkin-section__tag-text" :class="'checkin-section__tag-text--' + item.tagTheme">{{ item.tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 体测报告区域 -->
|
||||
<view class="body-report-section">
|
||||
<view class="body-report-section__inner">
|
||||
@@ -514,27 +390,22 @@
|
||||
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 } from '@/common/memberInfo/store.js'
|
||||
import { getCenterPageData, renewMemberCard, computeRemainingDays, saveMemberStore, loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import { getMyMemberCards } from '@/api/main.js'
|
||||
import { getMyMemberCards, 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'
|
||||
|
||||
// 页面数据
|
||||
const userInfo = ref({})
|
||||
const stats = ref({})
|
||||
const cardInfo = ref({})
|
||||
const expiringCards = ref([])
|
||||
const bookingPreview = ref([])
|
||||
const checkIns = ref([])
|
||||
const bodyReport = ref({})
|
||||
const couponPoints = ref({})
|
||||
const referral = ref({})
|
||||
|
||||
// 导航栏样式
|
||||
const toolbarStyle = ref({})
|
||||
const toolbarSpacerStyle = ref({})
|
||||
const navRightStyle = ref({})
|
||||
|
||||
// 快捷操作配置
|
||||
const quickActionsRow1 = ref([
|
||||
{ key: 'booking', label: '预约课程', textClass: 'quick-actions__title', icon: '' },
|
||||
@@ -592,33 +463,6 @@ const displayAvatar = computed(() => {
|
||||
})
|
||||
|
||||
// 方法
|
||||
function syncNavSafeArea() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync()
|
||||
const statusBarHeight = sys.statusBarHeight || 0
|
||||
const navHeight = 44
|
||||
const menu = uni.getMenuButtonBoundingClientRect?.()
|
||||
|
||||
toolbarStyle.value = {
|
||||
paddingTop: `${statusBarHeight}px`
|
||||
}
|
||||
|
||||
toolbarSpacerStyle.value = {
|
||||
height: `${statusBarHeight + navHeight}px`
|
||||
}
|
||||
|
||||
if (menu && menu.width) {
|
||||
const capsuleGap = sys.windowWidth - menu.left + 8
|
||||
navRightStyle.value = {
|
||||
width: `${capsuleGap}px`,
|
||||
minWidth: `${capsuleGap}px`
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
toolbarSpacerStyle.value = { height: '44px' }
|
||||
}
|
||||
}
|
||||
|
||||
function refreshFromStore() {
|
||||
const store = loadMemberStore()
|
||||
const pageData = getCenterPageData(store)
|
||||
@@ -626,13 +470,94 @@ function refreshFromStore() {
|
||||
stats.value = pageData.stats
|
||||
cardInfo.value = pageData.cardInfo
|
||||
expiringCards.value = pageData.expiringCards || []
|
||||
bookingPreview.value = pageData.bookingPreview
|
||||
checkIns.value = pageData.checkIns
|
||||
bodyReport.value = pageData.bodyReport
|
||||
couponPoints.value = pageData.couponPoints
|
||||
referral.value = pageData.referral
|
||||
}
|
||||
|
||||
async function refreshStatsFromServer() {
|
||||
const store = loadMemberStore()
|
||||
let changed = false
|
||||
|
||||
// 获取签到统计(累计签到 = 团课签到次数)
|
||||
try {
|
||||
const statsRes = await getCheckInStats()
|
||||
console.log('[MemberCenter] getCheckInStats 原始响应:', JSON.stringify(statsRes))
|
||||
// 兼容多种后端响应格式
|
||||
const statsData = statsRes?.data || statsRes || {}
|
||||
const totalCount = Number(statsData.totalCount ?? statsData.total ?? 0)
|
||||
console.log('[MemberCenter] 解析后 totalCount:', totalCount)
|
||||
if (store.stats.checkInCount !== totalCount) {
|
||||
store.stats.checkInCount = totalCount
|
||||
changed = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[MemberCenter] 获取签到统计失败:', e)
|
||||
}
|
||||
|
||||
// 获取报课次数(会员预约数)
|
||||
try {
|
||||
const memberId = getMemberId()
|
||||
console.log('[MemberCenter] getMemberId 结果:', memberId)
|
||||
if (memberId) {
|
||||
const bookingsRes = await getMemberBookings(memberId)
|
||||
console.log('[MemberCenter] getMemberBookings 原始响应:', JSON.stringify(bookingsRes))
|
||||
// 兼容多种后端响应格式:
|
||||
// 1. [{...}, {...}] → 直接数组
|
||||
// 2. { data: [{...}] } → data 是数组
|
||||
// 3. { data: { content: [...] } } → Spring 分页双层包装
|
||||
// 4. { content: [...] } → Spring Page 扁平原样
|
||||
let bookings = []
|
||||
const raw = bookingsRes || {}
|
||||
if (Array.isArray(raw)) {
|
||||
bookings = raw
|
||||
} else if (Array.isArray(raw.data)) {
|
||||
bookings = raw.data
|
||||
} else if (raw.data && Array.isArray(raw.data.content)) {
|
||||
bookings = raw.data.content
|
||||
} else if (Array.isArray(raw.content)) {
|
||||
bookings = raw.content
|
||||
} else if (raw.data && typeof raw.data === 'object' && raw.data.data && Array.isArray(raw.data.data)) {
|
||||
bookings = raw.data.data
|
||||
} else if (raw.data && typeof raw.data === 'object' && raw.data.data && raw.data.data.content && Array.isArray(raw.data.data.content)) {
|
||||
bookings = raw.data.data.content
|
||||
}
|
||||
const bookingCount = bookings.length
|
||||
console.log('[MemberCenter] 解析后 bookingCount:', bookingCount, 'bookings:', JSON.stringify(bookings?.slice?.(0, 2)))
|
||||
if (store.stats.courseCount !== bookingCount) {
|
||||
store.stats.courseCount = bookingCount
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
console.warn('[MemberCenter] getMemberId 返回 null/undefined,无法获取报课次数')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[MemberCenter] 获取报课次数失败:', e)
|
||||
}
|
||||
|
||||
// 获取积分
|
||||
try {
|
||||
const detailRes = await getMemberDetail()
|
||||
console.log('[MemberCenter] getMemberDetail 原始响应:', JSON.stringify(detailRes))
|
||||
const detailData = detailRes?.data || detailRes || {}
|
||||
const points = Number(detailData.pointsBalance ?? detailData.points ?? 0)
|
||||
console.log('[MemberCenter] 解析后 points:', points)
|
||||
if (store.stats.pointsBalance !== points) {
|
||||
store.stats.pointsBalance = points
|
||||
changed = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[MemberCenter] 获取积分失败:', e)
|
||||
}
|
||||
|
||||
console.log('[MemberCenter] 最终 stats:', JSON.stringify(store.stats))
|
||||
|
||||
if (changed) {
|
||||
saveMemberStore(store)
|
||||
refreshFromStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCardsFromServer() {
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) return
|
||||
@@ -699,14 +624,6 @@ function goMemberCard() {
|
||||
navigateToPage(PAGE.MEMBER_CARD)
|
||||
}
|
||||
|
||||
function goBooking() {
|
||||
navigateToPage(PAGE.BOOKING)
|
||||
}
|
||||
|
||||
function goCheckInHistory() {
|
||||
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||
}
|
||||
|
||||
function goBodyTestHistory() {
|
||||
navigateToPage(PAGE.BODY_TEST_HISTORY)
|
||||
}
|
||||
@@ -759,14 +676,6 @@ function onQuickAction(type) {
|
||||
}
|
||||
}
|
||||
|
||||
function onCheckInTap(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: item.time,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
function copyReferralCode() {
|
||||
uni.setClipboardData({
|
||||
data: referral.value.code,
|
||||
@@ -819,11 +728,11 @@ function handleLogout() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncNavSafeArea()
|
||||
refreshFromStore()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
onShow(async () => {
|
||||
await refreshStatsFromServer()
|
||||
refreshCardsFromServer()
|
||||
})
|
||||
</script>
|
||||
@@ -833,17 +742,121 @@ onShow(() => {
|
||||
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/member-info-page.css';
|
||||
@import '@/common/style/memberInfo/member-info-header.css';
|
||||
@import '@/common/style/memberInfo/member-info-member-card.css';
|
||||
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-body-report.css';
|
||||
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||
@import '@/common/style/memberInfo/member-info-referral.css';
|
||||
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||
|
||||
/* 个人中心页面头部背景 */
|
||||
.member-center-page-header {
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
/* 简约个人信息卡片 */
|
||||
.profile-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(45, 74, 90, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
margin: 0 24rpx;
|
||||
}
|
||||
|
||||
.profile-card__main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border: 3rpx solid #E8F4F8;
|
||||
}
|
||||
|
||||
.profile-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-info__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.profile-info__name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A202C;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.profile-info__badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
background: linear-gradient(135deg, #FFF7ED, #FFF1E6);
|
||||
border-radius: 20rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
}
|
||||
|
||||
.profile-info__badge-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
.profile-info__level {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #ED8936;
|
||||
}
|
||||
|
||||
.profile-info__phone {
|
||||
font-size: 24rpx;
|
||||
color: #A0AEC0;
|
||||
}
|
||||
|
||||
.profile-card__stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #F0F4F8;
|
||||
}
|
||||
|
||||
.profile-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.profile-stat__value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #2D4A5A;
|
||||
}
|
||||
|
||||
.profile-stat__label {
|
||||
font-size: 22rpx;
|
||||
color: #A0AEC0;
|
||||
}
|
||||
|
||||
.tabbar-fixed {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@@ -853,18 +866,6 @@ onShow(() => {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.booking-section__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.booking-section__empty-text {
|
||||
font-size: 14px;
|
||||
color: #8A99B4;
|
||||
}
|
||||
|
||||
.expiring-cards-section {
|
||||
margin: 0 16px 12px;
|
||||
background: linear-gradient(135deg, #FFF7E6 0%, #FFE4CC 100%);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<view class="member-info-page">
|
||||
<!-- 固定页面头部 -->
|
||||
<view class="member-info-page-header">
|
||||
<PageHeader title="个人中心" subtitle="" />
|
||||
</view>
|
||||
|
||||
<!-- 滚动内容区域 -->
|
||||
<view class="member-scroll-wrap">
|
||||
<scroll-view scroll-y class="member-scroll">
|
||||
@@ -15,32 +20,13 @@
|
||||
<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"
|
||||
/>
|
||||
<MemberInfoQuickActions @action="onQuickAction" />
|
||||
<MemberInfoBookingList
|
||||
v-if="isLogin"
|
||||
:items="bookingPreview"
|
||||
@view-all="goBooking"
|
||||
@item-tap="goBooking"
|
||||
/>
|
||||
<MemberInfoCheckInList
|
||||
v-if="isLogin"
|
||||
:items="checkIns"
|
||||
@view-all="onCheckInViewAll"
|
||||
@item-tap="onCheckInTap"
|
||||
/>
|
||||
<MemberInfoCouponPoints
|
||||
v-if="isLogin"
|
||||
:data="couponPoints"
|
||||
@view-all="onCouponViewAll"
|
||||
@use-coupon="onUseCoupon"
|
||||
@redeem-points="onRedeemPoints"
|
||||
/>
|
||||
<MemberInfoSettings v-if="isLogin" @setting="onSetting" />
|
||||
<MemberInfoLogout v-if="isLogin" @logout="handleLogout" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -63,31 +49,27 @@ import {
|
||||
getCenterPageData,
|
||||
renewMemberCard,
|
||||
saveMemberStore,
|
||||
getDefaultStore
|
||||
getDefaultStore,
|
||||
loadMemberStore
|
||||
} from '@/common/memberInfo/store.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
import { login as miniappLogin, getUserInfo, getMemberDetail, getCheckInStats, updateUserInfo } from '@/api/main.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 MemberInfoQuickActions from '@/components/memberInfo/MemberInfoQuickActions.vue'
|
||||
import MemberInfoBookingList from '@/components/memberInfo/MemberInfoBookingList.vue'
|
||||
import MemberInfoCheckInList from '@/components/memberInfo/MemberInfoCheckInList.vue'
|
||||
import MemberInfoCouponPoints from '@/components/memberInfo/MemberInfoCouponPoints.vue'
|
||||
import MemberInfoSettings from '@/components/memberInfo/MemberInfoSettings.vue'
|
||||
import MemberInfoLogout from '@/components/memberInfo/MemberInfoLogout.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
|
||||
const userInfo = ref({})
|
||||
const stats = ref({})
|
||||
const cardInfo = ref({})
|
||||
const bookingPreview = ref([])
|
||||
const checkIns = ref([])
|
||||
const couponPoints = ref({})
|
||||
const loading = 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')
|
||||
@@ -147,6 +129,50 @@ async function fetchMemberInfo() {
|
||||
stats.value.checkInCount = 0
|
||||
}
|
||||
|
||||
// 获取报课次数
|
||||
try {
|
||||
const memberIdForBooking = getMemberId()
|
||||
console.log('[memberInfo] getMemberId 结果:', memberIdForBooking)
|
||||
if (memberIdForBooking) {
|
||||
const bookingsRes = await getMemberBookings(memberIdForBooking)
|
||||
console.log('[memberInfo] getMemberBookings 原始响应:', JSON.stringify(bookingsRes))
|
||||
// 兼容多种后端响应格式(数组 / data数组 / data.content / content / data.data.content)
|
||||
let bookings = []
|
||||
const raw = bookingsRes || {}
|
||||
if (Array.isArray(raw)) {
|
||||
bookings = raw
|
||||
} else if (Array.isArray(raw.data)) {
|
||||
bookings = raw.data
|
||||
} else if (raw.data && Array.isArray(raw.data.content)) {
|
||||
bookings = raw.data.content
|
||||
} else if (Array.isArray(raw.content)) {
|
||||
bookings = raw.content
|
||||
} else if (raw.data && typeof raw.data === 'object' && raw.data.data && Array.isArray(raw.data.data)) {
|
||||
bookings = raw.data.data
|
||||
} else if (raw.data && typeof raw.data === 'object' && raw.data.data && Array.isArray(raw.data.data.content)) {
|
||||
bookings = raw.data.data.content
|
||||
}
|
||||
stats.value.courseCount = bookings.length
|
||||
console.log('[memberInfo] 解析后 courseCount:', stats.value.courseCount)
|
||||
} else {
|
||||
stats.value.courseCount = 0
|
||||
console.warn('[memberInfo] getMemberId 返回 null/undefined')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[memberInfo] 获取报课次数失败:', e)
|
||||
stats.value.courseCount = 0
|
||||
}
|
||||
|
||||
// 将获取到的数据保存到store,供其他页面(如memberCenter)使用
|
||||
const store = loadMemberStore()
|
||||
store.stats = {
|
||||
...store.stats,
|
||||
checkInCount: stats.value.checkInCount ?? 0,
|
||||
courseCount: stats.value.courseCount ?? 0,
|
||||
pointsBalance: stats.value.pointsBalance ?? 0
|
||||
}
|
||||
saveMemberStore(store)
|
||||
|
||||
// 刷新store中的业务数据
|
||||
refreshFromStore()
|
||||
console.log('[memberInfo] fetchMemberInfo 执行完成')
|
||||
@@ -158,14 +184,11 @@ async function fetchMemberInfo() {
|
||||
}
|
||||
|
||||
function refreshFromStore() {
|
||||
const store = getDefaultStore()
|
||||
const store = loadMemberStore()
|
||||
const pageData = getCenterPageData(store)
|
||||
console.log('[memberInfo] refreshFromStore - pageData:', JSON.stringify(pageData))
|
||||
stats.value = pageData.stats
|
||||
cardInfo.value = pageData.cardInfo
|
||||
bookingPreview.value = pageData.bookingPreview
|
||||
checkIns.value = pageData.checkIns
|
||||
couponPoints.value = pageData.couponPoints
|
||||
console.log('[memberInfo] refreshFromStore - userInfo.value:', JSON.stringify(userInfo.value))
|
||||
}
|
||||
|
||||
@@ -181,8 +204,8 @@ function goPurchaseCard() {
|
||||
navigateToPage(PAGE.PURCHASE_CARD)
|
||||
}
|
||||
|
||||
function goBooking() {
|
||||
navigateToPage(PAGE.BOOKING)
|
||||
function goRechargeStoredCard() {
|
||||
navigateToPage(PAGE.RECHARGE_STORED_CARD)
|
||||
}
|
||||
|
||||
function onRenewCard() {
|
||||
@@ -196,68 +219,6 @@ function onRenewCard() {
|
||||
})
|
||||
}
|
||||
|
||||
function onQuickAction(type) {
|
||||
const routes = {
|
||||
coupon: PAGE.COUPONS,
|
||||
points: PAGE.POINTS
|
||||
}
|
||||
if (routes[type]) {
|
||||
navigateToPage(routes[type])
|
||||
}
|
||||
}
|
||||
|
||||
function onCheckInViewAll() {
|
||||
navigateToPage(PAGE.CHECK_IN_HISTORY)
|
||||
}
|
||||
|
||||
function onCheckInTap(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: item.time,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
function onCouponViewAll() {
|
||||
navigateToPage(PAGE.COUPONS)
|
||||
}
|
||||
|
||||
function onUseCoupon() {
|
||||
navigateToPage(PAGE.COUPONS)
|
||||
}
|
||||
|
||||
function onRedeemPoints() {
|
||||
navigateToPage(PAGE.POINTS)
|
||||
}
|
||||
|
||||
function onSetting(key) {
|
||||
const labels = {
|
||||
password: '修改密码',
|
||||
privacy: '隐私政策',
|
||||
delete: '注销账户'
|
||||
}
|
||||
if (key === 'delete') {
|
||||
uni.showModal({
|
||||
title: '注销账户',
|
||||
content: '注销后数据将无法恢复,确定继续吗?',
|
||||
confirmColor: '#E74C3C'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (key === 'privacy') {
|
||||
uni.showModal({
|
||||
title: '隐私政策',
|
||||
content: '我们重视您的隐私,详细政策请前往官网查看。',
|
||||
showCancel: false
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: `${labels[key] || '设置'}开发中`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
@@ -275,9 +236,6 @@ function handleLogout() {
|
||||
userInfo.value = {}
|
||||
stats.value = {}
|
||||
cardInfo.value = {}
|
||||
bookingPreview.value = []
|
||||
checkIns.value = []
|
||||
couponPoints.value = {}
|
||||
hasActiveCard.value = false
|
||||
refreshFromStore()
|
||||
uni.showToast({ title: '已退出', icon: 'success' })
|
||||
@@ -300,7 +258,13 @@ onShow(() => {
|
||||
isLogin.value = false
|
||||
}
|
||||
|
||||
// 强制刷新数据(忽略 loading 防重入)
|
||||
loading.value = false
|
||||
fetchMemberInfo()
|
||||
// 刷新子组件数据
|
||||
if (memberCardRef.value && typeof memberCardRef.value.loadMemberCard === 'function') {
|
||||
memberCardRef.value.loadMemberCard()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -310,11 +274,6 @@ onShow(() => {
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/member-info-page.css';
|
||||
@import '@/common/style/memberInfo/member-info-header.css';
|
||||
@import '@/common/style/memberInfo/member-info-member-card.css';
|
||||
@import '@/common/style/memberInfo/member-info-quick-actions.css';
|
||||
@import '@/common/style/memberInfo/member-info-booking-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-check-in-list.css';
|
||||
@import '@/common/style/memberInfo/member-info-coupon-points.css';
|
||||
@import '@/common/style/memberInfo/member-info-settings.css';
|
||||
@import '@/common/style/memberInfo/member-info-logout.css';
|
||||
|
||||
@@ -324,7 +283,15 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--gradient-sky);
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
/* 固定页面头部 */
|
||||
.member-info-page-header {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.member-scroll-wrap {
|
||||
|
||||
@@ -1,189 +1,334 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="bt-page">
|
||||
<MemberInfoSubNav title="我的课程" @back="goBack" />
|
||||
<view class="mi-mod-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="bt-tab"
|
||||
:class="{ 'bt-tab--active': activeTab === tab.key }"
|
||||
hover-class="mi-tap-tab--hover"
|
||||
@tap="activeTab = tab.key"
|
||||
>
|
||||
<text class="bt-tab__text">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<PageHeader title="我的课程" subtitle="" :show-back="true">
|
||||
<template #right>
|
||||
<view class="header-scan-btn" @click="handleScan">
|
||||
<text class="header-scan-icon">📷</text>
|
||||
</view>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<view class="bt-page__body">
|
||||
<!-- 团课 -->
|
||||
<template v-if="activeTab === 'group'">
|
||||
<view class="mi-mod-subtabs">
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': groupSub === 'ongoing' }"
|
||||
@tap="groupSub = 'ongoing'"
|
||||
>进行中</text>
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': groupSub === 'completed' }"
|
||||
@tap="groupSub = 'completed'"
|
||||
>已完成</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in groupList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="onGroupCourse(item)"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.banner" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<!-- 子标签:进行中 / 已完成 -->
|
||||
<view class="mi-mod-subtabs">
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': activeTab === 'ongoing' }"
|
||||
@tap="activeTab = 'ongoing'"
|
||||
>进行中</text>
|
||||
<text
|
||||
class="mi-mod-subtab"
|
||||
:class="{ 'mi-mod-subtab--on': activeTab === 'completed' }"
|
||||
@tap="activeTab = 'completed'"
|
||||
>已完成</text>
|
||||
</view>
|
||||
|
||||
<!-- 预约列表 -->
|
||||
<view
|
||||
v-for="item in displayedBookings"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
>
|
||||
<image
|
||||
class="mi-mod-course-card__banner"
|
||||
:src="item.coverImage || 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/default-course.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="mi-mod-course-card__content">
|
||||
<view class="mi-mod-course-card__header">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__coach">{{ item.coach }}</text>
|
||||
<view class="mi-mod-course-card__progress">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: pct(item) + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__progress-text">{{ item.progress }}/{{ item.total }}</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__meta">{{ item.schedule }} · {{ item.location }}</text>
|
||||
<view v-if="groupSub === 'ongoing' && item.canCancel" class="mi-mod-course-card__action" @tap.stop="goBooking">
|
||||
<text>取消预约</text>
|
||||
</view>
|
||||
<view v-if="groupSub === 'completed' && item.canEvaluate" class="mi-mod-course-card__action" @tap.stop="evaluate(item)">
|
||||
<text>去评价</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__status" :class="'mi-mod-course-card__status--' + item.statusClass">{{ item.statusLabel }}</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__meta">{{ item.schedule }} · {{ item.location }}</text>
|
||||
<!-- 仅已预约状态显示取消按钮 -->
|
||||
<view v-if="activeTab === 'ongoing' && item.statusClass === 'booked'" class="mi-mod-course-card__cancel" @tap.stop="handleCancel(item)">
|
||||
<text>取消预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 私教 -->
|
||||
<template v-else-if="activeTab === 'private'">
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">剩余课时 {{ privateData.remaining }} 节</text>
|
||||
<view class="mi-detail-coach">
|
||||
<image class="mi-detail-coach__avatar" :src="privateData.coachAvatar" mode="aspectFill" />
|
||||
<view>
|
||||
<text class="mi-detail-coach__name">{{ privateData.coach }}</text>
|
||||
<text class="mi-detail-coach__rating">下次 {{ privateData.nextClass }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bt-card">
|
||||
<text class="bt-card__title">已约课程</text>
|
||||
<view v-for="b in privateData.bookings" :key="b.id" class="mi-mod-session">
|
||||
<text class="mi-mod-session__title">{{ b.title }}</text>
|
||||
<text class="mi-mod-session__meta">{{ b.time }} · {{ b.location }} · {{ b.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 线上课 -->
|
||||
<template v-else-if="activeTab === 'online'">
|
||||
<view
|
||||
v-for="item in onlineList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
hover-class="mi-tap-card--hover"
|
||||
@tap="goOnline(item)"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.cover" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__meta">{{ item.duration }} · 进度 {{ item.progress }}%</text>
|
||||
<text v-if="item.type === 'live'" class="mi-mod-course-card__next">直播 {{ item.liveTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 训练营 -->
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="item in packageList"
|
||||
:key="item.id"
|
||||
class="mi-mod-course-card"
|
||||
>
|
||||
<image class="mi-mod-course-card__banner" :src="item.banner" mode="aspectFill" />
|
||||
<view class="mi-mod-course-card__content">
|
||||
<text class="mi-mod-course-card__title">{{ item.title }}</text>
|
||||
<text class="mi-mod-course-card__coach">{{ item.coach }}</text>
|
||||
<view class="mi-mod-course-card__progress">
|
||||
<view class="mi-mod-course-card__progress-bar">
|
||||
<view class="mi-mod-course-card__progress-fill" :style="{ width: pct(item) + '%' }"></view>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__progress-text">{{ item.progress }}/{{ item.total }} 节</text>
|
||||
</view>
|
||||
<text class="mi-mod-course-card__meta">{{ item.schedule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="isEmpty" class="bt-empty">
|
||||
<text class="bt-empty__text">暂无课程</text>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="!displayedBookings.length" class="bt-empty">
|
||||
<text class="bt-empty__text">
|
||||
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付密码弹窗 -->
|
||||
<PayPasswordModal
|
||||
:visible="showPayPwd"
|
||||
:title="payPwdTitle"
|
||||
:subtitle="payPwdSubtitle"
|
||||
:cancel-notice="payPwdNotice"
|
||||
@confirm="onPayPasswordConfirm"
|
||||
@cancel="onPayPasswordCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
import { moduleMock, getMyCoursesData } from '@/common/memberInfo/moduleStore.js'
|
||||
import { loadMemberStore } from '@/common/memberInfo/store.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import PayPasswordModal from '@/components/global/PayPasswordModal.vue'
|
||||
import { getMemberId } 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'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
tabs: moduleMock.myCourseTabs,
|
||||
activeTab: 'group',
|
||||
groupSub: 'ongoing',
|
||||
privateData: {},
|
||||
onlineList: [],
|
||||
packageList: [],
|
||||
groupData: { ongoing: [], completed: [] }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupList() {
|
||||
return this.groupData[this.groupSub] || []
|
||||
},
|
||||
isEmpty() {
|
||||
if (this.activeTab === 'group') return !this.groupList.length
|
||||
if (this.activeTab === 'private') return !this.privateData.bookings?.length
|
||||
if (this.activeTab === 'online') return !this.onlineList.length
|
||||
return !this.packageList.length
|
||||
}
|
||||
},
|
||||
onShow() { this.refresh() },
|
||||
methods: {
|
||||
refresh() {
|
||||
const store = loadMemberStore()
|
||||
this.groupData = getMyCoursesData(store, 'group')
|
||||
this.privateData = getMyCoursesData(store, 'private')
|
||||
this.onlineList = getMyCoursesData(store, 'online').list || []
|
||||
this.packageList = getMyCoursesData(store, 'package').list || []
|
||||
},
|
||||
pct(item) {
|
||||
return item.total ? Math.min(100, Math.round((item.progress / item.total) * 100)) : 0
|
||||
},
|
||||
goBooking() { navigateToPage(PAGE.BOOKING) },
|
||||
goOnline(item) { navigateToPage(`${PAGE.ONLINE_COURSE}?id=${item.id}`) },
|
||||
evaluate(item) {
|
||||
navigateToPage(`${PAGE.COURSE_EVALUATE}?title=${encodeURIComponent(item.title)}`)
|
||||
},
|
||||
onGroupCourse(item) {
|
||||
uni.showModal({
|
||||
title: item.title,
|
||||
content: `${item.coach}\n${item.schedule}\n${item.location}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
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
|
||||
: completedBookings.value
|
||||
})
|
||||
|
||||
async function fetchBookings() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
console.log('[myCourses] 开始获取预约记录')
|
||||
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) {
|
||||
console.warn('[myCourses] 未登录')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMemberBookings(memberId)
|
||||
const bookings = Array.isArray(res) ? res : (res?.data || res?.content || [])
|
||||
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')
|
||||
} else {
|
||||
ongoingBookings.value = []
|
||||
completedBookings.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[myCourses] 获取预约记录失败:', e)
|
||||
ongoingBookings.value = []
|
||||
completedBookings.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function mapBooking(b) {
|
||||
const courseStartTime = b.courseStartTime || b.startTime || ''
|
||||
const bookStatus = String(b.status || '0')
|
||||
|
||||
// status: 0-已预约,1-已取消,2-已出席,3-缺席
|
||||
let status = 'ongoing'
|
||||
let statusLabel = '已预约'
|
||||
let statusClass = 'booked'
|
||||
|
||||
if (bookStatus === '1') {
|
||||
status = 'completed'
|
||||
statusLabel = '已取消'
|
||||
statusClass = 'cancelled'
|
||||
} else if (bookStatus === '2') {
|
||||
// 已签到保留在进行中,方便用户查看签到结果
|
||||
status = 'ongoing'
|
||||
statusLabel = '已签到'
|
||||
statusClass = 'attended'
|
||||
} else if (bookStatus === '3') {
|
||||
status = 'ongoing'
|
||||
statusLabel = '缺席'
|
||||
statusClass = 'absent'
|
||||
} else if (bookStatus === '0') {
|
||||
status = 'ongoing'
|
||||
statusLabel = '已预约'
|
||||
statusClass = 'booked'
|
||||
}
|
||||
|
||||
return {
|
||||
id: b.id,
|
||||
courseId: b.courseId,
|
||||
title: b.courseName || '预约课程',
|
||||
status: status,
|
||||
originalStatus: bookStatus,
|
||||
statusLabel: statusLabel,
|
||||
statusClass: statusClass,
|
||||
schedule: formatBookingTime(courseStartTime),
|
||||
location: b.location || '',
|
||||
coverImage: b.coverImage || ''
|
||||
}
|
||||
}
|
||||
|
||||
function formatBookingTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
try {
|
||||
const d = new Date(timeStr)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const hour = d.getHours().toString().padStart(2, '0')
|
||||
const minute = d.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}月${day}日 ${hour}:${minute}`
|
||||
} catch {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(item) {
|
||||
uni.showModal({
|
||||
title: '取消预约',
|
||||
content: `确定要取消「${item.title}」吗?`,
|
||||
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
|
||||
})
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
uni.showToast({ title: '已取消', icon: 'success' })
|
||||
fetchBookings()
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '取消失败', icon: 'none', duration: 3000 })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[myCourses] 取消预约失败:', e)
|
||||
const errMsg = e?.message || '取消失败'
|
||||
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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({
|
||||
success: async (res) => {
|
||||
const qrContent = res.result
|
||||
console.log('[myCourses] 扫码结果:', qrContent)
|
||||
|
||||
// 解析二维码内容获取 courseId
|
||||
let courseId = null
|
||||
try {
|
||||
const parsed = JSON.parse(qrContent)
|
||||
courseId = Number(parsed.courseId)
|
||||
} catch {
|
||||
const num = Number(qrContent)
|
||||
if (!isNaN(num)) {
|
||||
courseId = num
|
||||
}
|
||||
}
|
||||
|
||||
if (!courseId) {
|
||||
uni.showToast({ title: '无效的签到二维码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const memberId = getMemberId()
|
||||
if (!memberId) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '签到中...' })
|
||||
try {
|
||||
const result = await qrSignInGroupCourse(courseId, memberId)
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
uni.showToast({ title: '签到成功', icon: 'success' })
|
||||
// 签到成功后刷新列表
|
||||
await fetchBookings()
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '签到失败', icon: 'none', duration: 3000 })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[myCourses] 扫码签到失败:', e)
|
||||
const errMsg = e?.message || e?.data?.message || '签到失败'
|
||||
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('[myCourses] 扫码取消或失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchBookings()
|
||||
fetchFreeLimit()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -191,7 +336,6 @@ export default {
|
||||
@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';
|
||||
@import '@/common/style/memberInfo/member-info-tap.css';
|
||||
@import '@/common/style/memberInfo/pages/body-test-common.css';
|
||||
@import '@/common/style/memberInfo/pages/module-pages-common.css';
|
||||
@@ -213,13 +357,80 @@ export default {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mi-mod-course-card__action {
|
||||
margin-top: 6px;
|
||||
.mi-mod-course-card__cancel {
|
||||
margin-top: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
border: 2rpx solid var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.mi-mod-course-card__action text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
.mi-mod-course-card__cancel text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.mi-mod-course-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.mi-mod-course-card__status {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mi-mod-course-card__status--booked {
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
color: #1890ff;
|
||||
border: 2rpx solid rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.mi-mod-course-card__status--attended {
|
||||
background: rgba(82, 196, 26, 0.1);
|
||||
color: #389e0d;
|
||||
border: 2rpx solid rgba(82, 196, 26, 0.2);
|
||||
}
|
||||
|
||||
.mi-mod-course-card__status--cancelled {
|
||||
background: rgba(153, 153, 153, 0.1);
|
||||
color: #999;
|
||||
border: 2rpx solid rgba(153, 153, 153, 0.2);
|
||||
}
|
||||
|
||||
.mi-mod-course-card__status--absent {
|
||||
background: rgba(250, 173, 20, 0.1);
|
||||
color: #d48806;
|
||||
border: 2rpx solid rgba(250, 173, 20, 0.2);
|
||||
}
|
||||
|
||||
/* 头部扫码按钮 */
|
||||
.header-scan-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-scan-btn:active {
|
||||
background: rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.header-scan-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -306,7 +306,7 @@ let wsReject = null
|
||||
let qrPaymentResolve = null
|
||||
let qrPaymentReject = null
|
||||
|
||||
const WS_BASE_URL = 'ws://192.168.43.89:8084/ws/payment'
|
||||
const WS_BASE_URL = 'ws://192.168.5.15:8084/ws/payment'
|
||||
|
||||
const TRADE_TYPE = 'ALIPAY'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="Pixso-frame-2_791">
|
||||
<MemberInfoSubNav title="个人信息" @back="goBack" />
|
||||
<PageHeader title="个人信息" subtitle="" :show-back="true" />
|
||||
<view class="Pixso-frame-2_802">
|
||||
<view class="frame-content-2_802">
|
||||
<view class="avatar-block">
|
||||
@@ -201,7 +201,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import { getUserInfo, updateUserInfo } from '@/api/main.js'
|
||||
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
|
||||
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import RecommendCourseCard from '@/components/recommendCourses/RecommendCourseCard.vue'
|
||||
import { getGroupCourseRecommendList } from '@/api/groupCourse.js'
|
||||
@@ -150,6 +151,11 @@ const handleCardClick = (recommend) => {
|
||||
onMounted(() => {
|
||||
fetchRecommendCourses()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
console.log('[Recommend Courses Page] onShow 刷新推荐课程')
|
||||
loadFromNetwork()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user