feat: 课程章节部分接口对接(编辑,查询,删除章节), 合并远程更新并恢复本地 api 修改
This commit is contained in:
parent
0c638147f2
commit
21845cb21a
@ -5,6 +5,7 @@ export * from './request'
|
|||||||
// 导出所有API模块
|
// 导出所有API模块
|
||||||
export { default as AuthApi } from './modules/auth'
|
export { default as AuthApi } from './modules/auth'
|
||||||
export { default as CourseApi } from './modules/course'
|
export { default as CourseApi } from './modules/course'
|
||||||
|
export { default as ChapterApi } from './modules/chapter'
|
||||||
export { default as CommentApi } from './modules/comment'
|
export { default as CommentApi } from './modules/comment'
|
||||||
export { default as FavoriteApi } from './modules/favorite'
|
export { default as FavoriteApi } from './modules/favorite'
|
||||||
export { default as OrderApi } from './modules/order'
|
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
|
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 {
|
export interface BackendInstructor {
|
||||||
id: string
|
id: string
|
||||||
|
@ -415,7 +415,10 @@ const handleOfflineCourse = (course: CourseDisplayItem) => {
|
|||||||
id: course.id!,
|
id: course.id!,
|
||||||
name: course.name,
|
name: course.name,
|
||||||
description: course.description,
|
description: course.description,
|
||||||
status: 2 // 2=已结束状态
|
status: 2, // 2=已结束状态
|
||||||
|
pause_exit: '0', // 默认值
|
||||||
|
allow_speed: '0', // 默认值
|
||||||
|
show_subtitle: '0' // 默认值
|
||||||
};
|
};
|
||||||
|
|
||||||
await TeachCourseApi.editCourse(updatedData);
|
await TeachCourseApi.editCourse(updatedData);
|
||||||
|
@ -188,7 +188,6 @@ import {
|
|||||||
import '@wangeditor/editor/dist/css/style.css'
|
import '@wangeditor/editor/dist/css/style.css'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
import TeachCourseApi from '@/api/modules/teachCourse'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
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 @click="exportChapters">导出</n-button>
|
||||||
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
|
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<n-input
|
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" style="width: 200px;">
|
||||||
v-model:value="searchKeyword"
|
|
||||||
placeholder="请输入想要搜索的内容"
|
|
||||||
style="width: 200px;"
|
|
||||||
>
|
|
||||||
</n-input>
|
</n-input>
|
||||||
<n-button type="primary" @click="searchChapters">搜索</n-button>
|
<n-button type="primary" @click="searchChapters">搜索</n-button>
|
||||||
</div>
|
</div>
|
||||||
@ -24,9 +20,9 @@
|
|||||||
|
|
||||||
<!-- 章节列表表格 -->
|
<!-- 章节列表表格 -->
|
||||||
<div class="table-box">
|
<div class="table-box">
|
||||||
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey"
|
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey" :checked-row-keys="selectedChapters"
|
||||||
:checked-row-keys="selectedChapters" @update:checked-row-keys="handleCheck" :bordered="false"
|
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
|
||||||
:single-line="false" size="medium" class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" />
|
class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" :loading="loading" />
|
||||||
|
|
||||||
<!-- 自定义分页器 -->
|
<!-- 自定义分页器 -->
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
@ -56,36 +52,35 @@
|
|||||||
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
|
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
|
||||||
@click="goToPage('last')">
|
@click="goToPage('last')">
|
||||||
尾页
|
尾页
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImportModal
|
<ImportModal v-model:show="showImportModal" template-name="custom_template.xlsx" import-type="custom"
|
||||||
v-model:show="showImportModal"
|
@success="handleImportSuccess" @template-download="handleTemplateDownload" />
|
||||||
template-name="custom_template.xlsx"
|
|
||||||
import-type="custom"
|
|
||||||
@success="handleImportSuccess"
|
|
||||||
@template-download="handleTemplateDownload" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, h } from 'vue'
|
import { ref, computed, h, onMounted } from 'vue'
|
||||||
import { NButton, useMessage, NDataTable, NInput, NSpace } from 'naive-ui'
|
import { NButton, useMessage, NDataTable, NInput, NSpace, useDialog } from 'naive-ui'
|
||||||
import type { DataTableColumns } from 'naive-ui'
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import ImportModal from '@/components/common/ImportModal.vue'
|
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 router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
// 章节类型定义
|
// 章节类型定义
|
||||||
interface Chapter {
|
interface Chapter {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
sort: string | number
|
sort: string | number
|
||||||
@ -93,157 +88,155 @@ interface Chapter {
|
|||||||
isParent: boolean
|
isParent: boolean
|
||||||
children?: Chapter[]
|
children?: Chapter[]
|
||||||
expanded?: boolean
|
expanded?: boolean
|
||||||
|
level?: number
|
||||||
|
parentId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const showImportModal = ref(false)
|
const showImportModal = ref(false)
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
// 获取当前用户ID作为courseId参数
|
||||||
|
const courseId = computed(() => userStore.user?.id?.toString() || '')
|
||||||
|
|
||||||
const handleImportSuccess = () => {
|
const handleImportSuccess = () => {
|
||||||
message.success('章节导入成功')
|
message.success('章节导入成功')
|
||||||
|
// 重新加载章节列表
|
||||||
|
loadChapters()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTemplateDownload = () => {
|
const handleTemplateDownload = () => {
|
||||||
message.success('模板下载成功')
|
message.success('模板下载成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
// 选中的章节
|
// 选中的章节
|
||||||
const selectedChapters = ref<number[]>([])
|
const selectedChapters = ref<string[]>([])
|
||||||
|
|
||||||
// 章节列表数据
|
// 章节列表数据
|
||||||
const chapterList = ref<Chapter[]>([
|
const chapterList = ref<Chapter[]>([])
|
||||||
{
|
|
||||||
id: 1,
|
// 原始章节数据(用于搜索过滤)
|
||||||
name: '第一章 课前准备',
|
const originalChapterList = ref<Chapter[]>([])
|
||||||
type: '-',
|
|
||||||
sort: '-',
|
// 加载章节数据
|
||||||
createTime: '2025.07.25 09:20',
|
const loadChapters = async () => {
|
||||||
isParent: true,
|
if (!courseId.value) {
|
||||||
expanded: false,
|
message.error('用户未登录,无法获取章节数据')
|
||||||
children: [
|
return
|
||||||
{
|
}
|
||||||
id: 2,
|
|
||||||
name: '开课彩蛋:新开始新征程',
|
try {
|
||||||
type: '视频',
|
loading.value = true
|
||||||
sort: 1,
|
error.value = ''
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
const params: ChapterQueryParams = {
|
||||||
},
|
courseId: courseId.value,
|
||||||
{
|
page: 1,
|
||||||
id: 3,
|
pageSize: 1000 // 获取所有章节
|
||||||
name: '课件准备PPT',
|
}
|
||||||
type: '课件',
|
|
||||||
sort: 2,
|
const response = await ChapterApi.getChapters(params)
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
console.log('🔍 完整API响应:', response)
|
||||||
},
|
console.log('🔍 response.data类型:', typeof response.data)
|
||||||
{
|
console.log('🔍 response.data是否为数组:', Array.isArray(response.data))
|
||||||
id: 4,
|
console.log('🔍 response.data内容:', response.data)
|
||||||
name: '第一节 课程定位与目标',
|
|
||||||
type: '视频',
|
if (response.code === 200 && response.data) {
|
||||||
sort: 3,
|
// 检查response.data是否有list属性
|
||||||
createTime: '2025.07.25 09:20',
|
if (response.data.list && Array.isArray(response.data.list)) {
|
||||||
isParent: false
|
// 转换API数据为组件需要的格式
|
||||||
},
|
const chapters = convertApiDataToChapter(response.data.list)
|
||||||
{
|
chapterList.value = chapters
|
||||||
id: 5,
|
originalChapterList.value = chapters
|
||||||
name: '第二节 教学安排及学习建议',
|
message.success('章节加载成功')
|
||||||
type: '作业',
|
} else {
|
||||||
sort: 4,
|
console.error('❌ response.data.list不是数组:', response.data)
|
||||||
createTime: '2025.07.25 09:20',
|
throw new Error('API返回的数据格式不正确')
|
||||||
isParent: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: '第三节 教学安排及学习建议',
|
|
||||||
type: '考试',
|
|
||||||
sort: 5,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
}
|
}
|
||||||
]
|
} else {
|
||||||
},
|
throw new Error(response.message || '获取章节失败')
|
||||||
{
|
}
|
||||||
id: 7,
|
} catch (err: any) {
|
||||||
name: '第二章 课前准备',
|
console.error('加载章节失败:', err)
|
||||||
type: '-',
|
error.value = err.message || '加载章节失败'
|
||||||
sort: '-',
|
message.error(error.value)
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: true,
|
// API调用失败时显示空列表,不使用模拟数据
|
||||||
expanded: false,
|
chapterList.value = []
|
||||||
children: [
|
originalChapterList.value = []
|
||||||
{
|
} finally {
|
||||||
id: 8,
|
loading.value = false
|
||||||
name: '第一节 新开始新征程',
|
}
|
||||||
type: '视频',
|
}
|
||||||
sort: 1,
|
|
||||||
createTime: '2025.07.25 09:20',
|
// 转换API数据为组件格式
|
||||||
isParent: false
|
const convertApiDataToChapter = (apiData: any[]): Chapter[] => {
|
||||||
},
|
console.log('🔍 convertApiDataToChapter 输入数据:', apiData)
|
||||||
{
|
console.log('🔍 输入数据类型:', typeof apiData)
|
||||||
id: 9,
|
console.log('🔍 输入数据是否为数组:', Array.isArray(apiData))
|
||||||
name: '第二节 教学安排及学习建议',
|
|
||||||
type: '课件',
|
if (!Array.isArray(apiData)) {
|
||||||
sort: 2,
|
console.error('❌ apiData不是数组:', apiData)
|
||||||
createTime: '2025.07.25 09:20',
|
return []
|
||||||
isParent: false
|
}
|
||||||
}
|
|
||||||
]
|
// 按 sort 排序
|
||||||
},
|
const sortedData = [...apiData].sort((a, b) => (a.sort || 0) - (b.sort || 0))
|
||||||
{
|
|
||||||
id: 10,
|
return sortedData.map(section => ({
|
||||||
name: '第三章 课前准备',
|
id: section.id,
|
||||||
type: '-',
|
name: section.name,
|
||||||
sort: '-',
|
type: mapChapterType(section.type),
|
||||||
createTime: '2025.07.25 09:20',
|
sort: section.sort,
|
||||||
isParent: true,
|
createTime: section.createdAt ? new Date(section.createdAt).toLocaleString() : '',
|
||||||
expanded: false,
|
isParent: section.level === 1, // level=1 表示章,为父章节
|
||||||
children: [
|
level: section.level,
|
||||||
{
|
parentId: section.parentId,
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: '第四章 课前准备',
|
|
||||||
type: '-',
|
|
||||||
sort: '-',
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: true,
|
|
||||||
expanded: false,
|
expanded: false,
|
||||||
children: []
|
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 flattenedChapters = computed(() => {
|
||||||
const result: Chapter[] = []
|
const result: Chapter[] = []
|
||||||
|
|
||||||
const flatten = (chapters: Chapter[]) => {
|
// 分离章节(level=1)和节(level=2),过滤掉level=0的项目
|
||||||
chapters.forEach(chapter => {
|
const chapters = chapterList.value.filter(item => item.level === 1)
|
||||||
result.push(chapter)
|
const sections = chapterList.value.filter(item => item.level === 2)
|
||||||
if (chapter.children && chapter.expanded) {
|
|
||||||
flatten(chapter.children)
|
// 为每个章节添加其子节
|
||||||
}
|
chapters.forEach(chapter => {
|
||||||
})
|
chapter.children = sections.filter(section => section.parentId === chapter.id)
|
||||||
}
|
result.push(chapter)
|
||||||
|
|
||||||
|
// 如果章节展开,添加其子节
|
||||||
|
if (chapter.expanded && chapter.children) {
|
||||||
|
chapter.children.forEach(section => {
|
||||||
|
result.push(section)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
flatten(chapterList.value)
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -302,7 +295,7 @@ const paginatedChapters = computed(() => {
|
|||||||
const rowKey = (row: Chapter) => row.id
|
const rowKey = (row: Chapter) => row.id
|
||||||
|
|
||||||
// 表格选择处理
|
// 表格选择处理
|
||||||
const handleCheck = (rowKeys: number[]) => {
|
const handleCheck = (rowKeys: string[]) => {
|
||||||
selectedChapters.value = rowKeys
|
selectedChapters.value = rowKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +344,7 @@ const goToPage = (page: string | number) => {
|
|||||||
|
|
||||||
// 展开/收起章节
|
// 展开/收起章节
|
||||||
const toggleChapter = (chapter: Chapter) => {
|
const toggleChapter = (chapter: Chapter) => {
|
||||||
if (chapter.isParent && chapter.children) {
|
if (chapter.level === 1 && chapter.children) {
|
||||||
chapter.expanded = !chapter.expanded
|
chapter.expanded = !chapter.expanded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -367,6 +360,7 @@ const addChapter = () => {
|
|||||||
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const importChapters = () => {
|
const importChapters = () => {
|
||||||
showImportModal.value = true
|
showImportModal.value = true
|
||||||
}
|
}
|
||||||
@ -375,39 +369,168 @@ const exportChapters = () => {
|
|||||||
message.info('导出章节功能')
|
message.info('导出章节功能')
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteSelected = () => {
|
const deleteSelected = async () => {
|
||||||
if (selectedChapters.value.length === 0) return
|
if (selectedChapters.value.length === 0) return
|
||||||
if (confirm(`确定要删除选中的 ${selectedChapters.value.length} 个章节吗?`)) {
|
|
||||||
selectedChapters.value.forEach((id: number) => {
|
try {
|
||||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === id)
|
if (!userStore.user?.id) {
|
||||||
if (index > -1) {
|
message.error('用户未登录,无法删除章节')
|
||||||
chapterList.value.splice(index, 1)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
selectedChapters.value = []
|
|
||||||
message.success('删除成功')
|
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 = []
|
||||||
|
} else {
|
||||||
|
message.error('批量删除失败:' + (response.data?.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 批量删除章节失败:', error)
|
||||||
|
message.error('批量删除失败:' + (error.message || '网络错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchChapters = () => {
|
const searchChapters = async () => {
|
||||||
message.info('搜索章节: ' + searchKeyword.value)
|
if (!courseId.value) {
|
||||||
currentPage.value = 1
|
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) => {
|
const editChapter = (chapter: Chapter) => {
|
||||||
message.info('编辑章节: ' + chapter.name)
|
console.log('编辑章节:', chapter)
|
||||||
}
|
// 跳转到章节编辑器页面
|
||||||
|
const courseId = route.params.id
|
||||||
const deleteChapter = (chapter: Chapter) => {
|
if (courseId) {
|
||||||
if (confirm('确定要删除这个章节吗?')) {
|
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
||||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === chapter.id)
|
} else {
|
||||||
if (index > -1) {
|
message.error('课程ID不存在')
|
||||||
chapterList.value.splice(index, 1)
|
|
||||||
message.success('删除成功')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在 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)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error('章节删除失败:' + (response.data?.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 删除章节失败:', error)
|
||||||
|
message.error('删除章节失败:' + (error.message || '网络错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadChapters()
|
||||||
|
})
|
||||||
|
|
||||||
// 表格列配置 - 使用 minWidth 实现响应式
|
// 表格列配置 - 使用 minWidth 实现响应式
|
||||||
const columns: DataTableColumns<Chapter> = [
|
const columns: DataTableColumns<Chapter> = [
|
||||||
{
|
{
|
||||||
@ -423,17 +546,18 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
tooltip: true
|
tooltip: true
|
||||||
},
|
},
|
||||||
render: (row: Chapter) => {
|
render: (row: Chapter) => {
|
||||||
|
const isChapter = row.level === 1; // level=1 表示章
|
||||||
return h('div', {
|
return h('div', {
|
||||||
style: {
|
style: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '20px',
|
gap: '20px',
|
||||||
cursor: row.isParent ? 'pointer' : 'default',
|
cursor: isChapter ? 'pointer' : 'default',
|
||||||
marginLeft: row.isParent ? '0px' : '-3px'
|
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',
|
class: 'n-base-icon',
|
||||||
style: {
|
style: {
|
||||||
transition: 'transform 0.2s',
|
transition: 'transform 0.2s',
|
||||||
@ -454,10 +578,10 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
]) : null,
|
]) : null,
|
||||||
h('span', {
|
h('span', {
|
||||||
style: {
|
style: {
|
||||||
color: row.isParent ? '#062333' : '#666666',
|
color: isChapter ? '#062333' : '#666666',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
fontWeight: row.isParent ? '500' : 'normal',
|
fontWeight: isChapter ? '500' : 'normal',
|
||||||
marginLeft: row.isParent ? '0' : '24px'
|
marginLeft: isChapter ? '0' : '24px'
|
||||||
}
|
}
|
||||||
}, row.name)
|
}, row.name)
|
||||||
])
|
])
|
||||||
@ -468,7 +592,8 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
key: 'type',
|
key: 'type',
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
render: (row: Chapter) => {
|
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('span', { style: { color: '#BABABA' } }, '-')
|
||||||
}
|
}
|
||||||
return h('div', {
|
return h('div', {
|
||||||
@ -489,6 +614,10 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
key: 'sort',
|
key: 'sort',
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
render: (row: Chapter) => {
|
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)
|
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user