368 lines
8.5 KiB
Vue
368 lines
8.5 KiB
Vue
|
|
<template>
|
|||
|
|
<a-modal
|
|||
|
|
v-model:visible="visible"
|
|||
|
|
title="新增原材料详情"
|
|||
|
|
:mask-closable="false"
|
|||
|
|
:esc-to-close="false"
|
|||
|
|
:width="1200"
|
|||
|
|
:style="{ height: '80vh' }"
|
|||
|
|
draggable
|
|||
|
|
@before-ok="save"
|
|||
|
|
@close="reset"
|
|||
|
|
>
|
|||
|
|
<div class="detail-container">
|
|||
|
|
<div class="left-section">
|
|||
|
|
<div class="weight-input">
|
|||
|
|
<label>称重重量(g)</label>
|
|||
|
|
<a-input v-model="weightValue" placeholder="等待电子秤数据..." disabled />
|
|||
|
|
</div>
|
|||
|
|
<div class="camera-section">
|
|||
|
|
<div class="image-container">
|
|||
|
|
<img
|
|||
|
|
:src="imgData.imgUrl"
|
|||
|
|
alt="宇视摄像头实时画面"
|
|||
|
|
style="width: 100%; height: 100%; object-fit: cover; border-radius: 4px;"
|
|||
|
|
/>
|
|||
|
|
<div v-if="cameraStatus === 'error'" class="video-overlay error">
|
|||
|
|
<icon-close-circle-fill style="color: #ff4d4f; font-size: 24px;" />
|
|||
|
|
<span>连接异常</span>
|
|||
|
|
<a-button size="small" type="primary" @click="enterCamera">重试</a-button>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="cameraStatus === 'entering'" class="video-overlay">
|
|||
|
|
<a-spin />
|
|||
|
|
<span style="margin-left: 8px;">加载中...</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="confirm-button">
|
|||
|
|
<a-button type="primary" @click="handleConfirm">确定</a-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="right-section">
|
|||
|
|
<div class="detail-list">
|
|||
|
|
<a-table :columns="detailColumns" :data="detailList" :pagination="false" bordered>
|
|||
|
|
<template #imgUrl="{ record }">
|
|||
|
|
<a-image width="80" height="60" :src="record.imgUrl" />
|
|||
|
|
</template>
|
|||
|
|
<template #action="{ record }">
|
|||
|
|
<a-button type="text" status="danger" @click="handleDeleteDetail(record)">
|
|||
|
|
删除
|
|||
|
|
</a-button>
|
|||
|
|
</template>
|
|||
|
|
</a-table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</a-modal>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
|
|||
|
|
import { Message } from '@arco-design/web-vue'
|
|||
|
|
import { getCaptureImage, getEnterWeighPage, getLeaveWeighPage } from '@/apis/weightManage/ys'
|
|||
|
|
import { weighAHStart, weighAHStop } from '@/apis/weightManage/weightManage'
|
|||
|
|
import { saveFullWorkOrderDetail } from '@/apis/fullWorkOrder/fullWorkOrder'
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
(e: 'save-success'): void
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const visible = ref(false)
|
|||
|
|
const weightValue = ref('')
|
|||
|
|
const cameraStatus = ref<'idle' | 'entering' | 'entered' | 'error'>('idle')
|
|||
|
|
const fullWorkOrder = ref('')
|
|||
|
|
|
|||
|
|
const imgData = reactive({
|
|||
|
|
imgUrl: 'http://localhost:6609/file/ys/carousel.jpg',
|
|||
|
|
baseUrl: 'http://localhost:6609/file/ys/carousel.jpg'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
let imageRefreshTimer: any = null
|
|||
|
|
|
|||
|
|
const detailList = ref<any[]>([])
|
|||
|
|
|
|||
|
|
const detailColumns = [
|
|||
|
|
{ title: '称重重量(g)', dataIndex: 'weight', key: 'weight' },
|
|||
|
|
{ title: '截图', dataIndex: 'imgUrl', key: 'imgUrl', slotName: 'imgUrl' },
|
|||
|
|
{ title: '操作', dataIndex: 'action', key: 'action', slotName: 'action', width: 80 }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// WebSocket连接
|
|||
|
|
const ws = ref<WebSocket | null>(null)
|
|||
|
|
const wsConnected = ref(false)
|
|||
|
|
|
|||
|
|
// 建立WebSocket连接(电子称)
|
|||
|
|
const establishWebSocket = () => {
|
|||
|
|
try {
|
|||
|
|
const wsUrl = 'ws://localhost:6609/ws/scale'
|
|||
|
|
ws.value = new WebSocket(wsUrl)
|
|||
|
|
|
|||
|
|
ws.value.onopen = () => {
|
|||
|
|
wsConnected.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ws.value.onmessage = (event) => {
|
|||
|
|
try {
|
|||
|
|
if (event.data) {
|
|||
|
|
weightValue.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 connectScale = async () => {
|
|||
|
|
try {
|
|||
|
|
await weighAHStart()
|
|||
|
|
establishWebSocket()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('连接电子称失败:', error)
|
|||
|
|
Message.error('与电子称的连接建立失败')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 断开电子称
|
|||
|
|
const disconnectScale = async () => {
|
|||
|
|
try {
|
|||
|
|
await weighAHStop()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('断开电子称失败:', error)
|
|||
|
|
}
|
|||
|
|
closeWebSocket()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const reset = () => {
|
|||
|
|
weightValue.value = ''
|
|||
|
|
detailList.value = []
|
|||
|
|
fullWorkOrder.value = ''
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const enterCamera = async () => {
|
|||
|
|
cameraStatus.value = 'entering'
|
|||
|
|
try {
|
|||
|
|
await getEnterWeighPage()
|
|||
|
|
cameraStatus.value = 'entered'
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('进入摄像头页面失败:', error)
|
|||
|
|
cameraStatus.value = 'error'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const leaveCamera = async () => {
|
|||
|
|
try {
|
|||
|
|
await getLeaveWeighPage()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('离开摄像头页面失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const onAdd = async (id: string) => {
|
|||
|
|
reset()
|
|||
|
|
fullWorkOrder.value = id
|
|||
|
|
visible.value = true
|
|||
|
|
// 独立连接电子称和摄像头
|
|||
|
|
await Promise.all([
|
|||
|
|
enterCamera(),
|
|||
|
|
connectScale()
|
|||
|
|
])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleConfirm = async () => {
|
|||
|
|
if (!weightValue.value || weightValue.value.trim() === '') {
|
|||
|
|
Message.error('电子秤称重结果为空!')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const data = {
|
|||
|
|
type: 2,
|
|||
|
|
}
|
|||
|
|
// todo
|
|||
|
|
const response = await getCaptureImage(data)
|
|||
|
|
// const response = {
|
|||
|
|
// data: 'http://localhost:6609/file/ys/workOrder/fullOrder_1774322914630.jpg',
|
|||
|
|
// }
|
|||
|
|
if (response && response.data) {
|
|||
|
|
const newItem = {
|
|||
|
|
key: Date.now().toString(),
|
|||
|
|
weight: weightValue.value,
|
|||
|
|
imgUrl: response.data
|
|||
|
|
}
|
|||
|
|
detailList.value.push(newItem)
|
|||
|
|
weightValue.value = ''
|
|||
|
|
Message.success('添加成功')
|
|||
|
|
} else {
|
|||
|
|
Message.error('抓图失败')
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('抓图失败:', error)
|
|||
|
|
Message.error('抓图失败')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleDeleteDetail = (record: any) => {
|
|||
|
|
detailList.value = detailList.value.filter(item => item.key !== record.key)
|
|||
|
|
Message.success('删除成功')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const save = async () => {
|
|||
|
|
if (detailList.value.length === 0) {
|
|||
|
|
Message.error('请至少添加一条详情记录')
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const data = detailList.value.map(item => ({
|
|||
|
|
weight: item.weight,
|
|||
|
|
imgUrl: item.imgUrl,
|
|||
|
|
fullWorkOrderId: fullWorkOrder.value,
|
|||
|
|
}))
|
|||
|
|
|
|||
|
|
|
|||
|
|
const res = await saveFullWorkOrderDetail(data)
|
|||
|
|
if (res.code === '0') {
|
|||
|
|
Message.success('保存成功')
|
|||
|
|
emit('save-success')
|
|||
|
|
return true
|
|||
|
|
} else {
|
|||
|
|
Message.error(res.msg || '保存失败')
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('保存失败:', error)
|
|||
|
|
Message.error('保存失败')
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
if (imageRefreshTimer) {
|
|||
|
|
clearInterval(imageRefreshTimer)
|
|||
|
|
imageRefreshTimer = null
|
|||
|
|
}
|
|||
|
|
leaveCamera()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
watch(visible, async (newVal) => {
|
|||
|
|
if (newVal) {
|
|||
|
|
imageRefreshTimer = setInterval(() => {
|
|||
|
|
imgData.imgUrl = `${imgData.baseUrl}?t=${Date.now()}`
|
|||
|
|
}, 1500)
|
|||
|
|
} else {
|
|||
|
|
// 独立断开电子称和摄像头
|
|||
|
|
await Promise.all([
|
|||
|
|
leaveCamera(),
|
|||
|
|
disconnectScale()
|
|||
|
|
])
|
|||
|
|
if (imageRefreshTimer) {
|
|||
|
|
clearInterval(imageRefreshTimer)
|
|||
|
|
imageRefreshTimer = null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
defineExpose({ onAdd })
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.detail-container {
|
|||
|
|
display: flex;
|
|||
|
|
height: 100%;
|
|||
|
|
gap: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-section {
|
|||
|
|
width: 45%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.weight-input {
|
|||
|
|
label {
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.camera-section {
|
|||
|
|
flex: 1;
|
|||
|
|
min-height: 300px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.image-container {
|
|||
|
|
position: relative;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
border: 1px solid #e8e8e8;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.confirm-button {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-section {
|
|||
|
|
width: 55%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-list {
|
|||
|
|
flex: 1;
|
|||
|
|
overflow: auto;
|
|||
|
|
}
|
|||
|
|
</style>
|