changan-inspection/src/views/flow/components/params/HttpNodeParams.vue
2026-07-01 15:05:08 +08:00

654 lines
27 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>
<el-form :inline="true" :model="httpNodeData" :rules="rules" class="http-form" ref="dynamicFormRef"
label-position="top" label-width="auto">
<el-collapse v-model="activeNames">
<el-collapse-item name="1" icon-position="left">
<template #title>
请求配置
<el-tooltip class="box-item" effect="dark" content="支持HTTP/HTTPS协议" placement="top">
<el-icon class="header-icon" style="margin-left: 6px">
<info-filled />
</el-icon>
</el-tooltip>
</template>
<div class="form__container">
<div v-for="(property, index) in httpNodeData.config" :key="index">
<el-row>
<el-form-item :label="index === 0 ? '参数名' : ''" :prop="`config.${index}.name`" :rules="[
{ required: true, message: '请输入参数名', trigger: 'blur' },
]">
<el-input :disabled="true" class="param-name" v-model="property.name" placeholder="请输入"
clearable />
</el-form-item>
<el-form-item :label="index === 0 ? '参数值' : ''" :prop="`config.${index}.input`">
<el-select v-model="property.type" class="param-type" @change="handleTypeChange(index)">
<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="`config.${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'">
<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="`config.${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)" />
</el-form-item>
</el-form-item>
</el-row>
</div>
</div>
</el-collapse-item>
<el-collapse-item name="2" icon-position="left">
<template #title>
Header配置
<el-button :circle="true" size="small" @click="(e) => addHttpNodeFormItem(e, 'headers')"
class="addFormItem">
<el-icon :size="14">
<Plus />
</el-icon>
</el-button>
</template>
<div class="form__container">
<div v-for="(property, index) in httpNodeData.headers" :key="index">
<el-row>
<el-form-item :label="index === 0 ? '参数名' : ''"
:prop="httpNodeData.headers.length > index ? `headers.${index}.name` : undefined"
:rules="[
{ required: true, message: '请输入参数名', trigger: 'blur' },
]">
<el-input class="param-name" v-model="property.name" placeholder="请输入" clearable />
</el-form-item>
<el-form-item :label="index === 0 ? '参数值' : ''"
:prop="httpNodeData.headers.length > index ? `headers.${index}.input` : undefined">
<el-select v-model="property.type" class="param-type" @change="handleTypeChange(index)">
<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="`headers.${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'">
<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="`headers.${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)" />
</el-form-item>
</el-form-item>
<el-button :icon="Minus" circle size="small" @click="handleHttpNodeDelete('headers', index)"
class="deleteBtn" />
</el-row>
</div>
</div>
</el-collapse-item>
<el-collapse-item name="3" icon-position="left">
<template #title>
Param配置
<el-button :circle="true" size="small" @click="(e) => addHttpNodeFormItem(e, 'params')"
class="addFormItem">
<el-icon :size="14">
<Plus />
</el-icon>
</el-button>
</template>
<div class="form__container">
<div v-for="(property, index) in httpNodeData.params" :key="index">
<el-row>
<el-form-item :label="index === 0 ? '参数名' : ''"
:prop="httpNodeData.params.length > index ? `params.${index}.name` : undefined" :rules="[
{ required: true, message: '请输入参数名123', trigger: 'blur' },
]">
<el-input class="param-name" v-model="property.name" placeholder="请输入" clearable />
</el-form-item>
<el-form-item :label="index === 0 ? '参数值' : ''"
:prop="httpNodeData.params.length > index ? `params.${index}.input` : undefined">
<el-select v-model="property.type" class="param-type" @change="handleTypeChange(index)">
<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="httpNodeData.params.length > index ? `params.${index}.input` : undefined">
<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'">
<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="`params.${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)" />
</el-form-item>
</el-form-item>
<el-button :icon="Minus" circle size="small" @click="handleHttpNodeDelete('params', index)"
class="deleteBtn" />
</el-row>
</div>
</div>
</el-collapse-item>
<el-collapse-item name="4" title="Body配置" icon-position="left">
<template #title>
Body配置
<el-button
:circle="true"
size="small"
@click="(e) => addHttpNodeFormItem(e, 'body')"
class="addFormItem"
v-if="httpNodeData?.body?.type === 'form-data'"
>
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</template>
<div class="form__container">
<el-select v-model="httpNodeData.body.type">
<el-option label="JSON" value="json" />
<el-option label="form-data" value="form-data" />
</el-select>
<div class="code__container" v-show="httpNodeData?.body?.type === 'json'">
<div ref="codeRef" style="width: 100%; height: 100%"></div>
</div>
<div class="test" v-if="httpNodeData?.body?.type === 'form-data'">
<div v-for="(property, index) in httpNodeData.body.formData" :key="index">
<el-row>
<el-form-item
:label="index === 0 ? '参数名' : ''"
:prop="`body.formData.${index}.name`"
:rules="[
{ required: true, message: '请输入参数名', trigger: 'blur' },
]"
>
<el-input
class="param-name"
v-model="property.name"
placeholder="请输入"
clearable
/>
</el-form-item>
<el-form-item
:label="index === 0 ? '参数值' : ''"
:prop="`body.formData.${index}.input`"
>
<el-select
v-model="property.type"
class="param-type"
@change="handleTypeChange(index, 'httpBody')"
>
<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="`body.formData.${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'"
>
<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="`body.formData.${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, 'httpBody')
"
@change="(value) => cascaderChange(value, index, 'httpBody')"
/>
</el-form-item>
</el-form-item>
<el-button
:icon="Minus"
circle
size="small"
@click="handleHttpNodeDelete('body', index)"
class="deleteBtn"
/>
</el-row>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</el-form>
</template>
<script setup>
import { ref, reactive, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue'
import * as monaco from 'monaco-editor';
import { getInput, transformHttpNodeData } 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 codeRef = ref()
let editorInstance
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);
}
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: '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,
},
});
}
// 设置代码值
const setValue = (value) => {
if (editorInstance) {
editorInstance.setValue(value)
}
}
// 销毁编辑器
const disposeEditor = () => {
if (editorInstance) {
editorInstance.dispose()
editorInstance = null
}
}
const rules = reactive({})
const dynamicFormRef = ref()
const activeNames = ref(['1', '2', '3', '4'])
// 使用共享的 quote 逻辑
const { quoteOptions, cascaderRefs, handleTypeChange, cascaderChange, visibleChange } = useQuote(props.data.id)
// 添加表单项
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 handleDelete = (index) => {
formData.nodeParams.splice(index, 1)
}
// 删除顶层输出参数(递归删除通过 FormItemRecursive 的 emit 处理)
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)
}
// HTTP 节点数据
const httpNodeData = ref({
body: {
bodyType: 'json',
json: '{}',
formData: []
}
});
const addHttpNodeFormItem = (e, type) => {
e.stopPropagation();
if (type === 'body') {
if (!httpNodeData.value.body.formData) {
httpNodeData.value.body.formData = [];
}
httpNodeData.value.body.formData.push({ name: "", type: "input", input: ""});
} else {
if (!httpNodeData.value[type]) {
httpNodeData.value[type] = [];
}
httpNodeData.value[type].push({ name: "", type: "input", input: ""});
}
};
const handleHttpNodeDelete = (type, index) => {
if (type === 'body') {
httpNodeData.value.body.formData.splice(index, 1);
} else {
httpNodeData.value[type].splice(index, 1);
}
};
// 初始化数据
const initData = () => {
const nodeParams = JSON.parse(JSON.stringify(props.data.properties.nodeParams || []))
const transformedData = transformHttpNodeData(nodeParams);
httpNodeData.value = transformedData;
if (httpNodeData.value.body.type === 'json') {
nextTick(() => {
initEditor('http');
});
setTimeout(() => {
if (editorInstance) {
setValue(httpNodeData.value.body.json || '{}')
}
}, 200)
}
}
// 保存验证
const validateAndSave = async () => {
let inputValid = true
if (dynamicFormRef.value) {
await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false })
}
const nodeParams = []
nodeParams.push({ name: "config", type: "input", input: "", children: httpNodeData.value.config, disabled: true, required: true })
if (httpNodeData.value?.headers?.length > 0) {
nodeParams.push({ name: "headers", type: "input", input: "", children: httpNodeData.value.headers, disabled: true, required: true })
}
if (httpNodeData.value?.params?.length > 0) {
nodeParams.push({ name: "params", type: "input", input: "", children: httpNodeData.value.params, disabled: true, required: true })
}
const bodyObj = {
name: "body", type: "input", input: "", children: [
{ name: 'bodyType', type: "input", input: httpNodeData.value.body.type, disabled: true },
{ name: 'json', type: "input", input: httpNodeData.value.body.type === 'json' ? editorInstance.getValue() : '' },
{ name: 'formData', type: "input", input: "", children: httpNodeData.value.body.formData }
]
}
nodeParams.push(bodyObj)
// 保存到 lf
lf.setProperties(props.data.id, {
...props.data.properties,
nodeParams
})
if (httpNodeData.value.body.type === 'json') {
if (hasErrors()) {
emit('save-error')
return
}
}
if (inputValid) {
emit('save-success')
} else {
emit('save-error')
}
}
onMounted(() => {
nextTick(() => {
initData()
})
})
defineExpose({ validateAndSave })
onBeforeUnmount(() => {
disposeEditor()
})
</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;
}
}
.code__container {
display: flex;
position: relative;
text-align: initial;
width: 100%;
height: 300px;
margin: 12px 0;
}
</style>