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:
zhonghua 2026-03-16 14:54:18 +08:00
parent b6e46ba21e
commit 23eab43590
8 changed files with 123 additions and 58 deletions

View File

@ -232,8 +232,8 @@ export function getSchoolCourseFullDetail(id: number) {
return api.schoolCourseControllerGetFullDetail(id) as any; return api.schoolCourseControllerGetFullDetail(id) as any;
} }
export function getTeacherSchoolCourseFullDetail(id: number) { export function getTeacherSchoolCourseFullDetail(id: number | string) {
return api.teacherSchoolCourseControllerGetFullDetail(id) as any; return api.teacherSchoolCourseControllerGetFullDetail(id as any) as any;
} }
// 更新校本课程包完整数据 // 更新校本课程包完整数据

View File

@ -57,23 +57,26 @@ export function getTeacherCourses(params: TeacherCourseQueryParams): Promise<{
pageSize: number; pageSize: number;
}> { }> {
// 使用 http 直接调用 API后端返回 list 字段,需要转换为 items // 使用 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: { params: {
pageNum: params.pageNum, pageNum: params.pageNum,
pageSize: params.pageSize, pageSize: params.pageSize,
keyword: params.keyword, keyword: params.keyword,
category: params.grade, category: params.grade,
}, },
}).then(res => ({ }).then(res => {
items: res.list || [], const list = res.list ?? res.records ?? [];
total: res.total || 0, return {
page: res.pageNum || 1, items: Array.isArray(list) ? list : [],
pageSize: res.pageSize || 10, total: typeof res.total === 'string' ? parseInt(res.total, 10) || 0 : (res.total || 0),
})); page: res.pageNum || 1,
pageSize: res.pageSize || 10,
};
});
} }
// 获取课程详情 // 获取课程详情id 使用 string 避免 Long 精度丢失)
export function getTeacherCourse(id: number): Promise<any> { export function getTeacherCourse(id: number | string): Promise<any> {
return http.get(`/v1/teacher/courses/${id}`) as any; return http.get(`/v1/teacher/courses/${id}`) as any;
} }
@ -153,9 +156,14 @@ export function getClassTeachers(classId: number): Promise<TeacherClassTeacher[]
// ==================== 授课记录 API ==================== // ==================== 授课记录 API ====================
export interface CreateLessonDto { export interface CreateLessonDto {
courseId: number; courseId: number | string; // string 避免 Long 精度丢失
classId: number; 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 { export interface FinishLessonDto {
@ -195,8 +203,8 @@ export function getLessons(params?: {
}) as any; }) as any;
} }
// 获取单个授课记录详情 // 获取单个授课记录详情id 使用 string 避免 Long 精度丢失)
export function getLesson(id: number): Promise<any> { export function getLesson(id: number | string): Promise<any> {
return http.get(`/v1/teacher/lessons/${id}`) as 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; return http.post('/v1/teacher/lessons', data) as any;
} }
// 开始上课 // 开始上课id 使用 string 避免 Long 精度丢失)
export function startLesson(id: number): Promise<any> { export function startLesson(id: number | string): Promise<any> {
return http.post(`/v1/teacher/lessons/${id}/start`) as any; return http.post(`/v1/teacher/lessons/${id}/start`) as any;
} }
// 结束上课 // 结束上课id 使用 string 避免 Long 精度丢失)
export function finishLesson(id: number, data: FinishLessonDto): Promise<any> { export function finishLesson(id: number | string, data: FinishLessonDto): Promise<any> {
return http.post(`/v1/teacher/lessons/${id}/complete`, data) as any; return http.post(`/v1/teacher/lessons/${id}/complete`, data) as any;
} }
// 取消课程 // 取消课程id 使用 string 避免 Long 精度丢失)
export function cancelLesson(id: number): Promise<any> { export function cancelLesson(id: number | string): Promise<any> {
return http.post(`/v1/teacher/lessons/${id}/cancel`) as any; return http.post(`/v1/teacher/lessons/${id}/cancel`) as any;
} }
// 保存学生评价记录 // 保存学生评价记录lessonId 使用 string 避免 Long 精度丢失)
export function saveStudentRecord( export function saveStudentRecord(
lessonId: number, lessonId: number | string,
studentId: number, studentId: number,
data: StudentRecordDto data: StudentRecordDto
): Promise<any> { ): Promise<any> {
@ -253,13 +261,13 @@ export interface StudentRecordsResponse {
students: StudentWithRecord[]; 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; return http.get(`/v1/teacher/lessons/${lessonId}/students/records`) as any;
} }
// 批量保存学生评价记录 // 批量保存学生评价记录lessonId 使用 string 避免 Long 精度丢失)
export function batchSaveStudentRecords( export function batchSaveStudentRecords(
lessonId: number, lessonId: number | string,
records: Array<{ studentId: number } & StudentRecordDto> records: Array<{ studentId: number } & StudentRecordDto>
): Promise<{ count: number; records: any[] }> { ): Promise<{ count: number; records: any[] }> {
return http.post(`/v1/teacher/lessons/${lessonId}/students/batch-records`, { records }) as any; return http.post(`/v1/teacher/lessons/${lessonId}/students/batch-records`, { records }) as any;
@ -491,13 +499,13 @@ export interface SaveLessonProgressDto {
progressData?: any; progressData?: any;
} }
// 保存课程进度 // 保存课程进度lessonId 使用 string 避免 Long 精度丢失)
export function saveLessonProgress(lessonId: number, data: SaveLessonProgressDto): Promise<LessonProgress> { export function saveLessonProgress(lessonId: number | string, data: SaveLessonProgressDto): Promise<LessonProgress> {
return http.put(`/v1/teacher/lessons/${lessonId}/progress`, data) as any; return http.put(`/v1/teacher/lessons/${lessonId}/progress`, data) as any;
} }
// 获取课程进度 // 获取课程进度lessonId 使用 string 避免 Long 精度丢失)
export function getLessonProgress(lessonId: number): Promise<LessonProgress> { export function getLessonProgress(lessonId: number | string): Promise<LessonProgress> {
return http.get(`/v1/teacher/lessons/${lessonId}/progress`) as any; return http.get(`/v1/teacher/lessons/${lessonId}/progress`) as any;
} }

View File

@ -803,7 +803,7 @@ const loadCourseDetail = async () => {
loading.value = true; loading.value = true;
try { try {
const data = await teacherApi.getTeacherCourse(parseInt(courseId)); const data = await teacherApi.getTeacherCourse(courseId);
course.value = data; course.value = data;
// //

View File

@ -63,11 +63,7 @@
placeholder="搜索课程名称..." placeholder="搜索课程名称..."
style="width: 240px;" style="width: 240px;"
@search="handleFilterChange" @search="handleFilterChange"
> />
<template #prefix>
<SearchOutlined style="color: #FF8C42;" />
</template>
</a-input-search>
</div> </div>
<div class="filter-item filter-right"> <div class="filter-item filter-right">
<a-select <a-select
@ -103,9 +99,9 @@
<div class="placeholder-text">精彩绘本</div> <div class="placeholder-text">精彩绘本</div>
</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-star"><StarFilled /></span>
<span class="rating-value">{{ course.avgRating.toFixed(1) }}</span> <span class="rating-value">{{ (course.avgRating ?? 0).toFixed(1) }}</span>
</div> </div>
</div> </div>
@ -158,7 +154,7 @@
<!-- 空状态 --> <!-- 空状态 -->
<div v-if="courses.length === 0 && !loading" class="empty-state"> <div v-if="courses.length === 0 && !loading" class="empty-state">
<div class="icon-wrapper"> <div class="icon-wrapper">
<SearchOutlined /> <InboxOutlined />
</div> </div>
<p class="empty-text">暂无符合条件的课程</p> <p class="empty-text">暂无符合条件的课程</p>
<p class="empty-hint">试试调整筛选条件吧</p> <p class="empty-hint">试试调整筛选条件吧</p>
@ -185,7 +181,7 @@ import { message } from 'ant-design-vue';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
TeamOutlined, TeamOutlined,
SearchOutlined, InboxOutlined,
BookOutlined, BookOutlined,
BookFilled, BookFilled,
StarOutlined, StarOutlined,
@ -236,6 +232,21 @@ const domainMap: Record<string, string> = {
MATH: '数学', math: '数学', 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 // URL
const getImageUrl = (path: string) => { const getImageUrl = (path: string) => {
if (!path) return ''; if (!path) return '';
@ -281,12 +292,19 @@ const loadCourses = async () => {
} }
const data = await teacherApi.getTeacherCourses(params); const data = await teacherApi.getTeacherCourses(params);
courses.value = (data.items || []).map((item: any) => ({ courses.value = (data.items || []).map((item: any) => {
...item, const gradeTags = parseTags(item.gradeTags);
gradeTags: translateGradeTags(item.gradeTags || []), const domainTags = parseTags(item.domainTags);
domainTags: translateDomainTags(item.domainTags || []), return {
pictureUrl: item.coverImagePath, ...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; pagination.total = data.total || 0;
} catch (error: any) { } catch (error: any) {
message.error(error.message || '获取课程列表失败'); message.error(error.message || '获取课程列表失败');

View File

@ -131,6 +131,7 @@ import { BookOpen } from 'lucide-vue-next';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import * as teacherApi from '@/api/teacher'; import * as teacherApi from '@/api/teacher';
import { useUserStore } from '@/stores/user';
import { getTeacherSchoolCourseFullDetail } from '@/api/school-course'; import { getTeacherSchoolCourseFullDetail } from '@/api/school-course';
import { translateGradeTags } from '@/utils/tagMaps'; import { translateGradeTags } from '@/utils/tagMaps';
import FilePreviewModal from '@/components/FilePreviewModal.vue'; import FilePreviewModal from '@/components/FilePreviewModal.vue';
@ -139,6 +140,7 @@ import PreparePreview from './components/PreparePreview.vue';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const userStore = useUserStore();
const loading = ref(false); const loading = ref(false);
// //
@ -152,7 +154,7 @@ const previewModalVisible = ref(false);
const previewFileUrl = ref(''); const previewFileUrl = ref('');
const previewFileName = ref(''); const previewFileName = ref('');
const courseId = ref(0); const courseId = ref<string>('');
const course = ref<any>({}); const course = ref<any>({});
const lessons = ref<any[]>([]); const lessons = ref<any[]>([]);
const classes = ref<any[]>([]); const classes = ref<any[]>([]);
@ -163,8 +165,23 @@ const scheduleModalVisible = ref(false);
const scheduleLoading = ref(false); const scheduleLoading = ref(false);
const scheduleDate = ref<dayjs.Dayjs | undefined>(undefined); 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(() => { const translatedGradeTags = computed(() => {
return translateGradeTags(course.value.gradeTags || []); return translateGradeTags(parseTags(course.value.gradeTags));
}); });
const totalDuration = computed(() => { const totalDuration = computed(() => {
@ -193,7 +210,7 @@ const getFileUrl = (filePath: string | null | undefined): string => {
}; };
const loadCourseData = async () => { const loadCourseData = async () => {
courseId.value = parseInt(route.params.id as string); courseId.value = (route.params.id as string) || '';
if (!courseId.value) return; if (!courseId.value) return;
loading.value = true; loading.value = true;
@ -203,8 +220,8 @@ const loadCourseData = async () => {
let data: any; let data: any;
if (isSchoolCourse) { if (isSchoolCourse) {
// // id string Long
const res = await getTeacherSchoolCourseFullDetail(courseId.value); const res = await getTeacherSchoolCourseFullDetail(courseId.value as any);
data = res.data || res; data = res.data || res;
// //
@ -259,11 +276,13 @@ const loadCourseData = async () => {
}; };
}); });
} else { } else {
// // id string Long
data = await teacherApi.getTeacherCourse(courseId.value); data = await teacherApi.getTeacherCourse(courseId.value);
course.value = { course.value = {
...data, ...data,
courseLessons: data.courseLessons || [], courseLessons: data.courseLessons || [],
gradeTags: parseTags(data.gradeTags),
domainTags: parseTags(data.domainTags),
}; };
// //
@ -358,10 +377,20 @@ const startTeaching = async () => {
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
try { try {
// const now = dayjs();
const teacherId = userStore.user?.id;
if (!teacherId) {
message.error('未获取到教师信息,请重新登录');
return;
}
// teacherIdtitlelessonDate
const lesson = await teacherApi.createLesson({ const lesson = await teacherApi.createLesson({
courseId: courseId.value, courseId: courseId.value,
classId: selectedClassId.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; scheduleLoading.value = true;
try { try {
const teacherId = userStore.user?.id;
if (!teacherId) {
message.error('未获取到教师信息,请重新登录');
scheduleLoading.value = false;
return;
}
const dt = scheduleDate.value!;
await teacherApi.createLesson({ await teacherApi.createLesson({
courseId: courseId.value, courseId: courseId.value,
classId: selectedClassId.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('预约成功,可在"上课记录"中查看'); message.success('预约成功,可在"上课记录"中查看');

View File

@ -183,7 +183,7 @@ const loadData = async () => {
error.value = ''; error.value = '';
try { try {
const data = await teacherApi.getLesson(parseInt(lessonId)); const data = await teacherApi.getLesson(lessonId);
// //
const parsePathArray = (paths: any) => { const parsePathArray = (paths: any) => {

View File

@ -237,7 +237,7 @@ import {
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const lessonId = computed(() => Number(route.params.id)); const lessonId = computed(() => (route.params.id as string) || '');
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);

View File

@ -473,7 +473,7 @@ const showTimer = ref(false);
const showNotesDrawer = ref(false); const showNotesDrawer = ref(false);
const currentLessonIndex = ref(0); const currentLessonIndex = ref(0);
const currentStepIndex = ref(0); const currentStepIndex = ref(0);
const lessonId = ref(0); const lessonId = ref<string>('');
// //
const previewModalVisible = ref(false); const previewModalVisible = ref(false);
@ -692,7 +692,7 @@ const getLessonStatus = (index: number): string => {
}; };
const loadLessonData = async () => { const loadLessonData = async () => {
lessonId.value = parseInt(route.params.id as string); lessonId.value = (route.params.id as string) || '';
if (!lessonId.value) return; if (!lessonId.value) return;
loading.value = true; loading.value = true;