plan_exam_mode_student.md 10 KB

学生端考试模式实现计划(修订版)

一、需求回顾

根据后端字段:

  • Assignment.examMode: 是否为考试模式
  • Assignment.examDuration: 考试时长(分钟)
  • EngagementVO.submitted: 是否已交卷

功能要求:

  1. 作业详情页显示考试模式标识
  2. 写作页面实时显示考试倒计时
  3. 结束前5分钟提示学生暂存内容
  4. 倒计时结束时自动暂存并提交
  5. 提交时提示"提交后不可再修改"
  6. 考试模式已提交学生不可再进入写作页面
  7. 教师端无需隐藏批改区域

二、涉及文件清单

文件 修改内容
src/hooks/useExamCountdown.ts 新建 - 考试倒计时 hook
src/views/HomeworkPage/component/HomeworkDetail.vue 显示考试模式标识、已提交则禁用开始按钮
src/views/EditPage/EditPage.vue 集成倒计时横幅、已提交则跳转离开
src/views/Home/component/Edit/index.vue 考试模式提交确认提示

三、详细实现计划

3.1 新建考试倒计时 Hook

文件: src/hooks/useExamCountdown.ts

import { ref, computed, onUnmounted } from 'vue';

interface UseExamCountdownOptions {
  startTime: Date;
  duration: number;
  onAutoSubmit: () => void;
  onWarning: () => void;
}

export function useExamCountdown(options: UseExamCountdownOptions) {
  const { startTime, duration, onAutoSubmit, onWarning } = options;

  const endTime = new Date(startTime.getTime() + duration * 60 * 1000);
  const remainingSeconds = ref(0);
  const isWarning = ref(false);
  const isExpired = ref(false);
  let intervalId: number | null = null;
  let warningTriggered = false;

  const formattedTime = computed(() => {
    const hours = Math.floor(remainingSeconds.value / 3600);
    const minutes = Math.floor((remainingSeconds.value % 3600) / 60);
    const seconds = remainingSeconds.value % 60;
    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  });

  const tick = () => {
    const now = new Date();
    const diff = Math.floor((endTime.getTime() - now.getTime()) / 1000);

    if (diff <= 0) {
      remainingSeconds.value = 0;
      isExpired.value = true;
      isWarning.value = false;
      stop();
      onAutoSubmit();
      return;
    }

    remainingSeconds.value = diff;

    if (diff <= 300 && !warningTriggered) {
      isWarning.value = true;
      warningTriggered = true;
      onWarning();
    }
  };

  const start = () => {
    tick();
    intervalId = window.setInterval(tick, 1000);
  };

  const stop = () => {
    if (intervalId !== null) {
      clearInterval(intervalId);
      intervalId = null;
    }
  };

  onUnmounted(() => {
    stop();
  });

  return {
    remainingSeconds,
    isWarning,
    isExpired,
    formattedTime,
    start,
    stop
  };
}

3.2 作业详情页修改

文件: src/views/HomeworkPage/component/HomeworkDetail.vue

3.2.1 数据模型修改

IAssignment 类型已包含 examModeexamDuration,无需修改接口

3.2.2 模板添加考试模式标识

<div class="item">
  <span style="font-weight: 600">截止日期: </span>
  {{new Date(Date.parse(data.endTime)).toLocaleString()}}
</div>
<!-- 新增:考试模式标识 -->
<div v-if="data.examMode" class="item">
  <span style="font-weight: 600">作业模式: </span>
  <el-tag type="warning">考试模式</el-tag>
  <span v-if="data.examDuration" style="margin-left: 10px; color: #909399;">
    时长: {{ data.examDuration }}分钟
  </span>
</div>

3.2.3 开始按钮逻辑修改

// 计算属性:判断作业是否应该禁用
const isAssignmentDisabled = computed(() => {
  const now = new Date()
  const startTime = new Date(data.startTime)
  const endTime = new Date(data.endTime)

  if (now < startTime) {
    return true
  }

  if (now > endTime) {
    if (engagementStatus.value) {
      return engagementStatus.value.status !== 'NOT_SUBMITTED'
    }
    return false
  }

  // 新增:考试模式且已提交,禁止再次进入
  if (data.examMode && engagementStatus.value?.submitted) {
    return true
  }

  return false
})

// 新增:按钮提示文本
const getButtonTooltip = computed(() => {
  // ... 现有逻辑 ...

  // 新增:考试模式已提交提示
  if (data.examMode && engagementStatus.value?.submitted) {
    return '您已完成交卷,无法再次编辑'
  }

  return ''
})

3.2.4 开始按钮模板修改

<el-button
  style="color: #FFFFFF;"
  color="#7ADBB0"
  @click="handleStart()"
  :disabled="isAssignmentDisabled"
  :title="getButtonTooltip"
>
  开始{{ data.type }}
</el-button>

3.3 写作页面修改

文件: src/views/EditPage/EditPage.vue

3.3.1 数据模型扩展

const dataForm = reactive<{
  messages: IDialogue[],
  dialogueId: string;
  engagement: engagementVO;
  assignment: IAssignment;  // 新增
}>({
  messages: [],
  dialogueId: '',
  engagement: {} as engagementVO,
  assignment: {} as IAssignment
})

