feat:课程模块下接口对接

This commit is contained in:
yuk255 2025-09-22 14:07:28 +08:00
parent 52b9e9a475
commit 3e1f1fdc67
16 changed files with 5098 additions and 1227 deletions

View File

@ -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
}
}
}

View File

@ -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;
}

View 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) // 93x3
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)
})
// IDprops
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>

View File

@ -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));

View File

@ -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)
}
// IDprops
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;

View File

@ -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>

View File

@ -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',

View File

@ -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 {

View File

@ -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, // uinull
attachment: null // uinull
}
}
@ -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

View File

@ -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 => {

View File

@ -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)

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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) {

View File

@ -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;