修改前端报错文案
This commit is contained in:
parent
a7c7936e0b
commit
d463fbf71a
|
|
@ -1,6 +1,7 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
import { hasChinese, responseErrorText } from '../utils/errorText'
|
||||||
|
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
|
|
@ -30,15 +31,18 @@ request.interceptors.response.use(
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
ElMessage.error(res.message || '请求失败')
|
ElMessage.error(hasChinese(res.message) ? res.message : '请求失败')
|
||||||
return Promise.reject(new Error(res.message))
|
return Promise.reject(new Error(res.message))
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem('wmms_token')
|
localStorage.removeItem('wmms_token')
|
||||||
localStorage.removeItem('wmms_user')
|
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) {
|
} else if (error.response?.status === 422) {
|
||||||
// Validation errors - parse and show friendly message
|
// Validation errors - parse and show friendly message
|
||||||
const data = error.response?.data
|
const data = error.response?.data
|
||||||
|
|
@ -55,11 +59,9 @@ request.interceptors.response.use(
|
||||||
msg = fields.map(f => labels[f] || f).join('、') + '不能为空'
|
msg = fields.map(f => labels[f] || f).join('、') + '不能为空'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ElMessage.error(msg || '请检查必填字段')
|
ElMessage.error(hasChinese(msg) ? msg : '请检查必填字段')
|
||||||
} else if (error.response?.data?.message) {
|
|
||||||
ElMessage.error(error.response.data.message)
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(error.message || '网络错误')
|
ElMessage.error(responseErrorText(error))
|
||||||
}
|
}
|
||||||
return Promise.reject(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 拦截器提示,
|
// 保存过程中抛出的运行时异常(不是接口错误)不会被 axios 拦截器提示,
|
||||||
// 若不在这里兜住,用户看到的就只是"点了保存没反应"。
|
// 若不在这里兜住,用户看到的就只是"点了保存没反应"。异常自带的英文消息
|
||||||
|
// 只写进控制台,界面上给中文兜底文案。
|
||||||
test('保存过程抛出运行时异常时给出提示,而不是静默卡住', async () => {
|
test('保存过程抛出运行时异常时给出提示,而不是静默卡住', async () => {
|
||||||
const wrapper = mountInbound()
|
const wrapper = mountInbound()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
@ -240,7 +241,7 @@ describe('入库单表单必填校验', () => {
|
||||||
await findButton(wrapper, '保存').trigger('click')
|
await findButton(wrapper, '保存').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(errorSpy).toHaveBeenCalledWith('boom')
|
expect(errorSpy).toHaveBeenCalledWith('保存失败')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ import BaseTable from '../components/business/BaseTable.vue'
|
||||||
import StatusBadge from '../components/business/StatusBadge.vue'
|
import StatusBadge from '../components/business/StatusBadge.vue'
|
||||||
import DeleteConfirm from '../components/business/DeleteConfirm.vue'
|
import DeleteConfirm from '../components/business/DeleteConfirm.vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { errorText } from '../utils/errorText'
|
||||||
|
|
||||||
const inboundStore = useInboundStore()
|
const inboundStore = useInboundStore()
|
||||||
const materialsStore = useMaterialsStore()
|
const materialsStore = useMaterialsStore()
|
||||||
|
|
@ -253,8 +254,7 @@ async function showDetail(row) {
|
||||||
currentRow.value = detail
|
currentRow.value = detail
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取详情失败:', error)
|
console.error('获取详情失败:', error)
|
||||||
const msg = error.response?.data?.message || error.message || '获取详情失败'
|
ElMessage.error(errorText(error, '获取详情失败'))
|
||||||
ElMessage.error(msg)
|
|
||||||
currentRow.value = { ...row, items: [] }
|
currentRow.value = { ...row, items: [] }
|
||||||
}
|
}
|
||||||
detailVisible.value = true
|
detailVisible.value = true
|
||||||
|
|
@ -359,7 +359,9 @@ async function handleSave() {
|
||||||
// (比如代码里的运行时错误)。没有这层捕获的话,异常会让弹窗原地不动、
|
// (比如代码里的运行时错误)。没有这层捕获的话,异常会让弹窗原地不动、
|
||||||
// 也不给任何提示,用户只能看到"点了保存没反应"。
|
// 也不给任何提示,用户只能看到"点了保存没反应"。
|
||||||
if (!error?.response) {
|
if (!error?.response) {
|
||||||
ElMessage.error(error?.message || '保存失败')
|
// 运行时异常的原始信息(多为英文栈信息)留给控制台,界面上只给中文
|
||||||
|
console.error('保存入库单失败:', error)
|
||||||
|
ElMessage.error(errorText(error, '保存失败'))
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
saveLoading.value = false
|
saveLoading.value = false
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { errorText } from '../utils/errorText'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
@ -51,7 +52,7 @@ async function handleLogin() {
|
||||||
await authStore.login(form.value.username, form.value.password)
|
await authStore.login(form.value.username, form.value.password)
|
||||||
router.push('/stock')
|
router.push('/stock')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage.value = e.message || '登录失败'
|
errorMessage.value = errorText(e, '用户名或密码错误')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ import { createMaterialForm, toMaterialPayload } from './materialForm'
|
||||||
import { codeRulesApi, materialCategoriesApi, unitsApi } from '../api/masterData'
|
import { codeRulesApi, materialCategoriesApi, unitsApi } from '../api/masterData'
|
||||||
import { getMajorCategories, getMinorCategories, resolveCategoryPath } from '../utils/materialCategories'
|
import { getMajorCategories, getMinorCategories, resolveCategoryPath } from '../utils/materialCategories'
|
||||||
import { searchMaterialOptions } from '../utils/materialSearch'
|
import { searchMaterialOptions } from '../utils/materialSearch'
|
||||||
|
import { errorText } from '../utils/errorText'
|
||||||
|
|
||||||
const materialsStore = useMaterialsStore()
|
const materialsStore = useMaterialsStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
@ -349,7 +350,10 @@ async function handleSave() {
|
||||||
headers: { 'Authorization': `Bearer ${authStore.token}` },
|
headers: { 'Authorization': `Bearer ${authStore.token}` },
|
||||||
body: uploadForm
|
body: uploadForm
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error('图片上传失败')
|
if (!res.ok) {
|
||||||
|
ElMessage.error('图片上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
formData.material_image = (data.data && data.data.url) ? data.data.url : ''
|
formData.material_image = (data.data && data.data.url) ? data.data.url : ''
|
||||||
}
|
}
|
||||||
|
|
@ -362,7 +366,12 @@ async function handleSave() {
|
||||||
}
|
}
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e.message || '保存失败')
|
// 接口失败的原因已由拦截器弹过中文提示,这里只兜住它覆盖不到的异常,
|
||||||
|
// 避免出现"点了保存没反应"
|
||||||
|
if (!e?.response) {
|
||||||
|
console.error('保存物料失败:', e)
|
||||||
|
ElMessage.error(errorText(e, '保存失败'))
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
saveLoading.value = false
|
saveLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,7 @@ import BaseTable from '../components/business/BaseTable.vue'
|
||||||
import StatusBadge from '../components/business/StatusBadge.vue'
|
import StatusBadge from '../components/business/StatusBadge.vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import WarehouseSettings from './inventory/WarehouseSettings.vue'
|
import WarehouseSettings from './inventory/WarehouseSettings.vue'
|
||||||
|
import { errorText } from '../utils/errorText'
|
||||||
import { searchMaterialOptions } from '../utils/materialSearch'
|
import { searchMaterialOptions } from '../utils/materialSearch'
|
||||||
import { materialCategoriesApi } from '../api/masterData'
|
import { materialCategoriesApi } from '../api/masterData'
|
||||||
import { getMajorCategories } from '../utils/materialCategories'
|
import { getMajorCategories } from '../utils/materialCategories'
|
||||||
|
|
@ -314,7 +315,7 @@ async function saveWarning() {
|
||||||
warningDialogVisible.value = false
|
warningDialogVisible.value = false
|
||||||
ElMessage.success('库存预警已更新')
|
ElMessage.success('库存预警已更新')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error.message || '库存预警更新失败')
|
ElMessage.error(errorText(error, '库存预警更新失败'))
|
||||||
} finally {
|
} finally {
|
||||||
warningSaving.value = false
|
warningSaving.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrDuplicateMaterialCode 表示同组织下物料编码已被占用。
|
// ErrDuplicateMaterialCode 表示同组织下物料编码已被占用。
|
||||||
var ErrDuplicateMaterialCode = errors.New("material code already exists")
|
var ErrDuplicateMaterialCode = errors.New("物料编码已存在")
|
||||||
|
|
||||||
// BeginTx starts a new database transaction
|
// BeginTx starts a new database transaction
|
||||||
func BeginTx() (*sql.Tx, error) {
|
func BeginTx() (*sql.Tx, error) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user