Files
wms-ui/src/views/fullClaim/FullWorkOrderDetailModal.vue
2026-04-12 23:18:33 +08:00

368 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>