| | |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="通讯协议" prop="protocolType"> |
| | | <el-select v-model="deviceForm.protocolType" placeholder="选择通讯协议" style="width: 100%;"> |
| | | <el-select |
| | | v-model="deviceForm.protocolType" |
| | | placeholder="选择通讯协议" |
| | | style="width: 100%;" |
| | | @change="handleProtocolTypeChange" |
| | | > |
| | | <el-option label="S7 Communication" value="S7 Communication" /> |
| | | <el-option label="Modbus TCP" value="Modbus TCP" /> |
| | | <el-option label="OPC UA" value="OPC UA" /> |
| | | <el-option label="EtherNet/IP" value="EtherNet/IP" /> |
| | | <el-option label="Profinet" value="Profinet" /> |
| | | <el-option label="其他" value="其他" /> |
| | | </el-select> |
| | | <span class="form-tip">S7系列PLC通常使用S7 Communication协议</span> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="超时时间(秒)" prop="timeout"> |
| | |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 设备逻辑配置 --> |
| | | <el-card class="form-section" shadow="never" style="margin-top: 20px;" v-if="deviceForm.deviceType"> |
| | | <template #header> |
| | | <span class="section-title">设备逻辑配置</span> |
| | | <span class="form-tip">根据设备类型配置特定的业务逻辑参数</span> |
| | | </template> |
| | | |
| | | <!-- 上大车设备逻辑配置 --> |
| | | <div v-if="deviceForm.deviceType === '上大车'"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="车辆容量"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.vehicleCapacity" |
| | | :min="1" |
| | | :max="10000" |
| | | :step="100" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">车辆最大容量</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="玻璃间隔(ms)"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.glassIntervalMs" |
| | | :min="100" |
| | | :max="10000" |
| | | :step="100" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">玻璃上料间隔时间(毫秒)</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="自动上料"> |
| | | <el-switch v-model="deviceLogicParams.autoFeed" /> |
| | | <span class="form-tip">是否自动触发上料请求</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="最大重试次数"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.maxRetryCount" |
| | | :min="0" |
| | | :max="10" |
| | | :step="1" |
| | | style="width: 100%;" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="位置映射"> |
| | | <div class="position-mapping"> |
| | | <div |
| | | v-for="(value, key, index) in deviceLogicParams.positionMapping" |
| | | :key="index" |
| | | class="mapping-item" |
| | | > |
| | | <el-input |
| | | v-model="mappingKeys[index]" |
| | | placeholder="位置代码" |
| | | size="small" |
| | | style="width: 150px; margin-right: 10px;" |
| | | @input="updatePositionMapping(index, $event, value)" |
| | | /> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.positionMapping[mappingKeys[index] || key]" |
| | | :min="0" |
| | | :max="100" |
| | | size="small" |
| | | style="width: 120px; margin-right: 10px;" |
| | | /> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | @click="removePositionMapping(key)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </div> |
| | | <el-button type="primary" size="small" @click="addPositionMapping"> |
| | | 添加位置映射 |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </div> |
| | | |
| | | <!-- 大理片设备逻辑配置 --> |
| | | <div v-if="deviceForm.deviceType === '大理片'"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="玻璃尺寸"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.glassSize" |
| | | :min="100" |
| | | :max="5000" |
| | | :step="100" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">玻璃尺寸(mm)</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="处理时间(ms)"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.processingTime" |
| | | :min="1000" |
| | | :max="60000" |
| | | :step="1000" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">玻璃处理时间(毫秒)</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="自动处理"> |
| | | <el-switch v-model="deviceLogicParams.autoProcess" /> |
| | | <span class="form-tip">是否自动触发处理请求</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="最大重试次数"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.maxRetryCount" |
| | | :min="0" |
| | | :max="10" |
| | | :step="1" |
| | | style="width: 100%;" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 玻璃存储设备逻辑配置 --> |
| | | <div v-if="deviceForm.deviceType === '玻璃存储'"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="存储容量"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.storageCapacity" |
| | | :min="1" |
| | | :max="1000" |
| | | :step="1" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">最大存储数量</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="取货模式"> |
| | | <el-select v-model="deviceLogicParams.retrievalMode" style="width: 100%;"> |
| | | <el-option label="先进先出 (FIFO)" value="FIFO" /> |
| | | <el-option label="后进先出 (LIFO)" value="LIFO" /> |
| | | <el-option label="随机 (RANDOM)" value="RANDOM" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="自动存储"> |
| | | <el-switch v-model="deviceLogicParams.autoStore" /> |
| | | <span class="form-tip">是否自动触发存储请求</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="自动取货"> |
| | | <el-switch v-model="deviceLogicParams.autoRetrieve" /> |
| | | <span class="form-tip">是否自动触发取货请求</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="最大重试次数"> |
| | | <el-input-number |
| | | v-model="deviceLogicParams.maxRetryCount" |
| | | :min="0" |
| | | :max="10" |
| | | :step="1" |
| | | style="width: 100%;" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 描述信息 --> |
| | | <el-card class="form-section" shadow="never" style="margin-top: 20px;"> |
| | | <template #header> |
| | |
| | | const saving = ref(false) |
| | | const testing = ref(false) |
| | | const testResult = ref(null) |
| | | |
| | | // 设备逻辑参数(根据设备类型动态显示) |
| | | const deviceLogicParams = reactive({ |
| | | // 上大车参数 |
| | | vehicleCapacity: 6000, |
| | | glassIntervalMs: 1000, |
| | | autoFeed: true, |
| | | maxRetryCount: 5, |
| | | positionMapping: {}, |
| | | // 大理片参数 |
| | | glassSize: 2000, |
| | | processingTime: 5000, |
| | | autoProcess: true, |
| | | // 玻璃存储参数 |
| | | storageCapacity: 100, |
| | | retrievalMode: 'FIFO', |
| | | autoStore: true, |
| | | autoRetrieve: true |
| | | }) |
| | | |
| | | // 位置映射的键数组(用于v-for) |
| | | const mappingKeys = ref([]) |
| | | |
| | | // 设备表单数据 |
| | | const getDefaultForm = () => ({ |
| | |
| | | emit('update:modelValue', newVal) |
| | | }) |
| | | |
| | | // 监听PLC类型变化,自动设置通讯协议 |
| | | watch(() => deviceForm.plcType, (newPlcType) => { |
| | | // 如果选择的是S7系列PLC,自动设置通讯协议为S7 Communication |
| | | if (newPlcType && (newPlcType.startsWith('S') || newPlcType.includes('S7'))) { |
| | | if (!deviceForm.protocolType || deviceForm.protocolType === '其他') { |
| | | deviceForm.protocolType = 'S7 Communication' |
| | | } |
| | | } |
| | | }) |
| | | |
| | | // 处理通讯协议变化 |
| | | const handleProtocolTypeChange = (value) => { |
| | | // 如果选择了非S7协议,但PLC类型是S7系列,给出提示 |
| | | if (value && value !== 'S7 Communication' && deviceForm.plcType) { |
| | | const s7Types = ['S1200', 'S1500', 'S400', 'S300', 'S200', 'S200_SMART'] |
| | | if (s7Types.includes(deviceForm.plcType)) { |
| | | ElMessage.warning('S7系列PLC通常使用S7 Communication协议,请确认协议选择是否正确') |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 方法定义 |
| | | const parseJsonSafe = (str, defaultValue = null) => { |
| | | if (!str) return defaultValue |
| | |
| | | deviceForm.dbArea = plcConfig.dbArea || 'DB1' |
| | | deviceForm.beginIndex = plcConfig.beginIndex ?? 0 |
| | | deviceForm.autoModeInterval = plcConfig.autoModeInterval ?? 5000 |
| | | |
| | | // 加载配置参数(从 configJson) |
| | | // 兼容两种格式: |
| | | // 1. 数组格式:[{ paramKey, paramValue, description }] |
| | | // 2. 对象格式(旧格式):{ fieldName: offset } - 自动转换为数组格式 |
| | | loadConfigParams(data?.configJson) |
| | | |
| | | // 加载设备逻辑参数 |
| | | const deviceLogic = extraObj.deviceLogic || {} |
| | | loadDeviceLogicParams(deviceLogic, data?.deviceType) |
| | | } |
| | | |
| | | // 加载配置参数(兼容旧的对象格式) |
| | | const loadConfigParams = (configJson) => { |
| | | if (!configJson) { |
| | | deviceForm.configParams = [] |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const parsed = typeof configJson === 'string' ? JSON.parse(configJson) : configJson |
| | | |
| | | // 如果是数组格式,直接使用 |
| | | if (Array.isArray(parsed)) { |
| | | deviceForm.configParams = parsed |
| | | } |
| | | // 如果是对象格式(字段名 → 偏移量),转换为数组格式 |
| | | else if (typeof parsed === 'object' && parsed !== null) { |
| | | // 字段名到中文描述的映射 |
| | | const fieldDescriptionMap = { |
| | | 'plcRequest': 'PLC请求字', |
| | | 'inPosition': '进片位置', |
| | | 'plcGlassId1': '玻璃id1', |
| | | 'plcGlassId2': '玻璃id2', |
| | | 'plcGlassId3': '玻璃id3', |
| | | 'plcGlassId4': '玻璃id4', |
| | | 'plcGlassId5': '玻璃id5', |
| | | 'plcGlassId6': '玻璃id6', |
| | | 'plcGlassCount': '玻璃数量', |
| | | 'onlineState': '联机状态', |
| | | 'plcReport': 'PLC汇报', |
| | | 'state1': '状态1', |
| | | 'state2': '状态2', |
| | | 'state3': '状态3', |
| | | 'state4': '状态4', |
| | | 'state5': '状态5', |
| | | 'state6': '状态6', |
| | | 'mesSend': 'MES发送', |
| | | 'mesConfirm': 'MES确认', |
| | | 'trainInfo': '列车信息', |
| | | 'start1': '起始1', |
| | | 'start2': '起始2', |
| | | 'start3': '起始3', |
| | | 'start4': '起始4', |
| | | 'start5': '起始5', |
| | | 'start6': '起始6', |
| | | 'target1': '目标1', |
| | | 'target2': '目标2', |
| | | 'target3': '目标3', |
| | | 'target4': '目标4', |
| | | 'target5': '目标5', |
| | | 'target6': '目标6', |
| | | 'mesWidth1': 'MES宽度1', |
| | | 'mesWidth2': 'MES宽度2', |
| | | 'mesWidth3': 'MES宽度3', |
| | | 'mesWidth4': 'MES宽度4', |
| | | 'mesWidth5': 'MES宽度5', |
| | | 'mesWidth6': 'MES宽度6', |
| | | 'mesHeight1': 'MES高度1', |
| | | 'mesHeight2': 'MES高度2', |
| | | 'mesHeight3': 'MES高度3', |
| | | 'mesHeight4': 'MES高度4', |
| | | 'mesHeight5': 'MES高度5', |
| | | 'mesHeight6': 'MES高度6', |
| | | 'mesThickness1': 'MES厚度1', |
| | | 'mesThickness2': 'MES厚度2', |
| | | 'mesThickness3': 'MES厚度3', |
| | | 'mesThickness4': 'MES厚度4', |
| | | 'mesThickness5': 'MES厚度5', |
| | | 'mesThickness6': 'MES厚度6', |
| | | 'edgeDistance1': '边缘距离1', |
| | | 'edgeDistance2': '边缘距离2', |
| | | 'edgeDistance3': '边缘距离3', |
| | | 'edgeDistance4': '边缘距离4', |
| | | 'edgeDistance5': '边缘距离5', |
| | | 'edgeDistance6': '边缘距离6', |
| | | 'targetEdgeDistance1': '目标边缘距离1', |
| | | 'targetEdgeDistance2': '目标边缘距离2', |
| | | 'targetEdgeDistance3': '目标边缘距离3', |
| | | 'targetEdgeDistance4': '目标边缘距离4', |
| | | 'targetEdgeDistance5': '目标边缘距离5', |
| | | 'targetEdgeDistance6': '目标边缘距离6', |
| | | 'alarmInfo': '报警信息' |
| | | } |
| | | |
| | | // 转换为数组格式 |
| | | deviceForm.configParams = Object.keys(parsed).map(fieldName => ({ |
| | | paramKey: fieldName, |
| | | paramValue: String(parsed[fieldName]), |
| | | description: fieldDescriptionMap[fieldName] || fieldName |
| | | })) |
| | | } else { |
| | | deviceForm.configParams = [] |
| | | } |
| | | } catch (error) { |
| | | console.warn('解析configJson失败', error) |
| | | deviceForm.configParams = [] |
| | | } |
| | | } |
| | | |
| | | // 加载设备逻辑参数 |
| | | const loadDeviceLogicParams = (deviceLogic, deviceType) => { |
| | | if (deviceType === '上大车') { |
| | | deviceLogicParams.vehicleCapacity = deviceLogic.vehicleCapacity ?? 6000 |
| | | deviceLogicParams.glassIntervalMs = deviceLogic.glassIntervalMs ?? 1000 |
| | | deviceLogicParams.autoFeed = deviceLogic.autoFeed ?? true |
| | | deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 5 |
| | | deviceLogicParams.positionMapping = deviceLogic.positionMapping || {} |
| | | mappingKeys.value = Object.keys(deviceLogicParams.positionMapping) |
| | | } else if (deviceType === '大理片') { |
| | | deviceLogicParams.glassSize = deviceLogic.glassSize ?? 2000 |
| | | deviceLogicParams.processingTime = deviceLogic.processingTime ?? 5000 |
| | | deviceLogicParams.autoProcess = deviceLogic.autoProcess ?? true |
| | | deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3 |
| | | } else if (deviceType === '玻璃存储') { |
| | | deviceLogicParams.storageCapacity = deviceLogic.storageCapacity ?? 100 |
| | | deviceLogicParams.retrievalMode = deviceLogic.retrievalMode || 'FIFO' |
| | | deviceLogicParams.autoStore = deviceLogic.autoStore ?? true |
| | | deviceLogicParams.autoRetrieve = deviceLogic.autoRetrieve ?? true |
| | | deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3 |
| | | } |
| | | } |
| | | |
| | | // 位置映射相关方法 |
| | | const addPositionMapping = () => { |
| | | const newKey = `POS${Object.keys(deviceLogicParams.positionMapping).length + 1}` |
| | | deviceLogicParams.positionMapping[newKey] = 1 |
| | | mappingKeys.value.push(newKey) |
| | | } |
| | | |
| | | const removePositionMapping = (key) => { |
| | | delete deviceLogicParams.positionMapping[key] |
| | | mappingKeys.value = mappingKeys.value.filter(k => k !== key) |
| | | } |
| | | |
| | | const updatePositionMapping = (index, newKey, oldValue) => { |
| | | const oldKey = mappingKeys.value[index] |
| | | if (oldKey && oldKey !== newKey) { |
| | | delete deviceLogicParams.positionMapping[oldKey] |
| | | } |
| | | mappingKeys.value[index] = newKey |
| | | if (newKey) { |
| | | deviceLogicParams.positionMapping[newKey] = oldValue || 1 |
| | | } |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | Object.assign(deviceForm, getDefaultForm()) |
| | | deviceFormRef.value?.clearValidate() |
| | | |
| | | // 重置设备逻辑参数 |
| | | deviceLogicParams.vehicleCapacity = 6000 |
| | | deviceLogicParams.glassIntervalMs = 1000 |
| | | deviceLogicParams.autoFeed = true |
| | | deviceLogicParams.maxRetryCount = 5 |
| | | deviceLogicParams.positionMapping = {} |
| | | mappingKeys.value = [] |
| | | |
| | | deviceLogicParams.glassSize = 2000 |
| | | deviceLogicParams.processingTime = 5000 |
| | | deviceLogicParams.autoProcess = true |
| | | |
| | | deviceLogicParams.storageCapacity = 100 |
| | | deviceLogicParams.retrievalMode = 'FIFO' |
| | | deviceLogicParams.autoStore = true |
| | | deviceLogicParams.autoRetrieve = true |
| | | } |
| | | |
| | | const addConfigParam = () => { |
| | |
| | | plcType: deviceForm.plcType |
| | | } |
| | | |
| | | // 保存设备逻辑参数 |
| | | const deviceLogic = {} |
| | | if (deviceForm.deviceType === '上大车') { |
| | | deviceLogic.vehicleCapacity = deviceLogicParams.vehicleCapacity |
| | | deviceLogic.glassIntervalMs = deviceLogicParams.glassIntervalMs |
| | | deviceLogic.autoFeed = deviceLogicParams.autoFeed |
| | | deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount |
| | | deviceLogic.positionMapping = deviceLogicParams.positionMapping |
| | | } else if (deviceForm.deviceType === '大理片') { |
| | | deviceLogic.glassSize = deviceLogicParams.glassSize |
| | | deviceLogic.processingTime = deviceLogicParams.processingTime |
| | | deviceLogic.autoProcess = deviceLogicParams.autoProcess |
| | | deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount |
| | | } else if (deviceForm.deviceType === '玻璃存储') { |
| | | deviceLogic.storageCapacity = deviceLogicParams.storageCapacity |
| | | deviceLogic.retrievalMode = deviceLogicParams.retrievalMode |
| | | deviceLogic.autoStore = deviceLogicParams.autoStore |
| | | deviceLogic.autoRetrieve = deviceLogicParams.autoRetrieve |
| | | deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount |
| | | } |
| | | |
| | | if (Object.keys(deviceLogic).length > 0) { |
| | | extraObj.deviceLogic = deviceLogic |
| | | } |
| | | |
| | | // 构建 configJson:将 configParams 数组转换为 JSON 字符串 |
| | | // configParams 结构: [{ paramKey: '', paramValue: '', description: '' }] |
| | | let configJsonValue = null |
| | | if (deviceForm.configParams && deviceForm.configParams.length > 0) { |
| | | // 过滤掉空参数 |
| | | const validParams = deviceForm.configParams.filter( |
| | | param => param.paramKey && param.paramKey.trim() !== '' |
| | | ) |
| | | if (validParams.length > 0) { |
| | | configJsonValue = JSON.stringify(validParams) |
| | | } |
| | | } |
| | | |
| | | const saveData = { |
| | | deviceName: deviceForm.deviceName, |
| | | deviceCode: deviceForm.deviceCode, |
| | |
| | | isPrimary: deviceForm.isPrimary, |
| | | enabled: deviceForm.enabled, |
| | | description: deviceForm.description, |
| | | configJson: deviceForm.configParams.length > 0 |
| | | ? JSON.stringify(deviceForm.configParams) |
| | | : null, |
| | | configJson: configJsonValue, // 保存配置参数JSON |
| | | extraParams: JSON.stringify(extraObj) |
| | | } |
| | | |
| | |
| | | handleClose() |
| | | } catch (error) { |
| | | console.error('保存设备配置失败:', error) |
| | | // 如果是表单验证错误,显示更详细的错误信息 |
| | | if (error && typeof error === 'object' && !error.response) { |
| | | const errorFields = Object.keys(error) |
| | | if (errorFields.length > 0) { |
| | | const firstError = error[errorFields[0]] |
| | | const errorMessage = Array.isArray(firstError) |
| | | ? firstError[0]?.message || firstError[0] |
| | | : firstError?.message || firstError |
| | | ElMessage.error(`表单验证失败: ${errorMessage}`) |
| | | return |
| | | } |
| | | } |
| | | ElMessage.error(isEdit.value ? '更新设备配置失败' : '创建设备配置失败') |
| | | } finally { |
| | | saving.value = false |
| | |
| | | :deep(.el-card__body) { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .position-mapping { |
| | | width: 100%; |
| | | } |
| | | |
| | | .mapping-item { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | padding: 12px; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 6px; |
| | | background-color: #fafafa; |
| | | } |
| | | </style> |