kindergarten_java/docs/design/22-教师端重构开发规划.md
Claude Opus 4.6 7e625f31e3 fix: 修复前端路由配置和响应拦截器问题
- 修复路由配置:移除 top-level await,改用手动路由配置
- 修复响应拦截器:正确解包 { code, message, data } 格式的 API 响应
- 更新开发日志和变更日志,记录浏览器功能测试结果
- 添加教师端重构设计文档

修复的问题:
1. 登录功能无法正常工作(响应数据解包问题)
2. 页面无法加载(路由配置问题)

测试结果:
- 管理员登录: ✓ 成功
- 教师登录: ✓ 成功
- 主要页面导航: ✓ 正常

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

999 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 教师端重构开发规划
> 创建时间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`
```typescript
// 新增类型定义
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`
```typescript
// 确保课程详情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 # 课程介绍Tab8个富文本
│ ├── CourseContentTab.vue # 课程内容Tab课程列表
│ ├── CourseScheduleTab.vue # 排课参考Tab
│ ├── CourseEnvironmentTab.vue # 环创建设Tab
│ └── CourseReviewsTab.vue # 用户评价Tab
└── ActionButtons.vue # 底部操作按钮
```
#### 2.3 CourseContentTab 详细设计
```vue
<!-- 课程内容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 组件
```vue
<!-- 单个课程卡片 -->
<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`
```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`
```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`
```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. 更新资源展示逻辑
```
```vue
<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 进度记录逻辑
```typescript
// 课程进度记录结构
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 进度恢复逻辑
```typescript
// 进入上课模式时检查进度
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 校本课程包数据结构
```typescript
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*