Files
47-ZhonLian-MES_ManageView/src/views/EquipmentManagement/DeviceUpLineBarCodeDJ/index copy20250528.vue
XingCheng3 ec0e9a96ab 1.每次 点击手动上线时 记录 物料编码与版本号关系到数据库
2.当人工扫码时 收到MQTT消息 弹出窗口显示该物料号历史程序号 可人工选择或自行输入新版本号
3.点击上线按钮 需要二次确认信息弹窗 弹窗 显示产品或者工件的详细信息 需要二次确认或者取消
4.二次确认弹窗 中增加 程序号 版本号+物料编码
5.版本号 和 物料编码 放在一行显示 分两个框
2026-05-28 17:45:38 +08:00

1039 lines
41 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>
<div class="app-container">
<div style="text-align:center; margin-bottom: 12px;">
<div style="font-size: 24px; color: #ff9d00; font-weight: bold;">{{ opName }}: 操作平台</div>
</div>
<el-row :gutter="20">
<el-col :span="12">
<div class="workstation-panel">
<div class="workstation-title" style="text-align: right;">
<el-button :type="clickOpName === opNameList[0] ? 'success' : 'info'"
style="width: 200px;height: 60px; font-size: 16px; font-weight: bold;"
@click="selectOpNameFun(opNameList[0])">
{{ opNameList[0] }}<br v-if="clickOpName === opNameList[0]">
<div v-if="clickOpName === opNameList[0]" style="font-size: 12px;">[当前扫码位置]</div>
<div v-else> ·</div>
</el-button>
</div>
<div class="section-title">扫码上线验证信息</div>
<el-form :inline="true" label-width="50px" label-position="right" style="width: 100%;">
<el-row :gutter="10">
<el-col :span="14">
<el-form-item label="扫码值" style="width: 100%;" class="SmallLabel">
<el-input v-model="ws1Result.扫码值" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="物料编码" style="width: 100%;">
<el-input v-model="ws1Result.扫码物料编码" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="14">
<el-form-item label="工单号" style="width: 100%;">
<el-input v-model="ws1Result.工单号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="序列号" style="width: 100%;">
<el-input v-model="ws1Result.扫码序列号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="14">
<el-form-item label="错误消息" style="width: 100%;">
<el-input v-model="ws1Result.错误消息" readonly size="small" style="width: 100%;color:red;" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="程序版本" style="width: 100%;">
<el-select v-model="ws1Result.程序版本" filterable allow-create default-first-option size="small" style="width: 100%;">
<el-option v-for="item in programVersionOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="14">
<div style="height: 32px;"></div>
</el-col>
<el-col :span="10">
<el-form-item label="验证结果" style="width: 100%;">
<div v-if="ws1Result.验证结果 == 1"
style="background:#aaff33;color:#222;font-weight:bold;font-size:14px;padding:2px 8px;border-radius:4px;width:100%;text-align:center;">
验证通过</div>
<div v-else-if="ws1Result.验证结果 == 2"
style="background:#ff3b3b;color:#fff;font-weight:bold;font-size:14px;padding:2px 8px;border-radius:4px;width:100%;text-align:center;">
验证失败</div>
<div v-else-if="ws1Result.验证结果 == -1"
style="background:#ffe066;color:#222;font-weight:bold;font-size:14px;padding:2px 8px;border-radius:4px;width:100%;text-align:center;">
解析错误</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div style="display: flex; align-items: center; margin: 8px 0;">
<el-button type="primary" style="width: 200px;height: 50px; font-size: 14px;"
@click="partLoadOpName(opNameList[0])" :disabled="isAutoPartLoad">手动请求上线</el-button>
<el-button
:loading="isResetSerialLoading"
type="danger"
style="width: 200px;height: 50px; margin-left: 30px; font-size: 14px;"
@click="resetSerialNumber(opNameList[0])"
>重置序列号</el-button>
<div v-if="ws1PartLoadStatus.show" style="margin-left: 20px;">
<span v-if="ws1PartLoadStatus.success" style="color: #67C23A; font-size: 16px;">
操作成功
</span>
<span v-else style="color: #F56C6C; font-size: 16px;">
操作失败 {{ ws1PartLoadStatus.message }}
</span>
</div>
</div>
<el-divider></el-divider>
<div class="section-title">工件下线完成报工</div>
<el-form :inline="true" label-width="90px" label-position="right" style="width: 100%;">
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="物料编码" style="width: 100%;">
<el-input v-model="ws1OfflineData.物料编码" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序列号" style="width: 100%;">
<el-input v-model="ws1OfflineData.序列号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="工单号" style="width: 100%;">
<el-input v-model="ws1OfflineData.工单号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="托盘号" style="width: 100%;">
<el-input v-model="ws1OfflineData.托盘号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div style="display: flex; align-items: center; margin: 8px 0;">
<el-button type="warning" style="width: 200px;height: 50px; font-size: 14px;"
@click="handleOfflineReport(opNameList[0])">工件下线完成报工</el-button>
<div v-if="ws1OfflineStatus.show" style="margin-left: 20px;">
<span v-if="ws1OfflineStatus.success" style="color: #67C23A; font-size: 16px;">
操作成功
</span>
<span v-else style="color: #F56C6C; font-size: 16px;">
操作失败 {{ ws1OfflineStatus.message }}
</span>
</div>
</div>
<el-divider></el-divider>
<div class="section-title">读码写码清码操作</div>
<div>
<div class="status-bar">
<span>写码成功 <span class="status-dot" :class="{ 'ok': ws1Status.write }" /></span>
<span>读码成功 <span class="status-dot" :class="{ 'ok': ws1Status.read }" /></span>
<span>清码成功 <span class="status-dot" :class="{ 'ok': ws1Status.clear }" /></span>
<span>物料放置错误 <span class="status-dot" :class="{ 'err': ws1Status.materialError }" /></span>
</div>
</div>
<el-form :inline="true" label-width="90px" label-position="right" style="width: 100%;">
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="托盘物料编码" style="width: 100%;" class="ShortLabel">
<el-input v-model="ws1Tray.托盘物料编码" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="托盘号" style="width: 100%;">
<el-input v-model="ws1Tray.托盘号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="托盘工件状态" style="width: 100%;">
<el-input v-model="ws1Tray.托盘工件状态" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row style="margin-top: 8px;">
<el-col :span="24" style="display: flex;justify-content: space-evenly">
<el-button style="width: 120px;height: 45px; font-size: 14px;" type="primary"
@click="handleWriteCode(opNameList[0])">写码</el-button>
<el-button style="width: 120px;height: 45px;margin-left:10px; font-size: 14px;" type="success"
@click="handleReadCode(opNameList[0])">读码</el-button>
<el-button style="width: 120px;height: 45px;margin-left:10px; font-size: 14px;" type="warning"
@click="handleClearCode(opNameList[0])">清码</el-button>
<el-button style="width: 120px;height: 45px;margin-left:10px; font-size: 14px;" type="info"
@click="pollPlcOnce(opNameList[0])">刷新</el-button>
</el-col>
</el-row>
</el-form>
</div>
</el-col>
<el-col :span="12">
<div class="workstation-panel">
<div class="workstation-title" style="text-align: left;">
<el-button :type="clickOpName === opNameList[1] ? 'success' : 'info'"
style="width: 200px;height: 60px; font-size: 16px; font-weight: bold; margin-left: 20px;"
@click="selectOpNameFun(opNameList[1])">
{{ opNameList[1] }}<br v-if="clickOpName === opNameList[1]">
<div v-if="clickOpName === opNameList[1]" style="font-size: 12px;">[当前扫码位置]</div>
<div v-else> ·</div>
</el-button>
</div>
<div class="section-title">扫码上线验证信息</div>
<el-form :inline="true" label-width="50px" label-position="right" style="width: 100%;">
<el-row :gutter="10">
<el-col :span="14">
<el-form-item label="扫码值" style="width: 100%;" class="SmallLabel">
<el-input v-model="ws2Result.扫码值" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="物料编码" style="width: 100%;">
<el-input v-model="ws2Result.扫码物料编码" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="14">
<el-form-item label="工单号" style="width: 100%;">
<el-input v-model="ws2Result.工单号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="序列号" style="width: 100%;">
<el-input v-model="ws2Result.扫码序列号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="14">
<el-form-item label="错误消息" style="width: 100%;">
<el-input v-model="ws2Result.错误消息" readonly size="small" style="width: 100%;color:red;" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="程序版本" style="width: 100%;">
<el-select v-model="ws2Result.程序版本" filterable allow-create default-first-option size="small" style="width: 100%;">
<el-option v-for="item in programVersionOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="14">
<div style="height: 32px;"></div>
</el-col>
<el-col :span="10">
<el-form-item label="验证结果" style="width: 100%;">
<div v-if="ws2Result.验证结果 == 1"
style="background:#aaff33;color:#222;font-weight:bold;font-size:14px;padding:2px 8px;border-radius:4px;width:100%;text-align:center;">
验证通过</div>
<div v-else-if="ws2Result.验证结果 == 2"
style="background:#ff3b3b;color:#fff;font-weight:bold;font-size:14px;padding:2px 8px;border-radius:4px;width:100%;text-align:center;">
验证失败</div>
<div v-else-if="ws2Result.验证结果 == -1"
style="background:#ffe066;color:#222;font-weight:bold;font-size:14px;padding:2px 8px;border-radius:4px;width:100%;text-align:center;">
解析错误</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div style="display: flex; align-items: center; margin: 8px 0;">
<el-button type="primary" style="width: 200px;height: 50px; font-size: 14px;"
@click="partLoadOpName(opNameList[1])" :disabled="isAutoPartLoad">手动请求上线</el-button>
<el-button
:loading="isResetSerialLoading"
type="danger"
style="width: 200px;height: 50px; margin-left: 30px; font-size: 14px;"
@click="resetSerialNumber(opNameList[1])"
>重置序列号</el-button>
<div v-if="ws2PartLoadStatus.show" style="margin-left: 20px;">
<span v-if="ws2PartLoadStatus.success" style="color: #67C23A; font-size: 16px;">
操作成功
</span>
<span v-else style="color: #F56C6C; font-size: 16px;">
操作失败 {{ ws2PartLoadStatus.message }}
</span>
</div>
</div>
<el-divider></el-divider>
<div class="section-title">工件下线完成报工</div>
<el-form :inline="true" label-width="90px" label-position="right" style="width: 100%;">
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="物料编码" style="width: 100%;">
<el-input v-model="ws2OfflineData.物料编码" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="序列号" style="width: 100%;">
<el-input v-model="ws2OfflineData.序列号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="工单号" style="width: 100%;">
<el-input v-model="ws2OfflineData.工单号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="托盘号" style="width: 100%;">
<el-input v-model="ws2OfflineData.托盘号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div style="display: flex; align-items: center; margin: 8px 0;">
<el-button type="warning" style="width: 200px;height: 50px; font-size: 14px;"
@click="handleOfflineReport(opNameList[1])">工件下线完成报工</el-button>
<div v-if="ws2OfflineStatus.show" style="margin-left: 20px;">
<span v-if="ws2OfflineStatus.success" style="color: #67C23A; font-size: 16px;">
操作成功
</span>
<span v-else style="color: #F56C6C; font-size: 16px;">
操作失败 {{ ws2OfflineStatus.message }}
</span>
</div>
</div>
<el-divider></el-divider>
<div class="section-title">读码写码清码操作</div>
<div class="status-bar">
<span>写码成功 <span class="status-dot" :class="{ 'ok': ws2Status.write }" /></span>
<span>读码成功 <span class="status-dot" :class="{ 'ok': ws2Status.read }" /></span>
<span>清码成功 <span class="status-dot" :class="{ 'ok': ws2Status.clear }" /></span>
<span>物料放置错误 <span class="status-dot" :class="{ 'err': ws2Status.materialError }" /></span>
</div>
<el-form :inline="true" label-width="90px" label-position="right" style="width: 100%;">
<el-row :gutter="10">
<el-col :span="24">
<el-form-item label="托盘物料编码" style="width: 100%;" class="ShortLabel">
<el-input v-model="ws2Tray.托盘物料编码" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="托盘号" style="width: 100%;">
<el-input v-model="ws2Tray.托盘号" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="托盘工件状态" style="width: 100%;">
<el-input v-model="ws2Tray.托盘工件状态" readonly size="small" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row style="margin-top: 8px;">
<el-col :span="24" style="display: flex;justify-content: space-evenly">
<el-button style="width: 120px;height: 45px; font-size: 14px;" type="primary"
@click="handleWriteCode(opNameList[1])">写码</el-button>
<el-button style="width: 120px;height: 45px;margin-left:10px; font-size: 14px;" type="success"
@click="handleReadCode(opNameList[1])">读码</el-button>
<el-button style="width: 120px;height: 45px;margin-left:10px; font-size: 14px;" type="warning"
@click="handleClearCode(opNameList[1])">清码</el-button>
<el-button style="width: 120px;height: 45px;margin-left:10px; font-size: 14px;" type="info"
@click="pollPlcOnce(opNameList[1])">刷新</el-button>
</el-col>
</el-row>
</el-form>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import axios from 'axios'
import { mqttIp, mqttTopic, mqttInitRequestTopic, writePlcApi_BtnType, selectOpNameApi, multiReadPlcApi, partUnloadedApi } from '@/../static/config'
// ==================== 常量定义 ====================
/** 程序版本 PLC 字符串信号标识 */
const PROGRAM_VERSION_TAG_ID = '208640'
/** 程序版本下拉选项 */
const PROGRAM_VERSION_OPTIONS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
/** 轮询读取的所有 PLC 信号标识 */
const POLL_TAG_IDS = ['200004', '200005', '200006', '200007', '200380', '203240', '200440', PROGRAM_VERSION_TAG_ID]
/** 状态灯信号 → data 属性映射 */
const STATUS_TAG_MAP = {
'200004': 'materialError', // 放置错误
'200005': 'read', // 读码
'200006': 'write', // 写码
'200007': 'clear' // 清码
}
/** 托盘工件状态码 → 文本映射 */
const TRAY_STATUS_TEXT = {
'0': '毛坯',
'1': '已加工',
'2': '返修件',
'3': '空托'
}
/** 轮询间隔(毫秒) */
const POLL_INTERVAL = 1000
/** 轮询总次数10秒 = 10次 */
const POLL_TOTAL_COUNT = 120
export default {
data() {
return {
opNameType: '底架上线',
opName: '',
clickOpName: '',
opNameList: [],
mqtt: window.mqtt,
// 双工位扫码验证数据
ws1Result: { 扫码值: '', 工单号: '', 扫码物料编码: '', 扫码序列号: '', 错误消息: '', 验证结果: '', 程序版本: '' },
ws2Result: { 扫码值: '', 工单号: '', 扫码物料编码: '', 扫码序列号: '', 错误消息: '', 验证结果: '', 程序版本: '' },
programVersionOptions: PROGRAM_VERSION_OPTIONS,
// 双工位托盘信息
ws1Tray: { 托盘号: '', 托盘物料编码: '', 托盘工件状态: '' },
ws2Tray: { 托盘号: '', 托盘物料编码: '', 托盘工件状态: '' },
// 双工位状态灯
ws1Status: { clear: false, write: false, read: false, materialError: false },
ws2Status: { clear: false, write: false, read: false, materialError: false },
// 工件下线报工数据
ws1OfflineData: { 物料编码: '', 序列号: '', 工单号: '', 托盘号: '' },
ws2OfflineData: { 物料编码: '', 序列号: '', 工单号: '', 托盘号: '' },
// 报工状态
ws1OfflineStatus: { success: false, message: '', show: false },
ws2OfflineStatus: { success: false, message: '', show: false },
// 上线状态
ws1PartLoadStatus: { success: false, message: '', show: false },
ws2PartLoadStatus: { success: false, message: '', show: false },
isAutoPartLoad: false,
isResetSerialLoading: false,
// 轮询定时器(按工位分别存储,支持双工位同时轮询)
pollTimers: {},
}
},
computed: {
...mapGetters(['id', 'token'])
},
created() {
if (this.opNameType === '转台上线') {
this.opName = 'OP_C1'
this.opNameList = ['OP_C1L', 'OP_C1R']
} else if (this.opNameType === '底架上线') {
this.opName = 'OP_C2'
this.opNameList = ['OP_C2L', 'OP_C2R']
}
this.selectOpNameFun(this.opNameList[0])
this.initMqtt()
this.readAllPlcStatus()
},
beforeDestroy() {
this.stopAllPolling()
this.mqttClient.end()
},
methods: {
// ==================== 工位选择 ====================
selectOpNameFun(opName) {
axios.post(selectOpNameApi, { DeviceCode: opName }, { timeout: 5000 })
.then(response => {
if (response.data.code === 0) {
this.clickOpName = opName
this.$message.success('工位选择成功!')
} else {
this.$message.error(response.data.msg)
}
})
.catch(error => {
this.$message.error('选择工位失败' + error.message)
})
},
// ==================== PLC 数据读取与轮询 ====================
/** 读取单个PLC信号值 */
async getPLCValue(opName, tagID) {
try {
const res = await axios.post(multiReadPlcApi, {
tagTypeCodeIDs: [tagID],
OPName: opName
})
if (res.data && res.data.code === 0 && Array.isArray(res.data.ReadRes_SignalValues) && res.data.ReadRes_SignalValues.length > 0) {
return res.data.ReadRes_SignalValues[0].tagValue
}
return null
} catch (error) {
console.error('获取PLC值失败:', error)
return null
}
},
/** 写入单个PLC信号值 */
async writePLCValue(opName, tagID, tagValue) {
return axios.post(writePlcApi_BtnType, {
tagTypeCodeID: tagID,
OPName: opName,
tagValue
}, { timeout: 5000 })
},
/** 根据工位名获取对应的状态对象和托盘对象 */
getWorkstationData(opName) {
const isWs1 = opName === this.opNameList[0]
return {
status: isWs1 ? this.ws1Status : this.ws2Status,
tray: isWs1 ? this.ws1Tray : this.ws2Tray,
result: isWs1 ? this.ws1Result : this.ws2Result
}
},
/** 解析PLC响应数据统一更新对应工位的状态灯与托盘信息 */
applyPlcData(opName, signalValues) {
const valueMap = {}
signalValues.forEach(item => {
valueMap[item.tagTypeCodeID] = item.tagValue
})
const { status, tray, result } = this.getWorkstationData(opName)
// 更新四个状态灯
Object.entries(STATUS_TAG_MAP).forEach(([tagId, key]) => {
if (valueMap[tagId] !== undefined) {
status[key] = valueMap[tagId] === 'True'
}
})
// 更新托盘信息
if (valueMap['200380'] !== undefined) tray.托盘号 = valueMap['200380']
if (valueMap['203240'] !== undefined) this.convertMaterialCode(opName, valueMap['203240'])
if (valueMap['200440'] !== undefined) tray.托盘工件状态 = TRAY_STATUS_TEXT[valueMap['200440']] || ''
if (valueMap[PROGRAM_VERSION_TAG_ID] !== undefined) result.程序版本 = valueMap[PROGRAM_VERSION_TAG_ID] || ''
},
/** 将PLC读取到的物料号通过存储过程转换为物料编码 */
convertMaterialCode(opName, materialNo) {
if (!materialNo) return
const param = [['物料号', materialNo]]
const Data = this.CreateData('11', 'ZL_基础表_物料信息表_上线获取物料编码', param)
this.ExecDatabase(Data).then(response => {
const { tray } = this.getWorkstationData(opName)
if (response.data && response.data.length > 0 && response.data[0]['物料编码']) {
tray.托盘物料编码 = response.data[0]['物料编码']
} else {
tray.托盘物料编码 = materialNo // 转换失败时使用原始物料号
}
}).catch(err => {
console.error('物料编码转换失败:', err)
const { tray } = this.getWorkstationData(opName)
tray.托盘物料编码 = materialNo
})
},
/** 主动读取一次指定工位的全部PLC数据状态灯 + 托盘信息) */
pollPlcOnce(opName) {
axios.post(multiReadPlcApi, {
tagTypeCodeIDs: POLL_TAG_IDS,
OPName: opName
}).then(res => {
if (res.data && res.data.code === 0 && Array.isArray(res.data.ReadRes_SignalValues)) {
this.applyPlcData(opName, res.data.ReadRes_SignalValues)
}
}).catch(err => {
console.error('读取PLC数据失败:', err)
})
},
/** 启动轮询每秒读取一次PLC数据共10秒 */
startPollingPlcStatus(opName) {
// 如果该工位已有轮询,先停止
this.stopPolling(opName)
let count = 0
// 立即读取第一次
this.pollPlcOnce(opName)
count++
const timer = setInterval(() => {
count++
if (count > POLL_TOTAL_COUNT) {
this.stopPolling(opName)
return
}
this.pollPlcOnce(opName)
}, POLL_INTERVAL)
this.$set(this.pollTimers, opName, timer)
},
/** 停止指定工位的轮询 */
stopPolling(opName) {
if (this.pollTimers[opName]) {
clearInterval(this.pollTimers[opName])
this.$delete(this.pollTimers, opName)
}
},
/** 停止所有工位的轮询 */
stopAllPolling() {
Object.keys(this.pollTimers).forEach(key => {
clearInterval(this.pollTimers[key])
})
this.pollTimers = {}
},
/** 页面初始化时读取所有工位的PLC状态 */
readAllPlcStatus() {
this.opNameList.forEach(opName => {
this.pollPlcOnce(opName)
})
},
// ==================== MQTT ====================
initMqtt() {
const scope = this
const connection = {
host: mqttIp,
port: 8083,
endpoint: '/mqtt',
clean: true,
connectTimeout: 4000,
reconnectPeriod: 4000,
clientId: 'mqttjs_3be2c321' + new Date().getTime(),
username: 'WEB1',
password: ''
}
const subscription = {
topic: mqttTopic,
qos: 0
}
const { host, port, endpoint, ...options } = connection
const connectUrl = `ws://${host}:${port}${endpoint}`
try {
this.mqttClient = mqtt.connect(connectUrl, options)
} catch (error) {
console.log('MQTT connect error', error)
}
this.mqttClient.on('connect', () => {
this.$message.success('加载完成连接MQTT服务器成功')
console.log('MQTT Connection succeeded!')
const { topic, qos } = subscription
// 按工位分别订阅主题(主题格式:原主题/工位号)
this.opNameList.forEach(opName => {
this.mqttClient.subscribe(`${topic}/${opName}`, { qos }, (error) => {
if (error) console.log(`Subscribe ${topic}/${opName} error`, error)
})
this.mqttClient.subscribe(`UnloadingPartInfo/${opName}`, { qos: 0 }, (error) => {
if (error) console.log(`Subscribe UnloadingPartInfo/${opName} error`, error)
})
})
// 订阅完成后,发送初始化请求
this.publishInitRequest()
})
this.mqttClient.on('error', error => {
console.log('MQTT Connection failed', error)
try {
scope.$message.error('MQTT服务器连接失败')
if (scope.IsAllowConnect) {
scope.$message.warning('重新连接MQTT服务器')
console.log('重新连接MQTT服务器')
setTimeout(function () {
scope.initMqtt()
}, 5000)
}
} catch (exception) {
const error = exception.toString()
scope.$message.error('MQTT服务器出现错误,关闭连接' + error)
}
})
// 接收消息
this.mqttClient.on('message', (topic, message) => {
try {
scope.websocketonmessage(topic, { data: message.toString() })
} catch (exception) {
console.log(exception.toString())
}
})
},
/** 发送MQTT初始化请求后端收到后通过已有主题推送当前状态数据 */
publishInitRequest() {
const message = `WebInitRequest|${this.opName}`
this.mqttClient.publish(mqttInitRequestTopic, message, { qos: 0 }, (error) => {
if (error) {
console.error('发送初始化请求失败:', error)
} else {
console.log('已发送初始化请求:', message)
}
})
},
/** 处理MQTT消息仅处理工件下线信息和扫码验证结果 */
websocketonmessage(topic, msg) {
console.log('websocketonmessage', topic, msg)
// 处理工件下线信息 - JSON格式
if (topic.startsWith('UnloadingPartInfo')) {
try {
const offlineData = JSON.parse(msg.data)
console.log('收到工件下线信息JSON', offlineData)
// 根据工位号分配数据
let targetOfflineData = null
if (offlineData.OPCode === this.opNameList[0]) {
targetOfflineData = this.ws1OfflineData
} else if (offlineData.OPCode === this.opNameList[1]) {
targetOfflineData = this.ws2OfflineData
} else {
return
}
targetOfflineData.托盘号 = offlineData.PalletNo ? offlineData.PalletNo.toString() : ''
targetOfflineData.序列号 = offlineData.SerialNum || ''
targetOfflineData.工单号 = offlineData.WorkOrderCode || ''
targetOfflineData.物料编码 = offlineData.PartCode || ''
console.log('工件下线信息已更新:', targetOfflineData)
} catch (error) {
console.error('解析工件下线信息失败:', error, msg.data)
}
return
}
// 处理管道分隔的消息(扫码验证结果等)
const arrays_signal = msg.data.split('|')
const interFaceType = arrays_signal[0]
console.log('mqttaaa', topic, arrays_signal)
switch (interFaceType) {
case 'VerifyResult': {
if (arrays_signal.length < 11) {
this.$message.warning('收到扫码结果,但是数据异常,不予处理!')
console.log(msg)
return
}
const pos = arrays_signal[2]
let targetResult = null
if (pos === this.opNameList[0]) {
targetResult = this.ws1Result
} else if (pos === this.opNameList[1]) {
targetResult = this.ws2Result
} else {
return
}
targetResult.扫码工位 = arrays_signal[1]
targetResult.选择位置号 = arrays_signal[2]
targetResult.验证结果 = arrays_signal[3]
targetResult.错误消息 = arrays_signal[4]
targetResult.扫码值 = arrays_signal[5]
targetResult.扫码物料编码 = arrays_signal[6]
targetResult.扫码序列号 = arrays_signal[7]
targetResult.工单号 = arrays_signal[8]
targetResult.工单物料编号 = arrays_signal[9]
targetResult.工单序列号 = arrays_signal[10]
if (targetResult.验证结果 == 1) {
this.$message.success('收到校验结果,校验通过!' + targetResult.错误消息)
if (this.isAutoPartLoad && this.clickOpName === pos) {
this.partLoadOpName(targetResult.扫码工位)
}
} else if (targetResult.验证结果 == 2) {
this.$message.error('收到校验结果,校验失败!' + targetResult.错误消息)
} else {
this.$message.warning('校验未成功,服务未开启!' + targetResult.错误消息)
}
break
}
}
},
// ==================== 重置序列号 ====================
async resetSerialNumber(opName) {
const result = opName === this.opNameList[0] ? this.ws1Result : this.ws2Result
// if (!result.扫码值) {
// this.$message.warning('请先扫码获取验证信息!')
// return
// }
const param = []
param[0] = ['工位号', opName]
param[1] = ['扫码值', result.扫码值 || '']
param[2] = ['物料编号', result.扫码物料编码 || '']
param[3] = ['工单号', result.工单号 || '']
param[4] = ['序列号', result.扫码序列号 || '']
const Data = this.CreateData('12', 'Event_序列号信息_重置', param)
this.isResetSerialLoading = true
console.log(param)
this.ExecDatabase(Data).then(response => {
if (response.data && response.data[0] && response.data[0].result === '1') {
this.$message.success('重置序列号成功!')
} else {
this.$message.error('重置序列号失败!')
}
}).catch(error => {
this.$message.error('重置序列号失败:' + error.message)
}).finally(() => {
this.isResetSerialLoading = false
})
},
// ==================== 手动上线 ====================
async partLoadOpName(opName) {
const result = opName === this.opNameList[0] ? this.ws1Result : this.ws2Result
if (result.验证结果 != 1) {
let message = '请先进行扫码验证!'
if (result.验证结果 == 2) message = '验证失败,无法请求上线!'
else if (result.验证结果 == -1) message = '解析错误,无法请求上线!'
this.$message.warning(message)
return
}
const programVersion = result.程序版本 ? result.程序版本.toString() : ''
if (!programVersion) {
this.$message.warning('请先选择或输入程序版本!')
return
}
try {
await this.$confirm('确定要执行手动请求上线操作吗?', '确认操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch (error) {
return
}
const partLoadStatus = opName === this.opNameList[0] ? this.ws1PartLoadStatus : this.ws2PartLoadStatus
const plcValue = await this.getPLCValue(opName, '205015')
if (plcValue === 'True') {
partLoadStatus.success = false
partLoadStatus.message = '当前已请求上线状态!'
partLoadStatus.show = true
this.$message.warning('申请上线失败,当前已请求上线状态!')
return
}
this.writePLCValue(opName, PROGRAM_VERSION_TAG_ID, programVersion)
.then(response => {
if (response.data.code != 0) {
return Promise.reject(new Error(response.data.msg || '程序版本写入失败'))
}
return this.writePLCValue(opName, '205015', 'true')
})
.then(response => {
if (response.data.code == 0) {
partLoadStatus.success = true
partLoadStatus.message = ''
partLoadStatus.show = true
this.$message.success('申请上线成功!')
} else {
partLoadStatus.success = false
partLoadStatus.message = response.data.msg || '申请失败'
partLoadStatus.show = true
this.$message.error(response.data.msg)
}
})
.catch(error => {
partLoadStatus.success = false
partLoadStatus.message = error.message
partLoadStatus.show = true
this.$message.error('申请上线失败' + error.message)
})
},
// ==================== 读码 / 写码 / 清码 ====================
/** 写码 - 发送写码请求并启动10秒轮询 */
handleWriteCode(opName) {
axios.post(writePlcApi_BtnType, {
tagTypeCodeID: '205005',
OPName: opName,
tagValue: 'true'
}).then(() => {
this.$message.success('写码请求已发送')
this.startPollingPlcStatus(opName)
})
},
/** 读码 - 发送读码请求并启动10秒轮询 */
handleReadCode(opName) {
axios.post(writePlcApi_BtnType, {
tagTypeCodeID: '205006',
OPName: opName,
tagValue: 'true'
}).then(() => {
this.$message.success('读码请求已发送')
this.startPollingPlcStatus(opName)
})
},
/** 清码 - 发送清码请求并启动10秒轮询 */
handleClearCode(opName) {
axios.post(writePlcApi_BtnType, {
tagTypeCodeID: '205007',
OPName: opName,
tagValue: 'true'
}).then(() => {
this.$message.success('清码请求已发送')
this.startPollingPlcStatus(opName)
})
},
// ==================== 工件下线报工 ====================
async handleOfflineReport(opName) {
try {
await this.$confirm('确定要执行工件下线完成报工操作吗?', '确认操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch (error) {
return
}
const offlineStatus = opName === this.opNameList[0] ? this.ws1OfflineStatus : this.ws2OfflineStatus
axios.post(partUnloadedApi, {
OPCode: opName
}, { timeout: 5000 })
.then(response => {
if (response.data.code == 0) {
offlineStatus.success = true
offlineStatus.message = ''
offlineStatus.show = true
this.$message.success('工件下线完成报工成功!')
setTimeout(() => {
offlineStatus.show = false
}, 3000)
} else {
offlineStatus.success = false
offlineStatus.message = response.data.msg || '报工失败'
offlineStatus.show = true
this.$message.error('工件下线完成报工失败:' + response.data.msg)
}
})
.catch(error => {
offlineStatus.success = false
offlineStatus.message = error.message
offlineStatus.show = true
this.$message.error('工件下线完成报工失败:' + error.message)
})
},
}
}
</script>
<style lang="scss" scoped>
.app-container {
background: #fff;
}
.workstation-panel {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px 10px 12px 10px;
margin-bottom: 10px;
min-height: 400px;
}
.workstation-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
text-align: center;
}
.status-bar {
margin-bottom: 15px;
font-size: 16px;
margin: 0 auto 15px;
text-align: center;
&>span {
margin-right: 30px;
font-weight: 500;
&:last-child {
margin-right: 0;
}
}
}
.status-dot {
display: inline-block;
width: 30px;
height: 20px;
border-radius: 3px;
margin-left: 6px;
background: #e0e0e0;
vertical-align: middle;
}
.status-dot.ok {
background: #70b603;
}
.status-dot.err {
background: #ff3b3b;
}
.section-title {
font-size: 14px;
color: #ff9d00;
font-weight: bold;
margin: 10px 0 6px 0;
}
.el-form-item {
margin-bottom: 0px;
}
::v-deep .el-form-item__label {
width: 25% !important;
}
.ShortLabel {
::v-deep .el-form-item__label {
width: 15% !important;
}
::v-deep .el-form-item__content {
width: 85% !important;
}
}
.SmallLabel {
::v-deep .el-form-item__label {
width: 12% !important;
}
::v-deep .el-form-item__content {
width: 88% !important;
}
}
::v-deep .el-form-item__content {
width: 75% !important;
}
::v-deep .el-button {
span {
font-size: inherit !important;
}
}
</style>