2025-08-21 10:36:17 +08:00

1599 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="course-detail-page">
<!-- 自定义导航栏 -->
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="goBack">
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">课程详情02</view>
<view class="nav-right" @click="goShare">
<uni-icons type="redo" size="24" color="#333"></uni-icons>
</view>
</view>
</view>
<!-- 页面内容 -->
<view class="page-content" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- 主横幅 -->
<view class="main-banner">
<image :src="courseDetail.index_img || '/static/lunbo_bg.png'" mode="aspectFill" class="banner-image"></image>
</view>
<!-- 课程信息 -->
<view class="course-info">
<view class="course-header">
<text class="course-title">{{ courseDetail.title || '肝硬化与重症肝病' }}</text>
<view class="course-meta">
<text class="lesson-count" v-if="courseDetail.study_num > 0">{{ courseDetail.study_num || 0 }}人学·</text>
<text class="lesson-count">{{ courseDetail.video_num || 0 }}节课·{{ courseDetail.status === 1 ? '已完结' : '进行中' }}</text>
<text class="lesson-count" v-if="courseDetail.fuli_num > 0 && courseDetail.special_type_name == '福利课堂'">·福利剩余{{ courseDetail.fuli_remaining || 0 }}</text>
</view>
</view>
<view class="course-tags">
<view class="tag-item" v-if="courseDetail.fuli_bon > 0">
<text class="tag-text">福利课堂</text>
</view>
<text class="reward-info" v-if="courseDetail.fuli_bon > 0">课程学完预期将返还{{ courseDetail.fuli_bon }}积分</text>
</view>
</view>
<!-- 限时优惠模块 -->
<view class="limited-offer" v-if="showLimitedOffer">
<view class="offer-left">
<view class="price-info">
<text class="current-price">¥{{ currentPriceYuan.toFixed(2) }}</text>
<text class="original-price" v-if="originalPriceYuan > 0">¥{{ originalPriceYuan.toFixed(2) }}</text>
</view>
<view class="offer-tag">
<text class="tag-text">限时优惠</text>
</view>
</view>
<view class="offer-right">
<text class="countdown-text">距离优惠结束还剩</text>
<view class="countdown-timer">
<text class="days">{{ countdown.days }}天</text>
<view class="time-segments">
<view class="time-box">{{ countdown.hours }}</view>
<text class="time-separator">:</text>
<view class="time-box">{{ countdown.minutes }}</view>
<text class="time-separator">:</text>
<view class="time-box">{{ countdown.seconds }}</view>
</view>
</view>
</view>
</view>
<!-- 永久优惠模块(样式同限时优惠,但无倒计时) -->
<view class="limited-offer permanent" v-if="showPermanentOffer">
<view class="offer-left">
<view class="price-info">
<text class="current-price">¥{{ currentPriceYuan.toFixed(2) }}</text>
<text class="original-price" v-if="originalPriceYuan > 0">¥{{ originalPriceYuan.toFixed(2) }}</text>
</view>
<view class="offer-tag">
<text class="tag-text">永久优惠</text>
</view>
</view>
</view>
<!-- 标签导航 -->
<view class="tab-nav">
<view class="tab-item" :class="{ active: activeTab === 'intro' }" @click="switchTab('intro')">
<text class="tab-text">课程介绍</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'catalog' }" @click="switchTab('catalog')">
<text class="tab-text">课程目录</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'reviews' }" @click="switchTab('reviews')" data-tab="reviews">
<text class="tab-text">评价({{ courseDetail.comment_num || 0 }})</text>
</view>
</view>
<!-- 课程介绍内容 -->
<view class="course-intro" v-if="activeTab === 'intro'">
<!-- 课程描述 -->
<view class="course-description" v-if="courseDetail.descs">
<view class="description-content" v-html="courseDetail.descs"></view>
</view>
</view>
<!-- 课程目录内容 -->
<view class="course-catalog" v-if="activeTab === 'catalog'">
<!-- 课程概览 -->
<view class="catalog-overview">
<view class="overview-item">
<text class="overview-label">总课时</text>
<text class="overview-value">{{ courseDetail.video_num || 0 }}节课</text>
</view>
<view class="overview-item">
<text class="overview-label">总时长</text>
<text class="overview-value">约{{ Math.ceil((courseDetail.video_num || 0) * 0.75) }}小时</text>
</view>
<view class="overview-item">
<text class="overview-label">学习进度</text>
<text class="overview-value">{{ courseDetail.study_status || 0 }}%</text>
</view>
</view>
<!-- 课程章节列表 -->
<view class="chapter-list">
<view class="chapter-item" v-for="(chapter, index) in chapterList" :key="index" @click="toggleChapter(index)">
<view class="chapter-header">
<view class="chapter-info">
<view class="chapter-title">
<uni-icons :type="chapter.expanded ? 'down' : 'right'" size="16" color="#666"></uni-icons>
<text class="chapter-name">{{ chapter.title }}</text>
</view>
<view class="chapter-meta">
<text class="lesson-count">{{ chapter.lessons.length }}节课</text>
<text class="duration">{{ chapter.duration }}</text>
</view>
</view>
<view class="chapter-status">
<text class="status-text" :class="chapter.status">{{ chapter.statusText }}</text>
</view>
</view>
<!-- 课时列表 -->
<view class="lesson-list" v-if="chapter.expanded">
<view class="lesson-item" v-for="(lesson, lessonIndex) in chapter.lessons" :key="lessonIndex" @click="goLesson(lesson)">
<view class="lesson-info">
<view class="lesson-title">
<uni-icons type="play-circle" size="20" color="#999"></uni-icons>
<text class="lesson-name">{{ lesson.title }}</text>
</view>
<view class="lesson-meta">
<text class="lesson-duration">{{ lesson.duration }}</text>
<text class="lesson-teacher">{{ lesson.teacher }}</text>
</view>
</view>
<view class="lesson-status">
<text class="status-text" :class="lesson.status">{{ lesson.statusText }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 评价内容 -->
<view class="course-reviews" v-if="activeTab === 'reviews'">
<view class="review-header">
<text class="review-title">课程评价</text>
<view class="review-actions-header">
<text class="review-count">共{{ reviewTotal || courseDetail.comment_num || 0 }}条评价</text>
<button class="write-review-btn" @click="goToReview" v-if="courseDetail.is_buy === 1">写评价</button>
</view>
</view>
<view class="review-list" v-if="courseDetail.comment_num > 0">
<view class="review-item" v-for="(review, index) in reviewList" :key="index">
<view class="review-user">
<image :src="review.avatar" mode="aspectFill" class="user-avatar"></image>
<view class="user-info">
<text class="user-name">{{ review.userName }}</text>
<view class="user-rating">
<uni-icons v-for="star in 5" :key="star"
:type="star <= review.rating ? 'star-filled' : 'star'"
size="16"
:color="star <= review.rating ? '#FFB800' : '#E0E0E0'">
</uni-icons>
</view>
</view>
<text class="review-time">{{ review.time }}</text>
</view>
<view class="review-content">
<text class="review-text">{{ review.content }}</text>
</view>
<!-- 回复按钮 -->
<view class="review-actions">
<view class="reply-btn" @click="showReplyInput(review.id)">
<uni-icons type="chat" size="16" color="#666"></uni-icons>
<text class="reply-text">回复</text>
</view>
</view>
<!-- 二级评论列表 -->
<view class="reply-list" v-if="review.replies && review.replies.length > 0">
<view class="reply-item" v-for="(reply, replyIndex) in review.replies" :key="reply.id">
<view class="reply-user">
<image :src="reply.avatar" mode="aspectFill" class="reply-avatar"></image>
<view class="reply-info">
<view class="reply-header">
<text class="reply-name">{{ reply.userName }}</text>
<text class="reply-time">{{ reply.time }}</text>
<text v-if="reply.isAuthor" class="author-tag">讲师</text>
</view>
<text class="reply-content">{{ reply.content }}</text>
</view>
</view>
</view>
</view>
<!-- 回复输入框 -->
<view class="reply-input-container" v-if="activeReplyId === review.id">
<view class="reply-input-wrapper">
<input
class="reply-input"
v-model="replyText"
placeholder="写下你的回复..."
:focus="activeReplyId === review.id"
/>
<view class="reply-input-actions">
<button class="cancel-btn" @click="cancelReply">取消</button>
<button class="submit-btn" @click="submitReply(review.id)">发送</button>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="reviewPage < reviewPages && reviewList.length > 0">
<button class="load-more-btn" @click="loadMoreReviews" :disabled="reviewLoading">
<text v-if="!reviewLoading">加载更多</text>
<text v-else>加载中...</text>
</button>
</view>
<!-- 已加载全部 -->
<view class="load-all" v-if="reviewPage >= reviewPages && reviewList.length > 0">
<text class="load-all-text">已加载全部评论</text>
</view>
</view>
<!-- 空评价状态 -->
<view class="empty-reviews" v-if="reviewList.length === 0 && !reviewLoading">
<view class="empty-icon">
<uni-icons type="chat" size="80" color="#E0E0E0"></uni-icons>
</view>
<text class="empty-text">暂无评价</text>
<text class="empty-desc">成为第一个评价的人吧</text>
</view>
<!-- 加载中状态 -->
<view class="loading-reviews" v-if="reviewLoading && reviewList.length === 0">
<view class="loading-icon">
<uni-icons type="spinner-cycle" size="80" color="#E0E0E0"></uni-icons>
</view>
<text class="loading-text">加载中...</text>
</view>
</view>
</view>
<!-- 底部购买栏 -->
<view class="bottom-bar">
<view class="price-section">
<text class="price">¥{{ currentPriceYuan.toFixed(2) }}</text>
</view>
<view class="buy-section">
<button class="buy-btn" @click="goBuy" v-if="courseDetail.is_buy === 0">{{ courseDetail.is_buy === 0 ? '立即购买' : '已购买' }}</button>
<button class="buy-btn" @click="goStudy" v-else style="background-color: #4CAF50;">开始学习</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import course_api from "@/api/course_api.js"
const courseId = ref(0)
onMounted(() => {
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
courseId.value = options.id
console.log('课程ID:', options.id)
})
// 响应式数据
const statusBarHeight = ref(0)
const navBarHeight = ref(88)
const activeTab = ref('intro') // 默认激活课程介绍标签
const courseDetail = ref({})
// 倒计时与价格展示
const countdown = ref({
days: 0,
hours: '00',
minutes: '00',
seconds: '00'
})
let countdownTimer = null // 倒计时定时器
const showLimitedOffer = ref(false)
const showPermanentOffer = ref(false)
const currentPriceYuan = ref(0)
const originalPriceYuan = ref(0)
// 课程目录数据
const chapterList = ref([])
// 回复相关状态
const activeReplyId = ref(null)
const replyText = ref('')
// 方法
const getSystemInfo = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
// #ifdef MP-WEIXIN
navBarHeight.value = systemInfo.statusBarHeight + 44
// #endif
// #ifdef APP-PLUS
navBarHeight.value = systemInfo.statusBarHeight + 44
// #endif
}
const startCountdown = (endTime) => {
if (!endTime) return
countdownTimer = setInterval(() => {
const now = new Date().getTime()
const distance = endTime - now
if (distance < 0) {
// 倒计时结束
clearInterval(countdownTimer)
countdown.value = {
days: 0,
hours: '00',
minutes: '00',
seconds: '00'
}
return
}
// 计算剩余时间
const days = Math.floor(distance / (1000 * 60 * 60 * 24))
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((distance % (1000 * 60)) / 1000)
// 更新倒计时数据
countdown.value = {
days: days,
hours: hours.toString().padStart(2, '0'),
minutes: minutes.toString().padStart(2, '0'),
seconds: seconds.toString().padStart(2, '0')
}
}, 1000)
}
const goBack = () => {
uni.navigateBack()
}
const goShare = () => {
uni.showToast({
title: '分享功能',
icon: 'none'
})
}
const goBuy = () => {
uni.redirectTo({
url:'/pages_course/course_payment/course_payment?id='+courseId.value
})
}
const goStudy = () => {
uni.showToast({
title: '开始学习',
icon: 'none'
})
}
const reviewList = ref([])
const reviewPage = ref(1)
const reviewTotal = ref(0)
const reviewPages = ref(1)
const reviewLoading = ref(false)
const getReviewList = () => {
if (reviewLoading.value) return
reviewLoading.value = true
console.log('获取评论列表', courseId.value, reviewPage.value)
course_api.listExcellencourseComment(courseId.value, reviewPage.value).then(res => {
console.log('评论列表数据:', res)
if (res.code == 200) {
const data = res.data
// 更新分页信息
reviewTotal.value = data.total || 0
reviewPages.value = data.pages || 1
// 处理评论数据
if (data.list && data.list.length > 0) {
// 如果是第一页,直接替换;否则追加
if (reviewPage.value === 1) {
reviewList.value = data.list.map(item => ({
id: item.id,
userName: item.user_name || '匿名用户',
avatar: '/static/icon_home_my_patient.png', // 默认头像
rating: Math.floor(item.star / 2), // 将10分制转换为5分制
time: formatDate(item.create_date),
content: item.comment || '',
replies: item.excellentcourse_comment_list ? item.excellentcourse_comment_list.map(reply => ({
id: reply.id,
userName: reply.user_name || '匿名用户',
avatar: '/static/icon_home_my_patient.png',
time: formatDate(reply.create_date),
content: reply.comment || '',
isAuthor: false
})) : []
}))
} else {
// 追加数据
const newReviews = data.list.map(item => ({
id: item.id,
userName: item.user_name || '匿名用户',
avatar: '/static/icon_home_my_patient.png',
rating: Math.floor(item.star / 2),
time: formatDate(item.create_date),
content: item.comment || '',
replies: item.excellentcourse_comment_list ? item.excellentcourse_comment_list.map(reply => ({
id: reply.id,
userName: reply.user_name || '匿名用户',
avatar: '/static/icon_home_my_patient.png',
time: formatDate(reply.create_date),
content: reply.comment || '',
isAuthor: false
})) : []
}))
reviewList.value.push(...newReviews)
}
}
} else {
uni.showToast({
title: res.msg || '获取评论失败',
icon: 'none'
})
}
}).catch(err => {
console.error('获取评论列表失败:', err)
uni.showToast({
title: '获取评论失败',
icon: 'none'
})
}).finally(() => {
reviewLoading.value = false
})
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
const now = new Date()
const diff = now - date
// 小于1分钟
if (diff < 60 * 1000) {
return '刚刚'
}
// 小于1小时
if (diff < 60 * 60 * 1000) {
return `${Math.floor(diff / (60 * 1000))}分钟前`
}
// 小于1天
if (diff < 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`
}
// 小于7天
if (diff < 7 * 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`
}
// 超过7天显示具体日期
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
} catch (e) {
return dateStr
}
}
// 加载更多评论
const loadMoreReviews = () => {
if (reviewPage.value < reviewPages.value && !reviewLoading.value) {
reviewPage.value++
getReviewList()
}
}
// 刷新评论列表
const refreshReviews = () => {
reviewPage.value = 1
reviewList.value = []
getReviewList()
}
const switchTab = (tab) => {
activeTab.value = tab
console.log('切换到标签:', tab)
if (tab == 'reviews') {
refreshReviews()
}
}
const toggleChapter = (index) => {
chapterList.value[index].expanded = !chapterList.value[index].expanded
console.log('切换章节:', chapterList.value[index].title)
}
const goLesson = (lesson) => {
console.log('进入课时:', lesson.title)
if (courseDetail.value.is_buy === 0) {
uni.showToast({
title: '请先购买课程',
icon: 'none'
})
} else {
// 跳转到视频播放页面
uni.navigateTo({
url: `/pages_course/video/video?vid=${lesson.vid}&title=${encodeURIComponent(lesson.title)}`
})
}
}
const showReplyInput = (reviewId) => {
activeReplyId.value = reviewId
replyText.value = ''
}
const cancelReply = () => {
activeReplyId.value = null
replyText.value = ''
}
const submitReply = (reviewId) => {
if (!replyText.value.trim()) {
uni.showToast({
title: '请输入回复内容',
icon: 'none'
})
return
}
// 找到对应的评价
const review = reviewList.value.find(item => item.id === reviewId)
if (review) {
// 添加新回复
const newReply = {
id: Date.now(),
userName: '我',
avatar: '/static/icon_home_my_patient.png',
time: new Date().toLocaleDateString(),
content: replyText.value.trim(),
isAuthor: false
}
if (!review.replies) {
review.replies = []
}
review.replies.push(newReply)
// 重置状态
activeReplyId.value = null
replyText.value = ''
uni.showToast({
title: '回复成功',
icon: 'success'
})
}
}
const goToReview = () => {
uni.navigateTo({
url: '/pages_course/course_review/course_review'
})
}
const getCourseDetail = () => {
course_api.excellencourseDetail(courseId.value).then(res => {
console.log('课程详情数据:', res)
if(res.code == 200){
const data = res.data
courseDetail.value = data
// 更新页面数据
updatePageData(data)
// 优惠价格与倒计时
const now = Date.now()
const beginTs = data.discount_begin_date ? new Date(data.discount_begin_date).getTime() : null
const endTs = data.discount_end_date ? new Date(data.discount_end_date).getTime() : null
const originalCents = data.account || 0
let finalCents = originalCents
showLimitedOffer.value = false
showPermanentOffer.value = false
if (data.discount_type === 1 && data.discount_price > 0) {
finalCents = data.discount_price
showPermanentOffer.value = true
} else if (data.discount_type === 2 && data.discount_price > 0) {
const inWindow = (!beginTs || beginTs <= now) && (!!endTs && endTs > now)
if (inWindow) {
finalCents = data.discount_price
showLimitedOffer.value = true
startCountdown(endTs)
}
}
currentPriceYuan.value = (finalCents || 0) / 100
originalPriceYuan.value = (originalCents || 0) / 100
} else {
uni.showToast({
title: res.msg || '获取课程详情失败',
icon: 'none'
})
}
}).catch(err => {
console.error('获取课程详情失败:', err)
uni.showToast({
title: '获取课程详情失败',
icon: 'none'
})
})
}
// 更新页面数据
const updatePageData = (data) => {
// 更新课程标题
if (data.title) {
// 更新页面标题
uni.setNavigationBarTitle({
title: data.title
})
}
// 更新课程目录数据
if (data.excellentcourse_first_title_list && data.excellentcourse_first_title_list.length > 0) {
chapterList.value = data.excellentcourse_first_title_list.map((chapter, index) => {
// 计算该章节的总时长
let totalDuration = 0
if (chapter.excellentcourse_second_title_list) {
chapter.excellentcourse_second_title_list.forEach(lesson => {
if (lesson.time) {
const timeParts = lesson.time.split(':')
if (timeParts.length === 2) {
totalDuration += parseInt(timeParts[0]) + parseInt(timeParts[1]) / 60
}
}
})
}
return {
id: chapter.id,
title: chapter.title,
duration: totalDuration > 0 ? `${Math.ceil(totalDuration)}分钟` : '约45分钟',
expanded: false,
status: data.is_buy === 1 ? 'unlocked' : 'locked',
statusText: data.is_buy === 1 ? '已解锁' : '未解锁',
lessons: chapter.excellentcourse_second_title_list ? chapter.excellentcourse_second_title_list.map((lesson, lessonIndex) => {
// 从标题中提取讲师姓名
let teacherName = '未知讲师'
if (lesson.title && lesson.title.includes('-')) {
teacherName = lesson.title.split('-')[0]
}
return {
id: lesson.id,
title: lesson.title,
duration: lesson.time || '未知时长',
teacher: teacherName,
status: (data.is_buy === 1 || data.type === 0) ? 'unlocked' : 'locked',
statusText: (data.is_buy === 1 || data.type === 0) ? (data.type === 0 ? '试播' : '已解锁') : '未解锁',
vid: lesson.vid,
type: lesson.type
}
}) : []
}
})
}
// 更新评价数量
if (data.comment_num !== undefined) {
// 评价数量已经在模板中通过响应式数据绑定无需手动更新DOM
console.log('评价数量:', data.comment_num)
}
// 更新学习状态
if (data.study_status !== undefined) {
// 可以根据学习状态更新UI
console.log('学习状态:', data.study_status)
}
// 更新价格信息
if (data.discount_price !== undefined && data.discount_price > 0) {
// 如果有优惠价格,更新价格显示
console.log('优惠价格:', data.discount_price)
}
// 更新福利信息
if (data.fuli_bon !== undefined) {
console.log('福利积分:', data.fuli_bon)
}
}
// 生命周期
onMounted(() => {
getSystemInfo()
startCountdown()
getCourseDetail()
// 初始化评论列表
getReviewList()
})
onUnmounted(() => {
// 清除定时器
if (countdownTimer) {
clearInterval(countdownTimer)
}
})
</script>
<style lang="scss">
.course-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
// 导航栏
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: #fff;
border-bottom: 1rpx solid #e5e5e5;
.nav-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
.nav-left, .nav-right {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FF4757;
}
}
}
.page-content {
background-color: #f5f5f5;
}
// 主横幅
.main-banner {
position: relative;
height: 400rpx;
overflow: hidden;
.banner-image {
width: 100%;
height: 100%;
}
.banner-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
.banner-title {
font-size: 48rpx;
color: #1976D2;
font-weight: bold;
text-shadow: 0 2rpx 4rpx rgba(255,255,255,0.8);
}
}
}
// 课程信息
.course-info {
background-color: #fff;
padding: 32rpx 24rpx;
.course-header {
margin-bottom: 24rpx;
.course-title {
display: block;
font-size: 36rpx;
color: #333;
font-weight: 600;
margin-bottom: 16rpx;
}
.course-meta {
.lesson-count {
font-size: 28rpx;
color: #666;
}
}
}
.course-tags {
display: flex;
align-items: center;
justify-content: space-between;
.tag-item {
.tag-text {
background-color: #FF4757;
color: #fff;
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 8rpx;
}
}
.reward-info {
font-size: 24rpx;
color: #666;
flex: 1;
margin-left: 24rpx;
}
}
}
// 限时优惠模块
.limited-offer {
background: linear-gradient(135deg, #FF6B6B, #FF8E53);
margin: 24rpx;
border-radius: 20rpx;
padding: 32rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 107, 0.3);
.offer-left {
flex: 2;
.price-info {
position: relative;
margin-bottom: 16rpx;
.current-price {
font-size: 48rpx;
color: #fff;
font-weight: bold;
margin-right: 16rpx;
}
.original-price {
font-size: 28rpx;
color: #fff;
text-decoration: line-through;
opacity: 0.8;
}
}
.offer-tag {
display: inline-block;
background-color: #fff;
border-radius: 12rpx;
padding: 8rpx 16rpx;
.tag-text {
font-size: 22rpx;
color: #FF6B6B;
font-weight: 600;
}
}
}
.offer-right {
flex: 1;
text-align: center;
.countdown-text {
display: block;
font-size: 24rpx;
color: #fff;
margin-bottom: 12rpx;
}
.countdown-timer {
.days {
display: block;
font-size: 28rpx;
color: #fff;
font-weight: 600;
margin-bottom: 8rpx;
}
.time-segments {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
.time-box {
background-color: #FF8E53;
color: #fff;
font-size: 24rpx;
font-weight: 600;
padding: 8rpx 12rpx;
border-radius: 8rpx;
min-width: 40rpx;
text-align: center;
}
.time-separator {
color: #fff;
font-size: 24rpx;
font-weight: 600;
}
}
}
}
}
// 永久优惠优化:去除右侧留白、更紧凑的左区布局
.limited-offer.permanent {
justify-content: flex-start;
.offer-left {
flex: initial;
display: flex;
align-items: center;
gap: 16rpx;
.price-info {
margin-bottom: 0;
}
}
.offer-right { display: none; }
}
// 标签导航
.tab-nav {
background-color: #fff;
border-bottom: 1rpx solid #e5e5e5;
display: flex;
.tab-item {
flex: 1;
padding: 24rpx 0;
text-align: center;
position: relative;
.tab-text {
font-size: 28rpx;
color: #666;
}
&.active {
.tab-text {
color: #FF4757;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #FF4757;
border-radius: 2rpx;
}
}
}
}
// 课程介绍内容
.course-intro {
background-color: #fff;
padding: 32rpx 24rpx;
.intro-banner {
background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 32rpx;
text-align: center;
.intro-title {
font-size: 32rpx;
color: #1976D2;
font-weight: 600;
}
}
.course-description {
margin-bottom: 32rpx;
.description-title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 16rpx;
}
.description-content {
font-size: 26rpx;
color: #666;
line-height: 1.6;
img {
max-width: 100%;
height: auto;
border-radius: 8rpx;
margin: 16rpx 0;
}
}
}
.instructors {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200rpx, 1fr));
gap: 32rpx;
.instructor-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.instructor-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-bottom: 16rpx;
border: 4rpx solid #f0f0f0;
}
.instructor-info {
.instructor-name {
display: block;
font-size: 26rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
word-break: break-all;
}
.instructor-hospital {
display: block;
font-size: 22rpx;
color: #666;
line-height: 1.3;
}
}
}
}
}
// 课程目录内容
.course-catalog {
background-color: #fff;
padding: 32rpx 24rpx;
.catalog-overview {
display: flex;
justify-content: space-around;
padding: 24rpx;
background: linear-gradient(135deg, #F8F9FA, #E9ECEF);
border-radius: 16rpx;
margin-bottom: 32rpx;
.overview-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.overview-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.overview-value {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
}
}
.chapter-list {
.chapter-item {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
.chapter-header {
padding: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #f0f0f0;
.chapter-info {
flex: 1;
.chapter-title {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.chapter-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-left: 12rpx;
}
}
.chapter-meta {
display: flex;
gap: 24rpx;
margin-left: 28rpx;
.lesson-count, .duration {
font-size: 24rpx;
color: #666;
}
}
}
.chapter-status {
.status-text {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
&.locked {
background-color: #F5F5F5;
color: #999;
}
&.unlocked {
background-color: #E8F5E8;
color: #4CAF50;
}
&.completed {
background-color: #E3F2FD;
color: #2196F3;
}
}
}
}
.lesson-list {
background-color: #FAFAFA;
.lesson-item {
padding: 20rpx 24rpx 20rpx 48rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.lesson-info {
flex: 1;
.lesson-title {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.lesson-name {
font-size: 26rpx;
color: #333;
margin-left: 12rpx;
}
}
.lesson-meta {
display: flex;
gap: 24rpx;
margin-left: 32rpx;
.lesson-duration, .lesson-teacher {
font-size: 22rpx;
color: #666;
}
}
}
.lesson-status {
.status-text {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 8rpx;
&.locked {
background-color: #F5F5F5;
color: #999;
}
&.unlocked {
background-color: #E8F5E8;
color: #4CAF50;
}
&.completed {
background-color: #E3F2FD;
color: #2196F3;
}
}
}
}
}
}
}
}
// 评价内容
.course-reviews {
background-color: #fff;
padding: 32rpx 24rpx;
.review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
.review-title {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
.review-actions-header {
display: flex;
align-items: center;
gap: 24rpx;
.review-count {
font-size: 26rpx;
color: #666;
}
.write-review-btn {
background-color: #FF4757;
color: #fff;
border: none;
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
}
}
}
.review-list {
.review-item {
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.review-user {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.user-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.user-info {
flex: 1;
.user-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.user-rating {
display: flex;
gap: 4rpx;
}
}
.review-time {
font-size: 22rpx;
color: #999;
}
}
.review-content {
.review-text {
font-size: 26rpx;
color: #333;
line-height: 1.6;
}
}
// 回复按钮
.review-actions {
margin-top: 16rpx;
.reply-btn {
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
background-color: #f8f9fa;
border-radius: 20rpx;
.reply-text {
font-size: 24rpx;
color: #666;
}
}
}
// 二级评论列表
.reply-list {
margin-top: 16rpx;
padding-left: 32rpx;
border-left: 2rpx solid #f0f0f0;
.reply-item {
padding: 16rpx 0;
.reply-user {
display: flex;
align-items: flex-start;
.reply-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 12rpx;
margin-top: 4rpx;
}
.reply-info {
flex: 1;
.reply-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
.reply-name {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
.reply-time {
font-size: 20rpx;
color: #999;
}
.author-tag {
font-size: 20rpx;
color: #fff;
background-color: #FF4757;
padding: 4rpx 8rpx;
border-radius: 8rpx;
}
}
.reply-content {
font-size: 24rpx;
color: #333;
line-height: 1.5;
}
}
}
}
}
// 回复输入框
.reply-input-container {
margin-top: 16rpx;
padding: 16rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
.reply-input-wrapper {
.reply-input {
width: 100%;
height: 60rpx;
padding: 0 16rpx;
background-color: #fff;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 24rpx;
color: #333;
margin-bottom: 12rpx;
}
.reply-input-actions {
display: flex;
gap: 12rpx;
justify-content: flex-end;
.cancel-btn {
padding: 8rpx 20rpx;
background-color: #f0f0f0;
color: #666;
border: none;
border-radius: 8rpx;
font-size: 24rpx;
}
.submit-btn {
padding: 8rpx 20rpx;
background-color: #FF4757;
color: #fff;
border: none;
border-radius: 8rpx;
font-size: 24rpx;
}
}
}
}
}
}
// 加载更多
.load-more {
padding: 32rpx 0;
text-align: center;
.load-more-btn {
background-color: #f8f9fa;
color: #666;
border: 1rpx solid #e0e0e0;
padding: 16rpx 48rpx;
border-radius: 24rpx;
font-size: 26rpx;
&:disabled {
opacity: 0.6;
color: #999;
}
}
}
// 已加载全部
.load-all {
padding: 32rpx 0;
text-align: center;
.load-all-text {
font-size: 24rpx;
color: #999;
}
}
// 加载中状态
.loading-reviews {
padding: 80rpx 0;
text-align: center;
.loading-icon {
margin-bottom: 24rpx;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
// 空评价状态
.empty-reviews {
padding: 80rpx 0;
text-align: center;
.empty-icon {
margin-bottom: 24rpx;
}
.empty-text {
display: block;
font-size: 28rpx;
color: #999;
margin-bottom: 12rpx;
}
.empty-desc {
display: block;
font-size: 24rpx;
color: #ccc;
}
}
}
// 底部购买栏
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
background-color: #fff;
border-top: 1rpx solid #e5e5e5;
display: flex;
align-items: center;
padding: 0 24rpx;
z-index: 999;
.price-section {
flex: 1;
.price {
font-size: 48rpx;
color: #FF4757;
font-weight: bold;
}
}
.buy-section {
.buy-btn {
background-color: #FF4757;
color: #fff;
border: none;
padding: 24rpx 48rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 600;
}
}
}
</style>