feat(hardware): 重构扫码逻辑,新增全局常驻扫码架构
添加HardwareAPI全局扫码管理相关方法,支持全局启动扫码、注册/清除扫描结果处理器
新增全局扫码状态防重复启动逻辑,停止扫码时重置扫描状态标记
调整App.vue启动与显示逻辑,设备激活后自动启动全局扫码
重构首页、预约扫码页面的扫码处理流程,改用全局处理器接收扫码结果
在设备初始化页面添加全局扫码启动逻辑
修改原生扫码调用超时时间从2000ms至1500ms
优化调试弹窗代码,新增员工人脸缓存调试弹窗并注释旧的二维码调试弹窗
| | |
| | | import { getDeviceActivationStorage, hasActivatedDevice } from './utils/request'; |
| | | |
| | | export default { |
| | | onLaunch() { |
| | | console.log('App Launch'); |
| | | onLaunch() { |
| | | console.log('App Launch'); |
| | | |
| | | this.checkDeviceActivation(); |
| | | this.checkDeviceActivation(); |
| | | |
| | | HardwareAPI.init().then(() => { |
| | | console.log('硬件插件全局初始化完成'); |
| | | this.restoreOfflineActivation(); |
| | | }).catch((err) => { |
| | | console.error('硬件插件全局初始化失败:', err); |
| | | }); |
| | | HardwareAPI.init().then(async () => { |
| | | console.log('硬件插件全局初始化完成'); |
| | | await this.restoreOfflineActivation(); |
| | | await this.startGlobalScanIfActivated(); |
| | | }).catch((err) => { |
| | | console.error('硬件插件全局初始化失败:', err); |
| | | }); |
| | | |
| | | initDB().then(() => { |
| | | console.log('数据库初始化完毕'); |
| | | startSyncQueue(); |
| | | }).catch((err) => { |
| | | console.error('数据库初始化失败:', err); |
| | | }); |
| | | initDB().then(() => { |
| | | console.log('数据库初始化完毕'); |
| | | startSyncQueue(); |
| | | }).catch((err) => { |
| | | console.error('数据库初始化失败:', err); |
| | | }); |
| | | |
| | | uni.onNetworkStatusChange((res) => { |
| | | if (res.isConnected && res.networkType !== 'none') { |
| | | console.log('[App] 监听到网络恢复,触发同步队列'); |
| | | startSyncQueue(); |
| | | } |
| | | }); |
| | | uni.onNetworkStatusChange((res) => { |
| | | if (res.isConnected && res.networkType !== 'none') { |
| | | console.log('[App] 监听到网络恢复,触发同步队列'); |
| | | startSyncQueue(); |
| | | } |
| | | }); |
| | | |
| | | runDailyCleanup(); |
| | | runDailyCleanup(); |
| | | |
| | | setInterval(() => { |
| | | startSyncQueue(); |
| | | }, 5 * 60 * 1000); |
| | | }, |
| | | onShow() { |
| | | console.log('App Show'); |
| | | this.checkDeviceActivation(); |
| | | }, |
| | | onHide() { |
| | | console.log('App Hide'); |
| | | // 扫码硬件只在应用退到后台/关闭时统一停止,页面切换过程中不主动 stop |
| | | HardwareAPI.stopScan(); |
| | | }, |
| | | methods: { |
| | | async restoreOfflineActivation() { |
| | | // 已激活设备在每次 App 启动时都补调一次离线激活,确保原生插件状态恢复 |
| | | if (!hasActivatedDevice()) { |
| | | return; |
| | | } |
| | | setInterval(() => { |
| | | startSyncQueue(); |
| | | }, 5 * 60 * 1000); |
| | | }, |
| | | onShow() { |
| | | console.log('App Show'); |
| | | this.checkDeviceActivation(); |
| | | this.startGlobalScanIfActivated(); |
| | | }, |
| | | onHide() { |
| | | console.log('App Hide'); |
| | | // 扫码硬件只在应用退到后台/关闭时统一停止,页面切换过程中不主动 stop |
| | | HardwareAPI.stopScan(); |
| | | }, |
| | | methods: { |
| | | async restoreOfflineActivation() { |
| | | // 已激活设备在每次 App 启动时都补调一次离线激活,确保原生插件状态恢复 |
| | | if (!hasActivatedDevice()) { |
| | | return; |
| | | } |
| | | |
| | | const { deviceFingerprint } = getDeviceActivationStorage(); |
| | | if (!deviceFingerprint) { |
| | | return; |
| | | } |
| | | const { deviceFingerprint } = getDeviceActivationStorage(); |
| | | if (!deviceFingerprint) { |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | await HardwareAPI.offlineActivateDevice({ |
| | | code: deviceFingerprint, |
| | | }); |
| | | console.log('已激活设备启动时离线激活成功'); |
| | | } catch (error) { |
| | | console.error('已激活设备启动时离线激活失败:', error); |
| | | } |
| | | }, |
| | | checkDeviceActivation() { |
| | | const pages = getCurrentPages(); |
| | | if (pages.length > 0) { |
| | | const currentPage = pages[pages.length - 1]; |
| | | if (currentPage.route === 'pages/device-init/index') { |
| | | return; |
| | | } |
| | | } |
| | | try { |
| | | await HardwareAPI.offlineActivateDevice({ |
| | | code: deviceFingerprint, |
| | | }); |
| | | console.log('已激活设备启动时离线激活成功'); |
| | | } catch (error) { |
| | | console.error('已激活设备启动时离线激活失败:', error); |
| | | } |
| | | }, |
| | | async startGlobalScanIfActivated() { |
| | | if (!hasActivatedDevice()) { |
| | | return; |
| | | } |
| | | |
| | | // 只有“服务地址 + 设备ID + 设备指纹”都存在时,才认为设备已完成激活 |
| | | if (!hasActivatedDevice()) { |
| | | console.log('未检测到设备激活信息,强制跳转激活页'); |
| | | setTimeout(() => { |
| | | uni.reLaunch({ |
| | | url: '/pages/device-init/index', |
| | | }); |
| | | }, 100); |
| | | } |
| | | }, |
| | | }, |
| | | try { |
| | | await HardwareAPI.startGlobalScan(); |
| | | console.log('全局扫码器启动成功'); |
| | | } catch (error) { |
| | | console.error('全局扫码器启动失败:', error); |
| | | } |
| | | }, |
| | | checkDeviceActivation() { |
| | | const pages = getCurrentPages(); |
| | | if (pages.length > 0) { |
| | | const currentPage = pages[pages.length - 1]; |
| | | if (currentPage.route === 'pages/device-init/index') { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 只有“服务地址 + 设备ID + 设备指纹”都存在时,才认为设备已完成激活 |
| | | if (!hasActivatedDevice()) { |
| | | console.log('未检测到设备激活信息,强制跳转激活页'); |
| | | setTimeout(() => { |
| | | uni.reLaunch({ |
| | | url: '/pages/device-init/index', |
| | | }); |
| | | }, 100); |
| | | } |
| | | }, |
| | | }, |
| | | }; |
| | | </script> |
| | | |
| | | <style> |
| | | page { |
| | | width: 100%; |
| | | height: 100%; |
| | | font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | background: #f4f7fc; |
| | | width: 100%; |
| | | height: 100%; |
| | | font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | background: #f4f7fc; |
| | | } |
| | | |
| | | view, |
| | | text { |
| | | box-sizing: border-box; |
| | | box-sizing: border-box; |
| | | } |
| | | </style> |
| | |
| | | }); |
| | | let noticeDialogTimer = null; |
| | | let noticeDialogCloseHandler = null; |
| | | let appointmentScanResultHandler = null; |
| | | const INACTIVITY_AUTO_BACK_MS = 60 * 1000; |
| | | let inactivityAutoBackTimer = null; |
| | | |
| | |
| | | }); |
| | | }; |
| | | |
| | | const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => { |
| | | return new Promise((resolve) => { |
| | | uni.showModal({ |
| | | title: '二维码解析结果', |
| | | content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`, |
| | | showCancel: false, |
| | | success: () => resolve(), |
| | | fail: () => resolve(), |
| | | }); |
| | | }); |
| | | }; |
| | | // const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => { |
| | | // return new Promise((resolve) => { |
| | | // uni.showModal({ |
| | | // title: '二维码解析结果', |
| | | // content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`, |
| | | // showCancel: false, |
| | | // success: () => resolve(), |
| | | // fail: () => resolve(), |
| | | // }); |
| | | // }); |
| | | // }; |
| | | |
| | | const handleAppointmentQrcodeValidation = (payload) => { |
| | | const validationResult = validateAppointmentQrcodePayload(payload); |
| | |
| | | '' |
| | | ); |
| | | return false; |
| | | }; |
| | | |
| | | const registerAppointmentScanHandler = () => { |
| | | appointmentScanResultHandler = async (scanData) => { |
| | | // 全局扫码器常驻运行,只有预约登记二维码步骤才消费扫码结果。 |
| | | if (currentStepName.value !== 'qr' || mode.value !== 'appointment') { |
| | | return; |
| | | } |
| | | |
| | | resetInactivityTimer(); |
| | | |
| | | try { |
| | | const decryptedResult = await parseHikvisionQrcodePayload(scanData); |
| | | await showQrcodeParseDebugModal(scanData, decryptedResult); |
| | | |
| | | // 保存解密后的预约码数据,后续身份证读卡和接口校验都直接复用 |
| | | appointmentQrcodePayload.value = decryptedResult.payload; |
| | | saveAppointmentQrPayload(decryptedResult.payload); |
| | | |
| | | console.log('扫码成功,原始预约码:', scanData); |
| | | console.log('预约码解密结果:', decryptedResult.plainText); |
| | | console.log('预约码解析对象:', decryptedResult.payload); |
| | | |
| | | if (!handleAppointmentQrcodeValidation(decryptedResult.payload)) { |
| | | return; |
| | | } |
| | | |
| | | nextStep(); |
| | | } catch (error) { |
| | | console.error('预约码解密失败:', error); |
| | | openNoticeDialog( |
| | | '未识别二维码,请联系工作人员', |
| | | '' |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | HardwareAPI.setScanResultHandler(appointmentScanResultHandler); |
| | | }; |
| | | |
| | | const unregisterAppointmentScanHandler = () => { |
| | | HardwareAPI.clearScanResultHandler(appointmentScanResultHandler); |
| | | appointmentScanResultHandler = null; |
| | | }; |
| | | |
| | | // -------------------- 区域数据加载 -------------------- |
| | |
| | | // ---------------------------------------------------- |
| | | onMounted(async () => { |
| | | resetInactivityTimer(); |
| | | registerAppointmentScanHandler(); |
| | | await loadSystemFlowConfig(); |
| | | await loadEmployeeVerifyList(); |
| | | await loadVisitAreas(); |
| | |
| | | |
| | | onUnmounted(() => { |
| | | clearInactivityTimer(); |
| | | unregisterAppointmentScanHandler(); |
| | | dismissNameInput(); |
| | | stopScanLineAnimation(); |
| | | stopNoticeDialogCountdown(); |
| | | // 扫码硬件改为在页面退出时统一关闭,步骤内切换与识别成功跳步都不主动 stop。 |
| | | HardwareAPI.stopScan(); |
| | | HardwareAPI.stopReadCard(); |
| | | HardwareAPI.stopJudgeFace(); |
| | | HardwareAPI.stopFaceVerify(); |
| | |
| | | |
| | | // 按步骤统一拉起对应的硬件动作,避免逻辑散落在多个 watch / onMounted 里 |
| | | const triggerHardwareByStep = (stepName) => { |
| | | // 只有预约登记第一步才需要扫码 |
| | | if (mode.value === 'appointment' && stepName === 'qr') { |
| | | // 二次进入扫码步骤前先复位扫码硬件,避免沿用上一次扫码残留状态。 |
| | | HardwareAPI.stopScan(); |
| | | HardwareAPI.startScan(async (scanData) => { |
| | | // 不再在步骤切换时主动 stopScan,所以这里只允许二维码步骤消费扫码结果 |
| | | if (currentStepName.value !== 'qr' || mode.value !== 'appointment') { |
| | | return; |
| | | } |
| | | |
| | | resetInactivityTimer(); |
| | | |
| | | try { |
| | | const decryptedResult = await parseHikvisionQrcodePayload(scanData); |
| | | await showQrcodeParseDebugModal(scanData, decryptedResult); |
| | | |
| | | // 保存解密后的预约码数据,后续身份证读卡和接口校验都直接复用 |
| | | appointmentQrcodePayload.value = decryptedResult.payload; |
| | | saveAppointmentQrPayload(decryptedResult.payload); |
| | | |
| | | console.log('扫码成功,原始预约码:', scanData); |
| | | console.log('预约码解密结果:', decryptedResult.plainText); |
| | | console.log('预约码解析对象:', decryptedResult.payload); |
| | | |
| | | // await new Promise((resolve) => { |
| | | // uni.showModal({ |
| | | // title: '二维码解析结果', |
| | | // content: `解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`, |
| | | // showCancel: false, |
| | | // success: () => resolve(), |
| | | // fail: () => resolve(), |
| | | // }); |
| | | // }); |
| | | |
| | | if (!handleAppointmentQrcodeValidation(decryptedResult.payload)) { |
| | | return; |
| | | } |
| | | |
| | | nextStep(); |
| | | } catch (error) { |
| | | console.error('预约码解密失败:', error); |
| | | openNoticeDialog( |
| | | '未识别二维码,请联系工作人员', |
| | | '' |
| | | ); |
| | | } |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | if (stepName === 'face') { |
| | | // 访客进入人脸步骤后自动开始判脸和比对 |
| | | startFaceVerify(); |
| | |
| | | code: deviceFingerprint.value, |
| | | }); |
| | | |
| | | await HardwareAPI.startGlobalScan(); |
| | | |
| | | uni.setStorageSync('device_id', machineId); |
| | | uni.setStorageSync('device_fingerprint', deviceFingerprint.value); |
| | | uni.hideLoading(); |
| | |
| | | let pressTimer = null; |
| | | let homeScanSessionId = 0; |
| | | let isHandlingHomeScan = false; |
| | | let homeScanResultHandler = null; |
| | | |
| | | const handleTouchStart = () => { |
| | | pressTimer = setTimeout(() => { |
| | |
| | | noticeDialog.value.visible = false; |
| | | }; |
| | | |
| | | const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => { |
| | | // const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => { |
| | | // return new Promise((resolve) => { |
| | | // uni.showModal({ |
| | | // title: '二维码解析结果', |
| | | // content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`, |
| | | // showCancel: false, |
| | | // success: () => resolve(), |
| | | // fail: () => resolve(), |
| | | // }); |
| | | // }); |
| | | // }; |
| | | |
| | | const showEmployeeFaceCacheDebugModal = (employeeList = []) => { |
| | | const localFaceEmployees = (Array.isArray(employeeList) ? employeeList : []) |
| | | .filter((employee) => /^(file:\/\/|_doc\/|\/storage\/)/.test(String(employee?.faceImage || '').trim())); |
| | | |
| | | const previewEmployees = localFaceEmployees.slice(0, 5); |
| | | const previewContent = previewEmployees.length > 0 |
| | | ? previewEmployees.map((employee, index) => { |
| | | const employeeName = employee?.employeeName || employee?.name || employee?.userName || `员工${index + 1}`; |
| | | return `${index + 1}. ${employeeName}\n${employee.faceImage}`; |
| | | }).join('\n\n') |
| | | : '本次未发现已转为本地路径的员工人脸图片'; |
| | | |
| | | return new Promise((resolve) => { |
| | | uni.showModal({ |
| | | title: '二维码解析结果', |
| | | content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`, |
| | | title: `员工人脸本地缓存(共 ${localFaceEmployees.length} 条)`, |
| | | content: previewContent, |
| | | showCancel: false, |
| | | success: () => resolve(), |
| | | fail: () => resolve(), |
| | |
| | | }); |
| | | }; |
| | | |
| | | const initHomeScanner = async () => { |
| | | const registerHomeScanHandler = () => { |
| | | homeScanSessionId += 1; |
| | | const currentSessionId = homeScanSessionId; |
| | | isHandlingHomeScan = false; |
| | | |
| | | // 扫码硬件在二次进入首页时需要先复位一次,否则原生层可能残留旧的扫码状态。 |
| | | await HardwareAPI.stopScan(); |
| | | |
| | | HardwareAPI.startScan(async (scanData) => { |
| | | homeScanResultHandler = async (scanData) => { |
| | | if (currentSessionId !== homeScanSessionId || isHandlingHomeScan) { |
| | | return; |
| | | } |
| | |
| | | isHandlingHomeScan = false; |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | HardwareAPI.setScanResultHandler(homeScanResultHandler); |
| | | }; |
| | | |
| | | const unregisterHomeScanHandler = () => { |
| | | homeScanSessionId += 1; |
| | | isHandlingHomeScan = false; |
| | | HardwareAPI.clearScanResultHandler(homeScanResultHandler); |
| | | homeScanResultHandler = null; |
| | | }; |
| | | |
| | | const initHomeData = async () => { |
| | |
| | | // 员工图片 base64 转本地文件比较重,放到首页后台静默处理,避免阻塞后续 live-pusher 拉起 |
| | | setTimeout(() => { |
| | | syncEmployeeVerifyList() |
| | | .then((employeeList) => showEmployeeFaceCacheDebugModal(employeeList)) |
| | | .catch((error) => { |
| | | console.error('首页后台同步员工验证列表失败:', error); |
| | | }); |
| | |
| | | }); |
| | | |
| | | onShow(() => { |
| | | initHomeScanner(); |
| | | registerHomeScanHandler(); |
| | | }); |
| | | |
| | | onHide(() => { |
| | | homeScanSessionId += 1; |
| | | isHandlingHomeScan = false; |
| | | HardwareAPI.stopScan(); |
| | | unregisterHomeScanHandler(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | let initResult = null; |
| | | const PLUGIN_DEBUG_ENABLED = true; |
| | | let debugModalQueue = Promise.resolve(); |
| | | let globalScanStarted = false; |
| | | let currentScanResultHandler = null; |
| | | |
| | | const getPluginMethod = (methodNames = []) => { |
| | | if (!HardwarePlugin) { |
| | |
| | | }); |
| | | }, |
| | | |
| | | setScanResultHandler(handler) { |
| | | currentScanResultHandler = typeof handler === 'function' ? handler : null; |
| | | }, |
| | | |
| | | clearScanResultHandler(handler) { |
| | | if (!handler || currentScanResultHandler === handler) { |
| | | currentScanResultHandler = null; |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * 开始扫描 (二维码) |
| | | * 建议安卓端使用 invokeAndKeepAlive |
| | |
| | | |
| | | // #ifdef APP-PLUS |
| | | if (HardwarePlugin) { |
| | | HardwarePlugin.startScan({ duration: 2000 }, (res) => { |
| | | HardwarePlugin.startScan({ duration: 1500 }, (res) => { |
| | | // showPluginDebugModal('扫码回调', res); |
| | | if (res) { |
| | | callback(res); |
| | |
| | | // #ifdef APP-PLUS |
| | | if (HardwarePlugin) { |
| | | HardwarePlugin.stopScan({}); |
| | | globalScanStarted = false; |
| | | resolve(); |
| | | } else { |
| | | resolve(); |
| | |
| | | |
| | | // #ifndef APP-PLUS |
| | | console.log('[Mock] 停止扫码'); |
| | | globalScanStarted = false; |
| | | resolve(); |
| | | // #endif |
| | | }); |
| | | }, |
| | | |
| | | startGlobalScan() { |
| | | if (globalScanStarted) { |
| | | return Promise.resolve(); |
| | | } |
| | | |
| | | return new Promise((resolve) => { |
| | | // #ifdef APP-PLUS |
| | | if (HardwarePlugin) { |
| | | HardwarePlugin.startScan({ duration: 2000 }, (res) => { |
| | | if (!res) { |
| | | console.error('扫码失败或状态更新:', res); |
| | | return; |
| | | } |
| | | |
| | | if (typeof currentScanResultHandler === 'function') { |
| | | currentScanResultHandler(res); |
| | | } |
| | | }); |
| | | globalScanStarted = true; |
| | | } |
| | | resolve(); |
| | | // #endif |
| | | |
| | | // #ifndef APP-PLUS |
| | | console.log('[Mock] 全局开始扫码,等待回调...'); |
| | | globalScanStarted = true; |
| | | resolve(); |
| | | // #endif |
| | | }); |