feat: 课程详细页面:课程介绍,课程介绍,教学团队,章节目录,评论接入接口; 课程内容,字幕列表接入接口
This commit is contained in:
parent
6b3846ea50
commit
82ae528785
@ -277,6 +277,15 @@ export const ChatApi = {
|
|||||||
return ApiRequest.post(`/aiol/aiolChat/${chatId}/update_last_read/${messageId}`)
|
return ApiRequest.post(`/aiol/aiolChat/${chatId}/update_last_read/${messageId}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出群聊
|
||||||
|
* DELETE /aiol/aiolChat/{chatId}/exit
|
||||||
|
* 退出指定的群聊会话
|
||||||
|
*/
|
||||||
|
exitChat: (chatId: string): Promise<ApiResponse<any>> => {
|
||||||
|
return ApiRequest.delete(`/aiol/aiolChat/${chatId}/exit`)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用文件上传
|
* 通用文件上传
|
||||||
* POST /sys/common/upload
|
* POST /sys/common/upload
|
||||||
|
@ -11,13 +11,28 @@ import type {
|
|||||||
*/
|
*/
|
||||||
export class CommentApi {
|
export class CommentApi {
|
||||||
// 获取课程评论
|
// 获取课程评论
|
||||||
static getCourseComments(courseId: number, params?: {
|
static async getCourseComments(courseId: number, params?: {
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
sortBy?: 'newest' | 'oldest' | 'rating' | 'helpful'
|
sortBy?: 'newest' | 'oldest' | 'rating' | 'helpful'
|
||||||
rating?: number
|
rating?: number
|
||||||
}): Promise<ApiResponse<PaginationResponse<Comment>>> {
|
}): Promise<ApiResponse<any>> {
|
||||||
return ApiRequest.get(`/courses/${courseId}/comments`, params)
|
try {
|
||||||
|
console.log('🚀 获取课程评论:', { courseId, params })
|
||||||
|
|
||||||
|
// 使用正确的API路径 - 根据用户提供的接口
|
||||||
|
const response = await ApiRequest.get(`/aiol/aiolComment/course/${courseId}/list`, {
|
||||||
|
pageNo: params?.page || 1,
|
||||||
|
pageSize: params?.pageSize || 20,
|
||||||
|
...params
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('✅ 获取课程评论成功:', response)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 获取课程评论失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取课时评论
|
// 获取课时评论
|
||||||
@ -48,12 +63,54 @@ export class CommentApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加课程评论
|
// 添加课程评论
|
||||||
static addCourseComment(courseId: number, data: {
|
static async addCourseComment(courseId: number, data: {
|
||||||
content: string
|
content: string
|
||||||
rating?: number
|
rating?: number
|
||||||
parentId?: number
|
parentId?: number
|
||||||
}): Promise<ApiResponse<Comment>> {
|
}): Promise<ApiResponse<any>> {
|
||||||
return ApiRequest.post(`/courses/${courseId}/comments`, data)
|
try {
|
||||||
|
console.log('🚀 添加课程评论:', { courseId, data })
|
||||||
|
|
||||||
|
// 使用正确的API路径
|
||||||
|
const response = await ApiRequest.post(`/aiol/aiolComment/course/${courseId}/add`, {
|
||||||
|
content: data.content,
|
||||||
|
imgs: '',
|
||||||
|
parentId: data.parentId
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('✅ 添加课程评论成功:', response)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 添加课程评论失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回复评论 - 使用专门的回复接口
|
||||||
|
static async replyComment(data: {
|
||||||
|
content: string
|
||||||
|
targetType: 'course' | 'lesson' | 'comment'
|
||||||
|
targetId: string
|
||||||
|
parentId?: number
|
||||||
|
}): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
console.log('🚀 回复评论:', data)
|
||||||
|
|
||||||
|
// 使用专门的回复接口
|
||||||
|
const response = await ApiRequest.post(`/aiol/aiolComment/add`, {
|
||||||
|
content: data.content,
|
||||||
|
targetType: data.targetType,
|
||||||
|
targetId: data.targetId,
|
||||||
|
parentId: data.parentId,
|
||||||
|
imgs: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('✅ 回复评论成功:', response)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 回复评论失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加课时评论
|
// 添加课时评论
|
||||||
@ -78,11 +135,16 @@ export class CommentApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 点赞评论
|
// 点赞评论
|
||||||
static likeComment(commentId: number): Promise<ApiResponse<{
|
static async likeComment(commentId: string | number): Promise<ApiResponse<any>> {
|
||||||
likes: number
|
try {
|
||||||
isLiked: boolean
|
console.log('🚀 点赞评论:', commentId)
|
||||||
}>> {
|
const response = await ApiRequest.get(`/aiol/aiolComment/like/${commentId}`)
|
||||||
return ApiRequest.post(`/comments/${commentId}/like`)
|
console.log('✅ 点赞评论成功:', response)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 点赞评论失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消点赞评论
|
// 取消点赞评论
|
||||||
@ -211,6 +273,6 @@ export class CommentApi {
|
|||||||
}>>> {
|
}>>> {
|
||||||
return ApiRequest.get('/comments/reported', params)
|
return ApiRequest.get('/comments/reported', params)
|
||||||
}
|
}
|
||||||
}
|
}export default CommentApi
|
||||||
|
|
||||||
|
|
||||||
export default CommentApi
|
|
||||||
|
@ -157,7 +157,9 @@ export class CourseApi {
|
|||||||
question: item.question || '',
|
question: item.question || '',
|
||||||
video: item.video || '',
|
video: item.video || '',
|
||||||
// 添加AI伴学模式字段
|
// 添加AI伴学模式字段
|
||||||
izAi: item.izAi
|
izAi: item.izAi,
|
||||||
|
// 添加学期字段
|
||||||
|
semester: item.semester || ''
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -217,6 +219,8 @@ export class CourseApi {
|
|||||||
}
|
}
|
||||||
// 转换后端数据格式为前端格式
|
// 转换后端数据格式为前端格式
|
||||||
const item: BackendCourseItem = response.data.result
|
const item: BackendCourseItem = response.data.result
|
||||||
|
console.log('🔍 后端原始课程数据:', item)
|
||||||
|
console.log('📅 后端学期字段:', item.semester)
|
||||||
const course: Course = {
|
const course: Course = {
|
||||||
id: item.id, // 保持字符串格式,不转换为数字
|
id: item.id, // 保持字符串格式,不转换为数字
|
||||||
title: item.name || '',
|
title: item.name || '',
|
||||||
@ -259,7 +263,9 @@ export class CourseApi {
|
|||||||
createdAt: this.formatTimestamp(item.createTime),
|
createdAt: this.formatTimestamp(item.createTime),
|
||||||
updatedAt: this.formatTimestamp(item.updateTime),
|
updatedAt: this.formatTimestamp(item.updateTime),
|
||||||
// 添加AI伴学模式字段
|
// 添加AI伴学模式字段
|
||||||
izAi: item.izAi
|
izAi: item.izAi,
|
||||||
|
// 添加学期字段
|
||||||
|
semester: item.semester || ''
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -403,6 +409,8 @@ export class CourseApi {
|
|||||||
}
|
}
|
||||||
// 转换后端数据格式为前端格式
|
// 转换后端数据格式为前端格式
|
||||||
const item: BackendCourseItem = response.data.result
|
const item: BackendCourseItem = response.data.result
|
||||||
|
console.log('🔍 后端原始课程数据:', item)
|
||||||
|
console.log('📅 后端学期字段:', item.semester)
|
||||||
const course: Course = {
|
const course: Course = {
|
||||||
id: item.id, // 保持字符串格式,不转换为数字
|
id: item.id, // 保持字符串格式,不转换为数字
|
||||||
title: item.name || '',
|
title: item.name || '',
|
||||||
@ -445,7 +453,9 @@ export class CourseApi {
|
|||||||
createdAt: this.formatTimestamp(item.createTime),
|
createdAt: this.formatTimestamp(item.createTime),
|
||||||
updatedAt: this.formatTimestamp(item.updateTime),
|
updatedAt: this.formatTimestamp(item.updateTime),
|
||||||
// 添加AI伴学模式字段
|
// 添加AI伴学模式字段
|
||||||
izAi: item.izAi
|
izAi: item.izAi,
|
||||||
|
// 添加学期字段
|
||||||
|
semester: item.semester || ''
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -165,6 +165,7 @@ export interface Course {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
publishedAt?: string
|
publishedAt?: string
|
||||||
|
semester?: string // 新增学期字段
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后端实际返回的课程数据格式
|
// 后端实际返回的课程数据格式
|
||||||
@ -333,6 +334,7 @@ export interface BackendCourseItem {
|
|||||||
updateBy: string
|
updateBy: string
|
||||||
updateTime: string
|
updateTime: string
|
||||||
teacherList: BackendInstructor[] // 新增讲师列表字段
|
teacherList: BackendInstructor[] // 新增讲师列表字段
|
||||||
|
semester?: string // 新增学期字段
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后端课程列表响应格式
|
// 后端课程列表响应格式
|
||||||
|
@ -2,43 +2,40 @@
|
|||||||
<div class="course-content-management">
|
<div class="course-content-management">
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<n-select
|
|
||||||
v-model:value="selectedCourse"
|
|
||||||
:options="courseOptions"
|
|
||||||
placeholder="请选择课程"
|
|
||||||
style="width: 200px"
|
|
||||||
@update:value="handleCourseChange"
|
|
||||||
/>
|
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button type="primary" @click="showAddSummaryModal = true">
|
<n-select v-model:value="selectedSection" :options="sectionOptions" placeholder="请选择视频章节" style="width: 250px"
|
||||||
|
@update:value="handleSectionChange" />
|
||||||
|
</n-space>
|
||||||
|
<n-space>
|
||||||
|
<n-button type="primary" @click="showAddSummaryModal = true" :disabled="!selectedSection">
|
||||||
添加总结
|
添加总结
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" />
|
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" @keyup.enter="handleSearch" />
|
||||||
<n-button type="primary" @click="handleSearch">
|
<n-button type="primary" @click="handleSearch" :loading="searchLoading">
|
||||||
搜索
|
搜索
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button @click="handleClearSearch" v-if="searchKeyword">
|
||||||
|
清空
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="loadData" :loading="loading">
|
||||||
|
刷新
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 内容管理区域 -->
|
<!-- 内容管理区域 -->
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<div class="summary-section">
|
<div class="summary-section">
|
||||||
<n-data-table
|
<n-data-table :columns="summaryColumns" :data="filteredSummaryList" :pagination="pagination"
|
||||||
:columns="summaryColumns"
|
:loading="loading || searchLoading" :row-key="(row) => row.timestamp + row.title" striped size="small" />
|
||||||
:data="filteredSummaryList"
|
|
||||||
:pagination="pagination"
|
|
||||||
:loading="loading"
|
|
||||||
:row-key="(row: any) => row.timestamp + row.title"
|
|
||||||
striped
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加总结弹窗 -->
|
<!-- 添加总结弹窗 -->
|
||||||
<n-modal v-model:show="showAddSummaryModal" title="添加课程总结">
|
<n-modal v-model:show="showAddSummaryModal" title="添加课程总结">
|
||||||
<n-card style="width: 600px" title="添加课程总结" :bordered="false" size="huge">
|
<n-card style="width: 600px" title="添加课程总结" :bordered="false" size="huge">
|
||||||
<n-form ref="summaryFormRef" :model="summaryForm" :rules="summaryRules" label-placement="left" label-width="auto">
|
<n-form ref="summaryFormRef" :model="summaryForm" :rules="summaryRules" label-placement="left"
|
||||||
|
label-width="auto">
|
||||||
<n-form-item label="时间戳" path="timestamp">
|
<n-form-item label="时间戳" path="timestamp">
|
||||||
<n-input v-model:value="summaryForm.timestamp" placeholder="例如: 00:23" />
|
<n-input v-model:value="summaryForm.timestamp" placeholder="例如: 00:23" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@ -46,12 +43,7 @@
|
|||||||
<n-input v-model:value="summaryForm.title" placeholder="请输入总结标题" />
|
<n-input v-model:value="summaryForm.title" placeholder="请输入总结标题" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="内容" path="description">
|
<n-form-item label="内容" path="description">
|
||||||
<n-input
|
<n-input v-model:value="summaryForm.description" type="textarea" placeholder="请输入总结内容" :rows="4" />
|
||||||
v-model:value="summaryForm.description"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="请输入总结内容"
|
|
||||||
:rows="4"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -83,14 +75,22 @@ import {
|
|||||||
type FormRules,
|
type FormRules,
|
||||||
type DataTableColumns
|
type DataTableColumns
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
|
import { ApiRequest } from '@/api/request'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
courseId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const selectedCourse = ref('')
|
const selectedSection = ref('')
|
||||||
const showAddSummaryModal = ref(false)
|
const showAddSummaryModal = ref(false)
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
const sectionOptions = ref<Array<{ label: string; value: string }>>([])
|
||||||
|
|
||||||
// 表单引用
|
// 表单引用
|
||||||
const summaryFormRef = ref<FormInst | null>(null)
|
const summaryFormRef = ref<FormInst | null>(null)
|
||||||
@ -112,26 +112,14 @@ const pagination = ref({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 课程选项
|
|
||||||
const courseOptions = ref([
|
|
||||||
{ label: '职业探索与选择', value: 'course1' },
|
|
||||||
{ label: '软件工程导论', value: 'course2' },
|
|
||||||
{ label: '数据结构与算法', value: 'course3' }
|
|
||||||
])
|
|
||||||
|
|
||||||
// 总结数据
|
// 总结数据
|
||||||
const summaryList = ref([
|
const summaryList = ref<Array<{
|
||||||
{
|
id: string
|
||||||
timestamp: '00:23',
|
timestamp: string
|
||||||
title: '职业探索与选择:追求卓越与实现自我价值',
|
title: string
|
||||||
description: '本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。'
|
description: string
|
||||||
},
|
type?: string
|
||||||
{
|
}>>([])
|
||||||
timestamp: '00:45',
|
|
||||||
title: '职业探索与选择:追求卓越与实现自我价值',
|
|
||||||
description: '本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
@ -164,7 +152,7 @@ const summaryColumns: DataTableColumns<any> = [
|
|||||||
render: (row: any) => {
|
render: (row: any) => {
|
||||||
return h('span', {
|
return h('span', {
|
||||||
style: {
|
style: {
|
||||||
background: '#1890ff',
|
background: '#0C99DA',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
@ -186,6 +174,23 @@ const summaryColumns: DataTableColumns<any> = [
|
|||||||
title: '内容',
|
title: '内容',
|
||||||
key: 'description'
|
key: 'description'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
key: 'type',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
render: (row: any) => {
|
||||||
|
return h('span', {
|
||||||
|
style: {
|
||||||
|
background: '#52c41a',
|
||||||
|
color: 'white',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
fontSize: '11px'
|
||||||
|
}
|
||||||
|
}, row.type || '总结')
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
@ -227,32 +232,150 @@ const filteredSummaryList = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const handleCourseChange = (courseId: string) => {
|
const handleSectionChange = (sectionId: string) => {
|
||||||
console.log('切换课程:', courseId)
|
console.log('切换章节:', sectionId)
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadCourseSections = async () => {
|
||||||
loading.value = true
|
|
||||||
try {
|
try {
|
||||||
// 模拟API调用延迟
|
loading.value = true
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
console.log('📚 加载课程章节列表:', props.courseId)
|
||||||
// 这里可以加载对应课程的内容数据
|
|
||||||
|
const response = await ApiRequest.get(`/aiol/aiolCourse/${props.courseId}/section`)
|
||||||
|
console.log('✅ 章节列表响应:', response.data)
|
||||||
|
console.log('📊 原始章节数据:', response.data?.result)
|
||||||
|
|
||||||
|
if (response.data && response.data.result) {
|
||||||
|
const videoSections = response.data.result.filter((section: any) => section.type === 0)
|
||||||
|
console.log('🎬 筛选出的视频章节:', videoSections)
|
||||||
|
|
||||||
|
sectionOptions.value = videoSections.map((section: any) => ({
|
||||||
|
label: section.name,
|
||||||
|
value: section.id
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('✅ 视频章节选项:', sectionOptions.value)
|
||||||
|
|
||||||
|
// 默认选择第一个视频章节
|
||||||
|
if (sectionOptions.value.length > 0) {
|
||||||
|
selectedSection.value = sectionOptions.value[0].value
|
||||||
|
console.log('🎯 默认选择章节:', selectedSection.value)
|
||||||
|
// 自动加载第一个章节的数据
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 章节列表为空')
|
||||||
|
sectionOptions.value = []
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载数据失败:', error)
|
console.error('❌ 加载章节列表失败:', error)
|
||||||
message.error('加载数据失败,请重试')
|
message.error('加载章节列表失败,请重试')
|
||||||
|
sectionOptions.value = []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const loadData = async () => {
|
||||||
// 搜索功能已通过计算属性实现
|
if (!props.courseId || !selectedSection.value) {
|
||||||
|
summaryList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
console.log('📚 加载视频总结数据:', {
|
||||||
|
courseId: props.courseId,
|
||||||
|
sectionId: selectedSection.value
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('🔍 请求参数 resourceId:', selectedSection.value)
|
||||||
|
|
||||||
|
const response = await ApiRequest.get(`/aiol/aiolResourceContent/video_summary?resourceId=${selectedSection.value}`)
|
||||||
|
console.log('✅ 视频总结响应:', response.data)
|
||||||
|
|
||||||
|
if (response.data && response.data.result) {
|
||||||
|
const result = response.data.result
|
||||||
|
console.log('📊 原始结果数据:', result)
|
||||||
|
|
||||||
|
// 解析 contentData 字段中的JSON字符串
|
||||||
|
if (result.contentData) {
|
||||||
|
try {
|
||||||
|
// 先处理双重转义的JSON字符串
|
||||||
|
const unescapedData = result.contentData.replace(/\\"/g, '"').replace(/\\\\/g, '\\')
|
||||||
|
console.log('🔧 处理转义后的数据:', unescapedData)
|
||||||
|
|
||||||
|
const parsedData = JSON.parse(unescapedData)
|
||||||
|
console.log('📋 解析后的数据:', parsedData)
|
||||||
|
|
||||||
|
// 将解析后的数据转换为表格需要的格式
|
||||||
|
summaryList.value = parsedData.map((item: any, index: number) => ({
|
||||||
|
id: `${result.id}_${index}`,
|
||||||
|
timestamp: item.time,
|
||||||
|
title: item.title,
|
||||||
|
description: item.content,
|
||||||
|
type: '总结'
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('✅ 总结数据加载成功,共', summaryList.value.length, '条')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 解析contentData失败:', error)
|
||||||
|
summaryList.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ contentData字段为空')
|
||||||
|
summaryList.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 视频总结数据为空')
|
||||||
|
summaryList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 加载视频总结失败:', error)
|
||||||
|
message.error('加载视频总结失败,请重试')
|
||||||
|
summaryList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!searchKeyword.value.trim()) {
|
||||||
|
message.warning('请输入搜索关键词')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchLoading.value = true
|
||||||
console.log('搜索关键词:', searchKeyword.value)
|
console.log('搜索关键词:', searchKeyword.value)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟搜索延迟,让用户看到加载动画
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// 搜索功能通过计算属性 filteredSummaryList 自动实现
|
||||||
|
// 当 searchKeyword.value 改变时,filteredSummaryList 会自动重新计算
|
||||||
|
console.log('搜索完成,找到', filteredSummaryList.value.length, '条结果')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索失败:', error)
|
||||||
|
message.error('搜索失败,请重试')
|
||||||
|
} finally {
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
searchKeyword.value = ''
|
||||||
|
console.log('清空搜索')
|
||||||
}
|
}
|
||||||
|
|
||||||
const editSummary = (item: any) => {
|
const editSummary = (item: any) => {
|
||||||
summaryForm.value = { ...item }
|
summaryForm.value = {
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
title: item.title,
|
||||||
|
description: item.description
|
||||||
|
}
|
||||||
showAddSummaryModal.value = true
|
showAddSummaryModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,7 +387,17 @@ const deleteSummary = (index: number) => {
|
|||||||
const handleAddSummary = async () => {
|
const handleAddSummary = async () => {
|
||||||
try {
|
try {
|
||||||
await summaryFormRef.value?.validate()
|
await summaryFormRef.value?.validate()
|
||||||
summaryList.value.push({ ...summaryForm.value })
|
|
||||||
|
// 创建新的总结项
|
||||||
|
const newSummary = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
timestamp: summaryForm.value.timestamp,
|
||||||
|
title: summaryForm.value.title,
|
||||||
|
description: summaryForm.value.description,
|
||||||
|
type: '总结'
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryList.value.push(newSummary)
|
||||||
showAddSummaryModal.value = false
|
showAddSummaryModal.value = false
|
||||||
summaryForm.value = { timestamp: '', title: '', description: '' }
|
summaryForm.value = { timestamp: '', title: '', description: '' }
|
||||||
message.success('添加成功')
|
message.success('添加成功')
|
||||||
@ -274,12 +407,10 @@ const handleAddSummary = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化数据
|
// 初始化加载章节列表
|
||||||
if (courseOptions.value.length > 0) {
|
if (props.courseId) {
|
||||||
selectedCourse.value = courseOptions.value[0].value
|
loadCourseSections()
|
||||||
}
|
}
|
||||||
// 立即加载数据
|
|
||||||
loadData()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,43 +2,41 @@
|
|||||||
<div class="subtitle-management">
|
<div class="subtitle-management">
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<n-select
|
|
||||||
v-model:value="selectedCourse"
|
|
||||||
:options="courseOptions"
|
|
||||||
placeholder="请选择课程"
|
|
||||||
style="width: 200px"
|
|
||||||
@update:value="handleCourseChange"
|
|
||||||
/>
|
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button type="primary" @click="showAddSubtitleModal = true">
|
<n-select v-model:value="selectedSection" :options="sectionOptions" placeholder="请选择视频章节" style="width: 250px"
|
||||||
|
@update:value="handleSectionChange" />
|
||||||
|
</n-space>
|
||||||
|
<n-space>
|
||||||
|
<n-button type="primary" @click="showAddSubtitleModal = true" :disabled="!selectedSection">
|
||||||
添加字幕
|
添加字幕
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" />
|
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" @keyup.enter="handleSearch" />
|
||||||
<n-button type="primary" @click="handleSearch">
|
<n-button type="primary" @click="handleSearch" :loading="searchLoading">
|
||||||
搜索
|
搜索
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button @click="handleClearSearch" v-if="searchKeyword">
|
||||||
|
清空
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="loadData" :loading="loading">
|
||||||
|
刷新
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 字幕管理区域 -->
|
<!-- 字幕管理区域 -->
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<div class="subtitles-section">
|
<div class="subtitles-section">
|
||||||
<n-data-table
|
<n-data-table :columns="subtitleColumns" :data="filteredSubtitlesList" :pagination="pagination"
|
||||||
:columns="subtitleColumns"
|
:loading="loading || searchLoading" :row-key="(row) => row.startTime + row.endTime + row.text" striped
|
||||||
:data="filteredSubtitlesList"
|
size="small" />
|
||||||
:pagination="pagination"
|
|
||||||
:loading="loading"
|
|
||||||
:row-key="(row: any) => row.startTime + row.endTime + row.text"
|
|
||||||
striped
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加字幕弹窗 -->
|
<!-- 添加字幕弹窗 -->
|
||||||
<n-modal v-model:show="showAddSubtitleModal" title="添加字幕">
|
<n-modal v-model:show="showAddSubtitleModal" title="添加字幕">
|
||||||
<n-card style="width: 600px" title="添加字幕" :bordered="false" size="huge">
|
<n-card style="width: 600px" title="添加字幕" :bordered="false" size="huge">
|
||||||
<n-form ref="subtitleFormRef" :model="subtitleForm" :rules="subtitleRules" label-placement="left" label-width="auto">
|
<n-form ref="subtitleFormRef" :model="subtitleForm" :rules="subtitleRules" label-placement="left"
|
||||||
|
label-width="auto">
|
||||||
<n-form-item label="开始时间" path="startTime">
|
<n-form-item label="开始时间" path="startTime">
|
||||||
<n-input v-model:value="subtitleForm.startTime" placeholder="例如: 00:23" />
|
<n-input v-model:value="subtitleForm.startTime" placeholder="例如: 00:23" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@ -46,12 +44,7 @@
|
|||||||
<n-input v-model:value="subtitleForm.endTime" placeholder="例如: 00:45" />
|
<n-input v-model:value="subtitleForm.endTime" placeholder="例如: 00:45" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="字幕内容" path="text">
|
<n-form-item label="字幕内容" path="text">
|
||||||
<n-input
|
<n-input v-model:value="subtitleForm.text" type="textarea" placeholder="请输入字幕内容" :rows="3" />
|
||||||
v-model:value="subtitleForm.text"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="请输入字幕内容"
|
|
||||||
:rows="3"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -83,14 +76,22 @@ import {
|
|||||||
type FormRules,
|
type FormRules,
|
||||||
type DataTableColumns
|
type DataTableColumns
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
|
import { ApiRequest } from '@/api/request'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
courseId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const selectedCourse = ref('')
|
const selectedSection = ref('')
|
||||||
const showAddSubtitleModal = ref(false)
|
const showAddSubtitleModal = ref(false)
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const searchLoading = ref(false)
|
||||||
|
const sectionOptions = ref<Array<{ label: string; value: string }>>([])
|
||||||
|
|
||||||
// 表单引用
|
// 表单引用
|
||||||
const subtitleFormRef = ref<FormInst | null>(null)
|
const subtitleFormRef = ref<FormInst | null>(null)
|
||||||
@ -112,31 +113,14 @@ const pagination = ref({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 课程选项
|
|
||||||
const courseOptions = ref([
|
|
||||||
{ label: '职业探索与选择', value: 'course1' },
|
|
||||||
{ label: '软件工程导论', value: 'course2' },
|
|
||||||
{ label: '数据结构与算法', value: 'course3' }
|
|
||||||
])
|
|
||||||
|
|
||||||
// 字幕数据
|
// 字幕数据
|
||||||
const subtitlesList = ref([
|
const subtitlesList = ref<Array<{
|
||||||
{
|
id: string
|
||||||
startTime: '00:23',
|
startTime: string
|
||||||
endTime: '00:45',
|
endTime: string
|
||||||
text: '欢迎来到职业探索与选择课程,今天我们将探讨如何追求卓越与实现自我价值。'
|
text: string
|
||||||
},
|
language?: string
|
||||||
{
|
}>>([])
|
||||||
startTime: '00:45',
|
|
||||||
endTime: '01:12',
|
|
||||||
text: '首先,让我们来了解一下职业规划的重要性,以及如何根据个人兴趣和优势做出选择。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startTime: '01:12',
|
|
||||||
endTime: '01:35',
|
|
||||||
text: '通过分享不同领域的职场榜样,我们可以学习到成功人士的共同特质。'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const subtitleForm = ref({
|
const subtitleForm = ref({
|
||||||
@ -168,14 +152,14 @@ const subtitleColumns: DataTableColumns<any> = [
|
|||||||
render: (row: any) => {
|
render: (row: any) => {
|
||||||
return h('span', {
|
return h('span', {
|
||||||
style: {
|
style: {
|
||||||
background: '#1890ff',
|
background: '#0C99DA',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}
|
}
|
||||||
}, `${row.startTime} - ${row.endTime}`)
|
}, row.startTime || row.endTime || '')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -224,32 +208,150 @@ const filteredSubtitlesList = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const handleCourseChange = (courseId: string) => {
|
const handleSectionChange = (sectionId: string) => {
|
||||||
console.log('切换课程:', courseId)
|
console.log('切换章节:', sectionId)
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadCourseSections = async () => {
|
||||||
loading.value = true
|
|
||||||
try {
|
try {
|
||||||
// 模拟API调用延迟
|
loading.value = true
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
console.log('📚 加载课程章节列表:', props.courseId)
|
||||||
// 这里可以加载对应课程的字幕数据
|
|
||||||
|
const response = await ApiRequest.get(`/aiol/aiolCourse/${props.courseId}/section`)
|
||||||
|
console.log('✅ 章节列表响应:', response.data)
|
||||||
|
console.log('📊 原始章节数据:', response.data?.result)
|
||||||
|
|
||||||
|
if (response.data && response.data.result) {
|
||||||
|
const videoSections = response.data.result.filter((section: any) => section.type === 0)
|
||||||
|
console.log('🎬 筛选出的视频章节:', videoSections)
|
||||||
|
|
||||||
|
sectionOptions.value = videoSections.map((section: any) => ({
|
||||||
|
label: section.name,
|
||||||
|
value: section.id
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('✅ 视频章节选项:', sectionOptions.value)
|
||||||
|
|
||||||
|
// 默认选择第一个视频章节
|
||||||
|
if (sectionOptions.value.length > 0) {
|
||||||
|
selectedSection.value = sectionOptions.value[0].value
|
||||||
|
console.log('🎯 默认选择章节:', selectedSection.value)
|
||||||
|
// 自动加载第一个章节的数据
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 章节列表为空')
|
||||||
|
sectionOptions.value = []
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载数据失败:', error)
|
console.error('❌ 加载章节列表失败:', error)
|
||||||
message.error('加载数据失败,请重试')
|
message.error('加载章节列表失败,请重试')
|
||||||
|
sectionOptions.value = []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const loadData = async () => {
|
||||||
// 搜索功能已通过计算属性实现
|
if (!props.courseId || !selectedSection.value) {
|
||||||
|
subtitlesList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
console.log('📚 加载视频字幕数据:', {
|
||||||
|
courseId: props.courseId,
|
||||||
|
sectionId: selectedSection.value
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('🔍 请求参数 resourceId:', selectedSection.value)
|
||||||
|
|
||||||
|
const response = await ApiRequest.get(`/aiol/aiolResourceContent/video_subtitle?resourceId=${selectedSection.value}`)
|
||||||
|
console.log('✅ 视频字幕响应:', response.data)
|
||||||
|
|
||||||
|
if (response.data && response.data.result) {
|
||||||
|
const result = response.data.result
|
||||||
|
console.log('📊 原始结果数据:', result)
|
||||||
|
|
||||||
|
// 解析 contentData 字段中的JSON字符串
|
||||||
|
if (result.contentData) {
|
||||||
|
try {
|
||||||
|
// 先处理双重转义的JSON字符串
|
||||||
|
const unescapedData = result.contentData.replace(/\\"/g, '"').replace(/\\\\/g, '\\')
|
||||||
|
console.log('🔧 处理转义后的数据:', unescapedData)
|
||||||
|
|
||||||
|
const parsedData = JSON.parse(unescapedData)
|
||||||
|
console.log('📋 解析后的数据:', parsedData)
|
||||||
|
|
||||||
|
// 将解析后的数据转换为表格需要的格式
|
||||||
|
subtitlesList.value = parsedData.map((item: any, index: number) => ({
|
||||||
|
id: `${result.id}_${index}`,
|
||||||
|
startTime: item.startTime || item.time || '',
|
||||||
|
endTime: item.endTime || '',
|
||||||
|
text: item.text || item.content || '',
|
||||||
|
language: 'zh-CN'
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('✅ 字幕数据加载成功,共', subtitlesList.value.length, '条')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 解析contentData失败:', error)
|
||||||
|
subtitlesList.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ contentData字段为空')
|
||||||
|
subtitlesList.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 视频字幕数据为空')
|
||||||
|
subtitlesList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 加载视频字幕失败:', error)
|
||||||
|
message.error('加载视频字幕失败,请重试')
|
||||||
|
subtitlesList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!searchKeyword.value.trim()) {
|
||||||
|
message.warning('请输入搜索关键词')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchLoading.value = true
|
||||||
console.log('搜索关键词:', searchKeyword.value)
|
console.log('搜索关键词:', searchKeyword.value)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟搜索延迟,让用户看到加载动画
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// 搜索功能通过计算属性 filteredSubtitlesList 自动实现
|
||||||
|
// 当 searchKeyword.value 改变时,filteredSubtitlesList 会自动重新计算
|
||||||
|
console.log('搜索完成,找到', filteredSubtitlesList.value.length, '条结果')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索失败:', error)
|
||||||
|
message.error('搜索失败,请重试')
|
||||||
|
} finally {
|
||||||
|
searchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
searchKeyword.value = ''
|
||||||
|
console.log('清空搜索')
|
||||||
}
|
}
|
||||||
|
|
||||||
const editSubtitle = (item: any) => {
|
const editSubtitle = (item: any) => {
|
||||||
subtitleForm.value = { ...item }
|
subtitleForm.value = {
|
||||||
|
startTime: item.startTime,
|
||||||
|
endTime: item.endTime,
|
||||||
|
text: item.text
|
||||||
|
}
|
||||||
showAddSubtitleModal.value = true
|
showAddSubtitleModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +363,17 @@ const deleteSubtitle = (index: number) => {
|
|||||||
const handleAddSubtitle = async () => {
|
const handleAddSubtitle = async () => {
|
||||||
try {
|
try {
|
||||||
await subtitleFormRef.value?.validate()
|
await subtitleFormRef.value?.validate()
|
||||||
subtitlesList.value.push({ ...subtitleForm.value })
|
|
||||||
|
// 创建新的字幕项
|
||||||
|
const newSubtitle = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
startTime: subtitleForm.value.startTime,
|
||||||
|
endTime: subtitleForm.value.endTime,
|
||||||
|
text: subtitleForm.value.text,
|
||||||
|
language: 'zh-CN'
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitlesList.value.push(newSubtitle)
|
||||||
showAddSubtitleModal.value = false
|
showAddSubtitleModal.value = false
|
||||||
subtitleForm.value = { startTime: '', endTime: '', text: '' }
|
subtitleForm.value = { startTime: '', endTime: '', text: '' }
|
||||||
message.success('添加成功')
|
message.success('添加成功')
|
||||||
@ -270,15 +382,11 @@ const handleAddSubtitle = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化数据
|
// 初始化加载章节列表
|
||||||
if (courseOptions.value.length > 0) {
|
if (props.courseId) {
|
||||||
selectedCourse.value = courseOptions.value[0].value
|
loadCourseSections()
|
||||||
}
|
}
|
||||||
// 立即加载数据
|
|
||||||
loadData()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -40,7 +40,15 @@
|
|||||||
<!-- 右侧课程信息 -->
|
<!-- 右侧课程信息 -->
|
||||||
<div class="course-info-section">
|
<div class="course-info-section">
|
||||||
<h1 class="course-title">{{ courseInfo.title }}</h1>
|
<h1 class="course-title">{{ courseInfo.title }}</h1>
|
||||||
<div class="course-description" v-html="cleanHtmlContent(courseInfo.description)"></div>
|
<div class="course-description">
|
||||||
|
<div v-if="!showFullDescription" class="description-preview"
|
||||||
|
v-html="cleanHtmlContent(courseInfo.description?.substring(0, 200) + '...')"></div>
|
||||||
|
<div v-else class="description-full" v-html="cleanHtmlContent(courseInfo.description)"></div>
|
||||||
|
<button v-if="courseInfo.description && courseInfo.description.length > 200" @click="toggleDescription"
|
||||||
|
class="toggle-btn">
|
||||||
|
{{ showFullDescription ? '收起' : '展开' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 课程关键信息 -->
|
<!-- 课程关键信息 -->
|
||||||
<div class="course-metrics">
|
<div class="course-metrics">
|
||||||
@ -78,10 +86,15 @@
|
|||||||
|
|
||||||
<!-- 开课学期选择 -->
|
<!-- 开课学期选择 -->
|
||||||
<div class="semester-section">
|
<div class="semester-section">
|
||||||
<span class="semester-label">开课1学期</span>
|
<span class="semester-label">开课学期</span>
|
||||||
|
<div v-if="semesterOptions.length > 0" class="semester-select-container">
|
||||||
<n-select v-model:value="selectedSemester" :options="semesterOptions" class="semester-select"
|
<n-select v-model:value="selectedSemester" :options="semesterOptions" class="semester-select"
|
||||||
size="small" />
|
size="small" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="semester-empty">
|
||||||
|
<span class="semester-empty-text">暂无学期信息</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -134,12 +147,12 @@
|
|||||||
<transition name="tab-fade" mode="out-in">
|
<transition name="tab-fade" mode="out-in">
|
||||||
<!-- 课程介绍内容 -->
|
<!-- 课程介绍内容 -->
|
||||||
<div v-if="activeTab === 'intro'" key="intro" class="tab-pane">
|
<div v-if="activeTab === 'intro'" key="intro" class="tab-pane">
|
||||||
<CourseIntro />
|
<CourseIntro :course-info="courseInfo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 教学团队内容 -->
|
<!-- 教学团队内容 -->
|
||||||
<div v-else-if="activeTab === 'team'" key="team" class="tab-pane">
|
<div v-else-if="activeTab === 'team'" key="team" class="tab-pane">
|
||||||
<TeachingTeam />
|
<TeachingTeam :instructors="instructors" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 章节目录内容 -->
|
<!-- 章节目录内容 -->
|
||||||
@ -182,7 +195,7 @@ const courseInfo = ref({
|
|||||||
courseTime: '2025-08-25-2026.08-25',
|
courseTime: '2025-08-25-2026.08-25',
|
||||||
category: '分类名称',
|
category: '分类名称',
|
||||||
duration: '4小时28分钟',
|
duration: '4小时28分钟',
|
||||||
instructor: '王建国',
|
instructor: '加载中...',
|
||||||
teacherCount: 1,
|
teacherCount: 1,
|
||||||
credits: 60,
|
credits: 60,
|
||||||
thumbnail: '/images/teacher/fj.png'
|
thumbnail: '/images/teacher/fj.png'
|
||||||
@ -205,16 +218,70 @@ const courseStats = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 学期选择
|
// 学期选择
|
||||||
const selectedSemester = ref('2025-2026-1')
|
const selectedSemester = ref('')
|
||||||
const semesterOptions = [
|
const semesterOptions = ref<Array<{ label: string; value: string }>>([]) // 空数组,等待API数据
|
||||||
{ label: '2025-2026第一学期', value: '2025-2026-1' },
|
|
||||||
{ label: '2025-2026第二学期', value: '2025-2026-2' }
|
// 加载学期选项
|
||||||
]
|
const loadSemesterOptions = async () => {
|
||||||
|
try {
|
||||||
|
// 如果后端有学期列表接口,可以在这里调用
|
||||||
|
// const response = await SemesterApi.getSemesterList()
|
||||||
|
// if (response.data && response.data.code === 200) {
|
||||||
|
// semesterOptions.value = response.data.result.map(item => ({
|
||||||
|
// label: item.name,
|
||||||
|
// value: item.id
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 暂时保持空数组,等待后端提供学期接口
|
||||||
|
console.log('📅 学期选项加载完成:', semesterOptions.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 加载学期选项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 标签页状态
|
// 标签页状态
|
||||||
const activeTab = ref('intro')
|
const activeTab = ref('intro')
|
||||||
|
|
||||||
|
// 课程描述展开/收起状态
|
||||||
|
const showFullDescription = ref(false)
|
||||||
|
|
||||||
|
// 计算属性 - 暂时注释掉复杂实现,使用简单方式
|
||||||
|
// const truncatedDescription = computed(() => {
|
||||||
|
// try {
|
||||||
|
// if (!courseInfo.value?.description) return ''
|
||||||
|
//
|
||||||
|
// // 移除HTML标签来计算纯文本长度
|
||||||
|
// const textContent = courseInfo.value.description.replace(/<[^>]*>/g, '')
|
||||||
|
//
|
||||||
|
// if (textContent.length <= maxDescriptionLength) {
|
||||||
|
// return courseInfo.value.description
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 简单截取,在200字符处截断
|
||||||
|
// const truncated = textContent.substring(0, maxDescriptionLength) + '...'
|
||||||
|
// return truncated
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('截断描述时出错:', error)
|
||||||
|
// return courseInfo.value?.description || ''
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const needsTruncation = computed(() => {
|
||||||
|
// try {
|
||||||
|
// if (!courseInfo.value?.description) return false
|
||||||
|
// const textContent = courseInfo.value.description.replace(/<[^>]*>/g, '')
|
||||||
|
// return textContent.length > maxDescriptionLength
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('检查是否需要截断时出错:', error)
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
|
const toggleDescription = () => {
|
||||||
|
showFullDescription.value = !showFullDescription.value
|
||||||
|
}
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
router.go(-1)
|
router.go(-1)
|
||||||
@ -258,20 +325,21 @@ const loadCourseDetail = async () => {
|
|||||||
duration: course?.duration,
|
duration: course?.duration,
|
||||||
studentsCount: course?.studentsCount,
|
studentsCount: course?.studentsCount,
|
||||||
createdAt: course?.createdAt,
|
createdAt: course?.createdAt,
|
||||||
updatedAt: course?.updatedAt
|
updatedAt: course?.updatedAt,
|
||||||
|
semester: course?.semester
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新课程信息
|
// 更新课程信息
|
||||||
courseInfo.value = {
|
courseInfo.value = {
|
||||||
title: course?.title || '课程名称课程名称课',
|
title: course?.title || '',
|
||||||
description: course?.description || '本课程旨在带领学生系统地学习【课程核心领域】的知识。我们将从【最基础的概念】讲起,逐步深入到【高级主题或应用】。通过理论与实践相结合的方式,学生不仅能够掌握【具体的理论知识】,还能获得【具体的实践技能,如解决XX问题、开发XX应用等】。',
|
description: course?.description || '',
|
||||||
courseTime: formatCourseTime(course?.createdAt, course?.updatedAt),
|
courseTime: formatCourseTime(course?.createdAt, course?.updatedAt),
|
||||||
category: course?.category?.name || '分类名称',
|
category: course?.category?.name || '',
|
||||||
duration: course?.duration || '4小时28分钟',
|
duration: course?.duration || '',
|
||||||
instructor: course?.instructor?.name || '王建国',
|
instructor: course?.instructor?.name || '',
|
||||||
teacherCount: 1, // 暂时固定为1
|
teacherCount: course?.teacherList?.length || 0,
|
||||||
credits: 60, // 暂时固定为60
|
credits: 0, // 如果接口有学分字段,可以在这里设置
|
||||||
thumbnail: course?.thumbnail || '/images/teacher/fj.png'
|
thumbnail: course?.thumbnail || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试从课程管理API获取分类信息
|
// 尝试从课程管理API获取分类信息
|
||||||
@ -285,8 +353,22 @@ const loadCourseDetail = async () => {
|
|||||||
comments: Math.floor((course?.studentsCount || 0) * 0.3)
|
comments: Math.floor((course?.studentsCount || 0) * 0.3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置当前课程的学期
|
||||||
|
if (course?.semester) {
|
||||||
|
// 将课程学期添加到学期选项中
|
||||||
|
semesterOptions.value = [
|
||||||
|
{ label: course.semester, value: course.semester }
|
||||||
|
]
|
||||||
|
selectedSemester.value = course.semester
|
||||||
|
console.log('📅 设置课程学期:', course.semester)
|
||||||
|
console.log('📅 学期选项更新:', semesterOptions.value)
|
||||||
|
} else {
|
||||||
|
console.log('📅 课程未设置学期信息')
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🎯 课程信息更新完成:', courseInfo.value)
|
console.log('🎯 课程信息更新完成:', courseInfo.value)
|
||||||
console.log('📈 统计数据更新完成:', courseStats.value)
|
console.log('📈 统计数据更新完成:', courseStats.value)
|
||||||
|
console.log('📅 学期信息:', course?.semester || '未设置学期')
|
||||||
} else {
|
} else {
|
||||||
error.value = response.message || '获取课程详情失败'
|
error.value = response.message || '获取课程详情失败'
|
||||||
console.error('❌ API返回错误:', response)
|
console.error('❌ API返回错误:', response)
|
||||||
@ -334,7 +416,7 @@ const loadCourseInstructors = async () => {
|
|||||||
})
|
})
|
||||||
// 将所有教师名字用逗号连接
|
// 将所有教师名字用逗号连接
|
||||||
const allInstructorNames = sortedInstructors.map(teacher => teacher.name).join('、')
|
const allInstructorNames = sortedInstructors.map(teacher => teacher.name).join('、')
|
||||||
courseInfo.value.instructor = allInstructorNames || '王建国'
|
courseInfo.value.instructor = allInstructorNames || '暂无讲师'
|
||||||
console.log('👨🏫 更新所有教师:', courseInfo.value.instructor)
|
console.log('👨🏫 更新所有教师:', courseInfo.value.instructor)
|
||||||
console.log('📋 教师团队排序:', sortedInstructors.map(t => ({ name: t.name, sortOrder: t.sortOrder })))
|
console.log('📋 教师团队排序:', sortedInstructors.map(t => ({ name: t.name, sortOrder: t.sortOrder })))
|
||||||
}
|
}
|
||||||
@ -342,11 +424,13 @@ const loadCourseInstructors = async () => {
|
|||||||
console.warn('⚠️ 教师团队API返回错误:', response)
|
console.warn('⚠️ 教师团队API返回错误:', response)
|
||||||
// 保持默认值
|
// 保持默认值
|
||||||
courseInfo.value.teacherCount = 1
|
courseInfo.value.teacherCount = 1
|
||||||
|
courseInfo.value.instructor = '未知讲师'
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ 加载教师团队失败:', err)
|
console.error('❌ 加载教师团队失败:', err)
|
||||||
// 保持默认值
|
// 保持默认值
|
||||||
courseInfo.value.teacherCount = 1
|
courseInfo.value.teacherCount = 1
|
||||||
|
courseInfo.value.instructor = '未知讲师'
|
||||||
} finally {
|
} finally {
|
||||||
instructorsLoading.value = false
|
instructorsLoading.value = false
|
||||||
}
|
}
|
||||||
@ -405,7 +489,15 @@ const loadCourseCategoryFromManagementAPI = async () => {
|
|||||||
|
|
||||||
// 根据ID匹配分类名称
|
// 根据ID匹配分类名称
|
||||||
const categoryNames = categoryIds.map((id: number) => {
|
const categoryNames = categoryIds.map((id: number) => {
|
||||||
const category = categoryResponse.data.find(cat => cat.id === String(id))
|
// 尝试多种ID匹配方式
|
||||||
|
let category = categoryResponse.data.find(cat => cat.id === String(id))
|
||||||
|
if (!category) {
|
||||||
|
category = categoryResponse.data.find(cat => String(cat.id) === String(id))
|
||||||
|
}
|
||||||
|
if (!category) {
|
||||||
|
category = categoryResponse.data.find(cat => Number(cat.id) === id)
|
||||||
|
}
|
||||||
|
|
||||||
return category ? category.name : `未知分类${id}`
|
return category ? category.name : `未知分类${id}`
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
|
|
||||||
@ -428,7 +520,8 @@ onMounted(async () => {
|
|||||||
// 并行加载课程详情和教师团队信息
|
// 并行加载课程详情和教师团队信息
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadCourseDetail(),
|
loadCourseDetail(),
|
||||||
loadCourseInstructors()
|
loadCourseInstructors(),
|
||||||
|
loadSemesterOptions()
|
||||||
])
|
])
|
||||||
|
|
||||||
console.log('🎉 所有数据加载完成')
|
console.log('🎉 所有数据加载完成')
|
||||||
@ -558,6 +651,7 @@ onMounted(async () => {
|
|||||||
.retry-btn:hover {
|
.retry-btn:hover {
|
||||||
background: #0A8BC7;
|
background: #0A8BC7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 左侧课程图片 */
|
/* 左侧课程图片 */
|
||||||
.course-image-section {
|
.course-image-section {
|
||||||
flex: 0 0 305px;
|
flex: 0 0 305px;
|
||||||
@ -592,6 +686,30 @@ onMounted(async () => {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.description-preview {
|
||||||
|
max-height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-full {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #0288D1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover {
|
||||||
|
color: #0277BD;
|
||||||
|
}
|
||||||
|
|
||||||
/* 课程关键信息 */
|
/* 课程关键信息 */
|
||||||
.course-metrics {
|
.course-metrics {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -630,10 +748,30 @@ onMounted(async () => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.semester-select {
|
.semester-select-container {
|
||||||
width: 181px;
|
width: 181px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.semester-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semester-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 181px;
|
||||||
|
height: 37px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semester-empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
/* 学期选择器样式 */
|
/* 学期选择器样式 */
|
||||||
.semester-select :deep(.n-base-selection-label) {
|
.semester-select :deep(.n-base-selection-label) {
|
||||||
background-color: #0C99DA !important;
|
background-color: #0C99DA !important;
|
||||||
|
@ -11,10 +11,10 @@
|
|||||||
<OperationLog />
|
<OperationLog />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="content" tab="课程内容">
|
<n-tab-pane name="content" tab="课程内容">
|
||||||
<CourseContentManagement />
|
<CourseContentManagement :course-id="courseId" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="subtitles" tab="字幕列表">
|
<n-tab-pane name="subtitles" tab="字幕列表">
|
||||||
<SubtitleManagement />
|
<SubtitleManagement :course-id="courseId" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,157 +2,196 @@
|
|||||||
<div class="chapters-content">
|
<div class="chapters-content">
|
||||||
<h4>章节目录</h4>
|
<h4>章节目录</h4>
|
||||||
|
|
||||||
<div class="chapter-list">
|
<!-- 加载状态 -->
|
||||||
<div class="chapter-section">
|
<div v-if="loading" class="loading-state">
|
||||||
<div class="chapter-header" @click="toggleChapter(0)">
|
<div class="loading-spinner"></div>
|
||||||
<div class="chapter-info">
|
<span>正在加载章节目录...</span>
|
||||||
<span class="chapter-number">第一章</span>
|
|
||||||
<span class="chapter-title">课前准备</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="chapter-toggle" :class="{ 'expanded': chapters[0].expanded }">
|
|
||||||
|
<!-- 章节列表 -->
|
||||||
|
<div v-else-if="chapters.length > 0" class="chapter-list">
|
||||||
|
<div v-for="(chapter, index) in chapters" :key="chapter.id" class="chapter-section">
|
||||||
|
<div class="chapter-header" @click="toggleChapter(index)">
|
||||||
|
<div class="chapter-info">
|
||||||
|
<span class="chapter-number">第{{ index + 1 }}章</span>
|
||||||
|
<span class="chapter-title">{{ chapter.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||||
<path d="M4 3l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
<path d="M4 3l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="chapters[0].expanded" class="chapter-lessons">
|
<div v-if="chapter.expanded" class="chapter-lessons">
|
||||||
<div class="lesson-item">
|
<div v-for="lesson in getChapterLessons(chapter.id)" :key="lesson.id" class="lesson-item">
|
||||||
<div class="lesson-content">
|
<div class="lesson-content">
|
||||||
<div class="lesson-type-badge video">视频</div>
|
<div class="lesson-type-badge" :class="getLessonTypeClass(lesson.type)">
|
||||||
|
{{ getLessonTypeText(lesson.type) }}
|
||||||
|
</div>
|
||||||
<div class="lesson-info">
|
<div class="lesson-info">
|
||||||
<span class="lesson-title">开课彩蛋:新开始新征程</span>
|
<span class="lesson-title">{{ lesson.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-duration">
|
<div class="lesson-duration">
|
||||||
<span class="duration-text">01:03:56</span>
|
<span v-if="lesson.duration" class="duration-text">{{ lesson.duration }}</span>
|
||||||
<img src="/images/courses/video.png" alt="视频" class="duration-play-icon">
|
<img :src="getLessonIcon(lesson.type)" :alt="getLessonTypeText(lesson.type)" class="duration-icon">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-item">
|
|
||||||
<div class="lesson-content">
|
|
||||||
<div class="lesson-type-badge video">视频</div>
|
|
||||||
<div class="lesson-info">
|
|
||||||
<span class="lesson-title">课程定位与目标</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-duration">
|
|
||||||
<span class="duration-text">00:44:05</span>
|
|
||||||
<img src="/images/courses/video.png" alt="视频" class="duration-play-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-item">
|
|
||||||
<div class="lesson-content">
|
|
||||||
<div class="lesson-type-badge video">视频</div>
|
|
||||||
<div class="lesson-info">
|
|
||||||
<span class="lesson-title">教学安排及学习建议</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-duration">
|
|
||||||
<span class="duration-text">00:52:22</span>
|
|
||||||
<img src="/images/courses/video.png" alt="视频" class="duration-play-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-item">
|
|
||||||
<div class="lesson-content">
|
|
||||||
<div class="lesson-type-badge resource">资料</div>
|
|
||||||
<div class="lesson-info">
|
|
||||||
<span class="lesson-title">课前准备PPT</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-duration">
|
|
||||||
<img src="/images/courses/download.png" alt="下载" class="duration-download-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter-section">
|
<!-- 空状态 -->
|
||||||
<div class="chapter-header" @click="toggleChapter(1)">
|
<div v-else class="empty-state">
|
||||||
<div class="chapter-info">
|
<div class="empty-text">暂无章节目录</div>
|
||||||
<span class="chapter-number">第一章</span>
|
<div class="empty-desc">该课程还没有配置章节内容</div>
|
||||||
<span class="chapter-title">课前准备</span>
|
|
||||||
</div>
|
|
||||||
<span class="chapter-toggle" :class="{ 'expanded': chapters[1].expanded }">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
|
||||||
<path d="M4 3l4 3-4 3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="chapters[1].expanded" class="chapter-lessons">
|
|
||||||
<div class="lesson-item">
|
|
||||||
<div class="lesson-content">
|
|
||||||
<div class="lesson-type-badge video">视频</div>
|
|
||||||
<div class="lesson-info">
|
|
||||||
<span class="lesson-title">开课彩蛋:新开始新征程</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-duration">
|
|
||||||
<span class="duration-text">01:03:56</span>
|
|
||||||
<img src="/images/courses/video.png" alt="视频" class="duration-play-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-item">
|
|
||||||
<div class="lesson-content">
|
|
||||||
<div class="lesson-type-badge video">视频</div>
|
|
||||||
<div class="lesson-info">
|
|
||||||
<span class="lesson-title">课程定位与目标</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-duration">
|
|
||||||
<span class="duration-text">00:44:05</span>
|
|
||||||
<img src="/images/courses/video.png" alt="视频" class="duration-play-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-item">
|
|
||||||
<div class="lesson-content">
|
|
||||||
<div class="lesson-type-badge video">视频</div>
|
|
||||||
<div class="lesson-info">
|
|
||||||
<span class="lesson-title">教学安排及学习建议</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-duration">
|
|
||||||
<span class="duration-text">00:52:22</span>
|
|
||||||
<img src="/images/courses/video.png" alt="视频" class="duration-play-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-item">
|
|
||||||
<div class="lesson-content">
|
|
||||||
<div class="lesson-type-badge resource">资料</div>
|
|
||||||
<div class="lesson-info">
|
|
||||||
<span class="lesson-title">课前准备PPT</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-duration">
|
|
||||||
<img src="/images/courses/download.png" alt="下载" class="duration-download-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { CourseApi } from '@/api/modules/course'
|
||||||
|
import type { CourseSection } from '@/api/types'
|
||||||
|
|
||||||
|
// 扩展章节类型,添加展开状态
|
||||||
|
interface ChapterWithExpanded extends CourseSection {
|
||||||
|
expanded?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取路由参数
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
// 章节数据
|
// 章节数据
|
||||||
const chapters = ref([
|
const chapters = ref<ChapterWithExpanded[]>([])
|
||||||
{
|
const allSections = ref<CourseSection[]>([])
|
||||||
id: 1,
|
const loading = ref(false)
|
||||||
title: '课前准备',
|
|
||||||
expanded: true
|
// 从URL获取courseId
|
||||||
},
|
const courseId = computed(() => {
|
||||||
{
|
return route.params.id as string
|
||||||
id: 2,
|
})
|
||||||
title: '课前准备',
|
|
||||||
expanded: true
|
// 计算属性:获取一级章节(父章节)
|
||||||
|
const parentChapters = computed(() => {
|
||||||
|
if (!Array.isArray(allSections.value)) {
|
||||||
|
console.warn('⚠️ allSections.value 不是数组:', allSections.value)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调试:查看所有章节的level值
|
||||||
|
console.log('🔍 所有章节的level值:', allSections.value.map(s => ({ name: s.name, level: s.level, parentId: s.parentId })))
|
||||||
|
|
||||||
|
// 根据数据结构,看起来章节是通过parentId来区分的,而不是level
|
||||||
|
// 一级章节应该是没有parentId或者parentId为空的
|
||||||
|
const parentChapters = allSections.value.filter(section => !section.parentId || section.parentId === '')
|
||||||
|
console.log('🔍 找到的一级章节:', parentChapters.map(s => ({ name: s.name, level: s.level, parentId: s.parentId })))
|
||||||
|
|
||||||
|
return parentChapters
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取章节的子课程
|
||||||
|
const getChapterLessons = (chapterId: string) => {
|
||||||
|
if (!Array.isArray(allSections.value)) {
|
||||||
|
console.warn('⚠️ allSections.value 不是数组:', allSections.value)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const lessons = allSections.value.filter(section => section.parentId === chapterId)
|
||||||
|
console.log(`🔍 章节 ${chapterId} 的子课程:`, lessons.map(s => ({ name: s.name, type: s.type })))
|
||||||
|
|
||||||
|
return lessons
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课程类型文本
|
||||||
|
const getLessonTypeText = (type: number | null) => {
|
||||||
|
switch (type) {
|
||||||
|
case 0: return '视频'
|
||||||
|
case 1: return '资料'
|
||||||
|
case 2: return '考试'
|
||||||
|
case 3: return '作业'
|
||||||
|
default: return '课程'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课程类型样式类
|
||||||
|
const getLessonTypeClass = (type: number | null) => {
|
||||||
|
switch (type) {
|
||||||
|
case 0: return 'video'
|
||||||
|
case 1: return 'resource'
|
||||||
|
case 2: return 'exam'
|
||||||
|
case 3: return 'homework'
|
||||||
|
default: return 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课程类型图标
|
||||||
|
const getLessonIcon = (type: number | null) => {
|
||||||
|
switch (type) {
|
||||||
|
case 0: return '/images/courses/video.png'
|
||||||
|
case 1: return '/images/courses/download.png'
|
||||||
|
case 2: return '/images/courses/examination.png'
|
||||||
|
case 3: return '/images/courses/homework.png'
|
||||||
|
default: return '/images/courses/video.png'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
])
|
|
||||||
|
|
||||||
// 切换章节展开/收起
|
// 切换章节展开/收起
|
||||||
const toggleChapter = (index: number) => {
|
const toggleChapter = (index: number) => {
|
||||||
chapters.value[index].expanded = !chapters.value[index].expanded
|
chapters.value[index].expanded = !chapters.value[index].expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载章节数据
|
||||||
|
const loadChapters = async () => {
|
||||||
|
if (!courseId.value) {
|
||||||
|
console.warn('❌ 课程ID不存在,无法加载章节数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
console.log('🚀 开始加载课程章节,课程ID:', courseId.value)
|
||||||
|
console.log('🔍 API请求URL: /aiol/aiolCourse/' + courseId.value + '/section')
|
||||||
|
|
||||||
|
const response = await CourseApi.getCourseSections(courseId.value)
|
||||||
|
console.log('📊 章节API响应:', response)
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
// 从 CourseApi.getCourseSections 返回的数据结构中提取章节列表
|
||||||
|
const sectionsData = response.data.list || []
|
||||||
|
allSections.value = sectionsData
|
||||||
|
|
||||||
|
console.log('🔍 处理后的章节数据:', allSections.value)
|
||||||
|
console.log('🔍 数据类型:', typeof allSections.value, '是否为数组:', Array.isArray(allSections.value))
|
||||||
|
console.log('🔍 章节数量:', allSections.value.length)
|
||||||
|
|
||||||
|
// 获取一级章节并添加展开状态
|
||||||
|
chapters.value = parentChapters.value.map(chapter => ({
|
||||||
|
...chapter,
|
||||||
|
expanded: false // 默认收起
|
||||||
|
} as ChapterWithExpanded))
|
||||||
|
|
||||||
|
console.log('✅ 章节数据加载成功,共', chapters.value.length, '个章节')
|
||||||
|
console.log('📋 所有章节数据:', allSections.value)
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ API返回数据为空或失败')
|
||||||
|
chapters.value = []
|
||||||
|
allSections.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 加载章节失败:', error)
|
||||||
|
chapters.value = []
|
||||||
|
allSections.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadChapters()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -163,14 +202,66 @@ const toggleChapter = (index: number) => {
|
|||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #0288D1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
.chapter-list {
|
.chapter-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-section {
|
.chapter-section {}
|
||||||
}
|
|
||||||
|
|
||||||
.chapter-section:last-child {
|
.chapter-section:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@ -282,16 +373,29 @@ const toggleChapter = (index: number) => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-play-icon {
|
.duration-icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-download-icon {
|
/* 课程类型样式 */
|
||||||
width: 14px;
|
.lesson-type-badge.exam {
|
||||||
height: 14px;
|
background: transparent;
|
||||||
object-fit: contain;
|
border: 1px solid #FF9800;
|
||||||
|
color: #FF9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-type-badge.homework {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #9C27B0;
|
||||||
|
color: #9C27B0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-type-badge.default {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #E1E1E1;
|
||||||
|
color: #C0C0C0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
<div class="comments-content">
|
<div class="comments-content">
|
||||||
<h4>评论</h4>
|
<h4>评论</h4>
|
||||||
|
|
||||||
<div class="comment-list">
|
<!-- 评论列表 -->
|
||||||
|
<div class="comment-list" v-if="displayComments.length > 0">
|
||||||
<div class="comment-item" v-for="comment in displayComments" :key="comment.id">
|
<div class="comment-item" v-for="comment in displayComments" :key="comment.id">
|
||||||
<div class="comment-avatar">
|
<div class="comment-avatar">
|
||||||
<img :src="comment.avatar" :alt="comment.username" />
|
<img :src="comment.avatar" :alt="comment.username" />
|
||||||
@ -10,7 +11,8 @@
|
|||||||
<div class="comment-content">
|
<div class="comment-content">
|
||||||
<div class="comment-header">
|
<div class="comment-header">
|
||||||
<span class="comment-username">{{ comment.username }}</span>
|
<span class="comment-username">{{ comment.username }}</span>
|
||||||
<span v-if="comment.type === 'instructor'" class="instructor-badge">讲师</span>
|
<span v-if="comment.userType === 'instructor'" class="instructor-badge">{{ comment.userBadge }}</span>
|
||||||
|
<span v-else-if="comment.userType === 'student'" class="student-badge">{{ comment.userBadge }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-text">{{ comment.content }}</div>
|
<div class="comment-text">{{ comment.content }}</div>
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
@ -20,7 +22,13 @@
|
|||||||
<button class="action-btn">
|
<button class="action-btn">
|
||||||
<span>{{ comment.time }}</span>
|
<span>{{ comment.time }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn" @click="startReply(comment.id, comment.username)">回复</button>
|
<button class="action-btn" @click="likeComment(comment)">
|
||||||
|
<span>{{ comment.isLiked ? '已点赞' : '点赞' }} ({{ comment.likeCount }})</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="!comment.replies || comment.replies.length === 0" class="action-btn"
|
||||||
|
@click="startReply(comment.id, comment.username)">
|
||||||
|
回复
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 回复区域 -->
|
<!-- 回复区域 -->
|
||||||
@ -46,92 +54,288 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 回复输入框 -->
|
||||||
|
<div v-if="replyingTo === comment.id" class="reply-input-section">
|
||||||
|
<div class="reply-input-header">
|
||||||
|
<span>回复 @{{ replyToUsername }}:</span>
|
||||||
|
</div>
|
||||||
|
<div class="reply-input-content">
|
||||||
|
<textarea v-model="replyContent" class="reply-textarea" placeholder="请输入回复内容..." :maxlength="500"
|
||||||
|
rows="3"></textarea>
|
||||||
|
<div class="reply-input-actions">
|
||||||
|
<button class="reply-cancel-btn" @click="cancelReply">取消</button>
|
||||||
|
<button class="reply-submit-btn" @click="submitReply">发送</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else-if="!loading && !error" class="empty-state">
|
||||||
|
<div class="empty-content">
|
||||||
|
<h3 class="empty-title">暂无评论</h3>
|
||||||
|
<p class="empty-description">还没有人发表评论,快来抢沙发吧!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<div class="loading-content">
|
||||||
|
<p>正在加载评论...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div v-if="error" class="error-container">
|
||||||
|
<div class="error-content">
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
<button @click="loadComments" class="retry-btn">重试</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { CommentApi } from '@/api/modules/comment'
|
||||||
|
|
||||||
|
// 路由和消息
|
||||||
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 课程ID
|
||||||
|
const courseId = computed(() => route.params.id as string)
|
||||||
|
|
||||||
// 评论数据
|
// 评论数据
|
||||||
const displayComments = ref([
|
const displayComments = ref<any[]>([])
|
||||||
{
|
|
||||||
id: 1,
|
const loading = ref(false)
|
||||||
username: '春暖花开°C',
|
const error = ref('')
|
||||||
avatar: '/images/activity/1.png',
|
|
||||||
time: '2025.07.23 16:28',
|
|
||||||
content: '为了让科学教育有效,它必须广泛包容,而且应该认识到科学教师、科学家、家庭和社区如何合作实现学习和教学的目标。',
|
|
||||||
isPinned: true,
|
|
||||||
type: 'pinned',
|
|
||||||
replies: [
|
|
||||||
{
|
|
||||||
id: 'r1',
|
|
||||||
username: '汪波',
|
|
||||||
avatar: '/images/activity/3.png',
|
|
||||||
time: '2025.07.23 16:28',
|
|
||||||
content: '欢迎大家👋又不懂的地方可以私信老师',
|
|
||||||
type: 'instructor',
|
|
||||||
badge: '讲师'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
username: '春暖花开°C',
|
|
||||||
avatar: '/images/activity/2.png',
|
|
||||||
time: '2025.07.23 16:28',
|
|
||||||
content: '来了来了!老师讲的好好啊~',
|
|
||||||
isPinned: false,
|
|
||||||
type: 'comment',
|
|
||||||
replies: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
username: '春暖花开°C',
|
|
||||||
avatar: '/images/activity/4.png',
|
|
||||||
time: '2025.07.23 16:28',
|
|
||||||
content: '为了让科学教育有效,它必须广泛包容,而且应该认识到科学教师、科学家、家庭和社区如何合作实现学习和教学的目标。',
|
|
||||||
isPinned: true,
|
|
||||||
type: 'pinned',
|
|
||||||
replies: [
|
|
||||||
{
|
|
||||||
id: 'r2',
|
|
||||||
username: '汪波',
|
|
||||||
avatar: '/images/activity/6.png',
|
|
||||||
time: '2025.07.23 16:28',
|
|
||||||
content: '欢迎大家👋又不懂的地方可以私信老师',
|
|
||||||
type: 'instructor',
|
|
||||||
badge: '讲师'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
username: '春暖花开°C',
|
|
||||||
avatar: '/images/activity/5.png',
|
|
||||||
time: '2025.07.23 16:28',
|
|
||||||
content: '来了来了!老师讲的好好啊~',
|
|
||||||
isPinned: false,
|
|
||||||
type: 'comment',
|
|
||||||
replies: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
username: '春暖花开°C',
|
|
||||||
avatar: '/images/activity/7.png',
|
|
||||||
time: '2025.07.23 16:28',
|
|
||||||
content: '为了让科学教育有效,它必须广泛包容,而且应该认识到科学教师、科学家、家庭和社区如何合作实现学习和教学的目标。',
|
|
||||||
isPinned: true,
|
|
||||||
type: 'pinned',
|
|
||||||
replies: []
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 回复相关
|
// 回复相关
|
||||||
const replyingTo = ref<number | null>(null)
|
const replyingTo = ref<number | null>(null)
|
||||||
const replyToUsername = ref('')
|
const replyToUsername = ref('')
|
||||||
|
const replyContent = ref('')
|
||||||
|
|
||||||
|
// 时间格式化函数
|
||||||
|
const formatTime = (timeStr: string) => {
|
||||||
|
try {
|
||||||
|
const date = new Date(timeStr)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now.getTime() - date.getTime()
|
||||||
|
|
||||||
|
// 小于1分钟
|
||||||
|
if (diff < 60000) {
|
||||||
|
return '刚刚'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小于1小时
|
||||||
|
if (diff < 3600000) {
|
||||||
|
return `${Math.floor(diff / 60000)}分钟前`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小于1天
|
||||||
|
if (diff < 86400000) {
|
||||||
|
return `${Math.floor(diff / 3600000)}小时前`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小于7天
|
||||||
|
if (diff < 604800000) {
|
||||||
|
return `${Math.floor(diff / 86400000)}天前`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超过7天显示具体日期
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).replace(/\//g, '.')
|
||||||
|
} catch (error) {
|
||||||
|
return timeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载评论数据
|
||||||
|
const loadComments = async () => {
|
||||||
|
if (!courseId.value) {
|
||||||
|
console.error('❌ 课程ID不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
console.log('🚀 开始加载课程评论:', courseId.value)
|
||||||
|
|
||||||
|
// 调用评论API - 使用正确的接口路径
|
||||||
|
const response = await CommentApi.getCourseComments(Number(courseId.value), {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
sortBy: 'newest'
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📊 评论API响应:', response)
|
||||||
|
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
const comments = response.data.result || []
|
||||||
|
|
||||||
|
// 转换API数据格式
|
||||||
|
const apiComments = comments.map((comment: any) => {
|
||||||
|
// 调试:打印原始评论数据
|
||||||
|
console.log('🔍 原始评论数据:', comment)
|
||||||
|
console.log('🔍 用户身份相关字段:', {
|
||||||
|
userType: comment.userType,
|
||||||
|
user_type: comment.user_type,
|
||||||
|
role: comment.role,
|
||||||
|
isTeacher: comment.isTeacher,
|
||||||
|
is_teacher: comment.is_teacher,
|
||||||
|
userRole: comment.userRole,
|
||||||
|
user_role: comment.user_role,
|
||||||
|
type: comment.type,
|
||||||
|
userCategory: comment.userCategory,
|
||||||
|
user_category: comment.user_category
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据接口数据判断用户身份
|
||||||
|
let userType = 'user'
|
||||||
|
let userBadge = '用户'
|
||||||
|
|
||||||
|
// 检查是否是讲师/教师 - 扩展更多可能的字段
|
||||||
|
if (comment.userType === 'teacher' || comment.user_type === 'teacher' ||
|
||||||
|
comment.role === 'teacher' || comment.role === 'instructor' ||
|
||||||
|
comment.isTeacher === true || comment.is_teacher === true ||
|
||||||
|
comment.userRole === 'teacher' || comment.user_role === 'teacher' ||
|
||||||
|
comment.type === 'teacher' || comment.type === 'instructor' ||
|
||||||
|
comment.userCategory === 'teacher' || comment.user_category === 'teacher') {
|
||||||
|
userType = 'instructor'
|
||||||
|
userBadge = '讲师'
|
||||||
|
console.log('✅ 识别为讲师')
|
||||||
|
}
|
||||||
|
// 检查是否是学生
|
||||||
|
else if (comment.userType === 'student' || comment.user_type === 'student' ||
|
||||||
|
comment.role === 'student' || comment.isStudent === true ||
|
||||||
|
comment.is_student === true ||
|
||||||
|
comment.userRole === 'student' || comment.user_role === 'student' ||
|
||||||
|
comment.type === 'student' ||
|
||||||
|
comment.userCategory === 'student' || comment.user_category === 'student') {
|
||||||
|
userType = 'student'
|
||||||
|
userBadge = '学生'
|
||||||
|
console.log('✅ 识别为学生')
|
||||||
|
} else {
|
||||||
|
console.log('❌ 未识别用户身份,默认为用户')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: comment.id,
|
||||||
|
username: comment.userName || comment.username || '匿名用户',
|
||||||
|
avatar: comment.userAvatar || comment.avatar || '/images/activity/1.png',
|
||||||
|
time: formatTime(comment.createTime || comment.create_time),
|
||||||
|
content: comment.content,
|
||||||
|
isPinned: comment.isPinned || comment.izTop === 1,
|
||||||
|
type: comment.isPinned ? 'pinned' : 'comment',
|
||||||
|
likeCount: comment.likeCount || 0,
|
||||||
|
isLiked: comment.isLiked || false,
|
||||||
|
userType: userType,
|
||||||
|
userBadge: userBadge,
|
||||||
|
replies: (comment.replies || []).map((reply: any) => {
|
||||||
|
// 调试:打印原始回复数据
|
||||||
|
console.log('🔍 原始回复数据:', reply)
|
||||||
|
console.log('🔍 回复用户身份相关字段:', {
|
||||||
|
userType: reply.userType,
|
||||||
|
user_type: reply.user_type,
|
||||||
|
role: reply.role,
|
||||||
|
isTeacher: reply.isTeacher,
|
||||||
|
is_teacher: reply.is_teacher,
|
||||||
|
userRole: reply.userRole,
|
||||||
|
user_role: reply.user_role,
|
||||||
|
type: reply.type,
|
||||||
|
userCategory: reply.userCategory,
|
||||||
|
user_category: reply.user_category
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据接口数据判断用户身份
|
||||||
|
let userType = 'user'
|
||||||
|
let userBadge = '用户'
|
||||||
|
|
||||||
|
// 检查是否是讲师/教师 - 扩展更多可能的字段
|
||||||
|
if (reply.userType === 'teacher' || reply.user_type === 'teacher' ||
|
||||||
|
reply.role === 'teacher' || reply.role === 'instructor' ||
|
||||||
|
reply.isTeacher === true || reply.is_teacher === true ||
|
||||||
|
reply.userRole === 'teacher' || reply.user_role === 'teacher' ||
|
||||||
|
reply.type === 'teacher' || reply.type === 'instructor' ||
|
||||||
|
reply.userCategory === 'teacher' || reply.user_category === 'teacher') {
|
||||||
|
userType = 'instructor'
|
||||||
|
userBadge = '讲师'
|
||||||
|
console.log('✅ 回复识别为讲师')
|
||||||
|
}
|
||||||
|
// 检查是否是学生
|
||||||
|
else if (reply.userType === 'student' || reply.user_type === 'student' ||
|
||||||
|
reply.role === 'student' || reply.isStudent === true ||
|
||||||
|
reply.is_student === true ||
|
||||||
|
reply.userRole === 'student' || reply.user_role === 'student' ||
|
||||||
|
reply.type === 'student' ||
|
||||||
|
reply.userCategory === 'student' || reply.user_category === 'student') {
|
||||||
|
userType = 'student'
|
||||||
|
userBadge = '学生'
|
||||||
|
console.log('✅ 回复识别为学生')
|
||||||
|
} else {
|
||||||
|
console.log('❌ 回复未识别用户身份,默认为用户')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: reply.id || `reply_${Date.now()}`,
|
||||||
|
username: reply.userName || reply.username || reply.user_name || '匿名用户',
|
||||||
|
avatar: reply.userAvatar || reply.avatar || reply.user_avatar || '/images/activity/1.png',
|
||||||
|
time: formatTime(reply.createTime || reply.create_time || reply.time),
|
||||||
|
content: reply.content || reply.text || '',
|
||||||
|
type: userType,
|
||||||
|
badge: userBadge
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 只使用API返回的真实数据
|
||||||
|
displayComments.value = apiComments
|
||||||
|
|
||||||
|
console.log('✅ API评论数据:', apiComments.length, '条')
|
||||||
|
console.log('✅ 转换后的评论数据:', displayComments.value)
|
||||||
|
|
||||||
|
// 调试:查看API返回的原始回复数据
|
||||||
|
if (comments.length > 0 && comments[0].replies) {
|
||||||
|
console.log('🔍 API原始回复数据:', comments[0].replies)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ 评论API返回数据为空或失败')
|
||||||
|
// 显示空状态
|
||||||
|
displayComments.value = []
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('❌ 加载评论失败:', err)
|
||||||
|
console.error('❌ 错误详情:', {
|
||||||
|
message: err.message,
|
||||||
|
status: err.response?.status,
|
||||||
|
statusText: err.response?.statusText,
|
||||||
|
data: err.response?.data,
|
||||||
|
url: err.config?.url
|
||||||
|
})
|
||||||
|
error.value = `加载评论失败: ${err.response?.status || err.message}`
|
||||||
|
|
||||||
|
// 显示空状态
|
||||||
|
displayComments.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 开始回复
|
// 开始回复
|
||||||
const startReply = (commentId: number, username: string) => {
|
const startReply = (commentId: number, username: string) => {
|
||||||
@ -139,11 +343,80 @@ const startReply = (commentId: number, username: string) => {
|
|||||||
replyToUsername.value = username
|
replyToUsername.value = username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提交回复
|
||||||
|
const submitReply = async () => {
|
||||||
|
if (!replyContent.value.trim()) {
|
||||||
|
message.warning('请输入回复内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🚀 发送回复请求:', {
|
||||||
|
content: replyContent.value,
|
||||||
|
targetType: 'comment',
|
||||||
|
targetId: replyingTo.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用专门的回复接口
|
||||||
|
const response = await CommentApi.replyComment({
|
||||||
|
content: replyContent.value,
|
||||||
|
targetType: 'comment',
|
||||||
|
targetId: String(replyingTo.value),
|
||||||
|
parentId: replyingTo.value || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📊 回复API响应:', response)
|
||||||
|
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
message.success('回复成功')
|
||||||
|
replyContent.value = ''
|
||||||
|
replyingTo.value = null
|
||||||
|
replyToUsername.value = ''
|
||||||
|
|
||||||
|
// 重新加载评论
|
||||||
|
await loadComments()
|
||||||
|
} else {
|
||||||
|
message.error(response.data?.message || '回复失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 回复失败:', err)
|
||||||
|
message.error('回复失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点赞评论
|
||||||
|
const likeComment = async (comment: any) => {
|
||||||
|
try {
|
||||||
|
console.log('🚀 发送点赞请求:', comment.id)
|
||||||
|
|
||||||
|
const response = await CommentApi.likeComment(comment.id)
|
||||||
|
|
||||||
|
console.log('📊 点赞API响应:', response)
|
||||||
|
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
comment.isLiked = !comment.isLiked
|
||||||
|
comment.likeCount += comment.isLiked ? 1 : -1
|
||||||
|
message.success(comment.isLiked ? '点赞成功' : '取消点赞')
|
||||||
|
} else {
|
||||||
|
message.error(response.data?.message || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 点赞失败:', err)
|
||||||
|
message.error('点赞失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 取消回复
|
// 取消回复
|
||||||
// const cancelReply = () => {
|
const cancelReply = () => {
|
||||||
// replyingTo.value = null
|
replyContent.value = ''
|
||||||
// replyToUsername.value = ''
|
replyingTo.value = null
|
||||||
// }
|
replyToUsername.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadComments()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -200,6 +473,17 @@ const startReply = (commentId: number, username: string) => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.student-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.comment-text {
|
.comment-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@ -307,6 +591,16 @@ const startReply = (commentId: number, username: string) => {
|
|||||||
color: #52c41a;
|
color: #52c41a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reply-badge.student {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-badge.teacher {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
.reply-time {
|
.reply-time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
@ -345,4 +639,154 @@ const startReply = (commentId: number, username: string) => {
|
|||||||
.reply-action-btn:hover {
|
.reply-action-btn:hover {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 空状态样式 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 加载和错误状态样式 */
|
||||||
|
.loading-container,
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content,
|
||||||
|
.error-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content p,
|
||||||
|
.error-content p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
background: #1890ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:hover {
|
||||||
|
background: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 回复输入框样式 */
|
||||||
|
.reply-input-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea:focus {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-textarea::placeholder {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-input-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-cancel-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
background: white;
|
||||||
|
color: #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-cancel-btn:hover {
|
||||||
|
border-color: #40a9ff;
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-submit-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border: none;
|
||||||
|
background: #1890ff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-submit-btn:hover {
|
||||||
|
background: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-submit-btn:active {
|
||||||
|
background: #096dd9;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,15 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="intro-content">
|
<div class="intro-content">
|
||||||
<h4>课程介绍</h4>
|
<h4>课程介绍</h4>
|
||||||
<img src="/images/courses/课程介绍区.png" alt="课程介绍图片" class="course-intro-image">
|
|
||||||
|
<!-- 课程描述 -->
|
||||||
|
<div v-if="courseDescription" class="course-description" v-html="courseDescription"></div>
|
||||||
|
|
||||||
|
<!-- 课程大纲 -->
|
||||||
|
<div v-if="courseOutline" class="course-outline">
|
||||||
|
<h5>课程大纲</h5>
|
||||||
|
<div class="outline-content" v-html="courseOutline"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 授课目标 -->
|
||||||
|
<div v-if="courseTarget" class="course-target">
|
||||||
|
<h5>授课目标</h5>
|
||||||
|
<div class="target-content" v-html="courseTarget"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预备知识 -->
|
||||||
|
<div v-if="coursePrerequisite" class="course-prerequisite">
|
||||||
|
<h5>预备知识</h5>
|
||||||
|
<div class="prerequisite-content" v-html="coursePrerequisite"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 参考资料 -->
|
||||||
|
<div v-if="courseReference" class="course-reference">
|
||||||
|
<h5>参考资料</h5>
|
||||||
|
<div class="reference-content" v-html="courseReference"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 学时安排 -->
|
||||||
|
<div v-if="courseArrangement" class="course-arrangement">
|
||||||
|
<h5>学时安排</h5>
|
||||||
|
<div class="arrangement-content" v-html="courseArrangement"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 常见问题 -->
|
||||||
|
<div v-if="courseQuestion" class="course-question">
|
||||||
|
<h5>常见问题</h5>
|
||||||
|
<div class="question-content" v-html="courseQuestion"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 课程介绍图片 -->
|
||||||
|
<div v-if="courseIntroImage && !isAvatarImage" class="course-intro-image-container">
|
||||||
|
<img :src="courseIntroImage" alt="课程介绍图片" class="course-intro-image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 如果没有内容,显示空状态 -->
|
||||||
|
<div v-if="!hasContent && !courseIntroImage" class="no-content">
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">📚</div>
|
||||||
|
<p class="empty-text">暂无课程介绍内容</p>
|
||||||
|
<p class="empty-desc">课程介绍信息正在完善中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps<{
|
||||||
|
courseInfo?: {
|
||||||
|
description?: string
|
||||||
|
outline?: string
|
||||||
|
target?: string
|
||||||
|
prerequisite?: string
|
||||||
|
reference?: string
|
||||||
|
arrangement?: string
|
||||||
|
question?: string
|
||||||
|
introImage?: string
|
||||||
|
cover?: string
|
||||||
|
thumbnail?: string
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const courseDescription = computed(() => props.courseInfo?.description)
|
||||||
|
const courseOutline = computed(() => props.courseInfo?.outline)
|
||||||
|
const courseTarget = computed(() => props.courseInfo?.target)
|
||||||
|
const coursePrerequisite = computed(() => props.courseInfo?.prerequisite)
|
||||||
|
const courseReference = computed(() => props.courseInfo?.reference)
|
||||||
|
const courseArrangement = computed(() => props.courseInfo?.arrangement)
|
||||||
|
const courseQuestion = computed(() => props.courseInfo?.question)
|
||||||
|
|
||||||
|
// 课程介绍图片(优先使用 introImage,其次 cover,最后 thumbnail)
|
||||||
|
const courseIntroImage = computed(() => {
|
||||||
|
return props.courseInfo?.introImage || props.courseInfo?.cover || props.courseInfo?.thumbnail
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断是否为头像图片(通过URL路径判断)
|
||||||
|
const isAvatarImage = computed(() => {
|
||||||
|
const imageUrl = courseIntroImage.value
|
||||||
|
if (!imageUrl) return false
|
||||||
|
|
||||||
|
// 检查URL中是否包含头像相关的关键词
|
||||||
|
const avatarKeywords = ['avatar', 'head', 'portrait', 'user', 'profile', 'PixPin']
|
||||||
|
return avatarKeywords.some(keyword => imageUrl.toLowerCase().includes(keyword.toLowerCase()))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查是否有内容
|
||||||
|
const hasContent = computed(() => {
|
||||||
|
return courseDescription.value ||
|
||||||
|
courseOutline.value ||
|
||||||
|
courseTarget.value ||
|
||||||
|
coursePrerequisite.value ||
|
||||||
|
courseReference.value ||
|
||||||
|
courseArrangement.value ||
|
||||||
|
courseQuestion.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.intro-content h4 {
|
.intro-content h4 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -17,6 +120,71 @@
|
|||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.intro-content h5 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin: 20px 0 12px 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #F1F3F4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-description,
|
||||||
|
.outline-content,
|
||||||
|
.target-content,
|
||||||
|
.prerequisite-content,
|
||||||
|
.reference-content,
|
||||||
|
.arrangement-content,
|
||||||
|
.question-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-description {
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-intro-image-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.course-intro-image {
|
.course-intro-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -2,25 +2,11 @@
|
|||||||
<div class="team-content">
|
<div class="team-content">
|
||||||
<h4>教学团队</h4>
|
<h4>教学团队</h4>
|
||||||
<div class="speaker-container">
|
<div class="speaker-container">
|
||||||
<div class="speaker">
|
<div v-for="instructor in props.instructors" :key="instructor.id" class="speaker">
|
||||||
<img src="/images/special/avatar1.png" alt="讲师1">
|
<img :src="instructor.avatar || '/images/special/avatar1.png'" :alt="instructor.name" @error="handleImageError">
|
||||||
<div>
|
<div>
|
||||||
<div class="speaker-name">汪波</div>
|
<div class="speaker-name">{{ instructor.name }}</div>
|
||||||
<div class="speaker-title">教授</div>
|
<div class="speaker-title">{{ instructor.title || '教师' }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="speaker">
|
|
||||||
<img src="/images/special/avatar1.png" alt="讲师2">
|
|
||||||
<div>
|
|
||||||
<div class="speaker-name">汪波</div>
|
|
||||||
<div class="speaker-title">教授</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="speaker">
|
|
||||||
<img src="/images/special/avatar1.png" alt="讲师3">
|
|
||||||
<div>
|
|
||||||
<div class="speaker-name">汪波</div>
|
|
||||||
<div class="speaker-title">教授</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +14,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 教学团队组件
|
// 定义 props
|
||||||
|
const props = defineProps<{
|
||||||
|
instructors?: any[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 处理图片加载错误
|
||||||
|
const handleImageError = (event: Event) => {
|
||||||
|
const img = event.target as HTMLImageElement
|
||||||
|
img.src = '/images/special/avatar1.png'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -335,7 +335,7 @@
|
|||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="actions-section">
|
<div class="actions-section">
|
||||||
<button class="action-btn danger-btn">清空聊天记录</button>
|
<button class="action-btn danger-btn">清空聊天记录</button>
|
||||||
<button class="action-btn danger-btn">退出班级群</button>
|
<button class="action-btn danger-btn" @click="handleExitGroup">退出班级群</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -435,7 +435,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||||
import { NIcon, NBadge, useMessage } from 'naive-ui'
|
import { NIcon, NBadge, useMessage, useDialog } from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
PeopleOutline,
|
PeopleOutline,
|
||||||
ChatbubbleEllipsesOutline
|
ChatbubbleEllipsesOutline
|
||||||
@ -509,6 +509,7 @@ const activeContactId = ref<string | null>(null)
|
|||||||
const messagesContainer = ref<HTMLElement>()
|
const messagesContainer = ref<HTMLElement>()
|
||||||
const messageInputRef = ref()
|
const messageInputRef = ref()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 详情面板状态
|
// 详情面板状态
|
||||||
@ -1119,6 +1120,46 @@ const handleCurrentUserNotDisturb = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理退出群聊
|
||||||
|
const handleExitGroup = async () => {
|
||||||
|
if (!activeContactId.value) return
|
||||||
|
|
||||||
|
// 使用组件化对话框
|
||||||
|
dialog.warning({
|
||||||
|
title: '退出群聊',
|
||||||
|
content: '确定要退出这个群聊吗?退出后将无法接收群聊消息。',
|
||||||
|
positiveText: '确定退出',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
console.log('🚪 退出群聊:', { chatId: activeContactId.value, groupName: activeContact.value?.name })
|
||||||
|
|
||||||
|
// 调用退出群聊API
|
||||||
|
await ChatApi.exitChat(activeContactId.value!)
|
||||||
|
|
||||||
|
message.success('已成功退出群聊')
|
||||||
|
|
||||||
|
// 从联系人列表中移除该群聊
|
||||||
|
const contactIndex = contacts.value.findIndex((c: Contact) => c.id === activeContactId.value)
|
||||||
|
if (contactIndex !== -1) {
|
||||||
|
contacts.value.splice(contactIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空当前会话
|
||||||
|
activeContactId.value = null
|
||||||
|
messages.value = []
|
||||||
|
showDetailsPanel.value = false
|
||||||
|
|
||||||
|
console.log('✅ 退出群聊成功,已从联系人列表中移除')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 退出群聊失败:', error)
|
||||||
|
message.error('退出群聊失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 更新联系人的最后消息
|
// 更新联系人的最后消息
|
||||||
const updateContactLastMessage = (chatId: string, lastMessage: Message) => {
|
const updateContactLastMessage = (chatId: string, lastMessage: Message) => {
|
||||||
const contact = contacts.value.find((c: Contact) => c.id === chatId)
|
const contact = contacts.value.find((c: Contact) => c.id === chatId)
|
||||||
@ -1357,6 +1398,11 @@ const currentMessages = computed(() => {
|
|||||||
|
|
||||||
// 方法定义
|
// 方法定义
|
||||||
const selectContact = async (contactId: string) => {
|
const selectContact = async (contactId: string) => {
|
||||||
|
// 如果详情面板是展开状态,先收起它
|
||||||
|
if (showDetailsPanel.value) {
|
||||||
|
showDetailsPanel.value = false
|
||||||
|
}
|
||||||
|
|
||||||
activeContactId.value = contactId
|
activeContactId.value = contactId
|
||||||
|
|
||||||
// 重置群成员展开状态、群组信息和搜索状态
|
// 重置群成员展开状态、群组信息和搜索状态
|
||||||
@ -1907,8 +1953,8 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-avatar img {
|
.contact-avatar img {
|
||||||
width: 70px;
|
width: 50px;
|
||||||
height: 70px;
|
height: 50px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user