根据后端字段:
Assignment.examMode: 是否为考试模式Assignment.examDuration: 考试时长(分钟)EngagementVO.submitted: 是否已交卷功能要求:
| 文件 | 修改内容 |
|---|---|
src/hooks/useExamCountdown.ts |
新建 - 考试倒计时 hook |
src/views/HomeworkPage/component/HomeworkDetail.vue |
显示考试模式标识、已提交则禁用开始按钮 |
src/views/EditPage/EditPage.vue |
集成倒计时横幅、已提交则跳转离开 |
src/views/Home/component/Edit/index.vue |
考试模式提交确认提示 |
文件: 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
};
}
文件: src/views/HomeworkPage/component/HomeworkDetail.vue
在 IAssignment 类型已包含 examMode 和 examDuration,无需修改接口
<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>
// 计算属性:判断作业是否应该禁用
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 ''
})
<el-button
style="color: #FFFFFF;"
color="#7ADBB0"
@click="handleStart()"
:disabled="isAssignmentDisabled"
:title="getButtonTooltip"
>
开始{{ data.type }}
</el-button>
文件: src/views/EditPage/EditPage.vue
const dataForm = reactive<{
messages: IDialogue[],
dialogueId: string;
engagement: engagementVO;
assignment: IAssignment; // 新增
}>({
messages: [],
dialogueId: '',
engagement: {} as engagementVO,
assignment: {} as IAssignment
})
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)
<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>
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')
})
文件: 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;
}
}
文件: 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 |
- | 提交作业 |
后端需确保:
Assignment 返回中包含 examMode 和 examDurationEngagementVO 包含 submitted 字段