From 1ca6e6c77af3573255b58769629a866097123bc0 Mon Sep 17 00:00:00 2001 From: yuantao Date: Mon, 1 Dec 2025 19:07:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E5=8A=9F=E8=83=BD=E9=87=8D=E6=9E=84=20-=20?= =?UTF-8?q?=E9=9B=86=E6=88=90uniapp-error-monitor=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E7=8E=B0=E4=BC=98=E9=9B=85=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E5=92=8C=E5=AE=8C=E6=95=B4=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/request.js | 16 +- common/utils/tool.js | 732 ++++++++++++------------------------------- package-lock.json | 19 ++ package.json | 3 +- 4 files changed, 231 insertions(+), 539 deletions(-) diff --git a/api/request.js b/api/request.js index 2207b10..b75df27 100644 --- a/api/request.js +++ b/api/request.js @@ -32,7 +32,21 @@ http.interceptors.response.use(response => { } if (response.data.code !== 1 || response.data.code !== 200) { - tool.reportError('network', response.data) + tool.reportError( + 'api', + { + url: response.config.url, + method: response.config.method, + statusCode: response.data.code || response.statusCode, + statusText: response.data.msg || response.data.message || '未知错误', + responseTime: Date.now() - (response.config.startTime || Date.now()), + requestData: response.config.data, + requestHeaders: response.config.header, + environment: import.meta.env.MODE, + }, + {}, + true + ) } if (response.statusCode == 200) { diff --git a/common/utils/tool.js b/common/utils/tool.js index 07ffa27..2a90d05 100644 --- a/common/utils/tool.js +++ b/common/utils/tool.js @@ -1,5 +1,113 @@ -const baseUrl = import.meta.env.VITE_BASE_URL -const assetsUrl = import.meta.env.VITE_ASSETSURL +// 环境变量处理 +const getEnvVar = (key, defaultValue = '') => { + if (typeof import !== 'undefined' && import.meta && import.meta.env) { + return import.meta.env[key] || defaultValue + } + if (typeof process !== 'undefined' && process.env) { + return process.env[key] || defaultValue + } + return defaultValue +} + +const baseUrl = getEnvVar('VITE_BASE_URL', 'https://api.example.com') +const assetsUrl = getEnvVar('VITE_ASSETSURL', '/assets/') + +// 错误监控实例 +let errorMonitor = null + +/** + * 初始化错误监控实例 + */ +function initErrorMonitorInstance() { + if (errorMonitor) return errorMonitor + + try { + // 在 Node.js 环境下不尝试导入 uniapp-error-monitor + if (typeof window === 'undefined' && typeof uni === 'undefined') { + throw new Error('非浏览器/UniApp 环境,跳过 uniapp-error-monitor 导入') + } + + // 尝试导入 uniapp-error-monitor + const ErrorMonitorModule = require('uniapp-error-monitor') + if (ErrorMonitorModule.default) { + errorMonitor = new ErrorMonitorModule.default() + } else if (ErrorMonitorModule.ErrorMonitor) { + errorMonitor = new ErrorMonitorModule.ErrorMonitor() + } else { + throw new Error('未找到可用的 ErrorMonitor 类') + } + console.log('[Tool] 使用 uniapp-error-monitor') + } catch (error) { + console.warn('[Tool] uniapp-error-monitor 导入失败,将使用本地错误监控实现:', error.message) + + // 本地错误监控实现 + errorMonitor = { + errorStats: { + total: 0, + global: 0, + promise: 0, + console: 0, + miniProgram: 0, + api: 0, + network: 0, + manual: 0, + lastErrorTime: null, + }, + + init(config = {}) { + console.log('[Tool] 使用本地错误监控实现') + // 这里可以添加本地错误监控逻辑 + }, + + reportError(type = 'manual', error, context = {}, forceSend = false) { + this.errorStats.total++ + this.errorStats[type] = (this.errorStats[type] || 0) + 1 + this.errorStats.lastErrorTime = Date.now() + console.log(`[Tool] 错误上报: ${type}`, error) + }, + + wrapPromise(promise) { + return promise.catch(error => { + this.reportError('promise', error) + throw error + }) + }, + + getErrorStats() { + return { ...this.errorStats } + }, + + resetErrorStats() { + this.errorStats = { + total: 0, + global: 0, + promise: 0, + console: 0, + miniProgram: 0, + api: 0, + network: 0, + manual: 0, + lastErrorTime: null, + } + }, + + getEnvironmentInfo() { + return { + isProduction: getEnvVar('MODE') === 'production', + mode: getEnvVar('MODE', 'development'), + platform: 'Unknown', + errorMonitorEnabled: true, + timestamp: Date.now(), + } + } + } + } + + return errorMonitor +} + +// 初始化错误监控实例 +const monitorInstance = initErrorMonitorInstance() /** * 工具类 - 提供常用的工具方法 @@ -12,32 +120,15 @@ class Tool { NONE: 0, SUCCESS: 1, LOADING: 2, + ERROR: 3, + WARNING: 4, } // 字体加载状态缓存 this.loadedFonts = new Set() - // 初始化错误统计 - this.errorStats = { - total: 0, - global: 0, - promise: 0, - console: 0, - miniProgram: 0, - lastErrorTime: null, - } - - // Promise包装方法 - this.wrapPromise = null - - // 项目信息 - this.projectInfo = { - name: 'template', - version: '1.0.0', - } - - // 尝试从 manifest.json 加载项目信息 - this._loadProjectInfo() + // 错误监控配置 + this.config = null } /** @@ -57,6 +148,8 @@ class Tool { [this.ICON_TYPES.NONE]: 'none', [this.ICON_TYPES.SUCCESS]: 'success', [this.ICON_TYPES.LOADING]: 'loading', + [this.ICON_TYPES.ERROR]: 'error', + [this.ICON_TYPES.WARNING]: 'none', // uni-app toast不支持warning图标 } uni.showToast({ @@ -360,16 +453,58 @@ class Tool { }) }) } + + /** + * 处理支付流程的完整方法 + * @param {Object} paymentData 支付参数 + * @param {Function} successCallback 成功回调 + * @param {Function} errorCallback 错误回调 + * @returns {Promise} 支付结果 + */ + async handlePayment(paymentData, successCallback = null, errorCallback = null) { + try { + // 调用微信支付 + const res = await this.requestPayment(paymentData) + this.alert('支付完成,正在验证结果...', this.ICON_TYPES.LOADING) + + // 如果有成功回调,调用它 + if (successCallback && typeof successCallback === 'function') { + await successCallback(res) + } + + return res + } catch (error) { + console.log('支付失败:', error) + + // 错误处理 + if (error.errMsg && error.errMsg.includes('cancel')) { + await this.alert('支付已取消', this.ICON_TYPES.WARNING) + } else { + await this.alert('支付失败,请重试', this.ICON_TYPES.ERROR) + } + + // 如果有错误回调,调用它 + if (errorCallback && typeof errorCallback === 'function') { + await errorCallback(error) + } + + throw error + } + } upload(filePath) { return new Promise((resolve, reject) => { uni.uploadFile({ - url: `${baseUrl}file/upload`, + url: `${baseUrl}/j1732/api/front/upload/image`, fileType: 'image', header: { - Authorization: `Bearer ${this.storage('token')}`, + 'Authori-zation': `${this.storage('token')}`, }, filePath, - name: 'file', + name: 'multipart', + formData: { + model: 'user', // 用户模块 + pid: 7, // 前台用户分类ID + }, success: ({ data }) => { resolve(JSON.parse(data)) }, @@ -380,32 +515,6 @@ class Tool { }) } - /** - * 检测是否为生产环境 - * @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 - } - - // 默认:开发环境和体验版不启用,生产环境启用 - return true - } - /** * 初始化全局错误监控 * @param {Object} options 配置选项 @@ -429,149 +538,32 @@ class Tool { ...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(), - }) - } + // 使用 uniapp-error-monitor 或本地实现初始化错误监控 + monitorInstance.init(config) - // 处理未捕获的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('错误监控已初始化') + console.log('错误监控已初始化(基于 uniapp-error-monitor)') } /** * 手动上报错误 + * @param {string} type 错误类型 ('manual', 'api', 'network', 'global', 'promise', 'console', 'miniProgram') * @param {Error|Object} error 错误对象或错误信息 * @param {Object} [context] 错误上下文信息 * @param {boolean} [forceSend=false] 强制发送(忽略环境检查) */ reportError(type = 'manual', error, context = {}, forceSend = false) { - const errorInfo = { - type, - 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(), - } - this.errorStats.total++ - this.errorStats.miniProgram++ - this.errorStats.lastErrorTime = errorInfo.timestamp + monitorInstance.reportError(type, error, context, forceSend) + } - if (forceSend) { - // 强制发送 - this._sendErrorToWebhook(errorInfo, 0, true) - } else { - this._sendErrorToWebhook(errorInfo) - } + /** + * 包装Promise,自动捕获Promise错误 + * @param {Promise} promise 要包装的Promise + * @returns {Promise} 包装后的Promise + */ + wrapPromise(promise) { + return monitorInstance.wrapPromise(promise) } /** @@ -579,21 +571,14 @@ class Tool { * @returns {Object} 错误统计信息 */ getErrorStats() { - return { ...this.errorStats } + return monitorInstance.getErrorStats() } /** * 重置错误统计 */ resetErrorStats() { - this.errorStats = { - total: 0, - global: 0, - promise: 0, - console: 0, - miniProgram: 0, - lastErrorTime: null, - } + monitorInstance.resetErrorStats() } /** @@ -601,358 +586,31 @@ class Tool { * @returns {Object} 环境信息 */ getEnvironmentInfo() { - return { - isProduction: this._isProduction(), - mode: import.meta.env.MODE, - platform: this._getUserAgent(), - errorMonitorEnabled: !!this.config, - timestamp: Date.now(), - } + return monitorInstance.getEnvironmentInfo() } /** - * 处理全局错误 - * @private + * 确认对话框 + * @param {string} content 确认内容 + * @param {string} [title='提示'] 标题 + * @returns {Promise} 用户选择结果,true为确认,false为取消 */ - _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, - }) + confirm(content, title = '提示') { + return new Promise(resolve => { + uni.showModal({ + title, + content, + confirmText: '确认', + cancelText: '取消', + success: res => { + resolve(res.confirm) + }, + fail: () => { + resolve(false) + }, }) - - 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 - */ - _loadProjectInfo() { - try { - if (uni.getSystemInfoSync()) { - const AppInfo = uni.getSystemInfoSync() - this.projectInfo.name = AppInfo.appName - this.projectInfo.version = AppInfo.appVersion - } - } catch (error) { - // 如果加载失败,使用默认信息 - console.warn('无法加载项目信息,使用默认值') - } - } - - /** - * 格式化错误消息 - * @private - */ - _formatErrorMessage(errorInfo) { - const timestamp = new Date(errorInfo.timestamp).toLocaleString('zh-CN') - - let message = `🚨 JavaScript错误报告\n` - message += `📦 项目: ${this.projectInfo.name}\n` - message += `🏷️ 版本: ${this.projectInfo.version}\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 '未知页面' } // 创建单例并导出 diff --git a/package-lock.json b/package-lock.json index 2d19ff4..5d97905 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "dependencies": { "dayjs": "*", "dotenv": "^17.2.2", + "uniapp-error-monitor": "^1.0.0", "vue": "^3.5.21" } }, @@ -274,6 +275,24 @@ "node": ">=0.10.0" } }, + "node_modules/uniapp-error-monitor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uniapp-error-monitor/-/uniapp-error-monitor-1.0.0.tgz", + "integrity": "sha512-B2dyJV8Sx9W6k/J1vg/dKf7Tsbzs50tGrVz/Qm+ruMbZ/3u0oIzvjgrnF96hdlJx21JUgWt5cigfOaTyx7qfmA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + }, + "peerDependencies": { + "@dcloudio/uni-app": "^3.0.0" + }, + "peerDependenciesMeta": { + "@dcloudio/uni-app": { + "optional": true + } + } + }, "node_modules/vue": { "version": "3.5.25", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz", diff --git a/package.json b/package.json index 3dd694d..3d2535f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "dependencies": { - "dotenv": "^17.2.2", "dayjs": "*", + "dotenv": "^17.2.2", + "uniapp-error-monitor": "^1.0.0", "vue": "^3.5.21" } }