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.pngstatic/appointment-scan/device-scan.pngstatic/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.pngstatic/appointment-scan/qr-active.png
static/appointment-scan/qr-completed.png
static/appointment-scan/step-arrow-active.pngstatic/appointment-scan/step-arrow-inactive.pngstatic/appointment-scan/success.png
static/home/admin-btn-bg.pngstatic/home/appointment.pngstatic/home/avatar.pngstatic/home/home-bg.pngstatic/home/operation.pngstatic/home/register.pngstatic/home/sign-out.pngstatic/home/welcome-bg.pngstatic/kmN54.pngBinary files differ
static/logo.pngBinary 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;