zjk
2026-07-24 42608ec5590bdde375c385e987dc9a862c153de1
feat: 新增启动页,重构硬件回调与多项业务流程

- 新增启动页pages/splash,处理应用初始化加载流程,自动根据设备激活状态跳转至对应页面
- 重构硬件通信逻辑,改用uni全局事件总线传递扫码、读卡回调,移除全局常驻扫码任务,改为页面按需注册监听
- 优化员工验证列表缓存读取策略,联网时优先拉取最新列表并更新本地缓存,无网络时回退使用本地缓存
- 优化访客记录导出功能,改为生成兼容Excel的xls格式文件,修复身份证号被自动转换为科学计数法的问题
- 完善离线同步队列的日志输出,添加详细的同步状态与结果日志,便于问题排查
- 调整设备初始化与页面跳转逻辑,移除device-init页面的自动全局扫码,统一在App.vue中初始化硬件监听
- 重构预约扫描页面的硬件回调处理流程,统一读卡和扫码的业务逻辑
- 清理废弃的调试代码与冗余逻辑,优化页面生命周期处理
1个文件已添加
9个文件已修改
584 ■■■■■ 已修改文件
App.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
api/employee.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/admin/AdminRecordsPanel.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages.json 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/appointment-scan/n-index.nvue 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/device-init/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/home/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/splash/index.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
utils/syncEngine.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
utils/visitorDevice.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
App.vue
@@ -8,12 +8,17 @@
    onLaunch() {
        console.log('App Launch');
        this.checkDeviceActivation();
        HardwareAPI.init().then(async () => {
            console.log('硬件插件全局初始化完成');
            await this.restoreOfflineActivation();
            await this.startGlobalScanIfActivated();
            // 全局开启扫码和读卡监听,避免多线程干扰
            HardwareAPI.startScan((res) => {
                uni.$emit('Hardware-Scan', res);
            });
            HardwareAPI.startReadCard((res) => {
                uni.$emit('Hardware-ReadCard', res);
            });
        }).catch((err) => {
            console.error('硬件插件全局初始化失败:', err);
        });
@@ -40,13 +45,11 @@
    },
    onShow() {
        console.log('App Show');
        this.checkDeviceActivation();
        this.startGlobalScanIfActivated();
    },
    onHide() {
        console.log('App Hide');
        // 扫码硬件只在应用退到后台/关闭时统一停止,页面切换过程中不主动 stop
        HardwareAPI.stopScan();
        // HardwareAPI.stopScan();
    },
    methods: {
        async restoreOfflineActivation() {
@@ -68,38 +71,7 @@
            } catch (error) {
                console.error('已激活设备启动时离线激活失败:', error);
            }
        },
        async startGlobalScanIfActivated() {
            if (!hasActivatedDevice()) {
                return;
            }
            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>
api/employee.js
@@ -294,7 +294,7 @@
  }
};
export const getLocalEmployeeVerifyList = async () => {
const getEmployeeVerifyListFromLocalCache = async () => {
  const offlineEmployeeList = await getOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY);
  const storageEmployeeList = uni.getStorageSync(EMPLOYEE_VERIFY_LIST_KEY);
  const localEmployeeList = Array.isArray(offlineEmployeeList)
@@ -327,3 +327,17 @@
  return sanitizedEmployeeList;
};
export const getLocalEmployeeVerifyList = async () => {
  const networkType = await getCurrentNetworkType();
  if (networkType !== 'none') {
    try {
      // 只要当前还能联网,每次读取员工列表都优先拉最新数据,并同步覆盖本地缓存。
      return await runEmployeeVerifyListSync();
    } catch (error) {
      console.error('联网状态刷新员工验证列表失败,回退本地缓存:', error);
    }
  }
  return getEmployeeVerifyListFromLocalCache();
};
components/admin/AdminRecordsPanel.vue
@@ -686,38 +686,63 @@
  uni.showLoading({ title: '正在生成导出文件...' });
  try {
    let csvString = '\uFEFF';
    const headers = ['访客姓名', '身份证号', '被访人', '部门', '登记方式', '访问区域', '来访事由', '状态', '有效期'];
    csvString += headers.join(',') + '\n';
    const escapeHtml = (value) => {
      if (value === null || value === undefined) {
        return '';
      }
      return String(value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
    };
    let tableRows = '<tr>';
    headers.forEach((header) => {
      tableRows += `<th>${escapeHtml(header)}</th>`;
    });
    tableRows += '</tr>';
    records.value.forEach(item => {
      const escapeCsv = (str) => {
        if (str === null || str === undefined) return '""';
        const s = String(str).replace(/"/g, '""');
        return `"${s}"`;
      };
      const hostName = item.host.split('(')[0] || item.host;
      const hostName = (item.host || '').split('(')[0] || item.host;
      const deptName = item.department;
      const row = [
        escapeCsv(item.name),
        escapeCsv(item.idCard + '\t'), // 加 \t 防止身份证被 Excel 转成科学计数法
        escapeCsv(hostName),
        escapeCsv(deptName),
        escapeCsv(item.type),
        escapeCsv(item.area),
        escapeCsv(item.reason),
        escapeCsv(item.status),
        escapeCsv(item.time)
      ];
      csvString += row.join(',') + '\n';
      tableRows += '<tr>';
      tableRows += `<td>${escapeHtml(item.name)}</td>`;
      // 用文本格式输出身份证,避免表格软件自动转成科学计数法或丢失末尾 X。
      tableRows += `<td style="mso-number-format:'\\@';">${escapeHtml(item.idCard)}</td>`;
      tableRows += `<td>${escapeHtml(hostName)}</td>`;
      tableRows += `<td>${escapeHtml(deptName)}</td>`;
      tableRows += `<td>${escapeHtml(item.type)}</td>`;
      tableRows += `<td>${escapeHtml(item.area)}</td>`;
      tableRows += `<td>${escapeHtml(item.reason)}</td>`;
      tableRows += `<td>${escapeHtml(item.status)}</td>`;
      tableRows += `<td>${escapeHtml(item.time)}</td>`;
      tableRows += '</tr>';
    });
    const fileName = `访客记录_${Date.now()}.csv`;
    const excelString = `\uFEFF
<html xmlns:o="urn:schemas-microsoft-com:office:office"
      xmlns:x="urn:schemas-microsoft-com:office:excel"
      xmlns="http://www.w3.org/TR/REC-html40">
<head>
  <meta charset="UTF-8">
  <!-- 让 Excel/WPS 按工作表渲染,避免安卓端把文件识别成普通文本 -->
</head>
<body>
  <table border="1">
    ${tableRows}
  </table>
</body>
</html>`;
    const fileName = `访客记录_${Date.now()}.xls`;
    // #ifdef H5
    const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8' });
    const blob = new Blob([excelString], { type: 'application/vnd.ms-excel;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
@@ -755,7 +780,7 @@
          writer.seek(0);
          writer.truncate(0);
          writer.write(csvString);
          writer.write(excelString);
        }, (err) => {
          uni.hideLoading();
          uni.showToast({ title: '创建文件写入器失败', icon: 'none' });
pages.json
@@ -1,6 +1,14 @@
{
  "pages": [
    {
      "path": "pages/splash/index",
      "style": {
        "navigationStyle": "custom",
        "disableScroll": true,
        "backgroundColor": "#F4F7FC"
      }
    },
    {
      "path": "pages/device-init/index",
      "style": {
        "navigationStyle": "custom",
pages/appointment-scan/n-index.nvue
@@ -790,6 +790,37 @@
let appointmentScanResultHandler = null;
const INACTIVITY_AUTO_BACK_MS = 60 * 1000;
let inactivityAutoBackTimer = null;
let pageDebugModalQueue = Promise.resolve();
const formatPageDebugPayload = (payload) => {
  if (payload instanceof Error) {
    return payload.message;
  }
  if (typeof payload === 'string') {
    return payload;
  }
  try {
    const text = JSON.stringify(payload, null, 2);
    return text.length > 1600 ? `${text.slice(0, 1600)}\n...内容过长,已截断` : text;
  } catch (error) {
    return String(payload);
  }
};
const showPageDebugModal = (title, payload) => {
  const content = formatPageDebugPayload(payload);
  pageDebugModalQueue = pageDebugModalQueue.finally(() => new Promise((resolve) => {
    uni.showModal({
      title,
      content,
      showCancel: false,
      success: () => resolve(),
      fail: () => resolve(),
    });
  }));
};
const clearInactivityTimer = () => {
  if (inactivityAutoBackTimer) {
@@ -2042,37 +2073,13 @@
};
// 接待人人脸验证:
// - 预约/签离:按预约中的接待人信息精确匹配员工后做人脸比对
// - 现场登记:没有默认接待人,按所有带人脸照片的员工依次轮询比对,直到成功或全部失败
// - 统一按员工列表中所有带人脸照片的员工依次轮询比对
// - 预约场景仍保留刷卡时对指定接待人的精确校验,但纯人脸验证不再预先锁定某一个员工
const startHostVerify = async () => {
  // 接待人验证不再此处阻塞拉取员工列表,改为直接使用最新缓存
  // (在进入该页面前的 onLoad 或前置页面静默完成拉取)
  employeeVerifyList.value = await getLocalEmployeeVerifyList();
  hostMatchedEmployee.value = findMatchedHostEmployee();
  hostMatchedEmployee.value = null;
  const isOnsiteHostVerify = mode.value === 'online';
  const matchedEmployee = isOnsiteHostVerify ? null : findMatchedHostEmployee();
  const candidateFaceEmployees = isOnsiteHostVerify ? getHostFaceEmployees() : [];
  hostMatchedEmployee.value = matchedEmployee;
  if (!isOnsiteHostVerify && !matchedEmployee) {
    openNoticeDialog(
      '未找到接待人员工信息',
      '请确认接待人信息与员工档案一致'
    );
    return;
  }
  const employeeFacePhotoAddress = isOnsiteHostVerify ? '' : resolveEmployeeFacePhotoAddress(matchedEmployee);
  if (!isOnsiteHostVerify && !employeeFacePhotoAddress) {
    openNoticeDialog(
      '未获取到接待人人脸照片',
      '当前接待人员工档案未配置人脸照片,请先完善员工资料'
    );
    return;
  }
  if (isOnsiteHostVerify && candidateFaceEmployees.length === 0) {
  const candidateFaceEmployees = getHostFaceEmployees();
  if (candidateFaceEmployees.length === 0) {
    openNoticeDialog(
      '未获取到员工人脸照片',
      '当前员工列表中没有可用于比对的人脸照片,请先同步员工档案'
@@ -2097,12 +2104,7 @@
            }
            hostVerifyStatusText.value = '人脸比对中...';
            let hostCompareResult = null;
            if (isOnsiteHostVerify) {
              hostCompareResult = await compareHostFaceWithAllEmployees(livePhotoAddress);
            } else {
              hostCompareResult = await compareHostFaceWithEmployee(livePhotoAddress, matchedEmployee);
            }
            const hostCompareResult = await compareHostFaceWithAllEmployees(livePhotoAddress);
            if (sessionId !== judgeFaceSessionId) {
              return;
@@ -2112,9 +2114,7 @@
              hostVerifyStatusText.value = '接待人验证失败';
              openHostFaceVerifyFailureDialog(
                '接待人验证失败,请重新验证',
                isOnsiteHostVerify
                  ? '未匹配到现场接待人员工,请重新验证'
                  : '请正视屏幕重新进行人脸认证'
                '请正视屏幕重新进行人脸认证'
              );
              return;
            }
@@ -2381,8 +2381,27 @@
const registerAppointmentScanHandler = () => {
  appointmentScanResultHandler = async (scanData) => {
    // 全局扫码器常驻运行,只有预约登记二维码步骤才消费扫码结果。
    console.log('[AppointmentScan] 收到扫码结果:', {
      scanData,
      mode: mode.value,
      currentStepName: currentStepName.value,
    });
    // 页面内扫码回调会一直存在,只有预约登记二维码步骤才消费扫码结果。
    if (currentStepName.value !== 'qr' || mode.value !== 'appointment') {
      console.warn('[AppointmentScan] 扫码结果已忽略,未处于预约二维码步骤:', {
        mode: mode.value,
        currentStepName: currentStepName.value,
      });
      return;
    }
    if (!scanData) {
      console.warn('[AppointmentScan] 扫码结果为空,按未识别二维码处理');
      openNoticeDialog(
        '未识别二维码,请联系工作人员',
        ''
      );
      return;
    }
@@ -2414,12 +2433,14 @@
    }
  };
  HardwareAPI.setScanResultHandler(appointmentScanResultHandler);
  uni.$on('Hardware-Scan', appointmentScanResultHandler);
};
const unregisterAppointmentScanHandler = () => {
  HardwareAPI.clearScanResultHandler(appointmentScanResultHandler);
  appointmentScanResultHandler = null;
  if (appointmentScanResultHandler) {
    uni.$off('Hardware-Scan', appointmentScanResultHandler);
    appointmentScanResultHandler = null;
  }
};
// -------------------- 区域数据加载 --------------------
@@ -2455,10 +2476,6 @@
onMounted(async () => {
  resetInactivityTimer();
  registerAppointmentScanHandler();
  await loadSystemFlowConfig();
  await loadEmployeeVerifyList();
  await loadVisitAreas();
  clearAppointmentIdCardPayload();
  // 首页扫码成功后会直接跳到预约登记的身份证步骤,这里需要保留并恢复之前已缓存的二维码数据,
  // 否则后续身份证一致性校验会因为拿不到 visitorIdCard 而被误判为不一致。
@@ -2471,76 +2488,10 @@
  hasMountedHardwareFlow = true;
  // 页面加载后立即开启身份证读卡监听。
  // 只有当前处于身份证读卡步骤或接待人验证步骤时,才消费回调结果。
  HardwareAPI.startReadCard(async (cardData) => {
    resetInactivityTimer();
    if (currentStepName.value === 'host') {
      const hostCardVerifyResult = verifyHostEmployeeCard(cardData);
      if (hostCardVerifyResult.reason === 'card_no_missing') {
        return;
      }
      if (!hostCardVerifyResult.passed) {
        if (hostCardVerifyResult.reason === 'employee_not_found') {
          openNoticeDialog(
            '未找到接待人员工信息',
            '请确认首页已成功同步员工列表,且预约中的接待人信息与员工档案一致'
          );
          return;
        }
        if (hostCardVerifyResult.reason === 'employee_card_missing') {
          openNoticeDialog(
            '接待人员工未配置卡号',
            '当前员工档案缺少卡号,无法完成刷卡校验,请改用人脸验证'
          );
          return;
        }
        openNoticeDialog(
          '接待人卡号不匹配',
          '当前刷卡结果与接待人员工卡号不一致,请重新刷卡或改用人脸验证'
        );
        return;
      }
      await resetFacePreviewState();
      nextStep();
      return;
    }
    if (currentStepName.value !== 'idcard' || isManualInput.value) {
      return;
    }
    console.log('读卡成功,拿到身份证信息:', cardData);
    if (mode.value === 'signout') {
      const hasEntry = await verifySignoutEntry(cardData?.licId);
      if (!hasEntry) return;
    }
    if (mode.value === 'appointment' && !isAppointmentIdentityMatched(cardData)) {
      openNoticeDialog(
        '身份信息不一致',
        '请使用预约访客本人的身份证完成核验'
      );
      return;
    }
    if (mode.value === 'online' || mode.value === 'appointment') {
      const hasEntry = await verifyOnsiteEntry(cardData?.licId);
      if (hasEntry) return;
    }
    // 保存身份证识别结果,后续可用于身份校验和人脸照片比对
    appointmentIdCardPayload.value = cardData;
    saveAppointmentIdCardPayload(cardData);
    advanceToFaceStep();
  });
  await loadSystemFlowConfig();
  await loadEmployeeVerifyList();
  await loadVisitAreas();
  clearAppointmentIdCardPayload();
  // 首屏硬件动作不要依赖 init 回调,否则 init 卡住时扫码第一步不会触发
  triggerHardwareByStep(currentStepName.value);
@@ -2560,7 +2511,7 @@
  dismissNameInput();
  stopScanLineAnimation();
  stopNoticeDialogCountdown();
  HardwareAPI.stopReadCard();
  stopReadCardListener();
  HardwareAPI.stopJudgeFace();
  HardwareAPI.stopFaceVerify();
  stopLivePreview(faceCameraContext);
@@ -2623,6 +2574,8 @@
// 按步骤统一拉起对应的硬件动作,避免逻辑散落在多个 watch / onMounted 里
const triggerHardwareByStep = (stepName) => {
  syncReadCardByStep(stepName);
  if (stepName === 'face') {
    // 访客进入人脸步骤后自动开始判脸和比对
    startFaceVerify();
@@ -2638,6 +2591,69 @@
    setTimeout(() => {
      startPrintTicket();
    }, 500);
  }
};
let isReadCardListening = false;
const shouldEnableReadCardByStep = (stepName) => {
  return stepName === 'idcard';
};
const handleReadCardResult = async (cardData) => {
  resetInactivityTimer();
  if (currentStepName.value !== 'idcard' || isManualInput.value) {
    return;
  }
  console.log('读卡成功,拿到身份证信息:', cardData);
  if (mode.value === 'signout') {
    const hasEntry = await verifySignoutEntry(cardData?.licId);
    if (!hasEntry) return;
  }
  if (mode.value === 'appointment' && !isAppointmentIdentityMatched(cardData)) {
    openNoticeDialog(
      '身份信息不一致',
      '请使用预约访客本人的身份证完成核验'
    );
    return;
  }
  if (mode.value === 'online' || mode.value === 'appointment') {
    const hasEntry = await verifyOnsiteEntry(cardData?.licId);
    if (hasEntry) return;
  }
  // 保存身份证识别结果,后续可用于身份校验和人脸照片比对
  appointmentIdCardPayload.value = cardData;
  saveAppointmentIdCardPayload(cardData);
  advanceToFaceStep();
};
const startReadCardListener = () => {
  if (isReadCardListening) {
    return;
  }
  uni.$on('Hardware-ReadCard', handleReadCardResult);
  isReadCardListening = true;
};
const stopReadCardListener = () => {
  if (!isReadCardListening) {
    return;
  }
  uni.$off('Hardware-ReadCard', handleReadCardResult);
  isReadCardListening = false;
};
const syncReadCardByStep = (stepName) => {
  if (shouldEnableReadCardByStep(stepName)) {
    startReadCardListener();
  }
};
@@ -2837,6 +2853,11 @@
    let formData = {};
    // 统一使用本地时间格式,避免 toISOString() 转成 UTC 后导致时区偏差
    const nowFormat = formatStandardDateTime(new Date());
    const recordLocalImagePath = String(
      appointmentIdCardPayload.value?.livePhotoAddress
      || liveFacePhotoPath.value
      || ''
    ).trim();
    if (mode.value === 'signout') {
      formData = {
@@ -2876,10 +2897,12 @@
    const offlineRecord = {
      id: `record_${Date.now()}`,
      type: mode.value,
      localImgPath: liveFacePhotoPath.value || '',
      localImgPath: recordLocalImagePath,
      payload: formData,
    };
    console.log('[CheckIn] 准备写入离线记录:', offlineRecord);
    showPageDebugModal('离线记录写入前', offlineRecord);
    await insertOfflineRecord(offlineRecord);
    console.log('离线记录保存成功');
    // uni.showModal({
pages/device-init/index.vue
@@ -128,8 +128,6 @@
            code: deviceFingerprint.value,
        });
        await HardwareAPI.startGlobalScan();
        uni.setStorageSync('device_id', machineId);
        uni.setStorageSync('device_fingerprint', deviceFingerprint.value);
        uni.hideLoading();
pages/home/index.vue
@@ -241,29 +241,6 @@
//   });
// };
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: `员工人脸本地缓存(共 ${localFaceEmployees.length} 条)`,
      content: previewContent,
      showCancel: false,
      success: () => resolve(),
      fail: () => resolve(),
    });
  });
};
const registerHomeScanHandler = () => {
  homeScanSessionId += 1;
  const currentSessionId = homeScanSessionId;
@@ -275,6 +252,7 @@
    }
    if (!scanData) {
      openNoticeDialog('未识别二维码,请联系工作人员', '');
      return;
    }
@@ -283,7 +261,7 @@
    try {
      const decryptedResult = await parseHikvisionQrcodePayload(scanData);
      const payload = decryptedResult.payload;
      await showQrcodeParseDebugModal(scanData, decryptedResult);
    //   await showQrcodeParseDebugModal(scanData, decryptedResult);
      const validation = validateAppointmentQrcodePayload(payload);
      if (!validation.valid) {
@@ -315,14 +293,16 @@
    }
  };
  HardwareAPI.setScanResultHandler(homeScanResultHandler);
  uni.$on('Hardware-Scan', homeScanResultHandler);
};
const unregisterHomeScanHandler = () => {
  homeScanSessionId += 1;
  isHandlingHomeScan = false;
  HardwareAPI.clearScanResultHandler(homeScanResultHandler);
  homeScanResultHandler = null;
  if (homeScanResultHandler) {
    uni.$off('Hardware-Scan', homeScanResultHandler);
    homeScanResultHandler = null;
  }
};
const initHomeData = async () => {
@@ -355,7 +335,6 @@
  // 员工图片 base64 转本地文件比较重,放到首页后台静默处理,避免阻塞后续 live-pusher 拉起
  setTimeout(() => {
    syncEmployeeVerifyList()
      .then((employeeList) => showEmployeeFaceCacheDebugModal(employeeList))
      .catch((error) => {
        console.error('首页后台同步员工验证列表失败:', error);
      });
pages/splash/index.vue
New file
@@ -0,0 +1,38 @@
<template>
  <view class="splash-container">
    <!-- 空白背景,由 JS 唤起加载框 -->
  </view>
</template>
<script setup>
import { onMounted } from 'vue';
import { hasActivatedDevice } from '../../utils/request';
onMounted(() => {
  uni.showLoading({
    title: '加载中...',
    mask: true
  });
  setTimeout(() => {
    uni.hideLoading();
    if (hasActivatedDevice()) {
      uni.reLaunch({
        url: '/pages/home/index',
      });
    } else {
      uni.reLaunch({
        url: '/pages/device-init/index',
      });
    }
  }, 100);
});
</script>
<style scoped>
.splash-container {
  width: 100vw;
  height: 100vh;
  background-color: #F4F7FC;
}
</style>
utils/syncEngine.js
@@ -24,14 +24,20 @@
 * 4. 成功后:将数据库记录置为已同步,并删除本地沙盒图片释放内存
 */
export const startSyncQueue = async () => {
  console.log('[Sync] startSyncQueue 进入,当前同步状态:', {
    isSyncing,
  });
  if (isSyncing) {
    console.warn('[Sync] 同步队列已在执行中,本次跳过');
    return;
  }
  // 简单判断当前网络是否可用
  const networkType = await getCurrentNetworkType();
  console.log('[Sync] 当前网络状态:', networkType);
  if (networkType === 'none') {
    console.warn('[Sync] 当前离线,跳过同步队列');
    return;
  }
@@ -41,12 +47,22 @@
    let pendingRecords = [];
    try {
      pendingRecords = await getPendingSyncRecords();
      console.log('[Sync] 读取到待同步记录:', {
        count: pendingRecords.length,
        records: pendingRecords.map((record) => ({
          id: record.id,
          type: record.type,
          localImgPath: record.local_img_path,
          createTime: record.create_time,
        })),
      });
    } catch (dbErr) {
      console.error('[Sync] 读取待同步记录失败:', dbErr);
      return;
    }
    if (pendingRecords.length === 0) {
      isSyncing = false;
      console.warn('[Sync] 当前没有待同步记录');
      return;
    }
@@ -66,7 +82,18 @@
        // 1. 如果存在本地图片,先上传图片,再把云端地址回填到业务 payload
        if (record.local_img_path) {
          console.log('[Sync] 开始上传本地图片:', {
            recordId: record.id,
            type: record.type,
            localImgPath: record.local_img_path,
          });
          const uploadRes = await uploadImage(record.local_img_path);
          console.log('[Sync] 图片上传结果:', {
            recordId: record.id,
            type: record.type,
            localImgPath: record.local_img_path,
            uploadRes,
          });
          if (uploadRes && uploadRes.code === 200 && uploadRes.url) {
            // 根据记录类型,将云端 URL 赋值给不同的业务 payload 字段
            if (record.type === 'signout') {
@@ -101,11 +128,26 @@
      try {
        let submitRes;
        if (item.type === 'signout') {
          console.log('[Sync] 开始调用签离接口:', {
            recordId: item.recordId,
            payload: item.payload,
          });
          submitRes = await idCardSingOut(item.payload);
        } else {
          // type 为 appointment, online, 或它们加上 _signedout 后缀时,都作为进场记录同步
          console.log('[Sync] 开始调用登记接口:', {
            recordId: item.recordId,
            type: item.type,
            payload: item.payload,
          });
          submitRes = await addVisitorRecord(item.payload);
        }
        console.log('[Sync] 单条业务接口返回:', {
          recordId: item.recordId,
          type: item.type,
          submitRes,
        });
        if (submitRes && submitRes.code === 200) {
          await markRecordSynced(item.recordId);
@@ -120,7 +162,12 @@
    if (signinDataToPull.length > 0) {
       try {
         const payloads = signinDataToPull.map(item => item.payload);
         console.log('[Sync] 开始调用批量补发签到接口:', {
           count: payloads.length,
           payloads,
         });
         const pullRes = await pullSigninData(payloads);
         console.log('[Sync] 批量补发签到接口结果:', pullRes);
         if (pullRes && pullRes.code === 200) {
            for (let item of signinDataToPull) {
               await markRecordSynced(item.recordId);
@@ -136,7 +183,12 @@
    if (signoutDataToPull.length > 0) {
       try {
         const payloads = signoutDataToPull.map(item => item.payload);
         console.log('[Sync] 开始调用批量补发签离接口:', {
           count: payloads.length,
           payloads,
         });
         const pullRes = await pullSignoutData(payloads);
         console.log('[Sync] 批量补发签离接口结果:', pullRes);
         if (pullRes && pullRes.code === 200) {
            for (let item of signoutDataToPull) {
               await markRecordSynced(item.recordId);
utils/visitorDevice.js
@@ -14,8 +14,6 @@
let initResult = null;
const PLUGIN_DEBUG_ENABLED = true;
let debugModalQueue = Promise.resolve();
let globalScanStarted = false;
let currentScanResultHandler = null;
const getPluginMethod = (methodNames = []) => {
  if (!HardwarePlugin) {
@@ -159,7 +157,7 @@
    // #ifdef APP-PLUS
    if (HardwarePlugin) {
      HardwarePlugin.startReadCard({}, (res) => {
        // showPluginDebugModal('读卡回调', res);
        showPluginDebugModal('读卡回调', res);
        // 如果插件回调中包含 licId (身份证号),说明读卡成功
        const resData = JSON.parse(res || '{}');
        if (resData && resData.licId) {
@@ -199,16 +197,6 @@
    });
  },
  setScanResultHandler(handler) {
    currentScanResultHandler = typeof handler === 'function' ? handler : null;
  },
  clearScanResultHandler(handler) {
    if (!handler || currentScanResultHandler === handler) {
      currentScanResultHandler = null;
    }
  },
  /**
   * 开始扫描 (二维码)
   * 建议安卓端使用 invokeAndKeepAlive
@@ -222,8 +210,8 @@
    // #ifdef APP-PLUS
    if (HardwarePlugin) {
      HardwarePlugin.startScan({ duration: 1500 }, (res) => {
        // showPluginDebugModal('扫码回调', res);
      HardwarePlugin.startScan({ duration: 2000 }, (res) => {
        showPluginDebugModal('扫码回调', res);
        if (res) {
          callback(res);
        } else {
@@ -247,7 +235,6 @@
      // #ifdef APP-PLUS
      if (HardwarePlugin) {
        HardwarePlugin.stopScan({});
        globalScanStarted = false;
        resolve();
      } else {
        resolve();
@@ -256,38 +243,6 @@
      // #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
    });