1
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user