218 lines
9.4 KiB
Vue
218 lines
9.4 KiB
Vue
<template>
|
||
<view class="home-page">
|
||
<!-- 状态栏 + 胶囊按钮占位 -->
|
||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||
<text class="nav-title">✯ 今日训练</text>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
|
||
<view class="content-inner">
|
||
<!-- 个人信息卡片(深色)+ 轮播图 -->
|
||
<view class="greeting-card">
|
||
<view class="greeting-header" @click="goToProfile">
|
||
<view class="member-info">
|
||
<text class="member-name">{{ memberInfo.nickname ? '你好,' + memberInfo.nickname : '欢迎您!点击前往更新您的信息。' }}</text>
|
||
</view>
|
||
<view class="checkin-badge" v-if="memberInfo.checkedInToday">
|
||
<text class="checkin-icon">✓</text>
|
||
<text>今日已签到</text>
|
||
</view>
|
||
</view>
|
||
<view class="stats">
|
||
<view class="stat-item">
|
||
<text class="stat-number">{{ memberInfo.totalSignDays }}</text>
|
||
<text class="stat-label">累计签到(天)</text>
|
||
</view>
|
||
<view class="stat-item">
|
||
<text class="stat-number">{{ memberInfo.pendingBookings }}</text>
|
||
<text class="stat-label">待上团课</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 轮播图 -->
|
||
<swiper class="banner-swiper" :indicator-dots="true" :autoplay="true" :interval="4000" :duration="500" circular>
|
||
<swiper-item v-for="(banner, idx) in banners" :key="idx">
|
||
<view class="banner-slide" :style="{ backgroundImage: 'url(' + banner.imageUrl + ')' }">
|
||
<text class="banner-title">{{ banner.title }}</text>
|
||
<text class="banner-subtitle">{{ banner.subtitle }}</text>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
</view>
|
||
|
||
<!-- 今日推荐 -->
|
||
<view class="recommend-section">
|
||
<view class="section-header">
|
||
<text class="section-title">✯ 今日推荐</text>
|
||
<text class="section-more" @click="goToSearch">查看全部 ›</text>
|
||
</view>
|
||
<view class="recommend-list">
|
||
<view v-for="(course, idx) in recommendCourses" :key="idx" class="recommend-card"
|
||
@click="goToCourseDetail(course.id)">
|
||
<view class="card-cover" :style="{ backgroundImage: 'url(' + course.imageUrl + ')' }"></view>
|
||
<view class="card-body">
|
||
<text class="card-name">{{ course.courseName }}</text>
|
||
<text class="card-reason">{{ course.recommendReason }}</text>
|
||
<view class="card-bottom">
|
||
<text class="card-price">{{ course.priceLabel }}</text>
|
||
<text class="card-count">{{ course.bookedCount }}人已预约</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="bottom-safe"></view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
const store = require('../../store/index')
|
||
const memberApi = require('../../api/member')
|
||
const courseApi = require('../../api/course')
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
statusBarHeight: 0,
|
||
navBarHeight: 44,
|
||
totalHeaderHeight: 44,
|
||
loading: true,
|
||
memberInfo: {
|
||
nickname: '加载中...',
|
||
checkedInToday: false,
|
||
totalSignDays: 0,
|
||
pendingBookings: 0
|
||
},
|
||
banners: [
|
||
{ imageUrl: 'https://img.zcool.cn/community/01f5c65e5d9c6ca801216518e1d2e0.jpg@1280w_1l_2o_100sh.jpg', title: '夏季燃脂计划', subtitle: 'HIIT + 动感单车,高效燃脂 7 天挑战' },
|
||
{ imageUrl: 'https://img.zcool.cn/community/01a5fe5e5d9c4ba80121985ac87c5f.jpg@1280w_1l_2o_100sh.jpg', title: '瑜伽月卡特惠', subtitle: '新人专享 5 折,流瑜伽 / 普拉提任选' },
|
||
{ imageUrl: 'https://img.zcool.cn/community/0177c85e5d9c84a8012165182ef63f.jpg@1280w_1l_2o_100sh.jpg', title: '私教一对一体验', subtitle: '免费体测 + 定制训练计划,限 30 名' }
|
||
],
|
||
recommendCourses: []
|
||
}
|
||
},
|
||
onLoad() {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
const statusBarHeight = systemInfo.statusBarHeight || 20
|
||
let navBarHeight = 44
|
||
// #ifdef MP-WEIXIN
|
||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||
if (menuButton) {
|
||
navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height
|
||
}
|
||
// #endif
|
||
this.statusBarHeight = statusBarHeight
|
||
this.navBarHeight = navBarHeight
|
||
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||
},
|
||
onShow() {
|
||
if (!store.isLoggedIn) {
|
||
// 未登录时不重定向,登录页是首入口,只需跳过数据加载
|
||
return
|
||
}
|
||
this.loadData()
|
||
},
|
||
onPullDownRefresh() {
|
||
this.loadData().finally(() => { uni.stopPullDownRefresh() })
|
||
},
|
||
methods: {
|
||
goToCourseDetail(id) { uni.navigateTo({ url: '/pages/course-detail/course-detail?id=' + id }) },
|
||
goToSearch() { uni.switchTab({ url: '/pages/search/search' }) },
|
||
goToProfile() { uni.switchTab({ url: '/pages/profile/profile' }) },
|
||
|
||
async loadData() {
|
||
this.loading = true
|
||
try {
|
||
// 并行获取会员信息和推荐课程
|
||
const [memberResult, recommendResult] = await Promise.allSettled([
|
||
memberApi.getMemberInfo(),
|
||
courseApi.getActiveRecommendations()
|
||
])
|
||
|
||
// 处理会员信息
|
||
if (memberResult.status === 'fulfilled') {
|
||
const info = memberResult.value
|
||
store.updateMemberInfo(info)
|
||
this.memberInfo = {
|
||
nickname: info.nickname || '',
|
||
checkedInToday: info.checkedInToday || false,
|
||
totalSignDays: info.totalSignInDays || 0,
|
||
pendingBookings: info.pendingBookings || 0
|
||
}
|
||
}
|
||
|
||
// 处理推荐课程(数据来自 group_course_recommend 表,关联 groupCourse 子对象)
|
||
if (recommendResult.status === 'fulfilled') {
|
||
this.recommendCourses = (recommendResult.value || []).map(r => {
|
||
const gc = r.groupCourse || {}
|
||
return {
|
||
id: gc.id || r.courseId,
|
||
courseName: gc.courseName || '推荐课程',
|
||
imageUrl: gc.coverImage || '',
|
||
recommendReason: r.recommendReason || '',
|
||
priceLabel: (gc.storedValueAmount > 0 ? '¥' + gc.storedValueAmount : '免费'),
|
||
bookedCount: gc.currentMembers || 0
|
||
}
|
||
})
|
||
}
|
||
} catch (e) {
|
||
console.error('首页数据加载失败:', e)
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.home-page { min-height: 100vh; background: #F5F7FA; overflow-x: hidden; }
|
||
.header-wrap { background: #1A1A1A; }
|
||
.nav-bar { background: #1A1A1A; padding: 0 20px; display: flex; align-items: center; }
|
||
.nav-title { font-size: 18px; font-weight: 700; color: #FFFFFF; }
|
||
.content-inner { padding: 16px; }
|
||
|
||
/* 个人信息卡片 */
|
||
.greeting-card { background: #1A1A1A; border-radius: 20px; padding: 18px 20px 0; color: #FFFFFF; margin-bottom: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); overflow: hidden; }
|
||
.greeting-header { display: flex; justify-content: space-between; align-items: flex-start; }
|
||
.member-info { display: flex; flex-direction: column; }
|
||
.member-name { font-size: 18px; font-weight: 600; }
|
||
.member-no { font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 2px; }
|
||
.checkin-badge { background: rgba(0,230,118,0.2); color: #00E676; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 12px; display: flex; align-items: center; }
|
||
.checkin-icon { font-size: 11px; margin-right: 3px; }
|
||
|
||
.stats { display: flex; margin-top: 16px; }
|
||
.stat-item { display: flex; flex-direction: column; margin-right: 24px; }
|
||
.stat-number { font-size: 22px; font-weight: 700; color: #00E676; }
|
||
.stat-label { font-size: 12px; color: rgba(255,255,255,0.6); margin-top: 2px; }
|
||
|
||
|
||
/* 轮播图 */
|
||
.banner-swiper { height: 160px; margin: 14px -20px 0; }
|
||
.banner-slide { height: 100%; background-size: cover; background-position: center; background-color: #2A2A2A; display: flex; flex-direction: column; justify-content: flex-end; padding: 16px 20px; }
|
||
.banner-title { font-size: 20px; font-weight: 700; color: #FFFFFF; text-shadow: 0 2px 6px rgba(0,0,0,0.5); }
|
||
.banner-subtitle { font-size: 14px; color: rgba(255,255,255,0.9); margin-top: 6px; text-shadow: 0 1px 4px rgba(0,0,0,0.5); }
|
||
|
||
/* 今日推荐 */
|
||
.recommend-section { margin-bottom: 16px; }
|
||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
|
||
.section-title { font-size: 18px; font-weight: 700; color: #1E1E1E; }
|
||
.section-more { font-size: 13px; color: #00C853; }
|
||
.recommend-list { display: flex; flex-wrap: wrap; justify-content: space-between; }
|
||
.recommend-card { width: calc(50% - 6px); background: #FFFFFF; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.06); margin-bottom: 12px; }
|
||
.card-cover { height: 130px; background-size: cover; background-position: center; background-color: #E8E8E8; display: flex; align-items: flex-start; justify-content: flex-end; padding: 8px; }
|
||
.card-tag { background: #00E676; color: #1A1A1A; font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 20px; }
|
||
.card-body { padding: 10px 12px 14px; }
|
||
.card-name { font-size: 14px; font-weight: 600; color: #1E1E1E; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; margin-bottom: 4px; }
|
||
.card-reason { font-size: 12px; color: #7A7E84; display: block; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.card-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 2px; }
|
||
.card-price { font-size: 14px; font-weight: 700; color: #00C853; }
|
||
.card-count { font-size: 11px; color: #BDBDBD; }
|
||
.bottom-safe { height: 20px; }
|
||
</style>
|