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

782 lines
19 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"
@getFreeCourse="handleGetFreeCourse"
@purchase="handlePurchase"
@toVip="goToVip"
@renew="handleRenew"
@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> -->
<!-- 商品选择器 -->
<GoodsSelector
:show="showGoodsSelector"
:goods="goodsList"
:isFudu="isFudu"
@select="handleGoodsSelect"
@confirm="handleGoodsConfirm"
@close="closeGoodsSelector"
/>
<!-- 购买协议弹窗 -->
<wd-popup v-model="showProtocol" position="center">
<view class="protocol-popup">
<view class="protocol-title">温馨提示</view>
<view class="protocol-content">
<text>
用户您好本软件对于一个用户名及密码仅允许一部电子设备登陆多部设备使用同一用户名操作软件的行为属于违规操作发现违规一次将提出警告再次违规您的用户名将被封号无法正常登陆如因此对您使用带来不便敬请谅解
</text>
<text>
课程购买之后一年内不打开此一年内不会计算有效学习时间一年后会自动开始计算有效学习时间
</text>
<text>
本课程一经购买暂不支持退款敬请谅解
</text>
<view style="color: red; font-weight: bold"> : </view>
<view>
1.手机pad电脑均为可登陆电子设备均有唯一标识码一个用户名仅允许在一个手机或一个ipad或一个电脑登陆请根据您的使用习惯自行选择<br />
2.如若申请变更登陆设备请联系客服<br />
客服电话:021-08371305<br />
客服微信号:yilujiankangkefu<br />
3.如因违反上述使用规定...概不退款本公司保留追究用户相关法律责任的权利<br />
4.点击同意按钮即表示您同意遵守以上条款
</view>
</view>
<view class="protocol-actions">
<wd-button type="info" plain @click="showProtocol = false">不同意</wd-button>
<wd-button type="primary" @click="confirmPurchase">同意</wd-button>
</view>
</view>
</wd-popup>
<!-- 评论编辑器 -->
<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 GoodsSelector from '@/components/order/GoodsSelector.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 showGoodsSelector = ref(false)
const goodsList = ref<IGoods[]>([])
const selectedGoods = ref<IGoods | null>(null)
const showProtocol = ref(false)
const isFudu = ref(false)
const fuduCatalogueId = ref<number>(0)
// 评论相关
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 handleGetFreeCourse = async (catalogue: ICatalogue) => {
if (!catalogue) return
const res = await courseApi.startStudyForMF(catalogue.id)
if (res.code === 0) {
uni.showToast({ title: '领取成功', icon: 'success' })
// 刷新页面数据
loadPageData()
} else {
uni.showToast({ title: res.msg || '领取失败', icon: 'none' })
}
}
/**
* 购买课程
*/
const handlePurchase = async (catalogue: ICatalogue) => {
if (!catalogue) return
isFudu.value = false
const res = await courseApi.getProductListForCourse(catalogue.id)
if (res.code === 0 && res.productList.length > 0) {
goodsList.value = res.productList
showGoodsSelector.value = true
} else {
uni.showToast({ title: '此课程暂无购买方式', icon: 'none' })
}
}
/**
* 续费/复读
*/
const handleRenew = async () => {
// if (!currentCatalogue.value) return
// isFudu.value = true
// fuduCatalogueId.value = currentCatalogue.value.id
// const res = await courseApi.getRenewProductList(currentCatalogue.value.id)
// if (res.code === 0 && res.productList.length > 0) {
// goodsList.value = res.productList
// showGoodsSelector.value = true
// } else {
// uni.showToast({ title: '暂无复读方案', icon: 'none' })
// }
}
/**
* 选择商品
*/
const handleGoodsSelect = (goods: IGoods) => {
selectedGoods.value = goods
}
/**
* 确认购买
*/
const handleGoodsConfirm = () => {
showGoodsSelector.value = false
showProtocol.value = true
}
/**
* 关闭商品选择器
*/
const closeGoodsSelector = () => {
showGoodsSelector.value = false
}
/**
* 确认购买协议
*/
const confirmPurchase = () => {
showProtocol.value = false
if (!selectedGoods.value) return
showProtocol.value = false
// 跳转到确认订单页
uni.navigateTo({
url: `/pages/order/goodsConfirm?goods=${selectedGoods.value.productId}`
})
}
/**
* 跳转到书籍详情
*/
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;
}
}
}
.protocol-popup {
width: 600rpx;
padding: 40rpx;
background-color: #fff;
border-radius: 12rpx;
.protocol-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.protocol-content {
max-height: 60vh;
overflow-y: auto;
font-size: 26rpx;
line-height: 1.8;
color: #666;
margin-bottom: 30rpx;
text {
display: block;
margin-bottom: 20rpx;
}
}
.protocol-actions {
display: flex;
gap: 20rpx;
}
}
:deep(.back-top) {
background-color: #fff !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>