错误提示处理

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>
<div v-if="visible" class="error-modal-overlay" @click="handleOverlayClick">
<div class="error-modal" :class="animationClass">
<!-- 错误图标 -->
<div class="error-icon">
<div class="error-circle">
<span class="error-x"></span>
<div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
<div class="modal" :class="[animationClass, modalTypeClass]">
<!-- 图标 -->
<div class="modal-icon">
<div class="icon-circle" :class="iconCircleClass">
<span class="icon-symbol" :class="iconSymbolClass">{{ iconSymbol }}</span>
</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">
<button class="error-btn" @click="confirm">{{ confirmText }}</button>
<div class="modal-actions">
<button class="modal-btn" :class="buttonClass" @click="confirm">{{ confirmText }}</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: 'Error'
default: 'Notice'
},
message: {
type: String,
required: true
},
type: {
type: String,
default: 'error', // 'error' | 'warning' | 'success' | 'info'
validator: (value) => ['error', 'warning', 'success', 'info'].includes(value)
},
confirmText: {
type: String,
default: 'OK'
@ -41,6 +46,10 @@ const props = defineProps({
clickToClose: {
type: Boolean,
default: true
},
duration: {
type: Number,
default: null //
}
})
@ -49,9 +58,35 @@ const emit = defineEmits(['confirm', 'close'])
const visible = ref(false)
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 = () => {
visible.value = true
animationClass.value = 'modal-enter'
//
if (props.duration && props.duration > 0) {
setTimeout(() => {
close()
}, props.duration)
}
}
const close = () => {
@ -86,7 +121,8 @@ defineExpose({
</script>
<style scoped>
.error-modal-overlay {
/* 基础模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
@ -100,7 +136,7 @@ defineExpose({
padding: 20px;
}
.error-modal {
.modal {
background: white;
border-radius: 16px;
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;
}
/* 错误图标 */
.error-icon {
/* 图标区域 */
.modal-icon {
margin-bottom: 16px;
display: flex;
justify-content: center;
}
.error-circle {
.icon-circle {
width: 56px;
height: 56px;
border-radius: 50%;
background-color: #FEE2E2;
border: 3px solid #FECACA;
display: flex;
align-items: center;
justify-content: center;
animation: errorPulse 0.6s ease-out;
animation: iconPulse 0.6s ease-out;
border: 3px solid;
}
.error-x {
color: #DC2626;
.icon-symbol {
font-size: 24px;
font-weight: bold;
line-height: 1;
}
/* 标题 */
.error-title {
.modal-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
@ -146,8 +180,8 @@ defineExpose({
line-height: 1.4;
}
/* 错误信息 */
.error-message {
/* 消息内容 */
.modal-message {
font-size: 14px;
color: #6B7280;
line-height: 1.5;
@ -156,14 +190,12 @@ defineExpose({
}
/* 按钮区域 */
.error-actions {
.modal-actions {
display: flex;
justify-content: center;
}
.error-btn {
background-color: #DC2626;
color: white;
.modal-btn {
border: none;
border-radius: 8px;
padding: 10px 32px;
@ -172,15 +204,99 @@ defineExpose({
cursor: pointer;
transition: all 0.2s;
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;
}
.error-btn:active {
.modal-error .modal-btn-error:active {
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% {
transform: scale(0.8);
opacity: 0.5;
@ -230,25 +346,25 @@ defineExpose({
/* 响应式设计 */
@media (max-width: 480px) {
.error-modal {
.modal {
max-width: 280px;
padding: 20px 16px 16px;
}
.error-circle {
.icon-circle {
width: 48px;
height: 48px;
}
.error-x {
.icon-symbol {
font-size: 20px;
}
.error-title {
.modal-title {
font-size: 16px;
}
.error-message {
.modal-message {
font-size: 13px;
}
}

View File

@ -1,6 +1,24 @@
// HTTP请求配置
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 NODE_ENV = getCurrentEnv()
const DEBUG_MODE = isDebugMode()
@ -83,7 +101,7 @@ export function setHeadersFromApp(headerInfo) {
}
// 在http.js中添加导出
export { COMMON_HEADERS }
export { COMMON_HEADERS, ApiError }
/**
* 通用HTTP请求函数
@ -124,7 +142,18 @@ export async function request(url, options = {}) {
const response = await fetch(fullUrl, config)
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()
@ -135,7 +164,16 @@ export async function request(url, options = {}) {
// 检查业务状态
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
@ -143,6 +181,16 @@ export async function request(url, options = {}) {
if (DEBUG_MODE) {
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
}
}

View File

@ -70,7 +70,33 @@ export const errorModal = new ErrorModalManager()
// 快捷方法 - 替代alert
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) {
title = 'Service Unavailable'
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')) {
title = 'Request Not Acceptable'
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) {
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 MobileHeader from '../components/MobileHeader.vue'
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()
@ -116,6 +117,7 @@ const fetchApplyRecords = async () => {
} catch (error) {
console.error('获取申请记录失败:', error)
applyRecords.value = []
showInfo(error.errorMsg)
}
}
@ -138,7 +140,25 @@ const handleCancelApply = async (record) => {
}
} catch (error) {
console.error('撤销申请失败:', error)
showError('撤销失败,请重试')
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('撤销失败,请重试')
}
}
}
@ -167,8 +187,23 @@ const submitApplication = async () => {
errorMessage.value = response.message || 'Application failed. Please try again.'
}
} 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 {
isLoading.value = false
}