fix(teacher): 教师端多项修复
- 课程中心: 修复搜索框重复图标、页面无数据、gradeTags/domainTags 解析 - 备课/上课: 课程/授课 ID 使用 string 避免 Long 精度丢失 - 预约上课: 补充 teacherId/title/lessonDate 等必填字段 - 备课模式: 解析 gradeTags 字符串修复 translateGradeTags 报错 - 涉及: CourseListView, PrepareModeView, LessonView, BroadcastView, LessonRecordsView Made-with: Cursor
This commit is contained in:
parent
b6e46ba21e
commit
23eab43590
@ -232,8 +232,8 @@ export function getSchoolCourseFullDetail(id: number) {
|
||||
return api.schoolCourseControllerGetFullDetail(id) as any;
|
||||
}
|
||||
|
||||
export function getTeacherSchoolCourseFullDetail(id: number) {
|
||||
return api.teacherSchoolCourseControllerGetFullDetail(id) as any;
|
||||
export function getTeacherSchoolCourseFullDetail(id: number | string) {
|
||||
return api.teacherSchoolCourseControllerGetFullDetail(id as any) as any;
|
||||
}
|
||||
|
||||
// 更新校本课程包完整数据
|
||||
|
||||
@ -57,23 +57,26 @@ export function getTeacherCourses(params: TeacherCourseQueryParams): Promise<{
|
||||
pageSize: number;
|
||||
}> {
|
||||
// 使用 http 直接调用 API,后端返回 list 字段,需要转换为 items
|
||||
return http.get<{ list: TeacherCourse[]; total: number; pageNum: number; pageSize: number }>('/v1/teacher/courses', {
|
||||
return http.get<{ list?: TeacherCourse[]; records?: TeacherCourse[]; total?: number | string; pageNum?: number; pageSize?: number }>('/v1/teacher/courses', {
|
||||
params: {
|
||||
pageNum: params.pageNum,
|
||||
pageSize: params.pageSize,
|
||||
keyword: params.keyword,
|
||||
category: params.grade,
|
||||
},
|
||||
}).then(res => ({
|
||||
items: res.list || [],
|
||||
total: res.total || 0,
|
||||
page: res.pageNum || 1,
|
||||
pageSize: res.pageSize || 10,
|
||||
}));
|
||||
}).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 || 1,
|
||||
pageSize: res.pageSize || 10,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 获取课程详情
|
||||
export function getTeacherCourse(id: number): Promise<any> {
|
||||
// 获取课程详情(id 使用 string 避免 Long 精度丢失)
|
||||
export function getTeacherCourse(id: number | string): Promise<any> {
|
||||
return http.get(`/v1/teacher/courses/${id}`) as any;
|
||||
}
|
||||
|
||||
@ -153,9 +156,14 @@ export function getClassTeachers(classId: number): Promise<TeacherClassTeacher[]
|
||||
// ==================== 授课记录 API ====================
|
||||
|
||||
export interface CreateLessonDto {
|
||||
courseId: number;
|
||||
courseId: number | string; // string 避免 Long 精度丢失
|
||||
classId: number;
|
||||
plannedDatetime?: string;
|
||||
teacherId: number;
|
||||
title: string;
|
||||
lessonDate: string; // YYYY-MM-DD
|
||||
startTime?: string; // HH:mm
|
||||
endTime?: string;
|
||||
plannedDatetime?: string; // 兼容:前端可传此字段,由 adapter 转换为 lessonDate+startTime
|
||||
}
|
||||
|
||||
export interface FinishLessonDto {
|
||||
@ -195,8 +203,8 @@ export function getLessons(params?: {
|
||||
}) as any;
|
||||
}
|
||||
|
||||
// 获取单个授课记录详情
|
||||
export function getLesson(id: number): Promise<any> {
|
||||
// 获取单个授课记录详情(id 使用 string 避免 Long 精度丢失)
|
||||
export function getLesson(id: number | string): Promise<any> {
|
||||
return http.get(`/v1/teacher/lessons/${id}`) as any;
|
||||
}
|
||||
|
||||
@ -205,24 +213,24 @@ export function createLesson(data: CreateLessonDto): Promise<any> {
|
||||
return http.post('/v1/teacher/lessons', data) as any;
|
||||
}
|
||||
|
||||
// 开始上课
|
||||
export function startLesson(id: number): Promise<any> {
|
||||
// 开始上课(id 使用 string 避免 Long 精度丢失)
|
||||
export function startLesson(id: number | string): Promise<any> {
|
||||
return http.post(`/v1/teacher/lessons/${id}/start`) as any;
|
||||
}
|
||||
|
||||
// 结束上课
|
||||
export function finishLesson(id: number, data: FinishLessonDto): Promise<any> {
|
||||
// 结束上课(id 使用 string 避免 Long 精度丢失)
|
||||
export function finishLesson(id: number | string, data: FinishLessonDto): Promise<any> {
|
||||
return http.post(`/v1/teacher/lessons/${id}/complete`, data) as any;
|
||||
}
|
||||
|
||||
// 取消课程
|
||||
export function cancelLesson(id: number): Promise<any> {
|
||||
// 取消课程(id 使用 string 避免 Long 精度丢失)
|
||||
export function cancelLesson(id: number | string): Promise<any> {
|
||||
return http.post(`/v1/teacher/lessons/${id}/cancel`) as any;
|
||||
}
|
||||
|
||||
// 保存学生评价记录
|
||||
// 保存学生评价记录(lessonId 使用 string 避免 Long 精度丢失)
|
||||
export function saveStudentRecord(
|
||||
lessonId: number,
|
||||
lessonId: number | string,
|
||||
studentId: number,
|
||||
data: StudentRecordDto
|
||||
): Promise<any> {
|
||||
@ -253,13 +261,13 @@ export interface StudentRecordsResponse {
|
||||
students: StudentWithRecord[];
|
||||
}
|
||||
|
||||
export function getStudentRecords(lessonId: number): Promise<StudentRecordsResponse> {
|
||||
export function getStudentRecords(lessonId: number | string): Promise<StudentRecordsResponse> {
|
||||
return http.get(`/v1/teacher/lessons/${lessonId}/students/records`) as any;
|
||||
}
|
||||
|
||||
// 批量保存学生评价记录
|
||||
// 批量保存学生评价记录(lessonId 使用 string 避免 Long 精度丢失)
|
||||
export function batchSaveStudentRecords(
|
||||
lessonId: number,
|
||||
lessonId: number | string,
|
||||
records: Array<{ studentId: number } & StudentRecordDto>
|
||||
): Promise<{ count: number; records: any[] }> {
|
||||
return http.post(`/v1/teacher/lessons/${lessonId}/students/batch-records`, { records }) as any;
|
||||
@ -491,13 +499,13 @@ export interface SaveLessonProgressDto {
|
||||
progressData?: any;
|
||||
}
|
||||
|
||||
// 保存课程进度
|
||||
export function saveLessonProgress(lessonId: number, data: SaveLessonProgressDto): Promise<LessonProgress> {
|
||||
// 保存课程进度(lessonId 使用 string 避免 Long 精度丢失)
|
||||
export function saveLessonProgress(lessonId: number | string, data: SaveLessonProgressDto): Promise<LessonProgress> {
|
||||
return http.put(`/v1/teacher/lessons/${lessonId}/progress`, data) as any;
|
||||
}
|
||||
|
||||
// 获取课程进度
|
||||
export function getLessonProgress(lessonId: number): Promise<LessonProgress> {
|
||||
// 获取课程进度(lessonId 使用 string 避免 Long 精度丢失)
|
||||
export function getLessonProgress(lessonId: number | string): Promise<LessonProgress> {
|
||||
return http.get(`/v1/teacher/lessons/${lessonId}/progress`) as any;
|
||||
}
|
||||
|
||||
|
||||
@ -803,7 +803,7 @@ const loadCourseDetail = async () => {
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await teacherApi.getTeacherCourse(parseInt(courseId));
|
||||
const data = await teacherApi.getTeacherCourse(courseId);
|
||||
course.value = data;
|
||||
|
||||
// 获取课程列表
|
||||
|
||||
@ -63,11 +63,7 @@
|
||||
placeholder="搜索课程名称..."
|
||||
style="width: 240px;"
|
||||
@search="handleFilterChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: #FF8C42;" />
|
||||
</template>
|
||||
</a-input-search>
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item filter-right">
|
||||
<a-select
|
||||
@ -103,9 +99,9 @@
|
||||
<div class="placeholder-text">精彩绘本</div>
|
||||
</div>
|
||||
<!-- 评分徽章 -->
|
||||
<div class="rating-badge" v-if="course.avgRating > 0">
|
||||
<div class="rating-badge" v-if="(course.avgRating ?? 0) > 0">
|
||||
<span class="rating-star"><StarFilled /></span>
|
||||
<span class="rating-value">{{ course.avgRating.toFixed(1) }}</span>
|
||||
<span class="rating-value">{{ (course.avgRating ?? 0).toFixed(1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -158,7 +154,7 @@
|
||||
<!-- 空状态 -->
|
||||
<div v-if="courses.length === 0 && !loading" class="empty-state">
|
||||
<div class="icon-wrapper">
|
||||
<SearchOutlined />
|
||||
<InboxOutlined />
|
||||
</div>
|
||||
<p class="empty-text">暂无符合条件的课程</p>
|
||||
<p class="empty-hint">试试调整筛选条件吧</p>
|
||||
@ -185,7 +181,7 @@ import { message } from 'ant-design-vue';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
TeamOutlined,
|
||||
SearchOutlined,
|
||||
InboxOutlined,
|
||||
BookOutlined,
|
||||
BookFilled,
|
||||
StarOutlined,
|
||||
@ -236,6 +232,21 @@ const domainMap: Record<string, string> = {
|
||||
MATH: '数学', math: '数学',
|
||||
};
|
||||
|
||||
// 解析标签(后端可能返回 JSON 字符串或数组)
|
||||
const parseTags = (val: any): string[] => {
|
||||
if (!val) return [];
|
||||
if (Array.isArray(val)) return val;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (path: string) => {
|
||||
if (!path) return '';
|
||||
@ -281,12 +292,19 @@ const loadCourses = async () => {
|
||||
}
|
||||
|
||||
const data = await teacherApi.getTeacherCourses(params);
|
||||
courses.value = (data.items || []).map((item: any) => ({
|
||||
...item,
|
||||
gradeTags: translateGradeTags(item.gradeTags || []),
|
||||
domainTags: translateDomainTags(item.domainTags || []),
|
||||
pictureUrl: item.coverImagePath,
|
||||
}));
|
||||
courses.value = (data.items || []).map((item: any) => {
|
||||
const gradeTags = parseTags(item.gradeTags);
|
||||
const domainTags = parseTags(item.domainTags);
|
||||
return {
|
||||
...item,
|
||||
gradeTags: translateGradeTags(gradeTags),
|
||||
domainTags: translateDomainTags(domainTags),
|
||||
duration: item.duration ?? item.durationMinutes ?? 0,
|
||||
usageCount: item.usageCount ?? 0,
|
||||
avgRating: item.avgRating ?? 0,
|
||||
pictureUrl: item.coverImagePath ?? item.pictureUrl,
|
||||
};
|
||||
});
|
||||
pagination.total = data.total || 0;
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取课程列表失败');
|
||||
|
||||
@ -131,6 +131,7 @@ import { BookOpen } from 'lucide-vue-next';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import * as teacherApi from '@/api/teacher';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { getTeacherSchoolCourseFullDetail } from '@/api/school-course';
|
||||
import { translateGradeTags } from '@/utils/tagMaps';
|
||||
import FilePreviewModal from '@/components/FilePreviewModal.vue';
|
||||
@ -139,6 +140,7 @@ import PreparePreview from './components/PreparePreview.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const loading = ref(false);
|
||||
|
||||
// 导航状态
|
||||
@ -152,7 +154,7 @@ const previewModalVisible = ref(false);
|
||||
const previewFileUrl = ref('');
|
||||
const previewFileName = ref('');
|
||||
|
||||
const courseId = ref(0);
|
||||
const courseId = ref<string>('');
|
||||
const course = ref<any>({});
|
||||
const lessons = ref<any[]>([]);
|
||||
const classes = ref<any[]>([]);
|
||||
@ -163,8 +165,23 @@ const scheduleModalVisible = ref(false);
|
||||
const scheduleLoading = ref(false);
|
||||
const scheduleDate = ref<dayjs.Dayjs | undefined>(undefined);
|
||||
|
||||
// 解析标签(后端可能返回 JSON 字符串或数组)
|
||||
const parseTags = (val: any): string[] => {
|
||||
if (!val) return [];
|
||||
if (Array.isArray(val)) return val;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const translatedGradeTags = computed(() => {
|
||||
return translateGradeTags(course.value.gradeTags || []);
|
||||
return translateGradeTags(parseTags(course.value.gradeTags));
|
||||
});
|
||||
|
||||
const totalDuration = computed(() => {
|
||||
@ -193,7 +210,7 @@ const getFileUrl = (filePath: string | null | undefined): string => {
|
||||
};
|
||||
|
||||
const loadCourseData = async () => {
|
||||
courseId.value = parseInt(route.params.id as string);
|
||||
courseId.value = (route.params.id as string) || '';
|
||||
if (!courseId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
@ -203,8 +220,8 @@ const loadCourseData = async () => {
|
||||
|
||||
let data: any;
|
||||
if (isSchoolCourse) {
|
||||
// 加载校本课程包
|
||||
const res = await getTeacherSchoolCourseFullDetail(courseId.value);
|
||||
// 加载校本课程包(id 传 string 避免 Long 精度丢失)
|
||||
const res = await getTeacherSchoolCourseFullDetail(courseId.value as any);
|
||||
data = res.data || res;
|
||||
|
||||
// 转换校本课程包数据结构
|
||||
@ -259,11 +276,13 @@ const loadCourseData = async () => {
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// 加载标准课程包
|
||||
// 加载标准课程包(id 传 string 避免 Long 精度丢失)
|
||||
data = await teacherApi.getTeacherCourse(courseId.value);
|
||||
course.value = {
|
||||
...data,
|
||||
courseLessons: data.courseLessons || [],
|
||||
gradeTags: parseTags(data.gradeTags),
|
||||
domainTags: parseTags(data.domainTags),
|
||||
};
|
||||
|
||||
// 转换课程数据格式以匹配前端组件期望
|
||||
@ -358,10 +377,20 @@ const startTeaching = async () => {
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 创建授课记录
|
||||
const now = dayjs();
|
||||
const teacherId = userStore.user?.id;
|
||||
if (!teacherId) {
|
||||
message.error('未获取到教师信息,请重新登录');
|
||||
return;
|
||||
}
|
||||
// 创建授课记录(后端要求 teacherId、title、lessonDate)
|
||||
const lesson = await teacherApi.createLesson({
|
||||
courseId: courseId.value,
|
||||
classId: selectedClassId.value!,
|
||||
teacherId,
|
||||
title: course.value.name || '授课',
|
||||
lessonDate: now.format('YYYY-MM-DD'),
|
||||
startTime: now.format('HH:mm'),
|
||||
});
|
||||
|
||||
// 开始上课
|
||||
@ -393,10 +422,20 @@ const confirmSchedule = async () => {
|
||||
|
||||
scheduleLoading.value = true;
|
||||
try {
|
||||
const teacherId = userStore.user?.id;
|
||||
if (!teacherId) {
|
||||
message.error('未获取到教师信息,请重新登录');
|
||||
scheduleLoading.value = false;
|
||||
return;
|
||||
}
|
||||
const dt = scheduleDate.value!;
|
||||
await teacherApi.createLesson({
|
||||
courseId: courseId.value,
|
||||
classId: selectedClassId.value!,
|
||||
plannedDatetime: scheduleDate.value.toISOString(),
|
||||
teacherId,
|
||||
title: course.value.name || '授课',
|
||||
lessonDate: dt.format('YYYY-MM-DD'),
|
||||
startTime: dt.format('HH:mm'),
|
||||
});
|
||||
|
||||
message.success('预约成功,可在"上课记录"中查看');
|
||||
|
||||
@ -183,7 +183,7 @@ const loadData = async () => {
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
const data = await teacherApi.getLesson(parseInt(lessonId));
|
||||
const data = await teacherApi.getLesson(lessonId);
|
||||
|
||||
// 解析课程中的路径数组
|
||||
const parsePathArray = (paths: any) => {
|
||||
|
||||
@ -237,7 +237,7 @@ import {
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const lessonId = computed(() => Number(route.params.id));
|
||||
const lessonId = computed(() => (route.params.id as string) || '');
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
@ -473,7 +473,7 @@ const showTimer = ref(false);
|
||||
const showNotesDrawer = ref(false);
|
||||
const currentLessonIndex = ref(0);
|
||||
const currentStepIndex = ref(0);
|
||||
const lessonId = ref(0);
|
||||
const lessonId = ref<string>('');
|
||||
|
||||
// 文件预览相关
|
||||
const previewModalVisible = ref(false);
|
||||
@ -692,7 +692,7 @@ const getLessonStatus = (index: number): string => {
|
||||
};
|
||||
|
||||
const loadLessonData = async () => {
|
||||
lessonId.value = parseInt(route.params.id as string);
|
||||
lessonId.value = (route.params.id as string) || '';
|
||||
if (!lessonId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user