- 修复路由配置:移除 top-level await,改用手动路由配置
- 修复响应拦截器:正确解包 { code, message, data } 格式的 API 响应
- 更新开发日志和变更日志,记录浏览器功能测试结果
- 添加教师端重构设计文档
修复的问题:
1. 登录功能无法正常工作(响应数据解包问题)
2. 页面无法加载(路由配置问题)
测试结果:
- 管理员登录: ✓ 成功
- 教师登录: ✓ 成功
- 主要页面导航: ✓ 正常
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
35 KiB
35 KiB
教师端重构开发规划
创建时间:2026-03-11 基于需求:17-课程包套餐重构需求.md 参考设计:19-教师端设计-重构版.md 状态:规划中
一、重构背景
1.1 已完成工作回顾
| 端 | 状态 | 完成内容 |
|---|---|---|
| 超管端 | ✅ 完成 | 套餐管理、课程包7步流程重构、主题字典、租户授权 |
| 学校端 | ✅ 完成 | 套餐查看/续订、校本课程包管理、课程详情适配 |
| 数据模型 | ✅ 完成 | CoursePackage、CourseLesson、LessonStep等新表 |
| 后端API | ✅ 完成 | 套餐、课程包、课程、环节相关接口 |
1.2 教师端现状分析
| 模块 | 当前状态 | 需要适配 |
|---|---|---|
| 课程中心 | 基于旧结构 | ❌ 需适配新课程包结构 |
| 课程详情页 | 基于旧结构 | ❌ 需展示7步内容 |
| 备课模式 | 基于课程维度 | ⚠️ 需适配新课程结构 |
| 上课模式 | 支持整体上课 | ⚠️ 需支持选择课程 |
| 校本课程包 | 已有框架 | ✅ 基本完成 |
1.3 核心变更点
旧结构:
Course(课程包)
└─ CourseScript(逐页脚本)
└─ CourseScriptPage(页面)
└─ CourseActivity(延伸活动)
新结构:
Course(课程包)
├─ 基本信息(主题、年级、绘本、核心内容)
├─ 课程介绍(8个富文本字段)
├─ 排课计划参考(表格)
├─ 环创建设(富文本)
└─ CourseLesson(课程)
├─ INTRODUCTION(导入课)
├─ COLLECTIVE(集体课)
└─ 五大领域课(LANGUAGE/HEALTH/SCIENCE/SOCIAL/ART)
└─ LessonStep(教学环节)
二、重构目标
2.1 功能目标
| 序号 | 目标 | 优先级 |
|---|---|---|
| 1 | 课程详情页适配新结构,展示7步内容 | P0 |
| 2 | 备课模式按课程维度切换(导入课→集体课→领域课) | P0 |
| 3 | 上课模式支持选择课程上课 | P0 |
| 4 | 校本课程包功能完善 | P1 |
| 5 | 课程进度追踪 | P1 |
2.2 用户体验目标
| 目标 | 说明 |
|---|---|
| 清晰的信息层次 | 课程包→课程→环节 三级导航清晰 |
| 灵活的教学方式 | 支持整体教学和选择性上课 |
| 本地化能力 | 教师可创建校本课程包 |
三、详细开发规划
Phase 1: 基础适配(1-2天)
1.1 API类型定义更新
文件: reading-platform-frontend/src/api/course.ts
// 新增类型定义
export interface CourseLesson {
id: number;
courseId: number;
lessonType: 'INTRODUCTION' | 'COLLECTIVE' | 'LANGUAGE' | 'HEALTH' | 'SCIENCE' | 'SOCIAL' | 'ART';
name: string;
description: string | null;
duration: number;
videoPath: string | null;
videoName: string | null;
pptPath: string | null;
pptName: string | null;
pdfPath: string | null;
pdfName: string | null;
objectives: string | null;
preparation: string | null;
extension: string | null;
reflection: string | null;
assessmentData: string | null;
useTemplate: boolean;
sortOrder: number;
steps: LessonStep[];
}
export interface LessonStep {
id: number;
lessonId: number;
name: string;
content: string;
duration: number;
objective: string | null;
resourceIds: string | null;
sortOrder: number;
}
// 更新Course接口
export interface Course {
// ... 保留原有字段 ...
// 新增字段
themeId: number | null;
coreContent: string | null;
introSummary: string | null;
introHighlights: string | null;
introGoals: string | null;
introSchedule: string | null;
introKeyPoints: string | null;
introMethods: string | null;
introEvaluation: string | null;
introNotes: string | null;
scheduleRefData: string | null;
environmentConstruction: string | null;
hasCollectiveLesson: boolean;
// 新增关联
theme: Theme | null;
lessons: CourseLesson[];
}
// 新增Theme接口
export interface Theme {
id: number;
name: string;
description: string | null;
}
1.2 后端API适配
文件: reading-platform-backend/src/modules/course/course.controller.ts
// 确保课程详情API返回lessons
@Get(':id')
async getCourseDetail(@Param('id') id: string) {
return this.courseService.findById(+id, {
include: {
theme: true,
lessons: {
include: {
steps: {
orderBy: { sortOrder: 'asc' }
}
},
orderBy: { sortOrder: 'asc' }
}
}
});
}
Phase 2: 课程详情页重构(1-2天)
2.1 课程详情页布局
文件: reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue
┌──────────────────────────────────────────────────────────────────────┐
│ ◀ 返回课程中心 [收藏] [创建校本] │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ 【顶部信息卡片】 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 封面 | 名称 | 主题 | 年级 | 评分 | 使用数 | 包含课程数 | 时长 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 【Tab导航】 │
│ ┌────────┬────────┬────────┬────────┬────────┐ │
│ │ 课程介绍│ 课程内容│ 排课参考│ 环创建设│ 用户评价│ │
│ └────────┴────────┴────────┴────────┴────────┘ │
│ │
│ 【内容区域】 │
│ - 课程介绍Tab:展示8个富文本字段 │
│ - 课程内容Tab:展示导入课+集体课+领域课列表 │
│ - 排课参考Tab:展示排课计划表格 │
│ - 环创建设Tab:展示环创建设内容 │
│ - 用户评价Tab:展示用户评价 │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ [开始备课] [选择课程上课] │
│ │
└──────────────────────────────────────────────────────────────────────┘
2.2 组件结构
CourseDetailView.vue
├── CourseInfoCard.vue # 顶部信息卡片
├── CourseDetailTabs.vue # Tab导航
│ ├── CourseIntroTab.vue # 课程介绍Tab(8个富文本)
│ ├── CourseContentTab.vue # 课程内容Tab(课程列表)
│ ├── CourseScheduleTab.vue # 排课参考Tab
│ ├── CourseEnvironmentTab.vue # 环创建设Tab
│ └── CourseReviewsTab.vue # 用户评价Tab
└── ActionButtons.vue # 底部操作按钮
2.3 CourseContentTab 详细设计
<!-- 课程内容Tab - 展示所有课程 -->
<template>
<div class="course-content-tab">
<a-alert message="本课程包包含以下课程,建议按顺序教学" type="info" show-icon />
<div class="lesson-list">
<!-- 导入课 -->
<LessonCard
v-if="introductionLesson"
:lesson="introductionLesson"
:course-id="course.id"
type="导入课"
@prepare="handlePrepare"
@start-class="handleStartClass"
/>
<!-- 集体课 -->
<LessonCard
v-if="collectiveLesson"
:lesson="collectiveLesson"
:course-id="course.id"
type="集体课"
@prepare="handlePrepare"
@start-class="handleStartClass"
/>
<!-- 五大领域课 -->
<LessonCard
v-for="lesson in domainLessons"
:key="lesson.id"
:lesson="lesson"
:course-id="course.id"
:type="getLessonTypeName(lesson.lessonType)"
@prepare="handlePrepare"
@start-class="handleStartClass"
/>
</div>
<div class="action-bar">
<a-button type="primary" size="large" @click="handlePrepareAll">
整体备课(全部课程)
</a-button>
<a-button size="large" @click="handleSelectLessons">
选择课程上课
</a-button>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
course: Course;
}>();
// 按类型分组课程
const introductionLesson = computed(() =>
props.course.lessons?.find(l => l.lessonType === 'INTRODUCTION')
);
const collectiveLesson = computed(() =>
props.course.lessons?.find(l => l.lessonType === 'COLLECTIVE')
);
const domainLessons = computed(() =>
props.course.lessons?.filter(l =>
['LANGUAGE', 'HEALTH', 'SCIENCE', 'SOCIAL', 'ART'].includes(l.lessonType)
) || []
);
</script>
2.4 LessonCard 组件
<!-- 单个课程卡片 -->
<template>
<div class="lesson-card">
<div class="lesson-header">
<span class="lesson-type">{{ type }}</span>
<span class="lesson-name">{{ lesson.name }}</span>
<span class="lesson-duration">{{ lesson.duration }}分钟</span>
<a-button type="link" size="small" @click="$emit('prepare')">
[备课]
</a-button>
</div>
<div class="lesson-body">
<div class="lesson-objective">
<strong>教学目标:</strong>{{ lesson.objectives }}
</div>
<div v-if="lesson.steps?.length" class="lesson-steps">
<strong>教学环节:</strong>
<span v-for="(step, index) in lesson.steps" :key="step.id">
{{ step.name }}
{{ index < lesson.steps.length - 1 ? ' → ' : '' }}
</span>
</div>
<div v-if="type === '集体课' || type.includes('领域')" class="lesson-resources">
<strong>核心资源:</strong>
<span v-if="lesson.videoPath">动画视频</span>
<span v-if="lesson.pptPath">教学课件</span>
<span v-if="lesson.pdfPath">电子绘本</span>
</div>
</div>
<div class="lesson-footer">
<a-button type="primary" @click="$emit('start-class', lesson)">
开始上课
</a-button>
</div>
</div>
</template>
Phase 3: 备课模式重构(2-3天)
3.1 备课模式布局
文件: reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue
┌──────────────────────────────────────────────────────────────────────┐
│ 📚 备课模式:好饿的毛毛虫 [退出备课] │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌───────────────────────────────────┐ │
│ │ 【左侧:课程导航】 │ │ 【右侧:内容预览】 │ │
│ │ │ │ │ │
│ │ 📋 课程包概览 │ │ 当前选中内容的详细展示 │ │
│ │ ├─ 基本信息 │ │ │ │
│ │ ├─ 课程介绍 │ │ - 基本信息/课程介绍/排课参考 │ │
│ │ ├─ 排课计划参考 │ │ - 教学目标/教学准备 │ │
│ │ └─ 环创建设 │ │ - 教学环节列表 │ │
│ │ │ │ - 核心资源预览 │ │
│ │ 📖 包含课程 │ │ │ │
│ │ ├─ 1.导入课 │ │ │ │
│ │ │ ├─ 教学目标 │ │ │ │
│ │ │ ├─ 教学准备 │ │ │ │
│ │ │ ├─ 教学过程 │ │ │ │
│ │ │ └─ 教学反思 │ │ │ │
│ │ │ │ │ │ │
│ │ ├─ 2.集体课 ● │ │ │ │
│ │ │ ├─ 核心资源 │ │ │ │
│ │ │ ├─ 教学目标 │ │ │ │
│ │ │ ├─ 教学准备 │ │ │ │
│ │ │ ├─ 教学过程 │ │ │ │
│ │ │ │ ├─ 导入环节 │ │ │ │
│ │ │ │ ├─ 动画观看 │ │ │ │
│ │ │ │ ├─ 绘本跟读 │ │ │ │
│ │ │ │ └─ 结束环节 │ │ │ │
│ │ │ ├─ 教学延伸 │ │ │ │
│ │ │ └─ 教学反思 │ │ │ │
│ │ │ │ │ │ │
│ │ └─ 3.五大领域课... │ │ │ │
│ │ │ │ │ │
│ └─────────────────────────┘ └───────────────────────────────────┘ │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ 我的备课笔记: [保存] [清除] │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ • 备课笔记内容... │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [打印素材清单] [我已熟悉,开始上课] │
│ │
└──────────────────────────────────────────────────────────────────────┘
3.2 左侧导航组件
文件: reading-platform-frontend/src/views/teacher/courses/components/PrepareNavigation.vue
<template>
<div class="prepare-navigation">
<!-- 课程包概览 -->
<div class="nav-section">
<div class="nav-title" @click="selectSection('overview')">
📋 课程包概览
</div>
<div v-if="selectedSection === 'overview'" class="nav-items">
<div class="nav-item" @click="selectItem('basic')">基本信息</div>
<div class="nav-item" @click="selectItem('intro')">课程介绍</div>
<div class="nav-item" @click="selectItem('schedule')">排课计划参考</div>
<div class="nav-item" @click="selectItem('environment')">环创建设</div>
</div>
</div>
<!-- 包含课程 -->
<div class="nav-section">
<div class="nav-title">📖 包含课程</div>
<div class="nav-items">
<!-- 导入课 -->
<div
v-if="introductionLesson"
class="nav-lesson"
:class="{ active: selectedLessonId === introductionLesson.id }"
@click="selectLesson(introductionLesson)"
>
<div class="lesson-header">
<span class="lesson-icon">📖</span>
<span>1. 导入课</span>
</div>
<div v-if="selectedLessonId === introductionLesson.id" class="lesson-items">
<div class="nav-item" @click.stop="selectLessonItem('objectives')">教学目标</div>
<div class="nav-item" @click.stop="selectLessonItem('preparation')">教学准备</div>
<div class="nav-item" @click.stop="selectLessonItem('steps')">教学过程</div>
<div class="nav-item" @click.stop="selectLessonItem('reflection')">教学反思</div>
</div>
</div>
<!-- 集体课 -->
<div
v-if="collectiveLesson"
class="nav-lesson"
:class="{ active: selectedLessonId === collectiveLesson.id }"
@click="selectLesson(collectiveLesson)"
>
<div class="lesson-header">
<span class="lesson-icon">👥</span>
<span>2. 集体课</span>
</div>
<div v-if="selectedLessonId === collectiveLesson.id" class="lesson-items">
<div class="nav-item" @click.stop="selectLessonItem('resources')">核心资源</div>
<div class="nav-item" @click.stop="selectLessonItem('objectives')">教学目标</div>
<div class="nav-item" @click.stop="selectLessonItem('preparation')">教学准备</div>
<div class="nav-item" @click.stop="selectLessonItem('steps')">教学过程</div>
<div class="nav-item" @click.stop="selectLessonItem('extension')">教学延伸</div>
<div class="nav-item" @click.stop="selectLessonItem('reflection')">教学反思</div>
</div>
</div>
<!-- 五大领域课 -->
<div
v-for="lesson in domainLessons"
:key="lesson.id"
class="nav-lesson"
:class="{ active: selectedLessonId === lesson.id }"
@click="selectLesson(lesson)"
>
<div class="lesson-header">
<span class="lesson-icon">{{ getLessonIcon(lesson.lessonType) }}</span>
<span>{{ getLessonNumber(lesson) }}. {{ getLessonTypeName(lesson.lessonType) }}</span>
</div>
<div v-if="selectedLessonId === lesson.id" class="lesson-items">
<div class="nav-item" @click.stop="selectLessonItem('resources')">核心资源</div>
<div class="nav-item" @click.stop="selectLessonItem('objectives')">教学目标</div>
<div class="nav-item" @click.stop="selectLessonItem('preparation')">教学准备</div>
<div class="nav-item" @click.stop="selectLessonItem('steps')">教学过程</div>
<div class="nav-item" @click.stop="selectLessonItem('extension')">教学延伸</div>
<div class="nav-item" @click.stop="selectLessonItem('reflection')">教学反思</div>
</div>
</div>
</div>
</div>
</div>
</template>
3.3 右侧预览组件
文件: reading-platform-frontend/src/views/teacher/courses/components/PreparePreview.vue
<template>
<div class="prepare-preview">
<!-- 课程包概览 -->
<CourseOverviewContent
v-if="selectedType === 'overview'"
:course="course"
:selected-item="selectedItem"
/>
<!-- 教学目标 -->
<LessonObjectivesContent
v-if="selectedType === 'lesson' && selectedItem === 'objectives'"
:lesson="selectedLesson"
/>
<!-- 教学准备 -->
<LessonPreparationContent
v-if="selectedType === 'lesson' && selectedItem === 'preparation'"
:lesson="selectedLesson"
/>
<!-- 教学过程(环节列表) -->
<LessonStepsContent
v-if="selectedType === 'lesson' && selectedItem === 'steps'"
:lesson="selectedLesson"
@select-step="handleSelectStep"
/>
<!-- 单个环节详情 -->
<LessonStepDetailContent
v-if="selectedType === 'step'"
:step="selectedStep"
:lesson="selectedLesson"
/>
<!-- 教学延伸 -->
<LessonExtensionContent
v-if="selectedType === 'lesson' && selectedItem === 'extension'"
:lesson="selectedLesson"
/>
<!-- 教学反思 -->
<LessonReflectionContent
v-if="selectedType === 'lesson' && selectedItem === 'reflection'"
:lesson="selectedLesson"
/>
<!-- 核心资源 -->
<LessonResourcesContent
v-if="selectedType === 'lesson' && selectedItem === 'resources'"
:lesson="selectedLesson"
/>
</div>
</template>
Phase 4: 上课模式重构(2-3天)
4.1 课程选择弹窗
文件: reading-platform-frontend/src/views/teacher/lessons/components/SelectLessonsModal.vue
<template>
<a-modal
v-model:open="visible"
title="选择上课内容"
width="700px"
:footer="null"
>
<div class="select-lessons-modal">
<div class="course-info">
<strong>课程包:</strong>{{ course.name }}
</div>
<div class="class-info">
<strong>当前班级:</strong>{{ currentClass.name }}
</div>
<a-divider />
<!-- 推荐:整体教学 -->
<div class="section">
<h4>【推荐:整体教学】</h4>
<a-radio-group v-model:value="selectionMode">
<a-radio value="all">
<div class="radio-option">
<div class="option-title">按课程包完整教学</div>
<div class="option-desc">
按顺序完成:导入课 → 集体课 → 五大领域课
</div>
<div class="option-meta">
预计总时长:约 {{ totalDuration }} 分钟(可分多次完成)
</div>
<div class="option-hint">适合:首次教学、完整学习</div>
</div>
</a-radio>
</a-radio-group>
</div>
<!-- 灵活:选择课程 -->
<div class="section">
<h4>【灵活:选择课程】</h4>
<a-radio-group v-model:value="selectionMode">
<a-radio value="custom">
<div class="radio-option">
<div class="option-title">选择单次课程</div>
</div>
</a-radio>
</a-radio-group>
<div v-if="selectionMode === 'custom'" class="lesson-checkboxes">
<a-checkbox-group v-model:value="selectedLessonIds">
<div v-if="introductionLesson" class="lesson-checkbox-item">
<a-checkbox :value="introductionLesson.id">
<span class="lesson-name">1. 导入课 - {{ introductionLesson.name }}</span>
<span class="lesson-duration">({{ introductionLesson.duration }}分钟)</span>
</a-checkbox>
</div>
<div v-if="collectiveLesson" class="lesson-checkbox-item">
<a-checkbox :value="collectiveLesson.id">
<span class="lesson-name">2. 集体课 - {{ collectiveLesson.name }}</span>
<span class="lesson-duration">({{ collectiveLesson.duration }}分钟)</span>
</a-checkbox>
</div>
<div v-for="lesson in domainLessons" :key="lesson.id" class="lesson-checkbox-item">
<a-checkbox :value="lesson.id">
<span class="lesson-name">{{ getLessonNumber(lesson) }}. {{ getLessonTypeName(lesson.lessonType) }} - {{ lesson.name }}</span>
<span class="lesson-duration">({{ lesson.duration }}分钟)</span>
</a-checkbox>
</div>
</a-checkbox-group>
<div class="selection-summary">
已选择 {{ selectedLessonIds.length }} 节课,预计时长:{{ selectedDuration }} 分钟
</div>
</div>
</div>
<a-divider />
<div class="modal-footer">
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :disabled="!canStart" @click="handleConfirm">
开始上课
</a-button>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
const selectionMode = ref<'all' | 'custom'>('all');
const selectedLessonIds = ref<number[]>([]);
const totalDuration = computed(() => {
return props.course.lessons?.reduce((sum, l) => sum + l.duration, 0) || 0;
});
const selectedDuration = computed(() => {
return props.course.lessons
?.filter(l => selectedLessonIds.value.includes(l.id))
.reduce((sum, l) => sum + l.duration, 0) || 0;
});
const canStart = computed(() => {
if (selectionMode.value === 'all') return true;
return selectedLessonIds.value.length > 0;
});
</script>
4.2 上课模式主界面
文件: reading-platform-frontend/src/views/teacher/lessons/LessonView.vue
主要修改:
1. 支持传入课程列表(而非单个课程)
2. 添加课程进度导航
3. 添加课程内环节进度导航
4. 更新资源展示逻辑
<template>
<div class="lesson-view">
<!-- 顶部状态栏 -->
<div class="lesson-header">
<div class="timer">{{ formattedTime }}</div>
<a-button @click="handleExit">◀ 退出课堂</a-button>
<div class="lesson-title">
{{ currentCourse?.name }} - {{ currentLesson?.name }}
</div>
<div class="class-info">{{ currentClass?.name }}</div>
<a-button type="primary" danger @click="handleFinish">结束</a-button>
</div>
<!-- 课程进度 -->
<div v-if="lessons.length > 1" class="course-progress">
<a-steps :current="currentCourseIndex" size="small">
<a-step
v-for="(lesson, index) in lessons"
:key="lesson.id"
:title="getLessonShortName(lesson)"
:status="getLessonStatus(index)"
/>
</a-steps>
</div>
<!-- 环节进度 -->
<div class="step-progress">
<div class="progress-bar">
<div class="progress-label">
当前:{{ currentStep?.name }} ({{ currentStepIndex + 1 }}/{{ currentLesson?.steps?.length }})
</div>
<a-progress
:percent="stepProgressPercent"
:show-info="false"
stroke-color="#52c41a"
/>
</div>
<div class="step-indicators">
<span
v-for="(step, index) in currentLesson?.steps"
:key="step.id"
class="step-indicator"
:class="{
completed: index < currentStepIndex,
current: index === currentStepIndex
}"
>
{{ step.name }}
</span>
</div>
</div>
<!-- 资源展示区域 -->
<div class="resource-display">
<!-- 动画播放器 -->
<VideoPlayer
v-if="currentStepHasVideo"
:src="currentVideoUrl"
:poster="currentPosterUrl"
@ended="handleVideoEnded"
/>
<!-- 课件查看器 -->
<SlidesViewer
v-if="currentStepHasSlides"
:src="currentSlidesUrl"
/>
<!-- 电子绘本 -->
<EbookViewer
v-if="currentStepHasEbook"
:src="currentEbookUrl"
/>
<!-- 默认展示 -->
<div v-if="!hasResource" class="no-resource">
<a-empty description="本环节无关联资源" />
</div>
</div>
<!-- 教师指引 -->
<div class="teacher-guide">
<div class="guide-header">
💡 教师指引 - {{ currentStep?.name }} (预计{{ currentStep?.duration }}分钟)
</div>
<div class="guide-content">
<div class="guide-section">
<strong>教学目的:</strong>
{{ currentStep?.objective }}
</div>
<div class="guide-section">
<strong>操作提示:</strong>
<ul>
<li v-for="tip in getCurrentTips()" :key="tip">{{ tip }}</li>
</ul>
</div>
<div class="guide-section">
<strong>📢 指导语:</strong>
<div class="guide-script">{{ getCurrentScript() }}</div>
</div>
</div>
</div>
<!-- 工具栏 -->
<div class="toolbar">
<a-button @click="handlePause">
<template #icon><PauseOutlined /></template>
暂停
</a-button>
<a-button @click="handleAudio">
<template #icon><SoundOutlined /></template>
音频
</a-button>
<a-button @click="handleNotes">
<template #icon><EditOutlined /></template>
笔记
</a-button>
<a-button @click="handleTimer">
<template #icon><ClockCircleOutlined /></template>
计时器
</a-button>
<a-button @click="handleFullscreen">
<template #icon><FullscreenOutlined /></template>
全屏
</a-button>
</div>
<!-- 环节切换 -->
<div class="step-navigation">
<a-button
size="large"
:disabled="currentStepIndex === 0"
@click="handlePreviousStep"
>
上一环节
</a-button>
<a-button
size="large"
type="primary"
@click="handleNextStep"
>
完成此环节 →
</a-button>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
course: Course;
lessons: CourseLesson[];
initialLessonId?: number;
classId: number;
}
const props = defineProps<Props>();
const currentCourseIndex = ref(0);
const currentLesson = ref<CourseLesson | null>(null);
const currentStepIndex = ref(0);
const currentStep = computed(() => {
return currentLesson.value?.steps?.[currentStepIndex.value];
});
</script>
Phase 5: 课程进度追踪(1天)
5.1 进度记录逻辑
// 课程进度记录结构
interface CourseProgress {
id: number;
teacherId: number;
classId: number;
courseId: number;
completedLessonIds: number[]; // 已完成的课程ID列表
currentLessonId: number | null; // 当前进行到的课程
currentStepId: number | null; // 当前进行到的环节
lastSessionAt: Date;
}
// 进度API
// GET /teacher/course-progress/:courseId/:classId
// POST /teacher/course-progress
// PUT /teacher/course-progress/:id
5.2 进度恢复逻辑
// 进入上课模式时检查进度
async function loadCourseProgress(courseId: number, classId: number) {
const progress = await api.getCourseProgress(courseId, classId);
if (progress) {
// 询问是否继续上次进度
Modal.confirm({
title: '继续上次课程?',
content: `上次进行到:${getLessonName(progress.currentLessonId)},是否继续?`,
onOk: () => {
restoreProgress(progress);
},
onCancel: () => {
startNewSession();
}
});
} else {
startNewSession();
}
}
Phase 6: 校本课程包功能完善(1-2天)
6.1 创建校本课程包流程
1. 选择平台课程包
2. 选择要调整的内容(教学目标/教学过程/资源等)
3. 保存为本校版本
6.2 校本课程包数据结构
interface SchoolCourse {
id: number;
tenantId: number;
sourceCourseId: number;
name: string;
description: string | null;
createdBy: number;
changesSummary: string | null; // 调整摘要
changesData: string | null; // 调整详情JSON
usageCount: number;
status: 'ACTIVE' | 'ARCHIVED';
sourceCourse: Course;
lessons: SchoolCourseLesson[];
}
interface SchoolCourseLesson {
id: number;
schoolCourseId: number;
sourceLessonId: number;
lessonType: string;
objectives: string | null;
preparation: string | null;
extension: string | null;
reflection: string | null;
changeNote: string | null;
stepsData: string | null; // 调整后的环节数据
}
四、开发时间表
| 阶段 | 任务 | 预计时间 | 优先级 |
|---|---|---|---|
| Phase 1 | API类型定义更新、后端API适配 | 1-2天 | P0 |
| Phase 2 | 课程详情页重构 | 1-2天 | P0 |
| Phase 3 | 备课模式重构 | 2-3天 | P0 |
| Phase 4 | 上课模式重构 | 2-3天 | P0 |
| Phase 5 | 课程进度追踪 | 1天 | P1 |
| Phase 6 | 校本课程包功能完善 | 1-2天 | P1 |
| 总计 | 8-13天 |
五、测试计划
5.1 功能测试
| 测试项 | 测试内容 | 预期结果 |
|---|---|---|
| 课程详情页 | 展示新结构内容 | 正确展示7步内容 |
| 备课模式 | 按课程维度切换 | 左侧导航正确展开课程 |
| 上课模式 | 选择课程上课 | 可选择单次或整体上课 |
| 进度追踪 | 保存和恢复进度 | 下次进入时可继续上次进度 |
5.2 兼容性测试
- 旧课程包数据正确显示
- 无课程数据的课程包不报错
- 各类课程(导入/集体/领域)正确展示
六、风险与注意事项
6.1 技术风险
| 风险 | 应对措施 |
|---|---|
| 旧数据兼容 | 保留旧字段,渐进式迁移 |
| 性能问题 | 课程数据量大时使用分页/虚拟滚动 |
| 状态管理复杂 | 使用Pinia管理备课/上课状态 |
6.2 用户体验风险
| 风险 | 应对措施 |
|---|---|
| 信息过载 | 默认折叠,按需展开 |
| 操作复杂 | 提供引导和默认选项 |
| 兼容性困惑 | 旧版课程标识"经典版",新版标识"完整版" |
七、后续优化方向
- 离线备课/上课支持
- 课程分享功能
- 教学数据分析
- AI辅助备课建议
- 家长端联动(学生表现反馈)
本文档创建于 2026-03-11 基于需求文档:17-课程包套餐重构需求.md