@@ -57,10 +57,10 @@
< div class = "form-item" >
< a-form-item label = "物料编码" >
< a-input
v-model = "formData.inputMaterialCode"
placeholder = "请点击此处确保光标闪烁,并使用扫码枪扫描物料编码"
@keydown ="handleKeyDown"
@input ="handleMaterialCodeChange"
v-model = "formData.inputMaterialCode"
placeholder = "请点击此处确保光标闪烁,并使用扫码枪扫描物料编码"
@keydown ="handleKeyDown"
@input ="handleMaterialCodeChange"
/ >
< / a-form-item >
< / div >
@@ -71,8 +71,38 @@
<!-- 右侧信息 -- >
< div class = "right-section" >
< div class = "camera-section" >
< div class = "image-placeholder square-image" > 实时画面 相机1 < / div >
< div class = "image-placehold er square-image" > 实时画面 相机2 < / div >
<!-- 相机1 - WebSocket视频流 -- >
< div class = "video-contain er square-image" >
< canvas
ref = "camera1Canvas"
style = "width: 100%; height: 100%;"
> < / canvas >
<!-- 加载状态 -- >
< div v-if = "camera1Status === 'connecting'" class="video-overlay" >
< a -spin / >
< span style = "margin-left: 8px;" > 加载中 ... < / span >
< / div >
<!-- 错误状态 -- >
< div v-if = "camera1Status === 'error'" class="video-overlay error" >
< icon -close -circle -fill style = "color: #ff4d4f; font-size: 24px;" / >
< span > 连接失败 < / span >
< a-button size = "small" type = "primary" @click ="reconnectCamera1" > 重试 < / a -button >
< / div >
<!-- 未连接状态 -- >
< div v-if = "camera1Status === 'disconnected'" class="video-overlay" >
< icon -pause -circle -fill style = "color: #999; font-size: 24px;" / >
< span > 未连接 < / span >
< a-button size = "small" type = "primary" @click ="initCamera1" > 连接 < / a -button >
< / div >
<!-- 摄像头状态标识 -- >
< div v-if = "camera1Status === 'connected'" class="camera-badge" >
< span class = "live-badge" > LIVE < / span >
< / div >
< / div >
< / div >
< div class = "info-section" >
@@ -93,16 +123,15 @@
< div class = "info-item" >
< span class = "label" > 比对结果 : < / span >
< span
class = "value match-result" : class = "{
'match-success': formData.m atchResult === 'success',
'match-failed': formData.m atchResult === 'failed',
'match-pending': !formData.m atchResult,
class = "value match-result" : class = "{
'match-success': compareM atchResult === 'success',
'match-failed': compareM atchResult === 'failed',
'match-pending': !compareM atchResult,
}"
>
< icon-check-circle-fill v-if = "formData.m atchResult === 'success'" style="color: #52c41a; margin-right: 8px; font-size: 25px;" / >
< icon-close-circle-fill v-else-if = "formData.m atchResult === 'failed'" style="color: #ff4d4f; margin-right: 8px; font-size: 25px;" / >
{ { formData . matchResult === 'success' ? '成功' : formData . matchResult === 'failed' ? '失败' : '-' } }
< icon-check-circle-fill v-if = "compareM atchResult === 'success'" style="color: #52c41a; margin-right: 8px; font-size: 25px;" / >
< icon-close-circle-fill v-else-if = "compareM atchResult === 'failed'" style="color: #ff4d4f; margin-right: 8px; font-size: 25px;" / >
{ { compareMatchResult === 'success' ? '成功' : compareMatchResult === 'failed' ? '失败' : '请放置需比对的物料到扫描区域!' } }
< / span >
< / div >
< / div >
@@ -144,11 +173,11 @@
<!-- 这里是摄像头画面 - 替换成FLV播放器 -- >
< div class = "video-container large-image" >
< video
ref = "cameraVideo"
autoplay
muted
playsinline
style = "width: 100%; height: 100%; object-fit: cover;"
ref = "cameraVideo"
autoplay
muted
playsinline
style = "width: 100%; height: 100%; object-fit: cover;"
> < / video >
<!-- 加载状态 -- >
@@ -247,20 +276,19 @@
<!-- 操作按钮 -- >
< div class = "action-buttons" >
< a-button
v-if = "activeStep > 1 && activeStep < 3"
class = "previous-button"
@click ="handlePrevious"
v-if = "activeStep > 1 && activeStep < 3"
class = "previous-button"
@click ="handlePrevious"
>
上一步
< / a-button >
< a-button
v-if = "activeStep < 3"
type = "primary"
: disabled = "activeStep === 1 && formData.matchResult !== 'success' "
class = "next-button "
@click ="handleNext"
v-if = "activeStep < 3"
type = "primary"
class = "next-button "
@click ="handleNext "
>
{ { activeStep === 2 ? '完成' : '下一步' } }
{ { activeStep === 2 ? '完成' : ( compareMatchResult === 'success' ? '下一步' : '开始比对' ) } }
< / a-button >
< / div >
< / div >
@@ -271,7 +299,7 @@
import { nextTick , onBeforeUnmount , onMounted , onUnmounted , reactive , ref } from 'vue'
import { Message , Modal , Notification } from '@arco-design/web-vue'
import { getMaterialDetail , validateWeighing } from '@/apis/weightManage/weightManage'
import { getMaterialDetail , validateWeighing , vmSend } from '@/apis/weightManage/weightManage'
import { type WorkOrderResp , addWorkOrder , getWorkOrder } from '@/apis/workOrder/workOrder'
import { catchPhoto } from '@/apis/material/materialInfo'
@@ -292,14 +320,23 @@ const formData = reactive({
materialSpec : '' , // 物料规格
unitWeight : 0 , // 重量
photoUrl : '' , // 样图URL
matchResult : '' , // 比对结果
} )
//比对结果
const compareMatchResult = ref ( '' )
// 摄像头状态
const cameraStatus = ref < 'connected' | 'connecting' | 'disconnected' | 'error' > ( 'disconnected' )
const camera1Status = ref < 'connected' | 'connecting' | 'disconnected' | 'error' > ( 'disconnected' )
// 视频元素引用
const cameraVideo = ref < HTMLVideoElement | null > ( null )
// 相机1使用canvas元素
const camera1Canvas = ref < HTMLCanvasElement | null > ( null )
// 相机1 WebSocket连接
const camera1Ws = ref < WebSocket | null > ( null )
const camera1WsConnected = ref ( false )
// FLV播放器实例
let player : any = null
@@ -519,20 +556,30 @@ const stopCamera = () => {
// 监听步骤变化
watch ( activeStep , ( newVal ) => {
if ( newVal === 2 ) {
if ( newVal === 1 ) {
// 进入扫码核验页面, 启动相机1
nextTick ( ( ) => {
initCamera1 ( )
} )
} else if ( newVal === 2 ) {
// 进入称重登记页面,启动摄像头
nextTick ( ( ) => {
initCamera ( )
} )
} else {
// 离开称重登记 页面,停止摄像头
// 离开相关 页面,停止摄像头
stopCamera ( )
closeCamera1WebSocket ( )
}
} )
// 组件挂载时
onMounted ( async ( ) => {
await loadFlvJs ( )
// 如果当前是扫码核验页面, 初始化相机1
if ( activeStep . value === 1 ) {
initCamera1 ( )
}
} )
// 组件卸载时
@@ -649,7 +696,7 @@ const originalHandleMaterialCodeChange = async () => {
formData . materialSpec = ''
formData . unitWeight = 0
formData . photoUrl = ''
formData . m atchResult = ''
compareM atchResult. value = '' ;
// 如果有物料编码输入,获取物料数据
if ( materialCode ) {
@@ -699,9 +746,6 @@ const fetchMaterialData = async (code: string) => {
formData . materialSpec = res . data ? . materialSpec || ''
formData . unitWeight = res . data ? . unitWeight || 0
formData . photoUrl = res . data ? . photoUrl || ''
// 假设后端返回比对结果
// formData.matchResult = res.data.matchResult || 'failed' // 这里根据实际接口返回调整
formData . matchResult = res . data ? . matchResult || 'success' // 这里根据实际接口返回调整
return true
}
} )
@@ -749,6 +793,35 @@ const handleNext = async () => {
}
} ,
} )
} else if ( activeStep . value === 1 && compareMatchResult . value !== 'success' ) {
// 当在扫码核验页面点击开始比对时,调用后端接口获取比对结果
try {
// 这里应该调用后端的比对接口, 暂时使用fetchMaterialData模拟
// 实际项目中应该替换为专门的比对接口
const materialCode = formData . inputMaterialCode ? . trim ( )
if ( ! materialCode ) {
Message . error ( '请先扫描物料编码' )
return
}
// 调用后端接口获取比对结果
vmSend ( materialCode ) . then ( ( res ) => {
if ( res . code === '0' ) {
compareMatchResult . value = JSON . parse ( res . data ? . matchResult || '' )
}
} )
if ( compareMatchResult . value === 'success' ) {
// 比对成功,按钮变为下一步
Message . success ( '比对成功' )
} else {
// 比对失败,提示错误
Message . error ( '比对失败' )
}
} catch ( error ) {
console . error ( '比对失败:' , error )
Message . error ( '比对失败,请重试' )
}
} else {
activeStep . value ++
// 进入称重页面时建立WebSocket连接
@@ -759,6 +832,121 @@ const handleNext = async () => {
}
}
// 初始化相机1 WebSocket连接
const initCamera1 = ( ) => {
camera1Status . value = 'connecting'
try {
const wsUrl = 'ws://localhost:6609/ws/camera'
camera1Ws . value = new WebSocket ( wsUrl )
camera1Ws . value . onopen = ( ) => {
camera1Status . value = 'connected'
camera1WsConnected . value = true
Message . success ( '已和相机1建立连接' )
}
// 修改前端代码,处理黑白相机的视频流数据
// 修改前端代码, 处理压缩后的JPEG图像数据
camera1Ws . value . onmessage = ( event ) => {
try {
if ( camera1Canvas . value && event . data instanceof Blob ) {
// 处理二进制图像数据
const reader = new FileReader ( ) ;
reader . onload = ( e ) => {
if ( e . target && e . target . result ) {
const arrayBuffer = e . target . result ;
const uint8Array = new Uint8Array ( arrayBuffer ) ;
// 解析消息格式: [width(4 bytes)][height(4 bytes)][image data]
if ( uint8Array . length < 8 ) {
console . error ( '相机1 WebSocket消息格式错误: 数据长度不足' ) ;
return ;
}
// 读取宽度和高度(小端序)
const width = uint8Array [ 0 ] | ( uint8Array [ 1 ] << 8 ) | ( uint8Array [ 2 ] << 16 ) | ( uint8Array [ 3 ] << 24 ) ;
const height = uint8Array [ 4 ] | ( uint8Array [ 5 ] << 8 ) | ( uint8Array [ 6 ] << 16 ) | ( uint8Array [ 7 ] << 24 ) ;
// 检查宽度和高度是否合理
if ( width <= 0 || height <= 0 || width > 4096 || height > 4096 ) {
console . error ( '相机1 WebSocket消息格式错误: 无效的图像尺寸' ) ;
return ;
}
// 提取压缩的图像数据
const imageDataStart = 8 ;
const imageData = uint8Array . subarray ( imageDataStart ) ;
// 创建Blob对象并转换为URL
const blob = new Blob ( [ imageData ] , { type : 'image/jpeg' } ) ;
const imageUrl = URL . createObjectURL ( blob ) ;
// 创建Image对象并显示
const img = new Image ( ) ;
img . onload = ( ) => {
// 设置canvas尺寸
camera1Canvas . value . width = width ;
camera1Canvas . value . height = height ;
// 获取canvas上下文
const ctx = camera1Canvas . value . getContext ( '2d' ) ;
if ( ctx ) {
// 绘制图像
ctx . drawImage ( img , 0 , 0 , width , height ) ;
}
// 释放URL对象
URL . revokeObjectURL ( imageUrl ) ;
} ;
img . onerror = ( ) => {
console . error ( '图像加载失败' ) ;
URL . revokeObjectURL ( imageUrl ) ;
} ;
img . src = imageUrl ;
}
} ;
reader . readAsArrayBuffer ( event . data ) ;
}
} catch ( error ) {
console . error ( '相机1 WebSocket消息解析失败:' , error )
}
}
camera1Ws . value . onclose = ( ) => {
camera1Status . value = 'disconnected'
camera1WsConnected . value = false
}
camera1Ws . value . onerror = ( error ) => {
console . error ( '相机1 WebSocket错误:' , error )
camera1Status . value = 'error'
Message . error ( '相机1连接失败' )
}
} catch ( error ) {
console . error ( '建立相机1 WebSocket连接失败:' , error )
camera1Status . value = 'error'
Message . error ( '无法建立相机1连接' )
}
}
// 重新连接相机1
const reconnectCamera1 = ( ) => {
closeCamera1WebSocket ( )
setTimeout ( ( ) => initCamera1 ( ) , 1000 )
}
// 关闭相机1 WebSocket连接
const closeCamera1WebSocket = ( ) => {
if ( camera1Ws . value ) {
camera1Ws . value . close ( )
camera1Ws . value = null
camera1WsConnected . value = false
camera1Status . value = 'disconnected'
}
}
// 处理上一步
const handlePrevious = ( ) => {
if ( activeStep . value > 1 ) {
@@ -783,7 +971,8 @@ const handleBackToFirst = () => {
formData . materialSpec = ''
formData . unitWeight = 0
formData . photoUrl = ''
formData . matchResult = ''
compareMatchResult . value = ''
// 重置称重登记页面数据
inputQuantity . value = ''
@@ -1009,6 +1198,7 @@ const closeWebSocket = () => {
// 组件卸载时关闭WebSocket连接
onUnmounted ( ( ) => {
closeWebSocket ( )
closeCamera1WebSocket ( )
} )
< / script >
@@ -1093,6 +1283,7 @@ onUnmounted(() => {
/* 主内容区域 */
. main - content {
display : flex ;
align - items : flex - start ;
margin - bottom : 30 px ;
}
@@ -1145,7 +1336,7 @@ onUnmounted(() => {
background : # 000 ;
border - radius : 4 px ;
overflow : hidden ;
margin : 20 px 0 ;
margin - bottom : 20 px ;
}
. video - container video {
@@ -1428,4 +1619,4 @@ onUnmounted(() => {
font - size : 14 px ;
margin - right : 12 px ;
}
< / style >
< / style >