huang
5 天以前 9571229a2013472dc701ecf5767f2873b36d8f90
mes-web/src/views/device/DeviceEditDialog.vue
@@ -40,10 +40,13 @@
            </el-form-item>
            <el-form-item label="设备类型" prop="deviceType">
              <el-select v-model="deviceForm.deviceType" placeholder="选择设备类型" style="width: 100%;">
                <el-option label="上大车" value="上大车" />
                <el-option label="大理片" value="大理片" />
                <el-option label="玻璃存储" value="玻璃存储" />
              <el-select v-model="deviceForm.deviceType" placeholder="选择设备类型" style="width: 100%;" :loading="deviceTypesLoading">
                <el-option
                  v-for="type in deviceTypes"
                  :key="type"
                  :label="type"
                  :value="type"
                />
              </el-select>
            </el-form-item>
@@ -51,10 +54,7 @@
              <el-select v-model="deviceForm.plcType" placeholder="选择PLC类型" style="width: 100%;" clearable>
                <el-option label="西门子 S7-1200" value="S1200" />
                <el-option label="西门子 S7-1500" value="S1500" />
                <el-option label="西门子 S7-400" value="S400" />
                <el-option label="西门子 S7-300" value="S300" />
                <el-option label="西门子 S7-200" value="S200" />
                <el-option label="西门子 S7-200 SMART" value="S200_SMART" />
                <el-option label="Modbus 控制器" value="MODBUS" />
              </el-select>
            </el-form-item>
@@ -106,13 +106,20 @@
            </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">
@@ -240,6 +247,29 @@
        </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>
        <!-- 使用动态组件加载对应设备类型的配置组件 -->
        <component
          :is="deviceConfigComponent"
          v-if="deviceConfigComponent"
          v-model="deviceLogicParams"
                />
        <div v-else class="no-config-tip">
          <el-alert
            :title="`设备类型「${deviceForm.deviceType}」暂无配置组件`"
            type="info"
            :closable="false"
            show-icon
          />
        </div>
      </el-card>
      <!-- 描述信息 -->
      <el-card class="form-section" shadow="never" style="margin-top: 20px;">
        <template #header>
@@ -295,6 +325,7 @@
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { deviceConfigApi } from '@/api/device/deviceManagement'
import { getDeviceConfigComponent } from './components/DeviceLogicConfig'
// Props定义
const props = defineProps({
@@ -317,6 +348,24 @@
const saving = ref(false)
const testing = ref(false)
const testResult = ref(null)
// 设备类型列表
const deviceTypes = ref([])
const deviceTypesLoading = ref(false)
// 设备逻辑参数(根据设备类型动态显示)
const deviceLogicParams = ref({})
const S7_PLC_TYPES = ['S1200', 'S1500']
const MODBUS_PLC_TYPES = ['MODBUS']
// 计算属性:根据设备类型获取对应的配置组件
const deviceConfigComponent = computed(() => {
  if (!deviceForm.deviceType) {
    return null
  }
  return getDeviceConfigComponent(deviceForm.deviceType)
})
// 设备表单数据
const getDefaultForm = () => ({
@@ -404,6 +453,8 @@
watch(() => props.modelValue, (newVal) => {
  dialogVisible.value = newVal
  if (newVal) {
    // 加载设备类型列表
    loadDeviceTypes()
    if (isEdit.value && props.deviceData) {
      loadDeviceData(props.deviceData)
    } else {
@@ -420,7 +471,75 @@
  emit('update:modelValue', newVal)
})
// 监听PLC类型变化,自动设置通讯协议
watch(() => deviceForm.plcType, (newPlcType) => {
  if (!newPlcType) {
    return
  }
  if (S7_PLC_TYPES.includes(newPlcType)) {
    if (!deviceForm.protocolType || deviceForm.protocolType === '其他' || deviceForm.protocolType === 'Modbus TCP') {
      deviceForm.protocolType = 'S7 Communication'
    }
    return
  }
  if (MODBUS_PLC_TYPES.includes(newPlcType)) {
    if (!deviceForm.protocolType || deviceForm.protocolType === '其他' || deviceForm.protocolType === 'S7 Communication') {
      deviceForm.protocolType = 'Modbus TCP'
    }
  }
})
// 处理通讯协议变化
const handleProtocolTypeChange = (value) => {
  if (!deviceForm.plcType || !value) {
    return
  }
  if (value !== 'S7 Communication' && S7_PLC_TYPES.includes(deviceForm.plcType)) {
      ElMessage.warning('S7系列PLC通常使用S7 Communication协议,请确认协议选择是否正确')
    return
    }
  if (value !== 'Modbus TCP' && MODBUS_PLC_TYPES.includes(deviceForm.plcType)) {
    ElMessage.warning('Modbus 类型PLC通常使用 Modbus TCP 协议,请确认协议选择是否正确')
  }
}
// 方法定义
// 加载设备类型列表
const loadDeviceTypes = async () => {
  if (deviceTypes.value.length > 0) {
    // 如果已经加载过,不再重复加载
    return
  }
  // 所有支持的设备类型(确保包含所有有配置组件的类型)
  const supportedTypes = ['大车设备', '大理片笼', '卧转立扫码', '卧转立']
  try {
    deviceTypesLoading.value = true
    const res = await deviceConfigApi.getDeviceTypes()
    if (res?.data && Array.isArray(res.data)) {
      // 合并数据库中的类型和支持的类型,去重并排序
      const dbTypes = res.data
      const allTypes = [...new Set([...supportedTypes, ...dbTypes])].sort()
      deviceTypes.value = allTypes
    } else {
      // 如果API返回失败,使用默认类型
      deviceTypes.value = supportedTypes
      console.warn('获取设备类型列表失败,使用默认类型')
    }
  } catch (error) {
    console.error('加载设备类型列表失败:', error)
    // 失败时使用默认类型
    deviceTypes.value = supportedTypes
    ElMessage.warning('加载设备类型列表失败,使用默认类型')
  } finally {
    deviceTypesLoading.value = false
  }
}
const parseJsonSafe = (str, defaultValue = null) => {
  if (!str) return defaultValue
  try {
@@ -453,11 +572,132 @@
  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) => {
  if (deviceLogic && Object.keys(deviceLogic).length > 0) {
    deviceLogicParams.value = { ...deviceLogic }
  } else {
    deviceLogicParams.value = {}
  }
}
const resetForm = () => {
  Object.assign(deviceForm, getDefaultForm())
  deviceFormRef.value?.clearValidate()
  // 重置设备逻辑参数
  deviceLogicParams.value = {}
}
const addConfigParam = () => {
@@ -531,6 +771,26 @@
    plcType: deviceForm.plcType
  }
    // 保存设备逻辑参数(直接使用deviceLogicParams,由各个配置组件管理)
    if (deviceLogicParams.value && Object.keys(deviceLogicParams.value).length > 0) {
      extraObj.deviceLogic = { ...deviceLogicParams.value }
    } else {
      delete extraObj.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,
@@ -542,9 +802,7 @@
      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)
    }
@@ -562,6 +820,18 @@
    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
@@ -647,4 +917,22 @@
: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;
}
.no-config-tip {
  padding: 20px;
}
</style>