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;