fix(ui): 其余视图错误提示收敛为「拦截器统一弹」,消除同族双弹
承接 R64(Admin.vue)已定口径,本轮覆盖其余全部视图。 根因:src/api/request.js 响应拦截器已覆盖全部失败路径,且每种失败只弹一次 ⇒ 调用方 catch 内再 ElMessage.error 即必然双弹。helper.error 不改 HTTP 状态, 故删除调用方提示不会丢后端文案。 删除 11 处重复提示(跨 5 文件): - Login.vue 管理员登录 catch(adminLogin 未传 silent ⇒ 拦截器必弹) - Activate.vue handleStep1 / handleActivate / resendSms - ForgotPassword.vue handleStep1 / handleReset / resendSms - Competition.vue submitUploadFiles / deleteUploadedWork / onMounted 外层 catch - Dashboard.vue enterContest(warning 形态,同族) 刻意保留(已有显式理由,不得误删): - silent 路径调用方唯一提示:Login 选手登录、Competition 大题加载/保存/交卷/自动交卷 - 客户端校验(未发请求)、原生 fetch(MockExam 样例模板)、独立 axios 实例(Materials)、 el-upload on-success(Admin) 另: - src/api/request.js 增补「错误提示口径」权威块(全站单一源 + 四类例外), 五个视图各加一行口径指针注释,防回退。 - 无行为变更:仅移除重复提示,finally/return/refreshCaptcha/router.push 全部保留。
This commit is contained in:
@@ -148,6 +148,21 @@ export function initProactiveTokenRefresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 错误提示口径(R64/R65 收敛 · 全站唯一权威) ====================
|
||||||
|
// 🔴 本拦截器已覆盖**全部**失败路径,且每种失败**只弹一次**:
|
||||||
|
// 业务码 ≠0/≠200 → `res.message`;403/404/500 → 固定文案;其它 HTTP 码 →
|
||||||
|
// `data.message`;超时(ECONNABORTED);无 response 的网络层错误。
|
||||||
|
// ⇒ **调用方 catch 内不要再 `ElMessage.error(...)`**,否则用户同时看到两条 = 同族双弹。
|
||||||
|
// ⇒ 已按此口径收敛:Admin.vue、Login.vue、Activate.vue、ForgotPassword.vue、
|
||||||
|
// Competition.vue、Dashboard.vue。
|
||||||
|
// 例外(**必须**由调用方负责提示,出现时须显式注明原因):
|
||||||
|
// ① 请求带 `silent: true` ⇒ 拦截器不说话,调用方是唯一提示方
|
||||||
|
// (登录 / 交卷 / 逐题保存 / 大题加载与保存 / 两个 Excel 导入 / resetTestData);
|
||||||
|
// ② 客户端校验失败(**根本没发请求**);
|
||||||
|
// ③ 非 axios 路径:`utils/download.js`(独立实例、不挂本拦截器)、原生 `fetch`、
|
||||||
|
// el-upload 的 on-success 手工判响应;
|
||||||
|
// ④ 401 由 `forceLogout()` 提示,**不受 `silent` 影响**(token 真失效必须让用户看到)。
|
||||||
|
|
||||||
// ==================== 响应拦截器 ====================
|
// ==================== 响应拦截器 ====================
|
||||||
|
|
||||||
request.interceptors.response.use(
|
request.interceptors.response.use(
|
||||||
|
|||||||
@@ -199,6 +199,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// ===== 错误提示口径(R65 收敛)=====
|
||||||
|
// 请求失败的提示一律由 src/api/request.js 响应拦截器统一给出(覆盖全部失败路径),
|
||||||
|
// 本文件 **不在 catch 内重复弹窗**(同族双弹)。例外:客户端校验(未发请求)/
|
||||||
|
// 已带 silent(调用方即唯一提示方)/ 非 axios 路径(原生 fetch、独立 axios 实例)。
|
||||||
/**
|
/**
|
||||||
* Activate.vue - 账号激活页面(公开访问)
|
* Activate.vue - 账号激活页面(公开访问)
|
||||||
*
|
*
|
||||||
@@ -377,8 +381,8 @@ async function handleStep1() {
|
|||||||
currentStep.value = 1
|
currentStep.value = 1
|
||||||
// 启动倒计时
|
// 启动倒计时
|
||||||
startSmsCountdown()
|
startSmsCountdown()
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (err?.message) ElMessage.error(err.message)
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
// 刷新验证码
|
// 刷新验证码
|
||||||
refreshCaptcha()
|
refreshCaptcha()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -423,8 +427,8 @@ async function handleActivate() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}, 1500)
|
}, 1500)
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (err?.message) ElMessage.error(err.message)
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
} finally {
|
} finally {
|
||||||
step3Loading.value = false
|
step3Loading.value = false
|
||||||
}
|
}
|
||||||
@@ -441,8 +445,8 @@ async function resendSms() {
|
|||||||
}
|
}
|
||||||
ElMessage.success('短信验证码已重新发送')
|
ElMessage.success('短信验证码已重新发送')
|
||||||
startSmsCountdown()
|
startSmsCountdown()
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (err?.message) ElMessage.error(err.message)
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
refreshCaptcha()
|
refreshCaptcha()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,6 +347,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// ===== 错误提示口径(R65 收敛)=====
|
||||||
|
// 请求失败的提示一律由 src/api/request.js 响应拦截器统一给出(覆盖全部失败路径),
|
||||||
|
// 本文件 **不在 catch 内重复弹窗**(同族双弹)。例外:客户端校验(未发请求)/
|
||||||
|
// 已带 silent(调用方即唯一提示方)/ 非 axios 路径(原生 fetch、独立 axios 实例)。
|
||||||
/**
|
/**
|
||||||
* Competition.vue - 在线答题页面
|
* Competition.vue - 在线答题页面
|
||||||
*
|
*
|
||||||
@@ -481,8 +485,8 @@ async function submitUploadFiles() {
|
|||||||
uploadRef.value?.clearFiles?.()
|
uploadRef.value?.clearFiles?.()
|
||||||
await fetchUploadedWorks()
|
await fetchUploadedWorks()
|
||||||
examStore.stopExam()
|
examStore.stopExam()
|
||||||
} catch (e) {
|
} catch {
|
||||||
ElMessage.error(e?.message || '上传失败')
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
uploading.value = false
|
uploading.value = false
|
||||||
@@ -507,8 +511,8 @@ async function deleteUploadedWork(w, silent = false) {
|
|||||||
await uploadApi.deleteWork(authStore.userGroup, w.id)
|
await uploadApi.deleteWork(authStore.userGroup, w.id)
|
||||||
uploadedWorks.value = uploadedWorks.value.filter(x => x.id !== w.id)
|
uploadedWorks.value = uploadedWorks.value.filter(x => x.id !== w.id)
|
||||||
if (!silent) ElMessage.success('已删除')
|
if (!silent) ElMessage.success('已删除')
|
||||||
} catch (e) {
|
} catch {
|
||||||
ElMessage.error(e?.message || '删除失败')
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
} finally {
|
} finally {
|
||||||
w.deleting = false
|
w.deleting = false
|
||||||
}
|
}
|
||||||
@@ -1072,8 +1076,8 @@ onMounted(async () => {
|
|||||||
examStore.startUploadCountdown(group)
|
examStore.startUploadCountdown(group)
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
ElMessage.error('加载考试信息失败')
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
router.push('/dashboard')
|
router.push('/dashboard')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
@@ -112,6 +112,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// ===== 错误提示口径(R65 收敛)=====
|
||||||
|
// 请求失败的提示一律由 src/api/request.js 响应拦截器统一给出(覆盖全部失败路径),
|
||||||
|
// 本文件 **不在 catch 内重复弹窗**(同族双弹)。例外:客户端校验(未发请求)/
|
||||||
|
// 已带 silent(调用方即唯一提示方)/ 非 axios 路径(原生 fetch、独立 axios 实例)。
|
||||||
/**
|
/**
|
||||||
* Dashboard.vue - 选手工作台
|
* Dashboard.vue - 选手工作台
|
||||||
*
|
*
|
||||||
@@ -216,8 +220,8 @@ async function enterContest() {
|
|||||||
ElMessage.warning(data.message || (data.status === 'ended' ? '考试已结束,无法进入' : '考试未在进行中,请等待考试开始'))
|
ElMessage.warning(data.message || (data.status === 'ended' ? '考试已结束,无法进入' : '考试未在进行中,请等待考试开始'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
ElMessage.warning('获取考试状态失败,请稍后重试')
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.push('/competition')
|
router.push('/competition')
|
||||||
|
|||||||
@@ -189,6 +189,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// ===== 错误提示口径(R65 收敛)=====
|
||||||
|
// 请求失败的提示一律由 src/api/request.js 响应拦截器统一给出(覆盖全部失败路径),
|
||||||
|
// 本文件 **不在 catch 内重复弹窗**(同族双弹)。例外:客户端校验(未发请求)/
|
||||||
|
// 已带 silent(调用方即唯一提示方)/ 非 axios 路径(原生 fetch、独立 axios 实例)。
|
||||||
/**
|
/**
|
||||||
* ForgotPassword.vue - 忘记密码页面(公开访问)
|
* ForgotPassword.vue - 忘记密码页面(公开访问)
|
||||||
*
|
*
|
||||||
@@ -345,8 +349,8 @@ async function handleStep1() {
|
|||||||
ElMessage.success('短信验证码已发送')
|
ElMessage.success('短信验证码已发送')
|
||||||
currentStep.value = 1
|
currentStep.value = 1
|
||||||
startSmsCountdown()
|
startSmsCountdown()
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (err?.message) ElMessage.error(err.message)
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
refreshCaptcha()
|
refreshCaptcha()
|
||||||
} finally {
|
} finally {
|
||||||
step1Loading.value = false
|
step1Loading.value = false
|
||||||
@@ -383,8 +387,8 @@ async function handleReset() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}, 1500)
|
}, 1500)
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (err?.message) ElMessage.error(err.message)
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
} finally {
|
} finally {
|
||||||
step3Loading.value = false
|
step3Loading.value = false
|
||||||
}
|
}
|
||||||
@@ -401,8 +405,8 @@ async function resendSms() {
|
|||||||
}
|
}
|
||||||
ElMessage.success('短信验证码已重新发送')
|
ElMessage.success('短信验证码已重新发送')
|
||||||
startSmsCountdown()
|
startSmsCountdown()
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (err?.message) ElMessage.error(err.message)
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
refreshCaptcha()
|
refreshCaptcha()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// ===== 错误提示口径(R65 收敛)=====
|
||||||
|
// 请求失败的提示一律由 src/api/request.js 响应拦截器统一给出(覆盖全部失败路径),
|
||||||
|
// 本文件 **不在 catch 内重复弹窗**(同族双弹)。例外:客户端校验(未发请求)/
|
||||||
|
// 已带 silent(调用方即唯一提示方)/ 非 axios 路径(原生 fetch、独立 axios 实例)。
|
||||||
/**
|
/**
|
||||||
* Login.vue - 登录页面(公开访问)
|
* Login.vue - 登录页面(公开访问)
|
||||||
*
|
*
|
||||||
@@ -254,10 +258,8 @@ async function handleAdminLogin() {
|
|||||||
ElMessage.success('管理员登录成功')
|
ElMessage.success('管理员登录成功')
|
||||||
const redirect = route.query.redirect || '/admin'
|
const redirect = route.query.redirect || '/admin'
|
||||||
router.push(redirect)
|
router.push(redirect)
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (err?.message) {
|
// 失败提示由响应拦截器统一给出,此处不再重复弹窗(同族双弹)
|
||||||
ElMessage.error(err.message)
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
adminLoading.value = false
|
adminLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user