zjk
2026-07-10 62146caf1f3a1bd7b3c1c3886af480eee455249e
refactor: 重构静态资源结构并修复H5环境兼容问题

- 重构静态资源目录结构,将图片资源归类到home、appointment-scan子目录
- 将页面及组件内联的SVG图标替换为本地图片资源,统一图标使用方式
- 修复visitorDevice工具在H5环境下因uni未定义导致的加载报错
- 删除未使用的初始页面和废弃静态资源
- 优化预约扫描页面的步骤触发逻辑与导航参数传递
3个文件已删除
13 文件已重命名
18个文件已添加
6个文件已修改
294 ■■■■■ 已修改文件
components/StepProgress.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/appointment-scan/index.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/home/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/index/index.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/online-complete/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/signout-complete/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/back.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/device-id-scan.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/device-scan.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/face-active.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/face-completed.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/face-inactive.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/form-active.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/form-completed.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/form-inactive.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/host-active.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/host-completed.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/host-inactive.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/idcard-active.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/idcard-completed.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/idcard-inactive.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/print-active.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/print-inactive.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/print-ticket-demo.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/qr-active.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/qr-completed.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/step-arrow-active.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/step-arrow-inactive.png 补丁 | 查看 | 原始文档 | blame | 历史
static/appointment-scan/success.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/admin-btn-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/appointment.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/avatar.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/home-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/operation.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/register.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/sign-out.png 补丁 | 查看 | 原始文档 | blame | 历史
static/home/welcome-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
static/kmN54.png 补丁 | 查看 | 原始文档 | blame | 历史
static/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
utils/visitorDevice.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components/StepProgress.vue
@@ -4,59 +4,7 @@
      <template v-for="(step, index) in stepsList" :key="index">
        <view class="step-item">
          <view class="step-circle" :class="getCircleClass(index + 1)">
            <!-- 渲染当前步骤对应的图标 -->
            <template v-if="step.icon === 'qr'">
              <svg width="38" height="38" viewBox="0 0 24 24" fill="none" :stroke="getIconStroke(index + 1)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
                <rect x="7" y="7" width="3" height="3"></rect>
                <rect x="14" y="7" width="3" height="3"></rect>
                <rect x="7" y="14" width="3" height="3"></rect>
                <rect x="14" y="14" width="3" height="3"></rect>
              </svg>
            </template>
            <template v-else-if="step.icon === 'idcard'">
              <svg width="38" height="38" viewBox="0 0 24 24" fill="none" :stroke="getIconStroke(index + 1)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M16 10h2"></path>
                <path d="M16 14h2"></path>
                <path d="M6.17 15a3 3 0 0 1 5.66 0"></path>
                <circle cx="9" cy="11" r="2"></circle>
                <rect x="2" y="5" width="20" height="14" rx="2"></rect>
              </svg>
            </template>
            <template v-else-if="step.icon === 'face'">
              <svg width="38" height="38" viewBox="0 0 24 24" fill="none" :stroke="getIconStroke(index + 1)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M3 7V5a2 2 0 0 1 2-2h2"></path>
                <path d="M17 3h2a2 2 0 0 1 2 2v2"></path>
                <path d="M21 17v2a2 2 0 0 1-2 2h-2"></path>
                <path d="M7 21H5a2 2 0 0 1-2-2v-2"></path>
                <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
                <path d="M9 9h.01"></path>
                <path d="M15 9h.01"></path>
              </svg>
            </template>
            <template v-else-if="step.icon === 'user'">
              <svg width="38" height="38" viewBox="0 0 24 24" fill="none" :stroke="getIconStroke(index + 1)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
                <circle cx="9" cy="7" r="4"></circle>
                <polyline points="16 11 18 13 22 9"></polyline>
              </svg>
            </template>
            <template v-else-if="step.icon === 'form'">
              <svg width="38" height="38" viewBox="0 0 24 24" fill="none" :stroke="getIconStroke(index + 1)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                <polyline points="14 2 14 8 20 8"></polyline>
                <line x1="16" y1="13" x2="8" y2="13"></line>
                <line x1="16" y1="17" x2="8" y2="17"></line>
                <polyline points="10 9 9 9 8 9"></polyline>
              </svg>
            </template>
            <template v-else-if="step.icon === 'print'">
              <svg width="38" height="38" viewBox="0 0 24 24" fill="none" :stroke="getIconStroke(index + 1)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="6 9 6 2 18 2 18 9"></polyline>
                <path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
                <rect x="6" y="14" width="12" height="8"></rect>
              </svg>
            </template>
            <image :src="getStepIconSrc(step.icon, index + 1)" class="step-icon" mode="aspectFit"></image>
          </view>
          <text class="step-text" :class="getTextClass(index + 1)">{{ step.text }}</text>
        </view>