3.3.2 页面加载时校验

import { ElMessage } from 'element-plus'

onMounted(async () => {
  await fetchData()

  // 新增:考试模式已提交则不允许进入
  if (isExamMode.value && dataForm.engagement.submitted) {
    ElMessage.warning('您已完成交卷,无法再次编辑')
    router.push(`/home/homeworkDetail/${assignmentId}`)
    return
  }

  if (isExamMode.value) {
    startCountdown()
  }

  registerAllEventListeners(indexedDB, store.user.id, assignmentId)
})

// 计算属性
const isExamMode = computed(() => dataForm.assignment?.examMode ?? false)

3.3.3 模板添加倒计时横幅

<div class="my-work" >
  <div class="my-work-slider">
    <el-button class="back" ...>返回</el-button>
    <AIChatter ... />
    <Dictionaries />
  </div>

  <!-- 考试模式倒计时横幅 -->
  <div v-if="isExamMode" class="exam-countdown-banner">
    <el-tag type="warning">考试模式</el-tag>
    <span class="countdown-time">{{ formattedTime }}</span>
    <el-tag v-if="isWarning" type="danger" effect="plain">
      距离结束还有5分钟,请及时暂存内容!
    </el-tag>
  </div>

  <div class="my-work-input">
    <Edit :engagement="dataForm.engagement" :is-exam-mode="isExamMode" />
  </div>

  <Slider :dialogueId="dataForm.dialogueId" :engagement="dataForm.engagement"/>
</div>

3.3.4 倒计时集成

import { useExamCountdown } from '@/hooks/useExamCountdown'

const handleStage = async () => {
  emitter.emit('get-html')
  emitter.emit('get-text')
  emitter.emit('get-quill-content')
  // 实际暂存逻辑...
}

const handleSubmit = async () => {
  // 实际提交逻辑...
}

const { remainingSeconds, isWarning, formattedTime, start: startCountdown, stop: stopCountdown } = useExamCountdown({
  startTime: new Date(dataForm.assignment.startTime),
  duration: dataForm.assignment.examDuration || 60,
  onAutoSubmit: async () => {
    ElMessage.warning('考试时间已到,正在自动提交...')
    await handleStage()
    await handleSubmit()
  },
  onWarning: () => {
    ElMessage.info('距离考试结束还有5分钟,请注意暂存您的内容!')
  }
})

onUnmounted(() => {
  stopCountdown()
  removeAllEventListeners()
  emitter.off('save-ai-dialogue-before-submit')
})

3.3.5 样式添加

文件: src/views/EditPage/index.scss

.exam-countdown-banner {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 12px 24px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  font-size: 16px;
  justify-content: center;

  .countdown-time {
    font-size: 28px;
    font-weight: bold;
    font-family: 'Monaco', 'Menlo', monospace;
    letter-spacing: 2px;
  }
}

3.4 编辑组件提交确认

文件: src/views/Home/component/Edit/index.vue

interface IEditProps {
  engagement: engagementVO
  isExamMode?: boolean
}
defineProps<IEditProps>()

const props = defineProps<{
  engagement: engagementVO
  isExamMode?: boolean
}>()

const handleSubmit = async () => {
  // 考试模式:强制确认
  if (props.isExamMode) {
    try {
      await ElMessageBox.confirm(
        '考试模式下提交后将无法再次修改,您确定要提交吗?',
        '确认交卷',
        {
          confirmButtonText: '确认提交',
          cancelButtonText: '取消',
          type: 'warning',
        }
      )
    } catch {
      return
    }
  }

  // 执行提交逻辑...
}

四、关键差异对照表

功能 普通作业模式 考试模式
暂存按钮 ✅ 显示 ✅ 显示
AI助手 ✅ 显示 ✅ 显示
词典 ✅ 显示 ✅ 显示
批改区域 ✅ 显示 ✅ 显示(教师端不隐藏)
倒计时 ❌ 不显示 ✅ 显示
提交确认 有"不可修改"提示
5分钟警告 有暂存提示
自动交卷 倒计时结束自动暂存并提交
已提交禁止再进入 ✅ 有

五、实施顺序

顺序 文件 说明
1 src/hooks/useExamCountdown.ts 新建倒计时 hook
2 src/views/EditPage/index.scss 倒计时横幅样式
3 src/views/HomeworkPage/component/HomeworkDetail.vue 模式标识 + 已提交禁用
4 src/views/EditPage/EditPage.vue 集成倒计时 + 提交拦截
5 src/views/Home/component/Edit/index.vue 提交确认提示

六、后端接口需求

无需新增接口,现有 API 已足够:

接口 返回数据 用途
GET /api/assignment/byAssignment/{id} examMode, examDuration 获取作业考试配置
GET /api/assignment/engage submitted 获取学生交卷状态
PUT /api/assignment/engage/stage - 暂存作业内容
PUT /api/assignment/engage/submit - 提交作业

后端需确保

  1. Assignment 返回中包含 examModeexamDuration
  2. EngagementVO 包含 submitted 字段
  3. 提交接口校验考试模式只能提交一次