fix: 教师端课程与授课记录优化
- 修复 assessment_data JSON 字段:普通文本自动包装为有效 JSON - 修复返回时页面 ID 丢失:校验无效 ID 并跳转,goBackToDetail 优先使用路由 ID - 修复上课记录列表:getLessons 支持 pageNum、日期范围、状态映射,list 转 items - 修复班级与课程取值:LessonResponse 增加 courseName/className,接口返回时自动填充 - 备课/详情页增加 ID 校验,防止跳转到 undefined Made-with: Cursor
This commit is contained in:
parent
e8b44b25e0
commit
c8ecbe277c
@ -181,26 +181,52 @@ export interface StudentRecordDto {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
// 后端状态值: scheduled, in_progress, completed, cancelled
|
||||
const STATUS_TO_BACKEND: Record<string, string> = {
|
||||
PLANNED: 'scheduled',
|
||||
IN_PROGRESS: 'in_progress',
|
||||
COMPLETED: 'completed',
|
||||
CANCELLED: 'cancelled',
|
||||
};
|
||||
|
||||
// 获取授课记录列表
|
||||
export function getLessons(params?: {
|
||||
pageNum?: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
courseId?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): Promise<{
|
||||
items: any[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> {
|
||||
return http.get('/v1/teacher/lessons', {
|
||||
params: {
|
||||
pageNum: params?.pageNum,
|
||||
pageSize: params?.pageSize,
|
||||
status: params?.status,
|
||||
startDate: params?.courseId, // 如果需要可以传其他参数
|
||||
},
|
||||
}) as any;
|
||||
const pageNum = params?.pageNum ?? params?.page ?? 1;
|
||||
const status = params?.status ? (STATUS_TO_BACKEND[params.status] || params.status) : undefined;
|
||||
return http
|
||||
.get<{ list?: any[]; records?: any[]; total?: number | string; pageNum?: number; pageSize?: number }>(
|
||||
'/v1/teacher/lessons',
|
||||
{
|
||||
params: {
|
||||
pageNum,
|
||||
pageSize: params?.pageSize ?? 10,
|
||||
status,
|
||||
startDate: params?.startDate,
|
||||
endDate: params?.endDate,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
const list = res?.list ?? res?.records ?? [];
|
||||
return {
|
||||
items: Array.isArray(list) ? list : [],
|
||||
total: typeof res?.total === 'string' ? parseInt(res.total, 10) || 0 : (res?.total ?? 0),
|
||||
page: res?.pageNum ?? pageNum,
|
||||
pageSize: res?.pageSize ?? 10,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 获取单个授课记录详情(id 使用 string 避免 Long 精度丢失)
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<a-button @click="createSchoolVersion">
|
||||
<CopyOutlined /> 创建校本版本
|
||||
</a-button>
|
||||
<a-button type="primary" @click="startPrepare">
|
||||
<a-button type="primary" @click="startPrepare" :disabled="!course.id || loading">
|
||||
<EditOutlined /> 开始备课
|
||||
</a-button>
|
||||
</div>
|
||||
@ -281,7 +281,7 @@
|
||||
<div class="lesson-section-title">教学环节 ({{ lesson.steps.length }}个)</div>
|
||||
<div class="steps-timeline">
|
||||
<div v-for="(step, index) in lesson.steps" :key="step.id || index" class="step-item">
|
||||
<div class="step-dot">{{ index + 1 }}</div>
|
||||
<div class="step-dot">{{ Number(index) + 1 }}</div>
|
||||
<div class="step-content">
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
<div class="step-duration">{{ step.duration }}分钟</div>
|
||||
@ -870,7 +870,12 @@ const createSchoolVersion = () => {
|
||||
};
|
||||
|
||||
const startPrepare = () => {
|
||||
router.push(`/teacher/courses/${course.value.id}/prepare`);
|
||||
const id = route.params.id || course.value?.id;
|
||||
if (!id || id === 'undefined') {
|
||||
message.warning('课程信息未加载完成,请稍后再试');
|
||||
return;
|
||||
}
|
||||
router.push(`/teacher/courses/${id}/prepare`);
|
||||
};
|
||||
|
||||
const toggleFavorite = () => {
|
||||
|
||||
@ -318,7 +318,12 @@ const viewCourseDetail = (course: any) => {
|
||||
};
|
||||
|
||||
const prepareCourse = (course: any) => {
|
||||
router.push(`/teacher/courses/${course.id}/prepare`);
|
||||
const id = course?.id;
|
||||
if (id == null || id === 'undefined') {
|
||||
message.warning('课程信息异常,无法进入备课');
|
||||
return;
|
||||
}
|
||||
router.push(`/teacher/courses/${id}/prepare`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import {
|
||||
LeftOutlined, BookOutlined, ClockCircleOutlined, TagOutlined,
|
||||
@ -210,8 +210,14 @@ const getFileUrl = (filePath: string | null | undefined): string => {
|
||||
};
|
||||
|
||||
const loadCourseData = async () => {
|
||||
courseId.value = (route.params.id as string) || '';
|
||||
if (!courseId.value) return;
|
||||
const id = route.params.id as string;
|
||||
// 校验 ID:无效时跳转回课程列表,避免请求 /courses/undefined
|
||||
if (!id || id === 'undefined' || id === 'null') {
|
||||
message.warning('课程 ID 无效,已返回课程列表');
|
||||
router.replace('/teacher/courses');
|
||||
return;
|
||||
}
|
||||
courseId.value = id;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
@ -358,7 +364,7 @@ const handleSelectStep = (step: any) => {
|
||||
selectedStep.value = step;
|
||||
};
|
||||
|
||||
const handlePreviewResource = (type: string, resource: any) => {
|
||||
const handlePreviewResource = (_type: string, resource: any) => {
|
||||
previewFileUrl.value = resource.url;
|
||||
previewFileName.value = resource.name || '资源文件';
|
||||
previewModalVisible.value = true;
|
||||
@ -452,12 +458,28 @@ const handleExit = () => {
|
||||
};
|
||||
|
||||
const goBackToDetail = () => {
|
||||
router.push(`/teacher/courses/${courseId.value}`);
|
||||
// 优先使用路由中的 ID,避免返回时 courseId 未加载导致跳转到 /courses/undefined
|
||||
const id = route.params.id || courseId.value;
|
||||
if (id && id !== 'undefined' && id !== 'null') {
|
||||
router.push(`/teacher/courses/${id}`);
|
||||
} else {
|
||||
router.push('/teacher/courses');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadCourseData();
|
||||
});
|
||||
|
||||
// 路由变化时重新加载(如从课程 A 的备课页切换到课程 B 的备课页,组件复用时)
|
||||
watch(
|
||||
() => route.params.id,
|
||||
(newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
loadCourseData();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -46,16 +46,16 @@
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-header">
|
||||
<h3 class="course-name">{{ lesson.course?.name || '未知课程' }}</h3>
|
||||
<h3 class="course-name">{{ lesson.courseName || lesson.course?.name || lesson.title || '未知课程' }}</h3>
|
||||
<span class="lesson-time">
|
||||
<ClockCircleOutlined />
|
||||
{{ formatDateTime(lesson.startDatetime || lesson.plannedDatetime) }}
|
||||
{{ formatDateTime(lesson.startDatetime || lesson.plannedDatetime || (lesson.lessonDate && lesson.startTime ? `${lesson.lessonDate}T${lesson.startTime}` : null)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="info-item">
|
||||
<TeamOutlined />
|
||||
<span>{{ lesson.class?.name || '未知班级' }}</span>
|
||||
<span>{{ lesson.className || lesson.class?.name || '未知班级' }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="lesson.actualDuration">
|
||||
<FieldTimeOutlined />
|
||||
@ -108,10 +108,10 @@
|
||||
<div class="detail-content" v-if="selectedLesson">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="课程名称">
|
||||
{{ selectedLesson.course?.name }}
|
||||
{{ selectedLesson.courseName || selectedLesson.course?.name || selectedLesson.title || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="授课班级">
|
||||
{{ selectedLesson.class?.name }}
|
||||
{{ selectedLesson.className || selectedLesson.class?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="课程状态">
|
||||
<a-tag :color="getStatusColor(selectedLesson.status)">
|
||||
@ -152,7 +152,7 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-section">
|
||||
<!-- 已计划状态 -->
|
||||
<template v-if="selectedLesson.status === 'PLANNED'">
|
||||
<template v-if="selectedLesson.status === 'PLANNED' || selectedLesson.status === 'scheduled'">
|
||||
<a-button type="primary" block @click="startPlannedLesson" style="margin-bottom: 12px;">
|
||||
开始上课
|
||||
</a-button>
|
||||
@ -226,12 +226,16 @@ const filters = reactive({
|
||||
const detailDrawerVisible = ref(false);
|
||||
const selectedLesson = ref<any>(null);
|
||||
|
||||
// 状态映射
|
||||
// 状态映射(兼容后端 scheduled/in_progress/completed/cancelled 与前端 PLANNED/IN_PROGRESS 等)
|
||||
const statusMap: Record<string, { text: string; color: string; class: string }> = {
|
||||
PLANNED: { text: '已计划', color: 'blue', class: 'status-planned' },
|
||||
scheduled: { text: '已计划', color: 'blue', class: 'status-planned' },
|
||||
IN_PROGRESS: { text: '进行中', color: 'orange', class: 'status-progress' },
|
||||
in_progress: { text: '进行中', color: 'orange', class: 'status-progress' },
|
||||
COMPLETED: { text: '已完成', color: 'green', class: 'status-completed' },
|
||||
completed: { text: '已完成', color: 'green', class: 'status-completed' },
|
||||
CANCELLED: { text: '已取消', color: 'default', class: 'status-cancelled' },
|
||||
cancelled: { text: '已取消', color: 'default', class: 'status-cancelled' },
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => statusMap[status]?.text || status;
|
||||
@ -247,12 +251,16 @@ const loadLessons = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
page: currentPage.value,
|
||||
pageNum: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
};
|
||||
if (filters.status) {
|
||||
params.status = filters.status;
|
||||
}
|
||||
if (filters.dateRange && filters.dateRange[0] && filters.dateRange[1]) {
|
||||
params.startDate = filters.dateRange[0].format('YYYY-MM-DD');
|
||||
params.endDate = filters.dateRange[1].format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
const data = await teacherApi.getLessons(params);
|
||||
lessons.value = data.items || [];
|
||||
@ -288,8 +296,9 @@ const viewDetail = (lesson: any) => {
|
||||
};
|
||||
|
||||
const goToPrepare = () => {
|
||||
if (selectedLesson.value?.course?.id) {
|
||||
router.push(`/teacher/courses/${selectedLesson.value.course.id}/prepare`);
|
||||
const courseId = selectedLesson.value?.courseId ?? selectedLesson.value?.course?.id;
|
||||
if (courseId) {
|
||||
router.push(`/teacher/courses/${courseId}/prepare`);
|
||||
detailDrawerVisible.value = false;
|
||||
}
|
||||
};
|
||||
@ -309,8 +318,9 @@ const goToRecords = () => {
|
||||
};
|
||||
|
||||
const goToCourseDetail = () => {
|
||||
if (selectedLesson.value?.course?.id) {
|
||||
router.push(`/teacher/courses/${selectedLesson.value.course.id}`);
|
||||
const courseId = selectedLesson.value?.courseId ?? selectedLesson.value?.course?.id;
|
||||
if (courseId) {
|
||||
router.push(`/teacher/courses/${courseId}`);
|
||||
detailDrawerVisible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -12,9 +12,13 @@ import com.reading.platform.dto.request.LessonUpdateRequest;
|
||||
import com.reading.platform.dto.request.StudentRecordRequest;
|
||||
import com.reading.platform.dto.response.LessonResponse;
|
||||
import com.reading.platform.dto.response.StudentRecordResponse;
|
||||
import com.reading.platform.entity.Clazz;
|
||||
import com.reading.platform.entity.Course;
|
||||
import com.reading.platform.entity.Lesson;
|
||||
import com.reading.platform.entity.LessonFeedback;
|
||||
import com.reading.platform.entity.StudentRecord;
|
||||
import com.reading.platform.mapper.ClazzMapper;
|
||||
import com.reading.platform.mapper.CourseMapper;
|
||||
import com.reading.platform.service.LessonService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@ -35,13 +39,17 @@ public class TeacherLessonController {
|
||||
private final LessonService lessonService;
|
||||
private final LessonMapper lessonMapper;
|
||||
private final StudentRecordMapper studentRecordMapper;
|
||||
private final CourseMapper courseMapper;
|
||||
private final ClazzMapper clazzMapper;
|
||||
|
||||
@Operation(summary = "Create lesson")
|
||||
@PostMapping
|
||||
public Result<LessonResponse> createLesson(@Valid @RequestBody LessonCreateRequest request) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
Lesson lesson = lessonService.createLesson(tenantId, request);
|
||||
return Result.success(lessonMapper.toVO(lesson));
|
||||
LessonResponse vo = lessonMapper.toVO(lesson);
|
||||
enrichWithCourseAndClass(vo);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "Update lesson")
|
||||
@ -55,7 +63,9 @@ public class TeacherLessonController {
|
||||
@GetMapping("/{id}")
|
||||
public Result<LessonResponse> getLesson(@PathVariable Long id) {
|
||||
Lesson lesson = lessonService.getLessonById(id);
|
||||
return Result.success(lessonMapper.toVO(lesson));
|
||||
LessonResponse vo = lessonMapper.toVO(lesson);
|
||||
enrichWithCourseAndClass(vo);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "Get my lessons")
|
||||
@ -69,6 +79,7 @@ public class TeacherLessonController {
|
||||
Long teacherId = SecurityUtils.getCurrentUserId();
|
||||
Page<Lesson> page = lessonService.getTeacherLessons(teacherId, pageNum, pageSize, status, startDate, endDate);
|
||||
List<LessonResponse> voList = lessonMapper.toVO(page.getRecords());
|
||||
voList.forEach(this::enrichWithCourseAndClass);
|
||||
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
|
||||
}
|
||||
|
||||
@ -98,7 +109,9 @@ public class TeacherLessonController {
|
||||
public Result<List<LessonResponse>> getTodayLessons() {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
List<Lesson> lessons = lessonService.getTodayLessons(tenantId);
|
||||
return Result.success(lessonMapper.toVO(lessons));
|
||||
List<LessonResponse> voList = lessonMapper.toVO(lessons);
|
||||
voList.forEach(this::enrichWithCourseAndClass);
|
||||
return Result.success(voList);
|
||||
}
|
||||
|
||||
@Operation(summary = "Get student records")
|
||||
@ -159,7 +172,24 @@ public class TeacherLessonController {
|
||||
@GetMapping("/{id}/progress")
|
||||
public Result<LessonResponse> getLessonProgress(@PathVariable Long id) {
|
||||
Lesson lesson = lessonService.getLessonProgress(id);
|
||||
return Result.success(lessonMapper.toVO(lesson));
|
||||
LessonResponse vo = lessonMapper.toVO(lesson);
|
||||
enrichWithCourseAndClass(vo);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 LessonResponse 补充课程名称和班级名称
|
||||
*/
|
||||
private void enrichWithCourseAndClass(LessonResponse vo) {
|
||||
if (vo == null) return;
|
||||
if (vo.getCourseId() != null) {
|
||||
Course course = courseMapper.selectById(vo.getCourseId());
|
||||
vo.setCourseName(course != null ? course.getName() : null);
|
||||
}
|
||||
if (vo.getClassId() != null) {
|
||||
Clazz clazz = clazzMapper.selectById(vo.getClassId());
|
||||
vo.setClassName(clazz != null ? clazz.getName() : null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -29,6 +29,12 @@ public class LessonResponse {
|
||||
@Schema(description = "班级 ID")
|
||||
private Long classId;
|
||||
|
||||
@Schema(description = "课程名称(用于列表展示)")
|
||||
private String courseName;
|
||||
|
||||
@Schema(description = "班级名称(用于列表展示)")
|
||||
private String className;
|
||||
|
||||
@Schema(description = "教师 ID")
|
||||
private Long teacherId;
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.reading.platform.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.reading.platform.common.exception.BusinessException;
|
||||
@ -112,8 +113,8 @@ public class CourseLessonService extends ServiceImpl<CourseLessonMapper, CourseL
|
||||
lesson.setPreparation(preparation);
|
||||
lesson.setExtension(extension);
|
||||
lesson.setReflection(reflection);
|
||||
// 处理空字符串为 null,避免 MySQL JSON 字段错误
|
||||
lesson.setAssessmentData(assessmentData == null || assessmentData.isEmpty() ? null : assessmentData);
|
||||
// 确保 assessment_data 为有效 JSON,MySQL JSON 列不接受纯文本
|
||||
lesson.setAssessmentData(toValidJsonOrNull(assessmentData));
|
||||
lesson.setUseTemplate(useTemplate);
|
||||
lesson.setSortOrder(maxSortOrder + 1);
|
||||
lesson.setCreatedAt(LocalDateTime.now());
|
||||
@ -178,7 +179,7 @@ public class CourseLessonService extends ServiceImpl<CourseLessonMapper, CourseL
|
||||
lesson.setReflection(reflection);
|
||||
}
|
||||
if (assessmentData != null) {
|
||||
lesson.setAssessmentData(assessmentData);
|
||||
lesson.setAssessmentData(toValidJsonOrNull(assessmentData));
|
||||
}
|
||||
if (useTemplate != null) {
|
||||
lesson.setUseTemplate(useTemplate);
|
||||
@ -355,6 +356,31 @@ public class CourseLessonService extends ServiceImpl<CourseLessonMapper, CourseL
|
||||
log.info("教学环节重新排序成功,lessonId={}", lessonId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 assessmentData 转为 MySQL JSON 列可接受的有效 JSON。
|
||||
* 空值返回 null;已是有效 JSON 则原样返回;否则包装为 JSON 字符串。
|
||||
*/
|
||||
private String toValidJsonOrNull(String assessmentData) {
|
||||
if (assessmentData == null || assessmentData.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = assessmentData.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// 已是有效 JSON(对象或数组)则直接使用
|
||||
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
||||
try {
|
||||
JSON.parse(trimmed);
|
||||
return trimmed;
|
||||
} catch (Exception ignored) {
|
||||
// 解析失败,当作普通文本处理
|
||||
}
|
||||
}
|
||||
// 普通文本包装为 JSON 字符串
|
||||
return JSON.toJSONString(assessmentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询教师的课程环节(带权限检查)
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user