diff --git a/reading-platform-frontend/src/views/teacher/classes/ClassListView.vue b/reading-platform-frontend/src/views/teacher/classes/ClassListView.vue index 8f40e12..d13be3c 100644 --- a/reading-platform-frontend/src/views/teacher/classes/ClassListView.vue +++ b/reading-platform-frontend/src/views/teacher/classes/ClassListView.vue @@ -175,7 +175,7 @@ const loadClasses = async () => { })); } catch (error: any) { console.error('Failed to load classes:', error); - message.error(error.response?.data?.message || '加载班级失败'); + message.error(error.message || '加载班级失败'); } finally { loading.value = false; } diff --git a/reading-platform-frontend/src/views/teacher/classes/ClassStudentsView.vue b/reading-platform-frontend/src/views/teacher/classes/ClassStudentsView.vue index 5f2c0bd..5901fe3 100644 --- a/reading-platform-frontend/src/views/teacher/classes/ClassStudentsView.vue +++ b/reading-platform-frontend/src/views/teacher/classes/ClassStudentsView.vue @@ -257,7 +257,7 @@ const loadStudents = async () => { avgScore: Math.round(Math.random() * 40 + 60), // 临时模拟数据 })); } catch (error: any) { - message.error(error.response?.data?.message || '加载失败'); + message.error(error.message || '加载失败'); } finally { loading.value = false; } diff --git a/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue b/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue index c78106a..5aa1b09 100644 --- a/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue @@ -1,372 +1,474 @@ - diff --git a/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue b/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue index c184fe7..67ef425 100644 --- a/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue @@ -289,7 +289,7 @@ const loadCourses = async () => { })); pagination.total = data.total || 0; } catch (error: any) { - message.error(error.response?.data?.message || '获取课程列表失败'); + message.error(error.message || '获取课程列表失败'); } finally { loading.value = false; } diff --git a/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue b/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue index f4585ec..15a632d 100644 --- a/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue @@ -7,34 +7,31 @@ 返回 +
+ 封面 +
-

{{ course.name || '备课模式' }}

+

+ + 备课模式:{{ course.name || '备课模式' }} +

- - {{ course.pictureBookName || '关联绘本' }} + + {{ course.pictureBookName }} + + + {{ course.theme.name }} - {{ course.duration || 25 }} 分钟 + 预计 {{ totalDuration }} 分钟 - + {{ tag }}
-
- 授课班级 - - - {{ cls.name }} - - -
预约上课 @@ -43,251 +40,41 @@ 开始上课 + + + 退出备课 +
- - -
-
-
- -
- 教学流程 - {{ scripts.length }} 个环节 -
- -
-
-
-
{{ index + 1 }}
-
-
-
-
{{ script.stepName }}
-
- - {{ script.duration }}分钟 - -
-
-
- -
-
-
- -
- 总时长 - {{ totalTime }} 分钟 -
-
+ + + - - -
-
-
-
- - {{ currentScript.stepType }} -
-

{{ currentScript.stepName }}

-
-
-
- - {{ currentScript.duration }} 分钟 -
-
-
- -
- -
-
-
- 教学目标 -
-
- {{ currentScript.objective }} -
-
- - -
-
-
- 教师讲稿 -
-
-
- {{ currentScript.teacherScript || '暂无讲稿内容' }} -
-
-
- - -
-
-
- 逐页脚本 - {{ pages.length }} 页 -
-
-
-
- 第{{ page.pageNumber }}页 - -
-
-
-
- 教师话术: - {{ currentPage.questions }} -
-
- 教学备注: - {{ currentPage.teacherNotes }} -
-
- 关联资源: -
- - - - - - - {{ res.name }} - -
-
-
- 该页暂无配置内容 -
-
-
-
-
-
- -
-
- -
-

请从左侧选择教学环节

-

查看详细的教师讲稿和教学目标

-
-
- - - - -
-
-
- -
- 我的备课笔记 - - 保存 - -
-
- -
-
- - -
-
-
- -
- 本环节材料 -
-
-
-
-
- - - - - -
-
- {{ item.name }} - {{ item.type }} -
- - - -
-
-
- - 暂无相关材料 -
-
-
- - -
-
-
- -
- 延伸活动 - {{ activities.length }} 个 -
-
- - - -
-

{{ activity.duration }}分钟

-

- {{ activity.onlineMaterialsText }} -

-
-
-
-
-
+ + +
@@ -337,60 +124,37 @@ import { ref, computed, onMounted } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { - LeftOutlined, - SaveOutlined, - PlayCircleOutlined, - FileOutlined, - BookOutlined, - ClockCircleOutlined, - TeamOutlined, - OrderedListOutlined, - RightOutlined, - ReadOutlined, - AimOutlined, - EditOutlined, - FileTextOutlined, - FormOutlined, - FolderOutlined, - InboxOutlined, - AppstoreOutlined, - SoundOutlined, - FilePdfOutlined, - FilePptOutlined, - PictureOutlined, - EyeOutlined, - CalendarOutlined, + LeftOutlined, BookOutlined, ClockCircleOutlined, TagOutlined, + PlayCircleOutlined, CalendarOutlined, CloseOutlined, } from '@ant-design/icons-vue'; +import { BookOpen } from 'lucide-vue-next'; import { message, Modal } from 'ant-design-vue'; import dayjs from 'dayjs'; import * as teacherApi from '@/api/teacher'; -import { - translateGradeTag, - getGradeTagStyle, - translateStepType, - translateActivityType, - getActivityTypeStyle, -} from '@/utils/tagMaps'; +import { getTeacherSchoolCourseFullDetail } from '@/api/school-course'; +import { translateGradeTags } from '@/utils/tagMaps'; import FilePreviewModal from '@/components/FilePreviewModal.vue'; +import PrepareNavigation from './components/PrepareNavigation.vue'; +import PreparePreview from './components/PreparePreview.vue'; const router = useRouter(); const route = useRoute(); const loading = ref(false); -const selectedStepKeys = ref([]); -const activePageKey = ref('1'); -const myNotes = ref(''); + +// 导航状态 +const selectedSection = ref<'overview' | 'lesson'>('overview'); +const selectedLessonId = ref(null); +const selectedItem = ref('basic'); +const selectedStep = ref(null); // 文件预览相关 const previewModalVisible = ref(false); const previewFileUrl = ref(''); const previewFileName = ref(''); -const courseId = ref(0); +const courseId = ref(0); const course = ref({}); -const scripts = ref([]); -const pages = ref([]); -const stepMaterials = ref([]); -const activities = ref([]); +const lessons = ref([]); const classes = ref([]); const selectedClassId = ref(); @@ -399,57 +163,25 @@ const scheduleModalVisible = ref(false); const scheduleLoading = ref(false); const scheduleDate = ref(undefined); +const translatedGradeTags = computed(() => { + return translateGradeTags(course.value.gradeTags || []); +}); + +const totalDuration = computed(() => { + return lessons.value.reduce((sum: number, l: any) => sum + (l.duration || 0), 0); +}); + +// 当前选中的课程 +const selectedLesson = computed(() => { + if (!selectedLessonId.value) return null; + return lessons.value.find(l => l.id === selectedLessonId.value); +}); + // 获取选中的班级信息 const selectedClassInfo = computed(() => { return classes.value.find(c => c.id === selectedClassId.value); }); -// 计算课程年级标签 -const courseGradeTags = computed(() => { - if (!course.value.gradeTags) return []; - const tags = Array.isArray(course.value.gradeTags) - ? course.value.gradeTags - : []; - return tags.map((tag: string) => translateGradeTag(tag)); -}); - -// 计算总时长 -const totalTime = computed(() => { - return scripts.value.reduce((sum, script) => sum + (script.duration || 0), 0); -}); - -// 当前选中的步骤 -const currentScript = computed(() => { - const id = selectedStepKeys.value[0]; - return scripts.value.find(s => String(s.id) === id); -}); - -// 当前选中的页面 -const currentPage = computed(() => { - return pages.value.find(p => String(p.id) === activePageKey.value); -}); - -// 检查页面是否有内容 -const hasPageContent = (page: any) => { - return (page.questions && page.questions.trim()) || - (page.teacherNotes && page.teacherNotes.trim()) || - (page.resourceIds && page.resourceIds.length > 0); -}; - -// 当前页面的资源列表 - 直接从课程数据中获取 -const currentPageResources = computed(() => { - if (!currentPage.value || !currentPage.value.resourceIds) return []; - const pageResourceIds = currentPage.value.resourceIds; - const resources: any[] = []; - pageResourceIds.forEach((resId: string) => { - const resource = getResourceById(resId); - if (resource) { - resources.push(resource); - } - }); - return resources; -}); - // 获取完整的文件 URL const getFileUrl = (filePath: string | null | undefined): string => { if (!filePath) return ''; @@ -460,69 +192,104 @@ const getFileUrl = (filePath: string | null | undefined): string => { return `${SERVER_BASE}${filePath}`; }; -// 预览资源 -const previewResource = (res: any) => { - if (res.url) { - previewFileUrl.value = getFileUrl(res.url); - previewFileName.value = res.name; - previewModalVisible.value = true; - } -}; - const loadCourseData = async () => { courseId.value = parseInt(route.params.id as string); if (!courseId.value) return; loading.value = true; try { - // 加载课程详情 - const data = await teacherApi.getTeacherCourse(courseId.value); + // 检查是否是校本课程包 + const isSchoolCourse = route.query.type === 'school'; - // 解析课程中的路径数组(可能是JSON字符串) - const parsePathArray = (paths: any) => { - if (!paths) return []; - if (typeof paths === 'string') { - try { - return JSON.parse(paths); - } catch { - return []; - } - } - return Array.isArray(paths) ? paths : []; - }; + let data: any; + if (isSchoolCourse) { + // 加载校本课程包 + const res = await getTeacherSchoolCourseFullDetail(courseId.value); + data = res.data || res; - course.value = { - ...data, - ebookPaths: parsePathArray(data.ebookPaths), - audioPaths: parsePathArray(data.audioPaths), - videoPaths: parsePathArray(data.videoPaths), - posterPaths: parsePathArray(data.posterPaths), - }; + // 转换校本课程包数据结构 + course.value = { + id: data.id, + name: data.name, + pictureBookName: data.sourceCourse?.name || '', + theme: data.themeId ? { id: data.themeId, name: '' } : null, + coverImagePath: data.coverImagePath, + gradeTags: data.gradeTags ? JSON.parse(data.gradeTags) : [], + domainTags: data.domainTags ? JSON.parse(data.domainTags) : [], + duration: data.duration || 25, + coreContent: data.coreContent || '', + introSummary: data.introSummary, + introHighlights: data.introHighlights, + introGoals: data.introGoals, + introSchedule: data.introSchedule, + introKeyPoints: data.introKeyPoints, + introMethods: data.introMethods, + introEvaluation: data.introEvaluation, + introNotes: data.introNotes, + scheduleRefData: data.scheduleRefData, + environmentConstruction: data.environmentConstruction, + }; - scripts.value = (data.scripts || []).map((script: any) => ({ - ...script, - stepType: translateStepType(script.stepType) || script.stepType, - interactionPointsText: Array.isArray(script.interactionPoints) - ? script.interactionPoints.join('、') - : script.interactionPoints, - resourceIds: typeof script.resourceIds === 'string' ? JSON.parse(script.resourceIds) : (script.resourceIds || []), - pages: (script.pages || []).map((page: any) => ({ - ...page, - resourceIds: typeof page.resourceIds === 'string' ? JSON.parse(page.resourceIds) : (page.resourceIds || []), - })), - })); + // 转换课程数据 - 解析 stepsData + lessons.value = (data.lessons || []).map((lesson: any) => { + const stepsData = lesson.stepsData ? JSON.parse(lesson.stepsData) : null; + return { + id: lesson.id, + lessonType: lesson.lessonType, + name: lesson.name, + description: lesson.description, + duration: lesson.duration || 25, + objectives: lesson.objectives, + preparation: lesson.preparation, + extension: lesson.extension, + reflection: lesson.reflection, + videoPath: lesson.videoPath, + videoName: lesson.videoName, + pptPath: lesson.pptPath, + pptName: lesson.pptName, + pdfPath: lesson.pdfPath, + pdfName: lesson.pdfName, + // 从 stepsData 解析 steps 和资源 + steps: stepsData?.steps || [], + images: stepsData?.resources?.images || [], + videos: stepsData?.resources?.videos || [], + audioList: stepsData?.resources?.audioList || [], + pptFiles: stepsData?.resources?.pptFiles || [], + documents: stepsData?.resources?.documents || [], + }; + }); + } else { + // 加载标准课程包 + data = await teacherApi.getTeacherCourse(courseId.value); + course.value = { + ...data, + courseLessons: data.courseLessons || [], + }; - activities.value = (data.activities || []).map((activity: any) => ({ - ...activity, - activityType: activity.activityType || '活动', - domain: activity.domain || '', - onlineMaterialsText: typeof activity.onlineMaterials === 'object' - ? activity.onlineMaterials?.content || '' - : activity.onlineMaterials || '', - objectivesText: Array.isArray(activity.objectives) - ? activity.objectives.join('、') - : activity.objectives || '', - })); + // 转换课程数据格式以匹配前端组件期望 + lessons.value = (data.courseLessons || []).map((lesson: any) => { + // 将资源路径转换为数组格式 + const videos = lesson.videoPath ? [{ path: lesson.videoPath, name: lesson.videoName || '视频' }] : []; + const pptFiles = lesson.pptPath ? [{ path: lesson.pptPath, name: lesson.pptName || '课件' }] : []; + const documents = lesson.pdfPath ? [{ path: lesson.pdfPath, name: lesson.pdfName || '文档' }] : []; + + // 转换steps数据格式:content -> description + const steps = (lesson.steps || []).map((step: any) => ({ + ...step, + description: step.content || step.description || '', + })); + + return { + ...lesson, + steps, + videos, + pptFiles, + documents, + images: [], + audioList: [], + }; + }); + } // 加载教师班级 const classesData = await teacherApi.getTeacherClasses(); @@ -531,171 +298,53 @@ const loadCourseData = async () => { selectedClassId.value = classes.value[0].id; } - // 默认选中第一个步骤 - if (scripts.value.length > 0) { - selectedStepKeys.value = [String(scripts.value[0].id)]; - loadStepData(scripts.value[0]); - } - - // 加载已保存的笔记 - const savedNotes = localStorage.getItem(`notes_${courseId.value}`); - if (savedNotes) { - myNotes.value = savedNotes; + // 如果有URL参数指定课程,自动选中 + const queryLessonId = route.query.lessonId ? parseInt(route.query.lessonId as string) : null; + if (queryLessonId) { + const lesson = lessons.value.find(l => l.id === queryLessonId); + if (lesson) { + handleSelectLesson(lesson); + } + } else { + // 默认选中课程包概览 + selectedSection.value = 'overview'; + selectedItem.value = 'basic'; } } catch (error: any) { - message.error(error.response?.data?.message || '获取课程数据失败'); + message.error(error.message || '获取课程数据失败'); } finally { loading.value = false; } }; -const loadStepData = (script: any) => { - // 加载逐页配置 - pages.value = script.pages || []; - if (pages.value.length > 0) { - activePageKey.value = String(pages.value[0].id); - } else { - activePageKey.value = ''; - } - - // 根据资源ID加载材料 - const resourceIds = script.resourceIds || []; - const materials: any[] = []; - - // 遍历资源ID,构建材料列表 - resourceIds.forEach((resId: string) => { - const material = getResourceById(resId); - if (material) { - materials.push(material); - } - }); - - stepMaterials.value = materials; +const handleSelectSection = (section: 'overview' | 'lesson') => { + selectedSection.value = section; + selectedLessonId.value = null; + selectedItem.value = 'basic'; + selectedStep.value = null; }; -// 根据资源ID获取资源详情 -const getResourceById = (resId: string) => { - if (!resId || !course.value) return null; - - // 解析资源ID格式: type-index (如 "ebook-0", "audio-1", "ppt-0", "poster-2") - const parts = resId.split('-'); - if (parts.length !== 2) return null; - - const type = parts[0]; - const index = parseInt(parts[1]); - - // 资源类型映射 - const typeNames: Record = { - ebook: '电子绘本', - audio: '音频', - video: '视频', - ppt: 'PPT课件', - poster: '教学挂图', - }; - - const typeIcons: Record = { - ebook: 'FilePdfOutlined', - audio: 'SoundOutlined', - video: 'PlayCircleOutlined', - ppt: 'FilePptOutlined', - poster: 'PictureOutlined', - }; - - // 根据类型从课程数据中获取资源 - switch (type) { - case 'ebook': { - const ebooks = course.value.ebookPaths || []; - if (index < ebooks.length) { - return { - id: resId, - name: ebooks[index].name || `电子绘本${index + 1}`, - type: typeNames.ebook, - icon: typeIcons.ebook, - url: ebooks[index].path, - }; - } - break; - } - case 'audio': { - const audios = course.value.audioPaths || []; - if (index < audios.length) { - return { - id: resId, - name: audios[index].name || `音频${index + 1}`, - type: typeNames.audio, - icon: typeIcons.audio, - url: audios[index].path, - }; - } - break; - } - case 'video': { - const videos = course.value.videoPaths || []; - if (index < videos.length) { - return { - id: resId, - name: videos[index].name || `视频${index + 1}`, - type: typeNames.video, - icon: typeIcons.video, - url: videos[index].path, - }; - } - break; - } - case 'ppt': { - if (course.value.pptPath) { - return { - id: resId, - name: course.value.pptName || '教学PPT', - type: typeNames.ppt, - icon: typeIcons.ppt, - url: course.value.pptPath, - }; - } - break; - } - case 'poster': { - const posters = course.value.posterPaths || []; - if (index < posters.length) { - return { - id: resId, - name: posters[index].name || `挂图${index + 1}`, - type: typeNames.poster, - icon: typeIcons.poster, - url: posters[index].path, - }; - } - break; - } - } - - return null; +const handleSelectLesson = (lesson: any) => { + selectedSection.value = 'lesson'; + selectedLessonId.value = lesson.id; + selectedItem.value = 'objectives'; + selectedStep.value = null; }; -const handleStepClick = ({ key }: { key: string | number }) => { - selectedStepKeys.value = [String(key)]; - const script = scripts.value.find(s => String(s.id) === String(key)); - if (script) { - loadStepData(script); - } +const handleSelectItem = (item: string) => { + selectedItem.value = item; }; -// 预览材料 -const previewMaterial = (item: any) => { - if (!item.url) { - message.warning('该材料暂无可预览的文件'); - return; - } - previewFileUrl.value = getFileUrl(item.url); - previewFileName.value = item.name; +const handleSelectStep = (step: any) => { + selectedStep.value = step; +}; + +const handlePreviewResource = (type: string, resource: any) => { + previewFileUrl.value = resource.url; + previewFileName.value = resource.name || '资源文件'; previewModalVisible.value = true; }; -const saveNotes = () => { - localStorage.setItem(`notes_${courseId.value}`, myNotes.value); - message.success('笔记已保存'); -}; - const startTeaching = async () => { if (!selectedClassId.value) { message.warning('请先选择班级'); @@ -721,13 +370,12 @@ const startTeaching = async () => { message.success('正在进入课堂...'); router.push(`/teacher/lessons/${lesson.id}`); } catch (error: any) { - message.error(error.response?.data?.message || '创建授课记录失败'); + message.error(error.message || '创建授课记录失败'); } }, }); }; -// 显示预约上课弹窗 const showScheduleModal = () => { if (!selectedClassId.value) { message.warning('请先选择班级'); @@ -737,7 +385,6 @@ const showScheduleModal = () => { scheduleModalVisible.value = true; }; -// 确认预约上课 const confirmSchedule = async () => { if (!scheduleDate.value) { message.warning('请选择计划上课时间'); @@ -755,13 +402,16 @@ const confirmSchedule = async () => { message.success('预约成功,可在"上课记录"中查看'); scheduleModalVisible.value = false; } catch (error: any) { - message.error(error.response?.data?.message || '预约失败'); + message.error(error.message || '预约失败'); } finally { scheduleLoading.value = false; } }; -// 返回课程详情页 +const handleExit = () => { + goBackToDetail(); +}; + const goBackToDetail = () => { router.push(`/teacher/courses/${courseId.value}`); }; @@ -797,19 +447,24 @@ onMounted(() => { gap: 16px; } -.back-btn { - color: #666; - font-size: 14px; +.course-cover { + width: 60px; + height: 60px; + border-radius: 10px; + overflow: hidden; + flex-shrink: 0; } -.back-btn:hover { - color: #FF8C42; +.course-cover img { + width: 100%; + height: 100%; + object-fit: cover; } .course-info { display: flex; flex-direction: column; - gap: 4px; + gap: 6px; } .course-title { @@ -817,6 +472,14 @@ onMounted(() => { font-weight: 600; color: #333; margin: 0; + display: flex; + align-items: center; + gap: 8px; + + .title-icon { + color: #FF8C42; + flex-shrink: 0; + } } .course-meta { @@ -833,32 +496,28 @@ onMounted(() => { gap: 4px; } +.grade-tag { + background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%); + color: #1976D2; + border: none; + padding: 2px 10px; + font-size: 12px; +} + .header-right { display: flex; align-items: center; - gap: 20px; -} - -.class-selector { - display: flex; - align-items: center; - gap: 8px; -} - -.selector-label { - font-size: 14px; - color: #666; - white-space: nowrap; + gap: 12px; } .schedule-btn { border: 1px solid #FF8C42; color: #FF8C42; - height: 32px; + height: 36px; padding: 0 16px; font-size: 14px; font-weight: 500; - border-radius: 6px; + border-radius: 8px; background: white; } @@ -868,20 +527,14 @@ onMounted(() => { color: #FF6B35; } -.schedule-btn:disabled { - border-color: #d9d9d9; - color: #d9d9d9; - background: white; -} - .start-btn { background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%); border: none; - height: 32px; + height: 36px; padding: 0 24px; font-size: 14px; font-weight: 500; - border-radius: 6px; + border-radius: 8px; box-shadow: 0 4px 12px rgba(255, 140, 66, 0.3); } @@ -891,536 +544,16 @@ onMounted(() => { box-shadow: 0 6px 16px rgba(255, 140, 66, 0.4); } -.start-btn:disabled { - background: #d9d9d9; - box-shadow: none; +.exit-btn { + height: 36px; + padding: 0 16px; + font-size: 14px; + border-radius: 8px; + border: 1px solid #d9d9d9; } /* 内容区域 */ .prepare-content { padding: 20px 24px; } - -/* 卡片通用样式 */ -.card-header { - display: flex; - align-items: center; - gap: 10px; - padding: 14px 16px; - border-bottom: 1px solid #f0f0f0; - font-weight: 600; - font-size: 14px; - color: #333; -} - -.header-icon { - width: 32px; - height: 32px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; -} - -/* 流程导航卡片 */ -.flow-nav-card { - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - overflow: hidden; -} - -.flow-nav-card .header-icon { - background: linear-gradient(135deg, #E8F4FD 0%, #D4E8F9 100%); - color: #1890ff; -} - -.step-count { - margin-left: auto; - font-size: 12px; - color: #999; - font-weight: 400; -} - -.flow-steps { - padding: 8px 0; -} - -.flow-step { - display: flex; - align-items: center; - padding: 12px 16px; - cursor: pointer; - transition: all 0.2s; -} - -.flow-step:hover { - background: #FFF7F0; -} - -.flow-step.active { - background: linear-gradient(90deg, #FFF5EB 0%, #FFF 100%); - border-left: 3px solid #FF8C42; -} - -.step-indicator { - display: flex; - flex-direction: column; - align-items: center; - margin-right: 12px; -} - -.step-number { - width: 28px; - height: 28px; - border-radius: 50%; - background: #f0f0f0; - color: #666; - display: flex; - align-items: center; - justify-content: center; - font-size: 13px; - font-weight: 600; - transition: all 0.2s; -} - -.flow-step.active .step-number { - background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%); - color: white; -} - -.step-line { - width: 2px; - height: 24px; - background: #e8e8e8; - margin-top: 4px; -} - -.step-content { - flex: 1; -} - -.step-name { - font-size: 14px; - color: #333; - font-weight: 500; -} - -.step-meta { - margin-top: 4px; -} - -.step-meta .duration { - font-size: 12px; - color: #999; - display: flex; - align-items: center; - gap: 4px; -} - -.step-arrow { - color: #d9d9d9; - opacity: 0; - transition: all 0.2s; -} - -.flow-step:hover .step-arrow, -.flow-step.active .step-arrow { - opacity: 1; - color: #FF8C42; -} - -.total-time { - display: flex; - justify-content: space-between; - padding: 12px 16px; - background: #FAFAFA; - font-size: 13px; - color: #666; -} - -.total-time strong { - color: #FF8C42; - font-size: 15px; -} - -/* 步骤详情卡片 */ -.step-detail-card { - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - overflow: hidden; - min-height: 500px; -} - -.detail-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - background: linear-gradient(135deg, #FFF5EB 0%, #FFF 100%); - border-bottom: 1px solid #f0f0f0; -} - -.step-badge { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - background: white; - border-radius: 20px; - font-size: 13px; - color: #FF8C42; - border: 1px solid #FFD4B8; -} - -.step-title { - font-size: 18px; - font-weight: 600; - color: #333; - margin: 8px 0 0 0; -} - -.time-display { - display: flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - background: #FFF0E5; - border-radius: 20px; - color: #FF8C42; - font-weight: 500; -} - -.detail-content { - padding: 20px; -} - -.content-section { - margin-bottom: 24px; -} - -.section-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 12px; - font-size: 14px; - font-weight: 600; - color: #333; -} - -.section-icon { - width: 28px; - height: 28px; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; -} - -.objective-icon { - background: #E6F7FF; - color: #1890ff; -} - -.script-icon { - background: #FFF0E5; - color: #FF8C42; -} - -.pages-icon { - background: #F6FFED; - color: #52c41a; -} - -.pages-count { - font-size: 12px; - color: #999; - font-weight: 400; - margin-left: auto; -} - -.section-body { - padding: 16px; - border-radius: 8px; -} - -.objective-body { - background: #F0F7FF; - color: #1890ff; - font-size: 14px; - line-height: 1.6; -} - -.script-body { - background: #FAFAFA; - border: 1px solid #E8E8E8; -} - -.script-content { - white-space: pre-wrap; - line-height: 1.8; - font-size: 14px; - color: #333; -} - -.pages-body { - background: #FAFAFA; - border: 1px solid #E8E8E8; - padding: 0; - overflow: hidden; -} - -.pages-tabs { - display: flex; - border-bottom: 1px solid #E8E8E8; - padding: 0 12px; - background: #F5F5F5; -} - -.page-tab { - padding: 10px 16px; - font-size: 13px; - color: #666; - cursor: pointer; - border-bottom: 2px solid transparent; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 4px; -} - -.page-tab:hover { - color: #FF8C42; -} - -.page-tab.active { - color: #FF8C42; - border-bottom-color: #FF8C42; - background: white; -} - -.page-tab.has-content .content-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: #52c41a; - display: inline-block; -} - -.page-resources { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 4px; -} - -.page-content { - padding: 16px; -} - -.page-item { - margin-bottom: 12px; - font-size: 13px; - line-height: 1.6; -} - -.page-label { - color: #999; - margin-right: 8px; -} - -.page-value { - color: #333; -} - -.page-empty { - text-align: center; - color: #999; - padding: 20px; -} - -/* 空状态 */ -.empty-state { - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - min-height: 400px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - color: #999; -} - -.empty-icon { - font-size: 48px; - color: #D9D9D9; - margin-bottom: 16px; -} - -.empty-state p { - margin: 4px 0; -} - -.empty-hint { - font-size: 13px; - color: #BFBFBF; -} - -/* 笔记卡片 */ -.notes-card { - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - overflow: hidden; - margin-bottom: 16px; -} - -.notes-icon { - background: #FFF0E5; - color: #FF8C42; -} - -.save-btn { - margin-left: auto; - color: #FF8C42; -} - -.notes-body { - padding: 16px; -} - -.notes-textarea { - border: 1px solid #E8E8E8; - border-radius: 8px; - font-size: 13px; - line-height: 1.6; -} - -.notes-textarea:focus { - border-color: #FF8C42; - box-shadow: 0 0 0 2px rgba(255, 140, 66, 0.1); -} - -/* 材料卡片 */ -.materials-card { - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - overflow: hidden; - margin-bottom: 16px; -} - -.materials-icon { - background: #E6F7FF; - color: #1890ff; -} - -.materials-body { - padding: 8px 0; -} - -.materials-list { - padding: 0 8px; -} - -.material-item { - display: flex; - align-items: center; - padding: 10px 12px; - border-radius: 8px; - transition: background 0.2s; -} - -.material-item:hover { - background: #F5F5F5; -} - -.material-icon { - width: 36px; - height: 36px; - background: #E6F7FF; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - color: #1890ff; - margin-right: 12px; -} - -.material-info { - flex: 1; - display: flex; - flex-direction: column; -} - -.material-name { - font-size: 13px; - color: #333; -} - -.material-type { - font-size: 12px; - color: #999; -} - -.empty-materials { - display: flex; - flex-direction: column; - align-items: center; - padding: 24px; - color: #BFBFBF; - gap: 8px; -} - -/* 活动卡片 */ -.activities-card { - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - overflow: hidden; -} - -.activities-icon { - background: #F6FFED; - color: #52c41a; -} - -.activity-count { - margin-left: auto; - font-size: 12px; - color: #999; - font-weight: 400; -} - -.activities-body { - padding: 8px 16px; -} - -.activity-header { - display: flex; - align-items: center; - gap: 8px; -} - -.activity-name { - font-size: 13px; - color: #333; -} - -.activity-detail { - font-size: 12px; - color: #666; -} - -.activity-detail p { - margin: 4px 0; - display: flex; - align-items: center; - gap: 6px; -} diff --git a/reading-platform-frontend/src/views/teacher/courses/components/LessonCard.vue b/reading-platform-frontend/src/views/teacher/courses/components/LessonCard.vue new file mode 100644 index 0000000..43eadb5 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/LessonCard.vue @@ -0,0 +1,329 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/PrepareNavigation.vue b/reading-platform-frontend/src/views/teacher/courses/components/PrepareNavigation.vue new file mode 100644 index 0000000..cbfd0e2 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/PrepareNavigation.vue @@ -0,0 +1,567 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/PreparePreview.vue b/reading-platform-frontend/src/views/teacher/courses/components/PreparePreview.vue new file mode 100644 index 0000000..0dc47b4 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/PreparePreview.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/SelectLessonsModal.vue b/reading-platform-frontend/src/views/teacher/courses/components/SelectLessonsModal.vue new file mode 100644 index 0000000..27b3cfc --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/SelectLessonsModal.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/CourseBasicInfo.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseBasicInfo.vue new file mode 100644 index 0000000..2febc14 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseBasicInfo.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/CourseEnvironmentContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseEnvironmentContent.vue new file mode 100644 index 0000000..339d6a2 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseEnvironmentContent.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/CourseIntroContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseIntroContent.vue new file mode 100644 index 0000000..b4215c9 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseIntroContent.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/CourseScheduleContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseScheduleContent.vue new file mode 100644 index 0000000..1cc7dbf --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/CourseScheduleContent.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/LessonExtensionContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonExtensionContent.vue new file mode 100644 index 0000000..f68388e --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonExtensionContent.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/LessonObjectivesContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonObjectivesContent.vue new file mode 100644 index 0000000..063841b --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonObjectivesContent.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/LessonPreparationContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonPreparationContent.vue new file mode 100644 index 0000000..adbd358 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonPreparationContent.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/LessonReflectionContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonReflectionContent.vue new file mode 100644 index 0000000..7bf07b9 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonReflectionContent.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/LessonResourcesContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonResourcesContent.vue new file mode 100644 index 0000000..7afc623 --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonResourcesContent.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/courses/components/content/LessonStepsContent.vue b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonStepsContent.vue new file mode 100644 index 0000000..f7bcc7c --- /dev/null +++ b/reading-platform-frontend/src/views/teacher/courses/components/content/LessonStepsContent.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/reading-platform-frontend/src/views/teacher/lessons/BroadcastView.vue b/reading-platform-frontend/src/views/teacher/lessons/BroadcastView.vue index 6e75b35..ac85e06 100644 --- a/reading-platform-frontend/src/views/teacher/lessons/BroadcastView.vue +++ b/reading-platform-frontend/src/views/teacher/lessons/BroadcastView.vue @@ -15,9 +15,10 @@ + + diff --git a/reading-platform-frontend/src/views/teacher/lessons/components/viewers/SlidesViewer.vue b/reading-platform-frontend/src/views/teacher/lessons/components/viewers/SlidesViewer.vue index de33b01..80a424f 100644 --- a/reading-platform-frontend/src/views/teacher/lessons/components/viewers/SlidesViewer.vue +++ b/reading-platform-frontend/src/views/teacher/lessons/components/viewers/SlidesViewer.vue @@ -13,33 +13,15 @@ alt="幻灯片" /> - - + - - - -
- -

PDF文件无法在此浏览器中预览

- - - 点击打开PDF - -
-
-
@@ -67,10 +49,10 @@

{{ pages[currentPage] }}

- -
+ +
-

正在加载{{ currentSlideType === 'pdf' ? 'PDF' : currentSlideType === 'ppt' ? 'PPT' : '内容' }}...

+

正在加载{{ currentSlideType === 'ppt' ? 'PPT' : '内容' }}...

@@ -82,8 +64,9 @@
- -
+ + +
@@ -108,8 +91,8 @@ {{ currentPage + 1 }} / {{ pages.length }}
- -
+ +
@@ -130,11 +113,12 @@ import { Download, ExternalLink, } from 'lucide-vue-next'; +import PdfViewer from './PdfViewer.vue'; interface Props { pages: string[]; currentPage: number; - type: 'ppt' | 'poster'; + type: 'ppt' | 'poster' | 'pdf'; } const props = withDefaults(defineProps(), { @@ -145,6 +129,7 @@ const props = withDefaults(defineProps(), { const emit = defineEmits<{ (e: 'pageChange', page: number): void; + (e: 'load', totalPages: number): void; }>(); // 状态 @@ -173,19 +158,6 @@ const currentSlideType = computed(() => { return 'image'; }); -// PDF嵌入URL - 添加参数以优化显示 -const pdfEmbedUrl = computed(() => { - const url = props.pages[currentPage.value] || ''; - if (!url) return ''; - - // 对于某些服务器,可以添加 #toolbar=0 等参数来控制PDF显示 - // 但如果URL已有查询参数,需要用 & 而不是 ? - if (url.includes('?')) { - return `${url}#toolbar=1&navpanes=0&scrollbar=1&view=FitH`; - } - return `${url}#toolbar=1&navpanes=0&scrollbar=1&view=FitH`; -}); - // 当前文件名 const currentFileName = computed(() => { const url = props.pages[currentPage.value] || ''; @@ -205,13 +177,32 @@ const onImageError = () => { showError.value = true; }; -const onPdfLoad = () => { - isLoading.value = false; +const onPdfLoad = (totalPages: number) => { + console.log('[SlidesViewer] onPdfLoad 被调用, 总页数:', totalPages); + // PDF加载完成后,不需要再显示SlidesViewer的loading状态 + // 因为PdfViewer有自己的loading overlay + showError.value = false; + emit('load', totalPages); +}; + +const handlePdfPageChange = (page: number) => { + // 对于单个 PDF 文件,currentPage 应该保持为 0 + // pageChange 事件表示 PdfViewer 内部翻页,不影响 pages 数组的索引 + // 只有当有多个文件时才需要发出 pageChange 事件 + if (props.pages.length > 1) { + currentPage.value = page; + emit('pageChange', page); + } + // 对于单个 PDF 文件,不发出事件,保持 currentPage = 0 }; const prevPage = () => { if (currentPage.value > 0) { - isLoading.value = true; + const isPdf = currentSlideType.value === 'pdf' || currentSlideType.value === 'pdf-fallback'; + // PDF类型不需要设置loading,因为PdfViewer有自己的loading状态 + if (!isPdf) { + isLoading.value = true; + } showError.value = false; currentPage.value--; emit('pageChange', currentPage.value); @@ -220,7 +211,11 @@ const prevPage = () => { const nextPage = () => { if (currentPage.value < props.pages.length - 1) { - isLoading.value = true; + const isPdf = currentSlideType.value === 'pdf' || currentSlideType.value === 'pdf-fallback'; + // PDF类型不需要设置loading,因为PdfViewer有自己的loading状态 + if (!isPdf) { + isLoading.value = true; + } showError.value = false; currentPage.value++; emit('pageChange', currentPage.value); @@ -229,7 +224,11 @@ const nextPage = () => { const goToPage = (index: number) => { if (index !== currentPage.value) { - isLoading.value = true; + const isPdf = currentSlideType.value === 'pdf' || currentSlideType.value === 'pdf-fallback'; + // PDF类型不需要设置loading,因为PdfViewer有自己的loading状态 + if (!isPdf) { + isLoading.value = true; + } showError.value = false; currentPage.value = index; emit('pageChange', currentPage.value); @@ -255,6 +254,12 @@ const handleContainerClick = (e: MouseEvent) => { // 键盘事件 const handleKeydown = (e: KeyboardEvent) => { + // PDF时不允许键盘翻页,因为需要与PDF交互 + const isPdf = currentSlideType.value === 'pdf' || currentSlideType.value === 'pdf-fallback'; + if (isPdf) { + return; // 让PdfViewer处理键盘事件 + } + switch (e.key) { case 'ArrowLeft': prevPage(); @@ -268,25 +273,39 @@ const handleKeydown = (e: KeyboardEvent) => { // 监听外部页码变化 watch(() => props.currentPage, (newPage) => { if (newPage !== currentPage.value) { - isLoading.value = true; + const isPdf = currentSlideType.value === 'pdf' || currentSlideType.value === 'pdf-fallback'; + // PDF类型不需要设置loading,因为PdfViewer有自己的loading状态 + if (!isPdf) { + isLoading.value = true; + } showError.value = false; currentPage.value = newPage; } }); -// 监听pages变化 -watch(() => props.pages, () => { - isLoading.value = true; - showError.value = false; -}, { immediate: true }); +// 监听pages变化 - 只在真正需要时才重新加载 +watch(() => props.pages, (newPages, oldPages) => { + // 只有当pages真正改变时才设置loading + const hasChanged = !oldPages || newPages.length !== oldPages.length || + newPages[0] !== oldPages[0]; + + if (hasChanged && newPages.length > 0) { + console.log('[SlidesViewer] pages 内容改变, 重新加载, 设置 isLoading = true'); + isLoading.value = true; + showError.value = false; + } +}); // 生命周期 onMounted(() => { + console.log('[SlidesViewer] 组件已挂载'); + console.log('[SlidesViewer] onPdfLoad 函数存在:', typeof onPdfLoad === 'function'); document.addEventListener('keydown', handleKeydown); // 设置加载超时 setTimeout(() => { if (isLoading.value) { + console.log('[SlidesViewer] 加载超时, 强制设置 isLoading = false'); isLoading.value = false; } }, 5000); @@ -354,50 +373,6 @@ onUnmounted(() => { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } -.slide-pdf { - width: 100%; - height: 100%; - border: none; - border-radius: 12px; - background: #fff; -} - -.pdf-fallback { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.95); - border-radius: 12px; - color: #333; - padding: 40px; - - p { - margin: 16px 0; - font-size: 16px; - } - - .pdf-download-btn { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 24px; - background: #FF8C42; - color: white; - text-decoration: none; - border-radius: 8px; - font-size: 16px; - transition: all 0.3s; - - &:hover { - background: #E67635; - transform: scale(1.05); - } - } -} - .ppt-container { width: 100%; height: 100%; diff --git a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseDetailView.vue b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseDetailView.vue index 003e446..7c97362 100644 --- a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseDetailView.vue +++ b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseDetailView.vue @@ -7,10 +7,37 @@ + +
+ + 个人课程中心 + + + 校本课程中心 + + + 待审核 + + + 已通过 + + + 已驳回 + +
+ {{ detail?.name }} @@ -26,6 +53,16 @@ {{ detail?.changesSummary || '-' }} + + 课程配置 + +
+ + {{ detail.coreContent || '-' }} + {{ detail.duration || 25 }} 分钟 + +
+ 课程列表 (null); +const detail = ref(null); const lessonColumns = [ { title: '课程类型', dataIndex: 'lessonType', key: 'lessonType', width: 120 }, - { title: '目标', dataIndex: 'objectives', key: 'objectives' }, - { title: '准备', dataIndex: 'preparation', key: 'preparation' }, - { title: '修改备注', dataIndex: 'changeNote', key: 'changeNote' }, + { title: '名称', dataIndex: 'name', key: 'name' }, + { title: '时长', dataIndex: 'duration', key: 'duration', width: 80 }, ]; const lessonTypeNames: Record = { + INTRODUCTION: '导入课', COLLECTIVE: '集体课', HEALTH: '健康', LANGUAGE: '语言', SOCIAL: '社会', SCIENCE: '科学', ART: '艺术', - DOMAIN: '领域课', }; const getLessonTypeName = (type: string) => lessonTypeNames[type] || type; @@ -85,8 +129,8 @@ const fetchData = async () => { loading.value = true; try { const id = Number(route.params.id); - const res = await getTeacherSchoolCourseDetail(id); - detail.value = res.data; + const res = await getTeacherSchoolCourseFullDetail(id) as any; + detail.value = res.data || res; } catch (error) { message.error('获取详情失败'); } finally { @@ -98,6 +142,22 @@ const handleEdit = () => { router.push(`/teacher/school-courses/${route.params.id}/edit`); }; +const handlePrepare = () => { + router.push({ + path: `/teacher/courses/${route.params.id}/prepare`, + query: { type: 'school' }, + }); +}; + +const handleStart = () => { + // 创建授课记录并进入课堂 + message.info('请先进入备课模式,选择班级后开始上课'); + router.push({ + path: `/teacher/courses/${route.params.id}/prepare`, + query: { type: 'school' }, + }); +}; + onMounted(() => { fetchData(); }); @@ -107,4 +167,14 @@ onMounted(() => { .school-course-detail-page { padding: 24px; } + +.status-bar { + margin-bottom: 16px; + display: flex; + gap: 8px; +} + +.course-preview { + margin-bottom: 16px; +} diff --git a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue index 8490241..147c949 100644 --- a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue +++ b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue @@ -1,57 +1,150 @@ @@ -59,104 +152,391 @@ import { ref, reactive, computed, onMounted } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { message } from 'ant-design-vue'; -import { - getTeacherSourceCourses, - getTeacherSchoolCourseDetail, - createTeacherSchoolCourse, - updateTeacherSchoolCourse, -} from '@/api/school-course'; +import { BookOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons-vue'; +import Step1BasicInfo from '@/components/course-edit/Step1BasicInfo.vue'; +import Step2CourseIntro from '@/components/course-edit/Step2CourseIntro.vue'; +import Step3ScheduleRef from '@/components/course-edit/Step3ScheduleRef.vue'; +import Step4IntroLesson from './components/Step4IntroLesson.vue'; +import Step5CollectiveLesson from './components/Step5CollectiveLesson.vue'; +import Step6DomainLessons from './components/Step6DomainLessons.vue'; +import Step7Environment from '@/components/course-edit/Step7Environment.vue'; +import * as schoolCourseApi from '@/api/school-course'; const router = useRouter(); const route = useRoute(); const isEdit = computed(() => !!route.params.id); -const courseId = computed(() => Number(route.params.id)); +const schoolCourseId = computed(() => Number(route.params.id)); +const loading = ref(false); const saving = ref(false); -const sourceCourses = ref([]); +const currentStep = ref(0); +const showSaveModal = ref(false); +const saveLocation = ref<'PERSONAL' | 'SCHOOL'>('PERSONAL'); +const sourceCourse = ref(null); -const form = reactive({ - sourceCourseId: undefined as number | undefined, - name: '', - description: '', - changesSummary: '', +// 步骤组件引用 +const step1Ref = ref(); +const step2Ref = ref(); +const step3Ref = ref(); +const step4Ref = ref(); +const step5Ref = ref(); +const step6Ref = ref(); +const step7Ref = ref(); + +// 表单数据 +const formData = reactive({ + basic: { + name: '', + themeId: undefined as number | undefined, + grades: [] as string[], + pictureBookName: '', + coreContent: '', + duration: 25, + domainTags: [] as string[], + coverImagePath: '', + }, + intro: { + introSummary: '', + introHighlights: '', + introGoals: '', + introSchedule: '', + introKeyPoints: '', + introMethods: '', + introEvaluation: '', + introNotes: '', + }, + scheduleRefData: '', + environmentConstruction: '', + lessons: { + introduction: null as any, + collective: null as any, + domainLessons: { + LANGUAGE: null as any, + HEALTH: null as any, + SCIENCE: null as any, + SOCIAL: null as any, + ART: null as any, + }, + }, }); -const filterOption = (input: string, option: any) => { - return option.children?.[0]?.children?.toLowerCase().includes(input.toLowerCase()); -}; +// 计算完成度 +const completionPercent = computed(() => { + let filled = 0; + let total = 0; -const fetchSourceCourses = async () => { - try { - const res = await getTeacherSourceCourses(); - sourceCourses.value = res.data || []; - } catch (error) { - console.error('获取源课程列表失败', error); - } -}; + // 基本信息必填项 + total += 4; + if (formData.basic.name) filled++; + if (formData.basic.themeId) filled++; + if (formData.basic.grades.length > 0) filled++; + if (formData.basic.coreContent) filled++; + // 课程介绍(8项可选) + total += 8; + if (formData.intro.introSummary) filled++; + if (formData.intro.introHighlights) filled++; + if (formData.intro.introGoals) filled++; + if (formData.intro.introSchedule) filled++; + if (formData.intro.introKeyPoints) filled++; + if (formData.intro.introMethods) filled++; + if (formData.intro.introEvaluation) filled++; + if (formData.intro.introNotes) filled++; + + return Math.round((filled / total) * 100); +}); + +const completionStatus = computed(() => { + if (completionPercent.value >= 75) return 'success'; + if (completionPercent.value >= 50) return 'normal'; + return 'exception'; +}); + +// 获取课程详情 const fetchDetail = async () => { if (!isEdit.value) return; + loading.value = true; try { - const res = await getTeacherSchoolCourseDetail(courseId.value); - const data = res.data; - form.name = data.name; - form.description = data.description || ''; - form.changesSummary = data.changesSummary || ''; - form.sourceCourseId = data.sourceCourseId; - } catch (error) { - message.error('获取详情失败'); + const res = await schoolCourseApi.getTeacherSchoolCourseFullDetail(schoolCourseId.value) as any; + const data = res.data || res; + + // 基本信息 + formData.basic.name = data.name || ''; + formData.basic.themeId = data.themeId; + formData.basic.grades = data.gradeTags ? JSON.parse(data.gradeTags) : []; + formData.basic.pictureBookName = ''; + formData.basic.coreContent = data.coreContent || data.core_content || ''; + formData.basic.duration = data.duration || 25; + formData.basic.domainTags = data.domainTags ? JSON.parse(data.domainTags) : []; + formData.basic.coverImagePath = data.coverImagePath || data.cover_image_path || ''; + + // 课程介绍 + formData.intro.introSummary = data.introSummary || data.intro_summary || ''; + formData.intro.introHighlights = data.introHighlights || data.intro_highlights || ''; + formData.intro.introGoals = data.introGoals || data.intro_goals || ''; + formData.intro.introSchedule = data.introSchedule || data.intro_schedule || ''; + formData.intro.introKeyPoints = data.introKeyPoints || data.intro_key_points || ''; + formData.intro.introMethods = data.introMethods || data.intro_methods || ''; + formData.intro.introEvaluation = data.introEvaluation || data.intro_evaluation || ''; + formData.intro.introNotes = data.introNotes || data.intro_notes || ''; + + // 排课参考和环创建设 + formData.scheduleRefData = data.scheduleRefData || data.schedule_ref_data || ''; + formData.environmentConstruction = data.environmentConstruction || data.environment_construction || ''; + + // 源课程信息 + sourceCourse.value = data.sourceCourse; + + // 课程配置 + if (data.lessons && data.lessons.length > 0) { + for (const lesson of data.lessons) { + const stepsData = lesson.stepsData ? JSON.parse(lesson.stepsData) : null; + + if (lesson.lessonType === 'INTRODUCTION') { + formData.lessons.introduction = { + ...lesson, + stepsData, + }; + } else if (lesson.lessonType === 'COLLECTIVE') { + formData.lessons.collective = { + ...lesson, + stepsData, + }; + } else if (['LANGUAGE', 'HEALTH', 'SCIENCE', 'SOCIAL', 'ART'].includes(lesson.lessonType)) { + formData.lessons.domainLessons[lesson.lessonType as keyof typeof formData.lessons.domainLessons] = { + ...lesson, + stepsData, + }; + } + } + } + } catch (error: any) { + message.error(error.message || '获取详情失败'); + } finally { + loading.value = false; } }; -const handleSourceChange = (value: any) => { - const course = sourceCourses.value.find((c) => c.id === value); - if (course) { - form.name = course.name + ' (校本版)'; - form.description = course.description || ''; +// 从源课程创建 +const createFromSource = async () => { + const sourceCourseId = Number(route.query.sourceCourseId); + if (!sourceCourseId) return; + + loading.value = true; + try { + const res = await schoolCourseApi.createTeacherSchoolCourseFromSource(sourceCourseId, saveLocation.value); + const data = res.data || res; + + message.success('创建成功,请编辑课程内容'); + router.replace(`/teacher/school-courses/${data.id}/edit`); + await fetchDetail(); + } catch (error: any) { + message.error(error.message || '创建失败'); + } finally { + loading.value = false; } }; -const handleSave = async () => { +// 步骤导航 +const onStepChange = (step: number) => { + if (step > currentStep.value) { + if (!validateCurrentStep()) { + return; + } + } + currentStep.value = step; +}; + +const prevStep = () => { + if (currentStep.value > 0) { + currentStep.value--; + } +}; + +const nextStep = () => { + if (!validateCurrentStep()) { + return; + } + if (currentStep.value < 6) { + currentStep.value++; + } +}; + +// 验证当前步骤 +const validateCurrentStep = () => { + if (currentStep.value === 0) { + const result = step1Ref.value?.validate(); + if (!result?.valid) { + message.warning(result?.errors[0] || '请完成基本信息'); + return false; + } + } + return true; +}; + +// 处理数据变化 +const handleDataChange = () => { + // 数据变化时可以自动保存草稿 +}; + +// 保存草稿 +const handleSaveDraft = async () => { + await handleSave(true); +}; + +// 保存 +const handleSave = async (_isDraft = false) => { + if (saving.value) return; + + if (!validateCurrentStep()) { + return; + } + saving.value = true; + try { const data = { - name: form.name, - description: form.description, - changesSummary: form.changesSummary, + name: formData.basic.name, + themeId: formData.basic.themeId, + gradeTags: JSON.stringify(formData.basic.grades), + coreContent: formData.basic.coreContent, + duration: formData.basic.duration, + domainTags: JSON.stringify(formData.basic.domainTags), + coverImagePath: formData.basic.coverImagePath, + + introSummary: formData.intro.introSummary, + introHighlights: formData.intro.introHighlights, + introGoals: formData.intro.introGoals, + introSchedule: formData.intro.introSchedule, + introKeyPoints: formData.intro.introKeyPoints, + introMethods: formData.intro.introMethods, + introEvaluation: formData.intro.introEvaluation, + introNotes: formData.intro.introNotes, + + scheduleRefData: formData.scheduleRefData, + environmentConstruction: formData.environmentConstruction, + + lessons: formData.lessons, }; if (isEdit.value) { - await updateTeacherSchoolCourse(courseId.value, data); + await schoolCourseApi.updateTeacherSchoolCourseFull(schoolCourseId.value, data); + message.success('保存成功'); } else { - if (!form.sourceCourseId) { - message.warning('请选择源课程包'); - return; - } - await createTeacherSchoolCourse({ - sourceCourseId: form.sourceCourseId, - ...data, - }); + await schoolCourseApi.createTeacherSchoolCourseFromSource( + Number(route.query.sourceCourseId), + saveLocation.value, + ); + message.success('创建成功'); } - message.success('保存成功'); - router.push('/teacher/school-courses'); - } catch (error) { - message.error('保存失败'); + showSaveModal.value = false; + router.back(); + } catch (error: any) { + message.error(error.message || '保存失败'); } finally { saving.value = false; } }; -onMounted(() => { - fetchSourceCourses(); - fetchDetail(); +onMounted(async () => { + if (route.query.sourceCourseId) { + await createFromSource(); + } else { + await fetchDetail(); + } }); - diff --git a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseListView.vue b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseListView.vue index 392133f..eaafb04 100644 --- a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseListView.vue +++ b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseListView.vue @@ -5,29 +5,62 @@ 我的校本课程包