|
|
@@ -64,7 +64,9 @@ import { verifyQrCode } from '@/api/inspection'
|
|
|
// jsQR:纯 JS QR 识别库,作为 BarcodeDetector 不可用时的 fallback(iOS Safari < 17 / 老 WebView)
|
|
|
import jsQR from 'jsqr'
|
|
|
|
|
|
-const taskId = ref<number>(0)
|
|
|
+const taskId = ref<string>('')
|
|
|
+// 注意:taskId 必须用 string 存!雪花 ID (2.08e18) 超过 JS Number.MAX_SAFE_INTEGER (9.0e15),
|
|
|
+// 用 number 会精度丢失(JSON 序列化时末几位被 round 掉,后端查不到记录)
|
|
|
const scanResult = ref<string>('')
|
|
|
const tipText = ref<string>('请将摄像头对准巡检点位的二维码')
|
|
|
const scanRetry = ref<boolean>(false)
|
|
|
@@ -78,7 +80,8 @@ let h5RafId: number | null = null
|
|
|
const slot = ref<string>('')
|
|
|
|
|
|
onLoad((q) => {
|
|
|
- taskId.value = Number(q?.taskId || 0)
|
|
|
+ // taskId 用 String 存,不要 Number() 转(雪花 ID 精度丢失)
|
|
|
+ taskId.value = String(q?.taskId || '')
|
|
|
slot.value = String(q?.slot || '')
|
|
|
// 自动启动扫码(大部分设备直接可用)
|
|
|
// iOS Safari 若因手势问题失败,会显示"点击重试"按钮
|
|
|
@@ -290,26 +293,61 @@ onUnmounted(() => {
|
|
|
showPhotoScan.value = false
|
|
|
})
|
|
|
|
|
|
+// ===== 扫码冷却:同一 QR 内容在 1.5s 内不重复弹错,避免 raf 反复命中弹 toast 干扰体验 =====
|
|
|
+let lastFailedQr: { content: string; at: number } | null = null
|
|
|
+const QR_COOLDOWN_MS = 1500
|
|
|
+
|
|
|
+/** 不匹配/位置超界/异常时:清空结果 + 重启 scanner,让用户能直接继续扫下一个 */
|
|
|
+function resumeScan() {
|
|
|
+ scanResult.value = ''
|
|
|
+ // h5StartScanner() 内部会重新 getUserMedia + 启动 raf 循环
|
|
|
+ // (h5Stream 此时已被 h5StopScanner 置 null,所以不要用 h5Stream 作判断)
|
|
|
+ h5StartScanner()
|
|
|
+}
|
|
|
+
|
|
|
async function onScanSuccess(qrContent: string) {
|
|
|
+ // 同一 QR 在冷却期内不重复处理(raf 高速识别,避免反复 toast)
|
|
|
+ const now = Date.now()
|
|
|
+ if (lastFailedQr && lastFailedQr.content === qrContent && now - lastFailedQr.at < QR_COOLDOWN_MS) {
|
|
|
+ return
|
|
|
+ }
|
|
|
uni.showLoading({ title: '校验中…' })
|
|
|
+
|
|
|
+ // ========== 1. 二维码校验(独立 try-catch,定位错误不能掩盖) ==========
|
|
|
+ let valid = false
|
|
|
try {
|
|
|
- // 1. 校验二维码与任务绑定关系
|
|
|
- // 走统一 request 后:data 即后端 R.data(这里后端直接返回 boolean)
|
|
|
- const valid = await verifyQrCode(taskId.value, qrContent)
|
|
|
- if (!valid) {
|
|
|
- uni.hideLoading()
|
|
|
- uni.showToast({ title: '二维码与任务不匹配', icon: 'none' })
|
|
|
- return
|
|
|
- }
|
|
|
+ valid = await verifyQrCode(taskId.value, qrContent)
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '二维码校验失败:' + (e?.message || '网络异常'), icon: 'none', duration: 2500 })
|
|
|
+ lastFailedQr = { content: qrContent, at: Date.now() }
|
|
|
+ setTimeout(resumeScan, QR_COOLDOWN_MS)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!valid) {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '二维码与任务不匹配,请重试', icon: 'none', duration: 2000 })
|
|
|
+ lastFailedQr = { content: qrContent, at: Date.now() }
|
|
|
+ setTimeout(resumeScan, QR_COOLDOWN_MS)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- // 2. 校验位置
|
|
|
+ // ========== 2. 位置校验(独立 try-catch + 失败不阻塞跳转) ==========
|
|
|
+ // 扫码本身已通过,位置错误不应该导致扫码流程卡死;
|
|
|
+ // 实际位置信息可以在 check 页提交时再获取,这里仅做软校验
|
|
|
+ let loc: { latitude: number; longitude: number } | null = null
|
|
|
+ try {
|
|
|
const auth = await ensureLocationAuth()
|
|
|
- if (!auth) {
|
|
|
- uni.hideLoading()
|
|
|
- return
|
|
|
+ if (auth) {
|
|
|
+ loc = await getCurrentLocation()
|
|
|
}
|
|
|
- const loc = await getCurrentLocation()
|
|
|
+ } catch (e: any) {
|
|
|
+ // H5 端用户拒绝位置 / 浏览器不支持 getLocation / 定位超时 —— 不阻塞扫码流程
|
|
|
+ console.warn('[scan] 获取位置失败(非阻塞):', e?.message || e)
|
|
|
+ loc = null
|
|
|
+ }
|
|
|
|
|
|
+ if (loc) {
|
|
|
// 任务点位经纬度(实际项目从任务详情中获取)
|
|
|
const taskLng = uni.getStorageSync(`task_lng_${taskId.value}`) || 0
|
|
|
const taskLat = uni.getStorageSync(`task_lat_${taskId.value}`) || 0
|
|
|
@@ -323,21 +361,23 @@ async function onScanSuccess(qrContent: string) {
|
|
|
icon: 'none',
|
|
|
duration: 3000
|
|
|
})
|
|
|
+ lastFailedQr = { content: qrContent, at: Date.now() }
|
|
|
+ setTimeout(resumeScan, 3000)
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- uni.hideLoading()
|
|
|
- // 3. 校验通过:跳转到打卡页
|
|
|
- // 每日多时段任务:带上当前 slot
|
|
|
- const slotQuery = slot.value ? `&slot=${encodeURIComponent(slot.value)}` : ''
|
|
|
- uni.redirectTo({
|
|
|
- url: `/pages/inspection/check/index?taskId=${taskId.value}&lng=${loc.longitude}&lat=${loc.latitude}${slotQuery}`
|
|
|
- })
|
|
|
- } catch (e) {
|
|
|
- uni.hideLoading()
|
|
|
- uni.showToast({ title: '校验失败', icon: 'none' })
|
|
|
}
|
|
|
+
|
|
|
+ // ========== 3. 通过:跳转到打卡页 ==========
|
|
|
+ uni.hideLoading()
|
|
|
+ // 每日多时段任务:带上当前 slot
|
|
|
+ // 没拿到位置也传 0,0;check 页提交时再申请位置
|
|
|
+ const slotQuery = slot.value ? `&slot=${encodeURIComponent(slot.value)}` : ''
|
|
|
+ const lng = loc?.longitude ?? 0
|
|
|
+ const lat = loc?.latitude ?? 0
|
|
|
+ uni.redirectTo({
|
|
|
+ url: `/pages/inspection/check/index?taskId=${taskId.value}&lng=${lng}&lat=${lat}${slotQuery}`
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
function onManualInput() {
|