diff --git a/src/api/admin.js b/src/api/admin.js index 1074445..d3ff337 100644 --- a/src/api/admin.js +++ b/src/api/admin.js @@ -28,8 +28,14 @@ export function importExcel(formData) { // 注意:不要手动设置 Content-Type。axios/浏览器在发送 FormData 时会自动补上 // `multipart/form-data; boundary=...`;手动写死 'multipart/form-data'(无 boundary) // 会导致后端 getFileStream 解析失败(Missing boundary)-> 500,且永不落库。 + // + // silent: true —— 抑制响应拦截器的通用错误提示。本接口的**失败提示由调用方负责**: + // 后端刻意只回传纯错误(admin.js importExcel catch 注释:「前端已统一加 + // 「Excel 导入失败:」前缀,这里只回传纯错误,避免重复前缀」),故此处必须静默, + // 否则会与 Admin.vue 的「Excel 导入失败:<原因>」构成同族双弹。 return request.post('/admin/import-excel', formData, { timeout: 60000, + silent: true, }) } @@ -134,8 +140,11 @@ export function setQuestions(group, questionType, data) { */ export function importQuestionsExcel(group, formData) { // 同上:不要手动设置 Content-Type,避免缺失 boundary 导致后端解析 500。 + // silent: true —— 同 importExcel:失败提示由调用方(Admin.vue onExcelFileChange) + // 以「Excel 导入失败:<原因>」形式统一给出,此处静默以避免同族双弹。 return request.post(`/admin/exam/${group}/questions/import-excel`, formData, { timeout: 120000, + silent: true, }) } diff --git a/src/views/Admin.vue b/src/views/Admin.vue index 30013e9..36ce214 100644 --- a/src/views/Admin.vue +++ b/src/views/Admin.vue @@ -1532,6 +1532,18 @@ * 6. 成绩管理 - 发布/撤回/导出/批量下载 * 7. 新闻管理 - CRUD + 置顶/分组 * 8. 轮播管理 - 添加/删除/排序/启用 + * + * 【错误提示口径(R64 收敛)—— 改本文件前必读】 + * 本文件**不在 catch 里弹错误提示**:请求失败的业务码 / HTTP 码提示, + * 一律由 `src/api/request.js` 的响应拦截器**统一给出**(它已覆盖全部失败路径: + * 业务码≠0、403/404/500、超时、网络异常 —— 每种都会弹、且只弹一次)。 + * 若在 catch 里再 `ElMessage.error(...)`,就是**同族双弹**:用户同时看到两条相同报错。 + * ⇒ 例外仅有三类,出现时**必须显式注明原因**: + * ① 该请求已带 `silent: true` ⇒ 调用方即唯一提示方 + * (如两个导入接口需加「Excel 导入失败:」前缀 —— 见后端 admin.js 注释 + * 「前端已统一加前缀,这里只回传纯错误,避免重复前缀」); + * ② 客户端校验(**根本没发请求**,如文件类型/大小、分值范围); + * ③ 非 axios 路径(`fetchBlobGuarded` / el-upload 的 on-success 手工判响应)。 */ import { ref, reactive, onMounted, watch, computed, nextTick, defineAsyncComponent, h } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' @@ -1677,10 +1689,10 @@ async function handleToggleTestFlag(row, val) { await adminApi.toggleTestFlag(row.uid, val) const label = val === 1 ? '测试数据' : '生产数据' ElMessage.success(`选手 ${row.name} 已标记为${label}`) - } catch (e) { + } catch { // 回滚开关状态 row.testFlag = val === 1 ? 0 : 1 - ElMessage.error('标记失败:' + (e.message || '未知错误')) + // 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹) } } @@ -2064,8 +2076,8 @@ async function saveBigQuestion() { } bigDialogVisible.value = false await fetchBigQuestions() - } catch (e) { - ElMessage.error(e?.message || '保存失败') + } catch { + // 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹) } finally { bigSaving.value = false } @@ -2087,9 +2099,9 @@ async function deleteBigQuestion(row) { await fetchBigQuestions() // 已删除的行可能仍被勾选(reserve-selection 会保留),导出前清掉避免请求到已删 ID clearBigSelection() - } catch (e) { - // 409:该题已被抽取,服务端拒绝删除;给出可执行的处理建议 - ElMessage.error(e?.message || '删除失败') + } catch { + // 409:该题已被抽取,服务端拒绝删除 —— 后端给出的可执行处理建议 + // 由响应拦截器统一展示,此处不再重复弹窗(同族双弹) } } @@ -2322,8 +2334,8 @@ async function saveBigScores() { // 一次刷新全部相关列表:成绩明细(总分 = 客观分 + 大题分)+ 大题作答 + 考生答题记录。 // 原实现只调 fetchBigAnswers(),测试人员看到另一处仍是旧分只能刷整页。 await refreshScorePageData(true) - } catch (e) { - ElMessage.error(e?.message || '评分保存失败') + } catch { + // 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹) } finally { bigScoreSaving.value = false } @@ -2430,7 +2442,7 @@ async function saveQuestion() { ElMessage.success('保存成功') questionDialogVisible.value = false fetchQuestions() - } catch (e) { ElMessage.error('保存失败') } finally { questionSaving.value = false } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } finally { questionSaving.value = false } } async function deleteQuestion(row, qType) { @@ -2624,7 +2636,7 @@ async function onMaterialUpload(file) { ElMessage.success('资料上传成功') materialsPage.value = 1 fetchAdminMaterials() - } catch (e) { ElMessage.error('上传失败') } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } } function onMaterialsGroupChange() { @@ -2764,7 +2776,7 @@ async function batchSaveWorkScores() { await fetchSubmissions() // R29:「考生答题记录」的组别已改跟随成绩管理页(scoreExportGroup) if (submissionGroup.value === scoreExportGroup.value) fetchExamSubmissions() - } catch (e) { ElMessage.error('保存失败') } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } } // ==================== 5. 时间设置 ==================== @@ -2816,7 +2828,7 @@ async function saveTimes() { jiaoshi: { start: timesForm.jiaoshiStart, end: timesForm.jiaoshiEnd, deadline: timesForm.jiaoshiDeadline, choiceCount: timesForm.jiaoshiChoiceCount, judgeCount: timesForm.jiaoshiJudgeCount }, }) ElMessage.success('时间设置保存成功') - } catch (e) { ElMessage.error('保存失败') } finally { timesSaving.value = false } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } finally { timesSaving.value = false } } // ==================== 6. 成绩管理 ==================== @@ -2919,7 +2931,7 @@ async function togglePublish() { await adminApi.publishScores(group, !current) scoresPublishedMap[group] = !current ElMessage.success(!current ? '成绩已发布' : '成绩已撤回') - } catch (e) { ElMessage.error('操作失败') } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } } async function exportExcel() { @@ -3060,7 +3072,7 @@ async function saveNews() { ElMessage.success('保存成功') newsDialogVisible.value = false fetchAdminNews() - } catch (e) { ElMessage.error('保存失败') } finally { newsSaving.value = false } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } finally { newsSaving.value = false } } async function deleteAdminNews(row) { @@ -3078,9 +3090,9 @@ async function togglePinned(row, val) { await adminApi.updateNews(row.id, { pinned: val }) row.pinned = val ElMessage.success(val ? '已置顶' : '已取消置顶') - } catch (e) { + } catch { row.pinned = !val - ElMessage.error('更新失败') + // 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹) } } @@ -3100,7 +3112,7 @@ async function saveNewsOrder() { fetchAdminNews() } catch (e) { console.error('新闻排序保存失败:', e) - ElMessage.error('排序保存失败,请刷新重试') + // 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹) } } @@ -3258,7 +3270,7 @@ async function saveCarousel() { ElMessage.success('保存成功') carouselDialogVisible.value = false fetchCarousels() - } catch (e) { ElMessage.error('保存失败') } finally { carouselSaving.value = false } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } finally { carouselSaving.value = false } } async function deleteCarouselItem(row) { @@ -3273,7 +3285,7 @@ async function deleteCarouselItem(row) { async function toggleCarousel(row) { try { await adminApi.updateCarousel(row.id, { enabled: row.enabled }) - } catch (e) { ElMessage.error('更新失败') } + } catch { /* 失败提示由响应拦截器统一给出,不再重复弹窗(同族双弹) */ } } /** 手动保存轮播排序 */ @@ -3287,7 +3299,7 @@ async function saveCarouselOrder() { ElMessage.success('排序保存成功') } catch (e) { console.error('轮播排序保存失败:', e) - ElMessage.error('排序保存失败,请刷新重试') + // 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹) } }