错误提示处理

This commit is contained in:
tianfeng 2025-08-21 12:59:40 +08:00
parent 71c8ed68b3
commit 483b4371d5
4 changed files with 282 additions and 48 deletions

View File

@ -1,39 +1,44 @@
<template> <template>
<div v-if="visible" class="error-modal-overlay" @click="handleOverlayClick"> <div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
<div class="error-modal" :class="animationClass"> <div class="modal" :class="[animationClass, modalTypeClass]">
<!-- 错误图标 --> <!-- 图标 -->
<div class="error-icon"> <div class="modal-icon">
<div class="error-circle"> <div class="icon-circle" :class="iconCircleClass">
<span class="error-x"></span> <span class="icon-symbol" :class="iconSymbolClass">{{ iconSymbol }}</span>
</div> </div>
</div> </div>
<!-- 标题 --> <!-- 标题 -->
<div class="error-title" v-if="title">{{ title }}</div> <div class="modal-title" v-if="title">{{ title }}</div>
<!-- 错误信息 --> <!-- 消息内容 -->
<div class="error-message">{{ message }}</div> <div class="modal-message">{{ message }}</div>
<!-- 按钮 --> <!-- 按钮 -->
<div class="error-actions"> <div class="modal-actions">
<button class="error-btn" @click="confirm">{{ confirmText }}</button> <button class="modal-btn" :class="buttonClass" @click="confirm">{{ confirmText }}</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: 'Error' default: 'Notice'
}, },
message: { message: {
type: String, type: String,
required: true required: true
}, },
type: {
type: String,
default: 'error', // 'error' | 'warning' | 'success' | 'info'
validator: (value) => ['error', 'warning', 'success', 'info'].includes(value)
},
confirmText: { confirmText: {
type: String, type: String,
default: 'OK' default: 'OK'
@ -41,6 +46,10 @@ const props = defineProps({
clickToClose: { clickToClose: {
type: Boolean, type: Boolean,
default: true default: true
},
duration: {
type: Number,
default: null //
} }
}) })
@ -49,9 +58,35 @@ const emit = defineEmits(['confirm', 'close'])
const visible = ref(false) const visible = ref(false)
const animationClass = ref('') const animationClass = ref('')
//
const iconSymbol = computed(() => {
const symbols = {
error: '✕',
warning: '⚠',
success: '✓',
info: 'i'
}
return symbols[props.type] || symbols.error
})
const modalTypeClass = computed(() => `modal-${props.type}`)
const iconCircleClass = computed(() => `icon-circle-${props.type}`)
const iconSymbolClass = computed(() => `icon-symbol-${props.type}`)
const buttonClass = computed(() => `modal-btn-${props.type}`)
const show = () => { const show = () => {
visible.value = true visible.value = true
animationClass.value = 'modal-enter' animationClass.value = 'modal-enter'
//
if (props.duration && props.duration > 0) {
setTimeout(() => {
close()
}, props.duration)
}
} }
const close = () => { const close = () => {
@ -86,7 +121,8 @@ defineExpose({
</script> </script>
<style scoped> <style scoped>
.error-modal-overlay { /* 基础模态框样式 */
.modal-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -100,7 +136,7 @@ defineExpose({
padding: 20px; padding: 20px;
} }
.error-modal { .modal {
background: white; background: white;
border-radius: 16px; border-radius: 16px;
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04); box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);
@ -111,34 +147,32 @@ defineExpose({
position: relative; position: relative;
} }
/* 错误图标 */ /* 图标区域 */
.error-icon { .modal-icon {
margin-bottom: 16px; margin-bottom: 16px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.error-circle { .icon-circle {
width: 56px; width: 56px;
height: 56px; height: 56px;
border-radius: 50%; border-radius: 50%;
background-color: #FEE2E2;
border: 3px solid #FECACA;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
animation: errorPulse 0.6s ease-out; animation: iconPulse 0.6s ease-out;
border: 3px solid;
} }
.error-x { .icon-symbol {
color: #DC2626;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
line-height: 1; line-height: 1;
} }
/* 标题 */ /* 标题 */
.error-title { .modal-title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #1F2937; color: #1F2937;
@ -146,8 +180,8 @@ defineExpose({
line-height: 1.4; line-height: 1.4;
} }
/* 错误信息 */ /* 消息内容 */
.error-message { .modal-message {
font-size: 14px; font-size: 14px;
color: #6B7280; color: #6B7280;
line-height: 1.5; line-height: 1.5;
@ -156,14 +190,12 @@ defineExpose({
} }
/* 按钮区域 */ /* 按钮区域 */
.error-actions { .modal-actions {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.error-btn { .modal-btn {
background-color: #DC2626;
color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 10px 32px; padding: 10px 32px;
@ -172,15 +204,99 @@ defineExpose({
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
min-width: 80px; min-width: 80px;
color: white;
} }
.error-btn:hover { .modal-btn:active {
transform: scale(0.98);
}
/* Error 类型样式 */
.modal-error .icon-circle-error {
background-color: #FEE2E2;
border-color: #FECACA;
}
.modal-error .icon-symbol-error {
color: #DC2626;
}
.modal-error .modal-btn-error {
background-color: #DC2626;
}
.modal-error .modal-btn-error:hover {
background-color: #B91C1C; background-color: #B91C1C;
} }
.error-btn:active { .modal-error .modal-btn-error:active {
background-color: #991B1B; background-color: #991B1B;
transform: scale(0.98); }
/* Warning 类型样式 */
.modal-warning .icon-circle-warning {
background-color: #FEF3C7;
border-color: #FDE68A;
}
.modal-warning .icon-symbol-warning {
color: #D97706;
}
.modal-warning .modal-btn-warning {
background-color: #D97706;
}
.modal-warning .modal-btn-warning:hover {
background-color: #B45309;
}
.modal-warning .modal-btn-warning:active {
background-color: #92400E;
}
/* Success 类型样式 */
.modal-success .icon-circle-success {
background-color: #D1FAE5;
border-color: #A7F3D0;
}
.modal-success .icon-symbol-success {
color: #059669;
}
.modal-success .modal-btn-success {
background-color: #059669;
}
.modal-success .modal-btn-success:hover {
background-color: #047857;
}
.modal-success .modal-btn-success:active {
background-color: #065F46;
}
/* Info 类型样式 */
.modal-info .icon-circle-info {
background-color: #DBEAFE;
border-color: #BFDBFE;
}
.modal-info .icon-symbol-info {
color: #2563EB;
}
.modal-info .modal-btn-info {
background-color: #2563EB;
}
.modal-info .modal-btn-info:hover {
background-color: #1D4ED8;
}
.modal-info .modal-btn-info:active {
background-color: #1E40AF;
} }
/* 动画效果 */ /* 动画效果 */
@ -214,7 +330,7 @@ defineExpose({
} }
} }
@keyframes errorPulse { @keyframes iconPulse {
0% { 0% {
transform: scale(0.8); transform: scale(0.8);
opacity: 0.5; opacity: 0.5;
@ -230,25 +346,25 @@ defineExpose({
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 480px) { @media (max-width: 480px) {
.error-modal { .modal {
max-width: 280px; max-width: 280px;
padding: 20px 16px 16px; padding: 20px 16px 16px;
} }
.error-circle { .icon-circle {
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
.error-x { .icon-symbol {
font-size: 20px; font-size: 20px;
} }
.error-title { .modal-title {
font-size: 16px; font-size: 16px;
} }
.error-message { .modal-message {
font-size: 13px; font-size: 13px;
} }
} }

View File

@ -1,6 +1,24 @@
// HTTP请求配置 // HTTP请求配置
import {getApiBaseUrl, getCurrentEnv, getUserAuth, isDebugMode} from './env.js' import {getApiBaseUrl, getCurrentEnv, getUserAuth, isDebugMode} from './env.js'
/**
* 自定义API错误类
*/
class ApiError extends Error {
constructor(message, options = {}) {
super(message)
this.name = 'ApiError'
this.status = options.status || null // HTTP状态码
this.errorCode = options.errorCode || null // 业务错误码 (如: 6010)
this.errorCodeName = options.errorCodeName || null // 错误码名称 (如: "TEAM_NOT_FOUND")
this.errorMsg = options.errorMsg || null // 业务错误消息
this.time = options.time || null // API响应时间戳
this.extValues = options.extValues || null // 扩展值
this.response = options.response || null // 原始响应数据
this.type = options.type || 'unknown' // 错误类型: 'http' | 'business' | 'network'
}
}
const API_BASE_URL = getApiBaseUrl() const API_BASE_URL = getApiBaseUrl()
const NODE_ENV = getCurrentEnv() const NODE_ENV = getCurrentEnv()
const DEBUG_MODE = isDebugMode() const DEBUG_MODE = isDebugMode()
@ -83,7 +101,7 @@ export function setHeadersFromApp(headerInfo) {
} }
// 在http.js中添加导出 // 在http.js中添加导出
export { COMMON_HEADERS } export { COMMON_HEADERS, ApiError }
/** /**
* 通用HTTP请求函数 * 通用HTTP请求函数
@ -124,7 +142,18 @@ export async function request(url, options = {}) {
const response = await fetch(fullUrl, config) const response = await fetch(fullUrl, config)
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`) // 尝试解析响应体获取更多错误信息
let errorData = await response.json()
throw new ApiError(`HTTP Error: ${response.status} ${response.statusText}`, {
type: 'http',
status: response.status,
response: errorData,
errorCode: errorData.errorCode,
errorCodeName: errorData.errorCodeName,
errorMsg: errorData.errorMsg,
time: errorData.time
})
} }
const result = await response.json() const result = await response.json()
@ -135,7 +164,16 @@ export async function request(url, options = {}) {
// 检查业务状态 // 检查业务状态
if (result.status === false) { if (result.status === false) {
throw new Error(result.message || `API Error: ${result.errorCode}`) throw new ApiError(result.errorMsg || result.message || `API Error: ${result.errorCode}`, {
type: 'business',
errorCode: result.errorCode,
errorCodeName: result.errorCodeName,
errorMsg: result.errorMsg,
time: result.time,
extValues: result.extValues,
response: result,
status: response.status,
})
} }
return result return result
@ -143,6 +181,16 @@ export async function request(url, options = {}) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.error('❌ API Error:', error) console.error('❌ API Error:', error)
} }
// 如果是网络错误或其他未知错误包装成ApiError
if (!(error instanceof ApiError)) {
throw new ApiError(error.message || 'Network or unknown error', {
type: 'network',
originalError: error,
code: 'NETWORK_ERROR'
})
}
throw error throw error
} }
} }

View File

@ -70,7 +70,33 @@ export const errorModal = new ErrorModalManager()
// 快捷方法 - 替代alert // 快捷方法 - 替代alert
export const showError = (message, title = 'Error') => { export const showError = (message, title = 'Error') => {
return errorModal.show(message, { title }) return errorModal.show(message, { title, type: 'error' })
}
// 信息提示
export const showInfo = (message, title = 'Info') => {
return errorModal.show(message, { title, type: 'info' })
}
// 成功提示
export const showSuccess = (message, title = 'Success') => {
return errorModal.show(message, { title, type: 'success' })
}
// 警告提示
export const showWarning = (message, title = 'Warning') => {
return errorModal.show(message, { title, type: 'warning' })
}
// 通用提示方法
export const showToast = (message, options = {}) => {
const defaultOptions = {
title: 'Notice',
type: 'info',
duration: 3000, // 自动关闭时间
...options
}
return errorModal.show(message, defaultOptions)
} }
// 错误处理的专用方法 // 错误处理的专用方法
@ -81,14 +107,23 @@ export const handleError = (error) => {
if (error.errorCode === 1083) { if (error.errorCode === 1083) {
title = 'Service Unavailable' title = 'Service Unavailable'
message = 'Exchange function is not available' message = 'Exchange function is not available'
} else if (error.errorCode === 6010) {
title = 'Team Not Found'
message = 'The team does not exist or has been disbanded'
} else if (error.message && error.message.includes('HTTP Error: 406')) { } else if (error.message && error.message.includes('HTTP Error: 406')) {
title = 'Request Not Acceptable' title = 'Request Not Acceptable'
message = 'Something went wrong, please retry.' message = 'Something went wrong, please retry.'
} else if (error.errorMsg) {
// 优先使用 errorMsg
message = error.errorMsg
if (error.errorCodeName) {
title = error.errorCodeName.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())
}
} else if (error.message) { } else if (error.message) {
message = error.message message = error.message
} }
return errorModal.show(message, { title }) return errorModal.show(message, { title, type: 'error' })
} }
// 确认对话框 // 确认对话框

View File

@ -96,7 +96,8 @@ import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue' import MobileHeader from '../components/MobileHeader.vue'
import { sendTeamApplyJoin, getWaitApplyRecord, cancelApply } from '../api/wallet.js' import { sendTeamApplyJoin, getWaitApplyRecord, cancelApply } from '../api/wallet.js'
import {showError} from "@/utils/toast.js"; import {showError, showWarning, showInfo} from "@/utils/toast.js";
import { ApiError } from '@/utils/http.js';
const router = useRouter() const router = useRouter()
@ -116,6 +117,7 @@ const fetchApplyRecords = async () => {
} catch (error) { } catch (error) {
console.error('获取申请记录失败:', error) console.error('获取申请记录失败:', error)
applyRecords.value = [] applyRecords.value = []
showInfo(error.errorMsg)
} }
} }
@ -138,8 +140,26 @@ const handleCancelApply = async (record) => {
} }
} catch (error) { } catch (error) {
console.error('撤销申请失败:', error) console.error('撤销申请失败:', error)
if (error instanceof ApiError) {
if (error.type === 'business') {
switch (error.errorCode) {
case 6404: //
showError('申请记录不存在', '撤销失败')
break
case 6003: //
showError('申请已被处理,无法撤销', '撤销失败')
break
default:
showError(error.errorMsg || error.message || '撤销失败,请重试', '撤销失败')
}
} else {
showError('网络错误,请检查网络后重试', '撤销失败')
}
} else {
showError('撤销失败,请重试') showError('撤销失败,请重试')
} }
}
} }
const submitApplication = async () => { const submitApplication = async () => {
@ -167,8 +187,23 @@ const submitApplication = async () => {
errorMessage.value = response.message || 'Application failed. Please try again.' errorMessage.value = response.message || 'Application failed. Please try again.'
} }
} catch (error) { } catch (error) {
console.error('提交申请失败:', error) //
errorMessage.value = 'Application failed. Please try again.' if (error instanceof ApiError) {
if (error.type === 'business') {
showError('business error!' + error.errorMsg)
} else if (error.type === 'http') {
// HTTP
if (error.status === 406) {
showWarning(error.errorMsg)
} else {
showError(error.message || '未知错误,请重试')
}
} else {
// ApiError
showError('未知错误,请重试')
}
}
} finally { } finally {
isLoading.value = false isLoading.value = false
} }