zjk
2026-07-23 cc2f6601aa3fef21173c40534687f6fd50c520de
feat: 优化扫码逻辑,重构弹窗与员工验证缓存

统一收口扫码停止逻辑到页面卸载时,移除页面中途的主动停止扫码代码。重构通用通知弹窗支持关闭回调,拆分人脸验证失败的专属弹窗适配不同验证步骤。新增本地员工人脸缓存有效性校验,自动清理失效缓存并在联网时自动同步最新列表。
3个文件已修改
212 ■■■■■ 已修改文件
api/employee.js 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/appointment-scan/n-index.nvue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/home/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
api/employee.js
@@ -3,10 +3,57 @@
const EMPLOYEE_VERIFY_LIST_KEY = 'employee_verify_list';
const EMPLOYEE_FACE_IMAGE_DIR = 'employee-face-images';
let employeeVerifyListSyncPromise = null;
const yieldMainThread = (delay = 0) => {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
};
const saveEmployeeVerifyListCache = async (employeeList = []) => {
  await saveOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY, employeeList);
  uni.setStorageSync(EMPLOYEE_VERIFY_LIST_KEY, employeeList);
};
const getCurrentNetworkType = async () => {
  return new Promise((resolve) => {
    uni.getNetworkType({
      success: (res) => resolve(res.networkType),
      fail: () => resolve('unknown'),
    });
  });
};
const isLocalEmployeeFacePath = (value = '') => {
  const normalizedValue = String(value || '').trim();
  return /^(file:\/\/|_doc\/|\/storage\/)/.test(normalizedValue);
};
const isLocalFileExists = (filePath = '') => {
  return new Promise((resolve) => {
    const normalizedFilePath = String(filePath || '').trim();
    if (!normalizedFilePath) {
      resolve(false);
      return;
    }
    // #ifdef APP-PLUS
    if (typeof plus === 'undefined' || !plus.io || !plus.io.resolveLocalFileSystemURL) {
      resolve(false);
      return;
    }
    plus.io.resolveLocalFileSystemURL(normalizedFilePath, () => {
      resolve(true);
    }, () => {
      resolve(false);
    });
    // #endif
    // #ifndef APP-PLUS
    resolve(true);
    // #endif
  });
};
@@ -156,6 +203,80 @@
  return [];
};
const sanitizeEmployeeFaceImageCache = async (employee = {}) => {
  if (!employee || typeof employee !== 'object') {
    return {
      employee,
      invalidLocalFaceImage: false,
    };
  }
  const normalizedEmployee = { ...employee };
  const normalizedFaceImage = String(employee.faceImage || '').trim();
  if (!isLocalEmployeeFacePath(normalizedFaceImage)) {
    return {
      employee: normalizedEmployee,
      invalidLocalFaceImage: false,
    };
  }
  const isFaceImageFileExists = await isLocalFileExists(normalizedFaceImage);
  if (isFaceImageFileExists) {
    return {
      employee: normalizedEmployee,
      invalidLocalFaceImage: false,
    };
  }
  // 本地图片文件被系统清理后,必须立刻清空失效路径,避免离线状态继续拿脏地址做人脸比对。
  normalizedEmployee.faceImage = '';
  return {
    employee: normalizedEmployee,
    invalidLocalFaceImage: true,
  };
};
const sanitizeEmployeeVerifyListCache = async (employeeList = []) => {
  const sanitizedEmployeeList = [];
  let hasInvalidLocalFaceImage = false;
  for (let index = 0; index < employeeList.length; index += 1) {
    const sanitizeResult = await sanitizeEmployeeFaceImageCache(employeeList[index]);
    sanitizedEmployeeList.push(sanitizeResult.employee);
    hasInvalidLocalFaceImage = hasInvalidLocalFaceImage || sanitizeResult.invalidLocalFaceImage;
  }
  return {
    employeeList: sanitizedEmployeeList,
    hasInvalidLocalFaceImage,
  };
};
const performSyncEmployeeVerifyList = async () => {
  const res = await getAllEmployeeVerifyAPI();
  if (res && res.code === 200) {
    const employeeList = normalizeEmployeeListResponse(res.data);
    const normalizedEmployeeList = await normalizeEmployeeListFaceImages(employeeList);
    await saveEmployeeVerifyListCache(normalizedEmployeeList);
    return normalizedEmployeeList;
  }
  throw new Error(res?.msg || '同步员工验证列表失败');
};
const runEmployeeVerifyListSync = async () => {
  if (employeeVerifyListSyncPromise) {
    return employeeVerifyListSyncPromise;
  }
  employeeVerifyListSyncPromise = performSyncEmployeeVerifyList();
  try {
    return await employeeVerifyListSyncPromise;
  } finally {
    employeeVerifyListSyncPromise = null;
  }
};
export const getAllEmployeeVerifyAPI = (data = {}) => {
  return request({
    url: '/api/employee/verify/allEmployee',
@@ -166,27 +287,43 @@
export const syncEmployeeVerifyList = async () => {
  try {
    const res = await getAllEmployeeVerifyAPI();
    if (res && res.code === 200) {
      const employeeList = normalizeEmployeeListResponse(res.data);
      const normalizedEmployeeList = await normalizeEmployeeListFaceImages(employeeList);
      await saveOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY, normalizedEmployeeList);
      uni.setStorageSync(EMPLOYEE_VERIFY_LIST_KEY, normalizedEmployeeList);
      return normalizedEmployeeList;
    }
    return await runEmployeeVerifyListSync();
  } catch (error) {
    console.error('同步员工验证列表失败:', error);
    return [];
  }
  return [];
};
export const getLocalEmployeeVerifyList = async () => {
  const offlineEmployeeList = await getOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY);
  if (Array.isArray(offlineEmployeeList)) {
    return offlineEmployeeList;
  const storageEmployeeList = uni.getStorageSync(EMPLOYEE_VERIFY_LIST_KEY);
  const localEmployeeList = Array.isArray(offlineEmployeeList)
    ? offlineEmployeeList
    : (Array.isArray(storageEmployeeList) ? storageEmployeeList : []);
  if (!Array.isArray(localEmployeeList) || localEmployeeList.length === 0) {
    return [];
  }
  const storageEmployeeList = uni.getStorageSync(EMPLOYEE_VERIFY_LIST_KEY);
  return Array.isArray(storageEmployeeList) ? storageEmployeeList : [];
  const {
    employeeList: sanitizedEmployeeList,
    hasInvalidLocalFaceImage,
  } = await sanitizeEmployeeVerifyListCache(localEmployeeList);
  if (!hasInvalidLocalFaceImage) {
    return sanitizedEmployeeList;
  }
  await saveEmployeeVerifyListCache(sanitizedEmployeeList);
  const networkType = await getCurrentNetworkType();
  if (networkType !== 'none') {
    try {
      return await runEmployeeVerifyListSync();
    } catch (error) {
      console.error('员工人脸缓存失效后自动重同步失败:', error);
    }
  }
  return sanitizedEmployeeList;
};
pages/appointment-scan/n-index.nvue
@@ -786,6 +786,7 @@
  countdown: 5,
});
let noticeDialogTimer = null;
let noticeDialogCloseHandler = null;
const INACTIVITY_AUTO_BACK_MS = 60 * 1000;
let inactivityAutoBackTimer = null;
@@ -1604,9 +1605,32 @@
  selectedAreas.value = selectedAreas.value.filter((item) => item !== area);
};
const openNoticeDialog = (title, description = '') => {
const openVisitorFaceVerifyFailureDialog = (title, description = '') => {
  openNoticeDialog(title, description, {
    onClose: () => {
      if (currentStepName.value !== 'face') {
        return;
      }
      handleFaceVerifyNextStep();
    },
  });
};
const openHostFaceVerifyFailureDialog = (title, description = '') => {
  openNoticeDialog(title, description, {
    onClose: () => {
      if (currentStepName.value !== 'host') {
        return;
      }
      retryFaceVerify();
    },
  });
};
const openNoticeDialog = (title, description = '', options = {}) => {
  // 页面内直接维护弹窗状态与倒计时,避免额外组件通信带来的不确定性
  stopNoticeDialogCountdown();
  noticeDialogCloseHandler = typeof options?.onClose === 'function' ? options.onClose : null;
  noticeDialog.value = {
    visible: true,
    title,
@@ -1620,11 +1644,17 @@
const closeNoticeDialog = () => {
  stopNoticeDialogCountdown();
  const currentCloseHandler = noticeDialogCloseHandler;
  noticeDialogCloseHandler = null;
  noticeDialog.value = {
    ...noticeDialog.value,
    visible: false,
    countdown: noticeDialog.value.duration,
  };
  if (typeof currentCloseHandler === 'function') {
    currentCloseHandler();
  }
};
const normalizeIdentityCardNo = (value) => {
@@ -2079,7 +2109,7 @@
            if (!hostCompareResult?.passed) {
              hostVerifyStatusText.value = '接待人验证失败';
              openNoticeDialog(
              openHostFaceVerifyFailureDialog(
                '接待人验证失败,请重新验证',
                isOnsiteHostVerify
                  ? '未匹配到现场接待人员工,请重新验证'
@@ -2097,7 +2127,7 @@
            }
            hostVerifyStatusText.value = '接待人验证失败';
            openNoticeDialog(
            openHostFaceVerifyFailureDialog(
              resolveFaceVerifyErrorMessage(error),
              '请正视屏幕重新进行人脸认证'
            );
@@ -2252,7 +2282,7 @@
            if (!isFaceVerifyPassed(faceResult)) {
              faceVerifyStatusText.value = '人证比对失败';
              openNoticeDialog(
              openVisitorFaceVerifyFailureDialog(
                '人证照片比对失败,请重新采集',
                '请正视屏幕重新进行人脸认证'
              );
@@ -2268,7 +2298,7 @@
            }
            faceVerifyStatusText.value = '人证比对失败';
            openNoticeDialog(
            openVisitorFaceVerifyFailureDialog(
              resolveFaceVerifyErrorMessage(error),
              '请正视屏幕重新进行人脸认证'
            );
@@ -2484,7 +2514,8 @@
  dismissNameInput();
  stopScanLineAnimation();
  stopNoticeDialogCountdown();
  // 扫码停止统一收口到应用关闭时调用,页面销毁时不主动 stop
  // 扫码硬件改为在页面退出时统一关闭,步骤内切换与识别成功跳步都不主动 stop。
  HardwareAPI.stopScan();
  HardwareAPI.stopReadCard();
  HardwareAPI.stopJudgeFace();
  HardwareAPI.stopFaceVerify();
@@ -2586,8 +2617,6 @@
          return;
        }
        // 扫码成功进入下一步前主动停止扫码,避免后续步骤继续收到扫码回调。
        await HardwareAPI.stopScan();
        nextStep();
      } catch (error) {
        console.error('预约码解密失败:', error);
pages/home/index.vue
@@ -277,8 +277,6 @@
      }
      saveAppointmentQrPayload(payload);
      // 首页扫码成功后主动停止扫码,确保再次回首页时硬件处于干净状态。
      await HardwareAPI.stopScan();
      homeScanSessionId += 1;
      // 扫码成功,直接进入预约登记的第二步(身份证阅读)