This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
+397
View File
@@ -0,0 +1,397 @@
<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>