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;
}
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;
}
// 更新校本课程包完整数据

View File

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

View File

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

View File

@ -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 || '获取课程列表失败');

View File

@ -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;
}
// teacherIdtitlelessonDate
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('预约成功,可在"上课记录"中查看');

View File

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

View File

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

View File

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