const baseUrl = import.meta.env.VITE_BASE_URL const assetsUrl = import.meta.env.VITE_ASSETSURL /** * 工具类 - 提供常用的工具方法 * @class Tool */ class Tool { constructor() { // 图标类型映射 this.ICON_TYPES = { NONE: 0, SUCCESS: 1, LOADING: 2, } // 字体加载状态缓存 this.loadedFonts = new Set() // 初始化错误统计 this.errorStats = { total: 0, global: 0, promise: 0, console: 0, miniProgram: 0, lastErrorTime: null, } // Promise包装方法 this.wrapPromise = null } /** * 文字轻提示 * @param {string} str 提示文字 * @param {number} [icon=0] 提示icon (0: none, 1: success, 2: loading) * @param {number} [duration=1500] 提示时间(毫秒) */ alert(str, icon = this.ICON_TYPES.NONE, duration = 1500) { return new Promise((resolve, reject) => { if (!str && str !== 0) { console.warn('alert方法需要提供提示文字') return } const iconMap = { [this.ICON_TYPES.NONE]: 'none', [this.ICON_TYPES.SUCCESS]: 'success', [this.ICON_TYPES.LOADING]: 'loading', } uni.showToast({ title: String(str), icon: iconMap[icon] || 'none', mask: true, duration, success: () => { setTimeout(resolve, duration) }, fail: reject, }) }) } /** * 显示loading加载 * @param {string} [title=' '] 加载文案 * @param {boolean} [mask=true] 是否显示遮罩 */ loading(title = ' ', mask = true) { uni.showLoading({ title, mask }) } /** * 关闭loading提示框 */ hideLoading() { uni.hideLoading() } /** * 统一处理URL格式,确保以/开头 * @param {string} url 页面地址 * @returns {string} 格式化后的URL * @private */ _formatUrl(url) { if (!url || typeof url !== 'string') { throw new Error('URL必须是字符串') } return url.startsWith('/') ? url : `/${url}` } /** * 可返回跳转(导航到新页面) * @param {string} url 页面地址 */ navigateTo(url) { const formattedUrl = this._formatUrl(url) uni.navigateTo({ url: formattedUrl, fail: err => { console.warn('navigateTo失败,尝试switchTab:', err) uni.switchTab({ url: formattedUrl }) }, }) } /** * 不可返回跳转(重定向到新页面) * @param {string} url 页面地址 */ redirectTo(url) { uni.redirectTo({ url: this._formatUrl(url) }) } /** * 清除页面栈跳转(重新启动到新页面) * @param {string} url 页面地址 */ reLaunch(url) { uni.reLaunch({ url: this._formatUrl(url) }) } /** * 跳转tabBar页 * @param {string} url 页面地址 */ switchTab(url) { uni.switchTab({ url: this._formatUrl(url) }) } /** * 返回上一页面或指定页面 * @param {number} [delta=1] 返回的页面数 * @param {string} [fallbackUrl='/pages/index/index'] 无上一页时的回退地址 */ navigateBack(delta = 1, fallbackUrl = '/pages/index/index') { const pages = getCurrentPages() if (pages.length <= 1) { console.warn('无上一页,使用回退地址') uni.reLaunch({ url: fallbackUrl }) } else { uni.navigateBack({ delta }) } } /** * 操作本地缓存 * @param {string} key 缓存键值 * @param {any} [value] 缓存数据,不传则为读取 * @returns {any|undefined} 读取操作时返回数据 */ storage(key, value) { if (typeof key !== 'string') { throw new Error('key必须是字符串') } // 设置操作 if (value !== undefined && value !== null) { uni.setStorageSync(key, value) return } // 读取操作 if (key !== '#') { return uni.getStorageSync(key) } // 特殊操作 if (key === '#') { uni.clearStorageSync() } } /** * 删除指定缓存 * @param {string} key 要删除的缓存键 */ removeStorage(key) { if (typeof key !== 'string') { throw new Error('key必须是字符串') } uni.removeStorageSync(key) } /** * 获取缓存信息 * @returns {Object} 缓存信息 */ getStorageInfo() { return uni.getStorageInfoSync() } /** * 复制文本到剪贴板 * @param {string} data 要复制的文本 * @returns {Promise} 复制是否成功 */ async copy(data) { if (!data && data !== 0) { this.alert('暂无内容') return false } try { await new Promise((resolve, reject) => { uni.setClipboardData({ data: String(data), success: resolve, fail: reject, }) }) this.alert('复制成功') return true } catch (error) { console.error('复制失败:', error) this.alert('复制失败,请重试') return false } } /** * 导入外部字体 * @param {string} fontName 字体文件名(不含路径) * @returns {Promise} 字体加载是否成功 */ async loadFont(fontName) { if (!fontName || typeof fontName !== 'string') { throw new Error('字体名称必须是字符串') } // 检查是否已加载过 if (this.loadedFonts.has(fontName)) { return true } try { const fontFamily = fontName.replace(/\.[^/.]+$/, '') // 移除文件扩展名 await new Promise((resolve, reject) => { uni.loadFontFace({ family: fontFamily, source: `url(${assetsUrl}${fontName})`, global: true, success: resolve, fail: reject, }) }) this.loadedFonts.add(fontName) return true } catch (error) { console.error(`字体加载失败: ${fontName}`, error) return false } } /** * 保存图片到相册 * @param {string} url 图片URL * @returns {Promise} 保存是否成功 */ async saveImageToPhotos(url) { if (!url) { this.alert('图片地址不能为空') return false } try { // 检查权限 const { authSetting } = await new Promise((resolve, reject) => { uni.getSetting({ success: resolve, fail: reject, }) }) if (!authSetting['scope.writePhotosAlbum']) { // 请求权限 await new Promise((resolve, reject) => { uni.authorize({ scope: 'scope.writePhotosAlbum', success: resolve, fail: reject, }) }) } // 获取图片信息 const { path } = await new Promise((resolve, reject) => { uni.getImageInfo({ src: url, success: resolve, fail: reject, }) }) // 保存到相册 await new Promise((resolve, reject) => { uni.saveImageToPhotosAlbum({ filePath: path, success: resolve, fail: reject, }) }) this.alert('已保存到相册') return true } catch (error) { console.error('保存图片失败:', error) if (error.errMsg && error.errMsg.includes('auth')) { // 权限相关错误 await new Promise(resolve => { uni.showModal({ title: '保存失败', content: '请开启访问手机相册权限', showCancel: false, success: resolve, }) }) uni.openSetting() } else { this.alert('保存失败,请重试') } return false } } /** * 微信支付 * @param {Object} paymentData 支付参数 * @returns {Promise} 支付结果 */ requestPayment(paymentData) { return new Promise((resolve, reject) => { uni.requestPayment({ provider: 'wxpay', ...paymentData, success: resolve, fail: reject, }) }) } upload(filePath) { return new Promise((resolve, reject) => { uni.uploadFile({ url: `${baseUrl}file/upload`, fileType: 'image', header: { Authorization: `Bearer ${this.storage('token')}`, }, filePath, name: 'file', success: ({ data }) => { resolve(JSON.parse(data)) }, fail: error => { reject(error) }, }) }) } /** * 检测是否为生产环境 * @private * @returns {boolean} 是否为生产环境 */ _isProduction() { // 检查uniapp运行模式 try { const systemInfo = uni.getSystemInfoSync?.() if (systemInfo?.mode && systemInfo.mode !== 'default') { // 体验版、开发版、预览版 return false } } catch (error) { // 忽略错误,继续检测 } // 检查环境变量MODE if (import.meta.env.MODE === 'development') { return false } // 检查自定义环境变量 const enableProductionMode = import.meta.env.VITE_ENABLE_ERROR_MONITOR === 'true' const disableProductionMode = import.meta.env.VITE_DISABLE_ERROR_MONITOR === 'true' if (disableProductionMode) { return false } if (enableProductionMode) { return true } // 默认:开发环境和体验版不启用,生产环境启用 return true } /** * 初始化全局错误监控 * @param {Object} options 配置选项 * @param {boolean} [options.enableGlobalError=true] 是否启用全局错误捕获 * @param {boolean} [options.enablePromiseError=true] 是否启用Promise错误捕获 * @param {boolean} [options.enableConsoleError=true] 是否启用console.error捕获 * @param {string} [options.webhookUrl] 自定义webhook地址,不传则使用环境变量 * @param {number} [options.maxRetries=3] 发送失败时最大重试次数 * @param {number} [options.retryDelay=1000] 重试延迟时间(毫秒) * @param {boolean} [options.forceEnable=false] 强制启用错误监控(忽略环境检查) */ initErrorMonitor(options = {}) { const config = { enableGlobalError: true, enablePromiseError: true, enableConsoleError: false, webhookUrl: import.meta.env.VITE_WEBHOOK, maxRetries: 3, retryDelay: 1000, forceEnable: false, ...options, } // 环境检查:只在生产环境下启用错误监控 if (!config.forceEnable && !this._isProduction()) { console.info('当前为非生产环境,错误监控已禁用') return } // 检查webhook配置 if (!config.webhookUrl) { console.warn('错误监控初始化失败:未配置webhook地址') return } this.config = config // 全局错误捕获(uniapp环境适配) if (config.enableGlobalError) { // Web环境 if (typeof window !== 'undefined') { window.onerror = (message, source, lineno, colno, error) => { this._handleGlobalError({ type: 'global', message, source, lineno, colno, error, timestamp: Date.now(), }) } // 处理未捕获的Promise错误 window.addEventListener('unhandledrejection', event => { this._handlePromiseError({ type: 'promise', reason: event.reason, promise: event.promise, timestamp: Date.now(), }) }) } // uniapp环境 - 提供Promise包装工具 if (typeof uni !== 'undefined' && config.enablePromiseError) { // 提供一个包装Promise的方法,让开发者可以手动包装重要的Promise this.wrapPromise = promise => { const self = this return promise.catch(error => { self._handlePromiseError({ type: 'promise', reason: error, timestamp: Date.now(), }) throw error }) } } } // console.error捕获(可选) if (config.enableConsoleError) { const originalError = console.error console.error = (...args) => { originalError.apply(console, args) this._handleConsoleError({ type: 'console', args: args.map(arg => this._serializeError(arg)), timestamp: Date.now(), }) } } // 微信小程序错误捕获 if (typeof uni !== 'undefined') { // 监听小程序错误事件 uni.onError && uni.onError(error => { this._handleMiniProgramError({ type: 'miniProgram', error, timestamp: Date.now(), }) }) // 监听小程序页面错误 uni.onPageNotFound && uni.onPageNotFound(result => { this._handleMiniProgramError({ type: 'pageNotFound', path: result.path, query: result.query, timestamp: Date.now(), }) }) // 监听小程序网络请求错误 const originalRequest = uni.request uni.request = options => { return originalRequest({ ...options, fail: err => { options.fail && options.fail(err) this._handleNetworkError({ type: 'network', url: options.url, method: options.method, error: err, timestamp: Date.now(), }) }, }) } } console.log('错误监控已初始化') } /** * 手动上报错误 * @param {Error|Object} error 错误对象或错误信息 * @param {Object} [context] 错误上下文信息 * @param {boolean} [forceSend=false] 强制发送(忽略环境检查) */ reportError(error, context = {}, forceSend = false) { const errorInfo = { type: 'manual', error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : null, context, timestamp: Date.now(), url: this._getCurrentUrl(), userAgent: this._getUserAgent(), page: getCurrentPageName(), } if (forceSend) { // 强制发送 this._sendErrorToWebhook(errorInfo, 0, true) } else { this._sendErrorToWebhook(errorInfo) } } /** * 获取错误统计信息 * @returns {Object} 错误统计信息 */ getErrorStats() { return { ...this.errorStats } } /** * 重置错误统计 */ resetErrorStats() { this.errorStats = { total: 0, global: 0, promise: 0, console: 0, miniProgram: 0, lastErrorTime: null, } } /** * 获取当前环境信息 * @returns {Object} 环境信息 */ getEnvironmentInfo() { return { isProduction: this._isProduction(), mode: import.meta.env.MODE, platform: this._getUserAgent(), errorMonitorEnabled: !!this.config, timestamp: Date.now(), } } /** * 处理全局错误 * @private */ _handleGlobalError(errorInfo) { this.errorStats.total++ this.errorStats.global++ this.errorStats.lastErrorTime = errorInfo.timestamp this._sendErrorToWebhook({ ...errorInfo, message: errorInfo.message || 'Unknown global error', source: errorInfo.source || '', lineno: errorInfo.lineno || 0, colno: errorInfo.colno || 0, url: this._getCurrentUrl(), userAgent: this._getUserAgent(), page: getCurrentPageName(), }) } /** * 处理Promise错误 * @private */ _handlePromiseError(errorInfo) { this.errorStats.total++ this.errorStats.promise++ this.errorStats.lastErrorTime = errorInfo.timestamp this._sendErrorToWebhook({ ...errorInfo, reason: this._serializeError(errorInfo.reason), url: this._getCurrentUrl(), userAgent: this._getUserAgent(), page: getCurrentPageName(), }) } /** * 处理console错误 * @private */ _handleConsoleError(errorInfo) { this.errorStats.total++ this.errorStats.console++ this.errorStats.lastErrorTime = errorInfo.timestamp this._sendErrorToWebhook({ ...errorInfo, url: this._getCurrentUrl(), userAgent: this._getUserAgent(), page: getCurrentPageName(), }) } /** * 处理小程序错误 * @private */ _handleMiniProgramError(errorInfo) { this.errorStats.total++ this.errorStats.miniProgram++ this.errorStats.lastErrorTime = errorInfo.timestamp this._sendErrorToWebhook({ ...errorInfo, url: this._getCurrentUrl(), userAgent: this._getUserAgent(), page: getCurrentPageName(), }) } /** * 处理网络错误 * @private */ _handleNetworkError(errorInfo) { this.errorStats.total++ this.errorStats.miniProgram++ this.errorStats.lastErrorTime = errorInfo.timestamp this._sendErrorToWebhook({ ...errorInfo, url: this._getCurrentUrl(), userAgent: this._getUserAgent(), page: getCurrentPageName(), }) } /** * 获取当前URL * @private */ _getCurrentUrl() { if (typeof window !== 'undefined') { return window.location?.href || '' } if (typeof uni !== 'undefined') { try { const pages = getCurrentPages() if (pages && pages.length > 0) { const currentPage = pages[pages.length - 1] return currentPage.route || '' } } catch (error) { // 忽略错误 } } return '' } /** * 获取用户代理信息 * @private */ _getUserAgent() { if (typeof navigator !== 'undefined') { return navigator.userAgent || '' } if (typeof uni !== 'undefined') { try { const systemInfo = uni.getSystemInfoSync() return `${systemInfo.platform} ${systemInfo.system} ${systemInfo.model}` } catch (error) { return 'Unknown Device' } } return 'Unknown Device' } /** * 序列化错误对象 * @private */ _serializeError(error) { if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack, } } if (typeof error === 'object' && error !== null) { try { return JSON.stringify(error, null, 2) } catch (e) { return String(error) } } return String(error) } /** * 发送错误到webhook * @private */ async _sendErrorToWebhook(errorInfo, retryCount = 0, forceSend = false) { // 环境检查:只在生产环境下发送错误信息 if (!forceSend && !this._isProduction() && !this.config?.forceEnable) { console.info('非生产环境,错误信息不上报到webhook:', errorInfo.type) return } const webhookUrl = import.meta.env.VITE_WEBHOOK if (!webhookUrl) { console.error('未配置webhook地址,无法发送错误信息') return } try { // 格式化错误信息 const message = this._formatErrorMessage(errorInfo) // 使用uni.request发送POST请求(适配uniapp环境) await new Promise((resolve, reject) => { uni.request({ url: webhookUrl, method: 'POST', header: { 'Content-Type': 'application/json', }, data: { msgtype: 'text', text: { content: message, mentioned_list: [], }, }, success: resolve, fail: reject, }) }) console.log('错误信息已发送到webhook') } catch (error) { console.error('发送错误到webhook失败:', error) // 重试机制 if (retryCount < (this.config?.maxRetries || 3)) { setTimeout(() => { this._sendErrorToWebhook(errorInfo, retryCount + 1) }, (this.config?.retryDelay || 1000) * (retryCount + 1)) } } } /** * 格式化错误消息 * @private */ _formatErrorMessage(errorInfo) { const timestamp = new Date(errorInfo.timestamp).toLocaleString('zh-CN') let message = `🚨 JavaScript错误报告\n` message += `⏰ 时间: ${timestamp}\n` message += `📱 页面: ${errorInfo.page || '未知页面'}\n` message += `🌐 链接: ${errorInfo.url || '未知链接'}\n\n` switch (errorInfo.type) { case 'global': message += `🔍 错误类型: 全局错误\n` message += `📝 错误信息: ${errorInfo.message}\n` if (errorInfo.source) { message += `📂 文件: ${errorInfo.source}\n` } if (errorInfo.lineno) { message += `📍 行号: ${errorInfo.lineno}:${errorInfo.colno}\n` } break case 'promise': message += `🔍 错误类型: Promise错误\n` message += `📝 错误信息: ${this._serializeError(errorInfo.reason)}\n` break case 'console': message += `🔍 错误类型: Console错误\n` message += `📝 错误信息: ${errorInfo.args.join(' ')}\n` break case 'miniProgram': message += `🔍 错误类型: 小程序错误\n` message += `📝 错误信息: ${errorInfo.error || 'Unknown'}\n` if (errorInfo.path) { message += `📱 页面路径: ${errorInfo.path}\n` } if (errorInfo.query) { message += `🔗 查询参数: ${errorInfo.query}\n` } break case 'network': message += `🔍 错误类型: 网络错误\n` message += `📝 请求地址: ${errorInfo.url || 'Unknown'}\n` message += `📝 请求方法: ${errorInfo.method || 'Unknown'}\n` message += `📝 错误信息: ${this._serializeError(errorInfo.error)}\n` break default: message += `🔍 错误类型: ${errorInfo.type}\n` message += `📝 错误信息: ${this._serializeError(errorInfo.error)}\n` } message += `\n📊 统计信息:\n` message += `总计错误: ${this.errorStats.total}\n` message += `全局错误: ${this.errorStats.global}\n` message += `Promise错误: ${this.errorStats.promise}\n` message += `Console错误: ${this.errorStats.console}\n` message += `小程序错误: ${this.errorStats.miniProgram}\n` // 添加设备信息 if (errorInfo.userAgent) { message += `\n📱 设备信息:\n${errorInfo.userAgent}\n` } return message } } /** * 获取当前页面名称 * @returns {string} 页面名称 */ function getCurrentPageName() { try { // 尝试从getCurrentPages获取 const pages = getCurrentPages() if (pages && pages.length > 0) { const currentPage = pages[pages.length - 1] return currentPage.route || currentPage.$page?.fullPath || '未知页面' } } catch (error) { // 忽略错误,返回默认值 } // 微信小程序环境 if (typeof uni !== 'undefined') { try { const currentPages = getCurrentPages?.() if (currentPages && currentPages.length > 0) { return currentPages[currentPages.length - 1]?.route || '未知页面' } } catch (error) { return '未知页面' } } // Web环境 try { if (typeof window !== 'undefined' && window.location) { return window.location.pathname || '未知页面' } } catch (error) { return '未知页面' } return '未知页面' } // 创建单例并导出 export default new Tool()