完善团课相关页面交互,完成团课列表页基础后端交互。(后端连接至服务器,版本为DEV分支版本)

This commit is contained in:
2026-06-13 17:11:36 +08:00
parent b345ceeb42
commit 96b8fd2534
16 changed files with 2760 additions and 384 deletions
+275 -94
View File
@@ -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;
}
/* 支付卡片(包含选择器和价格) */
.payment-card {
display: flex;
align-items: center;
background: linear-gradient(135deg, #FFF7F5 0%, #FFF0EC 100%);
border-radius: 52rpx;
padding: 8rpx;
height: 88rpx;
flex: 1;
min-width: 0;
border: 2rpx solid rgba(255, 107, 53, 0.15);
}
/* 支付滑块选择器 */
.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;
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;
padding-left: 20rpx;
.price {
font-size: 40rpx;
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;
}
}
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);
&: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;
box-shadow: none;
}
flex-shrink: 0;
}
.booking-btn.disabled {
background: linear-gradient(135deg, #D1D5DB 0%, #9CA3AF 100%);
color: #ffffff;
box-shadow: none;
}
/* 骨架屏样式 */