871 lines
22 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>
<div class="teacher-course-detail">
<!-- 顶部图片 -->
<div class="top-image-container" v-if="showTopImage">
<img src="/images/teacher/顶部.png" alt="顶部图片" class="top-image">
<button class="close-button" @click="handleClose">关闭</button>
</div>
<!-- 顶部导航 -->
<div class="top-navigation">
<n-button text @click="goBack" class="back-button">
返回
</n-button>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-content">
<p>正在加载课程详情...</p>
</div>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<div class="error-content">
<p>{{ error }}</p>
<button @click="loadCourseDetail" class="retry-btn">重试</button>
</div>
</div>
<!-- 课程内容 -->
<template v-else>
<!-- 左侧课程图片 -->
<div class="course-image-section">
<img :src="courseInfo.thumbnail || '/images/teacher/fj.png'" :alt="courseInfo.title" class="course-image" />
</div>
<!-- 右侧课程信息 -->
<div class="course-info-section">
<h1 class="course-title">{{ courseInfo.title }}</h1>
<div class="course-description" v-html="cleanHtmlContent(courseInfo.description)"></div>
<!-- 课程关键信息 -->
<div class="course-metrics">
<div class="metric-item">
<span class="metric-label">课程时间:</span>
<span class="metric-value">{{ courseInfo.courseTime }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课程分类:</span>
<span class="metric-value">{{ courseInfo.category }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课时:</span>
<span class="metric-value">{{ courseInfo.duration }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课程讲师:</span>
<span class="metric-value">
<span v-if="instructorsLoading">加载中...</span>
<span v-else>{{ courseInfo.instructor }}</span>
</span>
</div>
<div class="metric-item">
<span class="metric-label">教师团队:</span>
<span class="metric-value">
<span v-if="instructorsLoading">加载中...</span>
<span v-else>{{ courseInfo.teacherCount }}</span>
</span>
</div>
<div class="metric-item">
<span class="metric-label">课程积分:</span>
<span class="metric-value">{{ courseInfo.credits }}</span>
</div>
</div>
<!-- 开课学期选择 -->
<div class="semester-section">
<span class="semester-label">开课1学期</span>
<n-select v-model:value="selectedSemester" :options="semesterOptions" class="semester-select"
size="small" />
</div>
</div>
</template>
</div>
<!-- 底部统计数据 -->
<div class="bottom-stats-section">
<div class="stats-container">
<div class="stat-item">
<div class="stat-label">累计课程浏览量</div>
<div class="stat-value">{{ courseStats.views }}</div>
</div>
<div class="stat-item">
<div class="stat-label">累计报名人数</div>
<div class="stat-value">{{ courseStats.enrollments }}</div>
</div>
<div class="stat-item">
<div class="stat-label">累计章节学习人数</div>
<div class="stat-value">{{ courseStats.learners }}</div>
</div>
<div class="stat-item">
<div class="stat-label">累计互动评论人数</div>
<div class="stat-value">{{ courseStats.comments }}</div>
</div>
</div>
<!-- 分割线 -->
<div class="divider"></div>
<!-- 进入课程按钮 -->
<div class="action-section">
<n-button type="primary" size="large" class="enter-course-btn">
进入课程
</n-button>
</div>
</div>
<!-- 课程标签页 -->
<div class="course-tabs">
<div class="tab-nav">
<button class="tab-btn" :class="{ active: activeTab === 'intro' }" @click="activeTab = 'intro'">课程介绍</button>
<button class="tab-btn" :class="{ active: activeTab === 'team' }" @click="activeTab = 'team'">教学团队</button>
<button class="tab-btn" :class="{ active: activeTab === 'chapters' }"
@click="activeTab = 'chapters'">章节目录</button>
<button class="tab-btn" :class="{ active: activeTab === 'comments' }"
@click="activeTab = 'comments'">评论</button>
</div>
<!-- 标签页内容区域 -->
<div class="tab-content">
<transition name="tab-fade" mode="out-in">
<!-- 课程介绍内容 -->
<div v-if="activeTab === 'intro'" key="intro" class="tab-pane">
<CourseIntro />
</div>
<!-- 教学团队内容 -->
<div v-else-if="activeTab === 'team'" key="team" class="tab-pane">
<TeachingTeam />
</div>
<!-- 章节目录内容 -->
<div v-else-if="activeTab === 'chapters'" key="chapters" class="tab-pane">
<ChapterList />
</div>
<!-- 评论内容 -->
<div v-else-if="activeTab === 'comments'" key="comments" class="tab-pane">
<CourseComments />
</div>
</transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { NButton, NSelect } from 'naive-ui'
import { CourseApi } from '@/api/modules/course'
import { TeachCourseApi } from '@/api/modules/teachCourse'
import CourseIntro from './tabs/CourseIntro.vue'
import TeachingTeam from './tabs/TeachingTeam.vue'
import ChapterList from './tabs/ChapterList.vue'
import CourseComments from './tabs/CourseComments.vue'
const router = useRouter()
const route = useRoute()
const courseId = ref(route.params.id as string)
// 顶部图片控制
const showTopImage = ref(false) // 控制顶部图片显示/隐藏
// 课程信息
const courseInfo = ref({
title: '课程名称课程名称课',
description: '本课程旨在带领学生系统地学习【课程核心领域】的知识。我们将从【最基础的概念】讲起,逐步深入到【高级主题或应用】。通过理论与实践相结合的方式,学生不仅能够掌握【具体的理论知识】,还能获得【具体的实践技能,如解决XX问题、开发XX应用等】。',
courseTime: '2025-08-25-2026.08-25',
category: '分类名称',
duration: '4小时28分钟',
instructor: '王建国',
teacherCount: 1,
credits: 60,
thumbnail: '/images/teacher/fj.png'
})
// 课程数据状态
const loading = ref(false)
const error = ref('')
// 教师团队数据
const instructors = ref<any[]>([])
const instructorsLoading = ref(false)
// 课程统计数据
const courseStats = ref({
views: 0,
enrollments: 0,
learners: 0,
comments: 0
})
// 学期选择
const selectedSemester = ref('2025-2026-1')
const semesterOptions = [
{ label: '2025-2026第一学期', value: '2025-2026-1' },
{ label: '2025-2026第二学期', value: '2025-2026-2' }
]
// 标签页状态
const activeTab = ref('intro')
// 方法
const goBack = () => {
if (window.history.length > 1) {
router.go(-1)
} else {
router.push('/teacher/course-management')
}
}
const handleClose = () => {
showTopImage.value = false // 隐藏顶部图片容器
}
// 加载课程详情
const loadCourseDetail = async () => {
console.log('🚀 开始加载教师课程详情课程ID:', courseId.value)
if (!courseId.value || courseId.value.trim() === '') {
error.value = '课程ID无效'
console.error('❌ 课程ID无效:', courseId.value)
return
}
try {
loading.value = true
error.value = ''
console.log('📡 调用课程详情API...')
const response = await CourseApi.getCourseDetail(courseId.value)
console.log('📊 课程详情API响应:', response)
if (response.code === 0 || response.code === 200) {
const course = response.data
console.log('✅ 课程数据设置成功:', course)
console.log('📋 课程基本信息:', {
id: course?.id,
title: course?.title,
description: course?.description,
thumbnail: course?.thumbnail,
category: course?.category,
instructor: course?.instructor,
duration: course?.duration,
studentsCount: course?.studentsCount,
createdAt: course?.createdAt,
updatedAt: course?.updatedAt
})
// 更新课程信息
courseInfo.value = {
title: course?.title || '课程名称课程名称课',
description: course?.description || '本课程旨在带领学生系统地学习【课程核心领域】的知识。我们将从【最基础的概念】讲起,逐步深入到【高级主题或应用】。通过理论与实践相结合的方式,学生不仅能够掌握【具体的理论知识】,还能获得【具体的实践技能,如解决XX问题、开发XX应用等】。',
courseTime: formatCourseTime(course?.createdAt, course?.updatedAt),
category: course?.category?.name || '分类名称',
duration: course?.duration || '4小时28分钟',
instructor: course?.instructor?.name || '王建国',
teacherCount: 1, // 暂时固定为1
credits: 60, // 暂时固定为60
thumbnail: course?.thumbnail || '/images/teacher/fj.png'
}
// 尝试从课程管理API获取分类信息
await loadCourseCategoryFromManagementAPI()
// 更新统计数据这里可以根据实际API调整
courseStats.value = {
views: course?.studentsCount || 0,
enrollments: course?.studentsCount || 0,
learners: Math.floor((course?.studentsCount || 0) * 0.8),
comments: Math.floor((course?.studentsCount || 0) * 0.3)
}
console.log('🎯 课程信息更新完成:', courseInfo.value)
console.log('📈 统计数据更新完成:', courseStats.value)
} else {
error.value = response.message || '获取课程详情失败'
console.error('❌ API返回错误:', response)
}
} catch (err) {
console.error('❌ 加载课程详情失败:', err)
error.value = '网络错误,请稍后重试'
} finally {
loading.value = false
}
}
// 加载教师团队信息
const loadCourseInstructors = async () => {
console.log('👥 开始加载课程教师团队课程ID:', courseId.value)
if (!courseId.value || courseId.value.trim() === '') {
console.error('❌ 课程ID无效无法加载教师团队')
return
}
try {
instructorsLoading.value = true
console.log('📡 调用教师团队API...')
const response = await CourseApi.getCourseInstructors(courseId.value)
console.log('📊 教师团队API响应:', response)
if (response.code === 0 || response.code === 200) {
instructors.value = response.data || []
console.log('✅ 教师团队数据设置成功:', instructors.value)
console.log('👥 教师团队数量:', instructors.value.length)
// 更新课程信息中的教师团队人数
courseInfo.value.teacherCount = instructors.value.length
console.log('🎯 更新教师团队人数:', courseInfo.value.teacherCount)
// 从教师团队中获取所有教师名字按sortOrder排序
if (instructors.value.length > 0) {
// 按sortOrder排序如果没有sortOrder则按原始顺序
const sortedInstructors = [...instructors.value].sort((a, b) => {
const aOrder = a.sortOrder || 0
const bOrder = b.sortOrder || 0
return aOrder - bOrder
})
// 将所有教师名字用逗号连接
const allInstructorNames = sortedInstructors.map(teacher => teacher.name).join('、')
courseInfo.value.instructor = allInstructorNames || '王建国'
console.log('👨‍🏫 更新所有教师:', courseInfo.value.instructor)
console.log('📋 教师团队排序:', sortedInstructors.map(t => ({ name: t.name, sortOrder: t.sortOrder })))
}
} else {
console.warn('⚠️ 教师团队API返回错误:', response)
// 保持默认值
courseInfo.value.teacherCount = 1
}
} catch (err) {
console.error('❌ 加载教师团队失败:', err)
// 保持默认值
courseInfo.value.teacherCount = 1
} finally {
instructorsLoading.value = false
}
}
// 格式化课程时间
const formatCourseTime = (createdAt?: string, updatedAt?: string) => {
if (createdAt && updatedAt) {
const startDate = new Date(createdAt).toLocaleDateString('zh-CN')
const endDate = new Date(updatedAt).toLocaleDateString('zh-CN')
return `${startDate}-${endDate}`
}
return '2025-08-25-2026.08-25'
}
// 清理HTML内容确保安全显示
const cleanHtmlContent = (content: string) => {
if (!content) return ''
// 如果内容包含HTML标签直接返回假设后端已经处理过安全性
// 这里可以添加更多的HTML清理逻辑比如移除script标签等
return content
}
// 从课程管理API获取分类信息
const loadCourseCategoryFromManagementAPI = async () => {
try {
// 调用课程管理API获取课程信息包含categoryId
const response = await TeachCourseApi.getTeacherCourseList({})
if (response.data && response.data.result && response.data.result.length > 0) {
// 从所有课程中找到匹配当前课程ID的课程
const courseData = response.data.result.find(course => course.id === courseId.value)
if (!courseData) {
// 备选方案:直接获取分类列表,显示第一个分类作为示例
try {
const categoryResponse = await CourseApi.getCategories()
if (categoryResponse.code === 200 && categoryResponse.data && categoryResponse.data.length > 0) {
const firstCategory = categoryResponse.data[0]
courseInfo.value.category = firstCategory.name
}
} catch (error) {
console.error('❌ 备选方案失败:', error)
}
return
}
if (courseData.categoryId) {
// 获取分类列表
const categoryResponse = await CourseApi.getCategories()
if (categoryResponse.code === 200 && categoryResponse.data) {
// 解析categoryId可能是逗号分隔的字符串
const categoryIds = courseData.categoryId.toString().split(',').map((id: string) => parseInt(id.trim())).filter((id: number) => !isNaN(id))
// 根据ID匹配分类名称
const categoryNames = categoryIds.map((id: number) => {
const category = categoryResponse.data.find(cat => cat.id === id)
return category ? category.name : `未知分类${id}`
}).filter(Boolean)
// 更新课程信息中的分类
if (categoryNames.length > 0) {
courseInfo.value.category = categoryNames.join('、')
}
}
}
}
} catch (error) {
console.error('❌ 获取分类信息失败:', error)
}
}
// 组件挂载时加载数据
onMounted(async () => {
console.log('🎬 教师课程详情页面加载完成课程ID:', courseId.value)
// 并行加载课程详情和教师团队信息
await Promise.all([
loadCourseDetail(),
loadCourseInstructors()
])
console.log('🎉 所有数据加载完成')
})
</script>
<style scoped>
.teacher-course-detail {
background: #fff;
min-height: 100vh;
}
/* 顶部图片容器 */
.top-image-container {
position: relative;
width: 100%;
height: 130px;
overflow: hidden;
}
.top-image {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
object-fit: cover;
}
/* 关闭按钮样式 */
.close-button {
position: absolute;
top: 0;
right: 15px;
width: 30px;
height: 20px;
background-color: #7192DC;
color: white;
border: none;
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.3s ease;
}
.close-button:hover {
background-color: #999999;
}
/* 顶部导航 */
.top-navigation {
display: flex;
align-items: center;
padding: 25px 30px;
background: #fff;
border-bottom: 1.5px solid #e6e6e6;
position: sticky;
top: 0;
z-index: 100;
}
.back-button {
margin-right: 16px;
color: #333;
background: #F7F8FA;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
min-width: 74px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
}
.back-button:hover {
color: #0288D1;
background: #e3f2fd;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
/* 主要内容区域 */
.main-content {
display: flex;
gap: 40px;
max-width: 1420px;
margin: 25px auto;
}
/* 加载和错误状态 */
.loading-container,
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
text-align: center;
width: 100%;
}
.loading-content p,
.error-content p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
.retry-btn {
background: #0C99DA;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.retry-btn:hover {
background: #0A8BC7;
}
/* 左侧课程图片 */
.course-image-section {
flex: 0 0 305px;
}
.course-image {
width: 100%;
height: 215px;
object-fit: cover;
}
/* 右侧课程信息 */
.course-info-section {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
}
.course-title {
font-size: 22px;
font-weight: 500;
color: #333;
margin: 0;
line-height: 1.2;
}
.course-description {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 10px;
}
/* 课程关键信息 */
.course-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.metric-item {
display: flex;
align-items: center;
gap: 12px;
}
.metric-label {
font-size: 14px;
color: #666;
min-width: 80px;
}
.metric-value {
font-size: 14px;
color: #666666;
font-weight: 500;
}
/* 开课学期选择 */
.semester-section {
display: flex;
align-items: center;
gap: 20px;
margin-top: 8px;
}
.semester-label {
font-size: 18px;
color: #333;
}
.semester-select {
width: 181px;
}
/* 学期选择器样式 */
.semester-select :deep(.n-base-selection-label) {
background-color: #0C99DA !important;
padding: 4px 4px;
border: none !important;
color: white !important;
font-size: 14px !important;
width: 181px !important;
height: 37px !important;
}
.semester-select :deep(.n-base-selection-input__content) {
color: white !important;
font-size: 14px !important;
}
.semester-select :deep(.n-base-suffix__arrow) {
color: white !important;
}
.semester-select :deep(.n-base-suffix__arrow svg) {
fill: white !important;
}
/* 底部统计数据 */
.bottom-stats-section {
background: #F1F3F4;
width: 1420px;
margin: 35px auto;
padding: 15px 55px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stats-container {
display: flex;
gap: 200px;
}
/* 分割线 */
.divider {
width: 1px;
height: 40px;
background-color: #E0E0E0;
margin: 0 20px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.stat-label {
font-size: 12px;
color: #999;
text-align: left;
}
.stat-value {
font-size: 16px;
font-weight: 500;
color: #333;
}
/* 操作区域 */
.action-section {
flex-shrink: 0;
}
.enter-course-btn {
width: 136px !important;
height: 37px !important;
font-size: 14px !important;
font-weight: 600 !important;
border-radius: 2px !important;
background: #0C99DA !important;
border: none !important;
color: #fff !important;
transition: all 0.3s ease;
}
.enter-course-btn:hover {
background: #0A8BC7 !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(12, 153, 218, 0.3);
}
/* 课程标签页 */
.course-tabs {
background: #fff;
margin-top: 40px;
max-width: 1420px;
margin-left: auto;
margin-right: auto;
}
.tab-nav {
display: flex;
justify-content: center;
border-bottom: 1px solid #E6E6E6;
margin-bottom: 20px;
}
.tab-btn {
background: none;
border: none;
padding: 12px 0;
margin-right: 54px;
font-size: 16px;
color: #333;
cursor: pointer;
position: relative;
transition: color 0.3s;
}
.tab-btn.active {
color: #0C99DA;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 3px;
background: #0C99DA;
}
.tab-btn:hover {
color: #0C99DA;
}
.tab-content {
min-height: 300px;
padding: 0 0 24px 0;
}
/* Tab切换过渡动画 */
.tab-fade-enter-active,
.tab-fade-leave-active {
transition: all 0.1s ease;
}
.tab-fade-enter-from {
opacity: 0;
transform: translateX(20px);
}
.tab-fade-leave-to {
opacity: 0;
transform: translateX(-20px);
}
.tab-pane {
animation: fadeIn 0.1s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.top-image-container {
height: 100px;
}
}
@media (max-width: 480px) {
.top-image-container {
height: 80px;
}
}
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
padding: 20px;
}
.course-image-section {
flex: none;
}
.course-image {
height: 250px;
}
.bottom-stats-section {
flex-direction: column;
gap: 24px;
padding: 24px 20px;
}
.stats-container {
gap: 32px;
}
}
@media (max-width: 768px) {
.stats-container {
flex-wrap: wrap;
gap: 24px;
}
.stat-item {
flex: 1;
min-width: 120px;
}
.course-title {
font-size: 24px;
}
.course-description {
font-size: 14px;
}
}
</style>