This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
+279
View File
@@ -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),
* 改为定时器逐帧推进 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>