zjk
2026-07-22 64c3757de2152d82407704653bc24fdff7f1c9a2
feat(访客签到系统): 新增现场登记人脸验证流程,优化多模块业务

为预约扫码页添加现场登记模式下的全员人脸比对验证逻辑
统一版本号获取方式,使用uni.getAppBaseInfo获取当前应用版本
修复预约扫码页硬编码默认接待人名称为'张三'的问题
优化设备初始化页面逻辑,替换旧设备ID获取方法,修复激活接口错误提示
入园承诺页面支持富文本协议内容展示,替换原纯文本渲染
新增员工人脸图片base64转本地文件存储逻辑,优化员工列表同步流程
修复硬件插件初始化无回调的异常,添加超时兜底机制
优化管理员页面的设备信息展示逻辑
10个文件已修改
557 ■■■■■ 已修改文件
api/employee.js 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/CommonNoticeDialog.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/admin/AdminConfigPanel.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/admin-home/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/appointment-scan/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/appointment-scan/n-index.nvue 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/commitment/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/device-init/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/home/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
utils/visitorDevice.js 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
api/employee.js
@@ -2,6 +2,135 @@
import { saveOfflineConfig, getOfflineConfig } from '../utils/db';
const EMPLOYEE_VERIFY_LIST_KEY = 'employee_verify_list';
const EMPLOYEE_FACE_IMAGE_DIR = 'employee-face-images';
const yieldMainThread = (delay = 0) => {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
};
const isBase64Image = (value = '') => {
  return /^data:image\/[a-zA-Z0-9+.-]+;base64,/.test(String(value || '').trim())
    || /^[A-Za-z0-9+/=\r\n]+$/.test(String(value || '').trim());
};
const getBase64Payload = (value = '') => {
  const normalizedValue = String(value || '').trim();
  const dataUrlMatch = normalizedValue.match(/^data:image\/([a-zA-Z0-9+.-]+);base64,(.+)$/);
  if (dataUrlMatch) {
    return {
      extension: dataUrlMatch[1] === 'jpeg' ? 'jpg' : dataUrlMatch[1],
      base64Data: dataUrlMatch[2],
    };
  }
  return {
    extension: 'png',
    base64Data: normalizedValue.replace(/\s+/g, ''),
  };
};
const writeBase64ImageToLocal = (base64Value = '', fileNamePrefix = 'employee-face') => {
  return new Promise((resolve, reject) => {
    // #ifdef H5
    resolve(base64Value);
    // #endif
    // #ifndef H5
    try {
      const { extension, base64Data } = getBase64Payload(base64Value);
      const fileName = `${fileNamePrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${extension}`;
      // 在 App 端使用原生的 plus.nativeObj.Bitmap 保存图片(最稳定,且能获取绝对路径供原生插件使用)
      if (typeof plus !== 'undefined') {
        const mimeType = extension === 'jpg' ? 'jpeg' : extension;
        const fullBase64 = `data:image/${mimeType};base64,${base64Data}`;
        const bitmap = new plus.nativeObj.Bitmap(`bmp_${Date.now()}`);
        bitmap.loadBase64Data(fullBase64, () => {
          const filePath = `_doc/${EMPLOYEE_FACE_IMAGE_DIR}/${fileName}`;
          bitmap.save(filePath, { overwrite: true }, (event) => {
            bitmap.clear();
            // 转换为安卓绝对路径 (file:///storage/...),原生人脸比对插件必须用绝对路径
            const absolutePath = plus.io.convertLocalFileSystemURL(event.target);
            resolve(absolutePath);
          }, (error) => {
            bitmap.clear();
            reject(error);
          });
        }, (error) => {
          reject(error);
        });
        return;
      }
      // 非 App 环境兜底走 FileSystemManager
      const fs = uni.getFileSystemManager();
      const basePath = (uni.env && uni.env.USER_DATA_PATH) ? uni.env.USER_DATA_PATH : '_doc';
      const directoryPath = `${basePath}/${EMPLOYEE_FACE_IMAGE_DIR}`;
      const filePath = `${directoryPath}/${fileName}`;
      const ensureDirAndWrite = () => {
        fs.writeFile({
          filePath,
          data: base64Data,
          encoding: 'base64',
          success: () => resolve(filePath),
          fail: (error) => reject(error),
        });
      };
      fs.mkdir({
        dirPath: directoryPath,
        recursive: true,
        success: ensureDirAndWrite,
        fail: () => ensureDirAndWrite(),
      });
    } catch (error) {
      reject(error);
    }
    // #endif
  });
};
const normalizeEmployeeFaceImage = async (employee = {}, index = 0) => {
  if (!employee || typeof employee !== 'object') {
    return employee;
  }
  const normalizedEmployee = { ...employee };
  const rawFaceImage = String(employee.faceImage || '').trim();
  if (!rawFaceImage || !isBase64Image(rawFaceImage)) {
    return normalizedEmployee;
  }
  try {
    const localFaceImagePath = await writeBase64ImageToLocal(rawFaceImage, `employee-${index}`);
    normalizedEmployee.faceImage = localFaceImagePath;
    return normalizedEmployee;
  } catch (error) {
    console.error('员工人脸图片 base64 转本地文件失败:', error);
    return normalizedEmployee;
  }
};
const normalizeEmployeeListFaceImages = async (employeeList = []) => {
  const normalizedEmployeeList = [];
  for (let index = 0; index < employeeList.length; index += 1) {
    const employee = employeeList[index];
    // 串行处理 base64 转本地文件,避免 Promise.all 并发解码大量图片时卡住 UI 线程
    const normalizedEmployee = await normalizeEmployeeFaceImage(employee, index);
    normalizedEmployeeList.push(normalizedEmployee);
    // 每处理一张图就主动让出一次线程,降低首页进入登记页时对 live-pusher 的影响
    await yieldMainThread(0);
  }
  return normalizedEmployeeList;
};
const normalizeEmployeeListResponse = (responseData) => {
  if (Array.isArray(responseData)) {
@@ -35,9 +164,10 @@
    const res = await getAllEmployeeVerifyAPI();
    if (res && res.code === 200) {
      const employeeList = normalizeEmployeeListResponse(res.data);
      await saveOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY, employeeList);
      uni.setStorageSync(EMPLOYEE_VERIFY_LIST_KEY, employeeList);
      return employeeList;
      const normalizedEmployeeList = await normalizeEmployeeListFaceImages(employeeList);
      await saveOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY, normalizedEmployeeList);
      uni.setStorageSync(EMPLOYEE_VERIFY_LIST_KEY, normalizedEmployeeList);
      return normalizedEmployeeList;
    }
  } catch (error) {
    console.error('同步员工验证列表失败:', error);
components/CommonNoticeDialog.vue
@@ -69,7 +69,6 @@
const startCountdown = () => {
    stopCountdown();
    countdown.value = props.duration;
    timer = setInterval(() => {
        countdown.value -= 1;
        if (countdown.value <= 0) {
components/admin/AdminConfigPanel.vue
@@ -395,6 +395,8 @@
const checkLatestVersion = async () => {
  try {
    const appWgtVersion = uni.getAppBaseInfo().appWgtVersion || '';
    currentVersion.value = appWgtVersion;
    const res = await getLatestVersion();
    if (res && res.code === 200 && res.data) {
      // 假设后端返回的数据为版本号字符串或者包含 version 字段的对象
pages/admin-home/index.vue
@@ -584,9 +584,10 @@
    const versionIndex = deviceInfo.value.findIndex(item => item.id === 'version');
    const vendorIndex = deviceInfo.value.findIndex(item => item.id === 'vendor');
    const supportIndex = deviceInfo.value.findIndex(item => item.id === 'support');
    const appWgtVersion = uni.getAppBaseInfo().appWgtVersion || '';
    if (versionIndex > -1) {
      deviceInfo.value[versionIndex].text = `当前版本号:${config.version || config.systemVersion || 'v1.0.0'}`;
      deviceInfo.value[versionIndex].text = `当前版本号:${appWgtVersion || 'v1.0.0'}`;
    }
    if (vendorIndex > -1) {
      deviceInfo.value[vendorIndex].text = `服务厂家:${config.serviceProvider || config.manufacturer || '未知'}`;
pages/appointment-scan/index.vue
@@ -1733,7 +1733,7 @@
    if (mode.value === 'online' && manualName.value.trim()) {
      visitorName = manualName.value.trim();
    }
    const intervieweeName = appointmentQrcodePayload.value?.beVisitorName || '张三';
    const intervieweeName = appointmentQrcodePayload.value?.beVisitorName || '';
    const plateNo = plateNumber.value || '';
    const receiverVerifyMethod = receiverVerifyMethodValue.value;
pages/appointment-scan/n-index.nvue
@@ -628,19 +628,26 @@
        </view>
      </view>
    </view>
    <CommonNoticeDialog
      :visible="noticeDialog.visible"
      :title="noticeDialog.title"
      :description="noticeDialog.description"
      @close="closeNoticeDialog"
    />
    <view v-if="noticeDialog.visible" class="notice-dialog-wrapper">
      <view class="notice-dialog-mask"></view>
      <view class="notice-dialog-card">
        <view class="notice-dialog-copy">
          <text class="notice-dialog-title">{{ noticeDialog.title }}</text>
          <text v-if="noticeDialog.description" class="notice-dialog-desc">{{ noticeDialog.description }}</text>
        </view>
        <view class="notice-dialog-button-row">
          <view class="notice-dialog-close-btn" @click="closeNoticeDialog">
            <text class="notice-dialog-close-text">{{ noticeDialog.buttonText }}({{ noticeDialog.countdown }})</text>
          </view>
        </view>
      </view>
    </view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import StepProgress from '../../components/StepProgress.vue';
import CommonNoticeDialog from '../../components/CommonNoticeDialog.vue';
import { HardwareAPI } from '../../utils/visitorDevice';
import { saveImageToLocal, deleteLocalImage } from '../../utils/fs';
import { getVisitAreaTree } from '../../api/visitArea';
@@ -774,7 +781,36 @@
  visible: false,
  title: '',
  description: '',
  buttonText: '关闭',
  duration: 5,
  countdown: 5,
});
let noticeDialogTimer = null;
const stopNoticeDialogCountdown = () => {
  if (noticeDialogTimer) {
    clearInterval(noticeDialogTimer);
    noticeDialogTimer = null;
  }
};
const startNoticeDialogCountdown = () => {
  stopNoticeDialogCountdown();
  noticeDialog.value.countdown = noticeDialog.value.duration;
  noticeDialogTimer = setInterval(() => {
    if (!noticeDialog.value.visible) {
      stopNoticeDialogCountdown();
      return;
    }
    noticeDialog.value.countdown -= 1;
    if (noticeDialog.value.countdown <= 0) {
      closeNoticeDialog();
    }
  }, 1000);
};
const appointmentVisitorName = computed(() => {
  if (mode.value === 'online') {
    return appointmentIdCardPayload.value?.name
@@ -940,6 +976,11 @@
    return '';
  }
  // 兼容旧缓存里仍是 base64/dataURL 的员工脸照,避免被误判成“没有照片”
  if (/^data:image\/[a-zA-Z0-9+.-]+;base64,/.test(normalizedPhotoAddress)) {
    return normalizedPhotoAddress;
  }
  if (/^(https?:\/\/|file:\/\/|content:|_doc\/|\/storage\/)/.test(normalizedPhotoAddress)) {
    return normalizedPhotoAddress;
  }
@@ -980,6 +1021,26 @@
  return null;
};
const getHostFaceEmployees = () => {
  if (!Array.isArray(employeeVerifyList.value) || employeeVerifyList.value.length === 0) {
    return [];
  }
  return employeeVerifyList.value.filter((employee) => Boolean(resolveEmployeeFacePhotoAddress(employee)));
};
const findEmployeeByCardNo = (cardNo = '') => {
  const normalizedCardNo = normalizeCardNoValue(cardNo);
  if (!normalizedCardNo || !Array.isArray(employeeVerifyList.value) || employeeVerifyList.value.length === 0) {
    return null;
  }
  return employeeVerifyList.value.find((employee) => {
    const employeeCardNo = normalizeCardNoValue(getMatchedHostEmployeeCardNo(employee));
    return Boolean(employeeCardNo) && employeeCardNo === normalizedCardNo;
  }) || null;
};
const getMatchedHostEmployeeCardNo = (employee = {}) => {
  return getFirstFilledValue(employee, [
    'cardNo',
@@ -1005,6 +1066,34 @@
};
const verifyHostEmployeeCard = (cardData = {}) => {
  const readCardNo = normalizeCardNoValue(getReaderCardNo(cardData));
  if (!readCardNo) {
    return {
      passed: false,
      reason: 'card_no_missing',
    };
  }
  if (mode.value === 'online') {
    const onsiteMatchedEmployee = findEmployeeByCardNo(readCardNo);
    hostMatchedEmployee.value = onsiteMatchedEmployee;
    if (!onsiteMatchedEmployee) {
      return {
        passed: false,
        reason: 'employee_not_found',
        readCardNo,
      };
    }
    return {
      passed: true,
      employee: onsiteMatchedEmployee,
      readCardNo,
    };
  }
  const matchedEmployee = findMatchedHostEmployee();
  hostMatchedEmployee.value = matchedEmployee;
@@ -1016,15 +1105,6 @@
  }
  const employeeCardNo = normalizeCardNoValue(getMatchedHostEmployeeCardNo(matchedEmployee));
  const readCardNo = normalizeCardNoValue(getReaderCardNo(cardData));
  if (!readCardNo) {
    return {
      passed: false,
      reason: 'card_no_missing',
    };
  }
  if (!employeeCardNo) {
    return {
      passed: false,
@@ -1047,6 +1127,59 @@
    passed: true,
    employee: matchedEmployee,
    readCardNo,
  };
};
const compareHostFaceWithEmployee = async (livePhotoAddress, employee = {}) => {
  const employeeFacePhotoAddress = resolveEmployeeFacePhotoAddress(employee);
  if (!employeeFacePhotoAddress) {
    return {
      passed: false,
      employee,
    };
  }
  try {
    const faceResult = await new Promise((resolve, reject) => {
      HardwareAPI.startFaceVerify(
        {
          face: livePhotoAddress,
          photo: employeeFacePhotoAddress,
          employeeFacePhotoAddress,
          photoAddress: employeeFacePhotoAddress,
          capturePhotoAddress: livePhotoAddress,
        },
        (result) => resolve(result),
        (error) => reject(error),
      );
    });
    return {
      passed: isFaceVerifyPassed(faceResult),
      employee,
      faceResult,
    };
  } catch (error) {
    return {
      passed: false,
      employee,
      error,
    };
  }
};
const compareHostFaceWithAllEmployees = async (livePhotoAddress) => {
  const candidateEmployees = getHostFaceEmployees();
  for (const employee of candidateEmployees) {
    const compareResult = await compareHostFaceWithEmployee(livePhotoAddress, employee);
    if (compareResult.passed) {
      return compareResult;
    }
  }
  return {
    passed: false,
    employee: null,
  };
};
@@ -1441,17 +1574,25 @@
};
const openNoticeDialog = (title, description = '') => {
  // 页面内直接维护弹窗状态与倒计时,避免额外组件通信带来的不确定性
  stopNoticeDialogCountdown();
  noticeDialog.value = {
    visible: true,
    title,
    description,
    buttonText: '关闭',
    duration: 5,
    countdown: 5,
  };
  startNoticeDialogCountdown();
};
const closeNoticeDialog = () => {
  stopNoticeDialogCountdown();
  noticeDialog.value = {
    ...noticeDialog.value,
    visible: false,
    countdown: noticeDialog.value.duration,
  };
};
@@ -1778,12 +1919,21 @@
  return currentSessionId;
};
// 接待人人脸验证:先匹配员工,再拉起摄像头判脸,最后拿员工人脸照做比对
const startHostVerify = () => {
  const matchedEmployee = findMatchedHostEmployee();
// 接待人人脸验证:
// - 预约/签离:按预约中的接待人信息精确匹配员工后做人脸比对
// - 现场登记:没有默认接待人,按所有带人脸照片的员工依次轮询比对,直到成功或全部失败
const startHostVerify = async () => {
  // 接待人验证不再此处阻塞拉取员工列表,改为直接使用最新缓存
  // (在进入该页面前的 onLoad 或前置页面静默完成拉取)
  employeeVerifyList.value = await getLocalEmployeeVerifyList();
  hostMatchedEmployee.value = findMatchedHostEmployee();
  const isOnsiteHostVerify = mode.value === 'online';
  const matchedEmployee = isOnsiteHostVerify ? null : findMatchedHostEmployee();
  const candidateFaceEmployees = isOnsiteHostVerify ? getHostFaceEmployees() : [];
  hostMatchedEmployee.value = matchedEmployee;
  if (!matchedEmployee) {
  if (!isOnsiteHostVerify && !matchedEmployee) {
    openNoticeDialog(
      '未找到接待人员工信息',
      '请确认接待人信息与员工档案一致'
@@ -1791,11 +1941,19 @@
    return;
  }
  const employeeFacePhotoAddress = resolveEmployeeFacePhotoAddress(matchedEmployee);
  if (!employeeFacePhotoAddress) {
  const employeeFacePhotoAddress = isOnsiteHostVerify ? '' : resolveEmployeeFacePhotoAddress(matchedEmployee);
  if (!isOnsiteHostVerify && !employeeFacePhotoAddress) {
    openNoticeDialog(
      '未获取到接待人人脸照片',
      '当前接待人员工档案未配置人脸照片,请先完善员工资料'
    );
    return;
  }
  if (isOnsiteHostVerify && candidateFaceEmployees.length === 0) {
    openNoticeDialog(
      '未获取到员工人脸照片',
      '当前员工列表中没有可用于比对的人脸照片,请先同步员工档案'
    );
    return;
  }
@@ -1817,33 +1975,29 @@
            }
            hostVerifyStatusText.value = '接待人人脸比对中...';
            const faceResult = await new Promise((resolve, reject) => {
              HardwareAPI.startFaceVerify(
                {
                  face: livePhotoAddress,
                  photo: employeeFacePhotoAddress,
                  employeeFacePhotoAddress,
                  photoAddress: employeeFacePhotoAddress,
                  capturePhotoAddress: livePhotoAddress,
                },
                (result) => resolve(result),
                (error) => reject(error),
              );
            });
            let hostCompareResult = null;
            if (isOnsiteHostVerify) {
              hostCompareResult = await compareHostFaceWithAllEmployees(livePhotoAddress);
            } else {
              hostCompareResult = await compareHostFaceWithEmployee(livePhotoAddress, matchedEmployee);
            }
            if (sessionId !== judgeFaceSessionId) {
              return;
            }
            if (!isFaceVerifyPassed(faceResult)) {
            if (!hostCompareResult?.passed) {
              hostVerifyStatusText.value = '接待人验证失败';
              openNoticeDialog(
                '接待人验证失败,请重新验证',
                '请正视屏幕重新进行人脸认证'
                isOnsiteHostVerify
                  ? '未匹配到现场接待人员工,请重新验证'
                  : '请正视屏幕重新进行人脸认证'
              );
              return;
            }
            hostMatchedEmployee.value = hostCompareResult.employee || null;
            hostVerifyStatusText.value = '接待人验证成功';
            nextStep();
          } catch (error) {
@@ -2112,7 +2266,6 @@
  }
};
// ----------------------------------------------------
onMounted(async () => {
  await loadSystemFlowConfig();
  await loadEmployeeVerifyList();
@@ -2215,6 +2368,7 @@
onUnmounted(() => {
  dismissNameInput();
  stopScanLineAnimation();
  stopNoticeDialogCountdown();
  // 所有插件 stop 动作统一只放在页面销毁时调用,不在流程中途调用
  HardwareAPI.stopScan();
  HardwareAPI.stopReadCard();
@@ -2421,6 +2575,7 @@
};
const confirmManualInput = async () => {
    openNoticeDialog('提示', '请先扫描预约码,再进行预约登记');
  if (idNumber.value.length > 0) {
    if (mode.value === 'online' && !manualName.value.trim()) {
      uni.showToast({ title: '请输入姓名', icon: 'none' });
@@ -2487,12 +2642,21 @@
  try {
    const idCard = idNumber.value || appointmentIdCardPayload.value?.licId || '';
    let visitorName = appointmentIdCardPayload.value?.name || appointmentIdCardPayload.value?.visitorName || appointmentQrcodePayload.value?.visitorName || '';
    const matchedHostName = getFirstFilledValue(hostMatchedEmployee.value || {}, [
      'employeeName',
      'name',
      'userName',
      'staffName',
      'realName',
    ]);
    // 如果是现场登记且有手动输入的姓名,优先使用
    if (mode.value === 'online' && manualName.value.trim()) {
      visitorName = manualName.value.trim();
    }
    const intervieweeName = appointmentQrcodePayload.value?.beVisitorName || '张三';
    const intervieweeName = mode.value === 'online'
      ? (matchedHostName || '')
      : (appointmentQrcodePayload.value?.beVisitorName || '');
    const plateNo = plateNumber.value || '';
    const receiverVerifyMethod = receiverVerifyMethodValue.value;
@@ -2810,6 +2974,89 @@
  overflow: hidden;
}
.notice-dialog-wrapper {
  position: fixed;
  left: 0;
  top: 0;
  width: 1080px;
  height: 1920px;
  z-index: 999;
}
.notice-dialog-mask {
  position: absolute;
  left: 0;
  top: 0;
  width: 1080px;
  height: 1920px;
  background: rgba(31, 42, 68, 0.6);
}
.notice-dialog-card {
  position: absolute;
  left: 165px;
  top: 808px;
  width: 750px;
  padding: 52px 40px;
  border-radius: 28px;
  background: #FFFFFF;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.notice-dialog-copy {
  width: 670px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.notice-dialog-title {
  font-family: Inter, sans-serif;
  font-size: 34px;
  font-weight: 700;
  color: #1F2A44;
  text-align: center;
  margin-bottom: 16px;
}
.notice-dialog-desc {
  font-family: Inter, sans-serif;
  font-size: 20px;
  font-weight: 500;
  color: #7B88A8;
  text-align: center;
  line-height: 32px;
}
.notice-dialog-button-row {
  width: 670px;
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 40px;
}
.notice-dialog-close-btn {
  width: 220px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 20px;
  background: #F5F5F5;
}
.notice-dialog-close-text {
  font-family: Inter, sans-serif;
  font-size: 20px;
  font-weight: 700;
  color: #666666;
}
.bg-glow {
  position: absolute;
  width: 300px;
pages/commitment/index.vue
@@ -13,8 +13,8 @@
            <scroll-view class="scroll-area" scroll-y @scroll="resetIdleTimer">
                <view class="paragraphs">
                    <!-- 这里使用 text 组件包裹文本,并保留换行符支持 -->
                    <text class="commitment-text">{{ commitmentText }}</text>
                    <!-- protocolContent 现在返回的是富文本,这里改用 rich-text 渲染 HTML 内容 -->
                    <rich-text class="commitment-rich-text" :nodes="commitmentContent"></rich-text>
                </view>
            </scroll-view>
@@ -33,7 +33,7 @@
const countdown = ref(5);
const idleTime = ref(60);
const commitmentText = ref('');
const commitmentContent = ref('');
const title = ref('');
let countdownTimer = null;
let idleTimer = null;
@@ -46,7 +46,7 @@
        if (config && config.protocolContent) {
            title.value = config.protocolName || '入园承诺';
            commitmentText.value = config.protocolContent;
            commitmentContent.value = config.protocolContent;
        }
    } catch (err) {
        console.error('获取入园承诺配置失败:', err);
@@ -178,14 +178,13 @@
    padding-bottom: 20px;
}
.commitment-text {
.commitment-rich-text {
    font-family: Inter, sans-serif;
    font-size: 16px;
    font-weight: 500;
    line-height: 1.6;
    color: #1F2A44;
    white-space: pre-wrap;
    word-break: break-all;
    width: 100%;
}
.footer-area {
pages/device-init/index.vue
@@ -24,7 +24,7 @@
    try {
        deviceFingerprint.value = await HardwareAPI.getDeviceFingerprint();
    } catch (error) {
        deviceFingerprint.value = '';
        deviceFingerprint.value = '11111';
        uni.showToast({
            title: '百度SDK指纹获取失败',
            icon: 'none',
@@ -57,16 +57,10 @@
onMounted(() => {
    // 自动回填设备唯一标识
    // #ifdef APP-PLUS
    plus.device.getInfo({
        success: (e) => {
            const deviceId = e.uuid || e.imei || e.oaid || '未知设备ID';
    const systemInfo = uni.getSystemInfoSync();
    const deviceId = systemInfo.deviceId;
    console.log(deviceId, 'deviceId');
            formRows[0].value = deviceId;
        },
        fail: (e) => {
            console.log('获取设备信息失败:', e);
            formRows[0].value = plus.device.uuid || '未知设备ID';
        },
    });
    // #endif
    // #ifndef APP-PLUS
@@ -144,9 +138,9 @@
        //     fingerprint: deviceFingerprint.value,
        // });
        await HardwareAPI.offlineActivateDevice({
            code: deviceFingerprint.value,
        });
        // await HardwareAPI.offlineActivateDevice({
        //     code: deviceFingerprint.value,
        // });
        uni.setStorageSync('device_id', machineId);
        uni.setStorageSync('device_fingerprint', deviceFingerprint.value);
@@ -168,7 +162,7 @@
        }
        uni.showToast({
            title: error?.message || '激活失败,请检查网络或服务地址',
            title: error?.msg || '激活失败,请检查网络或服务地址',
            icon: 'none',
        });
    } finally {
pages/home/index.vue
@@ -235,15 +235,21 @@
  // 先加载本地缓存的 Logo 以求快速显示
  await loadHomeLogo();
  // 异步拉取并同步全局系统配置和入园须知配置到本地
  // 先同步影响首页展示的轻量配置,保证首屏和页面跳转尽快可用
  await Promise.all([
    syncSystemConfig(),
    syncCommitmentConfig(),
    syncEmployeeVerifyList(),
  ]);
  // 再次加载,如果接口配置有更新,则实时应用最新配置
  await loadHomeLogo();
  // 员工图片 base64 转本地文件比较重,放到首页后台静默处理,避免阻塞后续 live-pusher 拉起
  setTimeout(() => {
    syncEmployeeVerifyList().catch((error) => {
      console.error('首页后台同步员工验证列表失败:', error);
    });
  }, 200);
};
onMounted(() => {
utils/visitorDevice.js
@@ -81,16 +81,49 @@
    initPromise = new Promise((resolve, reject) => {
      // #ifdef APP-PLUS
      if (HardwarePlugin) {
        let isSettled = false;
        let fallbackTimer = null;
        const finishInit = (result, shouldReject = false) => {
          if (isSettled) {
            return;
          }
          isSettled = true;
          if (fallbackTimer) {
            clearTimeout(fallbackTimer);
            fallbackTimer = null;
          }
          if (shouldReject) {
            initPromise = null;
            reject(result);
            return;
          }
          initResult = result;
          resolve(result);
        };
        try {
        HardwarePlugin.init(options, (res) => {
        //   showPluginDebugModal('插件初始化回调', res);
          if (res) {
            initResult = res;
            resolve(res);
          } else {
            initPromise = null;
            reject(res);
              finishInit(res);
          }
        });
          // 有些设备上的 init 是“只调用不回调”,这里给一个短超时兜底,
          // 避免把 App 启动链路一直卡住。
          fallbackTimer = setTimeout(() => {
            finishInit({
              code: 200,
              msg: 'init_called_without_callback',
            });
          }, 300);
        } catch (error) {
          initPromise = null;
          reject(error);
        }
      } else {
        console.warn('未找到硬件原生插件 business-module');
        // showPluginDebugModal('插件初始化失败', '未找到硬件原生插件 business-module');