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> |