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

35 KiB
Raw Blame History

教师端重构开发规划

创建时间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      # 课程介绍Tab8个富文本
│   ├── 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