完善团课相关页面交互,完成团课列表页基础后端交互。(后端连接至服务器,版本为DEV分支版本)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import request from "@/utils/request.js"
|
||||
|
||||
export function getGroupCourseList(params = {}, options = {}) {
|
||||
return request.get('/groupCourse/list', params, options)
|
||||
}
|
||||
|
||||
export function getGroupCoursePage(params = {}, options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
|
||||
const { page = 0, size = 10, sort = 'id', order = 'asc', keyword } = params
|
||||
return request.post('/groupCourse/page', { page, size, sort, order, keyword }, options)
|
||||
}
|
||||
|
||||
export function getGroupCourseById(id, options = { cache: true, cacheTime: 15 * 60 * 1000 }) {
|
||||
return request.get(`/groupCourse/${id}`, {}, options)
|
||||
}
|
||||
|
||||
export function getGroupCourseDetail(id, options = { cache: true, cacheTime: 15 * 60 * 1000 }) {
|
||||
return request.get(`/groupCourse/${id}/detail`, {}, options)
|
||||
}
|
||||
|
||||
export function createGroupCourse(params) {
|
||||
return request.post('/groupCourse', params)
|
||||
}
|
||||
|
||||
export function updateGroupCourse(id, params) {
|
||||
return request.put(`/groupCourse/${id}`, params)
|
||||
}
|
||||
|
||||
export function cancelGroupCourse(id) {
|
||||
return request.post(`/groupCourse/${id}/cancel`)
|
||||
}
|
||||
|
||||
export function deleteGroupCourse(id) {
|
||||
return request.delete(`/groupCourse/${id}`)
|
||||
}
|
||||
|
||||
export function getGroupCourseTypes(params = {}, options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
|
||||
return request.get('/groupCourse/types', params, options)
|
||||
}
|
||||
|
||||
export function getGroupCourseTypeById(id, options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
|
||||
return request.get(`/groupCourse/types/${id}`, {}, options)
|
||||
}
|
||||
|
||||
export function getTypeLabels(typeId, options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
|
||||
return request.get(`/groupCourse/types/${typeId}/labels`, {}, options)
|
||||
}
|
||||
|
||||
export function bookGroupCourse(params) {
|
||||
return request.post('/groupCourse/book', params)
|
||||
}
|
||||
|
||||
export function cancelBooking(bookingId, params) {
|
||||
return request.post(`/groupCourse/booking/${bookingId}/cancel`, params)
|
||||
}
|
||||
|
||||
export function getMemberBookings(memberId, options = {}) {
|
||||
return request.get(`/groupCourse/bookings/member/${memberId}`, {}, options)
|
||||
}
|
||||
|
||||
export default {
|
||||
getGroupCourseList,
|
||||
getGroupCoursePage,
|
||||
getGroupCourseById,
|
||||
getGroupCourseDetail,
|
||||
createGroupCourse,
|
||||
updateGroupCourse,
|
||||
cancelGroupCourse,
|
||||
deleteGroupCourse,
|
||||
getGroupCourseTypes,
|
||||
getGroupCourseTypeById,
|
||||
getTypeLabels,
|
||||
bookGroupCourse,
|
||||
cancelBooking,
|
||||
getMemberBookings
|
||||
}
|
||||
@@ -91,11 +91,6 @@ function checkShouldShow() {
|
||||
}
|
||||
const shouldHide = HIDE_TABBAR_PAGES.includes(routePath)
|
||||
shouldShowTabBar.value = !shouldHide
|
||||
console.log('=== TabBar 显示控制 ===')
|
||||
console.log('原始路径:', getCurrentRoutePath())
|
||||
console.log('标准化路径:', routePath)
|
||||
console.log('是否隐藏:', shouldHide)
|
||||
console.log('是否显示 TabBar:', shouldShowTabBar.value)
|
||||
}
|
||||
|
||||
let routeWatcher = null
|
||||
|
||||
@@ -47,10 +47,20 @@
|
||||
</view>
|
||||
|
||||
<!-- 课程时长 -->
|
||||
<view class="course-duration">
|
||||
<view class="course-tags">
|
||||
<view class="tag-item duration-tag">
|
||||
<uni-icons type="time" size="14" color="#8A99B4" />
|
||||
<text>{{ formatDuration(course.startTime, course.endTime) }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="label in courseTypeLabels"
|
||||
:key="label.id"
|
||||
class="tag-item type-tag"
|
||||
:style="{ background: `${label.color}15`, color: label.color }"
|
||||
>
|
||||
<text>{{ label.labelName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 卡片底部操作区域 -->
|
||||
@@ -109,6 +119,10 @@ const props = defineProps({
|
||||
course: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
courseTypeLabels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -150,9 +164,29 @@ const formatTime = (dateStr) => {
|
||||
|
||||
const formatDuration = (startStr, endStr) => {
|
||||
if (!startStr || !endStr) return ''
|
||||
|
||||
const start = new Date(startStr)
|
||||
const end = new Date(endStr)
|
||||
const minutes = Math.floor((end.getTime() - start.getTime()) / 60000)
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
console.warn(`[CourseCard] 无效的时间格式: startTime=${startStr}, endTime=${endStr}`)
|
||||
return ''
|
||||
}
|
||||
|
||||
const diffMs = end.getTime() - start.getTime()
|
||||
if (diffMs <= 0) {
|
||||
console.warn(`[CourseCard] 结束时间小于或等于开始时间: startTime=${startStr}, endTime=${endStr}`)
|
||||
return ''
|
||||
}
|
||||
|
||||
const maxDurationMinutes = 24 * 60
|
||||
const minutes = Math.floor(diffMs / 60000)
|
||||
|
||||
if (minutes > maxDurationMinutes) {
|
||||
console.warn(`[CourseCard] 课程时长超过24小时,已修正: ${minutes}分钟 -> ${maxDurationMinutes}分钟`)
|
||||
return `${maxDurationMinutes}分钟以上`
|
||||
}
|
||||
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
|
||||
@@ -340,19 +374,35 @@ const handleBooking = () => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 课程时长 */
|
||||
.course-duration {
|
||||
/* 课程标签区域 */
|
||||
.course-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
/* 标签项 */
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
gap: 6rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #3B82F6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 时长标签 */
|
||||
.duration-tag {
|
||||
background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%);
|
||||
color: #3B82F6;
|
||||
}
|
||||
|
||||
/* 类型标签 */
|
||||
.type-tag {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 卡片底部区域 */
|
||||
.card-footer {
|
||||
padding: 24rpx 32rpx 28rpx;
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<!-- 课程卡片 -->
|
||||
<view class="course-card" :style="cardStyle">
|
||||
<!-- 课程图片区域 -->
|
||||
<view class="course-image" :style="imageStyle">
|
||||
<!-- 课程封面图片 -->
|
||||
<image :src="course.image" mode="aspectFill" class="img" />
|
||||
<!-- 图片渐变遮罩 -->
|
||||
<view class="course-overlay"></view>
|
||||
<!-- 课程标签 -->
|
||||
<text :class="['course-tag', course.tagType]">{{ course.tag }}</text>
|
||||
<!-- 课程信息区域 -->
|
||||
<view class="course-info">
|
||||
<!-- 课程名称 -->
|
||||
<text class="course-name">{{ course.name }}</text>
|
||||
<!-- 课程元信息(时长、难度) -->
|
||||
<view class="course-meta">
|
||||
<!-- 时长信息 -->
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/time.png"/>
|
||||
</text>
|
||||
<text>{{ course.duration }}</text>
|
||||
</view>
|
||||
<!-- 难度信息 -->
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/intensity.png"/>
|
||||
</text>
|
||||
<text>{{ course.level }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 课程底部区域 -->
|
||||
<view class="course-footer">
|
||||
<!-- 参与人数信息 -->
|
||||
<view class="participants">
|
||||
<text class="fire-icon">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/hot.png"/>
|
||||
</text>
|
||||
<text>{{ course.participants }}人参与</text>
|
||||
</view>
|
||||
<!-- 去参与按钮 -->
|
||||
<view class="join-btn" @click="handleJoinCourse">
|
||||
<text>去参与</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, computed } from 'vue'
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
course: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
id: '',
|
||||
image: '',
|
||||
tag: '',
|
||||
tagType: 'default',
|
||||
name: '',
|
||||
duration: '',
|
||||
level: '',
|
||||
participants: 0,
|
||||
rawData: null
|
||||
})
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 320,
|
||||
description: '卡片宽度,单位 rpx'
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: null,
|
||||
description: '卡片高度,单位 rpx,不传则自适应'
|
||||
},
|
||||
imageHeight: {
|
||||
type: [Number, String],
|
||||
default: 280,
|
||||
description: '图片区域高度,单位 rpx'
|
||||
},
|
||||
borderRadius: {
|
||||
type: [Number, String],
|
||||
default: 24,
|
||||
description: '卡片圆角,单位 rpx'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算卡片样式
|
||||
const cardStyle = computed(() => {
|
||||
const style = {}
|
||||
|
||||
if (props.width) {
|
||||
style.width = typeof props.width === 'number' ? `${props.width}rpx` : props.width
|
||||
}
|
||||
|
||||
if (props.height) {
|
||||
style.height = typeof props.height === 'number' ? `${props.height}rpx` : props.height
|
||||
}
|
||||
|
||||
if (props.borderRadius) {
|
||||
style.borderRadius = typeof props.borderRadius === 'number' ? `${props.borderRadius}rpx` : props.borderRadius
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
// 计算图片区域样式
|
||||
const imageStyle = computed(() => {
|
||||
const style = {}
|
||||
|
||||
if (props.imageHeight) {
|
||||
style.height = typeof props.imageHeight === 'number' ? `${props.imageHeight}rpx` : props.imageHeight
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
// 处理参与课程点击
|
||||
const handleJoinCourse = () => {
|
||||
uni.navigateTo({ url: `/pages/groupCourse/detail?id=${props.course.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.course-card {
|
||||
min-width: 320rpx;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
min-height: 400rpx;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 28rpx var(--shadow-blue-light);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.course-image {
|
||||
min-height: 280rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.course-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, rgba(45, 74, 90, 0.7) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.course-tag {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||
z-index: 2;
|
||||
&.hot {
|
||||
background: linear-gradient(135deg, #6BA8C0, #8CC5D5);
|
||||
}
|
||||
&.new {
|
||||
background: linear-gradient(135deg, #6DB5C8, #90CEDD);
|
||||
}
|
||||
&.free {
|
||||
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||
}
|
||||
&.full {
|
||||
background: linear-gradient(135deg, #A0B8C8, #B8CCD8);
|
||||
}
|
||||
&.ended {
|
||||
background: linear-gradient(135deg, #B0C0CC, #C4D2DC);
|
||||
}
|
||||
&.default {
|
||||
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||
}
|
||||
}
|
||||
|
||||
.course-info {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
font-size: 20rpx;
|
||||
image{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 25rpx;
|
||||
height: 25rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.course-footer {
|
||||
padding: 16rpx 10rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.participants {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--tabbar-text-inactive);
|
||||
}
|
||||
|
||||
.fire-icon {
|
||||
font-size: 24rpx;
|
||||
image{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.join-btn {
|
||||
padding: 12rpx 28rpx;
|
||||
background: rgba(130, 220, 130, 0.9);
|
||||
border: none;
|
||||
border-radius: 9999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 6rpx 16rpx rgba(130, 220, 130, 0.35);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<view class="tab-page__header">
|
||||
<view v-if="showBack" :class="['tab-page__back-btn', { 'tab-page__back-btn--animate': isAnimating }]" @tap="goBack">
|
||||
<text class="tab-page__back-icon">‹</text>
|
||||
</view>
|
||||
<view class="tab-page__title-wrap">
|
||||
<text class="tab-page__title">{{ title }}</text>
|
||||
<text class="tab-page__subtitle">{{ subtitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '课程'
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '精品团课 · 私教 · 线上课'
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const isAnimating = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.showBack) {
|
||||
isAnimating.value = true
|
||||
}
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-page__back-btn {
|
||||
width: 48rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16rpx;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
border-radius: 16rpx;
|
||||
opacity: 0;
|
||||
transform: translateX(120rpx);
|
||||
transition: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tab-page__back-btn--animate {
|
||||
animation: slideInFromRight 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(120rpx);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-page__back-icon {
|
||||
font-size: 36rpx;
|
||||
color: $text-dark;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-page__title-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: $text-muted;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- 区域标题 -->
|
||||
<text class="section-title">推荐课程</text>
|
||||
<!-- 查看更多按钮 -->
|
||||
<view class="view-more">
|
||||
<view class="view-more" @tap="goMore">
|
||||
<text>查看更多</text>
|
||||
<text class="arrow">
|
||||
<uni-icons type="right" size="20" color="#8CA0B0"/>
|
||||
@@ -19,57 +19,12 @@
|
||||
<!-- 课程列表 -->
|
||||
<view class="courses-list">
|
||||
<!-- 课程卡片 -->
|
||||
<view
|
||||
<CourseCard
|
||||
v-for="(course, index) in courses"
|
||||
:key="course.id || index"
|
||||
class="course-card"
|
||||
>
|
||||
<!-- 课程图片区域 -->
|
||||
<view class="course-image">
|
||||
<!-- 课程封面图片 -->
|
||||
<image :src="course.image" mode="aspectFill" class="img" />
|
||||
<!-- 图片渐变遮罩 -->
|
||||
<view class="course-overlay"></view>
|
||||
<!-- 课程标签 -->
|
||||
<text :class="['course-tag', course.tagType]">{{ course.tag }}</text>
|
||||
<!-- 课程信息区域 -->
|
||||
<view class="course-info">
|
||||
<!-- 课程名称 -->
|
||||
<text class="course-name">{{ course.name }}</text>
|
||||
<!-- 课程元信息(时长、难度) -->
|
||||
<view class="course-meta">
|
||||
<!-- 时长信息 -->
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/time.png"/>
|
||||
</text>
|
||||
<text>{{ course.duration }}</text>
|
||||
</view>
|
||||
<!-- 难度信息 -->
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/intensity.png"/>
|
||||
</text>
|
||||
<text>{{ course.level }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 课程底部区域 -->
|
||||
<view class="course-footer">
|
||||
<!-- 参与人数信息 -->
|
||||
<view class="participants">
|
||||
<text class="fire-icon">
|
||||
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/hot.png"/>
|
||||
</text>
|
||||
<text>{{ course.participants }}人参与</text>
|
||||
</view>
|
||||
<!-- 去参与按钮 -->
|
||||
<view class="join-btn" @click="handleJoinCourse(course)">
|
||||
<text>去参与</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
:course="course"
|
||||
@join="handleJoinCourse"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -78,6 +33,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getGroupCoursePage } from '@/api/main.js'
|
||||
import CourseCard from './CourseCard.vue'
|
||||
|
||||
// 测试开关:设置为 true 时使用假数据,false 时使用真实API数据
|
||||
const USE_MOCK_DATA = true
|
||||
@@ -181,6 +137,10 @@ const handleJoinCourse = (course) => {
|
||||
}
|
||||
|
||||
onMounted(() => { fetchRecommendCourses() })
|
||||
|
||||
function goMore(){
|
||||
uni.navigateTo({ url: '/pages/recommendCourses/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -224,147 +184,4 @@ onMounted(() => { fetchRecommendCourses() })
|
||||
display: inline-flex;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.course-card {
|
||||
width: 320rpx;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 28rpx var(--shadow-blue-light);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.course-image {
|
||||
height: 280rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.course-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, rgba(45, 74, 90, 0.7) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.course-tag {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||
z-index: 2;
|
||||
&.hot {
|
||||
background: linear-gradient(135deg, #6BA8C0, #8CC5D5);
|
||||
}
|
||||
&.new {
|
||||
background: linear-gradient(135deg, #6DB5C8, #90CEDD);
|
||||
}
|
||||
&.free {
|
||||
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||
}
|
||||
&.full {
|
||||
background: linear-gradient(135deg, #A0B8C8, #B8CCD8);
|
||||
}
|
||||
&.ended {
|
||||
background: linear-gradient(135deg, #B0C0CC, #C4D2DC);
|
||||
}
|
||||
&.default {
|
||||
background: linear-gradient(135deg, #7AB5CC, #9CCFDF);
|
||||
}
|
||||
}
|
||||
|
||||
.course-info {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
font-size: 20rpx;
|
||||
image{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 25rpx;
|
||||
height: 25rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.course-footer {
|
||||
padding: 16rpx 10rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.participants {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--tabbar-text-inactive);
|
||||
}
|
||||
|
||||
.fire-icon {
|
||||
font-size: 24rpx;
|
||||
image{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.join-btn {
|
||||
padding: 12rpx 28rpx;
|
||||
background: rgba(130, 220, 130, 0.9);
|
||||
border: none;
|
||||
border-radius: 9999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 6rpx 16rpx rgba(130, 220, 130, 0.35);
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +1,27 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { groupCourseService } from '@/request_api/groupCourse.mock.js'
|
||||
import { getGroupCoursePage, getTypeLabels } from '@/api/groupCourse.js'
|
||||
|
||||
export function useGroupCourseList() {
|
||||
// 分页相关
|
||||
const pageNum = ref(1)
|
||||
const pageNum = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const totalPages = ref(0)
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
|
||||
// 团课列表数据
|
||||
const courseList = ref([])
|
||||
|
||||
// 搜索相关
|
||||
const searchKeyword = ref('')
|
||||
const hotKeywords = ref(['燃脂', '瑜伽', '单车', '普拉提', '高强度'])
|
||||
|
||||
// 排序相关
|
||||
const sortOptions = ref([
|
||||
{ label: '默认排序', value: 'default' },
|
||||
{ label: '价格从低到高', value: 'priceAsc' },
|
||||
{ label: '价格从高到低', value: 'priceDesc' },
|
||||
{ label: '剩余名额最多', value: 'spotsDesc' }
|
||||
{ label: '默认排序', value: 'id', order: 'asc' },
|
||||
{ label: '价格从低到高', value: 'storedValueAmount', order: 'asc' },
|
||||
{ label: '价格从高到低', value: 'storedValueAmount', order: 'desc' },
|
||||
{ label: '剩余名额最多', value: 'currentMembers', order: 'asc' }
|
||||
])
|
||||
const sortIndex = ref(0)
|
||||
|
||||
// 时间段选择相关
|
||||
const timePeriodOptions = ref([
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '早上 (6-12 点)', value: 'morning', startHour: 6, endHour: 12 },
|
||||
@@ -35,24 +30,20 @@ export function useGroupCourseList() {
|
||||
])
|
||||
const timePeriodIndex = ref(0)
|
||||
|
||||
// 时间筛选相关
|
||||
const showTimePicker = ref(false)
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const timeRangeText = ref('')
|
||||
|
||||
// 筛选后的课程列表
|
||||
const filteredCourseList = computed(() => {
|
||||
let result = [...courseList.value]
|
||||
|
||||
// 关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
result = result.filter(course =>
|
||||
course.courseName.includes(searchKeyword.value)
|
||||
)
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startDate.value || endDate.value) {
|
||||
result = result.filter(course => {
|
||||
const courseDate = course.startTime.split('T')[0]
|
||||
@@ -62,7 +53,6 @@ export function useGroupCourseList() {
|
||||
})
|
||||
}
|
||||
|
||||
// 时间段筛选
|
||||
const timePeriod = timePeriodOptions.value[timePeriodIndex.value]
|
||||
if (timePeriod.value !== 'all') {
|
||||
result = result.filter(course => {
|
||||
@@ -71,20 +61,18 @@ export function useGroupCourseList() {
|
||||
})
|
||||
}
|
||||
|
||||
// 排序
|
||||
const sortType = sortOptions.value[sortIndex.value].value
|
||||
if (sortType === 'priceAsc') {
|
||||
result.sort((a, b) => (a.storedValueAmount || a.pointCardAmount) - (b.storedValueAmount || b.pointCardAmount))
|
||||
} else if (sortType === 'priceDesc') {
|
||||
result.sort((a, b) => (b.storedValueAmount || b.pointCardAmount) - (a.storedValueAmount || a.pointCardAmount))
|
||||
} else if (sortType === 'spotsDesc') {
|
||||
result.sort((a, b) => (b.maxMembers - b.currentMembers) - (a.maxMembers - a.currentMembers))
|
||||
const sortType = sortOptions.value[sortIndex.value]
|
||||
if (sortType.value !== 'id' || sortType.order !== 'asc') {
|
||||
result.sort((a, b) => {
|
||||
const valA = a[sortType.value] || 0
|
||||
const valB = b[sortType.value] || 0
|
||||
return sortType.order === 'asc' ? valA - valB : valB - valA
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 获取所有搜索参数
|
||||
const getAllSearchParams = (searchBarRef, filterSectionRef, timePeriodRef, timeRangePickerRef) => {
|
||||
const searchParams = searchBarRef?.getSearchParams?.() || { keyword: searchKeyword.value }
|
||||
const filterParams = filterSectionRef?.getFilterParams?.() || { sortType: sortOptions.value[sortIndex.value].value }
|
||||
@@ -102,27 +90,24 @@ export function useGroupCourseList() {
|
||||
return allParams
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params) => {
|
||||
console.log('[useGroupCourseList] 搜索触发:', params)
|
||||
uni.showToast({
|
||||
title: params.keyword ? `搜索:${params.keyword}` : '请输入关键词',
|
||||
icon: 'none'
|
||||
})
|
||||
searchKeyword.value = params.keyword || ''
|
||||
pageNum.value = 0
|
||||
fetchCourseList()
|
||||
}
|
||||
|
||||
// 时间段变化处理
|
||||
const onTimePeriodChange = (option) => {
|
||||
console.log('[useGroupCourseList] 时间段选择:', option)
|
||||
}
|
||||
|
||||
// 时间范围确认处理
|
||||
const onTimeRangeConfirm = (params) => {
|
||||
console.log('[useGroupCourseList] 时间范围确认:', params)
|
||||
startDate.value = params.startDate || ''
|
||||
endDate.value = params.endDate || ''
|
||||
timeRangeText.value = params.timeRangeText
|
||||
}
|
||||
|
||||
// 预约处理
|
||||
const handleBooking = (course) => {
|
||||
console.log('[useGroupCourseList] 预约课程:', course)
|
||||
uni.showToast({
|
||||
@@ -131,7 +116,6 @@ export function useGroupCourseList() {
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转详情
|
||||
const goDetail = (courseId) => {
|
||||
console.log('[useGroupCourseList] 跳转到课程详情:', courseId)
|
||||
uni.navigateTo({
|
||||
@@ -139,20 +123,33 @@ export function useGroupCourseList() {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取团课列表
|
||||
const fetchCourseList = async (isLoadMore = false) => {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const result = await groupCourseService.getList({
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value
|
||||
const sortOption = sortOptions.value[sortIndex.value]
|
||||
console.log('[useGroupCourseList] 请求参数:', {
|
||||
page: isLoadMore ? pageNum.value + 1 : pageNum.value,
|
||||
size: pageSize.value,
|
||||
sort: sortOption.value,
|
||||
order: sortOption.order,
|
||||
keyword: searchKeyword.value
|
||||
})
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
const { list, total: totalCount, pageNum: currentPage, totalPages: pages } = result.data
|
||||
const result = await getGroupCoursePage({
|
||||
page: isLoadMore ? pageNum.value + 1 : pageNum.value,
|
||||
size: pageSize.value,
|
||||
sort: sortOption.value,
|
||||
order: sortOption.order,
|
||||
keyword: searchKeyword.value
|
||||
})
|
||||
|
||||
console.log('[useGroupCourseList] 响应结果:', JSON.stringify(result, null, 2))
|
||||
|
||||
if (result && result.content) {
|
||||
const { content: list, totalElements: totalCount, currentPage, totalPages: pages } = result
|
||||
|
||||
if (isLoadMore) {
|
||||
courseList.value = [...courseList.value, ...list]
|
||||
@@ -163,7 +160,7 @@ export function useGroupCourseList() {
|
||||
total.value = totalCount
|
||||
pageNum.value = currentPage
|
||||
totalPages.value = pages
|
||||
hasMore.value = pageNum.value < totalPages.value
|
||||
hasMore.value = currentPage < pages - 1
|
||||
|
||||
console.log('[useGroupCourseList] 团课列表获取成功:', {
|
||||
total: total.value,
|
||||
@@ -172,29 +169,41 @@ export function useGroupCourseList() {
|
||||
hasMore: hasMore.value
|
||||
})
|
||||
} else {
|
||||
console.error('[useGroupCourseList] 获取团课列表失败:', result.message)
|
||||
console.error('[useGroupCourseList] 获取团课列表失败:', {
|
||||
result: result,
|
||||
message: result?.message || '未知错误',
|
||||
code: result?.code,
|
||||
success: result?.success
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[useGroupCourseList] 获取团课列表异常:', error)
|
||||
console.error('[useGroupCourseList] 获取团课列表异常 - 错误详情:', {
|
||||
error: error,
|
||||
message: error?.message || '无错误信息',
|
||||
code: error?.code,
|
||||
statusCode: error?.statusCode,
|
||||
response: error?.response,
|
||||
stack: error?.stack
|
||||
})
|
||||
uni.showToast({
|
||||
title: `获取课程列表失败: ${error?.message || '网络错误'}`,
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (!hasMore.value || loading.value) return
|
||||
pageNum.value++
|
||||
fetchCourseList(true)
|
||||
}
|
||||
|
||||
// 滚动到底部触发
|
||||
const onScrollToLower = () => {
|
||||
loadMore()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
pageNum,
|
||||
pageSize,
|
||||
total,
|
||||
@@ -214,7 +223,6 @@ export function useGroupCourseList() {
|
||||
timeRangeText,
|
||||
filteredCourseList,
|
||||
|
||||
// 方法
|
||||
getAllSearchParams,
|
||||
handleSearch,
|
||||
onTimePeriodChange,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
{
|
||||
"path": "pages/course/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "课程",
|
||||
"app-plus": {
|
||||
"animationType": "fade-in",
|
||||
@@ -244,6 +245,7 @@
|
||||
{
|
||||
"path": "pages/groupCourse/list",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "团课列表"
|
||||
}
|
||||
},
|
||||
@@ -266,6 +268,13 @@
|
||||
"navigationBarTitleText": "搜索课程"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/recommendCourses/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "推荐课程",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "components/global/GlobalLoading",
|
||||
"style": {
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
<!-- 滚动内容区域 -->
|
||||
<scroll-view scroll-y="false" class="scroll-container">
|
||||
<view class="tab-page">
|
||||
<view class="tab-page__header">
|
||||
<text class="tab-page__title">课程</text>
|
||||
<text class="tab-page__subtitle">精品团课 · 私教 · 线上课</text>
|
||||
</view>
|
||||
<PageHeader title="课程" subtitle="精品团课 · 私教 · 线上课" />
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading" class="skeleton-container">
|
||||
@@ -44,6 +41,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import RecommendCourses from '@/components/index/RecommendCourses.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
|
||||
|
||||
@@ -136,25 +134,10 @@ function goMyCourses() {
|
||||
padding-bottom: 160rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.tab-page__header {
|
||||
padding: 48rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
.tab-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.tab-page__actions {
|
||||
display: flex;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="course-detail-page">
|
||||
<!-- 顶部导航 -->
|
||||
<MemberInfoSubNav :title="course.courseName || '课程详情'" @back="goBack" />
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader title="课程详情" subtitle="团课预约" :show-back="true" />
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading" class="skeleton">
|
||||
@@ -102,6 +102,27 @@
|
||||
<text class="meta-text">{{ course.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 课程标签 -->
|
||||
<view class="course-tags">
|
||||
<view class="tags-label">
|
||||
<uni-icons type="tag" size="20" color="#8A99B4" />
|
||||
<text>课程标签</text>
|
||||
</view>
|
||||
<view class="tags-list">
|
||||
<view
|
||||
v-for="label in courseLabels"
|
||||
:key="label.id"
|
||||
class="tag-item"
|
||||
:style="{ background: `${label.color}15`, color: label.color }"
|
||||
>
|
||||
<text>{{ label.labelName }}</text>
|
||||
</view>
|
||||
<view v-if="courseLabels.length === 0" class="no-tags">
|
||||
<text>暂无标签</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 课程详情卡片 -->
|
||||
@@ -138,39 +159,6 @@
|
||||
</view>
|
||||
<text class="info-value">ID: {{ course.coachId }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<view class="info-label">
|
||||
<uni-icons type="credit-card" size="24" color="#5E6F8D" />
|
||||
<text>支付方式</text>
|
||||
</view>
|
||||
<view class="payment-options">
|
||||
<view
|
||||
v-if="course.storedValueAmount > 0"
|
||||
:class="['payment-item', { active: selectedPayment === 'storedValue' || (!selectedPayment && course.pointCardAmount === 0) }]"
|
||||
@click="selectPayment('storedValue')"
|
||||
>
|
||||
<text class="payment-label">储值卡</text>
|
||||
<text class="payment-value">¥{{ course.storedValueAmount }}</text>
|
||||
<view v-if="selectedPayment === 'storedValue' || (!selectedPayment && course.pointCardAmount === 0)" class="payment-check">
|
||||
<uni-icons type="checkmark" size="20" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="course.pointCardAmount > 0"
|
||||
:class="['payment-item', { active: selectedPayment === 'pointCard' || (!selectedPayment && course.storedValueAmount === 0) }]"
|
||||
@click="selectPayment('pointCard')"
|
||||
>
|
||||
<text class="payment-label">次卡</text>
|
||||
<text class="payment-value">{{ course.pointCardAmount }}次</text>
|
||||
<view v-if="selectedPayment === 'pointCard' || (!selectedPayment && course.storedValueAmount === 0)" class="payment-check">
|
||||
<uni-icons type="checkmark" size="20" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="course.storedValueAmount === 0 && course.pointCardAmount === 0" class="payment-item free">
|
||||
<text class="payment-value">免费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预约须知 -->
|
||||
@@ -201,16 +189,40 @@
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="price-display">
|
||||
<text v-if="selectedPrice.type === 'storedValue'" class="price">
|
||||
<text class="currency">¥</text>{{ selectedPrice.amount }}
|
||||
</text>
|
||||
<text v-else-if="selectedPrice.type === 'pointCard'" class="price points">
|
||||
<uni-icons type="shop" size="20" color="#FF6B35" />
|
||||
<text>{{ selectedPrice.amount }}次</text>
|
||||
</text>
|
||||
<text v-else class="price free">免费</text>
|
||||
<!-- 左侧:支付方式选择(包含价格) -->
|
||||
<view v-if="hasMultiplePayment" class="payment-card">
|
||||
<!-- 左侧:滑块支付选择 -->
|
||||
<view class="payment-slider" @click="togglePayment">
|
||||
<view class="slider-bg" :class="{ right: selectedPayment === 'pointCard' }"></view>
|
||||
<view class="tab-item" :class="{ active: selectedPayment === 'storedValue' }">
|
||||
<text class="tab-label">储值卡</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: selectedPayment === 'pointCard' }">
|
||||
<text class="tab-label">次卡</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 右侧:当前选中价格 -->
|
||||
<view class="payment-price">
|
||||
<text class="price" :class="selectedPrice.type">
|
||||
<text class="currency">¥</text><text class="amount">{{ course.storedValueAmount }}</text>
|
||||
<text class="point-text"><text class="point-icon">⚡</text>{{ course.pointCardAmount }}次</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 免费课程或单一支付方式 -->
|
||||
<view v-else class="price-display">
|
||||
<text v-if="course.storedValueAmount === 0 && course.pointCardAmount === 0" class="price free">免费</text>
|
||||
<text v-else-if="course.storedValueAmount > 0" class="price">
|
||||
<text class="currency">¥</text>{{ course.storedValueAmount }}
|
||||
</text>
|
||||
<text v-else class="price">
|
||||
<text class="point-icon">⚡</text>
|
||||
<text>{{ course.pointCardAmount }}次</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧:预约按钮 -->
|
||||
<view :class="['booking-btn', { disabled: !canBook }]" @click="handleBooking">
|
||||
<text>{{ canBook ? '立即预约' : (course.currentMembers >= course.maxMembers ? '已满员' : statusText) }}</text>
|
||||
</view>
|
||||
@@ -222,7 +234,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { groupCourseService } from '@/request_api/groupCourse.mock.js'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
@@ -245,8 +257,8 @@ const course = ref({
|
||||
storedValueAmount: 0
|
||||
})
|
||||
|
||||
// 选中的支付方式 ('storedValue' | 'pointCard' | 'free')
|
||||
const selectedPayment = ref('')
|
||||
// 课程标签数据
|
||||
const courseLabels = ref([])
|
||||
|
||||
// 课程状态文本
|
||||
const statusText = computed(() => {
|
||||
@@ -296,6 +308,9 @@ const hasMultiplePayment = computed(() => {
|
||||
return course.value.storedValueAmount > 0 && course.value.pointCardAmount > 0
|
||||
})
|
||||
|
||||
// 当前选中的支付方式
|
||||
const selectedPayment = ref('storedValue')
|
||||
|
||||
// 当前选中的支付方式价格显示
|
||||
const selectedPrice = computed(() => {
|
||||
if (selectedPayment.value === 'storedValue') {
|
||||
@@ -313,11 +328,16 @@ const selectedPrice = computed(() => {
|
||||
return { type: 'free', amount: 0 }
|
||||
})
|
||||
|
||||
// 选择支付方式
|
||||
// 选择/切换支付方式
|
||||
const selectPayment = (type) => {
|
||||
selectedPayment.value = type
|
||||
}
|
||||
|
||||
// 切换支付方式(滑块点击)
|
||||
const togglePayment = () => {
|
||||
selectedPayment.value = selectedPayment.value === 'storedValue' ? 'pointCard' : 'storedValue'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
@@ -348,11 +368,6 @@ const formatDuration = (startStr, endStr) => {
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 预约处理
|
||||
const handleBooking = () => {
|
||||
if (!canBook.value) return
|
||||
@@ -393,6 +408,9 @@ const fetchCourseDetail = async (id) => {
|
||||
const result = await groupCourseService.getDetail(id)
|
||||
course.value = result
|
||||
console.log('[detail.vue] 课程详情获取成功:', course.value)
|
||||
|
||||
// 获取课程类型标签
|
||||
await fetchCourseLabels(course.value.courseType)
|
||||
} catch (error) {
|
||||
console.error('[detail.vue] 获取课程详情失败:', error)
|
||||
uni.showToast({
|
||||
@@ -404,6 +422,19 @@ const fetchCourseDetail = async (id) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取课程标签
|
||||
const fetchCourseLabels = async (courseType) => {
|
||||
try {
|
||||
const result = await groupCourseService.getLabelsByCourseType(courseType)
|
||||
if (result.code === 0) {
|
||||
courseLabels.value = result.data
|
||||
}
|
||||
console.log('[detail.vue] 课程标签获取成功:', courseLabels.value)
|
||||
} catch (error) {
|
||||
console.error('[detail.vue] 获取课程标签失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页面挂载时获取课程详情
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
@@ -550,6 +581,42 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 课程标签 */
|
||||
.course-tags {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid #F3F4F6;
|
||||
}
|
||||
|
||||
.tags-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #6B7280;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-tags {
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 详情卡片 */
|
||||
.detail-card {
|
||||
background: #ffffff;
|
||||
@@ -719,43 +786,162 @@ onMounted(() => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
padding-bottom: calc(16rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.price-display {
|
||||
/* 支付卡片(包含选择器和价格) */
|
||||
.payment-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #FFF7F5 0%, #FFF0EC 100%);
|
||||
border-radius: 52rpx;
|
||||
padding: 8rpx;
|
||||
height: 88rpx;
|
||||
flex: 1;
|
||||
padding-left: 20rpx;
|
||||
min-width: 0;
|
||||
border: 2rpx solid rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 40rpx;
|
||||
/* 支付滑块选择器 */
|
||||
.payment-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #F5F7FA;
|
||||
border-radius: 44rpx;
|
||||
padding: 6rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
/* 滑动背景指示器 */
|
||||
.payment-slider .slider-bg {
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
left: 6rpx;
|
||||
width: calc(50% - 6rpx);
|
||||
height: calc(100% - 12rpx);
|
||||
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||
border-radius: 38rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.payment-slider .slider-bg.right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.payment-slider .tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
border-radius: 38rpx;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.payment-slider .tab-item.active .tab-label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.payment-slider .tab-label {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #6B7280;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 支付卡片中的价格 */
|
||||
.payment-price {
|
||||
min-width: 140rpx;
|
||||
padding: 0 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.payment-price .price {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #FF6B35;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
|
||||
.currency {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.points {
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
&.free {
|
||||
color: #10B981;
|
||||
}
|
||||
}
|
||||
white-space: nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.payment-price .price .currency,
|
||||
.payment-price .price .amount,
|
||||
.payment-price .price .point-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-price .price.storedValue .currency,
|
||||
.payment-price .price.storedValue .amount {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.payment-price .price.pointCard .point-text {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.payment-price .currency {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.payment-price .point-icon {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* 价格展示(单一支付方式或免费) */
|
||||
.price-display {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.price-display .price {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.price-display .price.free {
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.price-display .price:not(.free) {
|
||||
color: #FF6B35;
|
||||
}
|
||||
|
||||
.price-display .currency {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-display .point-icon {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 预约按钮 */
|
||||
.booking-btn {
|
||||
padding: 24rpx 64rpx;
|
||||
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
|
||||
@@ -764,18 +950,13 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.3);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.2);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: linear-gradient(135deg, #F3F4F6 0%, #E5E7EB 100%);
|
||||
color: #9CA3AF;
|
||||
.booking-btn.disabled {
|
||||
background: linear-gradient(135deg, #D1D5DB 0%, #9CA3AF 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<view class="group-course-page">
|
||||
<!-- 页面头部 -->
|
||||
<PageHeader title="团课" subtitle="精品团课 · 专业教练 · 小班教学" :show-back="true" />
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<view class="search-section">
|
||||
<!-- 搜索框组件 -->
|
||||
@@ -39,6 +42,7 @@
|
||||
v-for="course in filteredCourseList"
|
||||
:key="course.id"
|
||||
:course="course"
|
||||
:courseTypeLabels="getCourseLabels(course)"
|
||||
@booking="handleBooking"
|
||||
@detail="goDetail"
|
||||
></GroupCourseCard>
|
||||
@@ -66,14 +70,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import GroupCourseCard from '@/components/groupCourse/CourseCard.vue'
|
||||
import SearchBar from '@/components/groupCourse/SearchBar.vue'
|
||||
import FilterSection from '@/components/groupCourse/FilterSection.vue'
|
||||
import TimePeriodSelector from '@/components/groupCourse/TimePeriodSelector.vue'
|
||||
import TimeRangePicker from '@/components/groupCourse/TimeRangePicker.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import { useGroupCourseList } from '@/composables/useGroupCourseList.js'
|
||||
import { getTypeLabels } from '@/api/groupCourse.js'
|
||||
|
||||
// 组件引用
|
||||
const searchBarRef = ref(null)
|
||||
@@ -81,6 +87,9 @@ const filterSectionRef = ref(null)
|
||||
const timePeriodRef = ref(null)
|
||||
const timeRangePickerRef = ref(null)
|
||||
|
||||
// 课程类型标签缓存
|
||||
const courseTypeLabelsCache = ref({})
|
||||
|
||||
// 使用组合式函数
|
||||
const {
|
||||
// 状态
|
||||
@@ -111,9 +120,11 @@ const {
|
||||
} = useGroupCourseList()
|
||||
|
||||
// 组件挂载时调用接口获取团课列表
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
console.log('[list.vue] 页面组件已挂载,开始获取团课列表')
|
||||
fetchCourseList()
|
||||
await fetchCourseList()
|
||||
// 获取所有团课的类型标签
|
||||
await fetchAllCourseTypeLabels()
|
||||
console.log('[list.vue] 可用的搜索参数获取方法:')
|
||||
console.log(' - searchBarRef.getSearchParams()')
|
||||
console.log(' - filterSectionRef.getFilterParams()')
|
||||
@@ -122,6 +133,27 @@ onMounted(() => {
|
||||
console.log(' - getAllSearchParams() 获取所有参数')
|
||||
})
|
||||
|
||||
const fetchAllCourseTypeLabels = async () => {
|
||||
const courseTypes = [...new Set(filteredCourseList.value.map(c => c.courseType))]
|
||||
for (const type of courseTypes) {
|
||||
if (!courseTypeLabelsCache.value[type]) {
|
||||
try {
|
||||
const result = await getTypeLabels(type)
|
||||
if (result) {
|
||||
courseTypeLabelsCache.value[type] = result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[list.vue] 获取类型标签失败,type=${type}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据课程获取对应的标签
|
||||
const getCourseLabels = (course) => {
|
||||
return courseTypeLabelsCache.value[course.courseType] || []
|
||||
}
|
||||
|
||||
// 暴露方法供外部调用
|
||||
defineExpose({
|
||||
getAllSearchParams: () => getAllSearchParams(searchBarRef.value, filterSectionRef.value, timePeriodRef.value, timeRangePickerRef.value),
|
||||
@@ -151,7 +183,7 @@ defineExpose({
|
||||
|
||||
/* 课程列表 */
|
||||
.course-list {
|
||||
height: calc(100vh - 380rpx);
|
||||
height: calc(100vh - 480rpx);
|
||||
padding: 24rpx 24rpx;
|
||||
padding-bottom: calc(160rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<!-- pages/recommendCourses/index.vue -->
|
||||
<template>
|
||||
<!-- 页面容器 -->
|
||||
<view class="recommend-page">
|
||||
<!-- 页面标题 -->
|
||||
<PageHeader title="推荐课程" subtitle="精选热门团课,等你来练" :show-back="true" />
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading" class="skeleton-container">
|
||||
<view class="skeleton-card" v-for="i in 3" :key="i">
|
||||
<view class="skeleton-img"></view>
|
||||
<view class="skeleton-text"></view>
|
||||
<view class="skeleton-text-short"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 课程列表 -->
|
||||
<scroll-view v-else class="courses-container" scroll-y="true">
|
||||
<view class="courses-grid">
|
||||
<CourseCard
|
||||
v-for="course in courses"
|
||||
:key="course.id"
|
||||
:course="course"
|
||||
@join="handleJoinCourse"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && courses.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无推荐课程</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<view class="bottom-placeholder"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import CourseCard from '@/components/index/CourseCard.vue'
|
||||
import PageHeader from '@/components/index/PageHeader.vue'
|
||||
import { groupCourseMockApi } from '@/request_api/groupCourse.mock.js'
|
||||
|
||||
// 课程列表数据
|
||||
const courses = ref([])
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
|
||||
// 课程类型映射(用于显示标签)
|
||||
const getCourseTypeName = (type) => {
|
||||
const typeMap = {
|
||||
'1': '瑜伽',
|
||||
'2': '搏击',
|
||||
'3': '塑形'
|
||||
}
|
||||
return typeMap[type] || '课程'
|
||||
}
|
||||
|
||||
// 根据课程信息获取标签文本
|
||||
const getTag = (course) => {
|
||||
if (course.currentMembers >= course.maxMembers) return '已满员'
|
||||
if (course.status === '2') return '已结束'
|
||||
if (course.status === '1') return '已取消'
|
||||
if (course.currentMembers / course.maxMembers >= 0.8) return '热门'
|
||||
return getCourseTypeName(course.courseType)
|
||||
}
|
||||
|
||||
// 根据课程信息获取标签样式类型
|
||||
const getTagType = (course) => {
|
||||
if (course.currentMembers >= course.maxMembers) return 'full'
|
||||
if (course.status === '2') return 'ended'
|
||||
if (course.status === '1') return 'ended'
|
||||
if (course.currentMembers / course.maxMembers >= 0.8) return 'hot'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
// 计算课程时长
|
||||
const calculateDuration = (startTime, endTime) => {
|
||||
if (!startTime || !endTime) return '60分钟'
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
const durationMinutes = Math.floor((end - start) / (1000 * 60))
|
||||
return `${durationMinutes}分钟`
|
||||
}
|
||||
|
||||
// 获取课程难度
|
||||
const getCourseLevel = (course) => {
|
||||
if (course.courseType === '2') return '中级'
|
||||
if (course.courseType === '3') return '高级'
|
||||
if (course.courseType === '1') return '初级'
|
||||
return '初级'
|
||||
}
|
||||
|
||||
// 处理图片URL
|
||||
const getImageUrl = (coverImage) => {
|
||||
if (!coverImage) return 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=400&q=80'
|
||||
if (coverImage.startsWith('http')) return coverImage
|
||||
return `https://your-domain.com${coverImage}`
|
||||
}
|
||||
|
||||
// 转换课程数据格式
|
||||
const transformCourseData = (course) => {
|
||||
return {
|
||||
id: course.id,
|
||||
image: getImageUrl(course.coverImage),
|
||||
tag: getTag(course),
|
||||
tagType: getTagType(course),
|
||||
name: course.courseName || '未知课程',
|
||||
duration: calculateDuration(course.startTime, course.endTime),
|
||||
level: getCourseLevel(course),
|
||||
participants: course.currentMembers || 0,
|
||||
rawData: course
|
||||
}
|
||||
}
|
||||
|
||||
// 获取推荐课程
|
||||
const fetchRecommendCourses = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 使用 mock API 获取数据
|
||||
const res = await groupCourseMockApi.getList({
|
||||
pageNum: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
if (res && res.data && res.data.list) {
|
||||
// 按参与人数排序,推荐热门课程
|
||||
const sortedList = res.data.list.sort((a, b) => b.currentMembers - a.currentMembers)
|
||||
courses.value = sortedList.map(course => transformCourseData(course))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取推荐课程失败', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理参与课程点击
|
||||
const handleJoinCourse = (course) => {
|
||||
if (course.rawData.status === '2') {
|
||||
uni.showToast({ title: '课程已结束', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (course.rawData.status === '1') {
|
||||
uni.showToast({ title: '课程已取消', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (course.rawData.currentMembers >= course.rawData.maxMembers) {
|
||||
uni.showToast({ title: '课程已满员', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 跳转到课程详情页
|
||||
uni.navigateTo({ url: `/pages/groupCourse/detail?id=${course.id}` })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRecommendCourses()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.recommend-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #E3F2FD 0%, #F5F5F5 100%);
|
||||
}
|
||||
|
||||
.courses-container {
|
||||
height: calc(100vh - 160rpx);
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
|
||||
.courses-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24rpx;
|
||||
justify-content: center;
|
||||
|
||||
// 两列网格布局,适度缩小卡片宽度
|
||||
> * {
|
||||
flex: 0 0 calc(45% - 12rpx);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #8CA0B0;
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.skeleton-container {
|
||||
padding: 32rpx 24rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
flex: 0 0 calc(50% - 10rpx);
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 28rpx rgba(45, 74, 90, 0.08);
|
||||
}
|
||||
|
||||
.skeleton-img {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
height: 32rpx;
|
||||
margin: 20rpx;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.skeleton-text-short {
|
||||
height: 24rpx;
|
||||
margin: 0 20rpx 20rpx;
|
||||
width: 60%;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
</style>
|
||||
@@ -301,6 +301,48 @@ const mockCourseList = [
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟团课类型标签数据
|
||||
const mockCourseTypeLabels = [
|
||||
{
|
||||
"id": "1",
|
||||
"createBy": null,
|
||||
"updateBy": null,
|
||||
"createdAt": "2026-06-11T11:29:07.859339",
|
||||
"updatedAt": "2026-06-11T11:29:07.859339",
|
||||
"deletedAt": null,
|
||||
"labelName": "高级进阶",
|
||||
"color": "#f5222d",
|
||||
"description": "适合高级学员"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"createBy": null,
|
||||
"updateBy": null,
|
||||
"createdAt": "2026-06-11T11:29:07.859339",
|
||||
"updatedAt": "2026-06-11T11:29:07.859339",
|
||||
"deletedAt": null,
|
||||
"labelName": "增肌强化",
|
||||
"color": "#13c2c2",
|
||||
"description": "有助于增肌强化"
|
||||
}
|
||||
]
|
||||
|
||||
// 团课类型与标签的关联关系
|
||||
const courseTypeLabelMap = {
|
||||
'1': [
|
||||
{ id: '1', labelName: '高级进阶', color: '#f5222d' },
|
||||
{ id: '2', labelName: '增肌强化', color: '#13c2c2' }
|
||||
],
|
||||
'2': [
|
||||
{ id: '3', labelName: '燃脂挑战', color: '#fa8c16' },
|
||||
{ id: '4', labelName: 'HIIT高强度', color: '#722ed1' }
|
||||
],
|
||||
'3': [
|
||||
{ id: '5', labelName: '塑形美体', color: '#eb2f96' },
|
||||
{ id: '6', labelName: '核心训练', color: '#52c41a' }
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟团课API(测试环境)
|
||||
* 接口签名与真实API保持一致
|
||||
@@ -401,6 +443,25 @@ export const groupCourseMockApi = {
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 基于类型ID获取标签列表
|
||||
* @param {string} courseType - 团课类型ID
|
||||
* @returns {Promise} - 标签列表数据
|
||||
*/
|
||||
getLabelsByCourseType: (courseType) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log('[groupCourse.mock.js] 模拟获取团课类型标签:', courseType)
|
||||
const labels = courseTypeLabelMap[courseType] || []
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: labels
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,33 +169,42 @@ export const request = (options) => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[API] 请求开始: ${method} ${BASE_URL + url}`)
|
||||
console.log(`[API] 请求参数:`, data)
|
||||
console.log(`[API] 请求头:`, requestHeader)
|
||||
|
||||
uni.request({
|
||||
url: BASE_URL + url,
|
||||
method: method,
|
||||
data: data,
|
||||
header: requestHeader,
|
||||
success: (res) => {
|
||||
console.log(`[API] 响应成功: ${method} ${BASE_URL + url}`)
|
||||
console.log(`[API] 响应状态码:`, res.statusCode)
|
||||
console.log(`[API] 响应头:`, res.header)
|
||||
console.log(`[API] 响应数据:`, JSON.stringify(res.data, null, 2))
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 如果启用缓存,保存响应数据
|
||||
if (cache && cacheKey && res.data) {
|
||||
setCache(cacheKey, res.data, cacheTime)
|
||||
}
|
||||
resolve(res.data)
|
||||
} else if (res.statusCode === 401) {
|
||||
// token过期,清除token并提示重新登录
|
||||
clearToken()
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
reject({ code: 401, message: '登录已过期' })
|
||||
reject({ code: 401, message: '登录已过期', statusCode: res.statusCode, response: res })
|
||||
} else {
|
||||
reject({ code: res.statusCode, message: res.data?.message || '请求失败' })
|
||||
console.error(`[API] 请求失败: ${res.statusCode} - ${res.data?.message || '未知错误'}`)
|
||||
reject({ code: res.statusCode, message: res.data?.message || '请求失败', statusCode: res.statusCode, response: res })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error(`[API] 请求失败: ${url}`, err)
|
||||
reject({ code: -1, message: '网络请求失败', error: err })
|
||||
console.error(`[API] 请求失败: ${method} ${BASE_URL + url}`)
|
||||
console.error(`[API] 错误详情:`, JSON.stringify(err, null, 2))
|
||||
reject({ code: -1, message: '网络请求失败', error: err, url: BASE_URL + url, method: method })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,11 +12,10 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
// 匹配所有 /api/ 开头的请求(排除静态文件)
|
||||
'/api': {
|
||||
target: 'http://192.168.5.15:8084', // 你的后端SpringBoot地址
|
||||
changeOrigin: true, // 开启跨域伪装
|
||||
// 只代理真正的后端API请求,排除 .js .vue 等静态文件
|
||||
target: 'https://www.gymmanage.xyz',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
bypass: function(req, res, proxyOptions) {
|
||||
if (req.url.indexOf('.js') !== -1 || req.url.indexOf('.vue') !== -1 ||
|
||||
req.url.indexOf('.css') !== -1 || req.url.indexOf('.json') !== -1) {
|
||||
|
||||
Reference in New Issue
Block a user