Files
taimed-international-app/pages/course/details/course.vue

610 lines
14 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">
<!-- 自定义导航栏 -->
<nav-bar :title="$t('courseDetails.detail')" />
<!-- VIP权益提示条 -->
<view v-if="vipTip" class="vip-tip">
<view class="tip-content">
<wd-icon name="info-circle" color="#fff" size="16"></wd-icon>
<text>{{ vipTip }}</text>
</view>
<wd-button v-if="!userVip" type="error" size="small" custom-class="vip-btn" @click="goToVip">{{ $t('courseDetails.purchase') }}</wd-button>
</view>
<!-- 课程信息 -->
<CourseInfo v-if="courseDetail" :course="courseDetail" :class="{'pt-10': !!vipTip}" />
<!-- 课程内容包装器 -->
<CatalogueList
v-if="catalogueList.length > 0"
:catalogues="catalogueList"
:userVip="userVip"
@toVip="goToVip"
@loadPageData="loadPageData"
@toDetail="handleToDetail"
/>
<!-- 学习进度 -->
<view class="learning-progress">
<view class="progress-title">
<text>{{ $t('courseDetails.progress') }}</text>
</view>
<wd-progress :percentage="learningProgress" show-text stroke-width="6" />
</view>
<!-- 相关书籍 -->
<!-- <view v-if="relatedBooks.length > 0" class="related-books">
<view class="section-title">
<text>{{ $t('courseDetails.relatedBooks') }}</text>
</view>
<scroll-view class="books-scroll" scroll-x>
<view
v-for="book in relatedBooks"
:key="book.productId"
class="book-item"
@click="goToBookDetail(book)"
>
<view class="book-cover">
<view v-if="book.isVipPrice === 1 && book.vipPrice" class="vip-badge">
VIP优惠
</view>
<image :src="book.productImages" mode="aspectFit" />
</view>
<view class="book-name">{{ book.productName }}</view>
<view class="book-price">
<text v-if="book.isVipPrice === 1 && book.vipPrice" class="vip-price">
{{ book.vipPrice.toFixed(2) }}
</text>
<text v-else-if="book.activityPrice" class="activity-price">
{{ book.activityPrice.toFixed(2) }}
</text>
<text v-else class="normal-price">
{{ book.price.toFixed(2) }}
</text>
</view>
</view>
</scroll-view>
</view> -->
<!-- 留言板 -->
<!-- <view class="comments-section">
<view class="section-header">
<text class="section-title">{{ $t('courseDetails.comments') }}</text>
<wd-button size="small" type="primary" @click="showCommentEditor">
{{ $t('courseDetails.publishComment') }}
</wd-button>
</view>
<CommentList
:comments="commentList"
:loading="commentsLoading"
:hasMore="hasMoreComments"
type="course"
@like="handleCommentLike"
@reply="handleCommentReply"
@loadMore="loadMoreComments"
/>
</view> -->
<!-- 评论编辑器 -->
<!-- <CommentEditor
:show="showEditor"
:parentComment="replyComment"
type="course"
@submit="handleCommentSubmit"
@close="closeCommentEditor"
/> -->
<!-- 返回顶部 -->
<wd-backtop :scrollTop="scrollTop" custom-class="back-top">
<wd-icon name="arrow-up" size="24px" color="#258feb" />
</wd-backtop>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad, onPageScroll, onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
import { courseApi } from '@/api/modules/course'
import CourseInfo from './components/CourseInfo.vue'
import CatalogueList from './components/CatalogueList.vue'
import CommentList from '@/components/comment/CommentList.vue'
import CommentEditor from '@/components/comment/CommentEditor.vue'
import type { ICourseDetail, ICatalogue, IChapter, IVipInfo } from '@/types/course'
import type { IGoods } from '@/types/order'
import type { IComment } from '@/types/comment'
// Stores
const userStore = useUserStore()
// 页面数据
const courseId = ref<number>(0)
const courseDetail = ref<ICourseDetail | null>(null)
const catalogueList = ref<ICatalogue[]>([])
const userVip = ref<IVipInfo | null>(null)
const vipModuleList = ref<string[]>([])
const learningProgress = ref(0)
const relatedBooks = ref<IGoods[]>([])
// 评论相关
const commentList = ref<IComment[]>([])
const commentsLoading = ref(false)
const hasMoreComments = ref(true)
const commentPage = ref(0)
const showEditor = ref(false)
const replyComment = ref<IComment | undefined>(undefined)
// UI状态
const scrollTop = ref(0)
/**
* VIP提示文案
*/
const vipTip = computed(() => {
if (userVip.value) {
const vipTypeMap: Record<number, string> = {
4: '中医学',
5: '针灸学',
6: '肿瘤学',
7: '国学',
8: '心理学',
9: '中西汇通学'
}
const typeName = vipTypeMap[userVip.value.type] || ''
return `尊贵的${typeName}VIP您的有效期到${userVip.value.endTime}`
}
if (vipModuleList.value.length > 0) {
const typeMap: Record<string, string> = {
'4': '中医学',
'5': '针灸学',
'6': '肿瘤学',
'7': '国学',
'8': '心理学',
'9': '中西汇通学'
}
const typeNames = vipModuleList.value.map(type => typeMap[type]).filter(Boolean)
return `购买${typeNames.join('/')}VIP即可畅享更多专属权益`
}
return ''
})
/**
* 页面加载
*/
onLoad(async (options: any) => {
courseId.value = parseInt(options.id)
})
/**
* 页面显示
*/
onShow(async () => {
await loadPageData()
})
/**
* 加载页面数据
*/
const loadPageData = async () => {
// 获取课程详情
const res = await courseApi.getCourseDetail(courseId.value)
if (res.code === 0 && res.data) {
courseDetail.value = res.data.course
catalogueList.value = res.data.catalogues || []
relatedBooks.value = res.data.shopProductList || []
// 计算学习进度
if (catalogueList.value.length > 0) {
const totalProgress = catalogueList.value.reduce((sum, cat) => sum + cat.completion, 0)
learningProgress.value = Number((totalProgress / catalogueList.value.length).toFixed(2))
}
}
// 检查VIP权益
await checkVipStatus()
// 加载评论
await loadComments()
}
/**
* 检查VIP状态
*/
const checkVipStatus = async () => {
const res = await courseApi.checkCourseVip(courseId.value)
if (res.code === 0) {
userVip.value = res.userVip || null
// 如果不是VIP获取需要的VIP类型
if (!userVip.value) {
const moduleRes = await courseApi.getCourseVipModule(courseId.value)
if (moduleRes.code === 0) {
vipModuleList.value = moduleRes.list || []
}
}
}
}
/**
* 点击章节
*/
const handleToDetail = (chapter: IChapter, catalogue: ICatalogue) => {
const noRecored = chapter.isAudition === 1 && catalogue.isBuy === 0 && !userVip.value
uni.navigateTo({
url: `/pages/course/details/chapter?id=${chapter.id}&courseId=${courseId.value}&courseTitle=${courseDetail.value?.title}&title=${chapter.title}&noRecored=${noRecored}`
})
}
/**
* 去开通vip
*/
const goToVip = () => {
uni.navigateTo({
url: '/pages/vip/course'
})
}
/**
* 跳转到书籍详情
*/
const goToBookDetail = (book: IGoods) => {
if (book.delFlag === -1) {
uni.showToast({ title: '商品已下架', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages/book/detail?id=${book.productId}`
})
}
/**
* 加载评论
*/
const loadComments = async () => {
if (commentsLoading.value) return
try {
commentsLoading.value = true
commentPage.value++
const res = await courseApi.getCourseComments(
courseId.value,
commentPage.value,
15,
userStore.userInfo.id
)
if (res.code === 0 && res.page) {
const newComments = res.page.records.map(comment => {
// 处理图片
if (comment.images) {
comment.imgList = comment.images.split(',')
} else {
comment.imgList = []
}
// 处理子评论
if (comment.children && comment.children.length > 0) {
comment.Bchildren = comment.children.slice(0, 5)
} else {
comment.Bchildren = []
}
return comment
})
commentList.value = [...commentList.value, ...newComments]
hasMoreComments.value = res.page.pages > commentPage.value
}
} catch (error) {
console.error('加载评论失败:', error)
} finally {
commentsLoading.value = false
}
}
/**
* 加载更多评论
*/
const loadMoreComments = () => {
loadComments()
}
/**
* 显示评论编辑器
*/
const showCommentEditor = () => {
replyComment.value = undefined
showEditor.value = true
}
/**
* 回复评论
*/
const handleCommentReply = (comment: IComment) => {
replyComment.value = comment
showEditor.value = true
}
/**
* 提交评论
*/
const handleCommentSubmit = async (content: string, images: string[]) => {
try {
const data = {
type: 0,
courseId: courseId.value,
chapterId: '',
pid: replyComment.value?.id || 0,
userId: userStore.userInfo.id,
forUserId: replyComment.value?.user.id || userStore.userInfo.id,
content,
images: images.join(',')
}
const res = await courseApi.addCourseComment(data)
if (res.code === 0 && res.courseGuestbook) {
uni.showToast({ title: '发布成功', icon: 'success' })
// 处理新评论
const newComment = res.courseGuestbook
newComment.imgList = newComment.images ? newComment.images.split(',') : []
newComment.children = []
newComment.Bchildren = []
if (replyComment.value) {
// 回复评论,添加到父评论的子评论列表
const parentIndex = commentList.value.findIndex(c => c.id === replyComment.value!.id)
if (parentIndex !== -1) {
commentList.value[parentIndex].children.unshift(newComment)
commentList.value[parentIndex].Bchildren.unshift(newComment)
}
} else {
// 一级评论,添加到列表顶部
commentList.value.unshift(newComment)
}
closeCommentEditor()
}
} catch (error) {
console.error('提交评论失败:', error)
uni.showToast({ title: '发布失败', icon: 'none' })
}
}
/**
* 关闭评论编辑器
*/
const closeCommentEditor = () => {
showEditor.value = false
replyComment.value = undefined
}
/**
* 点赞评论
*/
const handleCommentLike = async (commentId: number) => {
try {
// 查找评论
let targetComment: IComment | null = null
let parentComment: IComment | null = null
for (const comment of commentList.value) {
if (comment.id === commentId) {
targetComment = comment
break
}
for (const child of comment.children) {
if (child.id === commentId) {
targetComment = child
parentComment = comment
break
}
}
if (targetComment) break
}
if (!targetComment) return
// 调用API
if (targetComment.support) {
await courseApi.unlikeComment(userStore.userInfo.id, commentId)
targetComment.support = false
targetComment.supportCount--
} else {
await courseApi.likeComment(userStore.userInfo.id, commentId)
targetComment.support = true
targetComment.supportCount++
}
} catch (error) {
console.error('点赞失败:', error)
}
}
/**
* 页面滚动
*/
onPageScroll((e: any) => {
scrollTop.value = e.scrollTop
})
/**
* 下拉刷新
*/
onPullDownRefresh(async () => {
commentPage.value = 0
commentList.value = []
await loadPageData()
uni.stopPullDownRefresh()
})
/**
* 触底加载
*/
onReachBottom(() => {
if (hasMoreComments.value && !commentsLoading.value) {
loadComments()
}
})
</script>
<style lang="scss" scoped>
.course-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.page-content {
padding-bottom: 100rpx;
}
.vip-tip {
position: fixed;
left: 0;
right: 0;
z-index: 9;
display: flex;
justify-content: space-between;
align-items: center;
height: 30px;
padding: 5px 10px;
box-sizing: content-box;
background: linear-gradient(90deg, #258feb 0%, #00e1ec 100%);
color: #fff;
.tip-content {
flex: 1;
display: flex;
align-items: center;
gap: 10rpx;
font-size: 26rpx;
}
.vip-btn {
flex-shrink: 0;
font-size: 12px;
padding: 0px 5px;
border-radius: 4px;
}
}
.learning-progress {
padding: 20rpx;
background-color: #fff;
margin-top: 20rpx;
.progress-title {
margin-bottom: 20rpx;
font-size: 28rpx;
color: #666;
}
}
.related-books {
padding: 20rpx;
background-color: #fff;
margin-top: 20rpx;
.section-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
}
.books-scroll {
white-space: nowrap;
.book-item {
display: inline-block;
width: 210rpx;
margin-right: 20rpx;
vertical-align: top;
.book-cover {
position: relative;
width: 100%;
height: 260rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
overflow: hidden;
.vip-badge {
position: absolute;
top: 10rpx;
left: 10rpx;
padding: 4rpx 10rpx;
background-color: #f94f04;
color: #fff;
font-size: 22rpx;
border-radius: 4rpx;
z-index: 1;
}
image {
width: 100%;
height: 100%;
}
}
.book-name {
margin-top: 10rpx;
font-size: 24rpx;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.book-price {
margin-top: 5rpx;
.vip-price,
.activity-price {
font-size: 26rpx;
font-weight: bold;
color: #e97512;
}
.normal-price {
font-size: 26rpx;
font-weight: bold;
color: #333;
}
}
}
}
}
.comments-section {
padding: 20rpx;
background-color: #fff;
margin-top: 20rpx;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
}
}
:deep(.back-top) {
background-color: #fff !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>