feat:课程模块下接口对接
This commit is contained in:
parent
52b9e9a475
commit
3e1f1fdc67
@ -367,6 +367,19 @@ export class TeachCourseApi {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 章节绑定考试-考试列表查询
|
||||
*/
|
||||
static async getExamsList(params: { type: '0' | '1'; examName?: string; }): Promise<ApiResponseWithResult<any>> {
|
||||
try {
|
||||
const response = await ApiRequest.get<any>(`/aiol/aiolExam/queryExamList`, params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 章节绑定考试-考试列表查询失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除课程章节
|
||||
*/
|
||||
@ -494,8 +507,208 @@ export class TeachCourseApi {
|
||||
}
|
||||
}
|
||||
|
||||
// 作业相关类型定义
|
||||
export interface Homework {
|
||||
id?: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
attachment?: string | null
|
||||
max_score?: number | null
|
||||
pass_score?: number | null
|
||||
start_time?: string | null
|
||||
end_time?: string | null
|
||||
status?: number | null
|
||||
allow_makeup?: string // 是否允许补交 0不允许 1允许
|
||||
makeup_time?: string // 补交截止时间
|
||||
notify_time?: string // 作业通知时间
|
||||
classId?: string // 班级id,多个用逗号分割
|
||||
sectionId?: string // 章节id
|
||||
courseId?: string // 课程id,必填
|
||||
}
|
||||
|
||||
// 新建作业请求参数
|
||||
export interface CreateHomeworkRequest {
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
attachment?: string | null
|
||||
maxScore?: number | null
|
||||
passScore?: number | null
|
||||
startTime?: string | null
|
||||
endTime?: string | null
|
||||
allowMakeup: string // 是否允许补交 0不允许 1允许 (必填)
|
||||
makeupTime: string // 补交截止时间 (必填)
|
||||
notifyTime: number // 作业通知时间 (必填)
|
||||
classId: string // 班级id,多个用逗号分割 (必填)
|
||||
sectionId?: string // 章节id
|
||||
courseId: string // 课程id,必填
|
||||
}
|
||||
|
||||
// 编辑作业请求参数
|
||||
export interface EditHomeworkRequest {
|
||||
id: string // 必填
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
attachment?: string | null
|
||||
max_score?: number | null
|
||||
pass_score?: number | null
|
||||
start_time?: string | null
|
||||
end_time?: string | null
|
||||
status?: number | null
|
||||
}
|
||||
|
||||
// 作业提交信息类型
|
||||
export interface HomeworkSubmit {
|
||||
id?: string
|
||||
homeworkId?: string
|
||||
studentId?: string
|
||||
studentName?: string
|
||||
submitTime?: string
|
||||
score?: number | null
|
||||
comment?: string | null
|
||||
status?: number // 提交状态
|
||||
attachment?: string | null
|
||||
}
|
||||
|
||||
// 作业批阅请求参数
|
||||
export interface ReviewHomeworkRequest {
|
||||
submitId: string // 提交记录ID (必填)
|
||||
score: string // 得分 (必填)
|
||||
comment: string // 评语 (必填)
|
||||
}
|
||||
|
||||
/**
|
||||
* 作业API模块
|
||||
*/
|
||||
export class HomeworkApi {
|
||||
|
||||
/**
|
||||
* 新建作业
|
||||
*/
|
||||
static async createHomework(data: CreateHomeworkRequest): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 发送新建作业请求:', { url: '/aiol/aiolHomework/teacher_add', data })
|
||||
|
||||
const response = await ApiRequest.post<any>('/aiol/aiolHomework/teacher_add', data)
|
||||
|
||||
console.log('📝 新建作业响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 新建作业失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑作业
|
||||
*/
|
||||
static async editHomework(data: EditHomeworkRequest): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 发送编辑作业请求:', { url: '/aiol/aiolHomework/edit', data })
|
||||
|
||||
const response = await ApiRequest.put<any>('/aiol/aiolHomework/edit', data)
|
||||
|
||||
console.log('✏️ 编辑作业响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 编辑作业失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除作业
|
||||
*/
|
||||
static async deleteHomework(id: string): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 发送删除作业请求:', { url: '/aiol/aiolHomework/teacher_delete', id })
|
||||
|
||||
const response = await ApiRequest.delete<any>(`/aiol/aiolHomework/teacher_delete?id=${id}`)
|
||||
|
||||
console.log('🗑️ 删除作业响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 删除作业失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询作业详情
|
||||
*/
|
||||
static async getHomeworkById(id: string): Promise<ApiResponseWithResult<Homework>> {
|
||||
try {
|
||||
console.log('🚀 发送查询作业详情请求:', { url: '/aiol/aiolHomework/queryById', id })
|
||||
|
||||
const response = await ApiRequest.get<{ result: Homework }>(`/aiol/aiolHomework/queryById?id=${id}`)
|
||||
|
||||
console.log('📋 作业详情响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询作业详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询课程作业列表
|
||||
*/
|
||||
static async getTeacherHomeworkList(courseId: string): Promise<ApiResponse<{ code: number, result: Homework[] }>> {
|
||||
try {
|
||||
console.log('🚀 发送查询作业列表请求:', { url: '/aiol/aiolHomework/teacher_list', courseId })
|
||||
|
||||
const response = await ApiRequest.get<{ code: number, result: Homework[] }>('/aiol/aiolHomework/teacher_list', { courseId })
|
||||
|
||||
console.log('📋 作业列表响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询作业列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询作业提交情况
|
||||
*/
|
||||
static async getHomeworkSubmits(homeworkId: string): Promise<ApiResponseWithResult<HomeworkSubmit[]>> {
|
||||
try {
|
||||
console.log('🚀 发送查询作业提交情况请求:', {
|
||||
url: `/aiol/aiolHomeworkSubmit/homework/${homeworkId}/submits`,
|
||||
homeworkId
|
||||
})
|
||||
|
||||
const response = await ApiRequest.get<{ result: HomeworkSubmit[] }>(`/aiol/aiolHomeworkSubmit/homework/${homeworkId}/submits`)
|
||||
|
||||
console.log('📊 作业提交情况响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询作业提交情况失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 作业批阅
|
||||
*/
|
||||
static async reviewHomework(data: ReviewHomeworkRequest): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 发送作业批阅请求:', { url: '/aiol/aiolHomework/review', data })
|
||||
|
||||
const response = await ApiRequest.post<any>('/aiol/aiolHomework/review', data)
|
||||
|
||||
console.log('✅ 作业批阅响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 作业批阅失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认导出
|
||||
export default TeachCourseApi
|
||||
|
||||
// 导出讨论API
|
||||
export { DiscussionApi }
|
||||
/**
|
||||
* 班级相关API
|
||||
*/
|
||||
@ -632,4 +845,238 @@ export class ClassApi {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 讨论相关类型定义
|
||||
export interface Discussion {
|
||||
id?: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
sectionId?: string | null
|
||||
courseId?: string | null
|
||||
authorId?: string | null
|
||||
authorName?: string | null
|
||||
createTime?: string | null
|
||||
updateTime?: string | null
|
||||
status?: number | null
|
||||
replyCount?: number | null
|
||||
}
|
||||
|
||||
// 新增评论请求参数
|
||||
export interface CreateCommentRequest {
|
||||
content?: string // 评论内容
|
||||
imgs?: string // 图片
|
||||
targetType: string // 目标类型: course、comment、course_section、discussion
|
||||
targetId: string // 目标id
|
||||
}
|
||||
|
||||
// 新建讨论请求参数
|
||||
export interface CreateDiscussionRequest {
|
||||
title: string // 讨论标题 (必填)
|
||||
description: string // 讨论描述 (必填)
|
||||
sectionId?: string // 章节id
|
||||
courseId?: string // 课程id
|
||||
}
|
||||
|
||||
// 编辑讨论请求参数
|
||||
export interface EditDiscussionRequest {
|
||||
id: string // 讨论id (必填)
|
||||
title: string // 讨论标题 (必填)
|
||||
description?: string // 讨论描述
|
||||
sectionId?: string // 章节id
|
||||
}
|
||||
|
||||
/**
|
||||
* 讨论API模块
|
||||
*/
|
||||
class DiscussionApi {
|
||||
|
||||
/**
|
||||
* 新建讨论
|
||||
*/
|
||||
static async createDiscussion(data: CreateDiscussionRequest): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 发送新建讨论请求:', { url: '/aiol/aiolDiscussion/add', data })
|
||||
|
||||
const response = await ApiRequest.post<any>('/aiol/aiolDiscussion/add', data)
|
||||
|
||||
console.log('💬 新建讨论响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 新建讨论失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑讨论
|
||||
*/
|
||||
static async editDiscussion(data: EditDiscussionRequest): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 发送编辑讨论请求:', { url: '/aiol/aiolDiscussion/edit', data })
|
||||
|
||||
const response = await ApiRequest.put<any>('/aiol/aiolDiscussion/edit', data)
|
||||
|
||||
console.log('✏️ 编辑讨论响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 编辑讨论失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除讨论
|
||||
*/
|
||||
static async deleteDiscussion(id: string): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
console.log('🚀 发送删除讨论请求:', { url: '/aiol/aiolDiscussion/delete', id })
|
||||
|
||||
const response = await ApiRequest.delete<any>(`/aiol/aiolDiscussion/delete?id=${id}`)
|
||||
|
||||
console.log('🗑️ 删除讨论响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 删除讨论失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询课程讨论列表
|
||||
*/
|
||||
static async getDiscussionList(courseId: string): Promise<ApiResponseWithResult<Discussion[]>> {
|
||||
try {
|
||||
console.log('🚀 发送查询讨论列表请求:', { url: '/aiol/aiolDiscussion/teacher_list', courseId })
|
||||
|
||||
const response = await ApiRequest.get<{ result: Discussion[] }>('/aiol/aiolDiscussion/teacher_list', { courseId })
|
||||
|
||||
console.log('📋 讨论列表响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询讨论列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询讨论详情
|
||||
*/
|
||||
static async getDiscussionById(id: string): Promise<ApiResponseWithResult<Discussion>> {
|
||||
try {
|
||||
console.log('🚀 发送查询讨论详情请求:', { url: '/aiol/aiolDiscussion/queryById', id })
|
||||
|
||||
const response = await ApiRequest.get<{ result: Discussion }>(`/aiol/aiolDiscussion/queryById?id=${id}`)
|
||||
|
||||
console.log('📝 讨论详情响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询讨论详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 评论相关接口 ==========
|
||||
|
||||
/**
|
||||
* 查询讨论下的评论列表
|
||||
*/
|
||||
static async getDiscussionComments(discussionId: string): Promise<ApiResponseWithResult<any[]>> {
|
||||
try {
|
||||
console.log('🚀 发送查询讨论评论请求:', { url: `/aiol/aiolComment/discussion/${discussionId}/list`, discussionId })
|
||||
|
||||
const response = await ApiRequest.get<{ result: any[] }>(`/aiol/aiolComment/discussion/${discussionId}/list`)
|
||||
|
||||
console.log('💬 讨论评论响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询讨论评论失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询课程下的评论列表
|
||||
*/
|
||||
static async getCourseComments(courseId: string): Promise<ApiResponseWithResult<any[]>> {
|
||||
try {
|
||||
console.log('🚀 发送查询课程评论请求:', { url: `/aiol/aiolComment/course/${courseId}/list`, courseId })
|
||||
|
||||
const response = await ApiRequest.get<{ result: any[] }>(`/aiol/aiolComment/course/${courseId}/list`)
|
||||
|
||||
console.log('💬 课程评论响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询课程评论失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增评论
|
||||
*/
|
||||
static async createComment(commentData: CreateCommentRequest): Promise<ApiResponse> {
|
||||
try {
|
||||
console.log('🚀 发送新增评论请求:', { url: '/aiol/aiolComment/add', data: commentData })
|
||||
|
||||
const response = await ApiRequest.post<ApiResponse>('/aiol/aiolComment/add', commentData)
|
||||
|
||||
console.log('✅ 新增评论响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 新增评论失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询评论详情
|
||||
*/
|
||||
static async getCommentById(id: string): Promise<ApiResponseWithResult<any>> {
|
||||
try {
|
||||
console.log('🚀 发送查询评论详情请求:', { url: '/aiol/aiolComment/queryById', id })
|
||||
|
||||
const response = await ApiRequest.get<{ result: any }>(`/aiol/aiolComment/queryById?id=${id}`)
|
||||
|
||||
console.log('📝 评论详情响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 查询评论详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论点赞
|
||||
*/
|
||||
static async likeComment(commentId: string): Promise<ApiResponse> {
|
||||
try {
|
||||
console.log('🚀 发送评论点赞请求:', { url: `/aiol/aiolComment/like/${commentId}`, commentId })
|
||||
|
||||
const response = await ApiRequest.get<ApiResponse>(`/aiol/aiolComment/like/${commentId}`)
|
||||
|
||||
console.log('👍 评论点赞响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 评论点赞失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论置顶
|
||||
*/
|
||||
static async topComment(commentId: string): Promise<ApiResponse> {
|
||||
try {
|
||||
console.log('🚀 发送评论置顶请求:', { url: `/aiol/aiolComment/top/${commentId}`, commentId })
|
||||
|
||||
const response = await ApiRequest.get<ApiResponse>(`/aiol/aiolComment/top/${commentId}`)
|
||||
|
||||
console.log('👍 评论置顶响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('❌ 评论置顶失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
@ -149,7 +149,7 @@ onUnmounted(() => {
|
||||
background: #ffffff;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
border-radius: 0 0 2px 2px;
|
||||
z-index: 1000;
|
||||
z-index: 9999;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
605
src/components/DiscussionLibraryModal.vue
Normal file
605
src/components/DiscussionLibraryModal.vue
Normal file
@ -0,0 +1,605 @@
|
||||
<template>
|
||||
<n-modal :show="show" @update:show="handleUpdateShow" preset="card"
|
||||
style="width: 90%; max-width: 1200px; max-height: 80vh;" :mask-closable="false" :closable="false">
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">讨论</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="modal-content">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="search-item">
|
||||
<div class="custom-search-input">
|
||||
<input v-model="searchKeyword" type="text" placeholder="请输入讨论标题搜索" class="search-input-field"
|
||||
@keyup.enter="handleSearch" />
|
||||
<img src="/images/teacher/搜索.png" alt="搜索" class="search-icon" @click="handleSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
已全部加载,共{{ totalCount }}个讨论
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-section">
|
||||
<n-spin size="large" />
|
||||
<p>正在加载讨论列表...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="filteredDiscussions.length === 0" class="empty-section">
|
||||
<div class="empty-content">
|
||||
<n-icon size="48" color="#d9d9d9">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zM7 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5s1.5.67 1.5 1.5s-.67 1.5-1.5 1.5zM12 17.5c-2.33 0-4.31-1.46-5.11-3.5h10.22c-.8 2.04-2.78 3.5-5.11 3.5zM17 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5s1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<p class="empty-text">暂无讨论数据</p>
|
||||
<p class="empty-subtext" v-if="searchKeyword">尝试调整搜索条件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 讨论列表 -->
|
||||
<div v-else class="discussion-list">
|
||||
<div class="discussion-grid">
|
||||
<div v-for="discussion in paginatedDiscussions" :key="discussion.id" class="discussion-card"
|
||||
:class="{ 'selected': selectedDiscussion && selectedDiscussion.id === discussion.id }">
|
||||
<div class="card-checkbox">
|
||||
<n-radio :checked="!!(selectedDiscussion && selectedDiscussion.id === discussion.id)"
|
||||
@update:checked="selectDiscussion(discussion)" />
|
||||
</div>
|
||||
<div class="card-content" @click="selectDiscussion(discussion)">
|
||||
<div class="title-section">
|
||||
<h3 class="discussion-title">{{ discussion.title || '未命名讨论' }}</h3>
|
||||
<n-tag v-if="discussion.isPinned" type="info" size="small" class="pinned-tag">
|
||||
置顶
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="discussion-content">
|
||||
<p class="content-preview">{{ getContentPreview(discussion.description) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="discussion-footer">
|
||||
<div class="time-info">
|
||||
<n-icon size="14" color="#999">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8zM12.5 7H11v6l5.25 3.15l.75-1.23l-4.5-2.67z"/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<span class="time-text">{{ discussion.createTime || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-section" v-if="totalPages > 1">
|
||||
<CustomPagination
|
||||
:current-page="currentPage"
|
||||
:total-pages="totalPages"
|
||||
:page-size="pageSize"
|
||||
:total="filteredDiscussions.length"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<div class="selected-info">
|
||||
<span v-if="selectedDiscussion" class="selected-text">
|
||||
已选择: {{ selectedDiscussion.title }}
|
||||
</span>
|
||||
<span v-else class="no-selected-text">请选择一个讨论</span>
|
||||
</div>
|
||||
<div class="footer-buttons">
|
||||
<n-button @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" @click="handleConfirm" :disabled="!selectedDiscussion">
|
||||
确定选择
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { NModal, NButton, NRadio, NTag, NIcon, NSpin } from 'naive-ui'
|
||||
import { DiscussionApi } from '@/api/modules/teachCourse'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import CustomPagination from './CustomPagination.vue'
|
||||
|
||||
interface Discussion {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
authorName: string
|
||||
createTime: string
|
||||
chapterName: string
|
||||
sectionName: string
|
||||
replyCount: number
|
||||
status: number
|
||||
isPinned: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
courseId: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean]
|
||||
'confirm': [selectedDiscussion: Discussion]
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(9) // 每页显示9个,符合3x3网格布局
|
||||
const selectedDiscussion = ref<Discussion | null>(null)
|
||||
const loading = ref(false)
|
||||
const discussions = ref<Discussion[]>([])
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => discussions.value.length)
|
||||
|
||||
// 过滤后的讨论列表
|
||||
const filteredDiscussions = computed(() => {
|
||||
let filtered = discussions.value
|
||||
|
||||
// 搜索筛选
|
||||
if (searchKeyword.value.trim()) {
|
||||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||||
filtered = filtered.filter(d =>
|
||||
d.title?.toLowerCase().includes(keyword) ||
|
||||
d.description?.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 总页数
|
||||
const totalPages = computed(() => Math.ceil(filteredDiscussions.value.length / pageSize.value))
|
||||
|
||||
// 分页后的讨论列表
|
||||
const paginatedDiscussions = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return filteredDiscussions.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 获取课程ID(从props传入)
|
||||
const courseId = computed(() => props.courseId)
|
||||
|
||||
// 加载讨论列表
|
||||
const loadDiscussions = async () => {
|
||||
if (!courseId.value) {
|
||||
console.error('缺少课程ID')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await DiscussionApi.getDiscussionList(courseId.value)
|
||||
console.log('💬 讨论库API响应:', response)
|
||||
|
||||
if (response.data && response.data.result) {
|
||||
discussions.value = response.data.result.map((discussion: any) => ({
|
||||
id: discussion.id || '',
|
||||
title: discussion.title || '无标题',
|
||||
description: discussion.description || '',
|
||||
authorName: discussion.authorName || '匿名用户',
|
||||
createTime: formatTime(discussion.createTime),
|
||||
chapterName: discussion.sectionName || '未分类',
|
||||
sectionName: discussion.sectionName || '',
|
||||
replyCount: discussion.replyCount || 0,
|
||||
status: discussion.status || 0,
|
||||
isPinned: discussion.status === 1
|
||||
}))
|
||||
|
||||
console.log('💬 讨论库数据:', discussions.value)
|
||||
} else {
|
||||
discussions.value = []
|
||||
console.warn('💬 API响应格式异常:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取讨论列表失败:', error)
|
||||
message.error('获取讨论列表失败,请重试')
|
||||
discussions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string) => {
|
||||
try {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}月${day}日 ${hours}:${minutes}`
|
||||
} catch {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
// 获取内容预览
|
||||
const getContentPreview = (content: string) => {
|
||||
if (!content) return '暂无内容'
|
||||
// 移除HTML标签
|
||||
const textContent = content.replace(/<[^>]*>/g, '')
|
||||
return textContent.length > 100 ? textContent.substring(0, 100) + '...' : textContent
|
||||
}
|
||||
|
||||
// 选择讨论
|
||||
const selectDiscussion = (discussion: Discussion) => {
|
||||
selectedDiscussion.value = discussion
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1 // 搜索时重置到第一页
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page: number) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 模态框显示状态更新
|
||||
const handleUpdateShow = (value: boolean) => {
|
||||
emit('update:show', value)
|
||||
if (!value) {
|
||||
// 关闭时重置状态
|
||||
selectedDiscussion.value = null
|
||||
searchKeyword.value = ''
|
||||
currentPage.value = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (selectedDiscussion.value) {
|
||||
emit('confirm', selectedDiscussion.value)
|
||||
handleUpdateShow(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
handleUpdateShow(false)
|
||||
}
|
||||
|
||||
// 监听show变化,打开时加载数据
|
||||
watch(() => props.show, (newShow) => {
|
||||
if (newShow) {
|
||||
loadDiscussions()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听课程ID变化,重新加载数据
|
||||
watch(() => props.courseId, (newCourseId) => {
|
||||
if (newCourseId && props.show) {
|
||||
loadDiscussions()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听筛选条件变化,重置页码
|
||||
watch(searchKeyword, () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 0;
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 筛选区域 */
|
||||
.filter-section {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.custom-search-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input-field {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 8px 40px 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-input-field:focus {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 加载和空状态 */
|
||||
.loading-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 讨论列表 */
|
||||
.discussion-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.discussion-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.discussion-card {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.discussion-card:hover {
|
||||
border-color: #d9d9d9;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.discussion-card.selected {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.discussion-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.pinned-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.discussion-content {
|
||||
margin: 12px 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.discussion-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 分页区域 */
|
||||
.pagination-section {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 底部操作区域 */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0 0 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-selected-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.discussion-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -11,42 +11,28 @@
|
||||
<!-- 筛选和搜索区域 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="filter-item">
|
||||
<!-- <div class="filter-item">
|
||||
<span class="filter-label">类型:</span>
|
||||
<n-select v-model:value="selectedType" :options="typeOptions" placeholder="全部" class="type-select" />
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="search-item">
|
||||
<div class="custom-search-input">
|
||||
<input v-model="searchKeyword" type="text" placeholder="请输入文档名称" class="search-input-field" />
|
||||
<img src="/images/teacher/搜索.png" alt="搜索" class="search-icon" />
|
||||
</div>
|
||||
<n-input v-model:value="searchKeyword" placeholder="搜索试卷" />
|
||||
</div>
|
||||
<div class="info-text">
|
||||
已全部加载,共{{ totalCount }}份考试/练习
|
||||
</div>
|
||||
<n-button type="primary" class="import-btn">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5V19M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
导入试卷
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 试卷列表 -->
|
||||
<div class="exam-list">
|
||||
<div class="exam-grid">
|
||||
<div class="exam-grid" v-if="filteredExams.length > 0">
|
||||
<div v-for="exam in filteredExams" :key="exam.id" class="exam-card"
|
||||
:class="{ 'selected': selectedExams.includes(exam.id) }">
|
||||
:class="{ 'selected': selectedExamId === exam.id }">
|
||||
<div class="card-checkbox">
|
||||
<n-checkbox :checked="selectedExams.includes(exam.id)" @update:checked="toggleExamSelection(exam.id)" />
|
||||
<n-radio :checked="selectedExamId === exam.id" @update:checked="toggleExamSelection(exam.id)" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-content" @click="toggleExamSelection(exam.id)">
|
||||
<div class="title-section">
|
||||
<n-tag :type="exam.status === '未开始' ? 'info' : 'success'" size="small" class="status-tag"
|
||||
:data-status="exam.status">
|
||||
@ -63,17 +49,24 @@
|
||||
<span class="detail-label">考试时长:</span>
|
||||
<span class="detail-value">{{ exam.duration }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<!-- <div class="detail-item">
|
||||
<span class="detail-label">考题数量:</span>
|
||||
<span class="detail-value">{{ exam.questionCount }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="view-details" @click="viewExamDetails(exam)">查看详情 ></span>
|
||||
<!-- <span class="view-details" @click="viewExamDetails(exam)">查看详情 ></span> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-section">
|
||||
<div class="empty-content">
|
||||
<p class="empty-text">暂无考试数据</p>
|
||||
<p class="empty-subtext" v-if="searchKeyword">尝试调整搜索条件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
@ -90,7 +83,7 @@
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" @click="handleConfirm" :disabled="selectedExams.length === 0">
|
||||
<n-button type="primary" @click="handleConfirm" :disabled="selectedExamId === null">
|
||||
确定
|
||||
</n-button>
|
||||
</div>
|
||||
@ -100,8 +93,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { NModal, NButton, NSelect, NCheckbox, NTag, NIcon } from 'naive-ui'
|
||||
import { NModal, NButton, NTag } from 'naive-ui'
|
||||
import CustomPagination from './CustomPagination.vue'
|
||||
import { TeachCourseApi } from '@/api/modules/teachCourse'
|
||||
|
||||
interface ExamPaper {
|
||||
id: number
|
||||
@ -115,13 +109,14 @@ interface ExamPaper {
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
type: '0' | '1'
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean]
|
||||
'confirm': [selectedExams: ExamPaper[]]
|
||||
'confirm': [selectedExam: ExamPaper | null]
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
@ -129,73 +124,18 @@ const selectedType = ref('全部')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const selectedExams = ref<number[]>([])
|
||||
const selectedExamId = ref<number | null>(null)
|
||||
|
||||
// 类型选项
|
||||
const typeOptions = [
|
||||
{ label: '全部', value: '全部' },
|
||||
{ label: '考试', value: '考试' },
|
||||
{ label: '练习', value: '练习' },
|
||||
{ label: '测验', value: '测验' }
|
||||
]
|
||||
// const typeOptions = [
|
||||
// { label: '全部', value: '全部' },
|
||||
// { label: '考试', value: '考试' },
|
||||
// { label: '练习', value: '练习' },
|
||||
// { label: '测验', value: '测验' }
|
||||
// ]
|
||||
|
||||
// 模拟试卷数据
|
||||
const examPapers = ref<ExamPaper[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: 'C++语言程序设计基础考试',
|
||||
status: '未开始',
|
||||
startTime: '2025.07.18 10:00',
|
||||
duration: '120分钟',
|
||||
questionCount: '100题',
|
||||
type: '考试'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Java编程基础练习',
|
||||
status: '进行中',
|
||||
startTime: '2025.07.19 14:00',
|
||||
duration: '90分钟',
|
||||
questionCount: '80题',
|
||||
type: '练习'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Python数据分析测验',
|
||||
status: '未开始',
|
||||
startTime: '2025.07.20 09:00',
|
||||
duration: '60分钟',
|
||||
questionCount: '50题',
|
||||
type: '测验'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Web前端开发考试',
|
||||
status: '未开始',
|
||||
startTime: '2025.07.21 15:00',
|
||||
duration: '150分钟',
|
||||
questionCount: '120题',
|
||||
type: '考试'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '数据库设计练习',
|
||||
status: '进行中',
|
||||
startTime: '2025.07.22 11:00',
|
||||
duration: '100分钟',
|
||||
questionCount: '70题',
|
||||
type: '练习'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '算法与数据结构测验',
|
||||
status: '未开始',
|
||||
startTime: '2025.07.23 16:00',
|
||||
duration: '75分钟',
|
||||
questionCount: '60题',
|
||||
type: '测验'
|
||||
}
|
||||
])
|
||||
const examPapers = ref<ExamPaper[]>([])
|
||||
|
||||
// 计算属性
|
||||
const filteredExams = computed(() => {
|
||||
@ -224,41 +164,57 @@ const handleUpdateShow = (value: boolean) => {
|
||||
}
|
||||
|
||||
const toggleExamSelection = (examId: number) => {
|
||||
const index = selectedExams.value.indexOf(examId)
|
||||
if (index > -1) {
|
||||
selectedExams.value.splice(index, 1)
|
||||
// 单选切换:再次点击取消选择,否则选择该项
|
||||
if (selectedExamId.value === examId) {
|
||||
selectedExamId.value = null
|
||||
} else {
|
||||
selectedExams.value.push(examId)
|
||||
selectedExamId.value = examId
|
||||
}
|
||||
}
|
||||
|
||||
const viewExamDetails = (exam: ExamPaper) => {
|
||||
console.log('查看试卷详情:', exam)
|
||||
// 这里可以添加查看详情的逻辑
|
||||
}
|
||||
// const viewExamDetails = (exam: ExamPaper) => {
|
||||
// console.log('查看试卷详情:', exam)
|
||||
// // 这里可以添加查看详情的逻辑
|
||||
// }
|
||||
|
||||
const handleCancel = () => {
|
||||
selectedExams.value = []
|
||||
selectedExamId.value = null
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
const selectedExamPapers = examPapers.value.filter(exam =>
|
||||
selectedExams.value.includes(exam.id)
|
||||
)
|
||||
emit('confirm', selectedExamPapers)
|
||||
const selected = examPapers.value.find(exam => exam.id === selectedExamId.value) || null
|
||||
emit('confirm', selected)
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
// 监听显示状态变化
|
||||
watch(() => props.show, (newVal) => {
|
||||
if (!newVal) {
|
||||
selectedExams.value = []
|
||||
selectedExamId.value = null
|
||||
searchKeyword.value = ''
|
||||
selectedType.value = '全部'
|
||||
currentPage.value = 1
|
||||
}
|
||||
if(newVal){
|
||||
getList()
|
||||
}
|
||||
})
|
||||
|
||||
const getList = async() => {
|
||||
const res = await TeachCourseApi.getExamsList({type: props.type})
|
||||
examPapers.value = res.data.result.map((item:any)=>{
|
||||
return {
|
||||
id:item.id,
|
||||
title: item.name,
|
||||
status: item.status,
|
||||
startTime: item.startTime,
|
||||
duration: item.totalTime,
|
||||
questionCount: null,
|
||||
type: props.type === '0' ? '练习' : '考试'
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -372,6 +328,31 @@ watch(() => props.show, (newVal) => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.empty-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.exam-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
|
@ -8,75 +8,100 @@
|
||||
</template>
|
||||
|
||||
<div class="modal-content">
|
||||
<!-- 筛选和搜索区域 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="filter-item">
|
||||
<!-- 筛选和搜索区域 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-row">
|
||||
<!-- <div class="filter-item">
|
||||
<span class="filter-label">类型:</span>
|
||||
<n-select v-model:value="selectedType" :options="typeOptions" placeholder="全部" class="type-select" />
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<span class="search-label">搜索:</span>
|
||||
<div class="custom-search-input">
|
||||
<input v-model="searchKeyword" type="text" placeholder="请输入文档名称" class="search-input-field" />
|
||||
<img src="/images/teacher/搜索.png" alt="搜索" class="search-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
已全部加载,共{{ totalCount }}份作业
|
||||
</div>
|
||||
<n-button type="primary" class="import-btn">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5V19M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
导入作业
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="search-item">
|
||||
<span class="search-label">搜索:</span>
|
||||
<div class="custom-search-input">
|
||||
<input v-model="searchKeyword" type="text" placeholder="请输入作业名称" class="search-input-field" />
|
||||
<img src="/images/teacher/搜索.png" alt="搜索" class="search-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
已全部加载,共{{ totalCount }}份作业
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作业列表 -->
|
||||
<div class="homework-list">
|
||||
<div class="homework-grid">
|
||||
<div v-for="homework in filteredHomework" :key="homework.id" class="homework-card"
|
||||
:class="{ 'selected': selectedHomework.includes(homework.id) }">
|
||||
<div class="card-checkbox">
|
||||
<n-checkbox :checked="selectedHomework.includes(homework.id)" @update:checked="toggleHomeworkSelection(homework.id)" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="title-row">
|
||||
<h3 class="homework-title">{{ homework.title }}</h3>
|
||||
<span class="view-details" @click="viewHomeworkDetails(homework)">查看详情 ></span>
|
||||
</div>
|
||||
<div class="homework-description">
|
||||
{{ homework.description }}
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-section">
|
||||
<n-spin size="large" />
|
||||
<p>正在加载作业列表...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="filteredHomework.length === 0" class="empty-section">
|
||||
<div class="empty-content">
|
||||
<n-icon size="48" color="#d9d9d9">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zM7 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5s1.5.67 1.5 1.5s-.67 1.5-1.5 1.5zM12 17.5c-2.33 0-4.31-1.46-5.11-3.5h10.22c-.8 2.04-2.78 3.5-5.11 3.5zM17 13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5s1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
<p class="empty-text">暂无作业数据</p>
|
||||
<p class="empty-subtext" v-if="searchKeyword">尝试调整搜索条件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作业列表 -->
|
||||
<div v-else class="homework-list">
|
||||
<div class="homework-grid">
|
||||
<div v-for="homework in filteredHomework" :key="homework.id" class="homework-card"
|
||||
:class="{ 'selected': selectedHomework && selectedHomework.id === homework.id }">
|
||||
<div class="card-checkbox">
|
||||
<n-radio :checked="!!(selectedHomework && selectedHomework.id === homework.id)"
|
||||
@update:checked="selectHomework(homework)" />
|
||||
</div>
|
||||
<div class="card-content" @click="selectHomework(homework)">
|
||||
<div class="title-row">
|
||||
<h3 class="homework-title">{{ homework.title }}</h3>
|
||||
<!-- <span class="view-details" @click.stop="viewHomeworkDetails(homework)">查看详情 ></span> -->
|
||||
</div>
|
||||
<div class="homework-description" v-html="homework.description || '暂无描述'">
|
||||
</div>
|
||||
<div class="homework-footer">
|
||||
<div class="time-info">
|
||||
<span class="time-text">{{ formatTime(homework.start_time || null) }}</span>
|
||||
<span v-if="homework.end_time" class="end-time">
|
||||
至 {{ formatTime(homework.end_time || null) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-section">
|
||||
<!-- <div class="pagination-section">
|
||||
<CustomPagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="totalCount"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" @click="handleConfirm" :disabled="selectedHomework.length === 0">
|
||||
确定
|
||||
</n-button>
|
||||
<div class="selected-info">
|
||||
<span v-if="selectedHomework" class="selected-text">
|
||||
已选择: {{ selectedHomework.title }}
|
||||
</span>
|
||||
<span v-else class="no-selected-text">请选择一个作业</span>
|
||||
</div>
|
||||
<div class="footer-buttons">
|
||||
<n-button @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" @click="handleConfirm" :disabled="!selectedHomework">
|
||||
确定选择
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
@ -84,95 +109,70 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { NModal, NButton, NSelect, NCheckbox, NIcon } from 'naive-ui'
|
||||
import CustomPagination from './CustomPagination.vue'
|
||||
import { NModal, NButton, NRadio, NIcon, NSpin } from 'naive-ui'
|
||||
import { HomeworkApi } from '@/api/modules/teachCourse'
|
||||
import { useMessage } from 'naive-ui'
|
||||
// import CustomPagination from './CustomPagination.vue'
|
||||
|
||||
interface Homework {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
type: string
|
||||
id?: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
attachment?: string | null
|
||||
max_score?: number | null
|
||||
pass_score?: number | null
|
||||
start_time?: string | null
|
||||
end_time?: string | null
|
||||
status?: number | null
|
||||
allow_makeup?: string
|
||||
makeup_time?: string
|
||||
notify_time?: string
|
||||
classId?: string
|
||||
sectionId?: string
|
||||
courseId?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
courseId: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean]
|
||||
'confirm': [selectedHomework: Homework[]]
|
||||
'confirm': [selectedHomework: Homework]
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedType = ref('全部')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const selectedHomework = ref<number[]>([])
|
||||
// const pageSize = ref(10)
|
||||
const selectedHomework = ref<Homework | null>(null)
|
||||
const loading = ref(false)
|
||||
const homeworkList = ref<Homework[]>([])
|
||||
|
||||
// 类型选项
|
||||
const typeOptions = [
|
||||
{ label: '全部', value: '全部' },
|
||||
{ label: '编程作业', value: '编程作业' },
|
||||
{ label: '文档作业', value: '文档作业' },
|
||||
{ label: '实验作业', value: '实验作业' }
|
||||
]
|
||||
|
||||
// 模拟作业数据
|
||||
const homeworkList = ref<Homework[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '作业标题作业标题作业标题作业标题',
|
||||
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
|
||||
type: '编程作业'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '作业标题作业标题作业标题作业标题',
|
||||
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
|
||||
type: '实验作业'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '作业标题作业标题作业标题作业标题',
|
||||
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
|
||||
type: '文档作业'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '作业标题作业标题作业标题作业标题',
|
||||
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
|
||||
type: '编程作业'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '作业标题作业标题作业标题作业标题',
|
||||
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
|
||||
type: '实验作业'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '作业标题作业标题作业标题作业标题',
|
||||
description: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作.......',
|
||||
type: '文档作业'
|
||||
}
|
||||
])
|
||||
// const typeOptions = [
|
||||
// { label: '全部', value: '全部' },
|
||||
// { label: '编程作业', value: '编程作业' },
|
||||
// { label: '文档作业', value: '文档作业' },
|
||||
// { label: '实验作业', value: '实验作业' }
|
||||
// ]
|
||||
|
||||
// 计算属性
|
||||
const filteredHomework = computed(() => {
|
||||
let filtered = homeworkList.value
|
||||
|
||||
// 按类型筛选
|
||||
if (selectedType.value !== '全部') {
|
||||
filtered = filtered.filter(homework => homework.type === selectedType.value)
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
if (searchKeyword.value.trim()) {
|
||||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||||
filtered = filtered.filter(homework =>
|
||||
homework.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
homework.title?.toLowerCase().includes(keyword) ||
|
||||
homework.description?.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
@ -181,46 +181,119 @@ const filteredHomework = computed(() => {
|
||||
|
||||
const totalCount = computed(() => filteredHomework.value.length)
|
||||
|
||||
// 方法
|
||||
const handleUpdateShow = (value: boolean) => {
|
||||
emit('update:show', value)
|
||||
}
|
||||
// 获取课程ID(从props传入)
|
||||
const courseId = computed(() => props.courseId)
|
||||
|
||||
const toggleHomeworkSelection = (homeworkId: number) => {
|
||||
const index = selectedHomework.value.indexOf(homeworkId)
|
||||
if (index > -1) {
|
||||
selectedHomework.value.splice(index, 1)
|
||||
} else {
|
||||
selectedHomework.value.push(homeworkId)
|
||||
// 加载作业列表
|
||||
const loadHomeworkList = async () => {
|
||||
if (!courseId.value) {
|
||||
console.error('缺少课程ID')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await HomeworkApi.getTeacherHomeworkList(courseId.value)
|
||||
console.log('📋 作业库API响应:', response)
|
||||
|
||||
if (response.data && response.data.result) {
|
||||
homeworkList.value = response.data.result.map((homework: any) => ({
|
||||
id: homework.id || '',
|
||||
title: homework.title || '无标题',
|
||||
description: homework.description || '',
|
||||
attachment: homework.attachment || '',
|
||||
max_score: homework.max_score || 0,
|
||||
pass_score: homework.pass_score || 0,
|
||||
start_time: homework.start_time,
|
||||
end_time: homework.end_time,
|
||||
status: homework.status || 0,
|
||||
allow_makeup: homework.allow_makeup || '0',
|
||||
makeup_time: homework.makeup_time,
|
||||
notify_time: homework.notify_time,
|
||||
classId: homework.classId,
|
||||
sectionId: homework.sectionId,
|
||||
courseId: homework.courseId
|
||||
}))
|
||||
|
||||
console.log('📋 作业库数据:', homeworkList.value)
|
||||
} else {
|
||||
homeworkList.value = []
|
||||
console.warn('📋 API响应格式异常:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取作业列表失败:', error)
|
||||
message.error('获取作业列表失败,请重试')
|
||||
homeworkList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const viewHomeworkDetails = (homework: Homework) => {
|
||||
console.log('查看作业详情:', homework)
|
||||
// 这里可以添加查看详情的逻辑
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string | null) => {
|
||||
try {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}月${day}日 ${hours}:${minutes}`
|
||||
} catch {
|
||||
return timeStr || ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
selectedHomework.value = []
|
||||
emit('update:show', false)
|
||||
// 选择作业
|
||||
const selectHomework = (homework: Homework) => {
|
||||
selectedHomework.value = homework
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
const selectedHomeworkItems = homeworkList.value.filter(homework =>
|
||||
selectedHomework.value.includes(homework.id)
|
||||
)
|
||||
emit('confirm', selectedHomeworkItems)
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
// 监听显示状态变化
|
||||
watch(() => props.show, (newVal) => {
|
||||
if (!newVal) {
|
||||
selectedHomework.value = []
|
||||
// 方法
|
||||
const handleUpdateShow = (value: boolean) => {
|
||||
emit('update:show', value)
|
||||
if (!value) {
|
||||
// 关闭时重置状态
|
||||
selectedHomework.value = null
|
||||
searchKeyword.value = ''
|
||||
selectedType.value = '全部'
|
||||
currentPage.value = 1
|
||||
}
|
||||
}
|
||||
|
||||
// const viewHomeworkDetails = (homework: Homework) => {
|
||||
// console.log('查看作业详情:', homework)
|
||||
// // 这里可以添加查看详情的逻辑
|
||||
// }
|
||||
|
||||
const handleCancel = () => {
|
||||
handleUpdateShow(false)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selectedHomework.value) {
|
||||
emit('confirm', selectedHomework.value)
|
||||
handleUpdateShow(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听show变化,打开时加载数据
|
||||
watch(() => props.show, (newShow) => {
|
||||
if (newShow) {
|
||||
loadHomeworkList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听课程ID变化,重新加载数据
|
||||
watch(() => props.courseId, (newCourseId) => {
|
||||
if (newCourseId && props.show) {
|
||||
loadHomeworkList()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听筛选条件变化,重置页码
|
||||
watch(searchKeyword, () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -427,39 +500,122 @@ watch(() => props.show, (newVal) => {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 加载和空状态 */
|
||||
.loading-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.homework-footer {
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.end-time {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
font-size: 14px;
|
||||
color: #0288D1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-selected-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 自定义按钮样式 */
|
||||
.modal-footer :deep(.n-button) {
|
||||
.footer-buttons :deep(.n-button) {
|
||||
height: 32px !important;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 取消按钮样式 */
|
||||
.modal-footer :deep(.n-button--default-type) {
|
||||
.footer-buttons :deep(.n-button--default-type) {
|
||||
background-color: #E2F5FF !important;
|
||||
border-color: #0288D1 !important;
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
|
||||
.modal-footer :deep(.n-button--default-type:hover) {
|
||||
.footer-buttons :deep(.n-button--default-type:hover) {
|
||||
background-color: #D3E8F5 !important;
|
||||
border-color: #0277BD !important;
|
||||
color: #0277BD !important;
|
||||
}
|
||||
|
||||
/* 确定按钮样式 */
|
||||
.modal-footer :deep(.n-button--primary-type) {
|
||||
.footer-buttons :deep(.n-button--primary-type) {
|
||||
background-color: #0288D1 !important;
|
||||
border-color: #0288D1 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.modal-footer :deep(.n-button--primary-type:hover) {
|
||||
.footer-buttons :deep(.n-button--primary-type:hover) {
|
||||
background-color: #0277BD !important;
|
||||
border-color: #0277BD !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.modal-footer :deep(.n-button--primary-type:disabled) {
|
||||
.footer-buttons :deep(.n-button--primary-type:disabled) {
|
||||
background-color: #0288D1 !important;
|
||||
border-color: #0288D1 !important;
|
||||
color: #fff !important;
|
||||
|
@ -1,40 +1,149 @@
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" preset="card" style="width: 800px;" title="资源库">
|
||||
<n-modal v-model:show="showModal" preset="card" style="width: 800px;" :title="modalTitle">
|
||||
<div class="resource-library-container">
|
||||
<div class="resource-list">
|
||||
<div
|
||||
v-for="resource in resources"
|
||||
:key="resource.id"
|
||||
class="resource-item"
|
||||
:class="{ 'selected': selectedResources.includes(resource.id) }"
|
||||
@click="toggleResourceSelection(resource.id)"
|
||||
>
|
||||
<div class="resource-info">
|
||||
<div class="resource-icon">
|
||||
<img v-if="resource.type === 'ppt'" src="/images/teacher/课件.png" alt="PPT" />
|
||||
<img v-else-if="resource.type === 'video'" src="/images/teacher/Image.png" alt="视频" />
|
||||
<img v-else src="/images/teacher/文件格式.png" alt="文件" />
|
||||
</div>
|
||||
<div class="resource-details">
|
||||
<div class="resource-name">{{ resource.name }}</div>
|
||||
<div class="resource-meta">
|
||||
<span class="resource-size">{{ resource.size }}</span>
|
||||
<span class="resource-date">{{ resource.uploadDate }}</span>
|
||||
<!-- 选项卡切换 -->
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<!-- 资源库选择 -->
|
||||
<n-tab-pane name="library" tab="从资源库选择">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-section">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索资源名称..."
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<SearchOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div class="resource-list-wrapper">
|
||||
<n-spin :show="loading">
|
||||
<div v-if="filteredResources.length > 0" class="resource-list">
|
||||
<div
|
||||
v-for="resource in filteredResources"
|
||||
:key="resource.id"
|
||||
class="resource-item"
|
||||
:class="{ 'selected': selectedResource === resource.id }"
|
||||
@click="toggleResourceSelection(resource.id)"
|
||||
>
|
||||
<div class="resource-info">
|
||||
<div class="resource-icon">
|
||||
<n-icon size="32" :component="getResourceIcon(resource.type)" />
|
||||
</div>
|
||||
<div class="resource-details">
|
||||
<div class="resource-name">{{ resource.name }}</div>
|
||||
<div class="resource-meta">
|
||||
<span class="resource-size">{{ formatFileSize(resource.fileSize) }}</span>
|
||||
<span class="resource-date">{{ formatTime(resource.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
<!-- <n-button size="small" quaternary @click.stop="previewResource(resource)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<EyeOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
预览
|
||||
</n-button> -->
|
||||
</div>
|
||||
<div class="selection-indicator" v-if="selectedResource === resource.id">
|
||||
<n-icon size="14">
|
||||
<CheckmarkOutline />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<n-icon size="48">
|
||||
<FolderOpenOutline />
|
||||
</n-icon>
|
||||
</div>
|
||||
<p>{{ searchKeyword ? '没有找到匹配的资源' : '暂无可用资源' }}</p>
|
||||
<n-button type="primary" @click="activeTab = 'upload'">
|
||||
上传新资源
|
||||
</n-button>
|
||||
</div>
|
||||
</n-spin>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 本地上传 -->
|
||||
<n-tab-pane name="upload" tab="本地上传">
|
||||
<div class="upload-section">
|
||||
<!-- 文件上传区域 -->
|
||||
<div class="file-upload-area">
|
||||
<n-upload
|
||||
ref="uploadRef"
|
||||
:max="1"
|
||||
:default-upload="false"
|
||||
:file-list="uploadFileList"
|
||||
@update:file-list="handleUploadFileListChange"
|
||||
:accept="getUploadAccept()"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div class="upload-dragger-content">
|
||||
<n-icon size="48" :depth="3">
|
||||
<CloudUploadOutline />
|
||||
</n-icon>
|
||||
<n-text class="upload-text">
|
||||
点击或者拖动文件到该区域来上传
|
||||
</n-text>
|
||||
<n-p depth="3" class="upload-hint">
|
||||
{{ getUploadHint() }}
|
||||
</n-p>
|
||||
</div>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</div>
|
||||
|
||||
<!-- 文件名设置 -->
|
||||
<div v-if="uploadFileList.length > 0" class="file-name-section">
|
||||
<label class="form-label">文件名设置:</label>
|
||||
<n-input
|
||||
v-model:value="uploadFileName"
|
||||
placeholder="请输入文件名(不含扩展名)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<div v-if="uploading" class="upload-progress-section">
|
||||
<n-progress type="line" :percentage="uploadProgress" :show-indicator="true" />
|
||||
<p class="upload-status">{{ uploadStatus }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selection-indicator" v-if="selectedResources.includes(resource.id)">
|
||||
✓
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" @click="handleConfirm" :disabled="selectedResources.length === 0">
|
||||
确认选择 ({{ selectedResources.length }})
|
||||
<n-button
|
||||
v-if="activeTab === 'library'"
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:disabled="!selectedResource"
|
||||
>
|
||||
确认选择
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="activeTab === 'upload'"
|
||||
type="primary"
|
||||
@click="handleUpload"
|
||||
:disabled="!canUpload"
|
||||
:loading="uploading"
|
||||
>
|
||||
{{ uploading ? '上传中...' : '开始上传' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
@ -43,97 +152,345 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { SearchOutline, VideocamOutline, DocumentOutline, CloudUploadOutline, FolderOpenOutline, CheckmarkOutline } from '@vicons/ionicons5';
|
||||
import TeachCourseApi from '@/api/modules/teachCourse';
|
||||
|
||||
interface Resource {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'ppt' | 'video' | 'document';
|
||||
size: string;
|
||||
uploadDate: string;
|
||||
url: string;
|
||||
type: number; // 0: 视频, 2: 资料
|
||||
fileSize: number;
|
||||
createTime: string;
|
||||
fileUrl: string;
|
||||
originalData?: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
resourceType?: number; // 0: 视频, 2: 资料
|
||||
defaultTab?: string; // 默认激活的标签页: 'library' | 'upload'
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:show', value: boolean): void;
|
||||
(e: 'confirm', selectedResources: Resource[]): void;
|
||||
(e: 'uploadNew', resource: Resource): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
resourceType: 0,
|
||||
defaultTab: 'library'
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const route = useRoute();
|
||||
const message = useMessage();
|
||||
|
||||
const showModal = computed({
|
||||
get: () => props.show,
|
||||
set: (value) => emit('update:show', value)
|
||||
});
|
||||
|
||||
const selectedResources = ref<number[]>([]);
|
||||
// 弹窗标题
|
||||
const modalTitle = computed(() => {
|
||||
return props.resourceType === 0 ? '视频资源库' : '资料文档库';
|
||||
});
|
||||
|
||||
// 模拟资源数据
|
||||
const resources = ref<Resource[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '第一章课程介绍.pptx',
|
||||
type: 'ppt',
|
||||
size: '2.3 MB',
|
||||
uploadDate: '2024-01-15',
|
||||
url: '/resources/chapter1-intro.pptx'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '基础知识讲解视频.mp4',
|
||||
type: 'video',
|
||||
size: '156 MB',
|
||||
uploadDate: '2024-01-14',
|
||||
url: '/resources/basic-knowledge.mp4'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '实践操作演示.pptx',
|
||||
type: 'ppt',
|
||||
size: '4.1 MB',
|
||||
uploadDate: '2024-01-13',
|
||||
url: '/resources/practice-demo.pptx'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '高级技能培训视频.mp4',
|
||||
type: 'video',
|
||||
size: '298 MB',
|
||||
uploadDate: '2024-01-12',
|
||||
url: '/resources/advanced-skills.mp4'
|
||||
// 状态变量
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const selectedResource = ref<number | null>(null);
|
||||
const resources = ref<Resource[]>([]);
|
||||
|
||||
// 选项卡状态
|
||||
const activeTab = ref<string>(props.defaultTab);
|
||||
|
||||
// 上传相关状态
|
||||
const uploadFileList = ref<any[]>([]);
|
||||
const uploadFileName = ref<string>('');
|
||||
const uploading = ref(false);
|
||||
const uploadProgress = ref(0);
|
||||
const uploadStatus = ref('');
|
||||
|
||||
// 过滤后的资源列表
|
||||
const filteredResources = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return resources.value;
|
||||
}
|
||||
]);
|
||||
return resources.value.filter(resource =>
|
||||
resource.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const toggleResourceSelection = (resourceId: number) => {
|
||||
const index = selectedResources.value.indexOf(resourceId);
|
||||
if (index > -1) {
|
||||
selectedResources.value.splice(index, 1);
|
||||
} else {
|
||||
selectedResources.value.push(resourceId);
|
||||
// 是否可以上传
|
||||
const canUpload = computed(() => {
|
||||
return uploadFileList.value.length > 0 &&
|
||||
uploadFileName.value.trim() !== '' &&
|
||||
!uploading.value;
|
||||
});
|
||||
|
||||
// 加载资源列表
|
||||
const loadResources = async () => {
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const courseId = route.params.courseId as string;
|
||||
const response = await TeachCourseApi.queryCourseMaterials({
|
||||
courseId,
|
||||
resourceType: props.resourceType
|
||||
});
|
||||
|
||||
if (response.data && response.data.result) {
|
||||
resources.value = response.data.result.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name || item.fileName || '未命名文件',
|
||||
type: props.resourceType,
|
||||
fileSize: item.fileSize || 0,
|
||||
createTime: item.createTime || item.createdAt || '',
|
||||
fileUrl: item.fileUrl || '',
|
||||
originalData: item
|
||||
}));
|
||||
} else {
|
||||
resources.value = [];
|
||||
console.warn('获取资源列表失败:', response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载资源列表失败:', error);
|
||||
message.error('加载资源列表失败,请重试');
|
||||
resources.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过计算属性 filteredResources 实现
|
||||
};
|
||||
|
||||
// 获取资源图标
|
||||
const getResourceIcon = (type: number) => {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return VideocamOutline;
|
||||
case 2:
|
||||
return DocumentOutline;
|
||||
default:
|
||||
return DocumentOutline;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取上传文件类型
|
||||
const getUploadAccept = (): string => {
|
||||
switch (props.resourceType) {
|
||||
case 0: // 视频
|
||||
return '.mp4,.avi,.mov,.wmv,.flv,.webm';
|
||||
case 2: // 资料
|
||||
return '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt';
|
||||
default:
|
||||
return '*';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取上传提示
|
||||
const getUploadHint = (): string => {
|
||||
switch (props.resourceType) {
|
||||
case 0:
|
||||
return '支持 MP4、AVI、MOV、WMV、FLV、WebM 等视频格式';
|
||||
case 2:
|
||||
return '支持 PDF、Word、Excel、PowerPoint、TXT 等文档格式';
|
||||
default:
|
||||
return '请选择要上传的文件';
|
||||
}
|
||||
};
|
||||
|
||||
// 处理上传文件列表变化
|
||||
const handleUploadFileListChange = (files: any[]) => {
|
||||
uploadFileList.value = files;
|
||||
|
||||
// 自动设置文件名(去掉扩展名)
|
||||
if (files.length > 0 && files[0].file) {
|
||||
const fileName = files[0].file.name;
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
uploadFileName.value = lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
|
||||
} else {
|
||||
uploadFileName.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string): string => {
|
||||
if (!timeStr) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(timeStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hour = String(date.getHours()).padStart(2, '0');
|
||||
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}.${month}.${day} ${hour}:${minute}`;
|
||||
} catch (error) {
|
||||
return timeStr;
|
||||
}
|
||||
};
|
||||
|
||||
// 预览资源
|
||||
// const previewResource = (resource: Resource) => {
|
||||
// if (resource.fileUrl) {
|
||||
// window.open(resource.fileUrl, '_blank');
|
||||
// } else {
|
||||
// message.warning('该资源暂无预览链接');
|
||||
// }
|
||||
// };
|
||||
|
||||
// 切换资源选择(单选模式)
|
||||
const toggleResourceSelection = (resourceId: number) => {
|
||||
if (selectedResource.value === resourceId) {
|
||||
selectedResource.value = null; // 取消选择
|
||||
} else {
|
||||
selectedResource.value = resourceId; // 选择新的资源
|
||||
}
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
const selected = resources.value.filter(resource =>
|
||||
selectedResources.value.includes(resource.id)
|
||||
);
|
||||
emit('confirm', selected);
|
||||
if (!selectedResource.value) {
|
||||
message.warning('请选择一个资源');
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = resources.value.find(resource => resource.id === selectedResource.value);
|
||||
console.log('选中的资源ID:', selectedResource.value);
|
||||
console.log('所有资源:', resources.value);
|
||||
console.log('找到的资源:', selected);
|
||||
|
||||
if (selected) {
|
||||
emit('confirm', [selected]); // 保持数组格式以兼容现有接口
|
||||
} else {
|
||||
message.error('未找到选中的资源');
|
||||
return;
|
||||
}
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async () => {
|
||||
if (!canUpload.value) {
|
||||
message.error('请选择文件并输入文件名');
|
||||
return;
|
||||
}
|
||||
|
||||
uploading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
uploadStatus.value = '准备上传...';
|
||||
|
||||
try {
|
||||
const courseId = route.params.courseId as string;
|
||||
const file = uploadFileList.value[0].file;
|
||||
const fileName = uploadFileName.value.trim();
|
||||
|
||||
uploadStatus.value = '正在上传文件...';
|
||||
uploadProgress.value = 50;
|
||||
|
||||
let uploadResponse;
|
||||
|
||||
// 根据类型调用不同的上传接口
|
||||
if (props.resourceType === 0) {
|
||||
// 上传视频
|
||||
uploadResponse = await TeachCourseApi.uploadCursorVideo({
|
||||
courseId,
|
||||
file,
|
||||
name: fileName
|
||||
});
|
||||
} else if (props.resourceType === 2) {
|
||||
// 上传文档
|
||||
uploadResponse = await TeachCourseApi.uploadCursorDocument({
|
||||
courseId,
|
||||
file,
|
||||
name: fileName
|
||||
});
|
||||
} else {
|
||||
throw new Error('不支持的资源类型');
|
||||
}
|
||||
|
||||
uploadProgress.value = 100;
|
||||
uploadStatus.value = '上传完成';
|
||||
|
||||
// 创建新资源对象
|
||||
const newResource: Resource = {
|
||||
id: (uploadResponse as any)?.data?.result?.id || Date.now(),
|
||||
name: fileName,
|
||||
type: props.resourceType,
|
||||
fileSize: file.size,
|
||||
createTime: new Date().toISOString(),
|
||||
fileUrl: (uploadResponse as any)?.data?.result?.fileUrl || '',
|
||||
originalData: (uploadResponse as any)?.data?.result
|
||||
};
|
||||
|
||||
message.success(`${props.resourceType === 0 ? '视频' : '资料'}上传成功:${fileName}`);
|
||||
|
||||
// 触发上传完成事件
|
||||
emit('uploadNew', newResource);
|
||||
handleCancel();
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传资源文件失败:', error);
|
||||
message.error('文件上传失败,请重试');
|
||||
uploadStatus.value = '上传失败';
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消选择
|
||||
const handleCancel = () => {
|
||||
selectedResources.value = [];
|
||||
selectedResource.value = null;
|
||||
// 重置上传状态
|
||||
uploadFileList.value = [];
|
||||
uploadFileName.value = '';
|
||||
uploading.value = false;
|
||||
uploadProgress.value = 0;
|
||||
uploadStatus.value = '';
|
||||
activeTab.value = 'library';
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
// 当模态框打开时重置选择
|
||||
// 当模态框打开时重置选择并加载数据
|
||||
watch(() => props.show, (newValue) => {
|
||||
if (newValue) {
|
||||
selectedResources.value = [];
|
||||
selectedResource.value = null;
|
||||
searchKeyword.value = '';
|
||||
activeTab.value = props.defaultTab; // 使用props.defaultTab
|
||||
// 重置上传状态
|
||||
uploadFileList.value = [];
|
||||
uploadFileName.value = '';
|
||||
uploading.value = false;
|
||||
uploadProgress.value = 0;
|
||||
uploadStatus.value = '';
|
||||
loadResources();
|
||||
}
|
||||
});
|
||||
|
||||
// 当资源类型改变时重新加载数据
|
||||
watch(() => props.resourceType, () => {
|
||||
if (props.show) {
|
||||
loadResources();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -141,7 +498,20 @@ watch(() => props.show, (newValue) => {
|
||||
<style scoped>
|
||||
.resource-library-container {
|
||||
max-height: 500px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resource-list-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.resource-list {
|
||||
@ -176,16 +546,21 @@ watch(() => props.show, (newValue) => {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.resource-icon img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
.resource-icon {
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resource-item.selected .resource-icon {
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.resource-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.resource-name {
|
||||
@ -193,6 +568,11 @@ watch(() => props.show, (newValue) => {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.resource-item.selected .resource-name {
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.resource-meta {
|
||||
@ -202,6 +582,11 @@ watch(() => props.show, (newValue) => {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.resource-actions {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.selection-indicator {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@ -213,6 +598,23 @@ watch(() => props.show, (newValue) => {
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
@ -220,4 +622,57 @@ watch(() => props.show, (newValue) => {
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 上传相关样式 */
|
||||
.upload-section {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.file-upload-area {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.upload-dragger-content {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
display: block;
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.file-name-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.upload-progress-section {
|
||||
margin: 20px 0;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
margin: 8px 0 0 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
@ -55,6 +55,7 @@ import CertificateManagement from '@/views/teacher/certificate/CertificateManage
|
||||
import DiscussionManagement from '@/views/teacher/course/DiscussionManagement.vue'
|
||||
import CommentView from '@/views/teacher/course/CommentView.vue'
|
||||
import AddDiscussion from '@/views/teacher/course/AddDiscussion.vue'
|
||||
import DiscussionRepliesManagement from '@/views/teacher/course/DiscussionRepliesManagement.vue'
|
||||
import StatisticsManagement from '@/views/teacher/statistics/StatisticsManagement.vue'
|
||||
import NotificationManagement from '@/views/teacher/course/NotificationManagement.vue'
|
||||
import GeneralManagement from '@/views/teacher/course/GeneralManagement.vue'
|
||||
@ -303,6 +304,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: CommentView,
|
||||
meta: { title: '评论详情' }
|
||||
},
|
||||
{
|
||||
path: 'discussion/:discussionId/replies',
|
||||
name: 'DiscussionRepliesManagement',
|
||||
component: DiscussionRepliesManagement,
|
||||
meta: { title: '讨论回复管理' }
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
name: 'StatisticsManagement',
|
||||
|
@ -9,14 +9,14 @@
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<h1 class="page-title">添加讨论</h1>
|
||||
<h1 class="page-title">{{ isEditMode ? '编辑讨论' : '添加讨论' }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- 讨论表单 -->
|
||||
<div class="discussion-form">
|
||||
<!-- 标题输入 -->
|
||||
<div class="form-group">
|
||||
<input v-model="discussionTitle" type="text" placeholder="请添加标题" class="title-input" />
|
||||
<n-input v-model:value="discussionTitle" placeholder="请添加标题" size="large" />
|
||||
</div>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
@ -35,32 +35,22 @@
|
||||
<div class="section-actions-container">
|
||||
<!-- 章节选择 -->
|
||||
<div class="section-selector-wrapper">
|
||||
<button @click="toggleSectionSelector" class="section-selector" :class="{ active: showSectionSelector }">
|
||||
<span>{{ selectedSection || '选择章节' }}</span>
|
||||
<img :src="showSectionSelector ? '/images/teacher/箭头-蓝.png' : '/images/teacher/箭头-灰.png'" alt="箭头"
|
||||
class="arrow-icon" />
|
||||
</button>
|
||||
|
||||
<!-- 章节选择弹窗 -->
|
||||
<div v-if="showSectionSelector" class="section-popover">
|
||||
<div class="popover-header">选择章节:</div>
|
||||
<div class="section-list">
|
||||
<label v-for="section in sections" :key="section.id" class="section-item">
|
||||
<input type="radio" :value="section.id" v-model="selectedSectionId" class="section-radio" />
|
||||
<span class="section-name">{{ section.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="popover-actions">
|
||||
<n-button @click="cancelSectionSelection">取消</n-button>
|
||||
<n-button type="primary" @click="confirmSectionSelection">确认</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="selectedSectionId"
|
||||
:options="sectionOptions"
|
||||
:loading="chapterLoading"
|
||||
placeholder="选择章节"
|
||||
size="large"
|
||||
style="min-width: 256px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<n-button @click="cancelDiscussion">取消</n-button>
|
||||
<n-button type="primary" @click="publishDiscussion">发布</n-button>
|
||||
<n-button type="primary" :loading="publishing" @click="publishDiscussion">
|
||||
{{ isEditMode ? '保存' : '发布' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -72,16 +62,32 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, shallowRef } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount, shallowRef, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { NButton, NInput, NSelect, useMessage } from 'naive-ui'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
// @ts-ignore
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||
import TeachCourseApi, { DiscussionApi } from '@/api/modules/teachCourse'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
// 获取课程ID
|
||||
const courseId = computed(() => route.params.id)
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditMode = computed(() => route.query.mode === 'edit' && route.query.id)
|
||||
const discussionId = computed(() => route.query.id)
|
||||
|
||||
// 章节数据状态
|
||||
const chapterOptions = ref([])
|
||||
const chapterLoading = ref(false)
|
||||
|
||||
// 发布状态
|
||||
const publishing = ref(false)
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
@ -109,35 +115,72 @@ const handleCreated = (editor) => {
|
||||
// 表单数据
|
||||
const discussionTitle = ref('')
|
||||
const discussionContent = ref('')
|
||||
const selectedSection = ref('')
|
||||
const selectedSectionId = ref('')
|
||||
const showSectionSelector = ref(false)
|
||||
|
||||
// 章节数据
|
||||
const sections = ref([
|
||||
{ id: '1', name: '第一节 课程定位与目标' },
|
||||
{ id: '2', name: '第二节 课程定位与目标第二节课程定位与目标' },
|
||||
{ id: '3', name: '第二节 课程定位与目标第二节课程定位与目标' },
|
||||
{ id: '4', name: '第二节 课程定位与目标' }
|
||||
])
|
||||
// 为 n-select 创建选项
|
||||
const sectionOptions = computed(() => {
|
||||
return chapterOptions.value.map(chapter => ({
|
||||
label: chapter.label,
|
||||
value: chapter.value
|
||||
}))
|
||||
})
|
||||
|
||||
// 切换章节选择器
|
||||
const toggleSectionSelector = () => {
|
||||
showSectionSelector.value = !showSectionSelector.value
|
||||
}
|
||||
|
||||
// 取消章节选择
|
||||
const cancelSectionSelection = () => {
|
||||
showSectionSelector.value = false
|
||||
}
|
||||
|
||||
// 确认章节选择
|
||||
const confirmSectionSelection = () => {
|
||||
const section = sections.value.find(s => s.id === selectedSectionId.value)
|
||||
if (section) {
|
||||
selectedSection.value = section.name
|
||||
// 获取章节选项数据
|
||||
const fetchChapterOptions = async () => {
|
||||
if (!courseId.value) {
|
||||
console.warn('课程ID不存在,无法获取章节数据')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
chapterLoading.value = true
|
||||
|
||||
const response = await TeachCourseApi.getCourseSections(courseId.value)
|
||||
const sections = response.data.result || []
|
||||
// 只保留 level 为 1 的章节数据
|
||||
const chapters = sections.filter((section) => section.level === 1)
|
||||
|
||||
// 转换为下拉选项格式
|
||||
chapterOptions.value = chapters.map((chapter) => ({
|
||||
label: chapter.name || '',
|
||||
value: chapter.id || ''
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取章节数据失败:', error)
|
||||
chapterOptions.value = []
|
||||
} finally {
|
||||
chapterLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载讨论详情(编辑模式)
|
||||
const loadDiscussionDetail = async () => {
|
||||
if (!isEditMode.value || !discussionId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔍 加载讨论详情:', discussionId.value)
|
||||
const response = await DiscussionApi.getDiscussionById(discussionId.value)
|
||||
|
||||
if (response.data && response.data.result) {
|
||||
const discussion = response.data.result
|
||||
|
||||
// 回显数据
|
||||
discussionTitle.value = discussion.title || ''
|
||||
discussionContent.value = discussion.description || ''
|
||||
selectedSectionId.value = discussion.sectionId || ''
|
||||
|
||||
console.log('📝 讨论详情已加载:', discussion)
|
||||
message.success('讨论详情加载完成')
|
||||
} else {
|
||||
console.warn('⚠️ 讨论详情加载失败:', response)
|
||||
message.error('加载讨论详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载讨论详情失败:', error)
|
||||
message.error('加载讨论详情失败:' + (error.message || '网络错误'))
|
||||
}
|
||||
showSectionSelector.value = false
|
||||
}
|
||||
|
||||
// 取消讨论
|
||||
@ -146,39 +189,68 @@ const cancelDiscussion = () => {
|
||||
}
|
||||
|
||||
// 发布讨论
|
||||
const publishDiscussion = () => {
|
||||
const publishDiscussion = async () => {
|
||||
// 表单验证
|
||||
if (!discussionTitle.value.trim()) {
|
||||
alert('请输入讨论标题')
|
||||
message.warning('请输入讨论标题')
|
||||
return
|
||||
}
|
||||
if (!discussionContent.value.trim()) {
|
||||
alert('请输入讨论内容')
|
||||
return
|
||||
}
|
||||
if (!selectedSection.value) {
|
||||
alert('请选择章节')
|
||||
message.warning('请输入讨论内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 这里可以调用API发布讨论
|
||||
console.log('发布讨论:', {
|
||||
title: discussionTitle.value,
|
||||
content: discussionContent.value,
|
||||
section: selectedSection.value
|
||||
})
|
||||
try {
|
||||
publishing.value = true
|
||||
|
||||
// 发布成功后返回讨论列表
|
||||
router.go(-1)
|
||||
let response
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式:调用编辑接口
|
||||
response = await DiscussionApi.editDiscussion({
|
||||
id: discussionId.value,
|
||||
title: discussionTitle.value,
|
||||
description: discussionContent.value,
|
||||
sectionId: selectedSectionId.value
|
||||
})
|
||||
} else {
|
||||
// 新增模式:调用新增接口
|
||||
response = await DiscussionApi.createDiscussion({
|
||||
title: discussionTitle.value,
|
||||
description: discussionContent.value,
|
||||
sectionId: selectedSectionId.value,
|
||||
courseId: courseId.value
|
||||
})
|
||||
}
|
||||
|
||||
if (response.data && (response.data.success === true || response.data.code === 200 || response.data.code === 0)) {
|
||||
message.success(isEditMode.value ? '讨论编辑成功' : '讨论发布成功')
|
||||
router.go(-1)
|
||||
} else {
|
||||
console.error('操作失败:', response.data)
|
||||
message.error(`操作失败:${response.data?.message || '未知错误'}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
message.error(`操作失败:${error.message || '网络错误'}`)
|
||||
} finally {
|
||||
publishing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 组件挂载时的初始化逻辑
|
||||
onMounted(async () => {
|
||||
// 获取章节数据
|
||||
await fetchChapterOptions()
|
||||
|
||||
// 如果是编辑模式,加载讨论详情
|
||||
if (isEditMode.value) {
|
||||
await loadDiscussionDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add-discussion {
|
||||
min-height: 100vh;
|
||||
min-height: 100%;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
}
|
||||
@ -217,140 +289,12 @@ onMounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.title-input:focus {
|
||||
border-color: #0288D1;
|
||||
}
|
||||
|
||||
.rich-editor {
|
||||
border: 1px solid #e0e0e0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* WangEditor 富文本编辑器样式已由组件处理 */
|
||||
|
||||
.section-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 256px;
|
||||
height: 39px;
|
||||
padding: 12px 16px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-selector span {
|
||||
transition: color 0.2s;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.section-selector:hover,
|
||||
.section-selector.active {
|
||||
border-color: #0C99DA;
|
||||
}
|
||||
|
||||
.section-selector:hover span,
|
||||
.section-selector.active span {
|
||||
color: #0C99DA;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 13px;
|
||||
height: 8px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-popover {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 444px;
|
||||
min-height: 200px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 2px 24px 0px rgba(220, 220, 220, 0.5);
|
||||
z-index: 1000;
|
||||
margin-top: 45px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
padding-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
border-bottom: 1.5px solid #E6E6E6;
|
||||
}
|
||||
|
||||
.section-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.section-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.section-radio {
|
||||
margin-right: 12px;
|
||||
accent-color: #0288D1;
|
||||
}
|
||||
|
||||
.section-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.popover-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popover-actions :deep(.n-button) {
|
||||
height: 32px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.popover-actions :deep(.n-button--default-type) {
|
||||
border: 1px solid #0288D1 !important;
|
||||
background: #E2F5FF !important;
|
||||
color: #0288D1 !important;
|
||||
}
|
||||
|
||||
.popover-actions :deep(.n-button--default-type:hover) {
|
||||
border-color: #0277BD !important;
|
||||
background: #D1F0FF !important;
|
||||
color: #0277BD !important;
|
||||
}
|
||||
|
||||
/* 自定义按钮样式已由n-button组件处理 */
|
||||
|
||||
.agreement-text {
|
||||
|
@ -1,72 +1,50 @@
|
||||
<template>
|
||||
<div class="add-homework-container">
|
||||
<div class="form-container">
|
||||
<n-spin :show="dataLoading" description="正在加载作业数据...">
|
||||
<div class="form-container">
|
||||
<!-- 表单内容 -->
|
||||
<div class="form-content">
|
||||
<!-- 上半部分:两列布局 -->
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<n-button quaternary circle size="large" @click="handleCancel" class="back-button">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<h2 class="page-title">{{ isEditMode ? '编辑作业' : '新建作业' }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- 上半部分:两列布局 -->
|
||||
<div class="form-row">
|
||||
<!-- 左列 -->
|
||||
<div class="form-column">
|
||||
<!-- 作业名称 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label required">作业名称:</label>
|
||||
<n-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入作业名称"
|
||||
class="form-input"
|
||||
/>
|
||||
<n-input v-model:value="formData.name" placeholder="请输入作业名称" class="form-input" />
|
||||
</div>
|
||||
|
||||
<!-- 绑定班级 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label required">绑定班级:</label>
|
||||
<n-select
|
||||
v-model:value="formData.boundClass"
|
||||
:options="classOptions"
|
||||
placeholder="请选择班级"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 作业开始时间 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label required">作业开始时间:</label>
|
||||
<n-date-picker
|
||||
v-model:value="formData.startTime"
|
||||
type="datetime"
|
||||
placeholder="选择时间"
|
||||
class="form-input"
|
||||
/>
|
||||
<n-date-picker v-model:value="formData.startTime" type="datetime" placeholder="选择时间" class="form-input" />
|
||||
</div>
|
||||
|
||||
<!-- 允许作业补交 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label required">允许作业补交:</label>
|
||||
<div class="toggle-container">
|
||||
<n-switch
|
||||
v-model:value="formData.allowLateSubmission"
|
||||
class="form-toggle"
|
||||
/>
|
||||
<n-switch v-model:value="formData.allowLateSubmission" class="form-toggle" />
|
||||
<span class="toggle-label">允许补交</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 补交时间选择器 -->
|
||||
<div class="form-item" v-if="formData.allowLateSubmission">
|
||||
<label class="form-label"></label>
|
||||
<n-date-picker
|
||||
v-model:value="formData.lateSubmissionTime"
|
||||
type="datetime"
|
||||
placeholder="请选择补交时间"
|
||||
class="form-input"
|
||||
/>
|
||||
<n-date-picker v-model:value="formData.lateSubmissionTime" type="datetime" placeholder="请选择补交时间"
|
||||
class="form-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -75,35 +53,30 @@
|
||||
<!-- 所属章节 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label">所属章节:</label>
|
||||
<n-select
|
||||
v-model:value="formData.chapter"
|
||||
:options="chapterOptions"
|
||||
placeholder="请选择所属章节"
|
||||
class="form-input"
|
||||
/>
|
||||
<n-select v-model:value="formData.chapter" :options="chapterOptions" :loading="chapterLoading"
|
||||
placeholder="请选择所属章节" class="form-input" />
|
||||
</div>
|
||||
|
||||
<!-- 创建人 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label required">创建人:</label>
|
||||
<n-select
|
||||
v-model:value="formData.creator"
|
||||
multiple
|
||||
:options="creatorOptions"
|
||||
placeholder="请选择创建人"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 作业结束时间 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label required">作业结束时间:</label>
|
||||
<n-date-picker
|
||||
v-model:value="formData.endTime"
|
||||
type="datetime"
|
||||
placeholder="选择时间"
|
||||
class="form-input"
|
||||
/>
|
||||
<n-date-picker v-model:value="formData.endTime" type="datetime" placeholder="选择时间" class="form-input" />
|
||||
</div>
|
||||
|
||||
<!-- 是否指定班级 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label">是否指定班级:</label>
|
||||
<div class="toggle-container">
|
||||
<n-switch v-model:value="formData.enableClassBinding" class="form-toggle" />
|
||||
<span class="toggle-label">指定班级</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绑定班级 -->
|
||||
<div class="form-item" v-if="formData.enableClassBinding">
|
||||
<label class="form-label required">绑定班级:</label>
|
||||
<n-select v-model:value="formData.boundClass" :options="classOptions" :loading="classLoading" multiple
|
||||
placeholder="请选择班级" class="form-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,19 +86,10 @@
|
||||
<label class="form-label required">作业内容:</label>
|
||||
<div class="rich-editor-container">
|
||||
<div class="editor-container">
|
||||
<Toolbar
|
||||
style="border-bottom: 1px solid #ccc"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Editor
|
||||
style="height: 300px; overflow-y: hidden;"
|
||||
v-model="formData.content"
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="mode"
|
||||
@onCreated="handleCreated"
|
||||
/>
|
||||
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig"
|
||||
:mode="mode" />
|
||||
<Editor style="height: 300px; overflow-y: hidden;" v-model="formData.content"
|
||||
:defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -133,40 +97,29 @@
|
||||
<!-- 下半部分:设置选项 -->
|
||||
<div class="form-section">
|
||||
<!-- 积分设置 -->
|
||||
<div class="form-item form-integral">
|
||||
<label class="form-label required">积分设置</label>
|
||||
<div class="form-item form-integral" v-if="false">
|
||||
<label class="form-label">积分设置</label>
|
||||
<div class="setting-container">
|
||||
<n-switch
|
||||
v-model:value="formData.enableScore"
|
||||
class="form-toggle"
|
||||
/>
|
||||
<span class="setting-label">提交可获得</span>
|
||||
<n-input-number
|
||||
v-model:value="formData.score"
|
||||
:min="0"
|
||||
class="score-input"
|
||||
:show-button="false"
|
||||
/>
|
||||
<span class="setting-label">积分</span>
|
||||
<n-switch v-model:value="formData.enableScore" class="form-toggle" />
|
||||
<template v-if="formData.enableScore">
|
||||
<span class="setting-label">提交可获得</span>
|
||||
<n-input-number v-model:value="formData.score" :min="0" class="score-input" :show-button="false" />
|
||||
<span class="setting-label">积分</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 督促设置 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label required">督促设置</label>
|
||||
<div class="form-item form-integral">
|
||||
<label class="form-label">督促设置</label>
|
||||
<div class="setting-container">
|
||||
<n-switch
|
||||
v-model:value="formData.enableReminder"
|
||||
class="form-toggle"
|
||||
/>
|
||||
<span class="setting-label">作业结束前</span>
|
||||
<n-input-number
|
||||
v-model:value="formData.reminderHours"
|
||||
:min="0"
|
||||
class="reminder-input"
|
||||
:show-button="false"
|
||||
/>
|
||||
<span class="setting-label">小时发通知提醒未交学生</span>
|
||||
<n-switch v-model:value="formData.enableReminder" class="form-toggle" />
|
||||
<template v-if="formData.enableReminder">
|
||||
<span class="setting-label">作业结束前</span>
|
||||
<n-input-number v-model:value="formData.reminderHours" :min="0" class="reminder-input"
|
||||
:show-button="false" />
|
||||
<span class="setting-label">小时发通知提醒未交学生</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -175,34 +128,59 @@
|
||||
<!-- 底部按钮 -->
|
||||
<div class="form-footer">
|
||||
<n-button class="cancel-btn" @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" class="save-btn" @click="handleSave">{{ isEditMode ? '更新' : '保存' }}</n-button>
|
||||
<n-button type="primary" class="save-btn" :loading="saving" @click="handleSave">{{ isEditMode ? '更新' : '保存'
|
||||
}}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, shallowRef, onBeforeUnmount, onMounted, computed } from 'vue'
|
||||
import { reactive, ref, shallowRef, onBeforeUnmount, onMounted, computed, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
NInput,
|
||||
NDatePicker,
|
||||
NSelect,
|
||||
NSwitch,
|
||||
NButton,
|
||||
NInputNumber
|
||||
import {
|
||||
NInput,
|
||||
NDatePicker,
|
||||
NSelect,
|
||||
NSwitch,
|
||||
NButton,
|
||||
NInputNumber,
|
||||
NIcon,
|
||||
NSpin,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
// @ts-ignore
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||
import TeachCourseApi, { type CourseSection, ClassApi, HomeworkApi, type CreateHomeworkRequest } from '@/api/modules/teachCourse'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
// 获取路由参数
|
||||
const isEditMode = computed(() => route.query.mode === 'edit')
|
||||
const homeworkId = computed(() => route.query.id as string)
|
||||
const courseId = computed(() => route.params.id as string)
|
||||
|
||||
// 章节数据状态
|
||||
const chapterOptions = ref<{ label: string; value: string }[]>([])
|
||||
const chapterLoading = ref(false)
|
||||
|
||||
// 班级数据状态
|
||||
const classOptions = ref<{ label: string; value: string }[]>([])
|
||||
const classLoading = ref(false)
|
||||
|
||||
// 保存状态
|
||||
const saving = ref(false)
|
||||
|
||||
// 数据加载状态
|
||||
const dataLoading = ref(false)
|
||||
|
||||
// 原始作业数据,用于编辑时保持某些字段不变
|
||||
const originalHomeworkData = ref<any>(null)
|
||||
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef()
|
||||
@ -210,47 +188,93 @@ const editorRef = shallowRef()
|
||||
// 内容 HTML
|
||||
const valueHtml = ref('<p>hello</p>')
|
||||
|
||||
// 模拟 ajax 异步获取内容
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 获取章节数据
|
||||
await fetchChapterOptions()
|
||||
// 获取班级数据
|
||||
await fetchClassOptions()
|
||||
|
||||
if (isEditMode.value && homeworkId.value) {
|
||||
// 编辑模式:获取作业数据并回显
|
||||
loadHomeworkData(homeworkId.value)
|
||||
} else {
|
||||
// 新增模式:设置默认内容
|
||||
setTimeout(() => {
|
||||
valueHtml.value = '<p>模拟 Ajax 异步设置内容</p>'
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
|
||||
// 加载作业数据
|
||||
const loadHomeworkData = async (_id: string) => {
|
||||
const loadHomeworkData = async (id: string) => {
|
||||
try {
|
||||
// 这里应该调用实际的API获取作业数据
|
||||
// 模拟API调用
|
||||
const mockData = {
|
||||
name: '示例作业',
|
||||
boundClass: 'class1',
|
||||
startTime: '2024-01-01 00:00:00',
|
||||
endTime: '2024-01-31 23:59:59',
|
||||
allowLateSubmission: true,
|
||||
lateSubmissionTime: '2024-02-07 23:59:59',
|
||||
chapter: 'chapter1',
|
||||
creator: ['liqinglin', 'zhangsan'],
|
||||
content: '<p>这是示例作业内容,包含<strong>粗体文字</strong>和<em>斜体文字</em>。</p><p>第二段内容:</p><ul><li>列表项1</li><li>列表项2</li><li>列表项3</li></ul>',
|
||||
enableScore: true,
|
||||
score: 10,
|
||||
enableReminder: true,
|
||||
reminderHours: 24
|
||||
dataLoading.value = true
|
||||
console.log('编辑模式,作业ID:', id)
|
||||
|
||||
// 调用真实的获取作业详情接口
|
||||
const response = await HomeworkApi.getHomeworkById(id)
|
||||
const homeworkData = response.data.result
|
||||
|
||||
console.log('获取到的作业数据:', homeworkData)
|
||||
console.log('开始解析字段:', {
|
||||
startTime: (homeworkData as any).startTime || (homeworkData as any).start_time,
|
||||
endTime: (homeworkData as any).endTime || (homeworkData as any).end_time,
|
||||
makeupTime: (homeworkData as any).makeupTime || (homeworkData as any).makeup_time,
|
||||
notifyTime: (homeworkData as any).notifyTime || (homeworkData as any).notify_time
|
||||
})
|
||||
|
||||
// 保存原始数据
|
||||
originalHomeworkData.value = homeworkData
|
||||
|
||||
// 将时间字符串转换为时间戳
|
||||
const parseDateTime = (dateTimeStr: string | null | undefined): number | null => {
|
||||
if (!dateTimeStr) return null
|
||||
return new Date(dateTimeStr).getTime()
|
||||
}
|
||||
|
||||
// 处理班级ID字符串转数组
|
||||
const parseClassIds = (classIdStr: string | null | undefined): string[] => {
|
||||
if (!classIdStr) return []
|
||||
return classIdStr.split(',').filter(id => id.trim())
|
||||
}
|
||||
|
||||
// 将API返回的数据映射到表单字段
|
||||
// 使用类型断言处理可能的字段名差异
|
||||
const data = homeworkData as any
|
||||
|
||||
const mappedData = {
|
||||
name: homeworkData.title || '',
|
||||
boundClass: parseClassIds(homeworkData.classId),
|
||||
// 修复时间字段映射 - 支持驼峰和下划线两种格式
|
||||
startTime: parseDateTime(data.startTime || data.start_time),
|
||||
endTime: parseDateTime(data.endTime || data.end_time),
|
||||
// 修复补交逻辑 - 根据makeupTime判断是否允许补交
|
||||
allowLateSubmission: !!(data.makeupTime || data.makeup_time),
|
||||
lateSubmissionTime: parseDateTime(data.makeupTime || data.makeup_time),
|
||||
chapter: homeworkData.sectionId || null,
|
||||
content: homeworkData.description || '',
|
||||
enableScore: (homeworkData.max_score !== null && homeworkData.max_score !== undefined && homeworkData.max_score > 0),
|
||||
score: homeworkData.max_score || 0,
|
||||
// 修复督促设置逻辑 - 根据notifyTime判断是否开启督促
|
||||
enableReminder: !!((data.notifyTime || data.notify_time) && (data.notifyTime || data.notify_time) !== 0 && (data.notifyTime || data.notify_time) !== '0'),
|
||||
reminderHours: (() => {
|
||||
const notifyTime = data.notifyTime || data.notify_time
|
||||
return typeof notifyTime === 'string' ? parseInt(notifyTime) || 0 : notifyTime || 0
|
||||
})(),
|
||||
enableClassBinding: parseClassIds(homeworkData.classId).length > 0
|
||||
}
|
||||
|
||||
// 回显数据到表单
|
||||
Object.assign(formData, mockData)
|
||||
valueHtml.value = mockData.content
|
||||
|
||||
console.log('作业数据加载成功:', mockData)
|
||||
Object.assign(formData, mappedData)
|
||||
|
||||
// 设置编辑器内容
|
||||
if (homeworkData.description) {
|
||||
setTimeout(() => {
|
||||
valueHtml.value = homeworkData.description || '<p></p>'
|
||||
}, 100)
|
||||
}
|
||||
|
||||
console.log('作业数据回显成功:', mappedData)
|
||||
} catch (error) {
|
||||
console.error('加载作业数据失败:', error)
|
||||
message.error('加载作业数据失败')
|
||||
} finally {
|
||||
dataLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,47 +296,150 @@ const handleCreated = (editor: any) => {
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
boundClass: null,
|
||||
boundClass: [],
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
allowLateSubmission: true,
|
||||
lateSubmissionTime: null,
|
||||
chapter: null,
|
||||
creator: [],
|
||||
content: '',
|
||||
enableScore: true,
|
||||
score: 0,
|
||||
enableReminder: true,
|
||||
reminderHours: 0
|
||||
reminderHours: 0,
|
||||
enableClassBinding: false
|
||||
})
|
||||
|
||||
// 章节选项
|
||||
const chapterOptions = [
|
||||
{ label: '第一章 课程介绍', value: 'chapter1' },
|
||||
{ label: '第二章 基础知识', value: 'chapter2' },
|
||||
{ label: '第三章 进阶内容', value: 'chapter3' }
|
||||
]
|
||||
// 监听富文本编辑器内容变化,同步到表单
|
||||
watch(valueHtml, (newVal) => {
|
||||
formData.content = newVal
|
||||
})
|
||||
|
||||
// 班级选项
|
||||
const classOptions = [
|
||||
{ label: '班级一', value: 'class1' },
|
||||
{ label: '班级二', value: 'class2' },
|
||||
{ label: '班级三', value: 'class3' },
|
||||
{ label: '班级四', value: 'class4' }
|
||||
]
|
||||
// 监听表单内容变化,同步到编辑器
|
||||
watch(() => formData.content, (newVal) => {
|
||||
if (newVal !== valueHtml.value && editorRef.value) {
|
||||
valueHtml.value = newVal
|
||||
}
|
||||
})
|
||||
|
||||
// 创建人选项
|
||||
const creatorOptions = [
|
||||
{ label: '李清林', value: 'liqinglin' },
|
||||
{ label: '张三', value: 'zhangsan' },
|
||||
{ label: '李四', value: 'lisi' },
|
||||
{ label: '王五', value: 'wangwu' },
|
||||
{ label: '赵六', value: 'zhaoliu' },
|
||||
{ label: '孙七', value: 'sunqi' },
|
||||
{ label: '周八', value: 'zhouba' },
|
||||
{ label: '吴九', value: 'wujiu' },
|
||||
{ label: '郑十', value: 'zhengshi' }
|
||||
]
|
||||
// 获取章节选项数据
|
||||
const fetchChapterOptions = async () => {
|
||||
|
||||
if (!courseId.value) {
|
||||
console.warn('课程ID不存在,无法获取章节数据')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
chapterLoading.value = true
|
||||
|
||||
const response = await TeachCourseApi.getCourseSections(courseId.value)
|
||||
const sections = response.data.result || []
|
||||
// 只保留 level 为 1 的章节数据
|
||||
const chapters = sections.filter((section: CourseSection) => section.level === 1)
|
||||
|
||||
// 转换为下拉选项格式
|
||||
chapterOptions.value = chapters.map((chapter: CourseSection) => ({
|
||||
label: chapter.name || '',
|
||||
value: chapter.id || ''
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取章节数据失败:', error)
|
||||
chapterOptions.value = []
|
||||
} finally {
|
||||
chapterLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取班级选项数据
|
||||
const fetchClassOptions = async () => {
|
||||
if (!courseId.value) {
|
||||
console.warn('课程ID不存在,无法获取班级数据')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
classLoading.value = true
|
||||
console.log('获取课程班级数据,courseId:', courseId.value)
|
||||
|
||||
const response = await ClassApi.queryClassList({ course_id: courseId.value })
|
||||
const classes = response.data.result || []
|
||||
|
||||
// 转换为下拉选项格式,使用 name 和 id
|
||||
classOptions.value = classes.map((classItem: any) => ({
|
||||
label: classItem.name || '',
|
||||
value: classItem.id || ''
|
||||
}))
|
||||
|
||||
console.log('班级数据获取成功:', classOptions.value)
|
||||
} catch (error) {
|
||||
console.error('获取班级数据失败:', error)
|
||||
classOptions.value = []
|
||||
} finally {
|
||||
classLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 将表单数据转换为编辑API接口需要的格式
|
||||
const transformFormDataToEditApi = (): any => {
|
||||
// 将时间戳转换为字符串格式
|
||||
const formatDateTime = (timestamp: number | null): string => {
|
||||
if (!timestamp) return ''
|
||||
return new Date(timestamp).toISOString().replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
const classIds = formData.enableClassBinding && Array.isArray(formData.boundClass)
|
||||
? formData.boundClass.join(',')
|
||||
: ''
|
||||
|
||||
return {
|
||||
id: homeworkId.value, // 编辑时需要ID
|
||||
title: formData.name || '',
|
||||
description: formData.content || '',
|
||||
startTime: formatDateTime(formData.startTime),
|
||||
endTime: formatDateTime(formData.endTime),
|
||||
allowMakeup: formData.allowLateSubmission ? '1' : '0',
|
||||
makeupTime: formatDateTime(formData.lateSubmissionTime),
|
||||
notifyTime: formData.enableReminder ? formData.reminderHours : 0,
|
||||
classId: classIds,
|
||||
sectionId: formData.chapter || '',
|
||||
maxScore: formData.enableScore ? formData.score : null,
|
||||
passScore: originalHomeworkData.value?.pass_score || null, // 保持原值
|
||||
attachment: originalHomeworkData.value?.attachment || null, // 保持原值
|
||||
status: originalHomeworkData.value?.status || null // 保持原有状态
|
||||
}
|
||||
}
|
||||
|
||||
// 将表单数据转换为API接口需要的格式
|
||||
const transformFormDataToApi = (): CreateHomeworkRequest => {
|
||||
// 根据开关状态处理班级数据
|
||||
const classIds = formData.enableClassBinding && Array.isArray(formData.boundClass)
|
||||
? formData.boundClass.join(',')
|
||||
: ''
|
||||
|
||||
// 将时间戳转换为字符串格式
|
||||
const formatDateTime = (timestamp: number | null): string => {
|
||||
if (!timestamp) return ''
|
||||
return new Date(timestamp).toISOString().replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
return {
|
||||
courseId: courseId.value,
|
||||
title: formData.name || '',
|
||||
description: formData.content || '',
|
||||
startTime: formatDateTime(formData.startTime),
|
||||
endTime: formatDateTime(formData.endTime),
|
||||
allowMakeup: formData.allowLateSubmission ? '1' : '0',
|
||||
makeupTime: formatDateTime(formData.lateSubmissionTime),
|
||||
notifyTime: formData.enableReminder ? formData.reminderHours : 0,
|
||||
classId: classIds,
|
||||
sectionId: formData.chapter || '',
|
||||
// 积分相关字段(先按本地字段传递)
|
||||
maxScore: formData.enableScore ? formData.score : null,
|
||||
passScore: null, // 接口中有但ui中没有,设为null
|
||||
attachment: null // 接口中有但ui中没有,设为null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -321,16 +448,95 @@ const handleCancel = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = () => {
|
||||
if (isEditMode.value) {
|
||||
console.log('更新作业数据:', formData)
|
||||
// 这里添加更新逻辑
|
||||
} else {
|
||||
console.log('保存作业数据:', formData)
|
||||
// 这里添加保存逻辑
|
||||
// 表单验证
|
||||
const validateForm = (): string | null => {
|
||||
if (!formData.name?.trim()) {
|
||||
return '请输入作业名称'
|
||||
}
|
||||
if (!formData.startTime) {
|
||||
return '请选择作业开始时间'
|
||||
}
|
||||
if (!formData.endTime) {
|
||||
return '请选择作业结束时间'
|
||||
}
|
||||
if (!formData.content?.trim()) {
|
||||
return '请输入作业内容'
|
||||
}
|
||||
if (formData.allowLateSubmission && !formData.lateSubmissionTime) {
|
||||
return '请选择补交时间'
|
||||
}
|
||||
if (formData.enableClassBinding && (!formData.boundClass || formData.boundClass.length === 0)) {
|
||||
return '开启班级指定后,请至少选择一个班级'
|
||||
}
|
||||
if (new Date(formData.startTime) >= new Date(formData.endTime)) {
|
||||
return '作业开始时间不能晚于结束时间'
|
||||
}
|
||||
if (formData.allowLateSubmission && formData.lateSubmissionTime && new Date(formData.endTime) >= new Date(formData.lateSubmissionTime)) {
|
||||
return '补交时间不能早于作业结束时间'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
// 表单验证
|
||||
const validationError = validateForm()
|
||||
if (validationError) {
|
||||
message.warning(validationError)
|
||||
return
|
||||
}
|
||||
|
||||
if (isEditMode.value) {
|
||||
// 编辑作业
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
// 转换表单数据为编辑API格式
|
||||
const editData = transformFormDataToEditApi()
|
||||
console.log('发送作业编辑请求:', editData)
|
||||
|
||||
// 调用编辑作业接口
|
||||
const response = await HomeworkApi.editHomework(editData)
|
||||
|
||||
if (response.data && (response.data.success === true || response.data.code === 200 || response.data.code === 0)) {
|
||||
message.success('作业更新成功')
|
||||
router.back()
|
||||
} else {
|
||||
console.error('作业更新失败:', response.data)
|
||||
message.error('作业更新失败:' + (response.data?.message || '未知错误'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('作业更新失败:', error)
|
||||
message.error('作业更新失败:' + (error.message || '网络错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
} else {
|
||||
// 新增作业
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
// 转换表单数据为API格式
|
||||
const apiData = transformFormDataToApi()
|
||||
console.log('发送作业创建请求:', apiData)
|
||||
|
||||
// 调用创建作业接口
|
||||
const response = await HomeworkApi.createHomework(apiData)
|
||||
|
||||
if (response.data && (response.data.success === true || response.data.code === 200 || response.data.code === 0)) {
|
||||
message.success('作业创建成功')
|
||||
router.back()
|
||||
} else {
|
||||
console.error('作业创建失败:', response.data)
|
||||
message.error('作业创建失败:' + (response.data?.message || '未知错误'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('作业创建失败:', error)
|
||||
message.error('作业创建失败:' + (error.message || '网络错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -348,6 +554,24 @@ const handleSave = () => {
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 160px;
|
||||
@ -433,57 +657,6 @@ const handleSave = () => {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏默认下拉箭头 */
|
||||
.form-input :deep(.n-base-suffix__arrow) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 为所属章节和创建人下拉菜单添加自定义箭头 */
|
||||
.form-column:last-child .form-item:nth-child(1) .form-input :deep(.n-base-selection),
|
||||
.form-column:last-child .form-item:nth-child(2) .form-input :deep(.n-base-selection) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-column:last-child .form-item:nth-child(1) .form-input :deep(.n-base-selection)::after,
|
||||
.form-column:last-child .form-item:nth-child(2) .form-input :deep(.n-base-selection)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 13px;
|
||||
height: 8px;
|
||||
background-image: url('/images/teacher/箭头-灰.png');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 备用选择器 */
|
||||
.form-column:last-child .form-item:nth-child(1) .form-input :deep(.n-base-selection-input),
|
||||
.form-column:last-child .form-item:nth-child(2) .form-input :deep(.n-base-selection-input) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-column:last-child .form-item:nth-child(1) .form-input :deep(.n-base-selection-input)::after,
|
||||
.form-column:last-child .form-item:nth-child(2) .form-input :deep(.n-base-selection-input)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url('/images/teacher/箭头-灰.png');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 多选标签样式调整 */
|
||||
.form-input :deep(.n-tag) {
|
||||
background-color: #F5F8FB !important;
|
||||
@ -536,11 +709,13 @@ const handleSave = () => {
|
||||
|
||||
/* 补交时间选择器样式 */
|
||||
.form-item:has(.n-date-picker[placeholder="请选择补交时间"]) {
|
||||
padding-left: 100px; /* 占用标签的空间位置 */
|
||||
padding-left: 100px;
|
||||
/* 占用标签的空间位置 */
|
||||
}
|
||||
|
||||
.form-item:has(.n-date-picker[placeholder="请选择补交时间"]) .form-input {
|
||||
width: calc(100% - 100px); /* 减去左边距,保持与其他输入框相同宽度 */
|
||||
width: calc(100% - 100px);
|
||||
/* 减去左边距,保持与其他输入框相同宽度 */
|
||||
}
|
||||
|
||||
/* 自定义时间选择器图标 */
|
||||
@ -634,24 +809,24 @@ const handleSave = () => {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
|
||||
.form-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.editor-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
.toolbar-group {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
padding-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.toolbar-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -468,8 +468,7 @@ const columns: DataTableColumns<Chapter> = [
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: (isChapter && row.children && row.children.length > 0) ? 'pointer' : 'default',
|
||||
marginLeft: isChapter ? '0px' : '-3px'
|
||||
cursor: (isChapter && row.children && row.children.length > 0) ? 'pointer' : 'default'
|
||||
},
|
||||
onClick: (isChapter && row.children && row.children.length > 0) ? () => toggleChapter(row) : undefined
|
||||
}, [
|
||||
@ -485,9 +484,9 @@ const columns: DataTableColumns<Chapter> = [
|
||||
}
|
||||
}, {
|
||||
default: () => h(ChevronForwardOutline)
|
||||
}) : (isChapter ? h('span', { style: { marginRight: '22px' } }) : null),
|
||||
}) : (isChapter ? h('span', {}) : null),
|
||||
h('span', {
|
||||
style: { color: '#062333', fontSize: '13px' }
|
||||
style: { color: '#062333', fontSize: '13px', fontWeight: isChapter ? '600' : '400' }
|
||||
}, row.name)
|
||||
])
|
||||
}
|
||||
@ -499,7 +498,7 @@ const columns: DataTableColumns<Chapter> = [
|
||||
render: (row: Chapter) => {
|
||||
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', {
|
||||
style: {
|
||||
@ -509,7 +508,7 @@ const columns: DataTableColumns<Chapter> = [
|
||||
color: '#062333',
|
||||
fontSize: '12px'
|
||||
}
|
||||
}, row.type)
|
||||
}, getTypeText(row.type))
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -603,6 +602,17 @@ const columns: DataTableColumns<Chapter> = [
|
||||
}
|
||||
]
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'0': '视频',
|
||||
'1': '文档',
|
||||
'2': '音频',
|
||||
'3': '测试',
|
||||
'4': '作业'
|
||||
}
|
||||
return typeMap[type] || '-'
|
||||
}
|
||||
|
||||
const fetchCourseChapters = () => {
|
||||
loading.value = true
|
||||
TeachCourseApi.getCourseSections(courseId.value).then(res => {
|
||||
|
@ -9,7 +9,7 @@
|
||||
<!-- <n-button @click="importChapters">导入</n-button>
|
||||
<n-button @click="exportChapters">导出</n-button> -->
|
||||
<!-- <n-button @click="">上传文件</n-button> -->
|
||||
<div class="search-container">
|
||||
<!-- <div class="search-container">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="请输入关键字"
|
||||
@ -20,7 +20,7 @@
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -50,9 +50,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { NInput, NIcon } from 'naive-ui'
|
||||
// import { NInput, NIcon } from 'naive-ui'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { Search } from '@vicons/ionicons5'
|
||||
// import { Search } from '@vicons/ionicons5'
|
||||
// import ImportModal from '@/components/common/ImportModal.vue'
|
||||
// import FileInfoCard from '@/components/admin/FileInfoCard.vue'
|
||||
|
||||
@ -131,9 +131,9 @@ const filteredFolders = computed(() => {
|
||||
// return dateString
|
||||
// }
|
||||
|
||||
const handleSearch = () => {
|
||||
console.log('搜索:', searchKeyword.value)
|
||||
}
|
||||
// const handleSearch = () => {
|
||||
// console.log('搜索:', searchKeyword.value)
|
||||
// }
|
||||
|
||||
const handleFolderClick = (folder: FolderItem) => {
|
||||
console.log('点击文件夹:', folder.name)
|
||||
|
@ -8,7 +8,14 @@
|
||||
添加讨论
|
||||
</n-button>
|
||||
<div class="search-box">
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px;" clearable />
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="请输入关键词"
|
||||
style="width: 200px;"
|
||||
clearable
|
||||
@clear="clearSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -16,6 +23,22 @@
|
||||
|
||||
<!-- 讨论列表 -->
|
||||
<div class="discussion-list">
|
||||
<!-- 搜索结果提示 -->
|
||||
<div v-if="searchKeyword.trim()" class="search-result-tip">
|
||||
<span v-if="sortedDiscussions.length === 0" class="no-result">
|
||||
未找到包含"{{ searchKeyword.trim() }}"的讨论
|
||||
</span>
|
||||
<span v-else class="result-count">
|
||||
找到 {{ sortedDiscussions.length }} 条相关讨论
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="discussions.length === 0" class="empty-state">
|
||||
<p>暂无讨论数据</p>
|
||||
</div>
|
||||
|
||||
<!-- 讨论项列表 -->
|
||||
<div v-for="discussion in sortedDiscussions" :key="discussion.id" class="discussion-item">
|
||||
<!-- 操作按钮 -->
|
||||
<div class="discussion-actions">
|
||||
@ -36,35 +59,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<img :src="discussion.avatar" :alt="discussion.author" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 讨论内容 -->
|
||||
<div class="discussion-content">
|
||||
<div class="author-name">{{ discussion.author }}</div>
|
||||
<div class="topic-header">
|
||||
<h3 class="topic-title" @click="viewComments(discussion)">{{ discussion.title }}</h3>
|
||||
<span v-if="discussion.isPinned" class="pinned-badge">置顶</span>
|
||||
</div>
|
||||
|
||||
<div class="topic-content">{{ discussion.content }}</div>
|
||||
<div class="topic-content" v-html="discussion.content"></div>
|
||||
|
||||
<div class="topic-meta">
|
||||
<span class="chapter-name">{{ discussion.chapterName }}</span>
|
||||
<span class="chapter-name" v-if="discussion.chapterName">章节:{{ discussion.chapterName }}</span>
|
||||
<div class="meta-row">
|
||||
<span class="timestamp">{{ discussion.timestamp }}</span>
|
||||
<div class="interaction-buttons">
|
||||
<n-button quaternary @click="toggleLike(discussion)" :class="{ liked: discussion.isLiked }">
|
||||
<template #icon>
|
||||
<img src="/images/teacher/like.png" alt="点赞" style="width: 13px; height: 13px;" />
|
||||
</template>
|
||||
点赞
|
||||
</n-button>
|
||||
|
||||
<n-button quaternary @click="deleteDiscussion" style="color: #ff4d4f;">
|
||||
<template #icon>
|
||||
<img src="/images/teacher/delete2.png" alt="删除" style="width: 13px; height: 13px;" />
|
||||
@ -104,12 +112,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed, h, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { NButton, NInput, NDropdown, NIcon, NModal, NForm, NFormItem, NSelect, useMessage } from 'naive-ui'
|
||||
import { DiscussionApi } from '@/api/modules/teachCourse'
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 响应式数据
|
||||
const searchKeyword = ref('')
|
||||
@ -122,7 +132,23 @@ const newDiscussion = ref({
|
||||
|
||||
// 计算属性:排序后的讨论列表(置顶的在前)
|
||||
const sortedDiscussions = computed(() => {
|
||||
return [...discussions.value].sort((a, b) => {
|
||||
// 先进行搜索过滤
|
||||
let filteredDiscussions = discussions.value
|
||||
|
||||
if (searchKeyword.value.trim()) {
|
||||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||||
filteredDiscussions = discussions.value.filter((discussion: any) => {
|
||||
return (
|
||||
discussion.title?.toLowerCase().includes(keyword) ||
|
||||
discussion.content?.toLowerCase().includes(keyword) ||
|
||||
discussion.author?.toLowerCase().includes(keyword) ||
|
||||
discussion.chapterName?.toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// 再进行排序(置顶优先)
|
||||
return filteredDiscussions.sort((a, b) => {
|
||||
// 置顶的排在前面
|
||||
if (a.isPinned && !b.isPinned) return -1
|
||||
if (!a.isPinned && b.isPinned) return 1
|
||||
@ -132,56 +158,62 @@ const sortedDiscussions = computed(() => {
|
||||
})
|
||||
|
||||
// 讨论数据
|
||||
const discussions = ref([
|
||||
{
|
||||
id: 1,
|
||||
author: '王建',
|
||||
avatar: '/images/activity/1.png',
|
||||
title: '话题标题',
|
||||
content: '话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容',
|
||||
chapterName: '这是章节名称名称',
|
||||
timestamp: '7月20日 12:41',
|
||||
isPinned: true,
|
||||
isLiked: false,
|
||||
likes: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: '李小明',
|
||||
avatar: '/images/activity/2.png',
|
||||
title: '话题标题',
|
||||
content: '话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容话题讨论讨论的内容',
|
||||
chapterName: '这是章节名称名称',
|
||||
timestamp: '7月20日 12:41',
|
||||
isPinned: false,
|
||||
isLiked: false,
|
||||
likes: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: '张伟',
|
||||
avatar: '/images/activity/3.png',
|
||||
title: '关于课程内容的疑问',
|
||||
content: '老师,关于第三章的内容我有些疑问,能否详细解释一下数组和指针的关系?',
|
||||
chapterName: '第三章:数据结构基础',
|
||||
timestamp: '7月19日 15:30',
|
||||
isPinned: false,
|
||||
isLiked: true,
|
||||
likes: 5
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
author: '刘芳',
|
||||
avatar: '/images/activity/4.png',
|
||||
title: '作业提交问题',
|
||||
content: '请问老师,本周的编程作业什么时候截止?我还有一些问题需要解决。',
|
||||
chapterName: '第二章:编程基础',
|
||||
timestamp: '7月18日 20:15',
|
||||
isPinned: false,
|
||||
isLiked: false,
|
||||
likes: 2
|
||||
const discussions = ref<any[]>([])
|
||||
|
||||
// 获取课程ID
|
||||
const courseId = computed(() => route.params.id as string)
|
||||
|
||||
// 加载讨论列表
|
||||
const loadDiscussionList = async () => {
|
||||
if (!courseId.value) {
|
||||
console.error('缺少课程ID')
|
||||
return
|
||||
}
|
||||
])
|
||||
|
||||
try {
|
||||
const response = await DiscussionApi.getDiscussionList(courseId.value)
|
||||
console.log('💬 讨论列表API响应:', response)
|
||||
|
||||
if (response.data && response.data.result) {
|
||||
// 转换API数据为组件需要的格式
|
||||
discussions.value = response.data.result.map((discussion: any) => ({
|
||||
id: discussion.id || '',
|
||||
author: discussion.authorName || '匿名用户',
|
||||
avatar: '/images/activity/1.png', // 默认头像,可以根据用户信息调整
|
||||
title: discussion.title || '无标题',
|
||||
content: discussion.description || '',
|
||||
chapterName: discussion.sectionName || '',
|
||||
timestamp: discussion.createTime ? formatTime(discussion.createTime) : '',
|
||||
isPinned: discussion.status === 1, // 假设status=1为置顶
|
||||
isLiked: false, // 默认未点赞
|
||||
likes: discussion.replyCount || 0 // 使用回复数作为点赞数
|
||||
}))
|
||||
|
||||
console.log('💬 讨论列表数据:', discussions.value)
|
||||
} else {
|
||||
discussions.value = []
|
||||
console.warn('💬 API响应格式异常:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取讨论列表失败:', error)
|
||||
message.error('获取讨论列表失败,请重试')
|
||||
discussions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间显示
|
||||
const formatTime = (timeStr: string) => {
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}月${day}日 ${hours}:${minutes}`
|
||||
} catch {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
// 章节选项
|
||||
const chapterOptions = [
|
||||
@ -190,9 +222,38 @@ const chapterOptions = [
|
||||
{ label: '第三章:实战项目', value: 'chapter3' }
|
||||
]
|
||||
|
||||
// 在组件挂载时加载讨论列表
|
||||
onMounted(() => {
|
||||
loadDiscussionList()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleSearch = () => {
|
||||
message.info('搜索: ' + searchKeyword.value)
|
||||
const keyword = searchKeyword.value.trim()
|
||||
|
||||
if (!keyword) {
|
||||
message.info('请输入搜索关键词')
|
||||
return
|
||||
}
|
||||
|
||||
// 搜索逻辑已在 sortedDiscussions 计算属性中实现
|
||||
// 这里只需要给用户反馈
|
||||
const filteredCount = sortedDiscussions.value.length
|
||||
const totalCount = discussions.value.length
|
||||
|
||||
if (filteredCount === 0) {
|
||||
message.warning(`未找到包含"${keyword}"的讨论`)
|
||||
} else if (filteredCount === totalCount) {
|
||||
message.success(`显示全部 ${totalCount} 条讨论`)
|
||||
} else {
|
||||
message.success(`找到 ${filteredCount} 条相关讨论`)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除搜索
|
||||
const clearSearch = () => {
|
||||
searchKeyword.value = ''
|
||||
message.info('已清除搜索条件')
|
||||
}
|
||||
|
||||
const getMoreOptions = (discussion: any) => {
|
||||
@ -207,61 +268,24 @@ const getMoreOptions = (discussion: any) => {
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
if (discussion.isPinned) {
|
||||
options.push({
|
||||
label: '取消置顶',
|
||||
key: 'unpin',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/置顶.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
})
|
||||
} else {
|
||||
options.push({
|
||||
label: '置顶',
|
||||
key: 'pin',
|
||||
discussionId: discussion.id,
|
||||
icon: () => h('img', {
|
||||
src: '/images/teacher/置顶.png',
|
||||
style: 'width: 12px; height: 12px;'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
const handleMoreAction = (key: string, option: any) => {
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
message.info('编辑功能')
|
||||
break
|
||||
case 'pin':
|
||||
// 找到对应的讨论项并置顶
|
||||
const discussionToPin = discussions.value.find((d: any) => d.id === option.discussionId)
|
||||
if (discussionToPin) {
|
||||
discussionToPin.isPinned = true
|
||||
message.success('已置顶')
|
||||
}
|
||||
break
|
||||
case 'unpin':
|
||||
// 找到对应的讨论项并取消置顶
|
||||
const discussionToUnpin = discussions.value.find((d: any) => d.id === option.discussionId)
|
||||
if (discussionToUnpin) {
|
||||
discussionToUnpin.isPinned = false
|
||||
message.success('已取消置顶')
|
||||
}
|
||||
// 跳转到编辑页面,传递讨论ID
|
||||
router.push({
|
||||
name: 'AddDiscussion',
|
||||
query: {
|
||||
id: option.discussionId,
|
||||
mode: 'edit'
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const toggleLike = (discussion: any) => {
|
||||
discussion.isLiked = !discussion.isLiked
|
||||
discussion.likes += discussion.isLiked ? 1 : -1
|
||||
}
|
||||
|
||||
const deleteDiscussion = () => {
|
||||
message.success('删除成功')
|
||||
}
|
||||
@ -278,8 +302,24 @@ const addDiscussion = () => {
|
||||
|
||||
const viewComments = (discussion: any) => {
|
||||
router.push({
|
||||
name: 'CommentView',
|
||||
params: { id: discussion.id }
|
||||
name: 'DiscussionRepliesManagement',
|
||||
params: {
|
||||
id: courseId.value,
|
||||
discussionId: discussion.id
|
||||
},
|
||||
state: {
|
||||
discussionData: {
|
||||
id: discussion.id,
|
||||
title: discussion.title,
|
||||
content: discussion.content,
|
||||
author: discussion.author,
|
||||
avatar: discussion.avatar,
|
||||
chapterName: discussion.chapterName,
|
||||
timestamp: discussion.timestamp,
|
||||
isPinned: discussion.isPinned,
|
||||
likes: discussion.likes
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -357,6 +397,32 @@ const goToAddDiscussion = () => {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 搜索结果提示 */
|
||||
.search-result-tip {
|
||||
padding: 12px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-result-tip .no-result {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.search-result-tip .result-count {
|
||||
color: #0C99DA;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.discussion-item {
|
||||
display: flex;
|
||||
padding: 0 20px 20px 20px;
|
||||
|
1179
src/views/teacher/course/DiscussionRepliesManagement.vue
Normal file
1179
src/views/teacher/course/DiscussionRepliesManagement.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,19 +6,26 @@
|
||||
<n-button type="primary" @click="handleAddHomework">添加作业</n-button>
|
||||
<n-button @click="handleImport">导入</n-button>
|
||||
<n-button>导出</n-button>
|
||||
<n-button type="error">删除</n-button>
|
||||
<!-- <n-button type="error">删除</n-button> -->
|
||||
<div class="search-box">
|
||||
<n-input v-model:value="searchValue" placeholder="请输入想要搜索的内容" clearable />
|
||||
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||
<n-input v-model:value="searchValue" placeholder="请输入作业名称搜索" clearable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="table-container">
|
||||
<n-data-table :columns="columns" :data="sortedHomeworkList" :row-key="rowKey"
|
||||
<n-spin :show="loading" description="加载中...">
|
||||
<!-- 搜索结果提示 -->
|
||||
<div v-if="searchValue.trim()" class="search-result-info">
|
||||
<span>搜索 "{{ searchValue }}" 共找到 {{ filteredTotalCount }} 条结果</span>
|
||||
<n-button text @click="searchValue = ''" style="margin-left: 12px; font-size: 12px;">清除搜索</n-button>
|
||||
</div>
|
||||
|
||||
<n-data-table :columns="columns" :data="paginatedHomeworkList" :row-key="rowKey"
|
||||
:checked-row-keys="selectedHomework" @update:checked-row-keys="handleCheck" :bordered="false"
|
||||
:single-line="false" size="medium" class="homework-data-table" scroll-x="true" />
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
@ -60,95 +67,66 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, h, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, h, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { NButton, NDropdown, NDataTable, NInput } from 'naive-ui'
|
||||
import { NButton, NDropdown, NDataTable, NInput, NSpin, useMessage } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { HomeworkApi, TeachCourseApi } from '@/api/modules/teachCourse'
|
||||
|
||||
// 作业类型定义
|
||||
// 作业类型定义 - 更新为与API匹配的类型
|
||||
interface HomeworkItem {
|
||||
id: number
|
||||
name: string
|
||||
chapter: string
|
||||
class: string
|
||||
creator: string
|
||||
createTime: string
|
||||
isTop: boolean
|
||||
id: string
|
||||
title: string | null
|
||||
description: string | null
|
||||
attachment: string | null
|
||||
max_score: number | null
|
||||
pass_score: number | null
|
||||
start_time: string | null
|
||||
end_time: string | null
|
||||
status: number | null
|
||||
allow_makeup: string
|
||||
makeup_time: string
|
||||
notify_time: string
|
||||
classId: string
|
||||
sectionId: string | null
|
||||
courseId: string
|
||||
createTime?: string
|
||||
creator?: string
|
||||
chapter?: string
|
||||
class?: string
|
||||
isTop?: boolean
|
||||
}
|
||||
|
||||
// 路由实例
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
// 选中的作业行
|
||||
const selectedHomework = ref<number[]>([])
|
||||
|
||||
// 搜索相关
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const selectedHomework = ref<string[]>([])
|
||||
const searchValue = ref('')
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalCount = ref(6)
|
||||
|
||||
// 屏幕宽度响应式数据
|
||||
const totalCount = ref(0)
|
||||
const screenWidth = ref(window.innerWidth)
|
||||
|
||||
// 作业列表数据
|
||||
const homeworkList = ref<HomeworkItem[]>([])
|
||||
|
||||
// 获取课程ID
|
||||
const courseId = computed(() => route.params.id as string)
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
screenWidth.value = window.innerWidth
|
||||
}
|
||||
|
||||
// 响应式列配置计算属性
|
||||
// const responsiveColumns = computed(() => {
|
||||
// const width = screenWidth.value
|
||||
|
||||
// // 基础列宽配置
|
||||
// const baseColumns = {
|
||||
// selection: 50,
|
||||
// index: 80,
|
||||
// name: 200,
|
||||
// chapter: 180,
|
||||
// class: 150,
|
||||
// creator: 120,
|
||||
// createTime: 180,
|
||||
// actions: 200
|
||||
// }
|
||||
|
||||
// // 根据屏幕宽度调整列宽
|
||||
// if (width < 1200) {
|
||||
// // 小屏幕:减少各列宽度
|
||||
// return {
|
||||
// selection: 40,
|
||||
// index: 60,
|
||||
// name: Math.max(150, width * 0.2),
|
||||
// chapter: Math.max(120, width * 0.15),
|
||||
// class: Math.max(100, width * 0.12),
|
||||
// creator: 80,
|
||||
// createTime: 140,
|
||||
// actions: Math.max(160, width * 0.2)
|
||||
// }
|
||||
// } else if (width < 1600) {
|
||||
// // 中等屏幕:保持基础宽度
|
||||
// return baseColumns
|
||||
// } else {
|
||||
// // 大屏幕:适当增加宽度
|
||||
// return {
|
||||
// selection: 55,
|
||||
// index: 90,
|
||||
// name: 250,
|
||||
// chapter: 220,
|
||||
// class: 180,
|
||||
// creator: 140,
|
||||
// createTime: 200,
|
||||
// actions: 240
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
// 在组件挂载时添加事件监听器
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
await loadSectionsList() // 加载章节
|
||||
await loadHomeworkList() // 加载作业列表
|
||||
})
|
||||
|
||||
// 在组件卸载时移除事件监听器
|
||||
@ -156,8 +134,73 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value))
|
||||
const courseSectionsList = ref<any>([])
|
||||
|
||||
const loadSectionsList = async () => {
|
||||
const res = await TeachCourseApi.getCourseSections(courseId.value)
|
||||
courseSectionsList.value = res.data.result
|
||||
}
|
||||
|
||||
// 加载作业列表
|
||||
const loadHomeworkList = async () => {
|
||||
if (!courseId.value) {
|
||||
console.error('缺少课程ID')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await HomeworkApi.getTeacherHomeworkList(courseId.value)
|
||||
|
||||
if (response.data.code === 200 && response.data?.result) {
|
||||
// 转换API数据为组件需要的格式
|
||||
homeworkList.value = response.data.result.map((homework: any) => {
|
||||
const mappedHomework = {
|
||||
id: homework.id || '',
|
||||
title: homework.title || '未命名作业',
|
||||
description: homework.description || '',
|
||||
attachment: homework.attachment || null,
|
||||
max_score: homework.maxScore || null,
|
||||
pass_score: homework.passScore || null,
|
||||
start_time: homework.startTime || null,
|
||||
end_time: homework.endTime || null,
|
||||
status: homework.status || null,
|
||||
allow_makeup: homework.allowMakeup?.toString() || '0',
|
||||
makeup_time: homework.makeupTime || '',
|
||||
notify_time: homework.notifyTime || '',
|
||||
classId: homework.classId,
|
||||
sectionId: homework.sectionId,
|
||||
courseId: homework.courseId || '',
|
||||
createTime: homework.createTime || '',
|
||||
creator: homework.createBy || '未知',
|
||||
chapter: '-', // 暂时设置为默认值,如果有章节信息可以后续补充
|
||||
class: homework.classNames,
|
||||
isTop: false // 默认不置顶,如果需要置顶功能需要后端支持
|
||||
} as HomeworkItem
|
||||
return mappedHomework
|
||||
})
|
||||
|
||||
totalCount.value = response.data.result.length
|
||||
} else {
|
||||
console.warn('📋 API响应格式异常:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载作业列表失败:', error)
|
||||
message.error('加载作业列表失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总页数(使用筛选后的数据)
|
||||
const totalPages = computed(() => Math.ceil(filteredTotalCount.value / pageSize.value))
|
||||
|
||||
// 分页后的作业列表
|
||||
const paginatedHomeworkList = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return filteredHomeworkList.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 计算可见页码
|
||||
const visiblePages = computed(() => {
|
||||
@ -203,64 +246,6 @@ const goToPage = (target: number | string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 作业列表数据
|
||||
const homeworkList = ref<HomeworkItem[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '-',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
}
|
||||
])
|
||||
|
||||
// 排序后的作业列表(置顶作业优先显示)
|
||||
const sortedHomeworkList = computed(() => {
|
||||
return homeworkList.value.sort((a: HomeworkItem, b: HomeworkItem) => {
|
||||
@ -270,11 +255,33 @@ const sortedHomeworkList = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 筛选后的作业列表(根据搜索关键词筛选)
|
||||
const filteredHomeworkList = computed(() => {
|
||||
if (!searchValue.value || !searchValue.value.trim()) {
|
||||
return sortedHomeworkList.value
|
||||
}
|
||||
|
||||
const keyword = searchValue.value.trim().toLowerCase()
|
||||
return sortedHomeworkList.value.filter((homework: HomeworkItem) => {
|
||||
// 搜索作业名称字段
|
||||
const title = homework.title || ''
|
||||
return title.toLowerCase().includes(keyword)
|
||||
})
|
||||
})
|
||||
|
||||
// 监听搜索值变化,重置分页
|
||||
watch(searchValue, () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
// 更新总数为筛选后的数量
|
||||
const filteredTotalCount = computed(() => filteredHomeworkList.value.length)
|
||||
|
||||
// 行键
|
||||
const rowKey = (row: HomeworkItem) => row.id
|
||||
|
||||
// 处理复选框选择
|
||||
const handleCheck = (keys: number[]) => {
|
||||
const handleCheck = (keys: string[]) => {
|
||||
selectedHomework.value = keys
|
||||
}
|
||||
|
||||
@ -289,11 +296,11 @@ const columns = computed((): DataTableColumns<HomeworkItem> => [
|
||||
key: 'index',
|
||||
minWidth: 60,
|
||||
width: 60,
|
||||
render: (_, index) => index + 1
|
||||
render: (_, index) => (currentPage.value - 1) * pageSize.value + index + 1
|
||||
},
|
||||
{
|
||||
title: '作业名称',
|
||||
key: 'name',
|
||||
key: 'title',
|
||||
minWidth: 300,
|
||||
width: 300,
|
||||
ellipsis: {
|
||||
@ -301,18 +308,21 @@ const columns = computed((): DataTableColumns<HomeworkItem> => [
|
||||
},
|
||||
render: (row) => {
|
||||
return h('div', { style: 'display: flex; align-items: center; gap: 8px;' }, [
|
||||
h('span', { class: 'homework-name' }, row.name),
|
||||
h('span', { class: 'homework-name' }, row.title || '未命名作业'),
|
||||
row.isTop ? h('span', { class: 'tag tag-pinned' }, '置顶') : null
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '所属章节',
|
||||
key: 'chapter',
|
||||
key: 'sectionId',
|
||||
minWidth: 200,
|
||||
width: 200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render:(row) => {
|
||||
return courseSectionsList.value.find((e: { id: string | null })=>e.id==row.sectionId)?.name
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -322,19 +332,40 @@ const columns = computed((): DataTableColumns<HomeworkItem> => [
|
||||
width: 150,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render: (row: any) => {
|
||||
return row.class.length > 0 ? row.class.join(',') : '暂无班级'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
key: 'creator',
|
||||
minWidth: 100,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
minWidth: 140,
|
||||
width: 140
|
||||
width: 140,
|
||||
render: (row) => {
|
||||
// 格式化时间显示
|
||||
if (row.createTime) {
|
||||
try {
|
||||
// 处理后端返回的时间格式 "2025-09-20 11:32:25"
|
||||
const date = new Date(row.createTime)
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).replace(/\//g, '.')
|
||||
} else {
|
||||
// 如果日期解析失败,直接返回原始字符串
|
||||
return row.createTime
|
||||
}
|
||||
} catch {
|
||||
return row.createTime
|
||||
}
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@ -360,11 +391,6 @@ const columns = computed((): DataTableColumns<HomeworkItem> => [
|
||||
h(NDropdown, {
|
||||
trigger: 'click',
|
||||
options: [
|
||||
{
|
||||
label: '重命名',
|
||||
key: 'rename',
|
||||
icon: () => h('img', { src: '/public/images/teacher/重命名.png', style: 'width: 10px; height: 10px;' })
|
||||
},
|
||||
{
|
||||
label: row.isTop ? '取消置顶' : '置顶',
|
||||
key: 'toggleTop',
|
||||
@ -375,11 +401,11 @@ const columns = computed((): DataTableColumns<HomeworkItem> => [
|
||||
key: 'permissions',
|
||||
icon: () => h('img', { src: '/public/images/teacher/权限设置.png', style: 'width: 10px; height: 10px;' })
|
||||
},
|
||||
{
|
||||
label: '下载',
|
||||
key: 'download',
|
||||
icon: () => h('img', { src: '/public/images/teacher/下载.png', style: 'width: 10px; height: 10px;' })
|
||||
}
|
||||
// {
|
||||
// label: '下载',
|
||||
// key: 'download',
|
||||
// icon: () => h('img', { src: '/public/images/teacher/下载.png', style: 'width: 10px; height: 10px;' })
|
||||
// }
|
||||
],
|
||||
onSelect: (key) => handleAction(key, row)
|
||||
}, {
|
||||
@ -396,21 +422,35 @@ const columns = computed((): DataTableColumns<HomeworkItem> => [
|
||||
])
|
||||
|
||||
// 编辑作业
|
||||
const editHomework = (id: number) => {
|
||||
const editHomework = (id: string) => {
|
||||
console.log('编辑作业:', id)
|
||||
// 跳转到编辑作业页面,传递编辑模式和作业ID
|
||||
router.push({
|
||||
path: `/teacher/course-editor/${route.params.id}/homework/add-homework`,
|
||||
query: {
|
||||
mode: 'edit',
|
||||
id: id.toString()
|
||||
id: id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除作业
|
||||
const deleteHomework = (id: number) => {
|
||||
const deleteHomework = async (id: string) => {
|
||||
console.log('删除作业:', id)
|
||||
try {
|
||||
const response = await HomeworkApi.deleteHomework(id)
|
||||
// 根据实际API响应结构调整判断条件
|
||||
if (response.code === 200 || response.data?.code === 200) {
|
||||
message.success('删除作业成功')
|
||||
// 重新加载列表
|
||||
await loadHomeworkList()
|
||||
} else {
|
||||
message.error(response.message || response.data?.message || '删除作业失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除作业失败:', error)
|
||||
message.error('删除作业失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理更多操作
|
||||
@ -463,11 +503,6 @@ const handleImport = () => {
|
||||
// 跳转到作业库模板导入页面
|
||||
router.push(`/teacher/course-editor/${route.params.id}/homework/template-import`)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
console.log('搜索内容:', searchValue.value)
|
||||
// 这里可以添加搜索逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -523,54 +558,20 @@ const handleSearch = () => {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* Naive UI 表格样式定制 */
|
||||
:deep(.homework-data-table) {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-table) {
|
||||
border: 1px solid #F1F3F4;
|
||||
}
|
||||
|
||||
/* 表格头部样式 */
|
||||
:deep(.homework-data-table .n-data-table-thead) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-data-table-th) {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
color: #062333;
|
||||
.search-result-info {
|
||||
padding: 12px 20px;
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #e1e8ed;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
color: #586069;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* 表格行样式 */
|
||||
:deep(.homework-data-table .n-data-table-td) {
|
||||
font-size: 12px;
|
||||
color: #062333;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 12px 8px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 名称列也居中 */
|
||||
:deep(.homework-data-table .n-data-table-td[data-col-key="name"]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-data-table-th[data-col-key="name"]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-data-table-tr:hover) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 复选框样式 */
|
||||
:deep(.homework-data-table .n-checkbox) {
|
||||
|
@ -272,31 +272,6 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Naive UI Select 组件样式定制 */
|
||||
:deep(.class-select .n-base-selection) {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.class-select .n-base-selection:hover) {
|
||||
border-color: #0288D1;
|
||||
}
|
||||
|
||||
:deep(.class-select .n-base-selection.n-base-selection--focus) {
|
||||
border-color: #0288D1;
|
||||
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.1);
|
||||
}
|
||||
|
||||
:deep(.class-select .n-base-selection-input) {
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
:deep(.class-select .n-base-selection-placeholder) {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 作业列表 */
|
||||
.homework-list {
|
||||
padding: 0;
|
||||
|
Loading…
x
Reference in New Issue
Block a user