feat(composables): 新增图片加载、用户信息和错误处理的组合式函数
添加三个组合式函数: - useImageLoader 提供图片加载错误处理和重试逻辑 - useUserInfo 封装用户信息获取、缓存和刷新功能 - useErrorHandler 实现统一错误处理和表单验证 这些函数将用于统一处理应用中常见的业务逻辑
Showing
3 changed files
with
349 additions
and
0 deletions
src/composables/useErrorHandler.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 错误处理 composable | ||
| 3 | + * 提供统一的错误处理机制 | ||
| 4 | + * | ||
| 5 | + * @Date: 2026-01-18 | ||
| 6 | + * @Description: 统一处理各种错误,提供用户友好的错误提示 | ||
| 7 | + */ | ||
| 8 | + | ||
| 9 | +import { showToast, showFailToast } from 'vant' | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * @function useErrorHandler | ||
| 13 | + * @description 提供错误处理功能 | ||
| 14 | + * @returns {Object} 包含错误处理方法的对象 | ||
| 15 | + */ | ||
| 16 | +export function useErrorHandler() { | ||
| 17 | + /** | ||
| 18 | + * 默认错误消息 | ||
| 19 | + */ | ||
| 20 | + const DEFAULT_ERROR_MESSAGE = '操作失败,请稍后重试' | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 错误消息映射 | ||
| 24 | + * 将常见的错误码映射为用户友好的提示 | ||
| 25 | + */ | ||
| 26 | + const ERROR_MESSAGES = { | ||
| 27 | + 400: '请求参数错误', | ||
| 28 | + 401: '登录已过期,请重新登录', | ||
| 29 | + 403: '没有权限执行此操作', | ||
| 30 | + 404: '请求的资源不存在', | ||
| 31 | + 500: '服务器错误,请稍后重试', | ||
| 32 | + 502: '网关错误,请稍后重试', | ||
| 33 | + 503: '服务暂不可用,请稍后重试', | ||
| 34 | + 504: '请求超时,请稍后重试' | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 处理通用错误 | ||
| 39 | + * @param {Error|Object|string} error - 错误对象或错误消息 | ||
| 40 | + * @param {string} [customMessage] - 自定义错误消息,优先使用 | ||
| 41 | + */ | ||
| 42 | + const handleError = (error, customMessage = null) => { | ||
| 43 | + console.error('Error:', error) | ||
| 44 | + | ||
| 45 | + // 使用自定义消息 | ||
| 46 | + if (customMessage) { | ||
| 47 | + showFailToast(customMessage) | ||
| 48 | + return | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 从错误对象中提取消息 | ||
| 52 | + let message = DEFAULT_ERROR_MESSAGE | ||
| 53 | + | ||
| 54 | + if (typeof error === 'string') { | ||
| 55 | + message = error | ||
| 56 | + } else if (error && error.message) { | ||
| 57 | + message = error.message | ||
| 58 | + } else if (error && error.msg) { | ||
| 59 | + message = error.msg | ||
| 60 | + } else if (error && error.response) { | ||
| 61 | + // Axios 错误 | ||
| 62 | + const status = error.response.status | ||
| 63 | + message = ERROR_MESSAGES[status] || error.response.data?.msg || DEFAULT_ERROR_MESSAGE | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + showFailToast(message) | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + /** | ||
| 70 | + * 处理 API 错误(检查 code === 1) | ||
| 71 | + * @param {Object} response - API 响应对象 | ||
| 72 | + * @param {Function} onSuccess - 成功回调 | ||
| 73 | + * @param {Function} [onFailure] - 失败回调 | ||
| 74 | + * @returns {boolean} 是否成功 | ||
| 75 | + */ | ||
| 76 | + const handleApiResponse = (response, onSuccess, onFailure = null) => { | ||
| 77 | + const { code, data, msg } = response | ||
| 78 | + | ||
| 79 | + if (code === 1) { | ||
| 80 | + if (onSuccess) { | ||
| 81 | + onSuccess(data) | ||
| 82 | + } | ||
| 83 | + return true | ||
| 84 | + } else { | ||
| 85 | + const errorMessage = msg || DEFAULT_ERROR_MESSAGE | ||
| 86 | + console.error('API Error:', errorMessage) | ||
| 87 | + | ||
| 88 | + if (onFailure) { | ||
| 89 | + onFailure(errorMessage) | ||
| 90 | + } else { | ||
| 91 | + showFailToast(errorMessage) | ||
| 92 | + } | ||
| 93 | + return false | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * 处理异步操作错误 | ||
| 99 | + * @param {Function} asyncFn - 异步函数 | ||
| 100 | + * @param {Object} options - 配置选项 | ||
| 101 | + * @param {Function} [options.onSuccess] - 成功回调 | ||
| 102 | + * @param {Function} [options.onError] - 错误回调 | ||
| 103 | + * @param {string} [options.successMessage] - 成功提示消息 | ||
| 104 | + * @param {string} [options.errorMessage] - 错误提示消息 | ||
| 105 | + * @returns {Promise<boolean>} 是否成功 | ||
| 106 | + */ | ||
| 107 | + const handleAsyncOperation = async (asyncFn, options = {}) => { | ||
| 108 | + const { | ||
| 109 | + onSuccess, | ||
| 110 | + onError, | ||
| 111 | + successMessage, | ||
| 112 | + errorMessage | ||
| 113 | + } = options | ||
| 114 | + | ||
| 115 | + try { | ||
| 116 | + const result = await asyncFn() | ||
| 117 | + | ||
| 118 | + // 检查 API 响应 | ||
| 119 | + const { code, data, msg } = result | ||
| 120 | + | ||
| 121 | + if (code === 1) { | ||
| 122 | + if (successMessage) { | ||
| 123 | + showToast(successMessage) | ||
| 124 | + } | ||
| 125 | + if (onSuccess) { | ||
| 126 | + onSuccess(data) | ||
| 127 | + } | ||
| 128 | + return true | ||
| 129 | + } else { | ||
| 130 | + const message = errorMessage || msg || DEFAULT_ERROR_MESSAGE | ||
| 131 | + showFailToast(message) | ||
| 132 | + if (onError) { | ||
| 133 | + onError(message) | ||
| 134 | + } | ||
| 135 | + return false | ||
| 136 | + } | ||
| 137 | + } catch (err) { | ||
| 138 | + handleError(err, errorMessage) | ||
| 139 | + if (onError) { | ||
| 140 | + onError(err.message || DEFAULT_ERROR_MESSAGE) | ||
| 141 | + } | ||
| 142 | + return false | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + /** | ||
| 147 | + * 验证并处理表单错误 | ||
| 148 | + * @param {Object} formData - 表单数据 | ||
| 149 | + * @param {Object} rules - 验证规则 | ||
| 150 | + * @returns {Object} 包含 isValid 和 errors 的对象 | ||
| 151 | + */ | ||
| 152 | + const validateForm = (formData, rules) => { | ||
| 153 | + const errors = {} | ||
| 154 | + | ||
| 155 | + for (const [field, rule] of Object.entries(rules)) { | ||
| 156 | + const value = formData[field] | ||
| 157 | + | ||
| 158 | + // 必填检查 | ||
| 159 | + if (rule.required && (value === undefined || value === null || value === '')) { | ||
| 160 | + errors[field] = rule.message || `${field} 为必填项` | ||
| 161 | + continue | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + // 自定义验证 | ||
| 165 | + if (rule.validator && typeof rule.validator === 'function') { | ||
| 166 | + const result = rule.validator(value, formData) | ||
| 167 | + if (result !== true) { | ||
| 168 | + errors[field] = result || rule.message || `${field} 格式不正确` | ||
| 169 | + } | ||
| 170 | + } | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + return { | ||
| 174 | + isValid: Object.keys(errors).length === 0, | ||
| 175 | + errors | ||
| 176 | + } | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + return { | ||
| 180 | + handleError, | ||
| 181 | + handleApiResponse, | ||
| 182 | + handleAsyncOperation, | ||
| 183 | + validateForm, | ||
| 184 | + DEFAULT_ERROR_MESSAGE, | ||
| 185 | + ERROR_MESSAGES | ||
| 186 | + } | ||
| 187 | +} |
src/composables/useImageLoader.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 图片加载错误处理 composable | ||
| 3 | + * 提供统一的图片加载错误处理逻辑 | ||
| 4 | + * | ||
| 5 | + * @Date: 2026-01-18 | ||
| 6 | + * @Description: 统一处理图片加载失败,替换为默认头像 | ||
| 7 | + */ | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * @function useImageLoader | ||
| 11 | + * @description 提供图片加载错误处理功能 | ||
| 12 | + * @returns {Object} 包含图片错误处理方法的对象 | ||
| 13 | + */ | ||
| 14 | +export function useImageLoader() { | ||
| 15 | + /** | ||
| 16 | + * 默认头像 URL | ||
| 17 | + */ | ||
| 18 | + const DEFAULT_AVATAR = 'https://cdn.ipadbiz.cn/mlaj/images/default-avatar.jpeg' | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * 处理图片加载错误 | ||
| 22 | + * @param {Event} e - 图片加载错误事件对象 | ||
| 23 | + * @param {string} [fallbackUrl] - 自定义的备用图片 URL | ||
| 24 | + */ | ||
| 25 | + const handleImageError = (e, fallbackUrl = DEFAULT_AVATAR) => { | ||
| 26 | + if (e.target && e.target.src) { | ||
| 27 | + e.target.src = fallbackUrl | ||
| 28 | + } | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 处理图片加载错误(带重试逻辑) | ||
| 33 | + * @param {Event} e - 图片加载错误事件对象 | ||
| 34 | + * @param {string[]} fallbackUrls - 备用图片 URL 列表,按顺序尝试 | ||
| 35 | + */ | ||
| 36 | + const handleImageErrorWithRetry = (e, fallbackUrls) => { | ||
| 37 | + if (e.target && e.target.src) { | ||
| 38 | + const currentSrc = e.target.src | ||
| 39 | + const currentIndex = fallbackUrls.indexOf(currentSrc) | ||
| 40 | + | ||
| 41 | + if (currentIndex >= 0 && currentIndex < fallbackUrls.length - 1) { | ||
| 42 | + // 尝试下一个备用 URL | ||
| 43 | + e.target.src = fallbackUrls[currentIndex + 1] | ||
| 44 | + } else if (fallbackUrls.length > 0) { | ||
| 45 | + // 使用第一个或最后的备用 URL | ||
| 46 | + e.target.src = fallbackUrls[0] | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + return { | ||
| 52 | + handleImageError, | ||
| 53 | + handleImageErrorWithRetry, | ||
| 54 | + DEFAULT_AVATAR | ||
| 55 | + } | ||
| 56 | +} |
src/composables/useUserInfo.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 用户信息获取 composable | ||
| 3 | + * 提供统一的用户信息获取逻辑 | ||
| 4 | + * | ||
| 5 | + * @Date: 2026-01-18 | ||
| 6 | + * @Description: 统一获取用户信息,处理缓存和错误 | ||
| 7 | + */ | ||
| 8 | + | ||
| 9 | +import { ref } from 'vue' | ||
| 10 | +import { getUserInfoAPI } from '@/api/users' | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * @function useUserInfo | ||
| 14 | + * @description 提供用户信息获取功能 | ||
| 15 | + * @returns {Object} 包含用户信息状态和方法的对象 | ||
| 16 | + */ | ||
| 17 | +export function useUserInfo() { | ||
| 18 | + // 用户信息状态 | ||
| 19 | + const userInfo = ref(null) | ||
| 20 | + const loading = ref(false) | ||
| 21 | + const error = ref(null) | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 刷新用户信息 | ||
| 25 | + * @returns {Promise<Object>} 用户信息对象 | ||
| 26 | + * @throws {Error} 获取失败时抛出错误 | ||
| 27 | + */ | ||
| 28 | + const refreshUserInfo = async () => { | ||
| 29 | + loading.value = true | ||
| 30 | + error.value = null | ||
| 31 | + | ||
| 32 | + try { | ||
| 33 | + const { code, data, msg } = await getUserInfoAPI() | ||
| 34 | + | ||
| 35 | + if (code === 1) { | ||
| 36 | + userInfo.value = data | ||
| 37 | + // 合并用户和打卡数据 | ||
| 38 | + const mergedUser = { | ||
| 39 | + ...data.user, | ||
| 40 | + ...data.checkin | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + // 更新本地存储 | ||
| 44 | + localStorage.setItem('currentUser', JSON.stringify(mergedUser)) | ||
| 45 | + | ||
| 46 | + loading.value = false | ||
| 47 | + return mergedUser | ||
| 48 | + } else { | ||
| 49 | + throw new Error(msg || '获取用户信息失败') | ||
| 50 | + } | ||
| 51 | + } catch (err) { | ||
| 52 | + error.value = err.message || '获取用户信息失败' | ||
| 53 | + loading.value = false | ||
| 54 | + throw err | ||
| 55 | + } | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 从本地存储获取用户信息(不发起网络请求) | ||
| 60 | + * @returns {Object|null} 用户信息对象,如果不存在则返回 null | ||
| 61 | + */ | ||
| 62 | + const getUserInfoFromLocal = () => { | ||
| 63 | + try { | ||
| 64 | + const savedUser = localStorage.getItem('currentUser') | ||
| 65 | + return savedUser ? JSON.parse(savedUser) : null | ||
| 66 | + } catch (err) { | ||
| 67 | + console.error('解析本地用户信息失败:', err) | ||
| 68 | + return null | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * 清除本地用户信息 | ||
| 74 | + */ | ||
| 75 | + const clearUserInfo = () => { | ||
| 76 | + userInfo.value = null | ||
| 77 | + localStorage.removeItem('currentUser') | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + /** | ||
| 81 | + * 初始化用户信息(优先从本地存储,必要时刷新) | ||
| 82 | + * @param {boolean} [forceRefresh=false] - 是否强制刷新,忽略本地缓存 | ||
| 83 | + * @returns {Promise<Object|null>} 用户信息对象 | ||
| 84 | + */ | ||
| 85 | + const initUserInfo = async (forceRefresh = false) => { | ||
| 86 | + if (!forceRefresh) { | ||
| 87 | + const localUser = getUserInfoFromLocal() | ||
| 88 | + if (localUser) { | ||
| 89 | + userInfo.value = localUser | ||
| 90 | + return localUser | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + return await refreshUserInfo() | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + return { | ||
| 98 | + userInfo, | ||
| 99 | + loading, | ||
| 100 | + error, | ||
| 101 | + refreshUserInfo, | ||
| 102 | + getUserInfoFromLocal, | ||
| 103 | + clearUserInfo, | ||
| 104 | + initUserInfo | ||
| 105 | + } | ||
| 106 | +} |
-
Please register or login to post a comment