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;