@@ -70,8 +18,24 @@
<script setup>
import { computed } from 'vue';
import stepArrowActiveImage from '../static/step-arrow-active.png';
import stepArrowInactiveImage from '../static/step-arrow-inactive.png';
import qrActiveIcon from '../static/appointment-scan/qr-active.png';
import qrCompletedIcon from '../static/appointment-scan/qr-completed.png';
import idcardInactiveIcon from '../static/appointment-scan/idcard-inactive.png';
import idcardActiveIcon from '../static/appointment-scan/idcard-active.png';
import idcardCompletedIcon from '../static/appointment-scan/idcard-completed.png';
import faceInactiveIcon from '../static/appointment-scan/face-inactive.png';
import faceActiveIcon from '../static/appointment-scan/face-active.png';
import faceCompletedIcon from '../static/appointment-scan/face-completed.png';
import hostInactiveIcon from '../static/appointment-scan/host-inactive.png';
import hostActiveIcon from '../static/appointment-scan/host-active.png';
import hostCompletedIcon from '../static/appointment-scan/host-completed.png';
import formInactiveIcon from '../static/appointment-scan/form-inactive.png';
import formActiveIcon from '../static/appointment-scan/form-active.png';
import formCompletedIcon from '../static/appointment-scan/form-completed.png';
import printInactiveIcon from '../static/appointment-scan/print-inactive.png';
import printActiveIcon from '../static/appointment-scan/print-active.png';
import stepArrowActiveImage from '../static/appointment-scan/step-arrow-active.png';
import stepArrowInactiveImage from '../static/appointment-scan/step-arrow-inactive.png';
const props = defineProps({
  currentStep: {
@@ -122,10 +86,51 @@
  return 'inactive-text';
};
const getIconStroke = (stepNum) => {
  if (stepNum < props.currentStep) return '#FFFFFF';
  if (stepNum === props.currentStep) return '#2F6BFF';
  return '#C7D7F6';
const stepIconMap = {
  qr: {
    active: qrActiveIcon,
    completed: qrCompletedIcon,
    inactive: qrActiveIcon,
  },
  idcard: {
    active: idcardActiveIcon,
    completed: idcardCompletedIcon,
    inactive: idcardInactiveIcon,
  },
  face: {
    active: faceActiveIcon,
    completed: faceCompletedIcon,
    inactive: faceInactiveIcon,
  },
  user: {
    active: hostActiveIcon,
    completed: hostCompletedIcon,
    inactive: hostInactiveIcon,
  },
  form: {
    active: formActiveIcon,
    completed: formCompletedIcon,
    inactive: formInactiveIcon,
  },
  print: {
    active: printActiveIcon,
    completed: printInactiveIcon,
    inactive: printInactiveIcon,
  },
};
const getStepStatus = (stepNum) => {
  if (stepNum < props.currentStep) return 'completed';
  if (stepNum === props.currentStep) return 'active';
  return 'inactive';
};
const getStepIconSrc = (iconKey, stepNum) => {
  const iconGroup = stepIconMap[iconKey];
  if (!iconGroup) {
    return '';
  }
  return iconGroup[getStepStatus(stepNum)] || iconGroup.inactive;
};
const getArrowSrc = (stepNum) => {
@@ -172,6 +177,11 @@
  justify-content: center;
}
.step-icon {
  width: 38px;
  height: 38px;
}
.completed-circle {
  background: #78A8FF;
  border: 1px solid #DDE9FF;
pages/appointment-scan/index.vue
@@ -7,9 +7,7 @@
    <view class="header-area">
      <view class="left-action" @click="goBack">
        <view class="back-btn">
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="15 18 9 12 15 6"></polyline>
          </svg>
          <image :src="backIconImage" class="back-icon-img" mode="aspectFit"></image>
        </view>
        <text class="page-title">{{ mode === 'online' ? '现场登记' : (mode === 'signout' ? '签离' : '预约登记') }}</text>
      </view>
@@ -645,9 +643,10 @@
import { onLoad } from '@dcloudio/uni-app';
import StepProgress from '../../components/StepProgress.vue';
import { HardwareAPI } from '../../utils/visitorDevice';
import deviceScanImage from '../../static/device-scan.png';
import deviceIdScanImage from '../../static/device-id-scan.png';
import printTicketDemoImage from '../../static/print-ticket-demo.png';
import backIconImage from '../../static/appointment-scan/back.png';
import deviceScanImage from '../../static/appointment-scan/device-scan.png';
import deviceIdScanImage from '../../static/appointment-scan/device-id-scan.png';
import printTicketDemoImage from '../../static/appointment-scan/print-ticket-demo.png';
const showMissingEntryDialog = ref(false);
const dialogCountdown = ref(5);
@@ -847,11 +846,12 @@
    startDialogCountdown();
  }
  // 首屏硬件动作不要依赖 init 回调,否则 init 卡住时扫码第一步不会触发
  triggerHardwareByStep(currentStepName.value);
  // 初始化硬件插件
  HardwareAPI.init().then(() => {
    console.log('硬件插件初始化完成');
    // 如果页面一进来就是第一步,手动触发一下监听
    triggerHardwareByStep(currentStepName.value);
  }).catch(err => {
    console.error('硬件插件初始化失败:', err);
  });
@@ -899,14 +899,19 @@
// 动态触发硬件逻辑
const triggerHardwareByStep = (stepName) => {
  if (stepName === 'qr') {
  // 只有预约登记第一步才需要扫码
  if (mode.value === 'appointment' && stepName === 'qr') {
    HardwareAPI.startScan((scanData) => {
      console.log('扫码成功,拿到预约码:', scanData);
      // TODO: 这里可以发请求验证预约码是否有效
      // 验证成功后自动进入下一步
      nextStep();
    });
  } else if (stepName === 'idcard') {
    return;
  }
  // 预约登记第二步、现场登记第一步、签离第一步都走身份证读卡
  if (stepName === 'idcard') {
    HardwareAPI.startReadCard((cardData) => {
      console.log('读卡成功,拿到身份证信息:', cardData);
      // TODO: 保存身份证信息,展示给用户,或直接进入下一步
@@ -966,7 +971,7 @@
    });
  } else {
    uni.navigateTo({
      url: '/pages/online-complete/index'
      url: `/pages/online-complete/index?mode=${mode.value}`
    });
  }
};
@@ -1028,6 +1033,11 @@
  justify-content: center;
}
.back-icon-img {
  width: 20px;
  height: 20px;
}
.page-title {
  font-family: Inter, sans-serif;
  font-size: 30px;
pages/home/index.vue
@@ -1,13 +1,13 @@
<script setup>
import { ref } from 'vue';
import adminIcon from '../../static/avatar.png';
import operationImage from '../../static/operation.png';
import appointmentIcon from '../../static/appointment.png';
import registerIcon from '../../static/register.png';
import signOutIcon from '../../static/sign-out.png';
import homeBgImage from '../../static/home-bg.png';
import adminBtnBgImage from '../../static/admin-btn-bg.png';
import welcomeBgImage from '../../static/welcome-bg.png';
import adminIcon from '../../static/home/avatar.png';
import operationImage from '../../static/home/operation.png';
import appointmentIcon from '../../static/home/appointment.png';
import registerIcon from '../../static/home/register.png';
import signOutIcon from '../../static/home/sign-out.png';
import homeBgImage from '../../static/home/home-bg.png';
import adminBtnBgImage from '../../static/home/admin-btn-bg.png';
import welcomeBgImage from '../../static/home/welcome-bg.png';
// 模拟后台配置:是否显示管理员入口,默认为 true
const showAdminEntry = ref(true);
pages/index/index.vue
File was deleted
pages/online-complete/index.vue
@@ -7,11 +7,9 @@
    <view class="header-area">
      <view class="left-action" @click="goBack">
        <view class="back-btn">
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="15 18 9 12 15 6"></polyline>
          </svg>
          <image :src="backIconImage" class="back-icon-img" mode="aspectFit"></image>
        </view>
        <text class="page-title">预约登记</text>
        <text class="page-title">{{ pageTitle }}</text>
      </view>
    </view>
@@ -20,26 +18,24 @@
      <view class="success-badge">
        <text class="success-badge-text">登记已完成</text>
      </view>
      <view class="success-icon-area">
        <view class="icon-outer-circle">
          <view class="icon-inner-circle">
            <svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="20 6 9 17 4 12"></polyline>
            </svg>
            <image :src="successIconImage" class="success-icon-img" mode="aspectFit"></image>
          </view>
        </view>
      </view>
      <text class="success-title">欢迎来访,签到成功</text>
      <view class="reminder-bar">
        <view class="reminder-icon-bg">
          <text class="reminder-icon-text">i</text>
        </view>
        <text class="reminder-text">请妥善保管个人证件与访客凭条,避免遗失</text>
      </view>
      <view class="action-area">
        <view class="home-btn" @click="goHome">
          <text class="home-btn-text">返回首页</text>
@@ -55,9 +51,17 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import backIconImage from '../../static/appointment-scan/back.png';
import successIconImage from '../../static/appointment-scan/success.png';
const countdown = ref(15);
const pageTitle = ref('预约登记');
let timer = null;
onLoad((options) => {
  pageTitle.value = options.mode === 'online' ? '现场登记' : '预约登记';
});
onMounted(() => {
  timer = setInterval(() => {
@@ -142,6 +146,11 @@
  justify-content: center;
}
.back-icon-img {
  width: 20px;
  height: 20px;
}
.page-title {
  font-family: Inter, sans-serif;
  font-size: 30px;
@@ -215,6 +224,11 @@
  box-shadow: 0 10px 20px rgba(24, 182, 122, 0.3);
}
.success-icon-img {
  width: 60px;
  height: 60px;
}
.success-title {
  font-family: Inter, sans-serif;
  font-size: 48px;
pages/signout-complete/index.vue
@@ -5,9 +5,7 @@
    <view class="header-area">
      <view class="left-action" @click="goHome">
        <view class="back-btn">
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="15 18 9 12 15 6"></polyline>
          </svg>
          <image :src="backIconImage" class="back-icon-img" mode="aspectFit"></image>
        </view>
        <text class="page-title">签离</text>
      </view>
@@ -21,9 +19,7 @@
      <view class="success-icon-area">
        <view class="icon-outer-circle">
          <view class="icon-inner-circle">
            <svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="20 6 9 17 4 12"></polyline>
            </svg>
            <image :src="successIconImage" class="success-icon-img" mode="aspectFit"></image>
          </view>
        </view>
      </view>
@@ -51,6 +47,8 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import backIconImage from '../../static/appointment-scan/back.png';
import successIconImage from '../../static/appointment-scan/success.png';
const countdown = ref(15);
let timer = null;
@@ -133,6 +131,11 @@
  justify-content: center;
}
.back-icon-img {
  width: 20px;
  height: 20px;
}
.page-title {
  font-family: Inter, sans-serif;
  font-size: 30px;
@@ -205,6 +208,11 @@
  box-shadow: 0 10px 20px rgba(24, 182, 122, 0.3);
}
.success-icon-img {
  width: 60px;
  height: 60px;
}
.success-title {
  font-family: Inter, sans-serif;
  font-size: 48px;
static/appointment-scan/back.png
static/appointment-scan/device-id-scan.png

static/appointment-scan/device-scan.png

static/appointment-scan/face-active.png
static/appointment-scan/face-completed.png
static/appointment-scan/face-inactive.png
static/appointment-scan/form-active.png
static/appointment-scan/form-completed.png
static/appointment-scan/form-inactive.png
static/appointment-scan/host-active.png
static/appointment-scan/host-completed.png
static/appointment-scan/host-inactive.png
static/appointment-scan/idcard-active.png
static/appointment-scan/idcard-completed.png
static/appointment-scan/idcard-inactive.png
static/appointment-scan/print-active.png
static/appointment-scan/print-inactive.png
static/appointment-scan/print-ticket-demo.png

static/appointment-scan/qr-active.png
static/appointment-scan/qr-completed.png
static/appointment-scan/step-arrow-active.png

static/appointment-scan/step-arrow-inactive.png

static/appointment-scan/success.png
static/home/admin-btn-bg.png

static/home/appointment.png

static/home/avatar.png

static/home/home-bg.png

static/home/operation.png

static/home/register.png

static/home/sign-out.png

static/home/welcome-bg.png

static/kmN54.png
Binary files differ
static/logo.png
Binary files differ
utils/visitorDevice.js
@@ -4,7 +4,12 @@
 */
// 假设你们安卓开发的插件名称为 'business-module',请根据实际 package.json 里的 id 替换
const HardwarePlugin = uni.requireNativePlugin('business-module');
// H5 环境没有 uni.requireNativePlugin,这里要先做兼容判断,避免模块加载阶段直接报错
let HardwarePlugin = null;
if (typeof uni !== 'undefined' && typeof uni.requireNativePlugin === 'function') {
  HardwarePlugin = uni.requireNativePlugin('business-module');
}
let initPromise = null;
let initResult = null;
const PLUGIN_DEBUG_ENABLED = true;