修正多处问题
This commit is contained in:
@@ -45,9 +45,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getActiveBanners } from '@/api/main.js'
|
||||
|
||||
const banners = [
|
||||
// 默认静态数据(后端无数据时作为兜底)
|
||||
const fallbackBanners = [
|
||||
{
|
||||
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=800&q=80',
|
||||
title: '突破自我',
|
||||
@@ -68,9 +70,49 @@ const banners = [
|
||||
}
|
||||
]
|
||||
|
||||
// 先用回退数据初始化,确保 swiper 立即可见
|
||||
const banners = ref([...fallbackBanners])
|
||||
|
||||
const currentIndex = ref(0)
|
||||
// 记录每张图片的加载状态
|
||||
const imageLoaded = ref(banners.map(() => false))
|
||||
const imageLoaded = ref(fallbackBanners.map(() => false))
|
||||
const isFetching = ref(false)
|
||||
|
||||
async function fetchBanners() {
|
||||
isFetching.value = true
|
||||
try {
|
||||
// 禁用缓存,确保每次获取最新数据
|
||||
const res = await getActiveBanners({ cache: false })
|
||||
// 处理后端返回数据:可能是数组或 { data: [...] }
|
||||
const data = Array.isArray(res) ? res : (res?.data || [])
|
||||
|
||||
if (data.length > 0) {
|
||||
// 映射后端字段到组件字段,替换回退数据
|
||||
banners.value = data.map(item => ({
|
||||
image: item.imageUrl || '',
|
||||
title: item.title || '',
|
||||
subtitle: item.subtitle || '',
|
||||
desc: item.description || ''
|
||||
}))
|
||||
// 更新 imageLoaded 状态
|
||||
imageLoaded.value = banners.value.map(() => false)
|
||||
console.log('[BannerSwiper] 轮播图数据加载成功,共', data.length, '条')
|
||||
} else {
|
||||
console.log('[BannerSwiper] 后端无轮播图数据,使用回退数据')
|
||||
// 保持已有的 fallbackBanners,无需重新赋值
|
||||
imageLoaded.value = banners.value.map(() => false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[BannerSwiper] 轮播图数据加载失败,使用回退数据:', err)
|
||||
// 保持已有的 fallbackBanners,无需重新赋值
|
||||
imageLoaded.value = banners.value.map(() => false)
|
||||
} finally {
|
||||
isFetching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchBanners()
|
||||
})
|
||||
|
||||
const onSwiperChange = (e) => {
|
||||
currentIndex.value = e.detail.current
|
||||
@@ -86,30 +128,28 @@ const previewImage = (index) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 图片加载成功回调
|
||||
const onImageLoad = (index) => {
|
||||
imageLoaded.value[index] = true
|
||||
console.log(`图片 ${index} 加载完成`)
|
||||
}
|
||||
|
||||
// 图片加载失败回调
|
||||
const onImageError = (index) => {
|
||||
console.error(`图片 ${index} 加载失败`)
|
||||
// 可选:设置默认占位图
|
||||
// banners[index].image = '默认图片URL'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner-container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.banner-swiper {
|
||||
width: 100%;
|
||||
height: 480rpx;
|
||||
height: 360rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
@@ -124,7 +164,6 @@ const onImageError = (index) => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 添加图片占位符样式 */
|
||||
.image-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -158,10 +197,10 @@ const onImageError = (index) => {
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 2; /* 确保遮罩层在占位符上面 */
|
||||
background: linear-gradient(135deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.05) 50%, rgba(0,0,0,0.25) 100%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 其余样式保持不变 */
|
||||
.banner-text {
|
||||
position: absolute;
|
||||
left: 36rpx;
|
||||
@@ -198,9 +237,7 @@ const onImageError = (index) => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
margin-top: -100rpx;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
padding: 20rpx 0 8rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<view class="tab-page__header" :style="{ padding: `${statusBarHeight}rpx`, height: `${statusBarHeight + 80}rpx` }">
|
||||
<!-- 状态栏占位区(仅 App 端生效,H5/小程序无状态栏概念) -->
|
||||
<view class="tab-page__status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- 导航栏主体 -->
|
||||
<view class="tab-page__header">
|
||||
<view v-if="showBack" :class="['tab-page__back-btn', { 'tab-page__back-btn--animate': isAnimating }]" @tap="goBack">
|
||||
<text class="tab-page__back-icon">‹</text>
|
||||
</view>
|
||||
<view class="tab-page__title-wrap">
|
||||
<text class="tab-page__title">{{ title }}</text>
|
||||
<text class="tab-page__subtitle">{{ subtitle }}</text>
|
||||
<text v-if="subtitle" class="tab-page__subtitle">{{ subtitle }}</text>
|
||||
</view>
|
||||
<view v-if="$slots.right" class="tab-page__right">
|
||||
<slot name="right"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@@ -29,11 +35,19 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const isAnimating = ref(false)
|
||||
const statusBarHeight = ref(20)
|
||||
const statusBarHeight = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef APP-PLUS
|
||||
// App 端获取真实状态栏高度(px)
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 20
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 0
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
// H5/小程序:状态栏由浏览器或框架处理,不额外占位
|
||||
statusBarHeight.value = 0
|
||||
// #endif
|
||||
|
||||
if (props.showBack) {
|
||||
isAnimating.value = true
|
||||
}
|
||||
@@ -51,10 +65,16 @@ function goBack() {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 状态栏占位:仅在 App 端有高度,H5/小程序为 0,背景继承页面 */
|
||||
.tab-page__status-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 0 32rpx;
|
||||
padding: 0 32rpx 24rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -114,4 +134,11 @@ function goBack() {
|
||||
font-size: 24rpx;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.tab-page__right {
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -12,26 +12,12 @@
|
||||
<text class="entry-title">{{ item.title }}</text>
|
||||
<text class="entry-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="showCheckInMenu" class="checkin-menu-overlay" @tap="showCheckInMenu = false">
|
||||
<view class="checkin-menu" @tap.stop>
|
||||
<view class="menu-item" @tap="handleStoreCheckIn">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/checkIn.png" mode="aspectFit" class="menu-icon" />
|
||||
<text class="menu-text">到店签到</text>
|
||||
</view>
|
||||
<view class="menu-item" @tap="handleGroupCourseCheckIn">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/icons/course.png" mode="aspectFit" class="menu-icon" />
|
||||
<text class="menu-text">团课签到</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showCheckInMenu = ref(false)
|
||||
import { qrSignInGroupCourse } from '@/api/groupCourse.js'
|
||||
import { getMemberId } from '@/utils/request.js'
|
||||
|
||||
const handleEntryClick = (item) => {
|
||||
if (item.title === '签到') {
|
||||
@@ -53,7 +39,8 @@ const handleEntryClick = (item) => {
|
||||
})
|
||||
return
|
||||
}
|
||||
showCheckInMenu.value = !showCheckInMenu.value
|
||||
// 直接调用扫码签到
|
||||
qrScanSignIn()
|
||||
} else if (item.path) {
|
||||
if (item.isTabBar) {
|
||||
uni.switchTab({
|
||||
@@ -67,30 +54,57 @@ const handleEntryClick = (item) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleStoreCheckIn = () => {
|
||||
showCheckInMenu.value = false
|
||||
uni.navigateTo({
|
||||
url: '/pages/checkIn/checkIn'
|
||||
})
|
||||
}
|
||||
|
||||
const handleGroupCourseCheckIn = () => {
|
||||
showCheckInMenu.value = false
|
||||
const qrScanSignIn = () => {
|
||||
uni.scanCode({
|
||||
onlyFromCamera: true,
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
console.log('扫码结果:', res)
|
||||
uni.showToast({
|
||||
title: '扫码成功',
|
||||
icon: 'success'
|
||||
})
|
||||
const qrContent = res.result
|
||||
|
||||
// 解析二维码内容获取 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' })
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: '/pages/memberInfo/myCourses' })
|
||||
}, 1200)
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '签到失败', icon: 'none', duration: 3000 })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('团课签到失败:', e)
|
||||
const errMsg = e?.message || e?.data?.message || '签到失败'
|
||||
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('扫码失败:', err)
|
||||
uni.showToast({
|
||||
title: '扫码失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -118,15 +132,10 @@ const entries = [
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
background: #FFFFFF;
|
||||
margin: 24rpx;
|
||||
border-radius: 28rpx;
|
||||
box-shadow: 0 8rpx 32rpx var(--shadow-blue-light);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.7);
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
box-shadow: 0 8rpx 32rpx rgba(45,74,90,0.08);
|
||||
}
|
||||
|
||||
.entry-item {
|
||||
@@ -182,93 +191,4 @@ const entries = [
|
||||
font-size: 22rpx;
|
||||
color: var(--tabbar-text-inactive);
|
||||
}
|
||||
|
||||
.checkin-menu-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding-top: 280rpx;
|
||||
padding-right: 60rpx;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.checkin-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 140rpx;
|
||||
background: white;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
padding: 16rpx;
|
||||
min-width: 320rpx;
|
||||
animation: slideDown 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -24rpx;
|
||||
right: 60rpx;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 24rpx solid transparent;
|
||||
border-right: 24rpx solid transparent;
|
||||
border-bottom: 24rpx solid white;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-40rpx) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
& + .menu-item {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #2D4A5A;
|
||||
}
|
||||
</style>
|
||||
@@ -72,7 +72,10 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getGroupCoursePage } from '@/api/main.js'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { getActiveRecommendCourses } from '@/api/groupCourse.js'
|
||||
|
||||
console.log('[RecommendCourses] 组件脚本已加载')
|
||||
|
||||
// 推荐课程数据列表
|
||||
const courses = ref([])
|
||||
@@ -105,18 +108,23 @@ const getImageUrl = (coverImage) => {
|
||||
|
||||
// 获取推荐课程
|
||||
const fetchRecommendCourses = async () => {
|
||||
console.log('[RecommendCourses] fetchRecommendCourses 开始请求')
|
||||
try {
|
||||
const res = await getGroupCoursePage({
|
||||
page: 0, size: 10, sort: 'current_members', order: 'desc'
|
||||
}, { cache: true, cacheTime: 5 * 60 * 1000 })
|
||||
if (res && res.content && Array.isArray(res.content)) {
|
||||
// 筛选掉已结束和已满员的课程
|
||||
const filteredCourses = res.content.filter(course => {
|
||||
return course.status !== '2' && course.currentMembers < course.maxMembers
|
||||
const res = await getActiveRecommendCourses()
|
||||
console.log('[RecommendCourses] API 返回:', JSON.stringify(res).substring(0, 200))
|
||||
if (res && Array.isArray(res)) {
|
||||
// 提取推荐中的 groupCourse,只排除已满员的课程(已结束的仍展示,点击时拦截)
|
||||
const filteredCourses = res.filter(recommend => {
|
||||
const course = recommend.groupCourse
|
||||
if (!course) return false
|
||||
const current = Number(course.currentMembers) || 0
|
||||
const max = Number(course.maxMembers) || 999
|
||||
return current < max
|
||||
})
|
||||
|
||||
// 只取前3个符合条件的课程
|
||||
courses.value = filteredCourses.slice(0, 3).map(course => {
|
||||
// 只取前3个符合条件的推荐课程
|
||||
courses.value = filteredCourses.slice(0, 3).map(recommend => {
|
||||
const course = recommend.groupCourse
|
||||
return {
|
||||
id: course.id,
|
||||
image: getImageUrl(course.coverImage),
|
||||
@@ -127,6 +135,9 @@ const fetchRecommendCourses = async () => {
|
||||
rawData: course
|
||||
}
|
||||
})
|
||||
console.log('[RecommendCourses] 最终展示 courses.value 数量:', courses.value.length)
|
||||
} else {
|
||||
console.warn('[RecommendCourses] API 返回格式异常:', res)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取推荐课程失败:', err)
|
||||
@@ -143,7 +154,14 @@ const handleViewMore = () => {
|
||||
uni.navigateTo({ url: '/pages/recommendCourses/index' })
|
||||
}
|
||||
|
||||
onMounted(() => { fetchRecommendCourses() })
|
||||
onMounted(() => { console.log('[RecommendCourses] onMounted 触发'); fetchRecommendCourses() })
|
||||
|
||||
onShow(() => {
|
||||
console.log('[RecommendCourses] onShow 触发,刷新推荐课程')
|
||||
fetchRecommendCourses()
|
||||
})
|
||||
|
||||
defineExpose({ fetchRecommendCourses })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -45,7 +45,10 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getGroupCoursePage } from '@/api/main.js'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { getActiveRecommendCourses } from '@/api/groupCourse.js'
|
||||
|
||||
console.log('[TodayRecommend] 组件脚本已加载')
|
||||
|
||||
// 今日推荐数据列表
|
||||
const recommends = ref([])
|
||||
@@ -78,16 +81,22 @@ const getImageUrl = (coverImage) => {
|
||||
|
||||
// 获取今日推荐
|
||||
const fetchTodayRecommend = async () => {
|
||||
console.log('[TodayRecommend] fetchTodayRecommend 开始请求')
|
||||
try {
|
||||
const res = await getGroupCoursePage({
|
||||
page: 0, size: 10, sort: 'current_members', order: 'desc'
|
||||
}, { cache: true, cacheTime: 5 * 60 * 1000 })
|
||||
if (res && res.content && Array.isArray(res.content)) {
|
||||
const filteredCourses = res.content.filter(course => {
|
||||
return course.status !== '2' && course.currentMembers < course.maxMembers
|
||||
const res = await getActiveRecommendCourses()
|
||||
console.log('[TodayRecommend] API 返回:', JSON.stringify(res).substring(0, 200))
|
||||
if (res && Array.isArray(res)) {
|
||||
// 提取推荐中的 groupCourse,只排除已满员的课程
|
||||
const filteredCourses = res.filter(recommend => {
|
||||
const course = recommend.groupCourse
|
||||
if (!course) return false
|
||||
const current = Number(course.currentMembers) || 0
|
||||
const max = Number(course.maxMembers) || 999
|
||||
return current < max
|
||||
})
|
||||
|
||||
recommends.value = filteredCourses.slice(0, 3).map(course => {
|
||||
recommends.value = filteredCourses.slice(0, 3).map(recommend => {
|
||||
const course = recommend.groupCourse
|
||||
return {
|
||||
id: course.id,
|
||||
image: getImageUrl(course.coverImage),
|
||||
@@ -97,6 +106,9 @@ const fetchTodayRecommend = async () => {
|
||||
participants: String(course.currentMembers || 0)
|
||||
}
|
||||
})
|
||||
console.log('[TodayRecommend] 最终展示 recommends.value 数量:', recommends.value.length)
|
||||
} else {
|
||||
console.warn('[TodayRecommend] API 返回格式异常:', res)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取今日推荐失败:', err)
|
||||
@@ -108,7 +120,14 @@ const handleItemClick = (item) => {
|
||||
uni.navigateTo({ url: `/pages/groupCourse/detail?id=${item.id}` })
|
||||
}
|
||||
|
||||
onMounted(() => { fetchTodayRecommend() })
|
||||
onMounted(() => { console.log('[TodayRecommend] onMounted 触发'); fetchTodayRecommend() })
|
||||
|
||||
onShow(() => {
|
||||
console.log('[TodayRecommend] onShow 触发,刷新今日推荐')
|
||||
fetchTodayRecommend()
|
||||
})
|
||||
|
||||
defineExpose({ fetchTodayRecommend })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user