kindergarten_java/reading-platform-frontend/src/views/teacher/lessons/BroadcastView.vue
Claude Opus 4.6 4e13f186f3 fix: 统一修改错误处理逻辑
- 将所有 error.response?.data?.message 改为 error.message
- 影响所有教师端组件的错误处理
- 适配新的响应拦截器返回的错误对象结构

修改的文件:
- CourseListView.vue
- CourseDetailView.vue
- PrepareModeView.vue
- LessonListView.vue
- LessonView.vue
- LessonRecordsView.vue
- SchoolCourseEditView.vue
- ClassListView.vue
- ClassStudentsView.vue
- TaskListView.vue

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:33:44 +08:00

369 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="broadcast-view">
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<a-spin size="large" />
<p>正在加载展播内容...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<ExclamationCircleOutlined style="font-size: 48px; color: #ff4d4f;" />
<p>{{ error }}</p>
<a-button type="primary" @click="loadData">重新加载</a-button>
</div>
<!-- 展播内容 -->
<KidsMode
v-else-if="currentLesson && currentSteps.length > 0"
:course="course"
:current-lesson="currentLesson"
:steps="currentSteps"
:activities="activities"
:current-step-index="currentStepIndex"
:timer-seconds="0"
@exit="handleExit"
@step-change="handleStepChange"
/>
<!-- 空状态 -->
<div v-else class="empty-container">
<InboxOutlined style="font-size: 48px; color: #ccc;" />
<p>暂无展播内容</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRoute } from 'vue-router';
import { ExclamationCircleOutlined, InboxOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import KidsMode from './components/KidsMode.vue';
import * as teacherApi from '@/api/teacher';
const route = useRoute();
// 状态
const loading = ref(true);
const error = ref('');
const course = ref<any>(null);
const lessons = ref<any[]>([]);
const activities = ref<any[]>([]);
// 当前课程和环节索引
const currentLessonIndex = ref(0);
const currentStepIndex = ref(0);
// 获取URL参数
const lessonId = route.params.id as string;
const initialLessonIndex = parseInt(route.query.lessonIndex as string) || 0;
const initialStep = parseInt(route.query.stepIndex as string) || parseInt(route.query.step as string) || 0;
// 步骤类型映射
const stepTypeMap: Record<string, string> = {
'READING': '共读',
'DISCUSSION': '讨论',
'ACTIVITY': '活动',
'CREATION': '创作',
'SUMMARY': '总结',
'CUSTOM': '导入',
'WARMUP': '热身',
'INTRODUCTION': '导入',
'DEVELOPMENT': '发展',
'PRACTICE': '练习',
'EXTENSION': '延伸',
'CONCLUSION': '总结',
'ASSESSMENT': '评估',
};
// 当前课程
const currentLesson = computed(() => {
if (lessons.value.length === 0) return null;
return lessons.value[currentLessonIndex.value] || null;
});
// 当前课程的环节列表(转换为旧结构)
const currentSteps = computed(() => {
const lesson = currentLesson.value;
if (!lesson) return [];
// 新结构:使用 steps
if (lesson.steps && lesson.steps.length > 0) {
return lesson.steps.map((step: any) => {
// 转换资源数据:从后端的 resources 数组转换为前端的分类数组
const images: any[] = [];
const videos: any[] = [];
const audioList: any[] = [];
const pptFiles: any[] = [];
const documents: any[] = [];
const resourceIds: string[] = [];
if (step.resources && Array.isArray(step.resources)) {
step.resources.forEach((res: any) => {
const fileId = `${res.resourceType?.toLowerCase()}-${Date.now()}-${Math.random()}`;
switch (res.resourceType) {
case 'IMAGE':
case 'image':
images.push({ id: fileId, name: res.resourceName, path: res.fileUrl });
break;
case 'VIDEO':
case 'video':
videos.push({ id: fileId, name: res.resourceName, path: res.fileUrl });
break;
case 'AUDIO':
case 'audio':
audioList.push({ id: fileId, name: res.resourceName, path: res.fileUrl });
break;
case 'PPT':
case 'ppt':
pptFiles.push({ id: fileId, name: res.resourceName, path: res.fileUrl });
break;
case 'DOCUMENT':
case 'document':
case 'PDF':
documents.push({ id: fileId, name: res.resourceName, path: res.fileUrl });
break;
default:
// 对于未知类型,也作为文档处理
documents.push({ id: fileId, name: res.resourceName, path: res.fileUrl });
}
// 生成资源ID用于引用兼容旧的 resourceIds 方式)
resourceIds.push(`${res.resourceType?.toLowerCase() || 'unknown'}-${res.id}`);
});
}
return {
id: step.id,
stepName: step.name,
name: step.name,
stepType: step.stepType,
duration: step.duration || 5,
objective: step.objective,
description: step.description,
teacherScript: step.script,
script: step.script,
interactionPoints: [],
interactionPointsText: '',
resourceIds,
pages: [],
// 新资源结构(分类数组)
images,
videos,
audioList,
pptFiles,
documents,
// 保留原始 resources 数据
resources: step.resources || [],
};
});
}
// 兼容旧结构:使用 scripts
if (lesson.scripts && lesson.scripts.length > 0) {
return lesson.scripts.map((script: any) => ({
...script,
stepType: stepTypeMap[script.stepType] || script.stepType,
images: [],
videos: [],
audioList: [],
pptFiles: [],
documents: [],
}));
}
return [];
});
// 加载数据
const loadData = async () => {
loading.value = true;
error.value = '';
try {
const data = await teacherApi.getLesson(parseInt(lessonId));
// 解析课程中的路径数组
const parsePathArray = (paths: any) => {
if (!paths) return [];
if (typeof paths === 'string') {
try {
return JSON.parse(paths);
} catch {
return [];
}
}
return Array.isArray(paths) ? paths : [];
};
course.value = {
...data.course,
ebookPaths: parsePathArray(data.course?.ebookPaths),
audioPaths: parsePathArray(data.course?.audioPaths),
videoPaths: parsePathArray(data.course?.videoPaths),
posterPaths: parsePathArray(data.course?.posterPaths),
};
// 获取课程列表
// 后端返回的是 data.course.courseLessons
if (course.value.courseLessons && course.value.courseLessons.length > 0) {
// 使用课程包的所有课程
lessons.value = course.value.courseLessons;
} else {
// 兼容旧结构:单个课程,使用 scripts
lessons.value = [{
id: course.value.id,
name: course.value.name,
lessonType: 'CUSTOM',
duration: course.value.duration || 30,
steps: [],
scripts: (course.value.scripts || []).map((script: any) => ({
...script,
stepType: stepTypeMap[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 || []),
})),
})),
}];
}
// 设置初始课程索引
currentLessonIndex.value = Math.min(initialLessonIndex, Math.max(0, lessons.value.length - 1));
// 设置初始环节索引
const totalSteps = currentSteps.value.length;
currentStepIndex.value = Math.min(initialStep, Math.max(0, totalSteps - 1));
// 解析活动数据
if (data.course?.lessonPlanData) {
try {
const lessonPlan = typeof data.course.lessonPlanData === 'string'
? JSON.parse(data.course.lessonPlanData)
: data.course.lessonPlanData;
if (lessonPlan.activities && Array.isArray(lessonPlan.activities)) {
activities.value = lessonPlan.activities.map((activity: any, index: number) => ({
id: `activity-${index}`,
name: activity.name,
activityType: activity.type || 'OTHER',
duration: activity.duration,
objectives: activity.objectives,
activityGuide: activity.guide,
resourceUrl: activity.resourceUrl,
resourceType: activity.resourceType,
}));
}
} catch (parseErr) {
console.error('Failed to parse lesson plan:', parseErr);
}
}
} catch (err: any) {
console.error('Failed to load broadcast data:', err);
error.value = err.response?.data?.message || '加载失败,请重试';
} finally {
loading.value = false;
}
};
// 处理退出
const handleExit = () => {
message.info('展播模式已关闭,您可以关闭此标签页');
};
// 处理环节切换
const handleStepChange = (index: number) => {
currentStepIndex.value = index;
// 更新URL参数不刷新页面
const newUrl = `${window.location.pathname}?lessonIndex=${currentLessonIndex.value}&stepIndex=${index}`;
window.history.replaceState({}, '', newUrl);
};
// 键盘快捷键
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case 'Escape':
message.info('按 ESC 退出全屏,关闭标签页请使用 Ctrl+W');
break;
case 'F11':
e.preventDefault();
toggleFullscreen();
break;
case 'ArrowLeft':
// 上一环节
if (currentStepIndex.value > 0) {
handleStepChange(currentStepIndex.value - 1);
}
break;
case 'ArrowRight':
// 下一环节
if (currentStepIndex.value < currentSteps.value.length - 1) {
handleStepChange(currentStepIndex.value + 1);
}
break;
}
};
// 全屏切换
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
};
// 生命周期
onMounted(() => {
loadData();
document.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
// 退出全屏
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
}
});
</script>
<style scoped lang="scss">
.broadcast-view {
width: 100vw;
height: 100vh;
overflow: hidden;
background: #1a1a2e;
}
.loading-container,
.error-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
color: #fff;
gap: 20px;
p {
font-size: 18px;
opacity: 0.8;
}
}
.error-container {
p {
color: #ff4d4f;
}
}
</style>