2.当人工扫码时 收到MQTT消息 弹出窗口显示该物料号历史程序号 可人工选择或自行输入新版本号 3.点击上线按钮 需要二次确认信息弹窗 弹窗 显示产品或者工件的详细信息 需要二次确认或者取消 4.二次确认弹窗 中增加 程序号 版本号+物料编码 5.版本号 和 物料编码 放在一行显示 分两个框
1039 lines
41 KiB
Vue
1039 lines
41 KiB
Vue
<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>
|