修改前端报错文案
This commit is contained in:
parent
a7c7936e0b
commit
d463fbf71a
|
|
@ -1,6 +1,7 @@
|
|||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '../router'
|
||||
import { hasChinese, responseErrorText } from '../utils/errorText'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
|
|
@ -30,15 +31,18 @@ request.interceptors.response.use(
|
|||
if (res.code === 0) {
|
||||
return res
|
||||
}
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
ElMessage.error(hasChinese(res.message) ? res.message : '请求失败')
|
||||
return Promise.reject(new Error(res.message))
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('wmms_token')
|
||||
localStorage.removeItem('wmms_user')
|
||||
router.push('/login')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
// 登录页自己会把失败原因写在表单里,再弹一句"登录已过期"只会让人困惑
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
router.push('/login')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
}
|
||||
} else if (error.response?.status === 422) {
|
||||
// Validation errors - parse and show friendly message
|
||||
const data = error.response?.data
|
||||
|
|
@ -55,11 +59,9 @@ request.interceptors.response.use(
|
|||
msg = fields.map(f => labels[f] || f).join('、') + '不能为空'
|
||||
}
|
||||
}
|
||||
ElMessage.error(msg || '请检查必填字段')
|
||||
} else if (error.response?.data?.message) {
|
||||
ElMessage.error(error.response.data.message)
|
||||
ElMessage.error(hasChinese(msg) ? msg : '请检查必填字段')
|
||||
} else {
|
||||
ElMessage.error(error.message || '网络错误')
|
||||
ElMessage.error(responseErrorText(error))
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
|
|
|||
33
frontend/src/utils/errorText.js
Normal file
33
frontend/src/utils/errorText.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// 后端有一部分消息是英文(如 "material not found"),axios 自己生成的
|
||||
// "Request failed with status code 400" 也是英文。面向用户的提示只放行带中文的
|
||||
// 消息,其余换成中文,保证界面上不出现英文报错。
|
||||
|
||||
const STATUS_TEXT = {
|
||||
400: '请求参数有误,请检查填写内容',
|
||||
403: '没有操作权限',
|
||||
404: '请求的数据不存在',
|
||||
409: '数据已被改动,请刷新后重试',
|
||||
429: '操作过于频繁,请稍后再试'
|
||||
}
|
||||
|
||||
export function hasChinese(text) {
|
||||
return typeof text === 'string' && /[一-龥]/.test(text)
|
||||
}
|
||||
|
||||
// 组件层取错误文案用这个:后端给了中文就用原文,英文一律换成中文兜底。
|
||||
export function errorText(error, fallback = '操作失败') {
|
||||
const serverMessage = error?.response?.data?.message
|
||||
if (hasChinese(serverMessage)) return serverMessage
|
||||
if (hasChinese(error?.message)) return error.message
|
||||
return fallback
|
||||
}
|
||||
|
||||
// 拦截器自己弹提示时用这个:拿不到中文消息就按状态码兜底。
|
||||
export function responseErrorText(error) {
|
||||
const serverMessage = error?.response?.data?.message
|
||||
if (hasChinese(serverMessage)) return serverMessage
|
||||
const status = error?.response?.status
|
||||
if (STATUS_TEXT[status]) return STATUS_TEXT[status]
|
||||
if (status >= 500) return '服务器出错了,请稍后重试'
|
||||
return error?.response ? '请求失败' : '网络异常,请检查网络连接'
|
||||
}
|
||||
41
frontend/src/utils/errorText.test.js
Normal file
41
frontend/src/utils/errorText.test.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, expect, test } from 'vitest'
|
||||
import { errorText, responseErrorText } from './errorText'
|
||||
|
||||
// 后端返回英文消息时 axios 的 error.message 也是英文,两者都不该漏到界面上。
|
||||
function axiosError(status, message) {
|
||||
const error = new Error(`Request failed with status code ${status}`)
|
||||
error.response = { status, data: message ? { message } : {} }
|
||||
return error
|
||||
}
|
||||
|
||||
describe('面向用户的错误文案', () => {
|
||||
test('后端给了中文就用后端原文', () => {
|
||||
expect(errorText(axiosError(400, '物料编码已存在'), '保存失败')).toBe('物料编码已存在')
|
||||
})
|
||||
|
||||
test('后端消息是英文时换成中文兜底', () => {
|
||||
expect(errorText(axiosError(400, 'material code already exists'), '保存失败')).toBe('保存失败')
|
||||
})
|
||||
|
||||
test('拿不到响应时用中文兜底,不暴露 axios 的英文', () => {
|
||||
expect(errorText(new Error('Network Error'), '保存失败')).toBe('保存失败')
|
||||
})
|
||||
|
||||
test('自定义的中文错误保持原文', () => {
|
||||
expect(errorText(new Error('图片上传失败'), '保存失败')).toBe('图片上传失败')
|
||||
})
|
||||
|
||||
test('拦截器兜底时按状态码给中文', () => {
|
||||
expect(responseErrorText(axiosError(400, 'invalid material id'))).toBe('请求参数有误,请检查填写内容')
|
||||
expect(responseErrorText(axiosError(404, 'material not found'))).toBe('请求的数据不存在')
|
||||
expect(responseErrorText(axiosError(500, 'internal error'))).toBe('服务器出错了,请稍后重试')
|
||||
})
|
||||
|
||||
test('拦截器兜底时网络中断有单独提示', () => {
|
||||
expect(responseErrorText(new Error('Network Error'))).toBe('网络异常,请检查网络连接')
|
||||
})
|
||||
|
||||
test('拦截器兜底时中文消息优先于状态码文案', () => {
|
||||
expect(responseErrorText(axiosError(409, '该物料仍有库存,无法作废'))).toBe('该物料仍有库存,无法作废')
|
||||
})
|
||||
})
|
||||
|
|
@ -225,7 +225,8 @@ describe('入库单表单必填校验', () => {
|
|||
})
|
||||
|
||||
// 保存过程中抛出的运行时异常(不是接口错误)不会被 axios 拦截器提示,
|
||||
// 若不在这里兜住,用户看到的就只是"点了保存没反应"。
|
||||
// 若不在这里兜住,用户看到的就只是"点了保存没反应"。异常自带的英文消息
|
||||
// 只写进控制台,界面上给中文兜底文案。
|
||||
test('保存过程抛出运行时异常时给出提示,而不是静默卡住', async () => {
|
||||
const wrapper = mountInbound()
|
||||
await flushPromises()
|
||||
|
|
@ -240,7 +241,7 @@ describe('入库单表单必填校验', () => {
|
|||
await findButton(wrapper, '保存').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('boom')
|
||||
expect(errorSpy).toHaveBeenCalledWith('保存失败')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ import BaseTable from '../components/business/BaseTable.vue'
|
|||
import StatusBadge from '../components/business/StatusBadge.vue'
|
||||
import DeleteConfirm from '../components/business/DeleteConfirm.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { errorText } from '../utils/errorText'
|
||||
|
||||
const inboundStore = useInboundStore()
|
||||
const materialsStore = useMaterialsStore()
|
||||
|
|
@ -253,8 +254,7 @@ async function showDetail(row) {
|
|||
currentRow.value = detail
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
const msg = error.response?.data?.message || error.message || '获取详情失败'
|
||||
ElMessage.error(msg)
|
||||
ElMessage.error(errorText(error, '获取详情失败'))
|
||||
currentRow.value = { ...row, items: [] }
|
||||
}
|
||||
detailVisible.value = true
|
||||
|
|
@ -359,7 +359,9 @@ async function handleSave() {
|
|||
// (比如代码里的运行时错误)。没有这层捕获的话,异常会让弹窗原地不动、
|
||||
// 也不给任何提示,用户只能看到"点了保存没反应"。
|
||||
if (!error?.response) {
|
||||
ElMessage.error(error?.message || '保存失败')
|
||||
// 运行时异常的原始信息(多为英文栈信息)留给控制台,界面上只给中文
|
||||
console.error('保存入库单失败:', error)
|
||||
ElMessage.error(errorText(error, '保存失败'))
|
||||
}
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { ref } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { errorText } from '../utils/errorText'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
|
@ -51,7 +52,7 @@ async function handleLogin() {
|
|||
await authStore.login(form.value.username, form.value.password)
|
||||
router.push('/stock')
|
||||
} catch (e) {
|
||||
errorMessage.value = e.message || '登录失败'
|
||||
errorMessage.value = errorText(e, '用户名或密码错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ import { createMaterialForm, toMaterialPayload } from './materialForm'
|
|||
import { codeRulesApi, materialCategoriesApi, unitsApi } from '../api/masterData'
|
||||
import { getMajorCategories, getMinorCategories, resolveCategoryPath } from '../utils/materialCategories'
|
||||
import { searchMaterialOptions } from '../utils/materialSearch'
|
||||
import { errorText } from '../utils/errorText'
|
||||
|
||||
const materialsStore = useMaterialsStore()
|
||||
const authStore = useAuthStore()
|
||||
|
|
@ -349,7 +350,10 @@ async function handleSave() {
|
|||
headers: { 'Authorization': `Bearer ${authStore.token}` },
|
||||
body: uploadForm
|
||||
})
|
||||
if (!res.ok) throw new Error('图片上传失败')
|
||||
if (!res.ok) {
|
||||
ElMessage.error('图片上传失败')
|
||||
return
|
||||
}
|
||||
const data = await res.json()
|
||||
formData.material_image = (data.data && data.data.url) ? data.data.url : ''
|
||||
}
|
||||
|
|
@ -362,7 +366,12 @@ async function handleSave() {
|
|||
}
|
||||
dialogVisible.value = false
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || '保存失败')
|
||||
// 接口失败的原因已由拦截器弹过中文提示,这里只兜住它覆盖不到的异常,
|
||||
// 避免出现"点了保存没反应"
|
||||
if (!e?.response) {
|
||||
console.error('保存物料失败:', e)
|
||||
ElMessage.error(errorText(e, '保存失败'))
|
||||
}
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ import BaseTable from '../components/business/BaseTable.vue'
|
|||
import StatusBadge from '../components/business/StatusBadge.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import WarehouseSettings from './inventory/WarehouseSettings.vue'
|
||||
import { errorText } from '../utils/errorText'
|
||||
import { searchMaterialOptions } from '../utils/materialSearch'
|
||||
import { materialCategoriesApi } from '../api/masterData'
|
||||
import { getMajorCategories } from '../utils/materialCategories'
|
||||
|
|
@ -314,7 +315,7 @@ async function saveWarning() {
|
|||
warningDialogVisible.value = false
|
||||
ElMessage.success('库存预警已更新')
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '库存预警更新失败')
|
||||
ElMessage.error(errorText(error, '库存预警更新失败'))
|
||||
} finally {
|
||||
warningSaving.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
// ErrDuplicateMaterialCode 表示同组织下物料编码已被占用。
|
||||
var ErrDuplicateMaterialCode = errors.New("material code already exists")
|
||||
var ErrDuplicateMaterialCode = errors.New("物料编码已存在")
|
||||
|
||||
// BeginTx starts a new database transaction
|
||||
func BeginTx() (*sql.Tx, error) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user