<template>
|
<el-dialog
|
v-model="dialogVisible"
|
:title="title"
|
width="70%"
|
:close-on-click-modal="false"
|
@close="handleClose"
|
>
|
<el-form
|
ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="120px"
|
class="device-group-form"
|
>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<!-- 基本信息 -->
|
<el-card shadow="never" class="form-card">
|
<template #header>
|
<div class="card-header">
|
<span>基本信息</span>
|
</div>
|
</template>
|
|
<el-form-item label="组名称" prop="groupName">
|
<el-input
|
v-model="form.groupName"
|
placeholder="请输入设备组名称"
|
maxlength="50"
|
show-word-limit
|
/>
|
</el-form-item>
|
|
<el-form-item label="组编码" prop="groupCode">
|
<el-input
|
v-model="form.groupCode"
|
placeholder="请输入设备组编码"
|
maxlength="20"
|
:disabled="isEdit"
|
@blur="generateCode"
|
>
|
<template #append>
|
<el-button @click="generateCode" :disabled="isEdit">
|
自动生成
|
</el-button>
|
</template>
|
</el-input>
|
</el-form-item>
|
|
<el-form-item label="组类型" prop="groupType">
|
<el-select v-model="form.groupType" placeholder="选择组类型">
|
<el-option label="生产线" value="生产线" />
|
<el-option label="测试线" value="测试线" />
|
<el-option label="辅助设备组" value="辅助设备组" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="描述信息">
|
<el-input
|
v-model="form.description"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入设备组描述信息"
|
maxlength="200"
|
show-word-limit
|
/>
|
</el-form-item>
|
|
<el-form-item label="排序序号" prop="sortOrder">
|
<el-input-number
|
v-model="form.sortOrder"
|
:min="0"
|
:max="999"
|
placeholder="排序序号"
|
/>
|
</el-form-item>
|
</el-card>
|
</el-col>
|
|
<el-col :span="12">
|
<!-- 配置信息 -->
|
<el-card shadow="never" class="form-card">
|
<template #header>
|
<div class="card-header">
|
<span>配置信息</span>
|
</div>
|
</template>
|
|
<el-form-item label="组状态" prop="groupStatus">
|
<el-select v-model="form.groupStatus" placeholder="选择组状态">
|
<el-option label="启用" value="ENABLED" />
|
<el-option label="禁用" value="DISABLED" />
|
<el-option label="维护中" value="MAINTENANCE" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="执行模式" prop="executionMode">
|
<el-radio-group v-model="form.executionMode">
|
<el-radio label="SERIAL">串行执行</el-radio>
|
<el-radio label="PARALLEL">并行执行</el-radio>
|
</el-radio-group>
|
<div class="form-tip">串行:按顺序依次执行设备操作;并行:同时执行多个设备操作</div>
|
</el-form-item>
|
|
<el-form-item label="最大设备数" prop="maxDeviceCount">
|
<el-input-number
|
v-model="form.maxDeviceCount"
|
:min="1"
|
:max="1000"
|
placeholder="最大设备数"
|
/>
|
<div class="form-tip">该设备组最多可管理的设备数量</div>
|
</el-form-item>
|
|
<el-form-item label="心跳间隔" prop="heartbeatInterval">
|
<el-input-number
|
v-model="form.heartbeatInterval"
|
:min="1"
|
:max="3600"
|
placeholder="心跳间隔(秒)"
|
/>
|
<div class="form-tip">设备与该组的心跳检测间隔</div>
|
</el-form-item>
|
|
<el-form-item label="连接超时" prop="connectionTimeout">
|
<el-input-number
|
v-model="form.connectionTimeout"
|
:min="1"
|
:max="300"
|
placeholder="连接超时(秒)"
|
/>
|
<div class="form-tip">连接该组设备的最大等待时间</div>
|
</el-form-item>
|
|
<el-form-item label="重试次数" prop="retryCount">
|
<el-input-number
|
v-model="form.retryCount"
|
:min="0"
|
:max="10"
|
placeholder="重试次数"
|
/>
|
<div class="form-tip">连接失败时的重试次数</div>
|
</el-form-item>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<!-- 告警配置 -->
|
<el-row :gutter="20">
|
<el-col :span="24">
|
<el-card shadow="never" class="form-card">
|
<template #header>
|
<div class="card-header">
|
<span>告警配置</span>
|
</div>
|
</template>
|
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="启用告警" prop="enableAlert">
|
<el-switch v-model="form.enableAlert" />
|
</el-form-item>
|
|
<el-form-item label="离线告警阈值" prop="offlineThreshold">
|
<el-input-number
|
v-model="form.offlineThreshold"
|
:min="1"
|
:max="100"
|
placeholder="离线设备数量阈值"
|
/>
|
<div class="form-tip">超过此阈值时触发离线告警</div>
|
</el-form-item>
|
|
<el-form-item label="响应超时阈值" prop="responseTimeout">
|
<el-input-number
|
v-model="form.responseTimeout"
|
:min="1"
|
:max="600"
|
placeholder="响应超时阈值(秒)"
|
/>
|
<div class="form-tip">设备响应时间超过此阈值时告警</div>
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="12">
|
<el-form-item label="告警通知方式" prop="alertNotificationMethod">
|
<el-checkbox-group v-model="form.alertNotificationMethod">
|
<el-checkbox label="email">邮件</el-checkbox>
|
<el-checkbox label="sms">短信</el-checkbox>
|
<el-checkbox label="wechat">微信</el-checkbox>
|
<el-checkbox label="webhook">Webhook</el-checkbox>
|
</el-checkbox-group>
|
</el-form-item>
|
|
<el-form-item label="通知人" prop="notifyUsers">
|
<el-select
|
v-model="form.notifyUsers"
|
multiple
|
placeholder="选择通知人员"
|
style="width: 100%"
|
>
|
<el-option label="张三" value="user1" />
|
<el-option label="李四" value="user2" />
|
<el-option label="王五" value="user3" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="Webhook URL" v-if="form.alertNotificationMethod?.includes('webhook')">
|
<el-input
|
v-model="form.webhookUrl"
|
placeholder="输入Webhook通知地址"
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<!-- 高级配置 -->
|
<el-row :gutter="20">
|
<el-col :span="24">
|
<el-card shadow="never" class="form-card">
|
<template #header>
|
<div class="card-header">
|
<span>高级配置</span>
|
<el-button type="text" @click="showAdvanced = !showAdvanced">
|
{{ showAdvanced ? '收起' : '展开' }}
|
</el-button>
|
</div>
|
</template>
|
|
<div v-show="showAdvanced">
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="数据采集间隔" prop="dataCollectionInterval">
|
<el-input-number
|
v-model="form.dataCollectionInterval"
|
:min="1"
|
:max="3600"
|
placeholder="数据采集间隔(秒)"
|
/>
|
</el-form-item>
|
|
<el-form-item label="数据保存周期" prop="dataRetentionPeriod">
|
<el-input-number
|
v-model="form.dataRetentionPeriod"
|
:min="1"
|
:max="365"
|
placeholder="数据保存周期(天)"
|
/>
|
</el-form-item>
|
|
<el-form-item label="日志级别" prop="logLevel">
|
<el-select v-model="form.logLevel" placeholder="选择日志级别">
|
<el-option label="ERROR" value="ERROR" />
|
<el-option label="WARN" value="WARN" />
|
<el-option label="INFO" value="INFO" />
|
<el-option label="DEBUG" value="DEBUG" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="12">
|
<el-form-item label="启用自动备份" prop="enableAutoBackup">
|
<el-switch v-model="form.enableAutoBackup" />
|
</el-form-item>
|
|
<el-form-item label="备份间隔" prop="backupInterval" v-if="form.enableAutoBackup">
|
<el-input-number
|
v-model="form.backupInterval"
|
:min="1"
|
:max="168"
|
placeholder="备份间隔(小时)"
|
/>
|
</el-form-item>
|
|
<el-form-item label="自定义参数">
|
<el-input
|
v-model="customParamsText"
|
type="textarea"
|
:rows="3"
|
placeholder="JSON格式自定义参数"
|
@blur="validateCustomParams"
|
/>
|
<div class="form-tip">例如: {"key1": "value1", "key2": 123}</div>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
</el-form>
|
|
<!-- 配置验证结果 -->
|
<div v-if="validationResult" class="validation-result">
|
<el-alert
|
:title="validationResult.valid ? '配置验证通过' : '配置验证失败'"
|
:type="validationResult.valid ? 'success' : 'error'"
|
:closable="false"
|
show-icon
|
>
|
<template #default>
|
<div v-if="!validationResult.valid">
|
<p v-for="error in validationResult.errors" :key="error" style="margin: 0;">
|
{{ error }}
|
</p>
|
</div>
|
<div v-else>
|
<p style="margin: 0;">所有配置参数验证通过</p>
|
</div>
|
</template>
|
</el-alert>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleClose">取消</el-button>
|
<el-button @click="testConfiguration" :loading="testing" :disabled="!form.groupName">
|
测试配置
|
</el-button>
|
<el-button type="primary" @click="submit" :loading="saving">
|
{{ isEdit ? '更新' : '创建' }}
|
</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { deviceGroupApi } from '@/api/device/deviceManagement'
|
|
// Props
|
const props = defineProps({
|
visible: {
|
type: Boolean,
|
default: false
|
},
|
data: {
|
type: Object,
|
default: () => null
|
}
|
})
|
|
// Emits
|
const emit = defineEmits(['update:visible', 'success', 'close'])
|
|
// 响应式数据
|
const formRef = ref(null)
|
const showAdvanced = ref(false)
|
const testing = ref(false)
|
const saving = ref(false)
|
const validationResult = ref(null)
|
|
// 使用计算属性来同步 visible prop 和内部 dialogVisible
|
const dialogVisible = computed({
|
get: () => props.visible,
|
set: (val) => emit('update:visible', val)
|
})
|
const customParamsText = ref('')
|
|
// 表单数据
|
const form = reactive({
|
groupName: '',
|
groupCode: '',
|
groupType: '生产线',
|
description: '',
|
sortOrder: 0,
|
groupStatus: 'ENABLED',
|
executionMode: 'SERIAL', // 执行模式:SERIAL串行 / PARALLEL并行
|
maxDeviceCount: 100,
|
heartbeatInterval: 30,
|
connectionTimeout: 10,
|
retryCount: 3,
|
enableAlert: false,
|
offlineThreshold: 5,
|
responseTimeout: 30,
|
alertNotificationMethod: [],
|
notifyUsers: [],
|
webhookUrl: '',
|
dataCollectionInterval: 5,
|
dataRetentionPeriod: 30,
|
logLevel: 'INFO',
|
enableAutoBackup: false,
|
backupInterval: 24,
|
customParams: {}
|
})
|
|
// 计算属性
|
const isEdit = computed(() => !!props.data)
|
const title = computed(() => (isEdit.value ? '编辑设备组' : '新建设备组'))
|
|
// 验证规则
|
const rules = {
|
groupName: [
|
{ required: true, message: '请输入设备组名称', trigger: 'blur' },
|
{ min: 2, max: 50, message: '名称长度应在2-50个字符之间', trigger: 'blur' }
|
],
|
groupCode: [
|
{ required: true, message: '请输入设备组编码', trigger: 'blur' },
|
{ pattern: /^[A-Z0-9_]+$/, message: '编码只能包含大写字母、数字和下划线', trigger: 'blur' }
|
],
|
groupType: [
|
{ required: true, message: '请选择组类型', trigger: 'change' }
|
],
|
sortOrder: [
|
{ type: 'number', min: 0, max: 999, message: '排序序号应在0-999之间', trigger: 'blur' }
|
],
|
maxDeviceCount: [
|
{ type: 'number', min: 1, max: 1000, message: '最大设备数应在1-1000之间', trigger: 'blur' }
|
],
|
heartbeatInterval: [
|
{ type: 'number', min: 1, max: 3600, message: '心跳间隔应在1-3600秒之间', trigger: 'blur' }
|
],
|
connectionTimeout: [
|
{ type: 'number', min: 1, max: 300, message: '连接超时应在1-300秒之间', trigger: 'blur' }
|
],
|
retryCount: [
|
{ type: 'number', min: 0, max: 10, message: '重试次数应在0-10之间', trigger: 'blur' }
|
],
|
offlineThreshold: [
|
{ type: 'number', min: 1, max: 100, message: '离线告警阈值应在1-100之间', trigger: 'blur' }
|
],
|
responseTimeout: [
|
{ type: 'number', min: 1, max: 600, message: '响应超时阈值应在1-600秒之间', trigger: 'blur' }
|
],
|
dataCollectionInterval: [
|
{ type: 'number', min: 1, max: 3600, message: '数据采集间隔应在1-3600秒之间', trigger: 'blur' }
|
],
|
dataRetentionPeriod: [
|
{ type: 'number', min: 1, max: 365, message: '数据保存周期应在1-365天之间', trigger: 'blur' }
|
],
|
backupInterval: [
|
{ type: 'number', min: 1, max: 168, message: '备份间隔应在1-168小时之间', trigger: 'blur' }
|
]
|
}
|
|
// 监听数据变化
|
watch(() => props.visible, (newVal) => {
|
if (newVal) {
|
nextTick(() => {
|
resetForm()
|
if (props.data) {
|
loadFormData()
|
}
|
})
|
}
|
}, { immediate: true })
|
|
// 方法定义
|
const resetForm = () => {
|
Object.assign(form, {
|
groupName: '',
|
groupCode: '',
|
groupType: '生产线',
|
description: '',
|
sortOrder: 0,
|
groupStatus: 'ENABLED',
|
executionMode: 'SERIAL',
|
maxDeviceCount: 100,
|
heartbeatInterval: 30,
|
connectionTimeout: 10,
|
retryCount: 3,
|
enableAlert: false,
|
offlineThreshold: 5,
|
responseTimeout: 30,
|
alertNotificationMethod: [],
|
notifyUsers: [],
|
webhookUrl: '',
|
dataCollectionInterval: 5,
|
dataRetentionPeriod: 30,
|
logLevel: 'INFO',
|
enableAutoBackup: false,
|
backupInterval: 24,
|
customParams: {}
|
})
|
customParamsText.value = ''
|
validationResult.value = null
|
formRef.value?.clearValidate()
|
}
|
|
const loadFormData = () => {
|
if (!props.data) return
|
|
// 转换后端状态字段到前端格式
|
// 后端 status: 0=停用, 1=启用, 2=维护中
|
// 前端 groupStatus: "ENABLED"/"DISABLED"/"MAINTENANCE"
|
let groupStatus = 'ENABLED'
|
if (props.data.status !== undefined) {
|
// 优先使用后端的 status 字段
|
if (props.data.status === 1) {
|
groupStatus = 'ENABLED'
|
} else if (props.data.status === 2) {
|
groupStatus = 'MAINTENANCE'
|
} else {
|
groupStatus = 'DISABLED'
|
}
|
} else if (props.data.groupStatus) {
|
// 兼容前端已有的 groupStatus 字段
|
groupStatus = props.data.groupStatus
|
}
|
|
Object.assign(form, {
|
groupName: props.data.groupName || '',
|
groupCode: props.data.groupCode || '',
|
groupType: props.data.groupType || '生产线',
|
description: props.data.description || '',
|
sortOrder: props.data.sortOrder || 0,
|
groupStatus: groupStatus,
|
maxDeviceCount: props.data.maxDeviceCount || 100,
|
heartbeatInterval: props.data.heartbeatInterval || 30,
|
connectionTimeout: props.data.connectionTimeout || 10,
|
retryCount: props.data.retryCount || 3,
|
enableAlert: props.data.enableAlert || false,
|
offlineThreshold: props.data.offlineThreshold || 5,
|
responseTimeout: props.data.responseTimeout || 30,
|
alertNotificationMethod: props.data.alertNotificationMethod || [],
|
notifyUsers: props.data.notifyUsers || [],
|
webhookUrl: props.data.webhookUrl || '',
|
dataCollectionInterval: props.data.dataCollectionInterval || 5,
|
dataRetentionPeriod: props.data.dataRetentionPeriod || 30,
|
logLevel: props.data.logLevel || 'INFO',
|
enableAutoBackup: props.data.enableAutoBackup || false,
|
backupInterval: props.data.backupInterval || 24,
|
customParams: props.data.customParams || props.data.extraConfig ? (typeof props.data.extraConfig === 'string' ? JSON.parse(props.data.extraConfig) : props.data.extraConfig) : {}
|
})
|
|
// 从customParams或extraConfig中读取executionMode
|
let executionMode = 'SERIAL' // 默认串行
|
if (form.customParams && form.customParams.executionMode) {
|
executionMode = form.customParams.executionMode
|
} else if (props.data.extraConfig) {
|
try {
|
const extraConfig = typeof props.data.extraConfig === 'string' ? JSON.parse(props.data.extraConfig) : props.data.extraConfig
|
if (extraConfig.executionMode) {
|
executionMode = extraConfig.executionMode
|
}
|
} catch (e) {
|
console.warn('解析extraConfig失败:', e)
|
}
|
}
|
form.executionMode = executionMode
|
|
customParamsText.value = JSON.stringify(form.customParams, null, 2)
|
}
|
|
const generateCode = () => {
|
if (!form.groupName || isEdit.value) return
|
|
const pinyin = require('pinyin-pro')
|
let code = pinyin.pinyin(form.groupName, {
|
toneType: 'none',
|
type: 'array'
|
}).join('').replace(/[^a-zA-Z0-9]/g, '')
|
|
code = code.toUpperCase()
|
if (code.length > 0) {
|
form.groupCode = code
|
}
|
}
|
|
const validateCustomParams = () => {
|
if (!customParamsText.value.trim()) {
|
validationResult.value = { valid: true, errors: [] }
|
return
|
}
|
|
try {
|
const params = JSON.parse(customParamsText.value)
|
if (typeof params === 'object' && params !== null) {
|
form.customParams = params
|
validationResult.value = { valid: true, errors: [] }
|
} else {
|
throw new Error('自定义参数必须为JSON对象')
|
}
|
} catch (error) {
|
validationResult.value = {
|
valid: false,
|
errors: [`自定义参数格式错误: ${error.message}`]
|
}
|
}
|
}
|
|
const testConfiguration = async () => {
|
try {
|
testing.value = true
|
|
// 验证表单
|
await formRef.value.validate()
|
|
// 验证自定义参数
|
validateCustomParams()
|
|
if (validationResult.value && !validationResult.value.valid) {
|
ElMessage.error('请修正配置验证错误')
|
return
|
}
|
|
const config = {
|
...form,
|
customParams: form.customParams
|
}
|
|
const response = await deviceGroupApi.testConfiguration(config)
|
|
if (response.success) {
|
validationResult.value = { valid: true, errors: [] }
|
ElMessage.success('配置测试通过')
|
} else {
|
validationResult.value = {
|
valid: false,
|
errors: response.errors || ['配置测试失败']
|
}
|
ElMessage.error('配置测试失败')
|
}
|
} catch (error) {
|
console.error('测试配置失败:', error)
|
if (error.message && error.message.includes('验证失败')) {
|
ElMessage.error('请检查表单填写是否正确')
|
} else {
|
ElMessage.error('测试配置失败')
|
}
|
} finally {
|
testing.value = false
|
}
|
}
|
|
const submit = async () => {
|
try {
|
saving.value = true
|
|
// 验证表单
|
await formRef.value.validate()
|
|
// 验证自定义参数
|
validateCustomParams()
|
|
if (validationResult.value && !validationResult.value.valid) {
|
ElMessage.error('请修正配置验证错误')
|
return
|
}
|
|
// 转换前端状态字段到后端格式
|
// 前端 groupStatus: "ENABLED"/"DISABLED"/"MAINTENANCE"
|
// 后端 status: 0=停用, 1=启用, 2=维护中
|
let status = 1 // 默认启用
|
if (form.groupStatus === 'ENABLED') {
|
status = 1
|
} else if (form.groupStatus === 'MAINTENANCE') {
|
status = 2
|
} else {
|
status = 0
|
}
|
|
// 将executionMode保存到customParams中,以便后端从extraConfig中读取
|
const customParams = {
|
...form.customParams,
|
executionMode: form.executionMode
|
}
|
|
const config = {
|
...form,
|
status: status, // 后端需要的 status 字段
|
customParams: customParams,
|
extraConfig: JSON.stringify(customParams) // 后端使用extraConfig字段
|
}
|
// 移除前端专用的 groupStatus 和 executionMode 字段,避免后端混淆
|
delete config.groupStatus
|
delete config.executionMode
|
|
const response = isEdit.value
|
? await deviceGroupApi.update(props.data.id, config)
|
: await deviceGroupApi.create(config)
|
|
const ok = response && (response.success || response.code === 200 || response.isSuccess)
|
if (ok) {
|
ElMessage.success(isEdit.value ? '设备组更新成功' : '设备组创建成功')
|
emit('success', isEdit.value ? 'update' : 'create')
|
handleClose()
|
} else {
|
ElMessage.error(response?.message || (isEdit.value ? '更新失败' : '创建失败'))
|
}
|
} catch (error) {
|
console.error('保存配置失败:', error)
|
ElMessage.error(error.message || (isEdit.value ? '更新失败' : '创建失败'))
|
} finally {
|
saving.value = false
|
}
|
}
|
|
const handleClose = () => {
|
emit('update:visible', false)
|
emit('close')
|
}
|
|
// 监听自定义参数变化
|
watch(customParamsText, () => {
|
if (showAdvanced.value) {
|
validateCustomParams()
|
}
|
})
|
</script>
|
|
<style scoped>
|
.device-group-form {
|
max-height: 60vh;
|
overflow-y: auto;
|
padding-right: 10px;
|
}
|
|
.form-card {
|
margin-bottom: 20px;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.form-tip {
|
font-size: 12px;
|
color: #909399;
|
margin-top: 4px;
|
line-height: 1.4;
|
}
|
|
.validation-result {
|
margin: 16px 0;
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
/* 自定义滚动条 */
|
.device-group-form::-webkit-scrollbar {
|
width: 6px;
|
}
|
|
.device-group-form::-webkit-scrollbar-track {
|
background: #f1f1f1;
|
border-radius: 3px;
|
}
|
|
.device-group-form::-webkit-scrollbar-thumb {
|
background: #c0c4cc;
|
border-radius: 3px;
|
}
|
|
.device-group-form::-webkit-scrollbar-thumb:hover {
|
background: #909399;
|
}
|
|
/* 表单元素样式调整 */
|
:deep(.el-card__body) {
|
padding: 20px;
|
}
|
|
:deep(.el-form-item) {
|
margin-bottom: 18px;
|
}
|
|
:deep(.el-input-number) {
|
width: 100%;
|
}
|
|
:deep(.el-textarea__inner) {
|
font-family: 'Microsoft YaHei', sans-serif;
|
}
|
|
/* 高级配置展开收起动画 */
|
.advanced-config {
|
transition: all 0.3s ease;
|
}
|
</style>
|