- 修复路由配置:移除 top-level await,改用手动路由配置
- 修复响应拦截器:正确解包 { code, message, data } 格式的 API 响应
- 更新开发日志和变更日志,记录浏览器功能测试结果
- 添加教师端重构设计文档
修复的问题:
1. 登录功能无法正常工作(响应数据解包问题)
2. 页面无法加载(路由配置问题)
测试结果:
- 管理员登录: ✓ 成功
- 教师登录: ✓ 成功
- 主要页面导航: ✓ 正常
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
999 lines
35 KiB
Markdown
999 lines
35 KiB
Markdown
# 教师端重构开发规划
|
||
|
||
> 创建时间: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 # 课程介绍Tab(8个富文本)
|
||
│ ├── 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*
|