实现大部分页面前后端互通

This commit is contained in:
2026-07-17 15:48:36 +08:00
parent a9ccdab421
commit 2e7c2ced43
7 changed files with 560 additions and 215 deletions
+107 -27
View File
@@ -13,15 +13,20 @@
</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>
<picker mode="date" :value="startDate" @change="onStartDateChange" :end="endDate || '2099-12-31'">
<view class="date-range-item">
<text class="range-label">开始</text>
<text class="range-value" :class="{ placeholder: !startDate }">{{ startDate || '选择日期' }}</text>
</view>
</picker>
<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>
<picker mode="date" :value="endDate" @change="onEndDateChange" :start="startDate || '2020-01-01'">
<view class="date-range-item">
<text class="range-label">结束</text>
<text class="range-value" :class="{ placeholder: !endDate }">{{ endDate || '选择日期' }}</text>
</view>
</picker>
<text v-if="startDate || endDate" class="date-clear" @click="clearDateRange">&#10005;</text>
</view>
<view class="period-row">
@@ -33,14 +38,24 @@
</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 class="type-section">
<view class="type-all-row">
<view class="filter-tab" :class="{ active: currentFilter === 'all' }" @click="switchFilter('all')">全部</view>
</view>
<view class="type-grid" :class="{ expanded: typesExpanded }">
<view v-for="(row, ri) in typeRows" :key="ri" class="type-row" :class="{ 'row-visible': isRowVisible(ri) }">
<view v-for="(tab, ti) in row" :key="ti" class="filter-tab"
:class="{ active: currentFilter === tab.value }" @click="selectType(tab)">
{{ tab.label }}
</view>
</view>
</view>
<view v-if="typeRows.length > 1" class="type-expand-row">
<text class="type-expand-btn" @click="typesExpanded = !typesExpanded">
{{ typesExpanded ? '收起 ' : '展开更多 ' }}
</text>
</view>
</view>
</scroll-view>
<view class="sort-bar">
<text class="result-count"> {{ filteredCourses.length }} 个课程</text>
@@ -127,11 +142,10 @@
{ label: '下午', value: 'afternoon' },
{ label: '晚间', value: 'evening' }
],
filterTabs: [
{ label: '全部', value: 'all' }, { label: '有氧运动', value: '有氧运动' },
{ label: '高强度', value: '高强度' }, { label: '柔韧平衡', value: '柔韧平衡' },
{ label: '力量训练', value: '力量训练' }, { label: '免费', value: 'free' }
],
courseTypes: [], // 从后端获取的团课类型列表
typesExpanded: false, // 类型列表是否展开
visibleRowStart: 0, // 折叠时显示的起始行索引
COLS_PER_ROW: 4, // 每行类型数量
sortOptions: [
{ label: '时间', value: 'time' },
{ label: '难度', value: 'difficulty' },
@@ -154,8 +168,21 @@
this.navBarHeight = navBarHeight
this.totalHeaderHeight = statusBarHeight + navBarHeight
this.loadCourses()
this.loadTypes()
},
computed: {
// 将类型列表按每行 COLS_PER_ROW 个拆分为二维数组
typeRows() {
const tabs = this.courseTypes.map(t => ({
label: t.typeName,
value: String(t.id)
}))
const rows = []
for (let i = 0; i < tabs.length; i += this.COLS_PER_ROW) {
rows.push(tabs.slice(i, i + this.COLS_PER_ROW))
}
return rows
},
filteredCourses() {
let list = this.courses.map(c => {
const bd = c.baseDifficulty || c.calculatedDifficulty || 5
@@ -226,15 +253,38 @@
(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.currentFilter !== 'all') list = list.filter(c => String(c.courseType) === this.currentFilter)
if (this.currentPeriod !== 'all') list = list.filter(c => c.period === this.currentPeriod)
// 日期范围过滤:根据课程 startTime 的日期部分进行筛选
if (this.startDate || this.endDate) {
list = list.filter(c => {
const courseDate = (c.startTime || '').substring(0, 10)
if (!courseDate) return false
if (this.startDate && courseDate < this.startDate) return false
if (this.endDate && courseDate > this.endDate) return false
return true
})
}
if (this.currentSort === 'difficulty') list = [...list].sort((a, b) => a.baseDifficulty - b.baseDifficulty)
else if (this.currentSort === 'popular') list = [...list].sort((a, b) => (b.currentMembers || 0) - (a.currentMembers || 0))
else if (this.currentSort === 'time') {
list = [...list].sort((a, b) => {
const ta = a.startTime || ''
const tb = b.startTime || ''
return ta < tb ? -1 : ta > tb ? 1 : 0
})
}
// 只显示可预约的团课
list = list.filter(c => c.canBook)
return list
}
},
methods: {
// 判断某行是否可见:展开时全部可见,折叠时只有 visibleRowStart 行可见
isRowVisible(rowIndex) {
if (this.typesExpanded) return true
return rowIndex === this.visibleRowStart
},
async loadCourses() {
this.loading = true
try {
@@ -248,14 +298,31 @@
this.loading = false
}
},
async loadTypes() {
try {
const types = await courseApi.getCourseTypes()
// 按 id 排序
this.courseTypes = (Array.isArray(types) ? types : []).sort((a, b) => (a.id || 0) - (b.id || 0))
} catch (e) {
console.error('加载课程类型失败:', e)
}
},
onSearchInput() {},
clearSearch() { this.searchKeyword = '' },
onCoverLoad(course) { course.coverLoaded = true },
onCoverError(course) { course.coverError = true },
openStartDatePicker() { uni.showToast({ title: '选择开始日期', icon: 'none' }) },
openEndDatePicker() { uni.showToast({ title: '选择结束日期', icon: 'none' }) },
onStartDateChange(e) { this.startDate = e.detail.value },
onEndDateChange(e) { this.endDate = e.detail.value },
clearDateRange() { this.startDate = ''; this.endDate = '' },
selectPeriod(value) { this.currentPeriod = value },
switchFilter(value) { this.currentFilter = value },
selectType(tab) {
this.currentFilter = tab.value
// 找到该类型所在的行索引,折叠到那一行
const idx = this.typeRows.findIndex(row => row.some(t => t.value === tab.value))
if (idx >= 0) this.visibleRowStart = idx
this.typesExpanded = false
},
switchSort(value) { this.currentSort = value },
goToDetail(course) { uni.navigateTo({ url: '/pages/course-detail/course-detail?id=' + course.id }) },
bookCourse(course) { if (course.canBook) uni.navigateTo({ url: '/pages/course-detail/course-detail?id=' + course.id }) }
@@ -276,20 +343,33 @@
.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; }
.date-range-row picker { flex: 1; margin-right: 10px; }
.date-range-item { background: rgba(255,255,255,0.1); border-radius: 12px; padding: 10px 14px; }
.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; }
.date-clear { font-size: 16px; color: rgba(255,255,255,0.4); padding: 8px; flex-shrink: 0; }
.period-row { display: flex; }
.period-item { flex: 1; text-align: center; padding: 7px 0; border-radius: 40px; font-size: 13px; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.08); margin: 0 3px; }
.period-item.active { background: #00E676; color: #1A1A1A; font-weight: 600; }
.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; }
.type-section { background: #FFFFFF; padding: 12px 16px 14px; border-bottom: 1px solid #EEEEEE; }
.type-all-row { margin-bottom: 6px; }
.type-grid { }
.type-row {
display: flex; flex-wrap: wrap;
max-height: 0; opacity: 0;
margin-bottom: 0; overflow: hidden;
transition: max-height 0.25s ease, opacity 0.25s ease, margin-bottom 0.25s ease;
}
.type-row.row-visible { max-height: 100px; opacity: 1; margin-bottom: 6px; }
.type-grid.expanded .type-row { max-height: 100px; opacity: 1; margin-bottom: 6px; }
.filter-tab { display: inline-block; padding: 6px 16px; border-radius: 40px; font-size: 13px; color: #7A7E84; background: #F5F7FA; margin-right: 8px; margin-bottom: 6px; }
.filter-tab.active { background: #00E676; color: #1A1A1A; font-weight: 600; }
.type-expand-row { text-align: center; padding-top: 2px; }
.type-expand-btn { font-size: 12px; color: #00C853; font-weight: 500; }
.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; }