Files
hospital-rescue-app-v2/components/hr-scan-cam/hr-scan-cam.uvue
T
2026-09-24 16:25:22 +08:00

279 lines
6.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="cm">
<!-- 取景区:默认不启动相机,点击本区域才开启(避免进页面就占用摄像头) -->
<view class="cm-view" @click="openCamera">
<!-- 真实相机扫码(APP / H5 均可用),失败时保留下方深色取景底 -->
<!-- #ifdef APP || WEB -->
<x-mlkit-scannig-u v-if="cameraOn" :flash="false" :autoOpenCamera="true" :cameraWidth="1080"
:cameraeiHght="1080" style="width:100%;height:100%;position:absolute;left:0;top:0"
@scan="onScanResult"></x-mlkit-scannig-u>
<!-- #endif -->
<!-- 未开启:居中引导,提示点击整块取景区 -->
<view v-if="!cameraOn" class="cm-idle">
<x-icon name="camera-line" color="#60A5FA" font-size="30"></x-icon>
<text class="cm-idle-t">点击开启摄像头</text>
<text class="cm-idle-s">对准伤员腕带二维码,自动连续识别</text>
</view>
<view v-if="cameraOn" class="cm-frame">
<view class="cm-cf cm-cf-tl"></view>
<view class="cm-cf cm-cf-tr"></view>
<view class="cm-cf cm-cf-bl"></view>
<view class="cm-cf cm-cf-br"></view>
<view class="cm-laser" :style="laserStyle"></view>
</view>
<view class="cm-cnt">
<text class="cm-cnt-t">已识别 <text class="cm-cnt-n">{{ count }}</text> 人</text>
</view>
</view>
<view class="cm-acts">
<view :class="cameraOn ? 'cm-act cm-act-on' : 'cm-act'" @click="toggleCamera">
<x-icon :name="cameraOn ? 'scan-2-line' : 'play-circle-line'" :color="cameraOn ? '#6EE7B7' : '#FFFFFF'"
font-size="15"></x-icon>
<text :class="cameraOn ? 'cm-act-t cm-act-t-on' : 'cm-act-t'">
{{ cameraOn ? '连续扫码中' : '开启扫码' }}
</text>
</view>
<view class="cm-act" @click="onManual">
<x-icon name="keyboard-line" color="#FFFFFF" font-size="15"></x-icon>
<text class="cm-act-t">手动输入编号</text>
</view>
</view>
</view>
</template>
<script lang="uts" setup>
import { ref, computed, onUnmounted } from 'vue'
/**
* 连续扫码取景组件(批量处置 / 批量转运共用)
* 顶部相机为固定区,**默认不启动**:点击顶部取景区才开启摄像头实时识别腕带二维码,
* 识别结果通过 scan 事件抛给页面。
*/
const props = defineProps({
/** 已识别人数 */
count: { type: Number, default: 0 }
})
const emits = defineEmits<{
(e : 'scan', code : string) : void
(e : 'manual') : void
}>()
const cameraOn = ref(false)
const lastCode = ref('')
/*
* 取景框扫描线
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
* 改为定时器逐帧推进 topcm-frame 高 172rpx16%~84% ⇒ 28~145rpx。
* 周期 2.2s68ms × 32 帧),与设计稿 hrCamScanMove 一致。
*/
const laserTop = ref(28)
let laserT = 0.0
let laserTimer : number = 0
const laserStyle = computed(() : string => {
return 'top:' + laserTop.value.toString() + 'rpx;'
})
function startLaser() : void {
if (laserTimer != 0) { return }
laserTimer = setInterval(() => {
laserT = laserT + 0.03125 // 1/3232 帧正好走完一个往返,端点(16%/84%)可精确命中
if (laserT > 1) { laserT = laserT - 1 }
const k = laserT < 0.5 ? laserT * 2 : (1 - laserT) * 2
laserTop.value = Math.round(28 + k * 117)
}, 68)
}
function stopLaser() : void {
if (laserTimer != 0) {
clearInterval(laserTimer)
laserTimer = 0
}
}
function onScanResult(res : string[]) : void {
if (res.length == 0) { return }
const code = res[0]
// 同一条码 2 秒内不重复计入
if (code == lastCode.value) { return }
lastCode.value = code
emits('scan', code)
}
/** 点击顶部取景区:开启相机并启动扫描线(已在扫码则忽略) */
function openCamera() : void {
if (cameraOn.value) { return }
cameraOn.value = true
startLaser()
}
/** 底部按钮:开启 / 暂停连续扫码 */
function toggleCamera() : void {
cameraOn.value = !cameraOn.value
if (cameraOn.value) { startLaser() } else { stopLaser() }
}
function onManual() : void {
emits('manual')
}
onUnmounted(() => {
stopLaser()
})
</script>
<style>
.cm {
background-color: #0B1220;
padding: 16rpx 28rpx 18rpx 28rpx;
}
.cm-view {
height: 216rpx;
border-radius: 28rpx;
overflow: hidden;
position: relative;
background-color: #10192A;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
/* 未开启相机时的居中引导(整块取景区可点,引导用户点击开启) */
.cm-idle {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.cm-idle-t {
font-size: 28rpx;
font-weight: bold;
color: #E5E7EB;
margin-top: 14rpx;
}
.cm-idle-s {
font-size: 22.4rpx;
color: rgba(148, 163, 184, 0.92);
margin-top: 8rpx;
}
.cm-cnt {
position: absolute;
left: 20rpx;
top: 20rpx;
background-color: rgba(17, 24, 39, 0.62);
border-radius: 14rpx;
padding: 6rpx 16rpx 6rpx 16rpx;
}
.cm-cnt-t {
font-size: 23.6rpx;
font-weight: bold;
color: #FFFFFF;
}
.cm-cnt-n {
font-size: 23.6rpx;
font-weight: bold;
color: #60A5FA;
}
.cm-frame {
width: 172rpx;
height: 172rpx;
border-radius: 32rpx;
background-color: rgba(255, 255, 255, 0.03);
position: relative;
}
.cm-cf {
position: absolute;
width: 44rpx;
height: 44rpx;
border: 6rpx solid #60A5FA;
border-radius: 14rpx;
}
.cm-cf-tl {
left: 0rpx;
top: 0rpx;
border-right-width: 0rpx;
border-bottom-width: 0rpx;
}
.cm-cf-tr {
right: 0rpx;
top: 0rpx;
border-left-width: 0rpx;
border-bottom-width: 0rpx;
}
.cm-cf-bl {
left: 0rpx;
bottom: 0rpx;
border-right-width: 0rpx;
border-top-width: 0rpx;
}
.cm-cf-br {
right: 0rpx;
bottom: 0rpx;
border-left-width: 0rpx;
border-top-width: 0rpx;
}
/* 扫描线:⚠ App 端 uvue 不支持 @keyframes/animation
上下位移已改为 script 内定时器推进 laserTop + laserStyle 下发 top。 */
.cm-laser {
position: absolute;
left: 14rpx;
right: 14rpx;
top: 28rpx;
height: 4rpx;
border-radius: 4rpx;
background-color: #93C5FD;
}
.cm-acts {
display: flex;
flex-direction: row;
margin-top: 18rpx;
}
.cm-act {
flex: 1;
height: 68rpx;
border-radius: 18rpx;
background-color: rgba(255, 255, 255, 0.10);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.cm-act-on {
background-color: rgba(16, 185, 129, 0.20);
}
.cm-act-t {
font-size: 25.8rpx;
font-weight: bold;
color: rgba(255, 255, 255, 0.9);
margin-left: 10rpx;
}
.cm-act-t-on {
color: #6EE7B7;
}
</style>