feat: 课程章节部分接口对接(编辑,查询,删除章节), 合并远程更新并恢复本地 api 修改
This commit is contained in:
parent
0c638147f2
commit
21845cb21a
@ -5,6 +5,7 @@ export * from './request'
|
||||
// 导出所有API模块
|
||||
export { default as AuthApi } from './modules/auth'
|
||||
export { default as CourseApi } from './modules/course'
|
||||
export { default as ChapterApi } from './modules/chapter'
|
||||
export { default as CommentApi } from './modules/comment'
|
||||
export { default as FavoriteApi } from './modules/favorite'
|
||||
export { default as OrderApi } from './modules/order'
|
||||
|
314
src/api/modules/chapter.ts
Normal file
314
src/api/modules/chapter.ts
Normal file
@ -0,0 +1,314 @@
|
||||
// 课程章节相关API接口
|
||||
import { ApiRequest } from '../request'
|
||||
import type {
|
||||
ApiResponse,
|
||||
CourseSection,
|
||||
CourseSectionListResponse,
|
||||
BackendCourseSection,
|
||||
} from '../types'
|
||||
|
||||
// 章节查询参数类型
|
||||
export interface ChapterQueryParams {
|
||||
courseId: string
|
||||
keyword?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
type?: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=全部
|
||||
parentId?: string // 父章节ID,用于查询子章节
|
||||
level?: number // 章节层级:0=一级章节、1=二级章节
|
||||
}
|
||||
|
||||
/**
|
||||
* 课程章节API模块
|
||||
*/
|
||||
export class ChapterApi {
|
||||
|
||||
/**
|
||||
* 获取课程章节列表
|
||||
* @param params 查询参数
|
||||
* @returns 章节列表响应
|
||||
*/
|
||||
static async getChapters(params: ChapterQueryParams): Promise<ApiResponse<CourseSectionListResponse>> {
|
||||
try {
|
||||
console.log('🚀 调用课程章节列表API,参数:', params)
|
||||
|
||||
// 构建查询参数 - courseId作为Path参数,其他作为Query参数
|
||||
const queryParams: any = {}
|
||||
|
||||
if (params.keyword) queryParams.keyword = params.keyword
|
||||
if (params.type !== undefined) queryParams.type = params.type
|
||||
if (params.parentId) queryParams.parentId = params.parentId
|
||||
if (params.level !== undefined) queryParams.level = params.level
|
||||
if (params.page) queryParams.page = params.page
|
||||
if (params.pageSize) queryParams.pageSize = params.pageSize
|
||||
|
||||
console.log('🔍 Path参数 courseId (token):', params.courseId)
|
||||
console.log('🔍 Query参数:', queryParams)
|
||||
|
||||
// 调用后端API - courseId作为Path参数,其他作为Query参数
|
||||
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${params.courseId}/section`, queryParams)
|
||||
console.log('🔍 章节列表API响应:', response)
|
||||
|
||||
// 处理后端响应格式
|
||||
let rawData = null;
|
||||
if (response.data && response.data.success && response.data.result) {
|
||||
rawData = response.data.result;
|
||||
console.log('✅ 响应数据来源: result字段');
|
||||
} else if (response.data && response.data.list) {
|
||||
rawData = response.data.list;
|
||||
console.log('✅ 响应数据来源: list字段');
|
||||
} else if (Array.isArray(response.data)) {
|
||||
rawData = response.data;
|
||||
console.log('✅ 响应数据来源: 直接数组');
|
||||
}
|
||||
|
||||
if (rawData && Array.isArray(rawData)) {
|
||||
console.log('✅ 原始章节数据:', rawData)
|
||||
console.log('✅ 章节数据数量:', rawData.length)
|
||||
|
||||
// 适配数据格式 - 直接使用原始数据,因为字段名已经匹配
|
||||
const adaptedSections: CourseSection[] = rawData.map((section: any) => ({
|
||||
id: section.id,
|
||||
lessonId: section.lessonId, // 直接使用原始字段
|
||||
outline: section.outline || '', // 直接使用原始字段
|
||||
name: section.name,
|
||||
type: section.type, // 直接使用原始字段
|
||||
parentId: section.parentId || '', // 直接使用原始字段
|
||||
sort: section.sort, // 直接使用原始字段
|
||||
level: section.level,
|
||||
revision: section.revision || 1, // 直接使用原始字段
|
||||
createdAt: section.createdAt, // 直接使用原始字段
|
||||
updatedAt: section.updatedAt, // 直接使用原始字段
|
||||
deletedAt: section.deletedAt,
|
||||
completed: section.completed || false,
|
||||
duration: section.duration
|
||||
}))
|
||||
|
||||
console.log('✅ 适配后的章节数据:', adaptedSections)
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
list: adaptedSections,
|
||||
timestamp: Date.now(),
|
||||
traceId: ''
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||||
return {
|
||||
code: 500,
|
||||
message: '数据格式错误',
|
||||
data: {
|
||||
list: [],
|
||||
timestamp: Date.now(),
|
||||
traceId: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 章节API调用失败:', error)
|
||||
console.error('❌ 错误详情:', {
|
||||
message: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
response: (error as any).response?.data,
|
||||
status: (error as any).response?.status,
|
||||
statusText: (error as any).response?.statusText
|
||||
})
|
||||
|
||||
// 重新抛出错误,不使用模拟数据
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索课程章节
|
||||
* @param params 搜索参数
|
||||
* @returns 搜索结果
|
||||
*/
|
||||
static async searchChapters(params: ChapterQueryParams): Promise<ApiResponse<CourseSectionListResponse>> {
|
||||
try {
|
||||
console.log('🔍 搜索课程章节,参数:', params)
|
||||
|
||||
// 构建搜索参数 - courseId作为Path参数,keyword等作为Query参数
|
||||
const searchParams: any = {}
|
||||
|
||||
if (params.keyword) searchParams.keyword = params.keyword
|
||||
if (params.type !== undefined) searchParams.type = params.type
|
||||
if (params.parentId) searchParams.parentId = params.parentId
|
||||
if (params.level !== undefined) searchParams.level = params.level
|
||||
if (params.page) searchParams.page = params.page
|
||||
if (params.pageSize) searchParams.pageSize = params.pageSize
|
||||
|
||||
console.log('🔍 Path参数 courseId (token):', params.courseId)
|
||||
console.log('🔍 Query参数 (包含keyword):', searchParams)
|
||||
|
||||
// 调用后端API - courseId作为Path参数,keyword等作为Query参数
|
||||
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${params.courseId}/section`, searchParams)
|
||||
console.log('🔍 章节搜索API响应:', response)
|
||||
|
||||
// 处理后端响应格式
|
||||
if (response.data && response.data.success && response.data.result) {
|
||||
console.log('✅ 搜索响应状态码:', response.data.code)
|
||||
console.log('✅ 搜索响应消息:', response.data.message)
|
||||
console.log('✅ 搜索结果数据:', response.data.result)
|
||||
console.log('✅ 搜索结果数量:', response.data.result.length || 0)
|
||||
|
||||
// 适配数据格式 - 使用BackendCourseSection类型
|
||||
const adaptedSections: CourseSection[] = response.data.result.map((section: BackendCourseSection) => ({
|
||||
id: section.id,
|
||||
lessonId: section.courseId, // 使用BackendCourseSection字段
|
||||
outline: '',
|
||||
name: section.name,
|
||||
type: section.type,
|
||||
parentId: section.parentId || '', // 使用BackendCourseSection字段
|
||||
sort: section.sortOrder, // 使用BackendCourseSection字段
|
||||
level: section.level,
|
||||
revision: 1,
|
||||
createdAt: section.createTime ? new Date(section.createTime).getTime() : null, // 使用BackendCourseSection字段
|
||||
updatedAt: section.updateTime ? new Date(section.updateTime).getTime() : null, // 使用BackendCourseSection字段
|
||||
deletedAt: null,
|
||||
completed: false,
|
||||
duration: undefined
|
||||
}))
|
||||
|
||||
console.log('✅ 适配后的搜索结果:', adaptedSections)
|
||||
|
||||
return {
|
||||
code: response.data.code,
|
||||
message: response.data.message,
|
||||
data: {
|
||||
list: adaptedSections,
|
||||
timestamp: Date.now(),
|
||||
traceId: response.data.timestamp?.toString() || ''
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 搜索API返回的数据结构不正确:', response.data)
|
||||
return {
|
||||
code: 500,
|
||||
message: '搜索数据格式错误',
|
||||
data: {
|
||||
list: [],
|
||||
timestamp: Date.now(),
|
||||
traceId: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 章节搜索API调用失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建课程章节
|
||||
* @param sectionData 章节数据
|
||||
* @returns 创建结果
|
||||
*/
|
||||
static async createChapter(sectionData: any): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 调用新建章节API,数据:', sectionData)
|
||||
|
||||
// 包装数据为aiolCourseSectionDTO格式
|
||||
const requestData = {
|
||||
aiolCourseSectionDTO: sectionData
|
||||
}
|
||||
|
||||
// 调用后端API - 新建章节
|
||||
const response = await ApiRequest.post<any>('/aiol/aiolCourseSection/add', requestData)
|
||||
console.log('🔍 新建章节API响应:', response)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 新建章节失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑课程章节
|
||||
* @param sectionData 章节数据
|
||||
* @returns 编辑结果
|
||||
*/
|
||||
static async editChapter(sectionData: any): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 调用编辑章节API,数据:', sectionData)
|
||||
|
||||
// 尝试不同的数据格式
|
||||
const requestData = {
|
||||
...sectionData,
|
||||
// 确保所有必要字段都存在
|
||||
id: sectionData.id,
|
||||
name: sectionData.name,
|
||||
courseId: sectionData.courseId,
|
||||
type: sectionData.type || 0,
|
||||
sortOrder: sectionData.sortOrder || 10,
|
||||
parentId: sectionData.parentId || '0',
|
||||
level: sectionData.level || 1
|
||||
}
|
||||
|
||||
console.log('🔍 发送给服务器的完整请求数据:', JSON.stringify(requestData, null, 2))
|
||||
console.log('🔍 章节ID:', sectionData.id)
|
||||
console.log('🔍 章节名称:', sectionData.name)
|
||||
console.log('🔍 课程ID:', sectionData.courseId)
|
||||
|
||||
// 调用后端API - 编辑章节
|
||||
// 使用原来的edit路径
|
||||
const response = await ApiRequest.post<any>('/aiol/aiolCourseSection/edit', requestData)
|
||||
console.log('🔍 编辑章节API响应:', response)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 编辑章节失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除课程章节
|
||||
* @param sectionId 章节ID
|
||||
* @returns 删除结果
|
||||
*/
|
||||
static async deleteChapter(sectionId: string): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 调用删除章节API,章节ID:', sectionId)
|
||||
|
||||
// 调用后端API - 删除章节
|
||||
const response = await ApiRequest.delete<any>('/aiol/aiolCourseSection/delete', {
|
||||
id: sectionId
|
||||
})
|
||||
console.log('🔍 删除章节API响应:', response)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 删除章节失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除课程章节
|
||||
* @param sectionIds 章节ID数组
|
||||
* @returns 删除结果
|
||||
*/
|
||||
static async deleteChaptersBatch(sectionIds: string[]): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 调用批量删除章节API,章节IDs:', sectionIds)
|
||||
|
||||
// 调用后端API - 批量删除章节
|
||||
const response = await ApiRequest.delete<any>('/aiol/aiolCourseSection/deleteBatch', {
|
||||
ids: sectionIds.join(',')
|
||||
})
|
||||
console.log('🔍 批量删除章节API响应:', response)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 批量删除章节失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChapterApi
|
@ -462,6 +462,17 @@ export interface BackendCourseSectionListResponse {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 章节查询参数类型
|
||||
export interface ChapterQueryParams {
|
||||
courseId: string
|
||||
keyword?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
type?: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=全部
|
||||
parentId?: string // 父章节ID,用于查询子章节
|
||||
level?: number // 章节层级:0=一级章节、1=二级章节
|
||||
}
|
||||
|
||||
// 后端讲师数据结构
|
||||
export interface BackendInstructor {
|
||||
id: string
|
||||
|
@ -415,7 +415,10 @@ const handleOfflineCourse = (course: CourseDisplayItem) => {
|
||||
id: course.id!,
|
||||
name: course.name,
|
||||
description: course.description,
|
||||
status: 2 // 2=已结束状态
|
||||
status: 2, // 2=已结束状态
|
||||
pause_exit: '0', // 默认值
|
||||
allow_speed: '0', // 默认值
|
||||
show_subtitle: '0' // 默认值
|
||||
};
|
||||
|
||||
await TeachCourseApi.editCourse(updatedData);
|
||||
|
@ -188,7 +188,6 @@ import {
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
// @ts-ignore
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import TeachCourseApi from '@/api/modules/teachCourse'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,7 @@
|
||||
<n-button @click="exportChapters">导出</n-button>
|
||||
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
|
||||
<div class="search-container">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="请输入想要搜索的内容"
|
||||
style="width: 200px;"
|
||||
>
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" style="width: 200px;">
|
||||
</n-input>
|
||||
<n-button type="primary" @click="searchChapters">搜索</n-button>
|
||||
</div>
|
||||
@ -24,9 +20,9 @@
|
||||
|
||||
<!-- 章节列表表格 -->
|
||||
<div class="table-box">
|
||||
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey"
|
||||
:checked-row-keys="selectedChapters" @update:checked-row-keys="handleCheck" :bordered="false"
|
||||
:single-line="false" size="medium" class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" />
|
||||
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey" :checked-row-keys="selectedChapters"
|
||||
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
|
||||
class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" :loading="loading" />
|
||||
|
||||
<!-- 自定义分页器 -->
|
||||
<div class="custom-pagination">
|
||||
@ -61,31 +57,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ImportModal
|
||||
v-model:show="showImportModal"
|
||||
template-name="custom_template.xlsx"
|
||||
import-type="custom"
|
||||
@success="handleImportSuccess"
|
||||
@template-download="handleTemplateDownload" />
|
||||
<ImportModal v-model:show="showImportModal" template-name="custom_template.xlsx" import-type="custom"
|
||||
@success="handleImportSuccess" @template-download="handleTemplateDownload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { NButton, useMessage, NDataTable, NInput, NSpace } from 'naive-ui'
|
||||
import { ref, computed, h, onMounted } from 'vue'
|
||||
import { NButton, useMessage, NDataTable, NInput, NSpace, useDialog } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import ImportModal from '@/components/common/ImportModal.vue'
|
||||
import { ChapterApi } from '@/api'
|
||||
import type { ChapterQueryParams, CourseSection } from '@/api/types'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 章节类型定义
|
||||
interface Chapter {
|
||||
id: number
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
sort: string | number
|
||||
@ -93,157 +88,155 @@ interface Chapter {
|
||||
isParent: boolean
|
||||
children?: Chapter[]
|
||||
expanded?: boolean
|
||||
level?: number
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
|
||||
const showImportModal = ref(false)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// 获取当前用户ID作为courseId参数
|
||||
const courseId = computed(() => userStore.user?.id?.toString() || '')
|
||||
|
||||
const handleImportSuccess = () => {
|
||||
message.success('章节导入成功')
|
||||
// 重新加载章节列表
|
||||
loadChapters()
|
||||
}
|
||||
|
||||
const handleTemplateDownload = () => {
|
||||
message.success('模板下载成功')
|
||||
}
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 选中的章节
|
||||
const selectedChapters = ref<number[]>([])
|
||||
const selectedChapters = ref<string[]>([])
|
||||
|
||||
// 章节列表数据
|
||||
const chapterList = ref<Chapter[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '第一章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: '开课彩蛋:新开始新征程',
|
||||
type: '视频',
|
||||
sort: 1,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '课件准备PPT',
|
||||
type: '课件',
|
||||
sort: 2,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '第一节 课程定位与目标',
|
||||
type: '视频',
|
||||
sort: 3,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '第二节 教学安排及学习建议',
|
||||
type: '作业',
|
||||
sort: 4,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '第三节 教学安排及学习建议',
|
||||
type: '考试',
|
||||
sort: 5,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
const chapterList = ref<Chapter[]>([])
|
||||
|
||||
// 原始章节数据(用于搜索过滤)
|
||||
const originalChapterList = ref<Chapter[]>([])
|
||||
|
||||
// 加载章节数据
|
||||
const loadChapters = async () => {
|
||||
if (!courseId.value) {
|
||||
message.error('用户未登录,无法获取章节数据')
|
||||
return
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '第二章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 8,
|
||||
name: '第一节 新开始新征程',
|
||||
type: '视频',
|
||||
sort: 1,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: '第二节 教学安排及学习建议',
|
||||
type: '课件',
|
||||
sort: 2,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const params: ChapterQueryParams = {
|
||||
courseId: courseId.value,
|
||||
page: 1,
|
||||
pageSize: 1000 // 获取所有章节
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '第三章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 12,
|
||||
name: '第一节 新开始新征程',
|
||||
type: '视频',
|
||||
sort: 1,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: '第二节 教学安排及学习建议',
|
||||
type: '课件',
|
||||
sort: 2,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
|
||||
const response = await ChapterApi.getChapters(params)
|
||||
|
||||
console.log('🔍 完整API响应:', response)
|
||||
console.log('🔍 response.data类型:', typeof response.data)
|
||||
console.log('🔍 response.data是否为数组:', Array.isArray(response.data))
|
||||
console.log('🔍 response.data内容:', response.data)
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
// 检查response.data是否有list属性
|
||||
if (response.data.list && Array.isArray(response.data.list)) {
|
||||
// 转换API数据为组件需要的格式
|
||||
const chapters = convertApiDataToChapter(response.data.list)
|
||||
chapterList.value = chapters
|
||||
originalChapterList.value = chapters
|
||||
message.success('章节加载成功')
|
||||
} else {
|
||||
console.error('❌ response.data.list不是数组:', response.data)
|
||||
throw new Error('API返回的数据格式不正确')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: '第四章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
} else {
|
||||
throw new Error(response.message || '获取章节失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('加载章节失败:', err)
|
||||
error.value = err.message || '加载章节失败'
|
||||
message.error(error.value)
|
||||
|
||||
// API调用失败时显示空列表,不使用模拟数据
|
||||
chapterList.value = []
|
||||
originalChapterList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 转换API数据为组件格式
|
||||
const convertApiDataToChapter = (apiData: any[]): Chapter[] => {
|
||||
console.log('🔍 convertApiDataToChapter 输入数据:', apiData)
|
||||
console.log('🔍 输入数据类型:', typeof apiData)
|
||||
console.log('🔍 输入数据是否为数组:', Array.isArray(apiData))
|
||||
|
||||
if (!Array.isArray(apiData)) {
|
||||
console.error('❌ apiData不是数组:', apiData)
|
||||
return []
|
||||
}
|
||||
|
||||
// 按 sort 排序
|
||||
const sortedData = [...apiData].sort((a, b) => (a.sort || 0) - (b.sort || 0))
|
||||
|
||||
return sortedData.map(section => ({
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
type: mapChapterType(section.type),
|
||||
sort: section.sort,
|
||||
createTime: section.createdAt ? new Date(section.createdAt).toLocaleString() : '',
|
||||
isParent: section.level === 1, // level=1 表示章,为父章节
|
||||
level: section.level,
|
||||
parentId: section.parentId,
|
||||
expanded: false,
|
||||
children: []
|
||||
}))
|
||||
}
|
||||
])
|
||||
|
||||
// 映射章节类型 - 根据数据库字段说明:0:视频、1:资料、2:考试、3:作业
|
||||
const mapChapterType = (type: number | null): string => {
|
||||
switch (type) {
|
||||
case 0: return '视频'
|
||||
case 1: return '资料'
|
||||
case 2: return '考试'
|
||||
case 3: return '作业'
|
||||
default: return '-'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 扁平化章节列表(用于显示和分页)
|
||||
const flattenedChapters = computed(() => {
|
||||
const result: Chapter[] = []
|
||||
|
||||
const flatten = (chapters: Chapter[]) => {
|
||||
// 分离章节(level=1)和节(level=2),过滤掉level=0的项目
|
||||
const chapters = chapterList.value.filter(item => item.level === 1)
|
||||
const sections = chapterList.value.filter(item => item.level === 2)
|
||||
|
||||
// 为每个章节添加其子节
|
||||
chapters.forEach(chapter => {
|
||||
chapter.children = sections.filter(section => section.parentId === chapter.id)
|
||||
result.push(chapter)
|
||||
if (chapter.children && chapter.expanded) {
|
||||
flatten(chapter.children)
|
||||
}
|
||||
|
||||
// 如果章节展开,添加其子节
|
||||
if (chapter.expanded && chapter.children) {
|
||||
chapter.children.forEach(section => {
|
||||
result.push(section)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
flatten(chapterList.value)
|
||||
return result
|
||||
})
|
||||
|
||||
@ -302,7 +295,7 @@ const paginatedChapters = computed(() => {
|
||||
const rowKey = (row: Chapter) => row.id
|
||||
|
||||
// 表格选择处理
|
||||
const handleCheck = (rowKeys: number[]) => {
|
||||
const handleCheck = (rowKeys: string[]) => {
|
||||
selectedChapters.value = rowKeys
|
||||
}
|
||||
|
||||
@ -351,7 +344,7 @@ const goToPage = (page: string | number) => {
|
||||
|
||||
// 展开/收起章节
|
||||
const toggleChapter = (chapter: Chapter) => {
|
||||
if (chapter.isParent && chapter.children) {
|
||||
if (chapter.level === 1 && chapter.children) {
|
||||
chapter.expanded = !chapter.expanded
|
||||
}
|
||||
}
|
||||
@ -367,6 +360,7 @@ const addChapter = () => {
|
||||
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
||||
}
|
||||
|
||||
|
||||
const importChapters = () => {
|
||||
showImportModal.value = true
|
||||
}
|
||||
@ -375,39 +369,168 @@ const exportChapters = () => {
|
||||
message.info('导出章节功能')
|
||||
}
|
||||
|
||||
const deleteSelected = () => {
|
||||
const deleteSelected = async () => {
|
||||
if (selectedChapters.value.length === 0) return
|
||||
if (confirm(`确定要删除选中的 ${selectedChapters.value.length} 个章节吗?`)) {
|
||||
selectedChapters.value.forEach((id: number) => {
|
||||
|
||||
try {
|
||||
if (!userStore.user?.id) {
|
||||
message.error('用户未登录,无法删除章节')
|
||||
return
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
const confirmed = await new Promise<boolean>((resolve) => {
|
||||
dialog.warning({
|
||||
title: '确认批量删除章节',
|
||||
content: `确定要删除选中的 ${selectedChapters.value.length} 个章节吗?删除后无法恢复。`,
|
||||
positiveText: '确认删除',
|
||||
negativeText: '取消',
|
||||
positiveButtonProps: {
|
||||
type: 'error'
|
||||
},
|
||||
onPositiveClick: () => {
|
||||
resolve(true)
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
// 使用批量删除API
|
||||
const response = await ChapterApi.deleteChaptersBatch(selectedChapters.value)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
message.success(`成功删除 ${selectedChapters.value.length} 个章节`)
|
||||
|
||||
// 从列表中移除已删除的章节
|
||||
selectedChapters.value.forEach((id: string) => {
|
||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === id)
|
||||
if (index > -1) {
|
||||
chapterList.value.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
selectedChapters.value = []
|
||||
message.success('删除成功')
|
||||
} else {
|
||||
message.error('批量删除失败:' + (response.data?.message || '未知错误'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ 批量删除章节失败:', error)
|
||||
message.error('批量删除失败:' + (error.message || '网络错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const searchChapters = () => {
|
||||
message.info('搜索章节: ' + searchKeyword.value)
|
||||
const searchChapters = async () => {
|
||||
if (!courseId.value) {
|
||||
message.error('用户未登录,无法搜索章节')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const params: ChapterQueryParams = {
|
||||
courseId: courseId.value,
|
||||
keyword: searchKeyword.value,
|
||||
page: 1,
|
||||
pageSize: 1000
|
||||
}
|
||||
|
||||
const response = await ChapterApi.searchChapters(params)
|
||||
|
||||
if (response.code === 200 && response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||
// 转换API数据为组件需要的格式
|
||||
const chapters = convertApiDataToChapter(response.data.list)
|
||||
chapterList.value = chapters
|
||||
currentPage.value = 1
|
||||
message.success(`搜索到 ${chapters.length} 个章节`)
|
||||
} else {
|
||||
throw new Error(response.message || '搜索章节失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('搜索章节失败:', err)
|
||||
error.value = err.message || '搜索章节失败'
|
||||
message.error(error.value)
|
||||
|
||||
// 搜索失败时显示空列表,不使用本地过滤
|
||||
chapterList.value = []
|
||||
currentPage.value = 1
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editChapter = (chapter: Chapter) => {
|
||||
message.info('编辑章节: ' + chapter.name)
|
||||
console.log('编辑章节:', chapter)
|
||||
// 跳转到章节编辑器页面
|
||||
const courseId = route.params.id
|
||||
if (courseId) {
|
||||
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
||||
} else {
|
||||
message.error('课程ID不存在')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteChapter = (chapter: Chapter) => {
|
||||
if (confirm('确定要删除这个章节吗?')) {
|
||||
// 在 setup 函数中初始化 dialog
|
||||
const dialog = useDialog()
|
||||
|
||||
const deleteChapter = async (chapter: Chapter) => {
|
||||
try {
|
||||
if (!userStore.user?.id) {
|
||||
message.error('用户未登录,无法删除章节')
|
||||
return
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
const confirmed = await new Promise<boolean>((resolve) => {
|
||||
dialog.warning({
|
||||
title: '确认删除章节',
|
||||
content: `确定要删除章节"${chapter.name}"吗?删除后无法恢复。`,
|
||||
positiveText: '确认删除',
|
||||
negativeText: '取消',
|
||||
positiveButtonProps: {
|
||||
type: 'error'
|
||||
},
|
||||
onPositiveClick: () => {
|
||||
resolve(true)
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
// 调用删除API
|
||||
const response = await ChapterApi.deleteChapter(chapter.id.toString())
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
message.success('章节删除成功!')
|
||||
|
||||
// 从列表中移除已删除的章节
|
||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === chapter.id)
|
||||
if (index > -1) {
|
||||
chapterList.value.splice(index, 1)
|
||||
message.success('删除成功')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error('章节删除失败:' + (response.data?.message || '未知错误'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ 删除章节失败:', error)
|
||||
message.error('删除章节失败:' + (error.message || '网络错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadChapters()
|
||||
})
|
||||
|
||||
// 表格列配置 - 使用 minWidth 实现响应式
|
||||
const columns: DataTableColumns<Chapter> = [
|
||||
{
|
||||
@ -423,17 +546,18 @@ const columns: DataTableColumns<Chapter> = [
|
||||
tooltip: true
|
||||
},
|
||||
render: (row: Chapter) => {
|
||||
const isChapter = row.level === 1; // level=1 表示章
|
||||
return h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '20px',
|
||||
cursor: row.isParent ? 'pointer' : 'default',
|
||||
marginLeft: row.isParent ? '0px' : '-3px'
|
||||
cursor: isChapter ? 'pointer' : 'default',
|
||||
marginLeft: isChapter ? '0px' : '-3px'
|
||||
},
|
||||
onClick: row.isParent ? () => toggleChapter(row) : undefined
|
||||
onClick: isChapter ? () => toggleChapter(row) : undefined
|
||||
}, [
|
||||
row.isParent ? h('i', {
|
||||
isChapter ? h('i', {
|
||||
class: 'n-base-icon',
|
||||
style: {
|
||||
transition: 'transform 0.2s',
|
||||
@ -454,10 +578,10 @@ const columns: DataTableColumns<Chapter> = [
|
||||
]) : null,
|
||||
h('span', {
|
||||
style: {
|
||||
color: row.isParent ? '#062333' : '#666666',
|
||||
color: isChapter ? '#062333' : '#666666',
|
||||
fontSize: '14px',
|
||||
fontWeight: row.isParent ? '500' : 'normal',
|
||||
marginLeft: row.isParent ? '0' : '24px'
|
||||
fontWeight: isChapter ? '500' : 'normal',
|
||||
marginLeft: isChapter ? '0' : '24px'
|
||||
}
|
||||
}, row.name)
|
||||
])
|
||||
@ -468,7 +592,8 @@ const columns: DataTableColumns<Chapter> = [
|
||||
key: 'type',
|
||||
minWidth: 60,
|
||||
render: (row: Chapter) => {
|
||||
if (row.type === '-') {
|
||||
const isChapter = row.level === 1; // level=1 表示章
|
||||
if (isChapter || row.type === '-') {
|
||||
return h('span', { style: { color: '#BABABA' } }, '-')
|
||||
}
|
||||
return h('div', {
|
||||
@ -489,6 +614,10 @@ const columns: DataTableColumns<Chapter> = [
|
||||
key: 'sort',
|
||||
minWidth: 50,
|
||||
render: (row: Chapter) => {
|
||||
const isChapter = row.level === 1; // level=1 表示章
|
||||
if (isChapter) {
|
||||
return h('span', { style: { color: '#BABABA' } }, '-')
|
||||
}
|
||||
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user