kindergarten_java/reading-platform-frontend/src/views/teacher/lessons/LessonView.vue

1463 lines
35 KiB
Vue
Raw Normal View History

<template>
<div class="lesson-view">
<!-- 顶部工具栏 -->
<div class="lesson-toolbar">
<div class="toolbar-left">
<a-button class="exit-btn" @click="exitLesson">
<template #icon><ArrowLeftOutlined /></template>
退出上课
</a-button>
<div class="course-info" v-if="course.name">
<BookOutlined class="course-icon" />
<span class="course-name">{{ course.name }}</span>
<a-tag color="orange" v-if="classInfo.name">{{ classInfo.name }}</a-tag>
</div>
</div>
<!-- 中间计时器 -->
<div class="toolbar-center">
<div class="timer-display" @click="showTimer = true">
<ClockCircleOutlined class="timer-icon" />
<span class="timer-value">{{ formatTime(timerSeconds) }}</span>
</div>
</div>
<div class="toolbar-right">
<a-button-group class="nav-buttons">
<a-button @click="previousStep" :disabled="currentStepIndex === 0">
<StepBackwardOutlined /> 上一步
</a-button>
<a-button type="primary" class="next-btn" @click="nextStep" :disabled="currentStepIndex >= scripts.length - 1">
下一步 <StepForwardOutlined />
</a-button>
</a-button-group>
<!-- 展播模式按钮 - 在新标签页打开 -->
<a-button type="primary" class="broadcast-btn" @click="openBroadcastMode">
<template #icon><ExpandOutlined /></template>
展播模式
</a-button>
<a-button class="toolbar-icon-btn" @click="showNotesDrawer = true">
<EditOutlined />
</a-button>
<a-button type="primary" danger class="finish-btn" @click="finishLesson">
<CheckOutlined /> 结束课程
</a-button>
</div>
</div>
<!-- 主内容区 -->
<div class="lesson-content">
<!-- 左侧步骤导航 -->
<div class="step-nav">
<div class="nav-header">
<ReadOutlined />
<span>教学流程</span>
<span class="progress-text">{{ currentStepIndex + 1 }}/{{ scripts.length }}</span>
</div>
<div class="step-list">
<div
v-for="(script, index) in scripts"
:key="script.id"
class="step-item"
:class="{
active: selectedStepKeys.includes(String(script.id)),
completed: index < currentStepIndex
}"
@click="handleStepClick({ key: script.id })"
>
<div class="step-number">
<CheckOutlined v-if="index < currentStepIndex" />
<span v-else>{{ index + 1 }}</span>
</div>
<div class="step-info">
<span class="step-name">{{ script.stepName }}</span>
<span class="step-duration">{{ script.duration }} 分钟</span>
</div>
</div>
</div>
<!-- 进度条 -->
<div class="progress-bar">
<div class="progress-fill" :style="{ width: ((currentStepIndex + 1) / scripts.length * 100) + '%' }"></div>
</div>
</div>
<!-- 中间展示区域 -->
<div class="display-area">
<div v-if="currentScript" class="script-display">
<!-- 环节标题 -->
<div class="script-header">
<div class="header-left">
<div class="step-badge"> {{ currentStepIndex + 1 }} 环节</div>
<h2 class="step-title">{{ currentScript.stepName }}</h2>
</div>
<div class="header-tags">
<a-tag class="duration-tag">
<ClockCircleOutlined /> {{ currentScript.duration }}分钟
</a-tag>
</div>
</div>
<!-- 教学目标 -->
<div class="objective-card" v-if="currentScript.objective">
<div class="card-label">
<AimOutlined /> 教学目标
</div>
<div class="card-content">{{ currentScript.objective }}</div>
</div>
<!-- 教师讲稿 -->
<div class="script-card">
<div class="card-label script-label">
<SoundOutlined /> 教师讲稿
</div>
<div class="script-text">{{ currentScript.teacherScript || '暂无讲稿' }}</div>
</div>
<!-- 互动要点 -->
<div class="interaction-card" v-if="currentScript.interactionPointsText">
<div class="card-label interaction-label">
<BulbOutlined /> 互动要点
</div>
<div class="card-content">{{ currentScript.interactionPointsText }}</div>
</div>
<!-- 逐页内容 -->
<div class="pages-section" v-if="pages.length > 0">
<div class="section-header">
<FileTextOutlined /> 逐页脚本
<span class="page-count"> {{ pages.length }} </span>
</div>
<div class="page-tabs">
<div
v-for="(page, index) in pages"
:key="page.id"
class="page-tab"
:class="{ active: activePageIndex === index, 'has-content': hasPageContent(page) }"
@click="activePageIndex = index"
>
<span>{{ page.pageNumber }}</span>
<span v-if="hasPageContent(page)" class="content-dot"></span>
</div>
</div>
<div class="page-content" v-if="pages[activePageIndex]">
<div class="page-item" v-if="pages[activePageIndex].questions">
<span class="page-label">提问</span>
<span>{{ pages[activePageIndex].questions }}</span>
</div>
<div class="page-item" v-if="pages[activePageIndex].teacherNotes">
<span class="page-label">备注</span>
<span class="note-text">{{ pages[activePageIndex].teacherNotes }}</span>
</div>
<div class="page-item" v-if="currentPageResources.length > 0">
<span class="page-label">资源</span>
<div class="page-resources">
<a-tag
v-for="res in currentPageResources"
:key="res.id"
size="small"
style="cursor: pointer; margin: 2px;"
@click="openResource(res)"
>
<PlayCircleOutlined v-if="res.type === '视频'" style="margin-right: 4px;" />
<SoundOutlined v-else-if="res.type === '音频'" style="margin-right: 4px;" />
<FilePdfOutlined v-else-if="res.type === '电子绘本'" style="margin-right: 4px;" />
<FilePptOutlined v-else-if="res.type === 'PPT课件'" style="margin-right: 4px;" />
<PictureOutlined v-else style="margin-right: 4px;" />
{{ res.name }}
</a-tag>
</div>
</div>
<div class="page-empty" v-if="!pages[activePageIndex].questions && !pages[activePageIndex].teacherNotes && currentPageResources.length === 0">
该页暂无脚本内容
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<InboxOutlined />
<p>请选择一个教学步骤</p>
</div>
</div>
<!-- 右侧工具面板 -->
<div class="tool-panel">
<!-- 本步骤材料 -->
<div class="panel-card materials-card">
<div class="panel-header">
<FolderOutlined />
<span>本环节材料</span>
</div>
<div class="panel-body">
<div v-if="stepMaterials.length > 0" class="materials-list">
<div
v-for="item in stepMaterials"
:key="item.id"
class="material-item"
@click="previewMaterial(item)"
>
<div class="material-icon" :class="item.type">
<PlayCircleOutlined v-if="item.type === '视频'" />
<SoundOutlined v-else-if="item.type === '音频'" />
<FilePdfOutlined v-else-if="item.type === '电子绘本'" />
<FilePptOutlined v-else-if="item.type === 'PPT课件'" />
<PictureOutlined v-else />
</div>
<div class="material-info">
<span class="material-name">{{ item.name }}</span>
<span class="material-type">{{ item.type }}</span>
</div>
<EyeOutlined class="material-preview-icon" />
</div>
</div>
<div v-else class="empty-materials">
<InboxOutlined />
<span>暂无关联材料</span>
</div>
</div>
</div>
<!-- 学生评价 -->
<div class="panel-card evaluation-card">
<div class="panel-header">
<StarOutlined />
<span>课堂评价</span>
</div>
<div class="panel-body">
<div class="rating-item">
<span class="rating-label">整体表现</span>
<a-rate v-model:value="studentEvaluation.overall" allow-half />
</div>
<div class="rating-item">
<span class="rating-label">参与度</span>
<a-rate v-model:value="studentEvaluation.participation" allow-half />
</div>
<div class="rating-item">
<span class="rating-label">兴趣度</span>
<a-rate v-model:value="studentEvaluation.interest" allow-half />
</div>
<div class="rating-item">
<span class="rating-label">理解程度</span>
<a-rate v-model:value="studentEvaluation.understanding" allow-half />
</div>
</div>
</div>
</div>
</div>
<!-- 计时器弹窗 -->
<a-modal v-model:open="showTimer" title="课程计时器" :footer="null" width="360px" centered>
<div class="timer-modal">
<div class="timer-large">{{ formatTime(timerSeconds) }}</div>
<div class="timer-buttons">
<a-button type="primary" size="large" @click="startTimer" :disabled="timerRunning">
<PlayCircleOutlined /> 开始
</a-button>
<a-button size="large" @click="pauseTimer" :disabled="!timerRunning">
<PauseCircleOutlined /> 暂停
</a-button>
<a-button size="large" @click="resetTimer">
<ReloadOutlined /> 重置
</a-button>
</div>
</div>
</a-modal>
<!-- 课堂记录抽屉 -->
<a-drawer
v-model:open="showNotesDrawer"
title="课堂记录"
placement="right"
width="400"
>
<a-form layout="vertical">
<a-form-item label="完成情况">
<a-radio-group v-model:value="lessonRecord.completion">
<a-radio value="完全完成">完全完成</a-radio>
<a-radio value="基本完成">基本完成</a-radio>
<a-radio value="未完成">未完成</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="整体评价">
<a-rate v-model:value="lessonRecord.overallRating" />
</a-form-item>
<a-form-item label="学生参与度">
<a-rate v-model:value="lessonRecord.participationRating" />
</a-form-item>
<a-form-item label="完成备注">
<a-textarea
v-model:value="lessonRecord.completionNote"
placeholder="记录课程完成情况、学生表现等..."
:auto-size="{ minRows: 6, maxRows: 10 }"
/>
</a-form-item>
<a-button type="primary" block size="large" @click="saveLessonRecord">
保存并结束
</a-button>
</a-form>
</a-drawer>
<!-- 文件预览弹窗 -->
<FilePreviewModal
v-model:open="previewModalVisible"
:file-url="previewFileUrl"
:file-name="previewFileName"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import {
ArrowLeftOutlined,
StepBackwardOutlined,
StepForwardOutlined,
ClockCircleOutlined,
EditOutlined,
CheckOutlined,
BookOutlined,
ReadOutlined,
AimOutlined,
SoundOutlined,
BulbOutlined,
FileTextOutlined,
FolderOutlined,
InboxOutlined,
StarOutlined,
EyeOutlined,
PlayCircleOutlined,
PauseCircleOutlined,
ReloadOutlined,
FilePdfOutlined,
FilePptOutlined,
PictureOutlined,
ExpandOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import * as teacherApi from '@/api/teacher';
import FilePreviewModal from '@/components/FilePreviewModal.vue';
const router = useRouter();
const route = useRoute();
const loading = ref(false);
const showTimer = ref(false);
const showNotesDrawer = ref(false);
const selectedStepKeys = ref<string[]>([]);
const currentStepIndex = ref(0);
const activePageIndex = ref(0);
const lessonId = ref(0);
// 文件预览相关
const previewModalVisible = ref(false);
const previewFileUrl = ref('');
const previewFileName = ref('');
const timerSeconds = ref(0);
const timerRunning = ref(false);
let timerInterval: number | null = null;
const lesson = ref<any>({});
const course = ref<any>({});
const classInfo = ref<any>({});
const students = ref<any[]>([]);
const activities = ref<any[]>([]);
const studentEvaluation = ref({
overall: 0,
participation: 0,
interest: 0,
understanding: 0,
});
const lessonRecord = ref({
completion: '基本完成',
overallRating: 0,
participationRating: 0,
completionNote: '',
});
const scripts = ref<any[]>([]);
const pages = ref<any[]>([]);
const stepMaterials = ref<any[]>([]);
// 步骤类型映射
const stepTypeMap: Record<string, string> = {
INTRODUCTION: '导入',
READING: '共读',
DISCUSSION: '讨论',
ACTIVITY: '活动',
CREATIVE: '创作',
SUMMARY: '总结',
};
const currentScript = computed(() => {
const id = selectedStepKeys.value[0];
return scripts.value.find(s => String(s.id) === id);
});
const loadLessonData = async () => {
lessonId.value = parseInt(route.params.id as string);
if (!lessonId.value) return;
loading.value = true;
try {
const data = await teacherApi.getLesson(lessonId.value);
lesson.value = data;
// 解析课程中的路径数组可能是JSON字符串
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),
};
classInfo.value = data.class;
students.value = data.class?.students || [];
scripts.value = (data.course?.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 || []),
})),
}));
// 加载延伸活动
activities.value = data.course?.activities || [];
// 默认选中第一个步骤
if (scripts.value.length > 0) {
selectedStepKeys.value = [String(scripts.value[0].id)];
loadStepData(scripts.value[0]);
}
// 启动计时器
startTimer();
} catch (error: any) {
message.error(error.response?.data?.message || '获取课程数据失败');
} finally {
loading.value = false;
}
};
const loadStepData = (script: any) => {
pages.value = script.pages || [];
activePageIndex.value = 0;
// 根据资源ID加载材料已在loadLessonData中解析
const resourceIds = script.resourceIds || [];
const materials: any[] = [];
resourceIds.forEach((resId: string) => {
const material = getResourceById(resId);
if (material) {
materials.push(material);
}
});
stepMaterials.value = materials;
};
// 根据资源ID获取资源详情
const getResourceById = (resId: string) => {
if (!resId || !course.value) return null;
const parts = resId.split('-');
if (parts.length !== 2) return null;
const type = parts[0];
const index = parseInt(parts[1]);
const typeNames: Record<string, string> = {
ebook: '电子绘本',
audio: '音频',
video: '视频',
ppt: 'PPT课件',
poster: '教学挂图',
};
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,
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,
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,
url: videos[index].path,
};
}
break;
}
case 'ppt': {
if (course.value.pptPath) {
return {
id: resId,
name: course.value.pptName || '教学PPT',
type: typeNames.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,
url: posters[index].path,
};
}
break;
}
}
return null;
};
// 检查页面是否有内容
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 (!pages.value[activePageIndex.value] || !pages.value[activePageIndex.value].resourceIds) return [];
const pageResourceIds = pages.value[activePageIndex.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 '';
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return filePath;
}
const SERVER_BASE = import.meta.env.VITE_SERVER_BASE_URL || 'http://localhost:3000';
return `${SERVER_BASE}${filePath}`;
};
// 打开资源
const openResource = (res: any) => {
if (res.url) {
previewFileUrl.value = getFileUrl(res.url);
previewFileName.value = res.name;
previewModalVisible.value = true;
}
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
};
const handleStepClick = ({ key }: { key: string | number }) => {
selectedStepKeys.value = [String(key)];
currentStepIndex.value = scripts.value.findIndex(s => String(s.id) === String(key));
const script = scripts.value[currentStepIndex.value];
if (script) {
loadStepData(script);
}
};
const previousStep = () => {
if (currentStepIndex.value > 0) {
currentStepIndex.value--;
selectedStepKeys.value = [String(scripts.value[currentStepIndex.value].id)];
loadStepData(scripts.value[currentStepIndex.value]);
}
};
const nextStep = () => {
if (currentStepIndex.value < scripts.value.length - 1) {
currentStepIndex.value++;
selectedStepKeys.value = [String(scripts.value[currentStepIndex.value].id)];
loadStepData(scripts.value[currentStepIndex.value]);
}
};
// 打开展播模式(新标签页)
const openBroadcastMode = () => {
// 在新标签页打开展播模式
const broadcastUrl = `${window.location.origin}/teacher/broadcast/${lessonId.value}?step=${currentStepIndex.value}`;
window.open(broadcastUrl, '_blank', 'noopener,noreferrer');
};
const startTimer = () => {
timerRunning.value = true;
timerInterval = setInterval(() => {
timerSeconds.value++;
}, 1000) as unknown as number;
};
const pauseTimer = () => {
timerRunning.value = false;
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
};
const resetTimer = () => {
pauseTimer();
timerSeconds.value = 0;
};
const exitLesson = () => {
Modal.confirm({
title: '确认退出',
content: '确定要退出上课吗?课堂记录将不会保存。',
okText: '确认退出',
cancelText: '继续上课',
onOk: () => {
router.push(`/teacher/courses/${course.value.id}/prepare`);
},
});
};
const saveLessonRecord = async () => {
try {
await teacherApi.finishLesson(lessonId.value, {
overallRating: lessonRecord.value.overallRating > 0 ? String(lessonRecord.value.overallRating) : undefined,
participationRating: lessonRecord.value.participationRating > 0 ? String(lessonRecord.value.participationRating) : undefined,
completionNote: lessonRecord.value.completionNote,
actualDuration: timerSeconds.value > 0 ? Math.round(timerSeconds.value / 60) : undefined,
});
message.success('课程记录已保存');
showNotesDrawer.value = false;
router.push(`/teacher/courses/${course.value.id}/prepare`);
} catch (error: any) {
message.error(error.response?.data?.message || '保存记录失败');
}
};
const finishLesson = () => {
pauseTimer();
Modal.confirm({
title: '结束课程',
content: `本次课程已进行 ${formatTime(timerSeconds.value)},确定要结束吗?`,
okText: '确认结束',
cancelText: '继续上课',
onOk: () => {
showNotesDrawer.value = true;
},
});
};
// 预览材料
const previewMaterial = (item: any) => {
if (!item.url && !item.path) {
message.warning('该材料暂无可预览的文件');
return;
}
previewFileUrl.value = getFileUrl(item.url || item.path);
previewFileName.value = item.name || '教学材料';
previewModalVisible.value = true;
};
onMounted(() => {
loadLessonData();
});
onUnmounted(() => {
if (timerInterval) {
clearInterval(timerInterval);
}
});
</script>
<style scoped lang="scss">
.lesson-view {
min-height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #FFF5EB 0%, #FFF9F5 50%, #F0F7FF 100%);
&.kids-mode-active {
// 投屏模式下隐藏滚动条
overflow: hidden;
}
}
// 顶部工具栏
.lesson-toolbar {
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
padding: 12px 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 12px rgba(255, 140, 66, 0.2);
position: sticky;
top: 0;
z-index: 100;
.toolbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.toolbar-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.exit-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
&:hover {
background: rgba(255, 255, 255, 0.3);
color: white;
border-color: rgba(255, 255, 255, 0.5);
}
}
.kids-mode-btn {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border: none;
color: white;
font-weight: 500;
&:hover {
background: linear-gradient(135deg, #73d13d 0%, #52c41a 100%);
color: white;
}
}
.course-info {
display: flex;
align-items: center;
gap: 8px;
color: white;
.course-icon {
font-size: 18px;
}
.course-name {
font-size: 16px;
font-weight: 500;
}
}
.timer-display {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.2);
padding: 8px 20px;
border-radius: 24px;
color: white;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
.timer-icon {
font-size: 18px;
}
.timer-value {
font-size: 20px;
font-weight: 600;
font-family: 'Courier New', monospace;
min-width: 60px;
}
}
.nav-buttons {
display: flex;
gap: 5px;
.ant-btn {
border-radius: 6px;
font-weight: 500;
}
// 上一步按钮 - 白色背景深色文字
.ant-btn:not(.next-btn) {
background: white;
border-color: white;
color: #666;
&:hover:not(:disabled) {
background: #FFF5EB;
border-color: #FFD4B8;
color: #FF6B35;
}
&:disabled {
background: rgba(255, 255, 255, 0.5);
border-color: rgba(255, 255, 255, 0.5);
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}
}
// 下一步按钮 - 白色背景橙色文字
.next-btn {
background: white;
border-color: white;
color: #FF6B35;
font-weight: 600;
&:hover:not(:disabled) {
background: #FF6B35;
border-color: #FF6B35;
color: white;
}
&:disabled {
background: rgba(255, 255, 255, 0.5);
border-color: rgba(255, 255, 255, 0.5);
color: rgba(255, 107, 53, 0.4);
cursor: not-allowed;
}
}
}
// 全屏和课堂记录按钮 - 白色背景橙色图标
.toolbar-icon-btn {
background: white;
border-color: white;
color: #FF6B35;
width: 36px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
&:hover {
background: #FF6B35 !important;
border-color: #FF6B35 !important;
color: white !important;
}
:deep(.anticon) {
font-size: 16px;
}
}
// 结束课程按钮 - 保持绿色但优化可见度
.finish-btn {
background: #52c41a;
border-color: #52c41a;
color: white;
font-weight: 600;
&:hover {
background: #73d13d !important;
border-color: #73d13d !important;
color: white !important;
}
}
}
// 主内容区
.lesson-content {
flex: 1;
display: flex;
padding: 16px;
gap: 16px;
overflow: hidden;
}
// 左侧步骤导航
.step-nav {
width: 240px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
overflow: hidden;
.nav-header {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #333;
font-size: 15px;
.progress-text {
margin-left: auto;
color: #FF8C42;
font-size: 14px;
}
}
.step-list {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.step-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 8px;
&:hover {
background: #FFF5EB;
}
&.active {
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
box-shadow: 0 4px 12px rgba(255, 140, 66, 0.3);
.step-number {
background: white;
color: #FF6B35;
}
.step-name {
color: white;
font-weight: 600;
}
.step-duration {
color: rgba(255, 255, 255, 0.8);
}
}
&.completed {
.step-number {
background: #52c41a;
color: white;
}
}
}
.step-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
color: #666;
flex-shrink: 0;
}
.step-info {
flex: 1;
min-width: 0;
}
.step-name {
display: block;
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.step-duration {
font-size: 12px;
color: #999;
}
.progress-bar {
height: 4px;
background: #f0f0f0;
margin: 0 16px 16px;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #FF8C42, #FF6B35);
border-radius: 2px;
transition: width 0.3s ease;
}
}
}
// 中间展示区域
.display-area {
flex: 1;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow-y: auto;
padding: 24px;
}
.script-display {
.script-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.step-badge {
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.step-title {
margin: 0;
font-size: 24px;
color: #333;
}
.duration-tag {
background: #FFF5EB;
color: #FF6B35;
border: none;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
}
}
}
.objective-card,
.script-card,
.interaction-card {
background: #fafafa;
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 16px;
.card-label {
font-size: 13px;
color: #666;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.card-content {
font-size: 15px;
color: #333;
line-height: 1.6;
}
}
.objective-card {
background: #E6F7FF;
border-left: 4px solid #1890ff;
.card-label {
color: #1890ff;
}
}
.script-card {
background: #FFF5EB;
border-left: 4px solid #FF8C42;
.script-label {
color: #FF8C42;
font-weight: 500;
}
.script-text {
font-size: 16px;
line-height: 1.8;
color: #333;
white-space: pre-wrap;
}
}
.interaction-card {
background: #F6FFED;
border-left: 4px solid #52c41a;
.interaction-label {
color: #52c41a;
}
}
.pages-section {
margin-top: 24px;
padding-top: 20px;
border-top: 1px dashed #e8e8e8;
.section-header {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
.page-count {
font-size: 12px;
color: #999;
font-weight: normal;
}
}
.page-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.page-tab {
width: 36px;
height: 36px;
border-radius: 8px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
color: #666;
transition: all 0.2s;
position: relative;
&:hover {
background: #FFF5EB;
color: #FF8C42;
}
&.active {
background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%);
color: white;
font-weight: 600;
}
&.has-content .content-dot {
position: absolute;
top: 2px;
right: 2px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #52c41a;
}
}
.page-resources {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.page-content {
background: #fafafa;
border-radius: 8px;
padding: 16px;
}
.page-item {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.page-label {
color: #666;
font-size: 13px;
margin-right: 8px;
}
.note-text {
color: #999;
font-style: italic;
}
.page-empty {
color: #999;
text-align: center;
padding: 20px;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
.anticon {
font-size: 48px;
margin-bottom: 16px;
}
}
// 右侧工具面板
.tool-panel {
width: 280px;
display: flex;
flex-direction: column;
gap: 16px;
}
.panel-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow: hidden;
.panel-header {
padding: 14px 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.panel-body {
padding: 12px;
}
}
.materials-card {
.panel-header {
background: #FFF5EB;
color: #FF6B35;
}
}
.materials-list {
.material-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f5f5f5;
.material-preview-icon {
opacity: 1;
}
}
}
.material-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
&.视频 {
background: #F3E5F5;
color: #8E24AA;
}
&.音频 {
background: #E3F2FD;
color: #1976D2;
}
&.电子绘本 {
background: #FFF3E0;
color: #FB8C00;
}
&.PPT课件 {
background: #E8F5E9;
color: #43A047;
}
&.教学挂图 {
background: #FCE4EC;
color: #C2185B;
}
}
.material-info {
flex: 1;
min-width: 0;
}
.material-name {
display: block;
font-size: 13px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.material-type {
font-size: 11px;
color: #999;
}
.material-preview-icon {
color: #1890ff;
opacity: 0;
transition: opacity 0.2s;
}
}
.empty-materials {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
color: #999;
.anticon {
font-size: 32px;
margin-bottom: 8px;
}
}
.evaluation-card {
.panel-header {
background: #FFFBE6;
color: #FA8C16;
}
.rating-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.rating-label {
font-size: 13px;
color: #666;
}
:deep(.ant-rate) {
font-size: 16px;
}
}
// 计时器弹窗
.timer-modal {
text-align: center;
padding: 20px 0;
.timer-large {
font-size: 64px;
font-weight: 700;
color: #FF6B35;
font-family: 'Courier New', monospace;
margin-bottom: 24px;
text-shadow: 0 2px 4px rgba(255, 107, 53, 0.2);
}
.timer-buttons {
display: flex;
justify-content: center;
gap: 12px;
.ant-btn {
display: flex;
align-items: center;
gap: 6px;
}
}
}
</style>