huang
2 天以前 ab389a5a6b329b15a655340ba7b87bce7fd7871d
mes-web/src/views/device/DeviceEditDialog.vue
@@ -31,30 +31,34 @@
            </el-form-item>
            <el-form-item label="设备编码" prop="deviceCode">
              <el-input
                v-model="deviceForm.deviceCode"
                placeholder="请输入设备编码"
                maxlength="50"
                :disabled="isEdit"
              />
              <template v-if="isEdit">
                <el-input
                  v-model="deviceForm.deviceCode"
                  maxlength="50"
                  disabled
                />
              </template>
              <template v-else>
                <el-tag type="info">保存后自动生成</el-tag>
              </template>
            </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>
            <el-form-item label="PLC类型" prop="plcType">
              <el-select v-model="deviceForm.plcType" placeholder="选择PLC类型" style="width: 100%;" clearable>
            <el-form-item label="通讯类型" prop="plcType">
              <el-select v-model="deviceForm.plcType" placeholder="选择通讯类型" 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 TCP" value="MODBUS" />
              </el-select>
            </el-form-item>
@@ -105,22 +109,7 @@
              />
            </el-form-item>
            <el-form-item label="通讯协议" prop="protocolType">
              <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">
              <el-input-number
@@ -254,190 +243,19 @@
          <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%;"
        <!-- 使用动态组件加载对应设备类型的配置组件 -->
        <component
          :is="deviceConfigComponent"
          v-if="deviceConfigComponent"
          v-model="deviceLogicParams"
                />
                <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 v-else class="no-config-tip">
          <el-alert
            :title="`设备类型「${deviceForm.deviceType}」暂无配置组件`"
            type="info"
            :closable="false"
            show-icon
          />
        </div>
      </el-card>
@@ -496,6 +314,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({
@@ -519,27 +338,23 @@
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
})
// 设备类型列表
const deviceTypes = ref([])
const deviceTypesLoading = ref(false)
// 位置映射的键数组(用于v-for)
const mappingKeys = ref([])
// 设备逻辑参数(根据设备类型动态显示)
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 = () => ({
@@ -562,7 +377,8 @@
  description: '',
  isPrimary: false,
  enabled: true,
  extraParams: null
  extraParams: null,
  projectId: 1
})
const deviceForm = reactive(getDefaultForm())
@@ -571,14 +387,35 @@
const isEdit = computed(() => !!props.deviceData)
// 表单验证规则
const validateDeviceCode = async (rule, value, callback) => {
  if (!value) {
    // 允许留空,后台自动生成
    return callback()
  }
  try {
    const res = await deviceConfigApi.checkCode(
      value.trim(),
      isEdit.value ? props.deviceData?.id : null
    )
    if (res?.data === true) {
      callback(new Error('设备编码已存在,请更换'))
    } else {
      callback()
    }
  } catch (err) {
    console.error('检查设备编码失败', err)
    callback(new Error('设备编码校验失败,请稍后重试'))
  }
}
const deviceRules = {
  deviceName: [
    { required: true, message: '请输入设备名称', trigger: 'blur' },
    { min: 1, max: 50, message: '设备名称长度在 1 到 50 个字符', trigger: 'blur' }
  ],
  deviceCode: [
    { required: true, message: '请输入设备编码', trigger: 'blur' },
    { pattern: /^[A-Z0-9_]+$/, message: '设备编码只能包含大写字母、数字和下划线', trigger: 'blur' }
    { pattern: /^[A-Z0-9_]+$/, message: '设备编码只能包含大写字母、数字和下划线', trigger: 'blur' },
    { validator: validateDeviceCode, trigger: 'blur' }
  ],
  deviceType: [
    { required: true, message: '请选择设备类型', trigger: 'change' }
@@ -597,9 +434,7 @@
  moduleName: [
    { required: true, message: '请输入模块名称', trigger: 'blur' }
  ],
  protocolType: [
    { required: true, message: '请选择通讯协议', trigger: 'change' }
  ],
  timeout: [
    { required: true, message: '请输入超时时间', trigger: 'blur' },
    { type: 'number', min: 1, max: 300, message: '超时时间在 1 到 300 秒之间', trigger: 'blur' }
@@ -627,6 +462,8 @@
watch(() => props.modelValue, (newVal) => {
  dialogVisible.value = newVal
  if (newVal) {
    // 加载设备类型列表
    loadDeviceTypes()
    if (isEdit.value && props.deviceData) {
      loadDeviceData(props.deviceData)
    } else {
@@ -645,26 +482,59 @@
// 监听PLC类型变化,自动设置通讯协议
watch(() => deviceForm.plcType, (newPlcType) => {
  // 如果选择的是S7系列PLC,自动设置通讯协议为S7 Communication
  if (newPlcType && (newPlcType.startsWith('S') || newPlcType.includes('S7'))) {
    if (!deviceForm.protocolType || deviceForm.protocolType === '其他') {
  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) => {
  // 如果选择了非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 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 {
@@ -808,71 +678,21 @@
}
// 加载设备逻辑参数
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 loadDeviceLogicParams = (deviceLogic) => {
  if (deviceLogic && Object.keys(deviceLogic).length > 0) {
    deviceLogicParams.value = { ...deviceLogic }
  } else {
    deviceLogicParams.value = {}
  }
}
// 位置映射相关方法
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
  deviceLogicParams.value = {}
}
const addConfigParam = () => {
@@ -946,29 +766,11 @@
    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
    // 保存设备逻辑参数(直接使用deviceLogicParams,由各个配置组件管理)
    if (deviceLogicParams.value && Object.keys(deviceLogicParams.value).length > 0) {
      extraObj.deviceLogic = { ...deviceLogicParams.value }
    } else {
      delete extraObj.deviceLogic
    }
    // 构建 configJson:将 configParams 数组转换为 JSON 字符串
@@ -995,6 +797,7 @@
      isPrimary: deviceForm.isPrimary,
      enabled: deviceForm.enabled,
      description: deviceForm.description,
      projectId: deviceForm.projectId,
      configJson: configJsonValue,  // 保存配置参数JSON
      extraParams: JSON.stringify(extraObj)
    }
@@ -1124,4 +927,8 @@
  border-radius: 6px;
  background-color: #fafafa;
}
.no-config-tip {
  padding: 20px;
}
</style>