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

398 lines
9.6 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="hr-page" style="background-color:#F3F4F6">
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
<view class="wk-nav">
<text class="wk-nav-t">扫码出入库</text>
<view class="wk-nav-a" @click="toggleFlash">
<x-icon name="flashlight-line" :color="flash ? '#2563EB' : '#6B7280'" font-size="16"></x-icon>
<text class="wk-nav-a-t">手电</text>
</view>
</view>
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
<view class="hr-body">
<!-- ============ 取景框 ============ -->
<view class="sc-box">
<!-- 真实相机(APP / H5 -->
<!-- #ifdef APP || WEB -->
<x-mlkit-scannig-u :flash="flash" :autoOpenCamera="true" :cameraWidth="1080"
:cameraeiHght="1080" style="width:100%;height:100%;position:absolute;left:0;top:0"
@scan="onScan"></x-mlkit-scannig-u>
<!-- #endif -->
<view class="sc-frame">
<view class="sc-cf sc-cf-tl"></view>
<view class="sc-cf sc-cf-tr"></view>
<view class="sc-cf sc-cf-bl"></view>
<view class="sc-cf sc-cf-br"></view>
<view class="sc-line" :style="laserStyle"></view>
</view>
<text class="sc-tip">将条码对准框内 · 自动识别</text>
</view>
<!-- ============ 业务动作 ============ -->
<view class="hr-seg" style="margin-top:12px">
<view v-for="(m, i) in modes" :key="i" :class="i == modeIdx ? 'hr-seg-i hr-seg-i-on' : 'hr-seg-i'"
@click="modeIdx = i">
<text :class="i == modeIdx ? 'sc-seg-t sc-seg-t-on' : 'sc-seg-t'">{{ m }}</text>
</view>
</view>
<!-- ============ 识别反馈(三态) ============ -->
<hr-sec title="识别反馈" :first="false"></hr-sec>
<view v-if="feedbacks.length == 0" class="sc-empty">
<x-icon name="scan-line" color="#9CA3AF" font-size="24"></x-icon>
<text class="sc-empty-t">等待扫码…</text>
<text class="sc-empty-s">识别后会在此即时给出 ✅ 成功 / ⚠ 库存不足 / ❌ 未登记 三种反馈</text>
</view>
<view v-for="(f, i) in feedbacks" :key="i" :class="feedbackClass(f.theme)">
<view class="sc-st-ic" :style="{ backgroundColor: feedbackColor(f.theme) }">
<text class="sc-st-ic-t">{{ f.icon }}</text>
</view>
<view class="hr-flex1">
<text class="sc-st-b">{{ f.title }}</text>
<text class="sc-st-s">{{ f.sub }}</text>
</view>
</view>
<!-- ============ 本次连续扫入 ============ -->
<hr-sec title="本次连续扫入" :more="records.length + ' 条'"></hr-sec>
<hr-card variant="list">
<hr-lrow v-for="(r, i) in records" :key="i" :icon="r.icon"
:theme="r.theme == 'grn' ? 'grn' : (r.theme == 'amb' ? 'amb' : 'red')" :title="r.name"
:sub="r.time + ' · ' + r.delta" :value="r.status"
:value-theme="r.theme == 'grn' ? 'grn' : 'amb'" :value-bold="true"
:is-last="i == records.length - 1" @click="onRecord(r)"></hr-lrow>
</hr-card>
<view style="height:14px"></view>
</view>
</scroll-view>
<hr-tabbar :active="0"></hr-tabbar>
</view>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { scanFeedbacks, scanRecords } from '@/mock/index.uts'
import { HrScanFeedback, HrScanRecord } from '@/mock/types.uts'
/**
* s23 扫码出入库(仓管 Tab「扫码」)
* 连续扫码三态即时反馈:✅ 入库成功 / ⚠ 库存不足 / ❌ 未登记。
*/
const modes : string[] = ['入库', '出库', '盘点']
const modeIdx = ref(0)
const flash = ref(false)
const feedbacks = ref<HrScanFeedback[]>([
scanFeedbacks[0], scanFeedbacks[1], scanFeedbacks[2]
])
const records = ref<HrScanRecord[]>([
scanRecords[0], scanRecords[1], scanRecords[2]
])
let seq = 3
function shortCode(code : string) : string {
if (code.length <= 10) { return code }
return code.substring(0, 10)
}
function nowTime() : string {
const d = new Date()
const hh = d.getHours() < 10 ? '0' + d.getHours().toString() : d.getHours().toString()
const mm = d.getMinutes() < 10 ? '0' + d.getMinutes().toString() : d.getMinutes().toString()
return hh + ':' + mm
}
/** 依据条码特征给出三态反馈(mock 规则,后续替换为接口返回) */
function handleCode(code : string) : void {
seq = seq + 1
const fb : HrScanFeedback = {
theme: 'ok', icon: '✓',
title: '一次性医用口罩 · ' + modes[modeIdx.value] + '成功',
sub: '规格 50 只/盒 · 现存 320 盒 · 张明远 · 编码 ' + shortCode(code)
} as HrScanFeedback
const rec : HrScanRecord = {
name: '一次性医用口罩', time: nowTime(), delta: '+1 盒', status: '已入库', theme: 'grn', icon: '✓'
} as HrScanRecord
const fbArr : HrScanFeedback[] = []
fbArr.push(fb)
for (let i = 0; i < feedbacks.value.length; i++) {
if (i < 2) { fbArr.push(feedbacks.value[i]) }
}
feedbacks.value = fbArr
const recArr : HrScanRecord[] = []
recArr.push(rec)
for (let i = 0; i < records.value.length; i++) {
if (i < 4) { recArr.push(records.value[i]) }
}
records.value = recArr
uni.showToast({ title: '识别成功', icon: 'success' })
}
function onScan(res : any) : void {
const arr = res as string[]
if (arr == null || arr.length == 0) { return }
handleCode(arr[0])
}
function feedbackClass(theme : string) : string {
return `sc-st sc-st-${theme}`
}
function feedbackColor(theme : string) : string {
if (theme == 'ok') { return '#10B981' }
if (theme == 'warn') { return '#F59E0B' }
return '#DC2626'
}
function onRecord(r : HrScanRecord) : void {
uni.showModal({ title: r.name, content: r.time + ' · ' + r.delta + '\n状态:' + r.status, showCancel: false })
}
function toggleFlash() : void {
flash.value = !flash.value
uni.showToast({ title: flash.value ? '已打开手电' : '已关闭手电', icon: 'none' })
}
/*
* 取景框扫描线
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
* 改为定时器逐帧推进 topsc-frame 高 372rpx16%~84% ⇒ 60~313rpx。
* 周期 2.2s68ms × 32 帧),与设计稿 hrScanMove 一致。
*/
const laserTop = ref(60)
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(60 + k * 253)
}, 68)
}
function stopLaser() : void {
if (laserTimer != 0) {
clearInterval(laserTimer)
laserTimer = 0
}
}
onLoad(() => {
startLaser()
})
onUnload(() => {
stopLaser()
})
</script>
<style>
.wk-nav {
height: 96rpx;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 32rpx;
padding-right: 32rpx;
background-color: #FFFFFF;
border-bottom: 2rpx solid #F3F4F6;
}
.wk-nav-t {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
}
.wk-nav-a {
margin-left: auto;
display: flex;
flex-direction: row;
align-items: center;
}
.wk-nav-a-t {
font-size: 29.2rpx;
color: #6B7280;
margin-left: 8rpx;
}
/* ---------- 取景框 ---------- */
.sc-box {
width: 100%;
height: 686rpx;
background-color: #0F172A;
border-radius: 32rpx;
overflow: hidden;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.sc-frame {
width: 372rpx;
height: 372rpx;
position: relative;
}
.sc-cf {
position: absolute;
width: 52rpx;
height: 52rpx;
border: 6rpx solid #10B981;
border-radius: 8rpx;
}
.sc-cf-tl {
top: 0rpx;
left: 0rpx;
border-right-width: 0rpx;
border-bottom-width: 0rpx;
}
.sc-cf-tr {
top: 0rpx;
right: 0rpx;
border-left-width: 0rpx;
border-bottom-width: 0rpx;
}
.sc-cf-bl {
bottom: 0rpx;
left: 0rpx;
border-right-width: 0rpx;
border-top-width: 0rpx;
}
.sc-cf-br {
bottom: 0rpx;
right: 0rpx;
border-left-width: 0rpx;
border-top-width: 0rpx;
}
/* 扫描线:⚠ App 端 uvue 不支持 @keyframes/animation
上下位移已改为 script 内定时器推进 laserTop + laserStyle 下发 top。 */
.sc-line {
position: absolute;
left: 9%;
right: 9%;
top: 60rpx;
height: 4rpx;
background-color: #10B981;
}
.sc-tip {
position: absolute;
bottom: 24rpx;
left: 0rpx;
right: 0rpx;
text-align: center;
font-size: 25.8rpx;
color: rgba(255, 255, 255, 0.78);
}
.sc-seg-t {
font-size: 29.2rpx;
color: #6B7280;
}
.sc-seg-t-on {
color: #2563EB;
font-weight: bold;
}
/* ---------- 三态反馈 ---------- */
.sc-st {
display: flex;
flex-direction: row;
align-items: center;
padding: 22rpx 24rpx 22rpx 24rpx;
border-radius: 22rpx;
margin-bottom: 16rpx;
}
.sc-st-ok {
background-color: #ECFDF5;
border: 2rpx solid #A7F3D0;
}
.sc-st-warn {
background-color: #FFFBEB;
border: 2rpx solid #FDE68A;
}
.sc-st-err {
background-color: #FEF2F2;
border: 2rpx solid #FECACA;
}
.sc-st-ic {
width: 52rpx;
height: 52rpx;
border-radius: 16rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.sc-st-ic-t {
font-size: 29.2rpx;
font-weight: bold;
color: #FFFFFF;
}
.sc-st-b {
font-size: 28rpx;
font-weight: bold;
}
.sc-st-s {
font-size: 23.6rpx;
margin-top: 4rpx;
}
.sc-empty {
align-items: center;
padding-top: 40rpx;
padding-bottom: 40rpx;
}
.sc-empty-t {
font-size: 28rpx;
font-weight: bold;
color: #6B7280;
margin-top: 16rpx;
}
.sc-empty-s {
font-size: 23.6rpx;
color: #9CA3AF;
margin-top: 8rpx;
text-align: center;
}
</style>