This commit is contained in:
zc
2026-04-12 23:18:33 +08:00
parent 9927382054
commit fce1099b38
5 changed files with 487 additions and 5 deletions

View File

@@ -45,3 +45,13 @@ export function deleteFullWorkOrder(id: string) {
export function exportFullWorkOrder(query: FullWorkOrderQuery) {
return http.download(`${BASE_URL}/export`, query)
}
/** @desc 保存原材料详情 */
export function saveFullWorkOrderDetail(data: any) {
return http.post(`${BASE_URL}/detail`, data)
}
/** @desc 获取原材料详情列表 */
export function getFullWorkOrderDetailList(id: string) {
return http.get(`${BASE_URL}/infos/${id}`)
}

View File

@@ -0,0 +1,69 @@
<template>
<a-modal
v-model:visible="visible"
title="原材料详情"
:width="800"
:footer="false"
@cancel="handleClose"
>
<a-table
:columns="columns"
:data="detailList"
:loading="loading"
:pagination="false"
bordered
>
<template #imgUrl="{ record }">
<a-image
width="80"
height="60"
:src="record.imgUrl"
fit="cover"
/>
</template>
</a-table>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getFullWorkOrderDetailList } from '@/apis/fullWorkOrder/fullWorkOrder'
const visible = ref(false)
const loading = ref(false)
const detailList = ref<any[]>([])
const columns = [
{ title: '称重重量(g)', dataIndex: 'weight', key: 'weight' },
{ title: '截图', dataIndex: 'imgUrl', key: 'imgUrl', slotName: 'imgUrl' }
]
const onOpen = async (id: string) => {
visible.value = true
loading.value = true
try {
const res = await getFullWorkOrderDetailList(id)
if (res.code === '0') {
detailList.value = res.data || []
} else {
Message.error(res.msg || '获取详情失败')
}
} catch (error) {
console.error('获取详情失败:', error)
Message.error('获取详情失败')
} finally {
loading.value = false
}
}
const handleClose = () => {
visible.value = false
detailList.value = []
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,368 @@
<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>

View File

@@ -50,6 +50,16 @@
<template #action="{ record }">
<a-space>
<a-link
@click="onAddDetail(record.id)"
>
新增
</a-link>
<a-link
@click="onViewDetail(record)"
>
详情
</a-link>
<a-link
v-permission="['fullWorkOrder:fullWorkOrder:delete']"
status="danger"
@@ -64,11 +74,15 @@
</GiTable>
<FullWorkOrderAddModal ref="FullWorkOrderAddModalRef" @save-success="search" />
<FullWorkOrderDetailModal ref="FullWorkOrderDetailModalRef" @save-success="search" />
<FullWorkOrderDetailListModal ref="FullWorkOrderDetailListModalRef" />
</div>
</template>
<script setup lang="ts">
import FullWorkOrderAddModal from './FullWorkOrderAddModal.vue'
import FullWorkOrderDetailModal from './FullWorkOrderDetailModal.vue'
import FullWorkOrderDetailListModal from './FullWorkOrderDetailListModal.vue'
import { type FullWorkOrderResp, type FullWorkOrderQuery, deleteFullWorkOrder, exportFullWorkOrder, listFullWorkOrder } from '@/apis/fullWorkOrder/fullWorkOrder'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks'
@@ -138,11 +152,24 @@ const onExport = () => {
}
const FullWorkOrderAddModalRef = ref<InstanceType<typeof FullWorkOrderAddModal>>()
const FullWorkOrderDetailModalRef = ref<InstanceType<typeof FullWorkOrderDetailModal>>()
const FullWorkOrderDetailListModalRef = ref<InstanceType<typeof FullWorkOrderDetailListModal>>()
// 新增
const onAdd = () => {
FullWorkOrderAddModalRef.value?.onAdd()
}
// 新增原材料详情
const onAddDetail = (id: string) => {
FullWorkOrderDetailModalRef.value?.onAdd(id)
}
// 查看原材料详情
const onViewDetail = (record: FullWorkOrderResp) => {
FullWorkOrderDetailListModalRef.value?.onOpen(record.id)
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss"></style>

View File

@@ -528,7 +528,7 @@ const workOrderResp = ref<WorkOrderResp>({
const inputQuantity = ref()
const calculateNumber = ref()
// todo
const ahDeviceWeight = ref('6')
const ahDeviceWeight = ref()
const calculatedWeight = ref('')
const weighingCount = ref(1)
@@ -759,9 +759,17 @@ const printLabel = async (labelData: any) => {
setTimeout(() => {
printWindow.focus()
printWindow.print()
printWindow.close()
}, 500)
}
// 监听打印完成事件
printWindow.onafterprint = () => {
printWindow.close()
Message.success('打印成功,正在初始化称重管理页面...')
// 重新加载页面以初始化称重管理
window.location.reload()
}
}
}
@@ -927,7 +935,7 @@ const handleBackToFirst = () => {
// 重置称重登记页面数据
inputQuantity.value = ''
// todo
ahDeviceWeight.value = '6'
ahDeviceWeight.value = ''
calculatedWeight.value = ''
weighingCount.value = 1
// 清空称重列表
@@ -1014,7 +1022,7 @@ const handleConfirm = async () => {
// 重置输入(让用户能继续输入)
inputQuantity.value = ''
// todo
ahDeviceWeight.value = '6'
ahDeviceWeight.value = ''
calculatedWeight.value = ''
weighingCount.value = weighingList.value.length + 1