重写会员端,初步完善首页

This commit is contained in:
2026-07-15 17:26:07 +08:00
parent cacc8997ec
commit 7e45ecd144
26 changed files with 3405 additions and 199 deletions
+241
View File
@@ -0,0 +1,241 @@
<template>
<view class="search-page">
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
<text class="nav-title">&#8981; 团课搜索</text>
</view>
<view class="search-area">
<view class="search-box">
<text class="search-symbol">&#8981;</text>
<input class="search-input" v-model="searchKeyword" placeholder="搜索课程名称、教练、场地..."
placeholder-style="color: rgba(255,255,255,0.35);" @input="onSearchInput" />
<text v-if="searchKeyword" class="clear-btn" @click="clearSearch">&#10005;</text>
</view>
<view class="date-range-row">
<view class="date-range-item" @click="openStartDatePicker">
<text class="range-label">开始</text>
<text class="range-value" :class="{ placeholder: !startDate }">{{ startDate || '选择日期' }}</text>
</view>
<text class="range-sep"></text>
<view class="date-range-item" @click="openEndDatePicker">
<text class="range-label">结束</text>
<text class="range-value" :class="{ placeholder: !endDate }">{{ endDate || '选择日期' }}</text>
</view>
</view>
<view class="period-selector">
<text class="period-label">时段</text>
<scroll-view class="period-scroll" scroll-x>
<view v-for="(period, idx) in periods" :key="idx" class="period-item"
:class="{ active: currentPeriod === period.value }" @click="selectPeriod(period.value)">
<text class="period-symbol">{{ period.symbol }}</text>
<text>{{ period.label }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
<scroll-view class="filter-tabs" scroll-x>
<view class="filter-tabs-inner">
<view v-for="(tab, idx) in filterTabs" :key="idx" class="filter-tab"
:class="{ active: currentFilter === tab.value }" @click="switchFilter(tab.value)">
{{ tab.label }}
</view>
</view>
</scroll-view>
<view class="sort-bar">
<text class="result-count"> {{ filteredCourses.length }} 个课程</text>
<view class="sort-options">
<text v-for="(sort, idx) in sortOptions" :key="idx" class="sort-item"
:class="{ active: currentSort === sort.value }" @click="switchSort(sort.value)">
{{ sort.label }}
</text>
</view>
</view>
<scroll-view class="course-list" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px - 104px)' }">
<view class="course-list-inner">
<view v-for="(course, idx) in filteredCourses" :key="idx" class="course-card" @click="goToDetail(course)">
<view class="course-cover">
<text class="cover-text">{{ course.typeChar }}</text>
</view>
<view class="course-body">
<text class="course-name">{{ course.courseName }}</text>
<view class="course-row">
<text class="course-coach">教练 {{ course.coachName }}</text>
<text class="course-difficulty" :class="'diff-' + course.difficultyLevel">{{ course.difficultyLabel }}</text>
</view>
<view class="course-row">
<text class="course-time">日期 {{ course.startTime }}</text>
</view>
<view class="course-row">
<text class="course-location">{{ course.location }}</text>
<text class="course-capacity">{{ course.currentMembers }}/{{ course.maxMembers }}</text>
</view>
<view class="course-bottom">
<text class="course-price" :class="{ free: course.storedValueAmount === 0 }">
{{ course.storedValueAmount > 0 ? course.storedValueAmount + '元/次' : '免费' }}
</text>
<button class="btn-book" :class="{ disabled: course.isFull }" :disabled="course.isFull"
@click.stop="bookCourse(course)">
{{ course.isFull ? '已约满' : '预约' }}
</button>
</view>
</view>
</view>
<view v-if="filteredCourses.length === 0" class="empty-state">
<text class="empty-symbol">&#8857;</text>
<text class="empty-text">暂无匹配的课程</text>
</view>
<view class="bottom-safe"></view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
statusBarHeight: 0,
navBarHeight: 44,
totalHeaderHeight: 44,
searchKeyword: '', startDate: '', endDate: '',
currentPeriod: 'all', currentFilter: 'all', currentSort: 'time',
periods: [
{ label: '全部', value: 'all', symbol: '&#9716;' },
{ label: '早晨', value: 'morning', symbol: '&#9728;' },
{ label: '上午', value: 'forenoon', symbol: '&#9728;' },
{ label: '下午', value: 'afternoon', symbol: '&#9788;' },
{ label: '晚间', value: 'evening', symbol: '&#9790;' }
],
filterTabs: [
{ label: '全部', value: 'all' }, { label: '力量区', value: 'strength' },
{ label: '有氧', value: 'cardio' }, { label: '瑜伽', value: 'yoga' },
{ label: '私教', value: 'pt' }, { label: '团课', value: 'group' },
{ label: '免费', value: 'free' }
],
sortOptions: [
{ label: '时间', value: 'time' },
{ label: '难度', value: 'difficulty' },
{ label: '热度', value: 'popular' }
],
courses: [
{ id: 1, courseName: '综合力量训练', coachName: '张教练', location: '力量区', category: 'strength', typeChar: '力', startTime: '07-15 10:00', endTime: '07-15 10:45', maxMembers: 20, currentMembers: 8, isFull: false, difficulty: 5, difficultyLevel: 'mid', difficultyLabel: '中等', period: 'forenoon', storedValueAmount: 0 },
{ id: 2, courseName: '流瑜伽 · 中级', coachName: '李教练', location: '3号教室', category: 'yoga', typeChar: '瑜', startTime: '07-15 14:00', endTime: '07-15 15:00', maxMembers: 15, currentMembers: 15, isFull: true, difficulty: 5, difficultyLevel: 'mid', difficultyLabel: '中等', period: 'afternoon', storedValueAmount: 0 },
{ id: 3, courseName: '动感单车 · 冲刺', coachName: '王教练', location: '单车房', category: 'cardio', typeChar: '车', startTime: '07-15 18:00', endTime: '07-15 18:40', maxMembers: 25, currentMembers: 12, isFull: false, difficulty: 4, difficultyLevel: 'low', difficultyLabel: '入门', period: 'evening', storedValueAmount: 0 },
{ id: 4, courseName: 'HIIT训练', coachName: '赵教练', location: '多功能厅', category: 'cardio', typeChar: 'HI', startTime: '07-15 12:00', endTime: '07-15 12:30', maxMembers: 15, currentMembers: 10, isFull: false, difficulty: 7, difficultyLevel: 'high', difficultyLabel: '困难', period: 'forenoon', storedValueAmount: 0 },
{ id: 5, courseName: '普拉提', coachName: '李教练', location: '2号教室', category: 'yoga', typeChar: '普', startTime: '07-15 16:00', endTime: '07-15 16:50', maxMembers: 12, currentMembers: 5, isFull: false, difficulty: 4, difficultyLevel: 'low', difficultyLabel: '入门', period: 'afternoon', storedValueAmount: 0 },
{ id: 6, courseName: '搏击操', coachName: '刘教练', location: '搏击区', category: 'cardio', typeChar: '搏', startTime: '07-15 07:00', endTime: '07-15 07:45', maxMembers: 20, currentMembers: 6, isFull: false, difficulty: 5, difficultyLevel: 'mid', difficultyLabel: '中等', period: 'morning', storedValueAmount: 0 }
]
}
},
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
},
computed: {
filteredCourses() {
let list = this.courses
if (this.searchKeyword) {
const kw = this.searchKeyword.toLowerCase()
list = list.filter(c => c.courseName.toLowerCase().includes(kw) || c.coachName.toLowerCase().includes(kw) || c.location.toLowerCase().includes(kw))
}
if (this.currentFilter === 'free') list = list.filter(c => c.storedValueAmount === 0)
else if (this.currentFilter !== 'all') list = list.filter(c => c.category === this.currentFilter)
if (this.currentPeriod !== 'all') list = list.filter(c => c.period === this.currentPeriod)
if (this.currentSort === 'difficulty') list = [...list].sort((a, b) => a.difficulty - b.difficulty)
else if (this.currentSort === 'popular') list = [...list].sort((a, b) => b.currentMembers - a.currentMembers)
return list
}
},
methods: {
onSearchInput() {},
clearSearch() { this.searchKeyword = '' },
openStartDatePicker() { uni.showToast({ title: '选择开始日期', icon: 'none' }) },
openEndDatePicker() { uni.showToast({ title: '选择结束日期', icon: 'none' }) },
selectPeriod(value) { this.currentPeriod = value },
switchFilter(value) { this.currentFilter = value },
switchSort(value) { this.currentSort = value },
goToDetail(course) { uni.navigateTo({ url: '/pages/course-detail/course-detail?id=' + course.id }) },
bookCourse(course) { if (!course.isFull) uni.navigateTo({ url: '/pages/course-detail/course-detail?id=' + course.id }) }
}
}
</script>
<style scoped>
.search-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; }
.search-area { background: #1A1A1A; padding: 0 16px 20px; }
.search-box { background: rgba(255,255,255,0.12); border-radius: 40px; padding: 0 16px; display: flex; align-items: center; height: 42px; margin-bottom: 14px; }
.search-symbol { font-size: 15px; color: rgba(255,255,255,0.5); flex-shrink: 0; margin-right: 8px; }
.search-input { flex: 1; font-size: 14px; color: #FFFFFF; height: 100%; }
.clear-btn { font-size: 15px; color: rgba(255,255,255,0.5); padding: 4px; flex-shrink: 0; }
.date-range-row { display: flex; align-items: center; margin-bottom: 14px; }
.date-range-item { flex: 1; background: rgba(255,255,255,0.1); border-radius: 12px; padding: 10px 14px; margin-right: 10px; }
.range-label { font-size: 11px; color: rgba(255,255,255,0.5); display: block; margin-bottom: 2px; }
.range-value { font-size: 14px; font-weight: 600; color: #FFFFFF; }
.range-value.placeholder { color: rgba(255,255,255,0.35); font-weight: 400; }
.range-sep { color: rgba(255,255,255,0.5); font-size: 14px; flex-shrink: 0; margin-right: 10px; }
.period-selector { display: flex; align-items: center; }
.period-label { font-size: 13px; color: rgba(255,255,255,0.6); flex-shrink: 0; margin-right: 10px; }
.period-scroll { flex: 1; display: flex; white-space: nowrap; }
.period-item { display: inline-flex; align-items: center; padding: 6px 14px; border-radius: 40px; font-size: 13px; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.08); margin-right: 8px; }
.period-item.active { background: #00E676; color: #1A1A1A; font-weight: 600; }
.period-symbol { font-size: 13px; margin-right: 4px; }
.filter-tabs { background: #FFFFFF; display: flex; white-space: nowrap; border-bottom: 1px solid #EEEEEE; }
.filter-tabs-inner { padding: 14px 16px; display: flex; white-space: nowrap; }
.filter-tab { display: inline-block; padding: 6px 16px; border-radius: 40px; font-size: 13px; color: #7A7E84; background: #F5F7FA; margin-right: 8px; }
.filter-tab.active { background: #00E676; color: #1A1A1A; font-weight: 600; }
.sort-bar { display: flex; justify-content: space-between; align-items: center; padding: 10px 16px; background: #FFFFFF; border-bottom: 1px solid #EEEEEE; }
.result-count { font-size: 13px; color: #7A7E84; }
.sort-options { display: flex; }
.sort-item { font-size: 13px; color: #7A7E84; margin-right: 16px; }
.sort-item.active { color: #00C853; font-weight: 600; }
.course-list-inner { padding: 12px 16px 0; }
.course-card { background: #FFFFFF; border-radius: 20px; padding: 14px; margin-bottom: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); display: flex; }
.course-cover { width: 72px; height: 72px; border-radius: 12px; background: rgba(0,230,118,0.15); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 14px; }
.cover-text { font-size: 24px; font-weight: 700; color: #00C853; }
.course-body { flex: 1; display: flex; flex-direction: column; }
.course-name { font-size: 16px; font-weight: 700; color: #1E1E1E; }
.course-row { display: flex; align-items: center; font-size: 12px; color: #7A7E84; }
.course-row > * { margin-right: 12px; }
.course-difficulty { padding: 1px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.diff-low { background: rgba(0,230,118,0.15); color: #00C853; }
.diff-mid { background: rgba(255,165,2,0.15); color: #FFA502; }
.diff-high { background: rgba(255,82,82,0.15); color: #FF5252; }
.course-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 4px; }
.course-price { font-size: 13px; font-weight: 600; color: #00C853; }
.course-price.free { color: #00C853; }
.btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 6px 16px; border-radius: 40px; font-weight: 700; font-size: 13px; line-height: 1.4; box-shadow: 0 2px 6px rgba(0,230,118,0.3); }
.btn-book::after { border: none; }
.btn-book.disabled { background: #E0E0E0; color: #BDBDBD; box-shadow: none; }
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 60px 0; }
.empty-symbol { font-size: 40px; color: #BDBDBD; }
.empty-text { font-size: 14px; color: #BDBDBD; }
.bottom-safe { height: 30px; }
</style>