2026-02-26 15:22:26 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="lesson-view">
|
|
|
|
|
|
<!-- 顶部工具栏 -->
|
|
|
|
|
|
<div class="lesson-toolbar">
|
|
|
|
|
|
<div class="toolbar-left">
|
|
|
|
|
|
<a-button class="exit-btn" @click="exitLesson">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<template #icon>
|
|
|
|
|
|
<ArrowLeftOutlined />
|
|
|
|
|
|
</template>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
退出上课
|
|
|
|
|
|
</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">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<a-button @click="previousStep" :disabled="currentStepIndex === 0 && !hasPreviousLesson">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<StepBackwardOutlined /> 上一步
|
|
|
|
|
|
</a-button>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<a-button type="primary" class="next-btn" @click="nextStep" :disabled="isLastStepOfLastLesson">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
下一步
|
|
|
|
|
|
<StepForwardOutlined />
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-button-group>
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<a-button type="primary" class="broadcast-btn" :disabled="!hasCourseResources" @click="openBroadcastMode">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<template #icon>
|
|
|
|
|
|
<ExpandOutlined />
|
|
|
|
|
|
</template>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
展播模式
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-03-20 10:56:41 +08:00
|
|
|
|
<!-- 课程进度条(多课程时显示,子课程模式不显示) -->
|
|
|
|
|
|
<div v-if="displayLessons.length > 1" class="course-progress-bar">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<a-steps :current="currentLessonIndex" size="small" class="course-steps">
|
2026-03-20 10:56:41 +08:00
|
|
|
|
<a-step v-for="(lesson, index) in displayLessons" :key="lesson.id" :title="getLessonShortName(lesson)"
|
2026-03-20 10:07:17 +08:00
|
|
|
|
:status="getLessonStatus(index)" :disabled="index > currentLessonIndex" @click="handleLessonClick(index)"
|
|
|
|
|
|
class="clickable-step" />
|
2026-03-12 14:33:44 +08:00
|
|
|
|
</a-steps>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 环节进度条 -->
|
|
|
|
|
|
<div class="step-progress-bar">
|
|
|
|
|
|
<div class="progress-info">
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<span class="current-step-label break-all">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
{{ currentLesson?.name }} - {{ currentStep?.name || '准备中' }}
|
|
|
|
|
|
</span>
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<span class="step-count flex-shrink-0">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
环节 {{ currentStepIndex + 1 }}/{{ currentLesson?.steps?.length || 0 }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<a-progress :percent="stepProgressPercent" :show-info="false" stroke-color="#FF8C42" class="step-progress" />
|
2026-03-12 14:33:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<!-- 主内容区 -->
|
|
|
|
|
|
<div class="lesson-content">
|
|
|
|
|
|
<!-- 左侧:步骤导航 -->
|
|
|
|
|
|
<div class="step-nav">
|
|
|
|
|
|
<div class="nav-header">
|
|
|
|
|
|
<ReadOutlined />
|
|
|
|
|
|
<span>教学流程</span>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<span class="progress-text">{{ currentStepIndex + 1 }}/{{ currentLesson?.steps?.length || 0 }}</span>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="step-list">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="(step, index) in currentLesson?.steps || []" :key="step.id" class="step-item" :class="{
|
|
|
|
|
|
active: currentStepIndex === index,
|
|
|
|
|
|
completed: index < currentStepIndex
|
|
|
|
|
|
}" @click="handleStepClick(index)">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<div class="step-number">
|
|
|
|
|
|
<CheckOutlined v-if="index < currentStepIndex" />
|
|
|
|
|
|
<span v-else>{{ index + 1 }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-info">
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<span class="step-name break-all">{{ step.name }}</span>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<span class="step-duration">{{ step.duration }} 分钟</span>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 进度条 -->
|
|
|
|
|
|
<div class="progress-bar">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<div class="progress-fill" :style="{ width: stepProgressPercent + '%' }"></div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 中间:展示区域 -->
|
|
|
|
|
|
<div class="display-area">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<div v-if="currentStep" class="step-display">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<!-- 环节标题 -->
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<div class="step-header">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<div class="header-left">
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<div class="step-badge flex-shrink-0">第 {{ currentStepIndex + 1 }} 环节</div>
|
|
|
|
|
|
<h2 class="step-title break-all whitespace-pre-wrap">{{ currentStep.name }}</h2>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header-tags">
|
|
|
|
|
|
<a-tag class="duration-tag">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<ClockCircleOutlined /> {{ currentStep.duration }}分钟
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<a-tag v-if="currentStep.stepType" class="type-tag">
|
|
|
|
|
|
{{ getStepTypeName(currentStep.stepType) }}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 教学目标 -->
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<div v-if="currentStep.objective" class="objective-card">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<div class="card-label">
|
|
|
|
|
|
<AimOutlined /> 教学目标
|
|
|
|
|
|
</div>
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<div class="card-content break-all whitespace-pre-wrap">{{ currentStep.objective }}</div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<!-- 环节说明 -->
|
|
|
|
|
|
<div v-if="currentStep.description" class="description-card">
|
|
|
|
|
|
<div class="card-label">
|
|
|
|
|
|
<FileTextOutlined /> 环节说明
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<div class="card-content rich-content break-all whitespace-pre-wrap" v-html="currentStep.description"></div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<!-- 教师讲稿 -->
|
|
|
|
|
|
<div v-if="currentStep.script" class="script-card">
|
|
|
|
|
|
<div class="card-label script-label">
|
|
|
|
|
|
<SoundOutlined /> 教师讲稿
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<div class="script-text rich-content break-all whitespace-pre-wrap" v-html="currentStep.script"></div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<!-- 环节资源 -->
|
|
|
|
|
|
<div v-if="hasStepResources" class="resources-card">
|
|
|
|
|
|
<div class="card-label">
|
|
|
|
|
|
<FolderOutlined /> 环节资源
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<div class="resources-grid">
|
|
|
|
|
|
<!-- 图片资源 -->
|
|
|
|
|
|
<div v-if="currentStep.images?.length" class="resource-group">
|
|
|
|
|
|
<span class="resource-type-label">图片</span>
|
|
|
|
|
|
<div class="resource-items">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="(img, idx) in currentStep.images" :key="idx" class="resource-item"
|
|
|
|
|
|
@click="previewResource(img, 'image')">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<PictureOutlined /> {{ img.name || `图片${idx + 1}` }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 视频资源 -->
|
|
|
|
|
|
<div v-if="currentStep.videos?.length" class="resource-group">
|
|
|
|
|
|
<span class="resource-type-label">视频</span>
|
|
|
|
|
|
<div class="resource-items">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="(vid, idx) in currentStep.videos" :key="idx" class="resource-item"
|
|
|
|
|
|
@click="previewResource(vid, 'video')">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<VideoCameraOutlined /> {{ vid.name || `视频${idx + 1}` }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 音频资源 -->
|
|
|
|
|
|
<div v-if="currentStep.audioList?.length" class="resource-group">
|
|
|
|
|
|
<span class="resource-type-label">音频</span>
|
|
|
|
|
|
<div class="resource-items">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="(aud, idx) in currentStep.audioList" :key="idx" class="resource-item"
|
|
|
|
|
|
@click="previewResource(aud, 'audio')">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<AudioOutlined /> {{ aud.name || `音频${idx + 1}` }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- PPT资源 -->
|
|
|
|
|
|
<div v-if="currentStep.pptFiles?.length" class="resource-group">
|
|
|
|
|
|
<span class="resource-type-label">课件</span>
|
|
|
|
|
|
<div class="resource-items">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="(ppt, idx) in currentStep.pptFiles" :key="idx" class="resource-item"
|
|
|
|
|
|
@click="previewResource(ppt, 'ppt')">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<FilePptOutlined /> {{ ppt.name || `课件${idx + 1}` }}
|
|
|
|
|
|
</div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 文档资源 -->
|
|
|
|
|
|
<div v-if="currentStep.documents?.length" class="resource-group">
|
|
|
|
|
|
<span class="resource-type-label">文档</span>
|
|
|
|
|
|
<div class="resource-items">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="(doc, idx) in currentStep.documents" :key="idx" class="resource-item"
|
|
|
|
|
|
@click="previewResource(doc, 'document')">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<FileTextOutlined /> {{ doc.name || `文档${idx + 1}` }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="empty-state">
|
|
|
|
|
|
<InboxOutlined />
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<p>请选择一个教学环节</p>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:工具面板 -->
|
|
|
|
|
|
<div class="tool-panel">
|
2026-03-20 10:56:41 +08:00
|
|
|
|
<!-- 课程类型 -->
|
|
|
|
|
|
<div v-if="currentLesson?.lessonType" class="panel-card lesson-type-card">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<BookOutlined />
|
|
|
|
|
|
<span>课程类型</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel-body">
|
|
|
|
|
|
<a-tag size="large" class="lesson-type-tag" :style="getLessonTagStyle(currentLesson.lessonType)">
|
|
|
|
|
|
{{ getLessonTypeName(currentLesson.lessonType) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<!-- 课程核心资源 -->
|
|
|
|
|
|
<div v-if="hasCourseResources" class="panel-card materials-card">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<FolderOutlined />
|
|
|
|
|
|
<span>课程资源</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel-body">
|
|
|
|
|
|
<div v-if="courseResources.length > 0" class="materials-list">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="item in courseResources" :key="item.id" class="material-item"
|
|
|
|
|
|
@click="previewCourseResource(item)">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
<div class="material-icon" :class="item.type">
|
|
|
|
|
|
<VideoCameraOutlined v-if="item.type === 'video'" />
|
|
|
|
|
|
<AudioOutlined v-else-if="item.type === 'audio'" />
|
|
|
|
|
|
<FilePptOutlined v-else-if="item.type === 'ppt'" />
|
|
|
|
|
|
<FileTextOutlined v-else-if="item.type === 'document'" />
|
|
|
|
|
|
<PictureOutlined v-else />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="material-info">
|
|
|
|
|
|
<span class="material-name">{{ item.name }}</span>
|
|
|
|
|
|
<span class="material-type">{{ item.typeLabel }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<EyeOutlined class="material-preview-icon" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="empty-materials">
|
|
|
|
|
|
<InboxOutlined />
|
|
|
|
|
|
<span>暂无课程资源</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 教学准备 -->
|
|
|
|
|
|
<div v-if="currentLesson?.preparation" class="panel-card preparation-card">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<ToolOutlined />
|
|
|
|
|
|
<span>教学准备</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel-body preparation-body">
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<div class="preparation-text break-all whitespace-pre-wrap">{{ currentLesson.preparation }}</div>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 教学延伸 -->
|
|
|
|
|
|
<div v-if="currentLesson?.extension" class="panel-card extension-card">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<BranchesOutlined />
|
|
|
|
|
|
<span>教学延伸</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel-body extension-body">
|
2026-03-31 11:07:53 +08:00
|
|
|
|
<div class="extension-text break-all whitespace-pre-wrap">{{ currentLesson.extension }}</div>
|
2026-03-12 14:33:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 本环节材料 -->
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<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">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<div v-for="item in stepMaterials" :key="item.id" class="material-item" @click="previewMaterial(item)">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 课堂记录抽屉 -->
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<a-drawer v-model:open="showNotesDrawer" title="课堂记录" placement="right" width="400">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
<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="完成备注">
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<a-textarea v-model:value="lessonRecord.completionNote" placeholder="记录课程完成情况、学生表现等..."
|
|
|
|
|
|
:auto-size="{ minRows: 6, maxRows: 10 }" />
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-button type="primary" block size="large" @click="saveLessonRecord">
|
|
|
|
|
|
保存并结束
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-drawer>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文件预览弹窗 -->
|
2026-03-20 10:07:17 +08:00
|
|
|
|
<FilePreviewModal v-model:open="previewModalVisible" :file-url="previewFileUrl" :file-name="previewFileName" />
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-12 14:33:44 +08:00
|
|
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
2026-02-26 15:22:26 +08:00
|
|
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
|
StepBackwardOutlined,
|
|
|
|
|
|
StepForwardOutlined,
|
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
CheckOutlined,
|
|
|
|
|
|
BookOutlined,
|
|
|
|
|
|
ReadOutlined,
|
|
|
|
|
|
AimOutlined,
|
|
|
|
|
|
SoundOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
FolderOutlined,
|
2026-03-12 14:33:44 +08:00
|
|
|
|
BranchesOutlined,
|
|
|
|
|
|
ToolOutlined,
|
2026-02-26 15:22:26 +08:00
|
|
|
|
InboxOutlined,
|
|
|
|
|
|
StarOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
PlayCircleOutlined,
|
|
|
|
|
|
PauseCircleOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
FilePdfOutlined,
|
|
|
|
|
|
FilePptOutlined,
|
|
|
|
|
|
PictureOutlined,
|
|
|
|
|
|
ExpandOutlined,
|
2026-03-12 14:33:44 +08:00
|
|
|
|
VideoCameraOutlined,
|
|
|
|
|
|
AudioOutlined,
|
2026-02-26 15:22:26 +08:00
|
|
|
|
} from '@ant-design/icons-vue';
|
|
|
|
|
|
import { message, Modal } from 'ant-design-vue';
|
|
|
|
|
|
import * as teacherApi from '@/api/teacher';
|
|
|
|
|
|
import FilePreviewModal from '@/components/FilePreviewModal.vue';
|
2026-03-20 10:56:41 +08:00
|
|
|
|
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const showTimer = ref(false);
|
|
|
|
|
|
const showNotesDrawer = ref(false);
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const currentLessonIndex = ref(0);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
const currentStepIndex = ref(0);
|
2026-03-16 14:54:18 +08:00
|
|
|
|
const lessonId = ref<string>('');
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 文件预览相关
|
|
|
|
|
|
const previewModalVisible = ref(false);
|
|
|
|
|
|
const previewFileUrl = ref('');
|
|
|
|
|
|
const previewFileName = ref('');
|
|
|
|
|
|
|
|
|
|
|
|
const timerSeconds = ref(0);
|
|
|
|
|
|
const timerRunning = ref(false);
|
|
|
|
|
|
let timerInterval: number | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
const course = ref<any>({});
|
|
|
|
|
|
const classInfo = ref<any>({});
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const lessons = ref<any[]>([]);
|
2026-03-20 10:56:41 +08:00
|
|
|
|
/** 排课选择的课程类型(子课程模式:仅展示该子课程,子课程结束即上课结束) */
|
|
|
|
|
|
const scheduleLessonType = ref<string | undefined>(undefined);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
const studentEvaluation = ref({
|
|
|
|
|
|
overall: 0,
|
|
|
|
|
|
participation: 0,
|
|
|
|
|
|
interest: 0,
|
|
|
|
|
|
understanding: 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const lessonRecord = ref({
|
|
|
|
|
|
completion: '基本完成',
|
|
|
|
|
|
overallRating: 0,
|
|
|
|
|
|
participationRating: 0,
|
|
|
|
|
|
completionNote: '',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-20 10:56:41 +08:00
|
|
|
|
/** 判断排课 lessonType 与课程 lessonType 是否匹配(兼容 INTRODUCTION/INTRO、LANGUAGE/DOMAIN_LANGUAGE 等变体) */
|
|
|
|
|
|
const lessonTypeMatches = (scheduleType: string, lessonType: string): boolean => {
|
|
|
|
|
|
if (!scheduleType || !lessonType) return false;
|
|
|
|
|
|
const s = scheduleType.toUpperCase();
|
|
|
|
|
|
const l = lessonType.toUpperCase();
|
|
|
|
|
|
if (s === l) return true;
|
|
|
|
|
|
const pairs: [string, string][] = [
|
|
|
|
|
|
['INTRODUCTION', 'INTRO'],
|
|
|
|
|
|
['LANGUAGE', 'DOMAIN_LANGUAGE'],
|
|
|
|
|
|
['HEALTH', 'DOMAIN_HEALTH'],
|
|
|
|
|
|
['SCIENCE', 'DOMAIN_SCIENCE'],
|
|
|
|
|
|
['SOCIAL', 'DOMAIN_SOCIAL'],
|
|
|
|
|
|
['SOCIETY', 'DOMAIN_SOCIAL'],
|
|
|
|
|
|
['ART', 'DOMAIN_ART'],
|
|
|
|
|
|
];
|
|
|
|
|
|
for (const [a, b] of pairs) {
|
|
|
|
|
|
if ((s === a || s === b) && (l === a || l === b)) return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** 展示的课程列表:子课程模式时仅包含排课选中的子课程,否则为全部 */
|
|
|
|
|
|
const displayLessons = computed(() => {
|
|
|
|
|
|
const type = scheduleLessonType.value;
|
|
|
|
|
|
if (!type || lessons.value.length === 0) return lessons.value;
|
|
|
|
|
|
const matched = lessons.value.filter((l) => lessonTypeMatches(type, l.lessonType || ''));
|
|
|
|
|
|
return matched.length > 0 ? matched : lessons.value;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 当前课程
|
2026-03-20 10:56:41 +08:00
|
|
|
|
const currentLesson = computed(() => displayLessons.value[currentLessonIndex.value] || null);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 当前环节
|
|
|
|
|
|
const currentStep = computed(() => {
|
|
|
|
|
|
if (!currentLesson.value?.steps) return null;
|
|
|
|
|
|
return currentLesson.value.steps[currentStepIndex.value] || null;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 环节进度百分比
|
|
|
|
|
|
const stepProgressPercent = computed(() => {
|
|
|
|
|
|
const totalSteps = currentLesson.value?.steps?.length || 1;
|
|
|
|
|
|
return Math.round(((currentStepIndex.value + 1) / totalSteps) * 100);
|
|
|
|
|
|
});
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 是否有上一个课程
|
|
|
|
|
|
const hasPreviousLesson = computed(() => currentLessonIndex.value > 0);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-20 10:56:41 +08:00
|
|
|
|
// 是否是最后一个课程的最后一个环节(子课程模式下,当前子课程最后一环节即视为最后)
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const isLastStepOfLastLesson = computed(() => {
|
2026-03-20 10:56:41 +08:00
|
|
|
|
if (currentLessonIndex.value < displayLessons.value.length - 1) return false;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const totalSteps = currentLesson.value?.steps?.length || 1;
|
|
|
|
|
|
return currentStepIndex.value >= totalSteps - 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 当前环节是否有资源
|
|
|
|
|
|
const hasStepResources = computed(() => {
|
|
|
|
|
|
const step = currentStep.value;
|
|
|
|
|
|
if (!step) return false;
|
|
|
|
|
|
return (step.images?.length || 0) +
|
|
|
|
|
|
(step.videos?.length || 0) +
|
|
|
|
|
|
(step.audioList?.length || 0) +
|
|
|
|
|
|
(step.pptFiles?.length || 0) +
|
|
|
|
|
|
(step.documents?.length || 0) > 0;
|
|
|
|
|
|
});
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 本环节材料列表(从当前环节资源生成)
|
|
|
|
|
|
const stepMaterials = computed(() => {
|
|
|
|
|
|
const step = currentStep.value;
|
|
|
|
|
|
if (!step) return [];
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
const materials: any[] = [];
|
2026-03-12 14:33:44 +08:00
|
|
|
|
|
|
|
|
|
|
step.images?.forEach((img: any, idx: number) => {
|
|
|
|
|
|
materials.push({
|
|
|
|
|
|
id: `img-${idx}`,
|
|
|
|
|
|
name: img.name || `图片${idx + 1}`,
|
|
|
|
|
|
type: '图片',
|
|
|
|
|
|
url: img.path,
|
|
|
|
|
|
});
|
2026-02-26 15:22:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
step.videos?.forEach((vid: any, idx: number) => {
|
|
|
|
|
|
materials.push({
|
|
|
|
|
|
id: `video-${idx}`,
|
|
|
|
|
|
name: vid.name || `视频${idx + 1}`,
|
|
|
|
|
|
type: '视频',
|
|
|
|
|
|
url: vid.path,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
step.audioList?.forEach((aud: any, idx: number) => {
|
|
|
|
|
|
materials.push({
|
|
|
|
|
|
id: `audio-${idx}`,
|
|
|
|
|
|
name: aud.name || `音频${idx + 1}`,
|
|
|
|
|
|
type: '音频',
|
|
|
|
|
|
url: aud.path,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
step.pptFiles?.forEach((ppt: any, idx: number) => {
|
|
|
|
|
|
materials.push({
|
|
|
|
|
|
id: `ppt-${idx}`,
|
|
|
|
|
|
name: ppt.name || `课件${idx + 1}`,
|
|
|
|
|
|
type: 'PPT课件',
|
|
|
|
|
|
url: ppt.path,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
step.documents?.forEach((doc: any, idx: number) => {
|
|
|
|
|
|
materials.push({
|
|
|
|
|
|
id: `doc-${idx}`,
|
|
|
|
|
|
name: doc.name || `文档${idx + 1}`,
|
|
|
|
|
|
type: '文档',
|
|
|
|
|
|
url: doc.path,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
return materials;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 是否有课程级资源
|
|
|
|
|
|
const hasCourseResources = computed(() => {
|
|
|
|
|
|
const lesson = currentLesson.value;
|
|
|
|
|
|
if (!lesson) return false;
|
|
|
|
|
|
return (lesson.videos?.length || 0) +
|
|
|
|
|
|
(lesson.pptFiles?.length || 0) +
|
|
|
|
|
|
(lesson.documents?.length || 0) > 0;
|
|
|
|
|
|
});
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 课程资源列表
|
|
|
|
|
|
const courseResources = computed(() => {
|
|
|
|
|
|
const lesson = currentLesson.value;
|
|
|
|
|
|
if (!lesson) return [];
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const resources: any[] = [];
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
lesson.videos?.forEach((vid: any, idx: number) => {
|
|
|
|
|
|
resources.push({
|
|
|
|
|
|
id: `course-video-${idx}`,
|
|
|
|
|
|
name: vid.name || `视频${idx + 1}`,
|
|
|
|
|
|
type: 'video',
|
|
|
|
|
|
typeLabel: '视频',
|
|
|
|
|
|
url: vid.path,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
lesson.pptFiles?.forEach((ppt: any, idx: number) => {
|
|
|
|
|
|
resources.push({
|
|
|
|
|
|
id: `course-ppt-${idx}`,
|
|
|
|
|
|
name: ppt.name || `课件${idx + 1}`,
|
|
|
|
|
|
type: 'ppt',
|
|
|
|
|
|
typeLabel: '课件',
|
|
|
|
|
|
url: ppt.path,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
lesson.documents?.forEach((doc: any, idx: number) => {
|
|
|
|
|
|
resources.push({
|
|
|
|
|
|
id: `course-doc-${idx}`,
|
|
|
|
|
|
name: doc.name || `文档${idx + 1}`,
|
|
|
|
|
|
type: 'document',
|
|
|
|
|
|
typeLabel: '文档',
|
|
|
|
|
|
url: doc.path,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return resources;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听进度变化,自动保存
|
|
|
|
|
|
watch([currentLessonIndex, currentStepIndex], () => {
|
|
|
|
|
|
if (lessonId.value) {
|
|
|
|
|
|
saveProgress();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取环节类型名称
|
|
|
|
|
|
const getStepTypeName = (type: string): string => {
|
|
|
|
|
|
const typeMap: Record<string, string> = {
|
|
|
|
|
|
'WARMUP': '热身导入',
|
|
|
|
|
|
'INTRODUCTION': '导入环节',
|
|
|
|
|
|
'DEVELOPMENT': '发展环节',
|
|
|
|
|
|
'PRACTICE': '练习环节',
|
|
|
|
|
|
'EXTENSION': '延伸环节',
|
|
|
|
|
|
'CONCLUSION': '总结环节',
|
|
|
|
|
|
'ASSESSMENT': '评估环节',
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
2026-03-12 14:33:44 +08:00
|
|
|
|
return typeMap[type] || type;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取课程简称
|
|
|
|
|
|
const getLessonShortName = (lesson: any): string => {
|
|
|
|
|
|
const typeMap: Record<string, string> = {
|
|
|
|
|
|
'INTRO': '导入课',
|
|
|
|
|
|
'INTRODUCTION': '导入课',
|
|
|
|
|
|
'COLLECTIVE': '集体课',
|
|
|
|
|
|
'DOMAIN_LANGUAGE': '语言课',
|
|
|
|
|
|
'DOMAIN_HEALTH': '健康课',
|
|
|
|
|
|
'DOMAIN_SCIENCE': '科学课',
|
|
|
|
|
|
'DOMAIN_SOCIAL': '社会课',
|
|
|
|
|
|
'DOMAIN_ART': '艺术课',
|
|
|
|
|
|
// 兼容旧格式
|
|
|
|
|
|
'LANGUAGE': '语言课',
|
|
|
|
|
|
'HEALTH': '健康课',
|
|
|
|
|
|
'SCIENCE': '科学课',
|
|
|
|
|
|
'SOCIAL': '社会课',
|
|
|
|
|
|
'ART': '艺术课',
|
|
|
|
|
|
};
|
|
|
|
|
|
return typeMap[lesson.lessonType] || lesson.lessonType || '课程';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取课程状态
|
|
|
|
|
|
const getLessonStatus = (index: number): string => {
|
|
|
|
|
|
if (index < currentLessonIndex.value) return 'finish';
|
|
|
|
|
|
if (index === currentLessonIndex.value) return 'process';
|
|
|
|
|
|
return 'wait';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadLessonData = async () => {
|
2026-03-16 14:54:18 +08:00
|
|
|
|
lessonId.value = (route.params.id as string) || '';
|
2026-03-12 14:33:44 +08:00
|
|
|
|
if (!lessonId.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await teacherApi.getLesson(lessonId.value);
|
|
|
|
|
|
course.value = data.course || {};
|
|
|
|
|
|
classInfo.value = data.class || {};
|
|
|
|
|
|
|
2026-03-20 10:56:41 +08:00
|
|
|
|
// 排课选择的课程类型(子课程模式:直接进入该子课程,子课程结束即上课结束)
|
|
|
|
|
|
scheduleLessonType.value = data.lessonType || undefined;
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 获取课程列表
|
|
|
|
|
|
// 如果授课记录包含多个课程,使用该列表;否则使用课程包的所有课程
|
|
|
|
|
|
if (data.lessonCourses && data.lessonCourses.length > 0) {
|
|
|
|
|
|
lessons.value = data.lessonCourses;
|
|
|
|
|
|
} else if (course.value.courseLessons) {
|
|
|
|
|
|
// 转换课程数据格式,与备课模式保持一致
|
|
|
|
|
|
lessons.value = (course.value.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 || '',
|
|
|
|
|
|
}));
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-03-12 14:33:44 +08:00
|
|
|
|
...lesson,
|
|
|
|
|
|
steps,
|
|
|
|
|
|
videos,
|
|
|
|
|
|
pptFiles,
|
|
|
|
|
|
documents,
|
|
|
|
|
|
images: [],
|
|
|
|
|
|
audioList: [],
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
2026-03-12 14:33:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 兼容旧数据:使用 scripts
|
|
|
|
|
|
lessons.value = [{
|
|
|
|
|
|
id: course.value.id,
|
|
|
|
|
|
name: course.value.name,
|
|
|
|
|
|
lessonType: 'CUSTOM',
|
|
|
|
|
|
duration: course.value.duration || 30,
|
|
|
|
|
|
steps: (course.value.scripts || []).map((script: any) => ({
|
|
|
|
|
|
id: script.id,
|
|
|
|
|
|
name: script.stepName,
|
|
|
|
|
|
duration: script.duration || 5,
|
|
|
|
|
|
stepType: script.stepType,
|
|
|
|
|
|
objective: script.objective,
|
|
|
|
|
|
description: script.description,
|
|
|
|
|
|
script: script.teacherScript,
|
|
|
|
|
|
// 兼容旧的资源结构
|
|
|
|
|
|
images: [],
|
|
|
|
|
|
videos: [],
|
|
|
|
|
|
audioList: [],
|
|
|
|
|
|
pptFiles: course.value.pptPath ? [{ name: course.value.pptName || '教学PPT', path: course.value.pptPath }] : [],
|
|
|
|
|
|
documents: [],
|
|
|
|
|
|
})),
|
|
|
|
|
|
}];
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
2026-03-12 14:33:44 +08:00
|
|
|
|
|
2026-03-20 10:56:41 +08:00
|
|
|
|
// 子课程模式:根据排课 lessonType 直接进入对应子课程(优先于进度恢复和 URL 参数)
|
|
|
|
|
|
const matchedLessons = scheduleLessonType.value
|
|
|
|
|
|
? lessons.value.filter((l) => lessonTypeMatches(scheduleLessonType.value!, l.lessonType || ''))
|
|
|
|
|
|
: [];
|
|
|
|
|
|
if (matchedLessons.length > 0) {
|
|
|
|
|
|
currentLessonIndex.value = 0;
|
|
|
|
|
|
currentStepIndex.value = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 尝试恢复进度
|
|
|
|
|
|
try {
|
|
|
|
|
|
const progress = await teacherApi.getLessonProgress(lessonId.value);
|
2026-03-20 10:56:41 +08:00
|
|
|
|
if (progress && (progress.currentLessonId !== undefined || progress.currentStepId !== undefined)) {
|
|
|
|
|
|
const isSub = matchedLessons.length > 0;
|
|
|
|
|
|
const matchedLesson = matchedLessons[0];
|
|
|
|
|
|
const progressIsForMatched = isSub && progress.currentLessonId !== undefined
|
|
|
|
|
|
&& matchedLesson && progress.currentLessonId === matchedLesson.id;
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '检测到上次上课进度',
|
|
|
|
|
|
content: `上次上课到:${getProgressDescription(progress)},是否继续?`,
|
|
|
|
|
|
okText: '继续上课',
|
|
|
|
|
|
cancelText: '重新开始',
|
|
|
|
|
|
onOk: () => {
|
2026-03-20 10:56:41 +08:00
|
|
|
|
if (isSub && progressIsForMatched && progress.currentStepId !== undefined) {
|
|
|
|
|
|
// 子课程模式:仅恢复环节进度
|
|
|
|
|
|
currentLessonIndex.value = 0;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
currentStepIndex.value = progress.currentStepId;
|
2026-03-20 10:56:41 +08:00
|
|
|
|
} else if (!isSub) {
|
|
|
|
|
|
// 非子课程模式:恢复课程和环节进度
|
|
|
|
|
|
if (progress.currentLessonId !== undefined) {
|
|
|
|
|
|
const lessonIndex = lessons.value.findIndex((l) => l.id === progress.currentLessonId);
|
|
|
|
|
|
if (lessonIndex >= 0) currentLessonIndex.value = lessonIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (progress.currentStepId !== undefined) currentStepIndex.value = progress.currentStepId;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-03-20 10:56:41 +08:00
|
|
|
|
onCancel: () => clearProgress(),
|
2026-03-12 14:33:44 +08:00
|
|
|
|
});
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
2026-03-12 14:33:44 +08:00
|
|
|
|
} catch (progressError) {
|
|
|
|
|
|
console.log('No saved progress found');
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
2026-03-12 14:33:44 +08:00
|
|
|
|
|
2026-03-20 10:56:41 +08:00
|
|
|
|
// 非子课程模式时,URL 参数可覆盖
|
|
|
|
|
|
if (matchedLessons.length === 0) {
|
|
|
|
|
|
const queryLessonIndex = route.query.lessonIndex ? parseInt(route.query.lessonIndex as string) : 0;
|
|
|
|
|
|
if (queryLessonIndex >= 0 && queryLessonIndex < lessons.value.length) {
|
|
|
|
|
|
currentLessonIndex.value = queryLessonIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
const queryStepIndex = route.query.stepIndex ? parseInt(route.query.stepIndex as string) : 0;
|
|
|
|
|
|
const totalSteps = lessons.value[currentLessonIndex.value]?.steps?.length || 0;
|
|
|
|
|
|
if (queryStepIndex >= 0 && queryStepIndex < totalSteps) {
|
|
|
|
|
|
currentStepIndex.value = queryStepIndex;
|
|
|
|
|
|
}
|
2026-03-12 14:33:44 +08:00
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 启动计时器
|
|
|
|
|
|
startTimer();
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error.message || '获取课程数据失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 获取进度描述
|
|
|
|
|
|
const getProgressDescription = (progress: any): string => {
|
|
|
|
|
|
if (progress.currentLessonId !== undefined) {
|
|
|
|
|
|
const lesson = lessons.value.find((l) => l.id === progress.currentLessonId);
|
|
|
|
|
|
if (lesson) {
|
|
|
|
|
|
return `${lesson.name} - 第 ${progress.currentStepId + 1} 环节`;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 14:33:44 +08:00
|
|
|
|
return '未知';
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 保存进度
|
|
|
|
|
|
const saveProgress = async () => {
|
|
|
|
|
|
try {
|
2026-03-20 10:56:41 +08:00
|
|
|
|
const list = displayLessons.value;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
await teacherApi.saveLessonProgress(lessonId.value, {
|
2026-03-20 10:56:41 +08:00
|
|
|
|
lessonIds: list.map((l) => l.id),
|
|
|
|
|
|
completedLessonIds: list.slice(0, currentLessonIndex.value).map((l) => l.id),
|
|
|
|
|
|
currentLessonId: list[currentLessonIndex.value]?.id,
|
2026-03-12 14:33:44 +08:00
|
|
|
|
currentStepId: currentStepIndex.value,
|
|
|
|
|
|
progressData: {
|
|
|
|
|
|
timerSeconds: timerSeconds.value,
|
|
|
|
|
|
savedAt: new Date().toISOString(),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to save progress:', error);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 清除进度
|
|
|
|
|
|
const clearProgress = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await teacherApi.saveLessonProgress(lessonId.value, {
|
|
|
|
|
|
lessonIds: [],
|
|
|
|
|
|
completedLessonIds: [],
|
|
|
|
|
|
currentLessonId: undefined,
|
|
|
|
|
|
currentStepId: undefined,
|
|
|
|
|
|
progressData: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to clear progress:', error);
|
|
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const handleLessonClick = (index: number) => {
|
|
|
|
|
|
if (index <= currentLessonIndex.value) {
|
|
|
|
|
|
currentLessonIndex.value = index;
|
|
|
|
|
|
currentStepIndex.value = 0;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const handleStepClick = (index: number) => {
|
|
|
|
|
|
currentStepIndex.value = index;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
const previousStep = () => {
|
|
|
|
|
|
if (currentStepIndex.value > 0) {
|
|
|
|
|
|
currentStepIndex.value--;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
} else if (currentLessonIndex.value > 0) {
|
|
|
|
|
|
// 切换到上一个课程的最后一个环节
|
|
|
|
|
|
currentLessonIndex.value--;
|
|
|
|
|
|
const prevLesson = lessons.value[currentLessonIndex.value];
|
|
|
|
|
|
currentStepIndex.value = (prevLesson?.steps?.length || 1) - 1;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const nextStep = () => {
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const totalSteps = currentLesson.value?.steps?.length || 0;
|
|
|
|
|
|
if (currentStepIndex.value < totalSteps - 1) {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
currentStepIndex.value++;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
} else if (currentLessonIndex.value < lessons.value.length - 1) {
|
|
|
|
|
|
// 切换到下一个课程的第一个环节
|
|
|
|
|
|
currentLessonIndex.value++;
|
|
|
|
|
|
currentStepIndex.value = 0;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 09:59:19 +08:00
|
|
|
|
// 打开展播模式(新标签页);无课程级资源时展播无内容,与侧栏「课程资源」一致
|
2026-02-26 15:22:26 +08:00
|
|
|
|
const openBroadcastMode = () => {
|
2026-03-25 09:59:19 +08:00
|
|
|
|
if (!hasCourseResources.value) {
|
|
|
|
|
|
message.warning('暂无课程资源,无法进入展播模式');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-12 14:33:44 +08:00
|
|
|
|
const broadcastUrl = `${window.location.origin}/teacher/broadcast/${lessonId.value}?lessonIndex=${currentLessonIndex.value}&stepIndex=${currentStepIndex.value}`;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
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: '确认退出',
|
2026-03-24 16:43:25 +08:00
|
|
|
|
content: '确定要退出上课吗?当前未完成的授课记录将被删除,且无法恢复。',
|
2026-02-26 15:22:26 +08:00
|
|
|
|
okText: '确认退出',
|
|
|
|
|
|
cancelText: '继续上课',
|
2026-03-24 16:43:25 +08:00
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
if (!lessonId.value) {
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await teacherApi.abandonLesson(lessonId.value);
|
|
|
|
|
|
pauseTimer();
|
|
|
|
|
|
message.success('已退出上课');
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error?.message || error?.response?.data?.message || '退出失败');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
});
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 清除进度
|
|
|
|
|
|
await clearProgress();
|
2026-02-26 15:22:26 +08:00
|
|
|
|
message.success('课程记录已保存');
|
|
|
|
|
|
showNotesDrawer.value = false;
|
2026-03-20 10:07:17 +08:00
|
|
|
|
router.back();
|
2026-02-26 15:22:26 +08:00
|
|
|
|
} catch (error: any) {
|
2026-03-12 14:33:44 +08:00
|
|
|
|
message.error(error.message || '保存记录失败');
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const finishLesson = () => {
|
|
|
|
|
|
pauseTimer();
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '结束课程',
|
|
|
|
|
|
content: `本次课程已进行 ${formatTime(timerSeconds.value)},确定要结束吗?`,
|
|
|
|
|
|
okText: '确认结束',
|
|
|
|
|
|
cancelText: '继续上课',
|
|
|
|
|
|
onOk: () => {
|
|
|
|
|
|
showNotesDrawer.value = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 获取完整的文件 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 previewResource = (resource: any, type: string) => {
|
|
|
|
|
|
if (!resource.path) {
|
|
|
|
|
|
message.warning('该资源暂无可预览的文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
previewFileUrl.value = getFileUrl(resource.path);
|
|
|
|
|
|
previewFileName.value = resource.name || '教学资源';
|
|
|
|
|
|
previewModalVisible.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 预览课程级资源
|
|
|
|
|
|
const previewCourseResource = (resource: any) => {
|
|
|
|
|
|
if (!resource.url) {
|
|
|
|
|
|
message.warning('该资源暂无可预览的文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
previewFileUrl.value = getFileUrl(resource.url);
|
|
|
|
|
|
previewFileName.value = resource.name || '教学资源';
|
|
|
|
|
|
previewModalVisible.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
// 预览材料
|
|
|
|
|
|
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;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
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')}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
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%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 顶部工具栏
|
|
|
|
|
|
.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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.broadcast-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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
// 课程进度条
|
|
|
|
|
|
.course-progress-bar {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
|
|
|
|
|
|
.course-steps {
|
2026-03-20 10:07:17 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
:deep(.ant-steps-item-process),
|
|
|
|
|
|
:deep(.ant-steps-item-finish) {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clickable-step {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 环节进度条
|
|
|
|
|
|
.step-progress-bar {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
padding: 10px 24px;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
|
|
|
|
.progress-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.current-step-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-count {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-progress {
|
|
|
|
|
|
:deep(.ant-progress-bg) {
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
// 主内容区
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.step-display {
|
|
|
|
|
|
.step-header {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.duration-tag,
|
|
|
|
|
|
.type-tag {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
background: #FFF5EB;
|
|
|
|
|
|
color: #FF6B35;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 14px;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
margin-left: 8px;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.objective-card,
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.description-card,
|
2026-02-26 15:22:26 +08:00
|
|
|
|
.script-card,
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.resources-card {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.description-card {
|
|
|
|
|
|
background: #F6FFED;
|
|
|
|
|
|
border-left: 4px solid #52c41a;
|
|
|
|
|
|
|
|
|
|
|
|
.card-label {
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
.script-card {
|
|
|
|
|
|
background: #FFF5EB;
|
|
|
|
|
|
border-left: 4px solid #FF8C42;
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.card-label {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
color: #FF8C42;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.script-text {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.resources-card {
|
|
|
|
|
|
background: #FFFBE6;
|
|
|
|
|
|
border-left: 4px solid #FA8C16;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.card-label {
|
|
|
|
|
|
color: #FA8C16;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.resources-grid {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.resource-group {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
display: flex;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
flex-direction: column;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
gap: 8px;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.resource-type-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
text-transform: uppercase;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.resource-items {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
gap: 8px;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.resource-item {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
border-radius: 6px;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
cursor: pointer;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
font-size: 13px;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
color: #666;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2026-03-12 14:33:44 +08:00
|
|
|
|
border-color: #FF8C42;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
color: #FF8C42;
|
2026-03-12 14:33:44 +08:00
|
|
|
|
background: #FFF9F5;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 14:33:44 +08:00
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.rich-content {
|
|
|
|
|
|
:deep(p) {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
:deep(p:last-child) {
|
|
|
|
|
|
margin-bottom: 0;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
:deep(ul),
|
|
|
|
|
|
:deep(ol) {
|
|
|
|
|
|
padding-left: 24px;
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
:deep(li) {
|
|
|
|
|
|
margin-bottom: 6px;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 10:56:41 +08:00
|
|
|
|
.lesson-type-card {
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
|
background: #F5F5F5;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lesson-type-tag {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
&.图片 {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
background: #FFF3E0;
|
|
|
|
|
|
color: #FB8C00;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
&.PPT课件,
|
|
|
|
|
|
&.课件 {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
background: #E8F5E9;
|
|
|
|
|
|
color: #43A047;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
&.文档 {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:33:44 +08:00
|
|
|
|
.preparation-card {
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
|
background: #E3F2FD;
|
|
|
|
|
|
color: #1976D2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.preparation-body {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
.preparation-text {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.extension-card {
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
|
background: #F3E5F5;
|
|
|
|
|
|
color: #7B1FA2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.extension-body {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
.extension-text {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 15:22:26 +08:00
|
|
|
|
// 计时器弹窗
|
|
|
|
|
|
.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>
|