完善业务闭环,实现对轮播图的管理,优化数据库表文件
This commit is contained in:
@@ -74,40 +74,46 @@
|
||||
<view class="course-cover">
|
||||
<view class="cover-skeleton" v-if="course.coverImage && !course.coverLoaded && !course.coverError"></view>
|
||||
<image v-if="course.coverImage" class="cover-img" :class="{ loaded: course.coverLoaded }" :src="course.coverImage" mode="aspectFill" @load="onCoverLoad(course)" @error="onCoverError(course)"></image>
|
||||
<text class="cover-text" v-if="(!course.coverImage || course.coverError) && course.typeName">{{ course.typeName.slice(0, 2) }}</text>
|
||||
<view class="cover-placeholder" v-if="(!course.coverImage || course.coverError)">
|
||||
<text class="cover-icon">⛫</text>
|
||||
<text class="cover-type-name">{{ course.typeName }}</text>
|
||||
</view>
|
||||
<view class="cover-overlay">
|
||||
<view class="cover-badge diff-badge" :class="'diff-' + course.difficultyLevel">
|
||||
<text class="diff-badge-star" v-for="s in course.difficultyStars" :key="s">★</text>
|
||||
<text class="diff-badge-label">{{ course.difficultyLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="course-body">
|
||||
<view class="course-top">
|
||||
<text class="course-name">{{ course.courseName }}</text>
|
||||
<view class="course-tags">
|
||||
<text class="course-category">{{ course.category }}</text>
|
||||
<text class="course-difficulty" :class="'diff-' + course.difficultyLevel">{{ course.difficultyLabel }}</text>
|
||||
</view>
|
||||
<text class="course-price" :class="{ free: course.storedValueAmount === 0 }">
|
||||
{{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="course-labels" v-if="course.labels && course.labels.length">
|
||||
<text v-for="(label, li) in course.labels" :key="li" class="course-label"
|
||||
:style="{ background: label.color + '1a', color: label.color }">{{ label.labelName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="course-meta">
|
||||
<text class="meta-icon">🕓</text>
|
||||
<text class="course-date">{{ course.shortDate }}</text>
|
||||
<text class="course-time">{{ course.shortTime }} - {{ course.endShortTime }}</text>
|
||||
<text class="course-duration" v-if="course.duration">{{ course.duration }}</text>
|
||||
</view>
|
||||
<view class="course-meta">
|
||||
<text class="meta-icon">📍</text>
|
||||
<text class="course-location">{{ course.location }}</text>
|
||||
</view>
|
||||
<view class="course-meta">
|
||||
<view class="course-bottom">
|
||||
<text class="course-capacity">已预约 {{ course.currentMembers }}/{{ course.maxMembers }}人</text>
|
||||
<button class="btn-book" :class="{ disabled: !course.canBook }" :disabled="!course.canBook"
|
||||
@click.stop="bookCourse(course)">
|
||||
{{ course.canBook ? '立即预约' : course.statusLabel }}
|
||||
</button>
|
||||
</view>
|
||||
<view class="course-labels" v-if="course.labels">
|
||||
<text v-for="(label, li) in course.labels" :key="li" class="course-label"
|
||||
:style="{ background: label.color + '1a', color: label.color }">{{ label.labelName }}</text>
|
||||
</view>
|
||||
<text class="course-price" :class="{ free: course.storedValueAmount === 0 }">
|
||||
{{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }}
|
||||
</text>
|
||||
<view class="course-bottom">
|
||||
<button class="btn-book" :class="{ disabled: !course.canBook }" :disabled="!course.canBook"
|
||||
@click.stop="bookCourse(course)">
|
||||
{{ course.canBook ? '立即预约' : course.statusLabel }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -127,6 +133,7 @@
|
||||
const courseApi = require('../../api/course')
|
||||
const bookingApi = require('../../api/booking')
|
||||
const store = require('../../store/index')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -153,7 +160,8 @@
|
||||
{ label: '难度', value: 'difficulty' },
|
||||
{ label: '热度', value: 'popular' }
|
||||
],
|
||||
courses: []
|
||||
courses: [],
|
||||
activeBookedCourseIds: new Set() // 用户当前已预约(status=0)的课程ID集合
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -172,8 +180,11 @@
|
||||
this.loadCourses()
|
||||
this.loadTypes()
|
||||
},
|
||||
onShow() {
|
||||
this.loadActiveBookings()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
Promise.all([this.loadCourses(), this.loadTypes()])
|
||||
Promise.all([this.loadCourses(), this.loadTypes(), this.loadActiveBookings()])
|
||||
.finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
computed: {
|
||||
@@ -191,7 +202,7 @@
|
||||
},
|
||||
filteredCourses() {
|
||||
let list = this.courses.map(c => {
|
||||
const bd = c.baseDifficulty || c.calculatedDifficulty || 5
|
||||
const bd = this.getTypeDifficulty(c.courseType)
|
||||
const st = c.startTime || ''
|
||||
const et = c.endTime || ''
|
||||
// 兼容 "2026-07-15T16:45:00" 和 "2026-07-15 16:45:00" 两种格式
|
||||
@@ -226,14 +237,22 @@
|
||||
else if (statusStr !== 0) statusLabel = '已关闭'
|
||||
if (!canBook && statusStr === 0 && isFull) statusLabel = '已约满'
|
||||
|
||||
const diffLevel = bd <= 3 ? 'low' : bd <= 6 ? 'mid' : 'high'
|
||||
const diffLabel = bd <= 3 ? '入门' : bd <= 6 ? '中等' : '困难'
|
||||
const diffColor = diffLevel === 'low' ? '#00E676' : diffLevel === 'mid' ? '#FFA502' : '#FF5252'
|
||||
const diffStars = Math.min(Math.max(Math.ceil(bd / 2), 1), 5)
|
||||
|
||||
return {
|
||||
...c,
|
||||
currentMembers: curMembers,
|
||||
maxMembers: maxMems,
|
||||
isFull: isFull,
|
||||
canBook: canBook,
|
||||
difficultyLevel: bd <= 3 ? 'low' : bd <= 6 ? 'mid' : 'high',
|
||||
difficultyLabel: bd <= 3 ? '入门' : bd <= 6 ? '中等' : '困难',
|
||||
difficultyLevel: diffLevel,
|
||||
difficultyLabel: diffLabel,
|
||||
difficultyColor: diffColor,
|
||||
difficultyStars: diffStars,
|
||||
difficultyPercent: Math.round(bd * 10),
|
||||
baseDifficulty: bd,
|
||||
shortDate: md + '-' + dd,
|
||||
shortTime: hh + ':' + mm,
|
||||
@@ -247,8 +266,8 @@
|
||||
status: statusStr,
|
||||
statusLabel: statusLabel,
|
||||
storedValueAmount: c.storedValueAmount != null ? c.storedValueAmount : 0,
|
||||
labels: c.labels || [],
|
||||
coverImage: c.coverImage || ''
|
||||
labels: this.getTypeLabels(c.courseType),
|
||||
coverImage: resolveCoverUrl(c.coverImage),
|
||||
}
|
||||
})
|
||||
if (this.searchKeyword) {
|
||||
@@ -280,12 +299,26 @@
|
||||
return ta < tb ? -1 : ta > tb ? 1 : 0
|
||||
})
|
||||
}
|
||||
// 只显示可预约的团课
|
||||
list = list.filter(c => c.canBook)
|
||||
// 只显示可预约的团课,且排除用户已主动预约的课程
|
||||
list = list.filter(c => c.canBook && !this.activeBookedCourseIds.has(c.id))
|
||||
return list
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 从已加载的课程类型中匹配标签
|
||||
getTypeLabels(courseType) {
|
||||
const type = this.getTypeInfo(courseType)
|
||||
return type && type.labels ? type.labels : []
|
||||
},
|
||||
// 从已加载的课程类型中匹配难度
|
||||
getTypeDifficulty(courseType) {
|
||||
const type = this.getTypeInfo(courseType)
|
||||
return type && type.baseDifficulty != null ? type.baseDifficulty : 5
|
||||
},
|
||||
// 查找课程类型信息
|
||||
getTypeInfo(courseType) {
|
||||
return this.courseTypes.find(t => String(t.id) === String(courseType))
|
||||
},
|
||||
// 判断某行是否可见:展开时全部可见,折叠时只有 visibleRowStart 行可见
|
||||
isRowVisible(rowIndex) {
|
||||
if (this.typesExpanded) return true
|
||||
@@ -313,6 +346,24 @@
|
||||
console.error('加载课程类型失败:', e)
|
||||
}
|
||||
},
|
||||
async loadActiveBookings() {
|
||||
try {
|
||||
const memberInfo = store.getMemberInfo()
|
||||
const memberId = memberInfo ? (memberInfo.memberId || memberInfo.id) : null
|
||||
if (!memberId) {
|
||||
this.activeBookedCourseIds = new Set()
|
||||
return
|
||||
}
|
||||
const list = await bookingApi.getMemberBookings(memberId)
|
||||
// 只记录状态为"0"(已预约)的课程ID,status=1(已取消)、2(已出席)、3(缺席)不挡
|
||||
const bookings = Array.isArray(list) ? list : (list.content || [])
|
||||
this.activeBookedCourseIds = new Set(
|
||||
bookings.filter(b => String(b.status) === '0').map(b => b.courseId)
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('加载预约记录失败:', e)
|
||||
}
|
||||
},
|
||||
onSearchInput() {},
|
||||
clearSearch() { this.searchKeyword = '' },
|
||||
onCoverLoad(course) { course.coverLoaded = true },
|
||||
@@ -408,35 +459,56 @@
|
||||
.sort-item.active { color: #00C853; font-weight: 600; }
|
||||
|
||||
.course-list-inner { padding: 12px 16px 0; }
|
||||
.course-item { margin-bottom: 12px; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); overflow: hidden; }
|
||||
.course-card { background: #FFFFFF; border-radius: 20px; padding: 14px; display: flex; }
|
||||
.course-cover { width: 72px; height: 72px; border-radius: 12px; background: rgba(0,230,118,0.12); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 14px; overflow: hidden; position: relative; }
|
||||
.cover-skeleton { position: absolute; inset: 0; background: linear-gradient(90deg, #E8E8E8 25%, #F0F0F0 50%, #E8E8E8 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; z-index: 1; }
|
||||
.course-item { margin-bottom: 14px; border-radius: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.07); overflow: hidden; }
|
||||
.course-card { background: #FFFFFF; border-radius: 16px; display: flex; flex-direction: column; }
|
||||
|
||||
/* ---- 封面区域 ---- */
|
||||
.course-cover { width: 100%; height: 170px; background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; }
|
||||
.cover-skeleton { position: absolute; inset: 0; background: linear-gradient(90deg, #E8E8E8 25%, #F5F5F5 50%, #E8E8E8 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; z-index: 1; }
|
||||
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
.cover-img { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.3s; z-index: 0; }
|
||||
.cover-img.loaded { opacity: 1; z-index: 2; }
|
||||
.cover-text { font-size: 22px; font-weight: 700; color: #00C853; }
|
||||
.course-body { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.course-top { display: flex; justify-content: space-between; align-items: flex-start; }
|
||||
.course-name { font-size: 16px; font-weight: 700; color: #1E1E1E; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.course-tags { display: flex; flex-shrink: 0; margin-left: 6px; }
|
||||
.course-category { font-size: 11px; color: #00C853; background: rgba(0,230,118,0.12); padding: 1px 8px; border-radius: 20px; font-weight: 500; margin-right: 4px; }
|
||||
.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-meta { display: flex; align-items: center; font-size: 12px; color: #7A7E84; margin-top: 4px; }
|
||||
|
||||
/* 无封面时的占位 */
|
||||
.cover-placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 0; }
|
||||
.cover-icon { font-size: 36px; margin-bottom: 4px; }
|
||||
.cover-type-name { font-size: 16px; font-weight: 700; color: #00C853; }
|
||||
|
||||
/* 封面覆盖层 - 难度角标 */
|
||||
.cover-overlay { position: absolute; top: 10px; right: 10px; z-index: 5; }
|
||||
.diff-badge { display: flex; align-items: center; padding: 4px 10px; border-radius: 20px; background: rgba(0,0,0,0.55); backdrop-filter: blur(4px); }
|
||||
.diff-badge-star { font-size: 11px; margin-right: 1px; }
|
||||
.diff-badge-label { font-size: 11px; font-weight: 600; margin-left: 4px; }
|
||||
.diff-badge.diff-low .diff-badge-star { color: #00E676; }
|
||||
.diff-badge.diff-low .diff-badge-label { color: #69F0AE; }
|
||||
.diff-badge.diff-mid .diff-badge-star { color: #FFA502; }
|
||||
.diff-badge.diff-mid .diff-badge-label { color: #FFBE4D; }
|
||||
.diff-badge.diff-high .diff-badge-star { color: #FF5252; }
|
||||
.diff-badge.diff-high .diff-badge-label { color: #FF8A80; }
|
||||
|
||||
/* ---- 卡片内容 ---- */
|
||||
.course-body { padding: 14px 16px 16px; display: flex; flex-direction: column; }
|
||||
.course-top { display: flex; justify-content: space-between; align-items: center; }
|
||||
.course-name { font-size: 17px; font-weight: 700; color: #1E1E1E; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.course-price { font-size: 20px; font-weight: 700; color: #00C853; flex-shrink: 0; margin-left: 8px; }
|
||||
.course-price.free { color: #00C853; }
|
||||
|
||||
/* 标签 */
|
||||
.course-labels { display: flex; flex-wrap: wrap; margin-top: 8px; }
|
||||
.course-label { font-size: 11px; padding: 2px 8px; border-radius: 10px; margin-right: 6px; margin-bottom: 4px; }
|
||||
|
||||
/* 元数据行 */
|
||||
.course-meta { display: flex; align-items: center; font-size: 13px; color: #7A7E84; margin-top: 6px; }
|
||||
.meta-icon { font-size: 12px; margin-right: 4px; flex-shrink: 0; }
|
||||
.course-date { flex-shrink: 0; }
|
||||
.course-time { margin-left: 8px; }
|
||||
.course-duration { margin-left: 8px; color: #00C853; font-weight: 500; }
|
||||
.course-location { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.course-capacity { color: #7A7E84; }
|
||||
.course-labels { display: flex; flex-wrap: wrap; margin-top: 6px; }
|
||||
.course-label { font-size: 10px; padding: 1px 6px; border-radius: 10px; margin-right: 4px; margin-bottom: 2px; }
|
||||
.course-bottom { text-align: right; margin-top: 6px; }
|
||||
.course-price { font-size: 18px; font-weight: 700; color: #00C853; display: block; margin-top: 6px; }
|
||||
.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 8px rgba(0,230,118,0.3); }
|
||||
|
||||
/* 底部预约行 */
|
||||
.course-bottom { display: flex; align-items: center; margin-top: 12px; padding-top: 10px; border-top: 1px solid #F0F0F0; }
|
||||
.course-capacity { font-size: 13px; color: #7A7E84; flex: 1; }
|
||||
.btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 10px 28px; border-radius: 40px; font-weight: 700; font-size: 15px; line-height: 1.2; flex-shrink: 0; margin-left: auto; text-align: center; box-shadow: 0 2px 8px rgba(0,230,118,0.3); }
|
||||
.btn-book::after { border: none; }
|
||||
.btn-book.disabled { background: #E0E0E0; color: #BDBDBD; box-shadow: none; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user