From 39d3da0c932b3ce011671f025d03f22daac91eba Mon Sep 17 00:00:00 2001 From: qubiaobiao <3294694717@qq.com> Date: Wed, 30 Sep 2026 15:46:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(exam):=20=E9=80=89=E6=8B=A9=E9=A2=98?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E7=AD=89=E5=AE=BD=E5=B1=85=E4=B8=AD=20+=20?= =?UTF-8?q?=E6=88=90=E7=BB=A9=E5=8F=91=E5=B8=83=E9=97=A8=E6=8E=A7=E5=8A=A0?= =?UTF-8?q?=E5=9B=BA=20+=20=E5=B7=B2=E4=BA=A4=E5=8D=B7=E7=A6=81=E6=AD=A2?= =?UTF-8?q?=E5=86=8D=E8=BF=9B=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Competition.vue | 39 +++++++++++++++++++++++++------ src/views/Dashboard.vue | 48 ++++++++++++++++++++++++++++++++------- src/views/MockExam.vue | 15 ++++++++++++ 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/views/Competition.vue b/src/views/Competition.vue index 49eee51..2fc9e4b 100644 --- a/src/views/Competition.vue +++ b/src/views/Competition.vue @@ -980,9 +980,10 @@ onMounted(async () => { loading.value = true try { - // 统一入口校验:考试组(中职/高职)与提交组(本科/教师)一致—— - // 仅「截止后(status==='ended')」禁止进入;考试时间内(含已提交 / 意外退出)均可重新进入, - // 应对断网、断电、误操作导致的意外退出(重进后由下方逻辑恢复已答题目)。 + // 统一入口校验: + // · 「截止后(status==='ended')」一律禁止进入; + // · 考试组(中职/高职)「已正式交卷」禁止再进入(见下方 #3 判定); + // · 其余情况(未交卷的意外退出)可重进,由下方逻辑恢复已答题目。 const status = await examApi.getExamStatus(group) if (status.status === 'ended') { ElMessage.warning(isExam ? '考试已结束,无法再次进入' : '作品提交已截止,无法再次进入') @@ -990,10 +991,18 @@ onMounted(async () => { return } + // 🔴 考试组(中职/高职)为一次性答题:**正式交卷后禁止再进入**(测试反馈 #3)。 + // 只拦「已交卷」(submitted 仅在 submit_time 落库后为 true);未交卷的意外退出 + // (断网/断电/误操作)仍可重进并恢复作答,保留原有的容错能力。 + // 提交组(本科/教师)不受此限,截止前可重新进入重传作品。 + if (isExam && status.submitted) { + ElMessage.warning('您已提交试卷,无法再次进入考试') + router.push('/dashboard') + return + } + if (isExam) { - // 截止前可随意重新进入(含已提交 / 意外退出后重进),与顶部注释一致; - // 仅「截止后(status==='ended')」禁止进入(见上方判断)。考试组仍受团队锁约束, - // 同一时刻仅一名成员在考,避免重复进入互相干扰。 + // 考试组进入前仍受团队锁约束:同一时刻仅一名成员在考,避免重复进入互相干扰。 // 考试组(中职/高职):检查团队锁与活跃状态后进入考试 if (status.teamLocked) { ElMessage.warning(`您的小组已由 ${status.teamLockedBy || '队友'} 进入考试,不能重复进入`) @@ -1293,15 +1302,31 @@ onUnmounted(() => { .options-group { display: flex; flex-direction: column; + /* 覆盖 Element Plus `.el-radio-group{align-items:center}`:该声明会让各子项 + 按自身内容宽度收缩并居中,导致同题选项框宽窄不一(测试反馈 #1)。 + 显式 stretch 后子项统一撑满容器宽度。 */ + align-items: stretch; gap: 0.75rem; - width: 100%; + /* 以「字符最长的选项」的边框宽度为统一宽度:容器收缩到最长子项(max-content), + 再整体水平居中;文字在框内左对齐。 */ + width: max-content; + max-width: 100%; + margin: 0 auto; } .option-item { + width: 100%; + box-sizing: border-box; + text-align: left; padding: 0.6rem 1rem; border: 1px solid #e4e7ed; border-radius: 6px; transition: border-color 0.2s; } +/* 超长选项不得撑破卡片:允许换行,避免 max-content 溢出 */ +.options-group :deep(.el-radio__label) { + white-space: normal; + overflow-wrap: anywhere; +} .option-item:hover { border-color: #409eff; } diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index 42eeff4..18e3774 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -18,16 +18,20 @@

考试已结束,无法再次进入

-

+

+ 您已提交试卷,本次考试已结束,无法再次进入。 +

+

您已提交作品,截止前可重新提交。

+

{{ isUploadGroup ? '点击下方按钮进入比赛页面,请确保网络稳定,准备好后开始提交作品。' : '点击下方按钮进入比赛页面,请确保网络稳定,准备好后开始答题。' }}

-

请关注比赛时间安排

+

请关注比赛时间安排

{{ enterBtnText }} @@ -162,23 +166,43 @@ const isUploadGroup = computed(() => { return g === 'benke' || g === 'jiaoshi' }) -const noScore = computed(() => !scoreData.value || scoreData.value.score === undefined || scoreData.value.score === null) +// 🔴 成绩发布门控(铁律15):**必须**同时满足 published === true 才渲染分数。 +// 历史实现只判 score 是否为 null —— 这与后端「未发布则 score 置 null」的约定形成 +// 隐式耦合:一旦后端任一通道在未发布状态下带上数值 score,本卡片即直接泄露。 +// Score.vue 早已用 `v-if="scoreData && scoreData.published"` 同源判定,此处对齐, +// 形成「两条展示通道同一判据」的纵深防御。 +const noScore = computed(() => + !scoreData.value || + scoreData.value.published !== true || + scoreData.value.score === undefined || + scoreData.value.score === null +) const scoreMessage = computed(() => { if (contestStatus.value === 'ended') return '成绩暂未发布,请关注公告' return '暂无成绩,请耐心等待成绩发布后查看' }) -// 进入比赛按钮文案:截止后→「已结束」;已提交→ 提交组「重新提交」/考试组「继续考试」; -// 其余→「进入比赛」。考试组(中职/高职)已提交也可在截止前重进(应对意外退出)。 +// 进入比赛按钮文案: +// 截止后 → 「已结束」(禁用) +// 考试组 + 已提交 → 「已提交」(禁用,不可再进入;测试反馈 #3) +// 提交组 + 已提交 → 「重新提交」(截止前可重传,用户既定需求) +// 其余 → 「进入比赛」 +// ⚠️ 考试组(中职/高职)为一次性答题:正式交卷后不允许重进(防改答案再交卷)。 +// 未交卷的意外退出(断网/断电)不受影响——submitted 仅在 submit_time 落库后为 true。 const enterBtnText = computed(() => { if (contestStatus.value === 'ended') return '已结束' if (hasSubmitted.value) { - return isUploadGroup.value ? '重新提交' : '继续考试' + return isUploadGroup.value ? '重新提交' : '已提交' } return '进入比赛' }) +// 考试组已交卷 ⇒ 禁止再次进入 +const enterDisabled = computed(() => + contestStatus.value === 'ended' || (hasSubmitted.value && !isUploadGroup.value) +) + // 在线答题卡片右上角状态标签:随「考试窗口状态 + 本人是否提交」联动 // 后端 getExamStatus 返回 status ∈ {not_set, not_started, in_progress, ended}, // 此前前端误判 === 'open' 导致活动期恒定显示「未开始」;现统一在此映射并纳入已提交态。 @@ -212,10 +236,18 @@ function formatDate(dateStr) { async function enterContest() { // 所有组别统一:仅考试窗口内(isActive)允许进入;截止后(ended)统一禁止。 - // 考试组/提交组即便已提交,也可在截止前重新进入(应对断网/断电/误操作退出)。 + // 提交组(本科/教师)即便已提交,也可在截止前重新进入(应对断网/断电/误操作退出)。 + // 考试组(中职/高职)为一次性答题:**正式交卷后禁止再进入**(测试反馈 #3), + // 退出重进只对「未交卷」的情况开放。 try { const group = authStore.userGroup || 'zhongzhi' + const isExam = group === 'zhongzhi' || group === 'gaozhi' const data = await examApi.getExamStatus(group) + if (isExam && data.submitted) { + hasSubmitted.value = true + ElMessage.warning('您已提交试卷,无法再次进入考试') + return + } if (!data.isActive) { ElMessage.warning(data.message || (data.status === 'ended' ? '考试已结束,无法进入' : '考试未在进行中,请等待考试开始')) return diff --git a/src/views/MockExam.vue b/src/views/MockExam.vue index 137fd4b..60519f5 100644 --- a/src/views/MockExam.vue +++ b/src/views/MockExam.vue @@ -811,15 +811,30 @@ onUnmounted(() => { .options-group { display: flex; flex-direction: column; + /* 与 Competition.vue 同款修正(测试反馈 #1):覆盖 Element Plus + `.el-radio-group{align-items:center}`,否则各选项按自身内容宽度收缩居中、宽窄不一。 */ + align-items: stretch; gap: 12px; + /* 以最长选项的边框宽度为统一宽度,整块居中,文字左对齐 */ + width: max-content; + max-width: 100%; + margin: 0 auto; } .option-item { + width: 100%; + box-sizing: border-box; + text-align: left; padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 8px; transition: background 0.2s; } .option-item:hover { border-color: #409eff; } +/* 超长选项不得撑破卡片:允许换行,避免 max-content 溢出 */ +.options-group :deep(.el-radio__label) { + white-space: normal; + overflow-wrap: anywhere; +} .option-label { font-weight: 600; color: #409eff;