<template>
|
<el-dialog
|
v-model="dialogVisible"
|
:title="isEdit ? '编辑设备配置' : '创建设备配置'"
|
width="70%"
|
:close-on-click-modal="false"
|
:before-close="handleClose"
|
>
|
<el-form
|
ref="deviceFormRef"
|
:model="deviceForm"
|
:rules="deviceRules"
|
label-width="120px"
|
class="device-form"
|
>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<!-- 基本信息 -->
|
<el-card class="form-section" shadow="never">
|
<template #header>
|
<span class="section-title">基本信息</span>
|
</template>
|
|
<el-form-item label="设备名称" prop="deviceName">
|
<el-input
|
v-model="deviceForm.deviceName"
|
placeholder="请输入设备名称"
|
maxlength="50"
|
show-word-limit
|
/>
|
</el-form-item>
|
|
<el-form-item label="设备编码" prop="deviceCode">
|
<el-input
|
v-model="deviceForm.deviceCode"
|
placeholder="请输入设备编码"
|
maxlength="50"
|
:disabled="isEdit"
|
/>
|
</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>
|
</el-form-item>
|
|
<el-form-item label="PLC类型" prop="plcType">
|
<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-select>
|
</el-form-item>
|
|
<el-form-item label="PLC IP" prop="plcIp">
|
<el-input
|
v-model="deviceForm.plcIp"
|
placeholder="请输入PLC IP地址"
|
/>
|
</el-form-item>
|
|
<el-form-item label="端口号" prop="plcPort">
|
<el-input-number
|
v-model="deviceForm.plcPort"
|
:min="1"
|
:max="65535"
|
placeholder="端口号"
|
style="width: 100%;"
|
/>
|
</el-form-item>
|
|
<el-form-item label="主控设备">
|
<el-switch v-model="deviceForm.isPrimary" />
|
<span class="form-tip">主控设备不可禁用或删除</span>
|
</el-form-item>
|
</el-card>
|
</el-col>
|
|
<el-col :span="12">
|
<!-- 连接配置 -->
|
<el-card class="form-section" shadow="never">
|
<template #header>
|
<span class="section-title">连接配置</span>
|
</template>
|
|
<el-form-item label="模块名称" prop="moduleName">
|
<el-input
|
v-model="deviceForm.moduleName"
|
placeholder="请输入模块名称"
|
maxlength="100"
|
/>
|
</el-form-item>
|
|
<el-form-item label="模块编号" prop="moduleCode">
|
<el-input
|
v-model="deviceForm.moduleCode"
|
placeholder="请输入模块编号"
|
maxlength="50"
|
/>
|
</el-form-item>
|
|
<el-form-item label="通讯协议" prop="protocolType">
|
<el-select v-model="deviceForm.protocolType" placeholder="选择通讯协议" style="width: 100%;">
|
<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>
|
</el-form-item>
|
|
<el-form-item label="超时时间(秒)" prop="timeout">
|
<el-input-number
|
v-model="deviceForm.timeout"
|
:min="1"
|
:max="300"
|
:step="1"
|
style="width: 100%;"
|
/>
|
</el-form-item>
|
|
<el-form-item label="重试次数" prop="retryCount">
|
<el-input-number
|
v-model="deviceForm.retryCount"
|
:min="0"
|
:max="10"
|
:step="1"
|
style="width: 100%;"
|
/>
|
</el-form-item>
|
|
<el-form-item label="心跳间隔(秒)" prop="heartbeatInterval">
|
<el-input-number
|
v-model="deviceForm.heartbeatInterval"
|
:min="5"
|
:max="3600"
|
:step="5"
|
style="width: 100%;"
|
/>
|
</el-form-item>
|
</el-card>
|
|
<!-- PLC 地址配置 -->
|
<el-card class="form-section" shadow="never" style="margin-top: 20px;">
|
<template #header>
|
<span class="section-title">PLC 地址配置</span>
|
</template>
|
|
<el-form-item label="DB块" prop="dbArea">
|
<el-input
|
v-model="deviceForm.dbArea"
|
placeholder="如 DB1、DB38"
|
maxlength="20"
|
/>
|
</el-form-item>
|
|
<el-form-item label="起始索引" prop="beginIndex">
|
<el-input-number
|
v-model="deviceForm.beginIndex"
|
:min="0"
|
:max="65535"
|
:step="1"
|
style="width: 100%;"
|
/>
|
</el-form-item>
|
|
<el-form-item label="自动间隔(ms)" prop="autoModeInterval">
|
<el-input-number
|
v-model="deviceForm.autoModeInterval"
|
:min="100"
|
:max="600000"
|
:step="100"
|
style="width: 100%;"
|
/>
|
</el-form-item>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<!-- 配置参数 -->
|
<el-card class="form-section" shadow="never" style="margin-top: 20px;">
|
<template #header>
|
<div class="card-header">
|
<span class="section-title">配置参数</span>
|
<el-button type="primary" size="small" @click="addConfigParam">
|
添加参数
|
</el-button>
|
</div>
|
</template>
|
|
<div v-if="deviceForm.configParams.length === 0" class="empty-params">
|
<el-empty description="暂无配置参数" :image-size="60" />
|
</div>
|
|
<div v-else class="config-params">
|
<div
|
v-for="(param, index) in deviceForm.configParams"
|
:key="index"
|
class="config-param-item"
|
>
|
<el-row :gutter="12" style="width: 100%;">
|
<el-col :span="6">
|
<el-input
|
v-model="param.paramKey"
|
placeholder="参数键"
|
size="small"
|
/>
|
</el-col>
|
<el-col :span="6">
|
<el-input
|
v-model="param.paramValue"
|
placeholder="参数值"
|
size="small"
|
/>
|
</el-col>
|
<el-col :span="8">
|
<el-input
|
v-model="param.description"
|
placeholder="描述"
|
size="small"
|
/>
|
</el-col>
|
<el-col :span="4">
|
<el-button
|
type="danger"
|
size="small"
|
@click="removeConfigParam(index)"
|
>
|
删除
|
</el-button>
|
</el-col>
|
</el-row>
|
</div>
|
</div>
|
</el-card>
|
|
<!-- 描述信息 -->
|
<el-card class="form-section" shadow="never" style="margin-top: 20px;">
|
<template #header>
|
<span class="section-title">描述信息</span>
|
</template>
|
|
<el-form-item label="设备描述" prop="description">
|
<el-input
|
v-model="deviceForm.description"
|
type="textarea"
|
:rows="3"
|
placeholder="请输入设备描述"
|
maxlength="500"
|
show-word-limit
|
/>
|
</el-form-item>
|
</el-card>
|
</el-form>
|
|
<!-- 连接测试 -->
|
<el-card class="connection-test" shadow="never" style="margin-top: 20px;">
|
<template #header>
|
<span class="section-title">连接测试</span>
|
</template>
|
|
<div class="test-content">
|
<el-button type="primary" @click="testConnection" :loading="testing">
|
{{ testing ? '测试中...' : '测试连接' }}
|
</el-button>
|
|
<div v-if="testResult" class="test-result">
|
<el-alert
|
:title="testResult.message"
|
:type="testResult.success ? 'success' : 'error'"
|
:closable="false"
|
show-icon
|
/>
|
</div>
|
</div>
|
</el-card>
|
|
<template #footer>
|
<el-button @click="handleClose">取消</el-button>
|
<el-button @click="resetForm" v-if="!isEdit">重置</el-button>
|
<el-button type="primary" @click="saveDevice" :loading="saving">
|
{{ saving ? '保存中...' : (isEdit ? '更新' : '创建') }}
|
</el-button>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { ref, reactive, watch, computed } from 'vue'
|
import { ElMessage } from 'element-plus'
|
import { deviceConfigApi } from '@/api/device/deviceManagement'
|
|
// Props定义
|
const props = defineProps({
|
modelValue: {
|
type: Boolean,
|
default: false
|
},
|
deviceData: {
|
type: Object,
|
default: null
|
}
|
})
|
|
// Emits定义
|
const emit = defineEmits(['update:modelValue', 'success', 'close'])
|
|
// 响应式数据
|
const deviceFormRef = ref(null)
|
const dialogVisible = ref(false)
|
const saving = ref(false)
|
const testing = ref(false)
|
const testResult = ref(null)
|
|
// 设备表单数据
|
const getDefaultForm = () => ({
|
deviceName: '',
|
deviceCode: '',
|
deviceType: '',
|
plcType: '',
|
plcIp: '',
|
plcPort: 502,
|
moduleName: '',
|
moduleCode: '',
|
protocolType: '',
|
timeout: 30,
|
retryCount: 3,
|
heartbeatInterval: 30,
|
dbArea: 'DB1',
|
beginIndex: 0,
|
autoModeInterval: 5000,
|
configParams: [],
|
description: '',
|
isPrimary: false,
|
enabled: true,
|
extraParams: null
|
})
|
|
const deviceForm = reactive(getDefaultForm())
|
|
// 计算属性
|
const isEdit = computed(() => !!props.deviceData)
|
|
// 表单验证规则
|
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' }
|
],
|
deviceType: [
|
{ required: true, message: '请选择设备类型', trigger: 'change' }
|
],
|
plcType: [
|
{ required: true, message: '请选择PLC类型', trigger: 'change' }
|
],
|
plcIp: [
|
{ required: true, message: '请输入PLC IP地址', trigger: 'blur' },
|
{ pattern: /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, message: '请输入有效的IP地址', trigger: 'blur' }
|
],
|
port: [
|
{ required: true, message: '请输入端口号', trigger: 'blur' },
|
{ type: 'number', min: 1, max: 65535, message: '端口号在 1 到 65535 之间', trigger: 'blur' }
|
],
|
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' }
|
],
|
retryCount: [
|
{ required: true, message: '请输入重试次数', trigger: 'blur' },
|
{ type: 'number', min: 0, max: 10, message: '重试次数在 0 到 10 次之间', trigger: 'blur' }
|
],
|
heartbeatInterval: [
|
{ required: true, message: '请输入心跳间隔', trigger: 'blur' },
|
{ type: 'number', min: 5, max: 3600, message: '心跳间隔在 5 到 3600 秒之间', trigger: 'blur' }
|
],
|
dbArea: [
|
{ required: true, message: '请输入DB块', trigger: 'blur' }
|
],
|
beginIndex: [
|
{ type: 'number', min: 0, max: 65535, message: '起始索引在 0 到 65535 之间', trigger: 'blur' }
|
],
|
autoModeInterval: [
|
{ type: 'number', min: 100, max: 600000, message: '自动间隔在 100 到 600000 之间', trigger: 'blur' }
|
]
|
}
|
|
// 监听对话框显示状态
|
watch(() => props.modelValue, (newVal) => {
|
dialogVisible.value = newVal
|
if (newVal) {
|
if (isEdit.value && props.deviceData) {
|
loadDeviceData(props.deviceData)
|
} else {
|
// 创建模式,重置表单
|
resetForm()
|
}
|
// 清除测试结果
|
testResult.value = null
|
}
|
})
|
|
// 监听对话框关闭
|
watch(dialogVisible, (newVal) => {
|
emit('update:modelValue', newVal)
|
})
|
|
// 方法定义
|
const parseJsonSafe = (str, defaultValue = null) => {
|
if (!str) return defaultValue
|
try {
|
return JSON.parse(str)
|
} catch (error) {
|
console.warn('JSON解析失败:', error)
|
return defaultValue
|
}
|
}
|
|
const loadDeviceData = (data) => {
|
resetForm()
|
Object.assign(deviceForm, getDefaultForm(), {
|
...data,
|
plcPort: data?.plcPort ?? 502
|
})
|
|
deviceForm.configParams = parseJsonSafe(data?.configJson, []) || []
|
deviceForm.extraParams = data?.extraParams || null
|
|
const extraObj = parseJsonSafe(deviceForm.extraParams, {}) || {}
|
const connection = extraObj.connectionConfig || {}
|
deviceForm.moduleCode = connection.moduleCode || ''
|
deviceForm.protocolType = connection.protocolType || ''
|
deviceForm.timeout = connection.timeout ?? 30
|
deviceForm.retryCount = connection.retryCount ?? 3
|
deviceForm.heartbeatInterval = connection.heartbeatInterval ?? 30
|
|
const plcConfig = extraObj.plcConfig || {}
|
deviceForm.dbArea = plcConfig.dbArea || 'DB1'
|
deviceForm.beginIndex = plcConfig.beginIndex ?? 0
|
deviceForm.autoModeInterval = plcConfig.autoModeInterval ?? 5000
|
}
|
|
const resetForm = () => {
|
Object.assign(deviceForm, getDefaultForm())
|
deviceFormRef.value?.clearValidate()
|
}
|
|
const addConfigParam = () => {
|
deviceForm.configParams.push({
|
paramKey: '',
|
paramValue: '',
|
description: ''
|
})
|
}
|
|
const removeConfigParam = (index) => {
|
deviceForm.configParams.splice(index, 1)
|
}
|
|
const testConnection = async () => {
|
try {
|
testing.value = true
|
testResult.value = null
|
|
const testData = {
|
plcIp: deviceForm.plcIp,
|
plcPort: deviceForm.plcPort,
|
timeout: deviceForm.timeout
|
}
|
|
const response = await deviceConfigApi.testConnection(testData)
|
if (response.success) {
|
testResult.value = {
|
success: true,
|
message: response.data || '连接测试成功!设备可以正常通讯。'
|
}
|
} else {
|
testResult.value = {
|
success: false,
|
message: response.message || '连接测试失败,请检查网络连接和设备配置。'
|
}
|
}
|
} catch (error) {
|
console.error('连接测试失败:', error)
|
testResult.value = {
|
success: false,
|
message: '连接测试失败,请检查网络连接和设备配置。'
|
}
|
} finally {
|
testing.value = false
|
}
|
}
|
|
const saveDevice = async () => {
|
try {
|
// 表单验证
|
await deviceFormRef.value.validate()
|
|
saving.value = true
|
|
// 构建保存数据
|
const connectionConfig = {
|
moduleCode: deviceForm.moduleCode,
|
protocolType: deviceForm.protocolType,
|
timeout: deviceForm.timeout,
|
retryCount: deviceForm.retryCount,
|
heartbeatInterval: deviceForm.heartbeatInterval
|
}
|
|
const extraObj = parseJsonSafe(deviceForm.extraParams, {}) || {}
|
extraObj.connectionConfig = connectionConfig
|
extraObj.plcConfig = {
|
dbArea: deviceForm.dbArea,
|
beginIndex: deviceForm.beginIndex,
|
autoModeInterval: deviceForm.autoModeInterval,
|
plcType: deviceForm.plcType
|
}
|
|
const saveData = {
|
deviceName: deviceForm.deviceName,
|
deviceCode: deviceForm.deviceCode,
|
deviceType: deviceForm.deviceType,
|
plcType: deviceForm.plcType,
|
plcIp: deviceForm.plcIp,
|
plcPort: deviceForm.plcPort,
|
moduleName: deviceForm.moduleName,
|
isPrimary: deviceForm.isPrimary,
|
enabled: deviceForm.enabled,
|
description: deviceForm.description,
|
configJson: deviceForm.configParams.length > 0
|
? JSON.stringify(deviceForm.configParams)
|
: null,
|
extraParams: JSON.stringify(extraObj)
|
}
|
|
if (isEdit.value) {
|
// 更新设备
|
await deviceConfigApi.update(props.deviceData.id, saveData)
|
ElMessage.success('设备配置更新成功')
|
} else {
|
// 创建设备
|
await deviceConfigApi.create(saveData)
|
ElMessage.success('设备配置创建成功')
|
}
|
|
emit('success')
|
handleClose()
|
} catch (error) {
|
console.error('保存设备配置失败:', error)
|
ElMessage.error(isEdit.value ? '更新设备配置失败' : '创建设备配置失败')
|
} finally {
|
saving.value = false
|
}
|
}
|
|
const handleClose = () => {
|
dialogVisible.value = false
|
testResult.value = null
|
emit('close')
|
}
|
</script>
|
|
<style scoped>
|
.device-form {
|
max-height: 60vh;
|
overflow-y: auto;
|
padding-right: 10px;
|
}
|
|
.form-section {
|
margin-bottom: 20px;
|
}
|
|
.section-title {
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.form-tip {
|
margin-left: 10px;
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.empty-params {
|
padding: 20px;
|
}
|
|
.config-params {
|
max-height: 200px;
|
overflow-y: auto;
|
}
|
|
.config-param-item {
|
margin-bottom: 12px;
|
padding: 12px;
|
border: 1px solid #ebeef5;
|
border-radius: 6px;
|
background-color: #fafafa;
|
}
|
|
.connection-test {
|
margin-top: 20px;
|
}
|
|
.test-content {
|
display: flex;
|
align-items: center;
|
gap: 20px;
|
}
|
|
.test-result {
|
flex: 1;
|
}
|
|
:deep(.el-card__header) {
|
padding: 12px 20px;
|
background-color: #fafafa;
|
border-bottom: 1px solid #ebeef5;
|
}
|
|
:deep(.el-form-item__label) {
|
font-weight: 500;
|
}
|
|
:deep(.el-card__body) {
|
padding: 20px;
|
}
|
</style>
|