From 04420a43f18ea0931183214adfe5e4c7eaf66a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=A9=E4=BB=9488?= <> Date: Fri, 27 Feb 2026 09:39:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=AB=E7=A0=81=E8=87=AA=E5=8A=A8=E6=89=93?= =?UTF-8?q?=E5=BC=80=E5=A5=97=E9=A4=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- h5/App.vue | 237 +++++++++++++++++-------- h5/ERROR_REPORT.md | 282 ++++++++++++++++++++++++++++++ h5/api/index.js | 81 ++++++--- h5/index.html | 229 +++++++++++++++++++++++- h5/lu/axios.js | 112 +++++++----- h5/lu/errorReport.js | 270 ++++++++++++++++++++++++++++ h5/lu/index.js | 2 + h5/pages/main/bgcx/bgcx.vue | 40 +++-- h5/pages/main/index/index.vue | 67 ++++--- h5/pages/main/order/order.vue | 51 +++--- h5/pages/main/order/src/order.vue | 2 +- 11 files changed, 1176 insertions(+), 197 deletions(-) create mode 100644 h5/ERROR_REPORT.md create mode 100644 h5/lu/errorReport.js diff --git a/h5/App.vue b/h5/App.vue index 27e03b8..d8e4a59 100644 --- a/h5/App.vue +++ b/h5/App.vue @@ -4,17 +4,14 @@ * user:sa0ChunLuyu * date:2024年8月7日 20:05:05 */ - import { - ref - } from 'vue' import { $api, - $response, + $response, } from '@/api' import { onShow, - onHide, - onLoad,onError + onLoad, + onError } from '@dcloudio/uni-app' import { @@ -22,99 +19,201 @@ } from '@/store' const $store = useStore() + // 初始化错误上报 + try { + if (typeof uni.$lu !== 'undefined' && uni.$lu.errorReport) { + // 初始化全局错误捕获 + uni.$lu.errorReport.initErrorHandler() + console.log('[错误上报] 已初始化') + + // 处理早期错误缓冲区(index.html 中捕获的错误) + if (typeof window.flushErrorBuffer === 'function') { + try { + var earlyErrors = window.flushErrorBuffer() + if (earlyErrors && earlyErrors.length > 0) { + console.log('[错误上报] 早期错误数量:', earlyErrors.length) + // 重新上报早期错误 + earlyErrors.forEach(function(err) { + try { + uni.$lu.errorReport.reportError(err, 'early-error') + } catch (e) { + console.warn('[错误上报] 重新上报早期错误失败', e) + } + }) + } + } catch (e) { + console.warn('[错误上报] 处理早期错误缓冲区失败', e) + } + } + } + } catch (e) { + console.warn('[错误上报] 初始化失败', e) + } + const setConfigStore = () => { let config = {} try { const config_str = uni.getStorageSync('CONFIG_CONFIG') if (config_str) config = JSON.parse(config_str) } catch (e) { - uni.showToast({ - icon:"none", - title:e.message || '解析配置失败' - }) - console.warn('CONFIG_CONFIG 解析失败', e) + console.warn('CONFIG_CONFIG 解析失败', e) } $store.config = config - if (!config.color) { - document.body.classList.toggle('grayscale'); + try { + // 安全检查:确保 document.body 存在 + if (document && document.body && !config.color) { + document.body.classList.toggle('grayscale'); + } + } catch (e) { + console.warn('设置样式失败', e) + } + let openid_str = '' + try { + openid_str = uni.getStorageSync('OPENID') + } catch (e) { + console.warn('获取 OPENID 失败', e) + } + let url = '' + try { + url = window.location.href + } catch (e) { + console.warn('获取 URL 失败', e) } - const openid_str = uni.getStorageSync('OPENID') - let url = window.location.href if (!openid_str) { - if (url.indexOf('/pages/main/login/login') === -1) { + if (url && url.indexOf('/pages/main/login/login') === -1) { uni.redirectTo({ url: '/pages/main/login/login' }) } } - const save_info_str = uni.getStorageSync('SAVE_INFO') - if (!!save_info_str) { - $store.save_info = JSON.parse(save_info_str) - } else { + try { + const save_info_str = uni.getStorageSync('SAVE_INFO') + if (!!save_info_str) { + $store.save_info = JSON.parse(save_info_str) + } else { + $store.resetSaveInfo() + } + } catch (e) { + console.warn('解析 SAVE_INFO 失败', e) $store.resetSaveInfo() } } const getConfigConfig = async () => { - const response = await $api('ConfigConfig') - $response(response, () => { - uni.setStorageSync('CONFIG_CONFIG', JSON.stringify(response.data.config)) - setConfigStore() - }) + try { + const response = await $api('ConfigConfig') + $response(response, () => { + try { + uni.setStorageSync('CONFIG_CONFIG', JSON.stringify(response.data.config)) + setConfigStore() + } catch (e) { + console.error('保存配置失败', e) + } + }) + } catch (e) { + console.error('获取配置接口失败', e) + } } const getConfigVersion = async () => { - const response = await $api('ConfigVersion') - $response(response, () => { - const config_version = uni.getStorageSync('CONFIG_VERSION') - let get_config = false - if (!config_version) { - get_config = true - } else { - if (config_version !== response.data.version) { - get_config = true + try { + const response = await $api('ConfigVersion') + $response(response, () => { + try { + const config_version = uni.getStorageSync('CONFIG_VERSION') + let get_config = false + if (!config_version) { + get_config = true + } else { + if (config_version !== response.data.version) { + get_config = true + } + } + uni.setStorageSync('CONFIG_VERSION', response.data.version) + //if (!!get_config) { + if (true) { + getConfigConfig() + } else { + setConfigStore() + } + } catch (e) { + console.error('处理版本配置失败', e) } - } - uni.setStorageSync('CONFIG_VERSION', response.data.version) - //if (!!get_config) { - if (true) { - getConfigConfig() + }) + } catch (e) { + console.error('获取版本接口失败', e) + } + } + const handleFontSize = () => { + try { + // 设置网页字体为默认大小 + WeixinJSBridge.invoke('setFontSizeCallback', { + 'fontSize': 0 + }); + // 重写设置网页字体大小的事件 + WeixinJSBridge.on('menu:setfont', function() { + WeixinJSBridge.invoke('setFontSizeCallback', { + 'fontSize': 0 + }); + }); + } catch (e) { + console.warn('设置字体大小失败', e) + } + } + onShow(() => { + try { + if (typeof WeixinJSBridge == "object" && typeof WeixinJSBridge.invoke == "function") { + handleFontSize(); } else { - setConfigStore() + try { + if (document && document.addEventListener) { + document.addEventListener("WeixinJSBridgeReady", handleFontSize, false); + } else if (document && document.attachEvent) { + document.attachEvent("WeixinJSBridgeReady", handleFontSize); + document.attachEvent("onWeixinJSBridgeReady", handleFontSize); + } + } catch (e) { + console.warn('监听 WeixinJSBridge 失败', e) + } } - }) - } - const handleFontSize=()=> { - // 设置网页字体为默认大小 - WeixinJSBridge.invoke('setFontSizeCallback', { - 'fontSize': 0 - }); - // 重写设置网页字体大小的事件 - WeixinJSBridge.on('menu:setfont', function() { - WeixinJSBridge.invoke('setFontSizeCallback', { - 'fontSize': 0 - }); - }); - } - onShow(() => { - if (typeof WeixinJSBridge == "object" && typeof WeixinJSBridge.invoke == "function") { - - handleFontSize(); - } else { - if (document.addEventListener) { - document.addEventListener("WeixinJSBridgeReady", handleFontSize, false); - } else if (document.attachEvent) { - document.attachEvent("WeixinJSBridgeReady", handleFontSize); - document.attachEvent("onWeixinJSBridgeReady", handleFontSize); - } - } + } catch (e) { + console.warn('onShow 初始化失败', e) + } console.log(`\n %c 鹿和 %c https://sa0.online/ \n\n`, 'color: #ffffff; background: #fd6b60; padding:5px 0;', 'color: #fd6b60;background: #ffffff; padding:5px 0;') - getConfigVersion() + try { + getConfigVersion() + } catch (e) { + console.error('获取配置版本失败', e) + } + }) + onError((err) => { + console.error('[全局错误]', err) + + // iOS WebView 特殊处理:强制清理可能阻塞的loading + const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent) + if (isIOS) { + try { + uni.hideLoading() + console.log('[iOS清理] 强制关闭loading') + } catch (e) { + console.warn('[iOS清理loading失败]', e) + } + } + + // 上报错误 + try { + if (typeof uni.$lu !== 'undefined' && uni.$lu.errorReport) { + uni.$lu.errorReport.reportError(err, 'uni-app-error') + } + } catch (e) { + console.warn('[错误上报] 上报失败', e) + } }) + onLoad(()=>{ - - + + }) diff --git a/h5/ERROR_REPORT.md b/h5/ERROR_REPORT.md new file mode 100644 index 0000000..d2e45ca --- /dev/null +++ b/h5/ERROR_REPORT.md @@ -0,0 +1,282 @@ +# H5 错误上报功能说明 + +## 概述 +已为H5项目添加了全局错误上报功能,能够捕获和上报前端运行时错误,即使发生白屏也能获取到错误信息。 + +## 错误上报接口 +- **接口地址**: `https://tj-h5.hnxdfe.com/api/H5/test` +- **请求方式**: POST +- **Content-Type**: application/json + +## 上报的错误类型 + +### 1. JavaScript 运行时错误 (type: 'error') +- 语法错误 +- 引用错误 +- 类型错误 +- 范围错误等 + +### 2. Promise 未处理错误 (type: 'promise') +- Promise rejection 错误 +- async/await 未捕获的异常 + +### 3. uni-app 错误 (type: 'uni-app-error') +- uni-app 生命周期错误 +- 页面加载错误 + +### 4. 早期错误 (type: 'error' / phase: 'early') +- 应用初始化前的错误 +- 脚本加载错误 +- 白屏问题的主要来源 + +### 5. 手动上报错误 (type: 'manual') +- 业务逻辑中的自定义错误 + +## 错误数据格式 + +```json +{ + "type": "error", // 错误类型 + "message": "xxx is not defined", // 错误信息 + "stack": "Error: xxx...", // 堆栈信息 + "url": "https://...", // 当前页面URL + "ua": "Mozilla/5.0...", // 用户代理 + "timestamp": "2024-01-01T00:00:00.000Z", // 时间戳 + "openId": "xxxxx", // 用户OpenID(如果存在) + "phase": "early" // 阶段标识(仅早期错误) +} +``` + +## 功能特性 + +### 1. 多重上报保障 +错误上报采用三层保障机制,确保即使白屏也能上报: + +1. **优先使用 navigator.sendBeacon** + - 即使页面关闭也能发送 + - 不阻塞页面卸载 + - 最可靠的方式 + +2. **备用方案 fetch with keepalive** + - keepalive 选项保证请求完成 + - 适用于大多数浏览器 + +3. **兜底方案 uni.request** + - 使用 uni-app 的请求方法 + - 100ms 延迟避免阻塞 + +### 2. 早期错误捕获 +- 在 `index.html` 中尽早初始化错误监听 +- 捕获应用初始化前的错误 +- 使用错误缓冲区存储早期错误 +- 应用加载后统一重新上报 + +### 3. 性能监控 +自动上报以下性能指标: +- `loadTime`: 页面总加载时间 +- `domReadyTime`: DOM就绪时间 +- `firstPaint`: 首次渲染时间 + +## 使用方法 + +### 自动捕获(默认开启) +所有错误会自动上报,无需额外配置。 + +### 手动上报错误 +```javascript +// 在业务代码中手动上报 +uni.$lu.errorReport.report('自定义错误信息', { + // 额外的业务数据 + userId: '123', + action: 'submitOrder' +}) +``` + +### 手动上报错误对象 +```javascript +try { + // 可能出错的操作 +} catch (error) { + uni.$lu.errorReport.reportError(error, 'custom-error') +} +``` + +### 上报性能数据 +```javascript +uni.$lu.errorReport.reportPerformance({ + customMetric: 1234, + operation: 'loadData' +}) +``` + +## 日志输出 + +在开发环境中,可以在控制台看到以下日志: + +``` +[早期错误捕获] 已初始化 +[错误上报] 已初始化 +[错误上报] 早期错误数量: 2 +[错误上报] { type: 'error', message: '...', ... } +``` + +## 后端处理建议 + +### 1. 数据存储 +建议将错误数据存储到数据库或日志系统: +- MongoDB: 适合存储灵活的JSON数据 +- MySQL/PostgreSQL: 可以使用JSON字段 +- ELK Stack: 适合日志分析和检索 + +### 2. 错误统计 +可以统计以下指标: +- 错误类型分布 +- 错误发生频率 +- 受影响的用户数 +- 错误来源页面 +- iOS vs Android 错误对比 + +### 3. 告警机制 +可以设置告警规则: +- 某类错误短时间内激增 +- 同一用户连续报错 +- 白屏类型错误(phase: 'early') + +## 调试方法 + +### 1. 测试错误上报 +在浏览器控制台执行: +```javascript +// 触发一个错误 +throw new Error('测试错误上报') +``` + +### 2. 查看网络请求 +打开开发者工具 -> Network -> 查看POST请求到 `/api/H5/test` 的数据 + +### 3. 查看早期错误缓冲区 +```javascript +console.log(window.__ERROR_BUFFER__) +``` + +### 4. 小程序 WebView 设备调试(重要) + +由于小程序 WebView 无法查看控制台,已添加以下调试功能: + +#### 调试按钮 +页面右下角会显示三个调试按钮: + +1. **🐛 查看日志**(红色)- 切换日志面板显示/隐藏 +2. **🧪 测试**(绿色)- 测试日志功能是否正常工作 +3. **🗑️ 清空**(橙色)- 清空所有日志 + +#### 自动显示日志 +- 发生错误时,页面顶部会自动显示黑色半透明的调试面板 +- 面板中会显示完整的错误信息和调试日志 +- 点击 "🐛 查看日志" 按钮可手动切换显示状态 + +#### 日志内容 +日志面板中会显示: +- `[时间] [原始error事件]` - 原始错误事件的详细信息 +- `[时间] [早期错误捕获]` - 处理后的错误信息 +- `[时间] 调试系统已初始化` - 调试系统启动时的测试日志 +- 所有调试日志的时间戳和详细信息 + +#### 测试日志功能 +1. 点击 **🧪 测试** 按钮 +2. 如果日志面板出现并显示测试信息,说明日志功能正常 +3. 如果点击没反应,可能是被小程序 WebView 的安全策略限制 + +#### 获取完整日志 +如果需要将日志发送给开发者: + +方法1:截图 +- 将调试面板的内容截图 + +方法2:复制日志 +```javascript +// 在浏览器控制台执行(需要能访问控制台) +console.log(JSON.stringify(window.__DEBUG_LOGS__, null, 2)) +``` + +方法3:通过小程序调试工具 +- 如果小程序有调试模式,可以通过调试工具访问 `window.__DEBUG_LOGS__` + +#### 调试面板特性 +- 自动滚动到最新日志 +- 每条日志有分隔线,方便阅读 +- 自动换行,长文本不会溢出 +- 点击 "🐛 查看日志" 按钮切换显示/隐藏 + +## 注意事项 + +1. **隐私保护**: 错误数据不包含敏感信息,但建议后端做好脱敏处理 +2. **性能影响**: 错误上报使用异步方式,不影响页面性能 +3. **网络限制**: 如果网络不可用,错误会失败但不影响应用运行 +4. **跨域问题**: 确保接口支持 CORS 或在同一域名下 + +## iOS 白屏问题排查 + +通过错误上报,可以收集以下信息来排查 iOS 白屏问题: + +1. **早期错误 (phase: 'early')** + - 查看应用初始化前的错误 + - 可能是脚本加载或语法错误 + +2. **用户代理 (ua)** + - 识别具体的 iOS 版本 + - 区分不同的 WebView 环境 + +3. **错误堆栈 (stack)** + - 精确定位错误位置 + - 了解错误调用链 + +4. **URL 信息** + - 判断是否特定页面白屏 + - 检查路由参数是否正确 + +## 实际案例分析 + +### 案例1: iOS 14.5 微信小程序白屏 +```json +{ + "type": "error", + "message": "{}", + "stack": null, + "url": "https://tj-h5.hnxdfe.com/h5/#/pages/main/login/login?openid=oosgJj-yr2IrwSmBb3_ERy1hGMos", + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.63(0x18003f2f) NetType/4G Language/zh_CN miniProgram/wxece13dd0a5b22c73", + "timestamp": "2026-02-05T11:43:07.707Z", + "phase": "early" +} +``` + +**问题分析**: +- 错误信息为 `{}`,堆栈为 `null` +- 说明错误对象构建时出错,可能是早期错误对象结构异常 +- 发生在登录页面,URL 中带有 openid 参数 +- 环境是 iOS 14.5 + 微信 8.0.63 + 小程序 WebView + +**可能原因**: +1. 微信小程序 WebView 在早期阶段触发了 error 事件 +2. error 事件对象的结构与预期不同 +3. 某些初始化脚本在特定环境下执行失败 + +**已优化**: +- 增强了 `buildErrorInfo` 函数,处理更多异常情况 +- 添加了对 null/undefined 的特殊处理 +- JSON.stringify 失败时的降级处理 +- 确保 message 和 stack 永远不会是 null 或空对象 + +**建议后续观察**: +- 检查是否还有类似 `{}` 或 `null` 的错误信息 +- 统计 iOS 14.5 的错误频率 +- 检查登录页面是否是白屏高发页面 +- 检查 openid 参数处理是否正常 + +## 扩展建议 + +未来可以考虑: +1. 添加错误采样率(避免上报过多) +2. 添加用户反馈功能(用户主动报告问题) +3. 集成第三方错误监控(如 Sentry) +4. 添加错误聚合和去重机制 diff --git a/h5/api/index.js b/h5/api/index.js index 931c189..664816d 100644 --- a/h5/api/index.js +++ b/h5/api/index.js @@ -17,33 +17,66 @@ export const $url = (url_key) => { } } export const $api = async (url_key, data = {}, opt = {}) => { - const opt_data = { - ...$config, - ...opt, - } - const $store = useStore() - if (!(url_key in $store.api_map)) { - const api_map = await $post({ - url: opt_data.config.api_map_url - }, opt_data) - if (!api_map.status) { - uni.$lu.toast('获取接口失败') + let loadingTimer = null + + try { + const opt_data = { + ...$config, + ...opt, + } + const $store = useStore() + + // 防止loading一直显示的兜底机制(30秒后强制关闭) + loadingTimer = setTimeout(() => { + try { + uni.hideLoading() + console.warn('[api] 强制关闭loading(超时保护)') + } catch (e) { + console.warn('[api] 强制关闭loading失败', e) + } + }, 30000) + + if (!(url_key in $store.api_map)) { + const api_map = await $post({ + url: opt_data.config.api_map_url + }, opt_data) + if (!api_map.status) { + if (typeof uni.$lu !== 'undefined' && uni.$lu.toast) { + uni.$lu.toast('获取接口失败') + } + if (loadingTimer) clearTimeout(loadingTimer) + return false + } + $store.api_map = api_map.data.list + } + if (!(url_key in $store.api_map)) { + if (typeof uni.$lu !== 'undefined' && uni.$lu.toast) { + uni.$lu.toast(`接口不存在 [${url_key}]`) + } + if (loadingTimer) clearTimeout(loadingTimer) return false } - $store.api_map = api_map.data.list - } - if (!(url_key in $store.api_map)) { - uni.$lu.toast(`接口不存在 [${url_key}]`) - return false - } - const openid = uni.getStorageSync('OPENID') - if (!!openid) { - data.openid = openid + const openid = uni.getStorageSync('OPENID') + if (!!openid) { + data.openid = openid + } + const result = await $post({ + url: $store.api_map[url_key], + data + }, opt_data) + + // 清除定时器 + if (loadingTimer) clearTimeout(loadingTimer) + + return result + } catch (e) { + console.error('[api] API调用失败', url_key, e) + + // 清除定时器 + if (loadingTimer) clearTimeout(loadingTimer) + + throw e } - return await $post({ - url: $store.api_map[url_key], - data - }, opt_data) } export const $image = (path) => { diff --git a/h5/index.html b/h5/index.html index 309d38e..5ae0893 100644 --- a/h5/index.html +++ b/h5/index.html @@ -8,7 +8,234 @@ document.write( '') - + + var _window = typeof window !== 'undefined' ? window : {}; + + + + diff --git a/h5/lu/axios.js b/h5/lu/axios.js index a4b69de..5afda8d 100644 --- a/h5/lu/axios.js +++ b/h5/lu/axios.js @@ -8,50 +8,78 @@ export const $post = async ({ url, data = {} }, opt) => { - const $store = useStore() - let header = {} - if ('delete_token' in opt && !!opt.delete_token) { - if (header['Authorization']) { - delete header['Authorization'] + try { + const $store = useStore() + let header = {} + if ('delete_token' in opt && !!opt.delete_token) { + if (header['Authorization']) { + delete header['Authorization'] + } + } else { + const token = getToken() ? getToken() : ''; + header['Authorization'] = 'Bearer ' + token } - } else { - const token = getToken() ? getToken() : ''; - header['Authorization'] = 'Bearer ' + token - } - if ('delete_appid' in opt && !!opt.delete_appid) { - if (data['UNIAPP_APPID']) { - delete data['UNIAPP_APPID'] + if ('delete_appid' in opt && !!opt.delete_appid) { + if (data['UNIAPP_APPID']) { + delete data['UNIAPP_APPID'] + } + } else { + data['UNIAPP_APPID'] = opt.appid } - } else { - data['UNIAPP_APPID'] = opt.appid - } - if ('delete_apptype' in opt && !!opt.delete_apptype) { - if (data['UNIAPP_APPTYPE']) { - delete data['UNIAPP_APPTYPE'] + if ('delete_apptype' in opt && !!opt.delete_apptype) { + if (data['UNIAPP_APPTYPE']) { + delete data['UNIAPP_APPTYPE'] + } + } else { + data['UNIAPP_APPTYPE'] = opt.app_type } - } else { - data['UNIAPP_APPTYPE'] = opt.app_type - } - if (!!opt.loading) { - $store.loadingStart() - if ($store.loading === 1) uni.showLoading({ - title: opt.loading_text - }) - } - const res = await uni.request({ - url, - method: 'POST', - data, - header - }); - if (!!opt.loading) { - $store.loadingDone() - if ($store.loading === 0) uni.hideLoading() - } - if (!!res && res.data != '') { - return res.data - } else { - uni.$lu.toast("请求发生错误") - return false + + let isShowLoading = false + try { + if (!!opt.loading) { + $store.loadingStart() + isShowLoading = true + if ($store.loading === 1) { + try { + uni.showLoading({ + title: opt.loading_text + }) + } catch (loadingErr) { + console.warn('[axios] showLoading失败', loadingErr) + } + } + } + + const res = await uni.request({ + url, + method: 'POST', + data, + header + }); + + return res.data + } finally { + // 使用finally确保一定会清理loading + if (isShowLoading) { + try { + $store.loadingDone() + if ($store.loading === 0) { + uni.hideLoading() + } + } catch (cleanupErr) { + console.warn('[axios] 清理loading失败', cleanupErr) + } + } + } + } catch (e) { + console.error('[axios] 请求失败', e) + if (typeof uni.$lu !== 'undefined' && uni.$lu.toast) { + try { + uni.$lu.toast("请求发生错误") + } catch (toastErr) { + console.warn('[axios] toast失败', toastErr) + } + } + throw e } } \ No newline at end of file diff --git a/h5/lu/errorReport.js b/h5/lu/errorReport.js new file mode 100644 index 0000000..1df265c --- /dev/null +++ b/h5/lu/errorReport.js @@ -0,0 +1,270 @@ +/** + * 全局错误上报模块 + * 用于捕获和上报H5前端错误 + */ + +const ERROR_REPORT_API = 'https://tj-h5.hnxdfe.com/api/H5/test' + +/** + * 构建错误信息对象 + */ +const buildErrorInfo = (error, type = 'error') => { + const errorInfo = { + type, // error: js错误, promise: promise错误, unhandled: 未捕获异常 + message: '', + stack: '', + url: '', + ua: '', + timestamp: '', + openId: '', + rawError: null // 保留原始错误对象(用于调试) + } + + try { + errorInfo.timestamp = new Date().toISOString() + errorInfo.url = window.location.href + errorInfo.ua = navigator.userAgent + + // 获取用户信息 + try { + errorInfo.openId = uni.getStorageSync('OPENID') || '' + } catch (e) { + errorInfo.openId = '获取失败' + } + + // 处理不同类型的错误 + if (error instanceof Error) { + errorInfo.message = error.message || String(error) + errorInfo.stack = error.stack || '' + } else if (typeof error === 'string') { + errorInfo.message = error + errorInfo.stack = '' + } else if (error && typeof error === 'object') { + try { + // 检查 message 是否是 "{}" 或空字符串 + const msg = error.message || '' + + if (msg === '{}' || msg === '' || msg === 'undefined' || msg === 'null') { + // 尝试从其他字段获取信息 + if (error.reason) { + errorInfo.message = String(error.reason) + } else if (error.name) { + errorInfo.message = error.name + } else if (error.toString && typeof error.toString === 'function') { + const str = error.toString() + if (str && str !== '[object Object]') { + errorInfo.message = str + } else { + errorInfo.message = '未知错误对象(无有效信息)' + } + } else { + // 尝试序列化对象 + const jsonStr = JSON.stringify(error) + if (jsonStr && jsonStr !== '{}') { + errorInfo.message = jsonStr + } else { + errorInfo.message = '错误对象为空' + } + } + } else { + errorInfo.message = msg + } + + errorInfo.stack = error.stack || '' + + // 如果没有 stack,尝试构建堆栈信息 + if (!errorInfo.stack && error.filename) { + errorInfo.stack = `at ${error.filename}:${error.lineno || 0}:${error.colno || 0}` + } + + // 保留原始错误对象的前几个键(用于调试) + const keys = Object.keys(error).slice(0, 5) + if (keys.length > 0) { + errorInfo.rawError = {} + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + if (typeof error[key] !== 'function') { + errorInfo.rawError[key] = String(error[key]).substring(0, 100) + } + } + } + } catch (e) { + errorInfo.message = '解析错误对象失败: ' + String(e) + errorInfo.stack = '' + } + } else if (error === null || error === undefined) { + errorInfo.message = '错误对象为 ' + String(error) + errorInfo.stack = '' + } else { + errorInfo.message = String(error) || '未知错误' + errorInfo.stack = '' + } + + // 如果 message 还是空的或 {} + if (!errorInfo.message || errorInfo.message === '{}' || errorInfo.message === 'undefined') { + errorInfo.message = '无法获取错误详情(错误对象为空或无效)' + } + } catch (e) { + console.warn('[错误上报] 构建错误信息失败', e) + } + + return errorInfo +} + +/** + * 使用 sendBeacon 上报(推荐) + * 优势:即使页面关闭也能发送,不阻塞页面 + */ +const reportByBeacon = (errorInfo) => { + try { + if (navigator.sendBeacon) { + const data = JSON.stringify(errorInfo) + const blob = new Blob([data], { type: 'application/json' }) + return navigator.sendBeacon(ERROR_REPORT_API, blob) + } + } catch (e) { + console.warn('[错误上报] sendBeacon 失败', e) + } + return false +} + +/** + * 使用 fetch 上报(备用方案) + */ +const reportByFetch = (errorInfo) => { + try { + fetch(ERROR_REPORT_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(errorInfo), + keepalive: true // 即使页面关闭也保持连接 + }).catch(err => { + console.warn('[错误上报] fetch 失败', err) + }) + } catch (e) { + console.warn('[错误上报] fetch 执行失败', e) + } +} + +/** + * 使用 uni.request 上报(最后备用) + */ +const reportByUni = (errorInfo) => { + try { + uni.request({ + url: ERROR_REPORT_API, + method: 'POST', + data: errorInfo, + fail: (err) => { + console.warn('[错误上报] uni.request 失败', err) + } + }) + } catch (e) { + console.warn('[错误上报] uni.request 执行失败', e) + } +} + +/** + * 判断是否应该忽略的错误 + */ +const shouldIgnoreError = (error) => { + // 忽略请求中止错误(页面跳转时常见) + if (error && error.errMsg === 'request:fail abort') { + return true + } + if (error && error.message && error.message.includes('abort')) { + return true + } + // 忽略取消的错误 + if (error && error.name === 'AbortError') { + return true + } + return false +} + +/** + * 上报错误到服务器 + */ +const reportError = (error, type = 'error') => { + try { + // 检查是否应该忽略此错误 + if (shouldIgnoreError(error)) { + console.warn('[错误上报] 忽略请求中止错误', error) + return + } + + const errorInfo = buildErrorInfo(error, type) + + // 打印到控制台 + console.error('[错误上报]', errorInfo) + + // 优先使用 sendBeacon(最可靠) + const beaconSuccess = reportByBeacon(errorInfo) + if (beaconSuccess) { + return + } + + // 备用:使用 fetch + reportByFetch(errorInfo) + + // 兜底:使用 uni.request(异步,可能失败) + setTimeout(() => { + reportByUni(errorInfo) + }, 100) + } catch (e) { + console.error('[错误上报] 上报失败', e) + } +} + +/** + * 初始化全局错误捕获 + */ +const initErrorHandler = () => { + console.log('[错误上报] 初始化错误捕获') + + // 1. 捕获 JavaScript 运行时错误 + window.addEventListener('error', (event) => { + if (event.error) { + reportError(event.error, 'error') + } else { + reportError({ + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno + }, 'error') + } + }, true) + + // 2. 捕获 Promise 未处理的 rejection 错误 + window.addEventListener('unhandledrejection', (event) => { + reportError({ + message: 'Promise rejection', + reason: event.reason + }, 'promise') + // 阻止默认的控制台警告 + // event.preventDefault() + }) + + // 3. 捕获 Vue 错误(需要在 App.vue 中使用 app.config.errorHandler) +} + +/** + * 手动上报错误 + * 用于主动上报一些业务错误 + */ +const report = (message, data = {}) => { + const error = { + message, + ...data + } + reportError(error, 'manual') +} + +export default { + initErrorHandler, + report, + reportError +} diff --git a/h5/lu/index.js b/h5/lu/index.js index af0fe90..b09e016 100644 --- a/h5/lu/index.js +++ b/h5/lu/index.js @@ -1,8 +1,10 @@ import config from '@/config.js' import toast from './toast.js'; import format from './format.js'; +import errorReport from './errorReport.js'; export default { toast, format, config, + errorReport, }; \ No newline at end of file diff --git a/h5/pages/main/bgcx/bgcx.vue b/h5/pages/main/bgcx/bgcx.vue index c199f8f..2f7abb3 100644 --- a/h5/pages/main/bgcx/bgcx.vue +++ b/h5/pages/main/bgcx/bgcx.vue @@ -38,25 +38,29 @@ import TabBar from "@/common/TabBar.vue"; }; const gettjbgInfo = async (index) => { - tabIndex.value = index ? index : 0; - // 获取 体检报告列表 - console.log(tabIndex.value); - console.log($store, "store"); - let obj = { - // tj_status: tabIndex.value, - tj_status: 1, - hospital_id: $store.save.hospital - }; - uni.showLoading({ - title: "加载中", - }); - const response = await $api("GetReportList", obj); - $response(response, () => { - uni.hideLoading() - console.log(response, "response"); - bgcx_list.value = response.data.list; + try { + tabIndex.value = index ? index : 0; + // 获取 体检报告列表 + console.log(tabIndex.value); + console.log($store, "store"); + let obj = { + // tj_status: tabIndex.value, + tj_status: 1, + hospital_id: $store.save.hospital + }; + uni.showLoading({ + title: "加载中", + }); + const response = await $api("GetReportList", obj); uni.hideLoading(); - }); + $response(response, () => { + console.log(response, "response"); + bgcx_list.value = response.data.list; + }); + } catch (e) { + uni.hideLoading(); + console.error('[获取体检报告列表失败]', e); + } }; const config_ref = ref(null); diff --git a/h5/pages/main/index/index.vue b/h5/pages/main/index/index.vue index 699ccc5..7bdc2a8 100644 --- a/h5/pages/main/index/index.vue +++ b/h5/pages/main/index/index.vue @@ -38,12 +38,20 @@ }); const getUserInfo = async () => { - const response = await $api('UserInfo') - $response(response, () => { - // $store.user = response.data.info - $store.setUser(response.data.info); - GetNoticeFunc() - }) + try { + const response = await $api('UserInfo') + $response(response, () => { + // $store.user = response.data.info + $store.setUser(response.data.info); + GetNoticeFunc() + }) + } catch (e) { + console.error('[获取用户信息失败]', e) + // 上报错误 + if (typeof uni.$lu !== 'undefined' && uni.$lu.errorReport) { + uni.$lu.errorReport.reportError(e, 'UserInfo-error') + } + } } const switch_arr = [ // '/pages/main/index/index', @@ -233,21 +241,38 @@ //获取首页通知 let NoticeInfo=ref([]) const GetNoticeFunc = async () => { - - uni.showLoading(); - const response = await $api("GetHomeNotice",{person_id:$store.getUser().person_id,id_number:$store.getUser().id_number}); - uni.hideLoading(); - $response(response, () => { - NoticeInfo.value=response.data - if (!!$props.path && $props.path=='order') { - let orderid=$props.orderid?$props.orderid:'' - let status=$props.status?$props.status:'' - let canshu='?orderid='+orderid+'&status='+status - uni.navigateTo({ - url: "/pages/main/order/order"+canshu - }) - } - }); + try { + // 不要直接调用 uni.showLoading() + // 改用axios的loading机制 + const response = await $api("GetHomeNotice",{ + person_id:$store.getUser().person_id, + id_number:$store.getUser().id_number + }, { + loading: true, + loading_text: '加载中...' + }); + + $response(response, () => { + NoticeInfo.value=response.data + if (!!$props.path && $props.path=='order') { + let orderid=$props.orderid?$props.orderid:'' + let status=$props.status?$props.status:'' + let canshu='?orderid='+orderid+'&status='+status + uni.navigateTo({ + url: "/pages/main/order/order"+canshu + }) + }else if(!!$props.path && $props.path=='combo'){ + uni.navigateTo({ + url: "/pages/main/combo/combo" + }) + } + }); + } catch (e) { + console.error('[获取首页通知失败]', e); + if (typeof uni.$lu !== 'undefined' && uni.$lu.errorReport) { + uni.$lu.errorReport.reportError(e, 'GetHomeNotice-error') + } + } }; const getItemValue=(name) =>{ const item = NoticeInfo.value.find(item => item.label === name && item.value > 0); diff --git a/h5/pages/main/order/order.vue b/h5/pages/main/order/order.vue index e7bf7d2..a0f2c0f 100644 --- a/h5/pages/main/order/order.vue +++ b/h5/pages/main/order/order.vue @@ -43,28 +43,37 @@ import TabBar from "@/common/TabBar.vue"; const $store = useStore() const order_list = ref([]) const getOrderList = async () => { - uni.showLoading() - const response = await $api('OrderList',{ - searchInfo:searchInfo.value - }) - uni.hideLoading() - $response(response, () => { - order_list.value = response.data.list - DaiBanArr.value=response.data.DaiBanCountArr - - if(searchInfo.value.orderid && order_list.value.length>0){ - - nextTick(() => { - statusClick(getTabByOrder(order_list.value[0])) - }) - - } - if($props.status){ - status_active.value = $props.status + try { + uni.showLoading() + const response = await $api('OrderList',{ + searchInfo:searchInfo.value + }) + uni.hideLoading() + $response(response, () => { + order_list.value = response.data.list + DaiBanArr.value=response.data.DaiBanCountArr + + if(searchInfo.value.orderid && order_list.value.length>0){ + + nextTick(() => { + statusClick(getTabByOrder(order_list.value[0])) + }) + + } + if($props.status){ + status_active.value = $props.status + } + + + }) + } catch (e) { + uni.hideLoading() + console.error('[订单列表加载失败]', e) + // 可以添加错误上报 + if (typeof uni.$lu !== 'undefined' && uni.$lu.errorReport) { + uni.$lu.errorReport.reportError(e, 'OrderList-error') } - - - }) + } } const getStatusAllow = (statusActive) => { switch (statusActive) { diff --git a/h5/pages/main/order/src/order.vue b/h5/pages/main/order/src/order.vue index a9b89d7..4ba70c6 100644 --- a/h5/pages/main/order/src/order.vue +++ b/h5/pages/main/order/src/order.vue @@ -805,7 +805,7 @@ } .order_info_line_label_wrapper { - width: 140rpx; + width: 160rpx; font-weight: 400; font-size: 28rpx; color: #101010;