hookehuyr

feat(composables): 新增图片加载、用户信息和错误处理的组合式函数

添加三个组合式函数:
- useImageLoader 提供图片加载错误处理和重试逻辑
- useUserInfo 封装用户信息获取、缓存和刷新功能
- useErrorHandler 实现统一错误处理和表单验证

这些函数将用于统一处理应用中常见的业务逻辑
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 +}
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 +}
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 +}