错误提示处理
This commit is contained in:
parent
71c8ed68b3
commit
483b4371d5
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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' })
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user