1463 lines
35 KiB
Vue
1463 lines
35 KiB
Vue
|
|
<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>
|