1278 lines
33 KiB
Vue
1278 lines
33 KiB
Vue
<template>
|
||
<div class="gi_page">
|
||
<div class="container">
|
||
<!-- 步骤导航 -->
|
||
<div class="step-nav">
|
||
<div class="step-item" :class="{ active: activeStep >= 1, completed: activeStep > 1 }">
|
||
<div class="step-circle">1</div>
|
||
<div class="step-title">扫码核验</div>
|
||
</div>
|
||
<div class="step-line"></div>
|
||
<div class="step-item" :class="{ active: activeStep >= 2, completed: activeStep > 2 }">
|
||
<div class="step-circle">2</div>
|
||
<div class="step-title">称重登记</div>
|
||
</div>
|
||
<div class="step-line"></div>
|
||
<div class="step-item" :class="{ active: activeStep >= 3, completed: activeStep > 3 }">
|
||
<div class="step-circle">3</div>
|
||
<div class="step-title">完成创建</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤内容 -->
|
||
<!-- 扫码核验页面 -->
|
||
<div v-if="activeStep === 1" class="step-content">
|
||
<div class="main-content">
|
||
<!-- 左侧表单 -->
|
||
<div class="left-section">
|
||
<a-image v-if="!formData.photoUrl" :src="formData.photoUrl" />
|
||
<img v-else :src="formData.photoUrl" class="sample-image square-image" alt="样图">
|
||
|
||
<a-form :model="formData" layout="vertical">
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<a-form-item label="物料名称">
|
||
<a-input v-model="formData.materialName" placeholder="物料名称" disabled />
|
||
</a-form-item>
|
||
</div>
|
||
<div class="form-item">
|
||
<a-form-item label="物料规格">
|
||
<a-input v-model="formData.materialSpec" placeholder="物料规格" disabled />
|
||
</a-form-item>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<a-form-item label="物料编码">
|
||
<a-input v-model="formData.encoding" placeholder="物料编码" disabled />
|
||
</a-form-item>
|
||
</div>
|
||
<div class="form-item">
|
||
<a-form-item label="重量(g)">
|
||
<a-input v-model="formData.unitWeight" placeholder="Kg" disabled />
|
||
</a-form-item>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<a-form-item label="物料编码">
|
||
<a-input
|
||
ref="materialCodeInput"
|
||
v-model="formData.inputMaterialCode"
|
||
placeholder="请点击此处确保光标闪烁,且输入法为英文状态,使用扫码枪扫描物料编码"
|
||
@keydown="handleKeyDown"
|
||
@input="handleMaterialCodeChange"
|
||
/>
|
||
</a-form-item>
|
||
</div>
|
||
</div>
|
||
</a-form>
|
||
</div>
|
||
|
||
<!-- 右侧信息 -->
|
||
<div class="right-section">
|
||
<div class="camera-section">
|
||
<!-- 图片展示 -->
|
||
<div class="image-placeholder square-image">
|
||
<img
|
||
:src="imgData.imgUrl"
|
||
alt="图片展示"
|
||
style="width: 100%; height: 100%; object-fit: cover; border-radius: 4px;"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<div class="info-card">
|
||
<h4>识别信息</h4>
|
||
<div class="info-item">
|
||
<span class="label">物料名称:</span>
|
||
<span class="value">{{ formData.materialName || '-' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">物料编码:</span>
|
||
<span class="value">{{ formData.encoding || '-' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-card">
|
||
<h4>比对结果</h4>
|
||
<div class="info-item">
|
||
<span class="label">比对结果:</span>
|
||
<span
|
||
class="value match-result" :class="{
|
||
'match-success': compareMatchResult === 'success',
|
||
'match-failed': compareMatchResult === 'failed',
|
||
'match-pending': !compareMatchResult,
|
||
}"
|
||
>
|
||
<icon-check-circle-fill v-if="compareMatchResult === 'success'" style="color: #52c41a; margin-right: 8px; font-size: 25px;" />
|
||
<icon-close-circle-fill v-else-if="compareMatchResult === 'failed'" style="color: #ff4d4f; margin-right: 8px; font-size: 25px;" />
|
||
{{ compareMatchResult === 'success' ? '成功' : compareMatchResult === 'failed' ? '失败' : '请放置需比对的物料到扫描区域!' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 称重登记页面 - 这里放FLV播放器 -->
|
||
<div v-else-if="activeStep === 2" class="step-content">
|
||
<div class="weighing-content">
|
||
<!-- 左侧输入区域 -->
|
||
<div class="left-weighing-section">
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<label>物料名称:</label>
|
||
<a-input v-model="formData.materialName" placeholder="物料名称" disabled />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<label>输入数量:</label>
|
||
<a-input-number :min="1" mode="button" v-model="inputQuantity" placeholder="请输入数量" @change="calculateWeight" />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<label>标准重量(g):</label>
|
||
<a-input v-model="calculatedWeight" placeholder="-" disabled />
|
||
</div>
|
||
<div class="form-item">
|
||
<label>称重重量(g):</label>
|
||
<a-input v-model="ahDeviceWeight" placeholder="-" disabled />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<label>该物料重量范围:</label>
|
||
<a-input v-model="formData.weightRange" placeholder="该物料重量范围" disabled />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图片展示 -->
|
||
<div class="image-placeholder square-image">
|
||
<img
|
||
:src="weighingImgData.imgUrl"
|
||
alt="称重图片"
|
||
style="width: 100%; height: 100%; object-fit: cover; border-radius: 4px;"
|
||
/>
|
||
<!-- 错误状态 -->
|
||
<div v-if="weighingPageStatus === 'error'" class="video-overlay error">
|
||
<icon-close-circle-fill style="color: #ff4d4f; font-size: 24px;" />
|
||
<span>连接异常</span>
|
||
<a-button size="small" type="primary" @click="enterWeighPage">重试</a-button>
|
||
</div>
|
||
<!-- 加载状态 -->
|
||
<div v-if="weighingPageStatus === 'entering'" class="video-overlay">
|
||
<a-spin />
|
||
<span style="margin-left: 8px;">加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="weighing-actions">
|
||
<a-button type="primary" @click="handleConfirm">确定</a-button>
|
||
<a-button style="margin-left: 12px;" @click="handleReset">重置</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧表格区域 -->
|
||
<div class="right-weighing-section">
|
||
<div class="weighing-section">
|
||
<div class="section-header">
|
||
<h4>称重列表</h4>
|
||
</div>
|
||
<a-table :columns="columns" :data="weighingList" bordered>
|
||
<template #image="{ record }">
|
||
<a-image width="60" :src="record.image"/>
|
||
</template>
|
||
|
||
<template #action="{ record }">
|
||
<a-button type="text" status="danger" @click="handleDelete(record.key)">
|
||
删除
|
||
</a-button>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 完成创建页面 -->
|
||
<div v-else-if="activeStep === 3" class="step-content">
|
||
<div class="completion-content">
|
||
<div class="completion-icon">
|
||
<a-icon type="check-circle" :size="64" style="color: #52c41a;" />
|
||
</div>
|
||
<h2>{{ workOrderResp.matchResult === 'success' ? '创建成功' : '创建失败' }}</h2>
|
||
<p>{{ workOrderResp.matchResult === 'success' ? '任务创建成功' : '任务创建失败' }},以下是任务详情:</p>
|
||
<div class="completion-info">
|
||
<div class="info-item">
|
||
<span class="label">任务工单号:</span>
|
||
<span class="value">{{ workOrderResp.orderNo }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">物料名称:</span>
|
||
<span class="value">{{ formData.materialName }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">物料编码:</span>
|
||
<span class="value">{{ formData.encoding }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">物料总个数:</span>
|
||
<span class="value">{{ workOrderResp.totalCount }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">标准总重量(g):</span>
|
||
<span class="value">{{ workOrderResp.totalCalculatedWeight }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">实际总重量(g):</span>
|
||
<span class="value">{{ workOrderResp.totalWeight }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="completion-actions">
|
||
<a-button type="primary" @click="onPrint">打印</a-button>
|
||
<a-button type="primary" @click="handleBackToFirst">返回首页</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="action-buttons">
|
||
<a-button
|
||
v-if="activeStep > 1 && activeStep < 3"
|
||
class="previous-button"
|
||
@click="handlePrevious"
|
||
>
|
||
上一步
|
||
</a-button>
|
||
<a-button
|
||
v-if="activeStep < 3"
|
||
type="primary"
|
||
class="next-button"
|
||
@click="handleNext"
|
||
>
|
||
{{ activeStep === 2 ? '完成' : (compareMatchResult === 'success' ? '下一步' : '开始比对') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||
import { Message, Modal, Notification } from '@arco-design/web-vue'
|
||
import { getMaterialDetail, validateWeighing, vmSend } from '@/apis/weightManage/weightManage'
|
||
import {type WorkOrderResp, addWorkOrder} from '@/apis/workOrder/workOrder'
|
||
import {getCaptureImage, getCheckStatus, getEnterWeighPage, getLeaveWeighPage} from "@/apis/weightManage/ys";
|
||
import {brightness, connect, disconnect} from "@/apis/weightManage/light";
|
||
|
||
import router from "@/router";
|
||
import type {TableInstanceColumns} from "@/components/GiTable/type";
|
||
|
||
defineOptions({ name: 'WeightManage' })
|
||
|
||
// 当前步骤
|
||
const activeStep = ref(1)
|
||
|
||
// 表单数据
|
||
const formData = reactive({
|
||
inputMaterialCode: '', // 输入的物料编码
|
||
id: '', // 物料ID
|
||
encoding: '', // 物料编码
|
||
materialName: '', // 物料名称
|
||
materialSpec: '', // 物料规格
|
||
unitWeight: 0, // 重量
|
||
photoUrl: '', // 样图URL
|
||
weightRange: '', // 物料重量范围
|
||
})
|
||
|
||
const imgData = reactive({
|
||
imgUrl: 'http://localhost:6609/file/001.bmp', // 样图URL
|
||
baseUrl: 'http://localhost:6609/file/001.bmp' // 基础URL
|
||
})
|
||
|
||
const weighingImgData = reactive({
|
||
imgUrl: 'http://localhost:6609/file/ys/carousel.jpg', // 称重页面图片URL
|
||
baseUrl: 'http://localhost:6609/file/ys/carousel.jpg' // 基础URL
|
||
})
|
||
|
||
// 比对结果
|
||
const compareMatchResult = ref('')
|
||
|
||
// 物料编码输入框引用
|
||
const materialCodeInput = ref<any>(null)
|
||
|
||
// 称重页面状态
|
||
const weighingPageStatus = ref<'idle' | 'entering' | 'entered' | 'error'>('idle')
|
||
|
||
// 检查状态定时器
|
||
let statusCheckTimer: any = null
|
||
|
||
|
||
// 图片刷新定时器
|
||
let imageRefreshTimer: any = null
|
||
|
||
// 进入称重页面
|
||
const enterWeighPage = async () => {
|
||
weighingPageStatus.value = 'entering'
|
||
try {
|
||
const res = await getEnterWeighPage();
|
||
if (res.code === '0') {
|
||
weighingPageStatus.value = 'entered'
|
||
// 启动状态检查
|
||
startStatusCheck()
|
||
} else {
|
||
weighingPageStatus.value = 'error'
|
||
Message.error(res.msg)
|
||
}
|
||
} catch (error) {
|
||
weighingPageStatus.value = 'error'
|
||
Message.error("摄像头连接失败")
|
||
}
|
||
}
|
||
|
||
// 离开称重页面
|
||
const leaveWeighPage = async () => {
|
||
try {
|
||
await getLeaveWeighPage();
|
||
} catch (error) {
|
||
console.error('离开称重页面失败:', error)
|
||
}
|
||
// 停止状态检查
|
||
stopStatusCheck()
|
||
weighingPageStatus.value = 'idle'
|
||
}
|
||
|
||
// 手动抓图
|
||
const captureImage = async (imgName) => {
|
||
try {
|
||
const response = await getCaptureImage(imgName);
|
||
if (response.code === '0') {
|
||
return response.data
|
||
}
|
||
return null
|
||
} catch (error) {
|
||
console.error('抓图失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 检查状态
|
||
const checkStatus = async () => {
|
||
try {
|
||
const response = await getCheckStatus();
|
||
if (response.code !== '0') {
|
||
weighingPageStatus.value = 'error'
|
||
}
|
||
} catch (error) {
|
||
weighingPageStatus.value = 'error'
|
||
}
|
||
}
|
||
|
||
// 启动状态检查
|
||
const startStatusCheck = () => {
|
||
// 每30秒检查一次状态
|
||
statusCheckTimer = setInterval(checkStatus, 30000)
|
||
}
|
||
|
||
// 停止状态检查
|
||
const stopStatusCheck = () => {
|
||
if (statusCheckTimer) {
|
||
clearInterval(statusCheckTimer)
|
||
statusCheckTimer = null
|
||
}
|
||
}
|
||
|
||
// 组件挂载时
|
||
onMounted(() => {
|
||
// 启动图片自动刷新,每秒更新一次
|
||
imageRefreshTimer = setInterval(() => {
|
||
// 添加时间戳参数,避免浏览器缓存
|
||
imgData.imgUrl = `${imgData.baseUrl}?t=${Date.now()}`
|
||
weighingImgData.imgUrl = `${weighingImgData.baseUrl}?t=${Date.now()}`
|
||
}, 1500)
|
||
|
||
// 页面加载后自动聚焦到物料编码输入框
|
||
nextTick(() => {
|
||
if (materialCodeInput.value) {
|
||
materialCodeInput.value.focus()
|
||
}
|
||
})
|
||
|
||
// 初始进入扫码验证页面,连接灯光
|
||
if (activeStep.value === 1) {
|
||
connect().catch(error => {
|
||
console.error('连接灯光失败:', error)
|
||
})
|
||
}
|
||
|
||
// 监听步骤变化
|
||
watch(activeStep, (newVal) => {
|
||
if (newVal === 1) {
|
||
// 进入扫码验证页面,连接灯光
|
||
connect().catch(error => {
|
||
console.error('连接灯光失败:', error)
|
||
})
|
||
} else if (newVal === 2) {
|
||
// 进入称重登记页面,断开灯光
|
||
disconnect().catch(error => {
|
||
console.error('断开灯光失败:', error)
|
||
})
|
||
nextTick(() => {
|
||
enterWeighPage()
|
||
})
|
||
} else {
|
||
// 离开称重页面,断开灯光
|
||
leaveWeighPage()
|
||
disconnect().catch(error => {
|
||
console.error('断开灯光失败:', error)
|
||
})
|
||
}
|
||
})
|
||
})
|
||
|
||
// 组件卸载时
|
||
onBeforeUnmount(() => {
|
||
// 清除图片刷新定时器
|
||
if (imageRefreshTimer) {
|
||
clearInterval(imageRefreshTimer)
|
||
imageRefreshTimer = null
|
||
}
|
||
// 停止状态检查
|
||
stopStatusCheck()
|
||
// 离开称重页面
|
||
if (activeStep.value === 2) {
|
||
leaveWeighPage()
|
||
}
|
||
// 断开灯光连接
|
||
disconnect().catch(error => {
|
||
console.error('断开灯光失败:', error)
|
||
})
|
||
})
|
||
|
||
const workOrderResp = ref<WorkOrderResp>({
|
||
id: '',
|
||
title: '',
|
||
orderNo: '',
|
||
materialName: '',
|
||
encoding: '',
|
||
unitWeight: '',
|
||
materialSpec: '',
|
||
photoUrl: '',
|
||
totalWeight: '',
|
||
totalCount: '',
|
||
createUserString: '',
|
||
updateUserString: '',
|
||
totalCalculatedWeight: '',
|
||
workOrderInfos: [],
|
||
matchResult: 'failed',
|
||
qrCodeData: '',
|
||
})
|
||
|
||
// 称重登记页面数据
|
||
const inputQuantity = ref()
|
||
// todo
|
||
const ahDeviceWeight = ref('')
|
||
const calculatedWeight = ref('')
|
||
const weighingCount = ref(1)
|
||
|
||
// WebSocket连接
|
||
const ws = ref<WebSocket | null>(null)
|
||
const wsConnected = ref(false)
|
||
|
||
// 称重列表数据
|
||
const weighingList = ref([
|
||
// 初始数据可以在这里添加
|
||
])
|
||
|
||
// 称重表格列
|
||
const columns = ref<TableInstanceColumns[]>([
|
||
{ title: '序号', dataIndex: 'weightTime', key: 'weightTime',},
|
||
{ title: '数量', dataIndex: 'quantity', key: 'quantity', className: 'green-bg',},
|
||
{ title: '称重重量(g)', dataIndex: 'weight', key: 'weight', className: 'green-bg',},
|
||
{ title: '标准重量(g)', dataIndex: 'calculatedWeight', key: 'calculatedWeight',},
|
||
{ title: '抓图', dataIndex: 'image', key: 'image',slotName: 'image',},
|
||
{ title: '操作', dataIndex: 'action', key: 'action', slotName: 'action',},
|
||
])
|
||
|
||
// 防抖函数
|
||
const debounce = <T extends (...args: any[]) => any>(func: T, delay: number): ((...args: Parameters<T>) => void) => {
|
||
let timer: ReturnType<typeof setTimeout> | null = null
|
||
return function (...args: Parameters<T>) {
|
||
if (timer) clearTimeout(timer)
|
||
timer = setTimeout(() => {
|
||
func(...args)
|
||
}, delay)
|
||
}
|
||
}
|
||
|
||
// 原始的物料编码变化处理函数
|
||
const originalHandleMaterialCodeChange = async () => {
|
||
// 确保输入框内容是完整的物料编码
|
||
const materialCode = formData.inputMaterialCode?.trim()
|
||
|
||
// 无论是否有输入,先重置所有物料相关字段,确保新数据能完全覆盖旧数据
|
||
formData.encoding = ''
|
||
formData.materialName = ''
|
||
formData.materialSpec = ''
|
||
formData.unitWeight = 0
|
||
formData.photoUrl = ''
|
||
formData.weightRange = ''
|
||
compareMatchResult.value = '';
|
||
|
||
// 如果有物料编码输入,获取物料数据
|
||
if (materialCode) {
|
||
try {
|
||
await fetchMaterialData(materialCode)
|
||
} catch (error) {
|
||
console.error('获取物料数据失败:', error)
|
||
// 即使获取失败,也保持字段为空,避免显示旧数据
|
||
}
|
||
}
|
||
}
|
||
|
||
// 记录上次输入时间,用于判断是否是扫码枪输入
|
||
let lastInputTime = 0
|
||
// 记录是否正在接收扫码输入
|
||
let isScanning = false
|
||
|
||
// 处理键盘按下事件
|
||
const handleKeyDown = (event: KeyboardEvent) => {
|
||
// 获取当前时间
|
||
const currentTime = Date.now()
|
||
|
||
// 计算时间差
|
||
const timeDiff = currentTime - lastInputTime
|
||
// 检查是否是回车键(扫码枪通常以回车键结束)
|
||
if (event.key === 'Enter') {
|
||
// 扫码结束,标记为不在扫描中
|
||
isScanning = false
|
||
// 不阻止默认行为,让表单可以正常提交
|
||
}
|
||
// 检查是否是新的扫码开始
|
||
// 当时间间隔大于300ms,且不是修饰键,且不是功能键时,认为是新的扫码开始
|
||
else if (timeDiff > 300 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||
// 清空输入框,准备接收新的扫码数据
|
||
formData.inputMaterialCode = ''
|
||
// 标记为开始扫描
|
||
isScanning = true
|
||
// }
|
||
}
|
||
|
||
// 更新上次输入时间
|
||
lastInputTime = currentTime
|
||
}
|
||
|
||
// 带防抖的物料编码变化处理函数
|
||
const handleMaterialCodeChange = debounce(originalHandleMaterialCodeChange, 500) // 500ms防抖延迟
|
||
|
||
// 扫码核验获取物料信息
|
||
const fetchMaterialData = async (code: string) => {
|
||
const res = await getMaterialDetail(code);
|
||
if (res.code === '0') {
|
||
// 更新表单数据
|
||
formData.id = res.data?.id || ''
|
||
formData.encoding = res.data?.encoding || ''
|
||
formData.materialName = res.data?.materialName || ''
|
||
formData.materialSpec = res.data?.materialSpec || ''
|
||
formData.unitWeight = res.data?.unitWeight || 0
|
||
formData.photoUrl = res.data?.photoUrl || ''
|
||
formData.weightRange = (res.data?.downFloatRatio || '-') + '% ~ ' + (res.data?.upFloatRatio || '-') + '%'
|
||
}
|
||
if(res.data && res.data.id) {
|
||
await brightness(res.data.id);
|
||
}
|
||
}
|
||
|
||
// 处理下一步
|
||
const handleNext = async () => {
|
||
if (activeStep.value < 3) {
|
||
if (activeStep.value === 2) {
|
||
// 当在称重登记页面点击完成时,显示确认弹框
|
||
Modal.confirm({
|
||
title: '确认完成',
|
||
content: '确定要完成称重登记吗?',
|
||
onOk: async () => {
|
||
try {
|
||
// 准备工作订单数据
|
||
const workOrderData = {
|
||
materialId: formData.id,
|
||
workOrderInfos: weighingList.value,
|
||
}
|
||
// 调用后端接口
|
||
addWorkOrder(workOrderData).then((res) => {
|
||
if (res.code === '0') {
|
||
Notification.success({
|
||
title: '操作成功',
|
||
content: `工单创建成功!`,
|
||
})
|
||
workOrderResp.value.id = res.data?.id || ''
|
||
workOrderResp.value.matchResult = 'success'
|
||
workOrderResp.value.title = res.data?.title || ''
|
||
workOrderResp.value.orderNo = res.data?.orderNo || ''
|
||
workOrderResp.value.totalWeight = res.data?.totalWeight || ''
|
||
workOrderResp.value.totalCalculatedWeight = res.data?.totalCalculatedWeight || ''
|
||
workOrderResp.value.totalCount = res.data?.totalCount || ''
|
||
|
||
// 关闭WebSocket连接
|
||
closeWebSocket()
|
||
// 跳转到完成页面
|
||
activeStep.value++
|
||
return true
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('创建工作订单失败:', error)
|
||
Message.error('创建工作订单失败')
|
||
}
|
||
},
|
||
})
|
||
} else if (activeStep.value === 1 && compareMatchResult.value !== 'success') {
|
||
// 当在扫码核验页面点击开始比对时,调用后端接口获取比对结果
|
||
try {
|
||
// 这里应该调用后端的比对接口,暂时使用fetchMaterialData模拟
|
||
// 实际项目中应该替换为专门的比对接口
|
||
const materialCode = formData.inputMaterialCode?.trim()
|
||
if (!materialCode) {
|
||
Message.error('请先扫描物料编码')
|
||
return
|
||
}
|
||
|
||
// 调用后端接口获取比对结果 // todo
|
||
const res = await vmSend(materialCode);
|
||
if (res.data && res.data == materialCode) {
|
||
compareMatchResult.value === 'success';
|
||
Message.success('比对成功')
|
||
} else {
|
||
// 比对失败,提示错误
|
||
Message.error('比对失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('比对失败:', error)
|
||
Message.error('比对失败,请重试')
|
||
}
|
||
} else {
|
||
activeStep.value++
|
||
// 进入称重页面时建立WebSocket连接
|
||
if (activeStep.value === 2) {
|
||
establishWebSocket()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 处理上一步
|
||
const handlePrevious = () => {
|
||
if (activeStep.value > 1) {
|
||
activeStep.value--
|
||
// 离开称重页面时关闭WebSocket连接
|
||
if (activeStep.value !== 2) {
|
||
closeWebSocket()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 打印
|
||
const onPrint = () => {
|
||
// 跳转到标签打印页面,并传递数据
|
||
router.push({
|
||
path: '/print',
|
||
query: {
|
||
workerOrderId: workOrderResp.value.id,
|
||
}
|
||
})
|
||
}
|
||
|
||
// 返回第一步
|
||
const handleBackToFirst = () => {
|
||
// 重置所有数据
|
||
activeStep.value = 1
|
||
|
||
// 重置表单数据
|
||
formData.inputMaterialCode = ''
|
||
formData.id = ''
|
||
formData.encoding = ''
|
||
formData.materialName = ''
|
||
formData.materialSpec = ''
|
||
formData.unitWeight = 0
|
||
formData.photoUrl = ''
|
||
|
||
compareMatchResult.value = ''
|
||
|
||
// 重置称重登记页面数据
|
||
inputQuantity.value = ''
|
||
// todo
|
||
ahDeviceWeight.value = ''
|
||
calculatedWeight.value = ''
|
||
weighingCount.value = 1
|
||
// 清空称重列表
|
||
weighingList.value = []
|
||
|
||
// 重置工作订单响应数据
|
||
workOrderResp.value = {}
|
||
|
||
// 离开称重页面时关闭WebSocket连接
|
||
closeWebSocket()
|
||
|
||
// 回到第一步后自动聚焦到物料编码输入框
|
||
nextTick(() => {
|
||
if (materialCodeInput.value) {
|
||
materialCodeInput.value.focus()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 计算重量
|
||
const calculateWeight = () => {
|
||
if (inputQuantity.value && formData.unitWeight) {
|
||
const singleWeight = Number.parseFloat(formData.unitWeight.toString())
|
||
const quantity = Number.parseFloat(inputQuantity.value)
|
||
if (!isNaN(singleWeight) && !isNaN(quantity)) {
|
||
calculatedWeight.value = (singleWeight * quantity).toFixed(2)
|
||
}
|
||
} else {
|
||
calculatedWeight.value = ''
|
||
}
|
||
}
|
||
|
||
|
||
//处理确定
|
||
const handleConfirm = async () => {
|
||
// 校验输入数量是否为空
|
||
if (!inputQuantity.value || inputQuantity.value <= 0) {
|
||
Message.error('数量需大于0!')
|
||
return
|
||
}
|
||
if (!ahDeviceWeight.value || ahDeviceWeight.value.trim() === '') {
|
||
Message.error('电子秤称重结果为空!')
|
||
return
|
||
}
|
||
|
||
const data = {
|
||
calculatedWeight: calculatedWeight.value,
|
||
materialId: formData.id,
|
||
inputQuantity: inputQuantity.value,
|
||
ahDeviceWeight: ahDeviceWeight.value,
|
||
}
|
||
|
||
const res = await validateWeighing(data);
|
||
// 然后处理错误信息
|
||
if (!res || !res.code) {
|
||
return;
|
||
}
|
||
if (res.data.code !== '200') {
|
||
if (res.data.code !== '501') {
|
||
playAudio('tooMany')
|
||
}
|
||
if (res.data.code !== '502') {
|
||
playAudio('tooLess')
|
||
}
|
||
Message.error(res.data.msg || '系统异常!');
|
||
return;
|
||
}
|
||
|
||
|
||
// 立即创建一个新项的基本数据
|
||
const newItem = {
|
||
key: (weighingList.value.length + 1).toString(),
|
||
weightTime: weighingCount.value,
|
||
materialId: formData.id,
|
||
quantity: inputQuantity.value,
|
||
weight: ahDeviceWeight.value,
|
||
calculatedWeight: calculatedWeight.value,
|
||
image: '加载中...', // 先显示加载状态
|
||
}
|
||
|
||
// 先添加到列表,让用户能立即看到记录
|
||
weighingList.value.push(newItem)
|
||
|
||
// 重置输入(让用户能继续输入)
|
||
inputQuantity.value = ''
|
||
// todo
|
||
ahDeviceWeight.value = ''
|
||
calculatedWeight.value = ''
|
||
weighingCount.value = weighingList.value.length + 1
|
||
|
||
// 调用手动抓图接口
|
||
const imageUrl = await captureImage(newItem.key + '_' + newItem.materialId);
|
||
const addedItem = weighingList.value.find((item) => item.key === newItem.key)
|
||
if (addedItem) {
|
||
addedItem.image = imageUrl || '抓图失败'
|
||
// 触发视图更新
|
||
weighingList.value = [...weighingList.value]
|
||
}
|
||
Message.success('添加成功')
|
||
}
|
||
|
||
|
||
|
||
// 处理重置
|
||
const handleReset = () => {
|
||
inputQuantity.value = ''
|
||
ahDeviceWeight.value = ''
|
||
calculatedWeight.value = ''
|
||
}
|
||
|
||
// 处理删除
|
||
const handleDelete = (key) => {
|
||
// 过滤掉要删除的项
|
||
weighingList.value = weighingList.value.filter((item) => item.key !== key)
|
||
// 重新排序称重次数
|
||
weighingList.value.forEach((item, index) => {
|
||
item.weightTime = index + 1
|
||
})
|
||
// 更新当前称重次数
|
||
weighingCount.value = weighingList.value.length + 1
|
||
}
|
||
|
||
// 建立WebSocket连接
|
||
const establishWebSocket = () => {
|
||
try {
|
||
// 这里替换为实际的WebSocket服务器地址
|
||
const wsUrl = 'ws://localhost:6609/ws/scale'
|
||
ws.value = new WebSocket(wsUrl)
|
||
|
||
ws.value.onopen = () => {
|
||
Message.success('已和电子秤建立连接')
|
||
}
|
||
|
||
ws.value.onmessage = (event) => {
|
||
try {
|
||
if (event.data) {
|
||
ahDeviceWeight.value = event.data
|
||
}
|
||
} catch (error) {
|
||
console.error('WebSocket消息解析失败:', error)
|
||
}
|
||
}
|
||
|
||
ws.value.onclose = () => {
|
||
wsConnected.value = false
|
||
}
|
||
|
||
ws.value.onerror = (error) => {
|
||
console.error('WebSocket错误:', error)
|
||
wsConnected.value = false
|
||
Message.error('称重连接失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('建立WebSocket连接失败:', error)
|
||
Message.error('无法建立称重连接')
|
||
}
|
||
}
|
||
|
||
// 关闭WebSocket连接
|
||
const closeWebSocket = () => {
|
||
if (ws.value) {
|
||
ws.value.close()
|
||
ws.value = null
|
||
wsConnected.value = false
|
||
}
|
||
}
|
||
|
||
// 播放音频文件
|
||
const playAudio = (filename: string) => {
|
||
console.log(filename)
|
||
try {
|
||
// 创建音频对象并播放
|
||
const audio = new Audio(`/src/assets/wav/${filename}.wav`)
|
||
audio.play().catch(error => {
|
||
console.error('音频播放失败:', error)
|
||
})
|
||
} catch (error) {
|
||
console.error('播放音频时出错:', error)
|
||
}
|
||
}
|
||
|
||
// 组件卸载时关闭WebSocket连接
|
||
onUnmounted(() => {
|
||
closeWebSocket()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* 步骤导航 */
|
||
.step-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background-color: var(--color-bg-2);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.step-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
z-index: 1;
|
||
}
|
||
|
||
.step-circle {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background-color: #e8e8e8;
|
||
color: #666;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.step-title {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.step-line {
|
||
flex: 1;
|
||
height: 2px;
|
||
background-color: #e8e8e8;
|
||
margin: 0 20px;
|
||
}
|
||
|
||
.step-item.active .step-circle {
|
||
background-color: #1677ff;
|
||
color: white;
|
||
}
|
||
|
||
.step-item.active .step-title {
|
||
color: #1677ff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.step-item.completed .step-circle {
|
||
background-color: #52c41a;
|
||
color: white;
|
||
}
|
||
|
||
.step-item.completed .step-title {
|
||
color: #52c41a;
|
||
}
|
||
|
||
.step-item.completed ~ .step-line {
|
||
background-color: #52c41a;
|
||
}
|
||
|
||
/* 步骤内容 */
|
||
.step-content {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
/* 主内容区域 */
|
||
.main-content {
|
||
display: flex;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.left-section {
|
||
flex: 1;
|
||
margin-right: 30px;
|
||
}
|
||
|
||
.right-section {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 称重登记页面 */
|
||
.weighing-content {
|
||
display: flex;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.left-weighing-section {
|
||
width: 400px;
|
||
margin-right: 30px;
|
||
}
|
||
|
||
.right-weighing-section {
|
||
flex: 1;
|
||
}
|
||
|
||
.weighing-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.section-header h4 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
|
||
|
||
.input-placeholder {
|
||
width: 100%;
|
||
height: 32px;
|
||
background-color: #e8e8e8;
|
||
border-radius: 4px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.weighing-actions {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.table-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* 绿色背景列 */
|
||
:deep(.green-bg) {
|
||
background-color: #f6ffed;
|
||
}
|
||
|
||
/* 完成创建页面 */
|
||
.completion-content {
|
||
text-align: center;
|
||
padding: 60px 0;
|
||
}
|
||
|
||
.completion-icon {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.completion-content h2 {
|
||
margin-bottom: 20px;
|
||
color: #52c41a;
|
||
}
|
||
|
||
.completion-content p {
|
||
margin-bottom: 30px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.completion-info {
|
||
max-width: 400px;
|
||
margin: 0 auto 40px;
|
||
text-align: left;
|
||
background-color: var(--color-bg-2);
|
||
padding: 20px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.info-item {
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
}
|
||
|
||
.info-item .label {
|
||
width: 100px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.info-item .value {
|
||
flex: 1;
|
||
}
|
||
|
||
.completion-actions {
|
||
margin-top: 40px;
|
||
display: flex;
|
||
gap: 16px;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 图片占位符 */
|
||
.image-placeholder {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 150px;
|
||
background-color: #e8e8e8;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 20px;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 视频覆盖层样式(用于错误和加载状态) */
|
||
.video-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
gap: 10px;
|
||
}
|
||
|
||
.video-overlay.error {
|
||
background: rgba(255, 77, 79, 0.2);
|
||
}
|
||
|
||
/* 正方形图片 */
|
||
.square-image {
|
||
width: 200px;
|
||
height: 200px;
|
||
}
|
||
|
||
/* 样图 */
|
||
.sample-image {
|
||
object-fit: cover;
|
||
margin-bottom: 20px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* 相机区域 */
|
||
.camera-section {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.camera-section .image-placeholder {
|
||
width: calc(50% - 10px);
|
||
}
|
||
|
||
/* 表单区域 */
|
||
.form-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.form-item {
|
||
flex: 1;
|
||
margin-right: 20px;
|
||
}
|
||
|
||
.form-item.full-width {
|
||
flex: 1;
|
||
margin-right: 0;
|
||
}
|
||
|
||
/* 确保输入框宽度一致 */
|
||
:deep(.arco-input-wrapper) {
|
||
width: 100%;
|
||
height: 35px;
|
||
}
|
||
|
||
:deep(.arco-input) {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.form-item label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-size: 16px;
|
||
color: #666;
|
||
}
|
||
|
||
.form-actions {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* 信息区域 */
|
||
.info-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.info-card {
|
||
background-color: var(--color-bg-2);
|
||
border-radius: 4px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.info-card h4 {
|
||
margin: 0 0 16px 0;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
margin-bottom: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-item .label {
|
||
width: 100px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.info-item .value {
|
||
flex: 1;
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.match-result {
|
||
font-weight: 500;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.match-success {
|
||
color: #52c41a;
|
||
}
|
||
|
||
.match-failed {
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
.match-pending {
|
||
color: #d9d9d9;
|
||
}
|
||
|
||
.info-text {
|
||
margin-top: 20px;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 操作按钮 */
|
||
.action-buttons {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.next-button {
|
||
width: 120px;
|
||
height: 40px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.previous-button {
|
||
width: 120px;
|
||
height: 40px;
|
||
font-size: 14px;
|
||
margin-right: 12px;
|
||
}
|
||
</style> |