CMVR-IOT-UI/src/views/flow/components/params/DeviceUniversal.vue
zhanghao d733ea8525 feat: 增强流资源管理和节点执行组件
- 增加了检测流资源中引用循环的功能,以防止循环依赖。
- 改进了流引用的验证,以确保节点输出中不存在循环依赖。
- 增强了资源定义清理功能,以便在执行前移除过时的设备ID。
- 已实现复制资源角色的合并,以优化现有资源的优先级并避免重复。
- 更新了LoopIterationResults组件,以管理运行中迭代的自动扩展。
- 增强了NodeState和NodeTitle组件,以支持循环和子流结果的更宽弹出框显示。
- 优化了ParamsDrawer和SubFlowExecutionResults组件,以处理动态参数更新和执行结果。
- 改进了TaskExecutionLog,以统一输出处理并增强用户对日志的可见性。
- 更新了各种参数组件,以简化输入处理并提升用户体验。
- 在代码中添加了全面的注释和文档,以提高可维护性和易理解性。
2026-08-21 15:01:16 +08:00

521 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
* 文件说明:通用设备参数面板,选择设备并配置脚本和参数。
* 作用范围:仅服务于流程设计器模块。
-->
<template>
<RobotExecutionTarget :data="props.data" v-model="formData.executionTarget" />
<el-collapse v-model="activeNames">
<el-collapse v-model="activeNames">
<el-collapse-item name="1" 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>
</template>
<div class="form__container">
<el-form
:inline="true"
:model="formData"
:rules="rules"
ref="dynamicFormRef"
label-position="top"
label-width="auto"
>
<div v-for="(property, index) in formData.nodeParams" :key="index">
<el-row v-if="property.name !== 'requestJson'">
<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(property)"
>
<el-option label="引用" value="quote" />
<el-option label="输入" value="input" />
</el-select>
<el-form-item
v-if="property.type === 'input'"
:rules="[
{
required: property?.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="false"
:step-strictly="true"
placeholder="请输入"
clearable
/>
<el-select
v-model="property.input"
class="param-value"
v-else-if="property.componentType === 'select'"
:disabled="property.deviceLocked"
no-data-text="请先选择机器人设备"
>
<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"
:props="{ checkStrictly: true }"
:options="quoteOptions"
placeholder="请选择"
@visible-change="
(visible) => visibleChange(visible, index, property.quote)
"
@change="(value) => cascaderChange(value, property)"
/>
</el-form-item>
</el-form-item>
<el-button
:icon="Minus"
circle
size="small"
@click="handleDelete(index)"
:disabled="property?.disabled || false"
class="deleteBtn"
/>
</el-row>
</div>
</el-form>
</div>
</el-collapse-item>
<el-collapse-item title="JSON" name="2" icon-position="left">
<div class="code__container">
<div ref="codeRef" style="width: 100%; height: 100%"></div>
</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) => addFormItem(e, 'outputParams')"
class="addFormItem"
>
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</template>
<div class="form__container">
<el-form
:inline="true"
:model="formData"
label-position="top"
label-width="auto"
:rules="outputRules"
ref="outputFormRef"
>
<FormItemRecursive
formType="output"
:current-list="formData.outputParams"
prop-path="outputParams"
:depth="0"
:is-first-level="true"
:endDepth="2"
:parent-path="[]"
@delete-item="(path) => deleteTopLevelItem(path, 'outputParams')"
/>
</el-form>
</div>
</el-collapse-item>
</el-collapse>
</el-collapse>
</template>
<script setup>
import { ref, reactive, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue'
import RobotExecutionTarget from './RobotExecutionTarget.vue'
import * as monaco from 'monaco-editor';
import { useQuote } from './useQuote.js'
import { ElMessage } from 'element-plus'
const props = defineProps({
data: Object
})
const emit = defineEmits(['save-success', 'save-error'])
const formData = reactive({
nodeParams: [],
outputParams: [],
executionTarget: {}
})
const codeRef = ref()
let editorInstance
/**
* 判断 hasErrors 对应的数据或交互;作用范围仅限当前组件或模块。
*/
function hasErrors() {
const model = editorInstance.getModel();
if (!model) return false;
// 获取当前模型的所有标记
const markers = monaco.editor.getModelMarkers({ resource: model.uri });
// 检查是否存在错误级别标记
return markers.some(marker => marker.severity === monaco.MarkerSeverity.Error);
}
/**
* 初始化 initEditor 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} type 目标类型
*/
const initEditor = (type) => {
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true, // 允许非 ts 扩展名(.js)
checkJs: true, // 对 .js 文件进行类型检查
strict: true, // 启用所有严格检查
noImplicitAny: false, // 禁止隐式 any
noUnusedLocals: true, // 未使用的局部变量报错(可选)
noUnusedParameters: true, // 未使用的参数报错(可选)
});
// 确保语义验证开启(默认就是开启的,但可以显式设置)
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false, // 开启语义检查
noSyntaxValidation: false, // 开启语法检查
});
editorInstance = monaco.editor.create(codeRef.value, {
value: ['{}'].join('\n'),
language: type === 'code' ? 'javascript' : 'json',
fontSize: 14,
lineNumbers: 'on',
roundedSelection: true,
scrollBeyondLastLine: false,
formatOnPaste: true,
formatOnType: true,
quickSuggestions: true,
suggestOnTriggerCharacters: true,
// 自动完成
suggest: {
showWords: true,
showFunctions: true,
showVariables: true,
showClasses: true,
showModules: true,
},
});
}
// 设置代码值
/**
* 设置 setValue 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} value 待处理的新值
*/
const setValue = (value) => {
if (editorInstance) {
editorInstance.setValue(value)
}
}
// 销毁编辑器
/**
* 销毁 disposeEditor 对应的数据或交互;作用范围仅限当前组件或模块。
*/
const disposeEditor = () => {
if (editorInstance) {
editorInstance.dispose()
editorInstance = null
}
}
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)
// 添加表单项
/**
* 新增 addFormItem 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} e 触发操作的浏览器事件
* @param {*} type 目标类型
*/
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)
}
// 删除输入参数(非递归)
/**
* 处理 handleDelete 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} index 目标项索引
*/
const handleDelete = (index) => {
formData.nodeParams.splice(index, 1)
}
// 删除顶层输出参数(递归删除通过 FormItemRecursive 的 emit 处理)
/**
* 删除 deleteTopLevelItem 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} fullPath 字段完整路径
* @param {*} propPath 字段校验路径
*/
const deleteTopLevelItem = (fullPath, propPath) => {
let currentLevel = formData[propPath]
for (let i = 0; i < fullPath.length - 1; i++) {
currentLevel = currentLevel[fullPath[i]].children
}
const lastIndex = fullPath[fullPath.length - 1]
currentLevel.splice(lastIndex, 1)
}
// 初始化数据
/**
* 初始化 initData 对应的数据或交互;作用范围仅限当前组件或模块。
*/
const initData = () => {
const properties = props.data?.properties || {}
const nodeParams = JSON.parse(JSON.stringify(properties.nodeParams || []))
const outputParams = JSON.parse(JSON.stringify(properties.outputParams || []))
formData.nodeParams = nodeParams;
formData.outputParams = outputParams;
formData.executionTarget = JSON.parse(JSON.stringify(properties.executionTarget || {}));
const codeParams = nodeParams.find(item => item.name === 'requestJson')
if (codeParams) {
nextTick(() => {
if (editorInstance) {
setValue(codeParams.input)
}
})
}
}
watch(
() => props.data,
initData,
{ immediate: true, deep: true }
)
// 保存验证
/**
* 校验 validateAndSave 对应的数据或交互;作用范围仅限当前组件或模块。
*/
const validateAndSave = async () => {
const targetObj = formData.nodeParams.find(item => item.name === 'requestJson');
if (targetObj) {
// 存在:修改该对象的 input 属性
targetObj.input = editorInstance.getValue();
} else {
formData.nodeParams.push(
{ name: "requestJson", type: "input", input: editorInstance.getValue() }
)
}
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 })
}
if (!formData.executionTarget.roleKey || !formData.executionTarget.deviceSlotKey) {
inputValid = false
ElMessage.warning('请选择执行角色和设备用途')
}
// 保存到 lf
lf.setProperties(props.data.id, {
...props.data.properties,
...formData
})
if (hasErrors()) {
emit('save-error')
return
}
if (inputValid && outputValid) {
emit('save-success')
} else {
emit('save-error')
}
}
onMounted(() => {
nextTick(() => {
initEditor('requestJson')
const codeParams = formData.nodeParams.find(item => item.name === 'requestJson')
if (codeParams) setValue(codeParams.input)
})
})
defineExpose({ validateAndSave })
onBeforeUnmount(() => {
disposeEditor()
})
</script>
<style lang="scss" scoped>
.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;
}
}
.code__container {
display: flex;
position: relative;
text-align: initial;
width: 100%;
height: 300px;
margin: 12px 0;
}
</style>