CMVR-IOT-UI/src/views/flow/components/params/RecognizeNodeParams.vue

388 lines
14 KiB
Vue
Raw Normal View History

2026-08-07 14:38:16 +08:00
<!--
* 文件说明识别节点参数面板配置识别条件和告警规则
* 作用范围仅服务于流程设计器模块
-->
2026-07-28 09:22:22 +08:00
<template>
<el-form :inline="true" :model="RecognizeNodeData" :rules="rules" ref="dynamicFormRef" class="http-form"
label-position="top" label-width="auto">
<el-collapse v-model="activeNames">
<el-collapse-item name="1" title="识别配置" icon-position="left">
<div v-for="(property, index) in RecognizeNodeData.nodeParams" class="form__container" :key="index">
<el-row>
<el-form-item :label="index === 0 ? '参数名' : ''" :prop="`nodeParams.${index}.name`"
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]">
<el-input :disabled="property?.disabled || false" class="param-name" v-model="property.name"
placeholder="请输入" clearable />
</el-form-item>
<el-form-item :label="index === 0 ? '参数值' : ''" :prop="`nodeParams.${index}.type`">
<el-select v-model="property.type" class="param-type"
@change="handleTypeChange(index, RecognizeNodeData, property.type)">
<el-option label="引用" value="quote" />
<el-option label="输入" value="input" />
</el-select>
<el-form-item v-if="property.type === 'input'" :rules="{ required: true, message: '请输入参数值', trigger: 'blur' }"
:prop="`nodeParams.${index}.input`">
<el-input-number class="param-value" v-if="property.componentType === 'number'"
v-model="property.input" :min="0" :max="property.max || Infinity"
:controls="property?.controls || true" :step-strictly="true"
:step="property.step || 1" placeholder="请输入" clearable />
<el-select v-model="property.input" class="param-value"
v-else-if="property.componentType === 'select'">
<el-option v-for="item in property.selectOptions" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
<el-input class="param-value" v-else v-model="property.input" placeholder="请输入"
clearable />
</el-form-item>
<el-form-item v-if="property.type === 'quote'"
:rules="[{ required: true, message: '请选择参数值', trigger: 'blur' }]"
:prop="`nodeParams.${index}.quote`">
<el-cascader :ref="(el) => { if (el) cascaderRefs[index] = el }"
v-model="property.quote" :checkStrictly="true" :options="quoteOptions"
placeholder="请选择"
@visible-change="(visible) => visibleChange(visible, index, property.quote)"
@change="(value) => cascaderChange(value, index, RecognizeNodeData, 'nodeParams')" />
</el-form-item>
</el-form-item>
</el-row>
</div>
</el-collapse-item>
<el-collapse-item name="2" title="告警级别" icon-position="left">
<template #title>
告警级别
<el-button :circle="true" size="small" @click="addAlarmRule" class="addFormItem">
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</template>
<div class="form__container alarm-rules">
<el-row v-for="(rule, index) in RecognizeNodeData.alarmRules" :key="index">
<el-form-item label="告警级别" :prop="`alarmRules.${index}.level`"
:rules="[{ required: true, message: '请输入告警级别', trigger: 'change' }]">
<el-input-number v-model="rule.level" :min="1" :max="3" :step="1"
:step-strictly="true" />
</el-form-item>
<el-form-item label="比较符" :prop="`alarmRules.${index}.operator`"
:rules="[{ required: true, message: '请选择比较符', trigger: 'change' }]">
<el-select v-model="rule.operator" placeholder="请选择">
<el-option v-for="operator in alarmOperators" :key="operator.value"
:label="operator.label" :value="operator.value" />
</el-select>
</el-form-item>
<el-form-item label="阈值" :prop="`alarmRules.${index}.threshold`"
:rules="[{ required: true, message: '请输入阈值', trigger: 'change' }]">
<el-input-number v-model="rule.threshold" :controls="false" />
</el-form-item>
<el-form-item label="告警信息" :prop="`alarmRules.${index}.message`"
:rules="[{ required: true, message: '请输入告警信息', trigger: 'blur' }]">
<el-input v-model="rule.message" placeholder="请输入告警信息" clearable />
</el-form-item>
<el-button type="danger" link class="deleteBtn" @click="deleteAlarmRule(index)">
<el-icon><Minus /></el-icon>
</el-button>
</el-row>
<el-empty v-if="!RecognizeNodeData.alarmRules.length" description="暂无告警规则" :image-size="60" />
</div>
</el-collapse-item>
<el-collapse-item name="3" icon-position="left">
<template #title>
输出参数
<el-tooltip class="box-item" effect="dark" content="这里定义的输出变量可以被后续节点引用。" placement="top">
<el-icon class="header-icon" style="margin-left: 6px">
<info-filled />
</el-icon>
</el-tooltip>
<el-button :circle="true" size="small" @click="(e) => addRecognizeItem(e)" class="addFormItem">
<el-icon :size="14">
<Plus />
</el-icon>
</el-button>
</template>
<div class="form__container">
<el-form :inline="true" :model="RecognizeNodeData" label-position="top" label-width="auto"
:rules="outputRules" ref="outputFormRef">
<FormItemRecursive formType="output" :current-list="RecognizeNodeData.outputParams"
prop-path="outputParams" :depth="0" :is-first-level="true" :endDepth="2" :parent-path="[]"
@delete-item="(path) => deleteRecognizeItem(path, 'outputParams')" />
</el-form>
</div>
</el-collapse-item>
</el-collapse>
</el-form>
</template>
<script setup>
import { ref, reactive, watch, nextTick } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue'
import { transformRecognizeNodeData } from '@/utils/flow'
import { useQuote } from './useQuote.js'
const props = defineProps({
data: Object
})
const emit = defineEmits(['save-success', 'save-error'])
const formData = reactive({
nodeParams: [],
outputParams: []
})
const rules = reactive({})
const outputRules = reactive({})
const dynamicFormRef = ref()
const outputFormRef = ref()
const activeNames = ref(['1', '2'])
// 使用共享的 quote 逻辑
const { quoteOptions, cascaderRefs, handleTypeChange, cascaderChange, visibleChange } = useQuote(props.data.id)
// 添加表单项
2026-08-07 14:38:16 +08:00
/**
* 新增 addFormItem 对应的数据或交互作用范围仅限当前组件或模块
* @param {*} e 触发操作的浏览器事件
* @param {*} type 目标类型
*/
2026-07-28 09:22:22 +08:00
const addFormItem = (e, type) => {
e.stopPropagation()
const obj = {
name: '',
type: type === 'nodeParams' ? 'input' : 'string',
required: false,
children: [],
input: ''
}
if (!formData[type]) formData[type] = []
formData[type].push(obj)
}
// 商道智能体
const RecognizeNodeData = ref({})
const alarmOperators = [
{ label: '大于GT', value: 'GT' },
{ label: '大于等于GTE', value: 'GTE' },
{ label: '小于LT', value: 'LT' },
{ label: '小于等于LTE', value: 'LTE' },
{ label: '等于EQ', value: 'EQ' },
{ label: '不等于NE', value: 'NE' }
]
2026-08-07 14:38:16 +08:00
/**
* 新增 addAlarmRule 对应的数据或交互作用范围仅限当前组件或模块
* @param {*} e 触发操作的浏览器事件
*/
2026-07-28 09:22:22 +08:00
const addAlarmRule = (e) => {
e.stopPropagation()
RecognizeNodeData.value.alarmRules.push({
level: 1,
operator: 'GTE',
threshold: 0,
message: ''
})
}
2026-08-07 14:38:16 +08:00
/**
* 删除 deleteAlarmRule 对应的数据或交互作用范围仅限当前组件或模块
* @param {*} index 目标项索引
*/
2026-07-28 09:22:22 +08:00
const deleteAlarmRule = (index) => {
RecognizeNodeData.value.alarmRules.splice(index, 1)
}
2026-08-07 14:38:16 +08:00
/**
* 新增 addRecognizeItem 对应的数据或交互作用范围仅限当前组件或模块
* @param {*} e 触发操作的浏览器事件
*/
2026-07-28 09:22:22 +08:00
const addRecognizeItem = (e) => {
e.stopPropagation();
RecognizeNodeData.value.outputParams.push({ name: "", type: "string", input: "" });
}
2026-08-07 14:38:16 +08:00
/**
* 删除 deleteRecognizeItem 对应的数据或交互作用范围仅限当前组件或模块
* @param {*} fullPath 字段完整路径
* @param {*} propPath 字段校验路径
*/
2026-07-28 09:22:22 +08:00
const deleteRecognizeItem = (fullPath, propPath) => {
// 从顶层数据开始查找
let currentLevel = RecognizeNodeData.value[propPath];
// 遍历路径(除最后一个索引,因为最后一个是要删除的项)
for (let i = 0; i < fullPath.length - 1; i++) {
const index = fullPath[i];
// 进入下一层级
currentLevel = currentLevel[index].children;
}
// 最后一个索引是当前层级要删除的项
const lastIndex = fullPath[fullPath.length - 1];
currentLevel.splice(lastIndex, 1);
}
// 初始化数据
2026-08-07 14:38:16 +08:00
/**
* 初始化 initData 对应的数据或交互作用范围仅限当前组件或模块
*/
2026-07-28 09:22:22 +08:00
const initData = () => {
const nodeParams = JSON.parse(JSON.stringify(props.data.properties.nodeParams || []))
const outputParams = JSON.parse(JSON.stringify(props.data.properties.outputParams || []))
const transformedData = transformRecognizeNodeData(nodeParams);
RecognizeNodeData.value = {
...transformedData,
outputParams
};
}
watch(
() => props.data,
() => {
initData()
},
{ immediate: true, deep: true }
)
// 保存验证
2026-08-07 14:38:16 +08:00
/**
* 校验 validateAndSave 对应的数据或交互作用范围仅限当前组件或模块
*/
2026-07-28 09:22:22 +08:00
const validateAndSave = async () => {
let inputValid = true
let outputValid = true
if (dynamicFormRef.value) {
await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false })
}
if (outputFormRef.value) {
await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false })
}
const nodeParams = []
nodeParams.push(...RecognizeNodeData.value.nodeParams)
nodeParams.push({
name: "alarmRules",
type: "input",
input: RecognizeNodeData.value.alarmRules.map(({ level, operator, threshold, message }) => ({
level,
operator,
threshold,
message
})),
disabled: true
})
lf.setProperties(props.data.id, {
...props.data.properties,
nodeParams,
outputParams: RecognizeNodeData.value.outputParams
});
if (inputValid && outputValid) {
emit('save-success')
} else {
emit('save-error')
}
}
defineExpose({ validateAndSave })
</script>
<style lang="scss" scoped>
.http-form {
width: 100%;
.el-collapse {
width: 100%;
}
}
.form__container {
margin: 12px 0;
:deep(.el-row) {
align-items: end;
.el-form-item {
margin-right: 12px;
}
.param-name {
width: 160px;
}
.param-type {
width: 80px;
}
.param-value {
width: 160px;
}
}
.sub-properties {
margin-left: 10px;
.zw {
margin-left: 15px;
&::before {
content: "";
position: absolute;
left: 0;
top: -4px;
width: 10px;
border-left: 1px solid gray;
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px;
background-color: transparent;
height: 24px;
}
}
}
.gSon-properties {
margin-left: 10px;
.zw {
margin-left: 15px;
&::before {
content: "";
position: absolute;
left: 0;
top: -4px;
width: 10px;
border-left: 1px solid gray;
border-bottom: 1px solid gray;
border-bottom-left-radius: 4px;
background-color: transparent;
height: 24px;
}
}
}
.deleteBtn {
margin-bottom: 22px;
cursor: pointer;
}
}
.alarm-rules {
:deep(.el-row) {
flex-wrap: nowrap;
.el-form-item {
flex: 1;
}
.el-select,
.el-input,
.el-input-number {
width: 100%;
}
}
}
</style>