Files
gym-manage/gym-manage-uniapp/pages/index/index.vue
T
2026-07-23 18:56:35 +08:00

277 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="home-page">
<!-- 状态栏 + 胶囊按钮占位 -->
<view class="header-wrap" :style="headerStyle">
<view class="nav-bar" :style="navStyle">
<text class="nav-title">&#10031; 今日训练</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">&#10003;</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">&#10031; 今日推荐</text>
<text class="section-more" :style="{ color: brandConfig.primaryColor }" @click="goToSearch">查看全部 &#8250;</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>