279 lines
6.5 KiB
Plaintext
279 lines
6.5 KiB
Plaintext
<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),
|
||
* 改为定时器逐帧推进 top:cm-frame 高 172rpx,16%~84% ⇒ 28~145rpx。
|
||
* 周期 2.2s(68ms × 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/32:32 帧正好走完一个往返,端点(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> |