277 lines
12 KiB
Vue
277 lines
12 KiB
Vue
<template>
|
||
<view class="home-page">
|
||
<!-- 状态栏 + 胶囊按钮占位 -->
|
||
<view class="header-wrap" :style="headerStyle">
|
||
<view class="nav-bar" :style="navStyle">
|
||
<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="brand-section">
|
||
<image class="brand-logo" :src="brandConfig.logoUrl || '/static/logo.png'" mode="aspectFit" />
|
||
<view class="brand-text">
|
||
<text class="brand-name">{{ brandConfig.brandName }}</text>
|
||
<text class="brand-slogan">{{ brandConfig.slogan }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 个人信息卡片(深色)+ 轮播图 -->
|
||
<view class="greeting-card" :style="greetingCardStyle">
|
||
<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" :style="checkinBadgeStyle">
|
||
<text class="checkin-icon">✓</text>
|
||
<text>今日已签到</text>
|
||
</view>
|
||
</view>
|
||
<view class="stats">
|
||
<view class="stat-item">
|
||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ memberInfo.totalSignDays }}</text>
|
||
<text class="stat-label">累计签到(天)</text>
|
||
</view>
|
||
<view class="stat-item">
|
||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ 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" :style="{ color: brandConfig.primaryColor }" @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" :style="{ color: brandConfig.primaryColor }">{{ 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')
|
||
const bannerApi = require('../../api/banner')
|
||
const brandStore = require('../../store/brand')
|
||
const { resolveCoverUrl } = require('../../utils/request')
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
statusBarHeight: 0,
|
||
navBarHeight: 44,
|
||
totalHeaderHeight: 44,
|
||
loading: true,
|
||
memberInfo: {
|
||
nickname: '加载中...',
|
||
checkedInToday: false,
|
||
totalSignDays: 0,
|
||
pendingBookings: 0
|
||
},
|
||
banners: [],
|
||
recommendCourses: [],
|
||
brandConfig: brandStore.config
|
||
}
|
||
},
|
||
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
|
||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||
},
|
||
onUnload() {
|
||
uni.$off('brand:updated')
|
||
},
|
||
computed: {
|
||
headerStyle() {
|
||
return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||
},
|
||
navStyle() {
|
||
return 'height:' + this.navBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||
},
|
||
greetingCardStyle() {
|
||
return {
|
||
backgroundColor: this.brandConfig.secondaryColor,
|
||
backgroundImage: this.brandConfig.backgroundImageUrl ? 'url(' + this.brandConfig.backgroundImageUrl + ')' : 'none',
|
||
backgroundSize: 'cover',
|
||
backgroundPosition: 'center'
|
||
}
|
||
},
|
||
checkinBadgeStyle() {
|
||
return { backgroundColor: 'rgba(' + this.brandConfig.primaryColorRgb + ',0.2)', color: this.brandConfig.primaryColor }
|
||
}
|
||
},
|
||
onShow() {
|
||
if (!store.isLoggedIn) {
|
||
// 未登录时不重定向,登录页是首入口,只需跳过数据加载
|
||
return
|
||
}
|
||
this.loadData()
|
||
brandStore.fetchConfig()
|
||
},
|
||
onPullDownRefresh() {
|
||
Promise.all([this.loadData(), brandStore.fetchConfig()])
|
||
.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, bannerResult] = await Promise.allSettled([
|
||
memberApi.getMemberInfo(),
|
||
courseApi.getActiveRecommendations(),
|
||
bannerApi.getActiveBanners()
|
||
])
|
||
|
||
// 处理会员信息
|
||
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: resolveCoverUrl(gc.coverImage),
|
||
recommendReason: r.recommendReason || '',
|
||
priceLabel: (gc.storedValueAmount > 0 ? '¥' + gc.storedValueAmount : '免费'),
|
||
bookedCount: gc.currentMembers || 0
|
||
}
|
||
})
|
||
}
|
||
// 处理轮播图
|
||
if (bannerResult.status === 'fulfilled') {
|
||
const rawBanners = bannerResult.value || []
|
||
console.log('[Index] 获取到轮播图原始数据:', rawBanners.length, '条')
|
||
this.banners = rawBanners.map(b => {
|
||
const resolvedUrl = resolveCoverUrl(b.imageUrl)
|
||
console.log('[Index] Banner id=' + b.id + ' imageUrl原始=' + b.imageUrl + ' → 解析后=' + resolvedUrl)
|
||
return {
|
||
imageUrl: resolvedUrl,
|
||
title: b.title || '',
|
||
subtitle: b.subtitle || ''
|
||
}
|
||
})
|
||
console.log('[Index] 最终 banners:', JSON.stringify(this.banners))
|
||
} else {
|
||
console.error('[Index] 轮播图请求失败:', bannerResult.reason)
|
||
}
|
||
} 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; }
|
||
|
||
/* 品牌展示 */
|
||
.brand-section { display: flex; align-items: center; background: #FFFFFF; border-radius: 16px; padding: 16px 18px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
||
.brand-logo { width: 48px; height: 48px; border-radius: 10px; margin-right: 14px; flex-shrink: 0; }
|
||
.brand-text { display: flex; flex-direction: column; }
|
||
.brand-name { font-size: 18px; font-weight: 700; color: #1E1E1E; }
|
||
.brand-slogan { font-size: 12px; color: #9E9E9E; margin-top: 4px; }
|
||
|
||
/* 个人信息卡片 */
|
||
.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>
|