Files
admin-ui/src/views/settings/workflow/component/NodeConfigPanel.vue
T

316 lines
9.2 KiB
Vue

<template>
<div class="config-panel">
<h3>节点配置</h3>
<div v-if="selectedNode">
<el-form label-position="top">
<el-form-item label="节点名称">
<el-input :model-value="selectedNode.data?.label" @update:model-value="updateNodeLabel" />
</el-form-item>
<el-form-item label="节点类型">
<el-tag>{{ selectedNode.data?.nodeCode }}</el-tag>
</el-form-item>
<!-- 模型选择 -->
<el-form-item v-if="nodeConfig?.modelConfig && nodeConfig.modelConfig.length > 0" label="选择模型">
<el-button type="primary" @click="emit('openModelSelector')" style="width: 100%">选择模型</el-button>
<div v-if="selectedNode.data?.modelConfig?.modelName" class="selected-tag">
<el-tag type="success" size="large" closable @close="emit('removeModel')">
{{ selectedNode.data.modelConfig.modelName }}
</el-tag>
</div>
</el-form-item>
<!-- 技能选择 -->
<el-form-item v-if="nodeConfig?.skillOption" label="选择技能">
<el-button type="primary" @click="emit('openSkillSelector')" style="width: 100%">选择技能</el-button>
<div v-if="selectedNode.data?.skillName" class="selected-tag">
<el-tag type="success" size="large" closable @close="emit('removeSkill')">
{{ selectedNode.data.skillName }}
</el-tag>
</div>
</el-form-item>
<!-- 贴片布局开关 -->
<el-form-item v-if="nodeConfig?.patchLayout" label="贴片布局">
<el-switch
:model-value="selectedNode.data?.patchLayout ?? false"
@update:model-value="emit('update:patchLayout', $event)"
/>
</el-form-item>
<!-- 动态表单字段 -->
<template v-if="nodeConfig?.formConfig && nodeConfig.formConfig.length > 0">
<el-divider content-position="left">节点参数</el-divider>
<el-form-item v-for="field in nodeConfig.formConfig" :key="field.field" :label="field.label" :required="field.required">
<el-input
v-if="field.type === 'input' || field.type === 'string'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
:placeholder="field.required ? '必填' : '选填'"
/>
<el-input-number
v-else-if="field.type === 'number' || field.type === 'inputNumber'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
class="w100"
/>
<el-input
v-else-if="field.type === 'textarea'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
type="textarea"
:rows="3"
:placeholder="field.required ? '必填' : '选填'"
/>
<el-switch
v-else-if="field.type === 'switch'"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
/>
<el-select
v-else-if="field.type === 'select' && field.options"
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
:placeholder="field.required ? '必填' : '选填'"
class="w100"
>
<el-option v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<KeyValueEditor
v-else-if="field.type === 'keyValue'"
:model-value="getFieldValue(field.field)"
@update:model-value="(v: any) => updateFieldValue(field.field, v)"
/>
<el-button
v-else-if="field.type === 'schemaJson'"
@click="openSchemaEditor(field.field)"
style="width: 100%"
>配置</el-button>
<el-input
v-else
:model-value="getFieldValue(field.field)"
@update:model-value="updateFieldValue(field.field, $event)"
:placeholder="field.required ? '必填' : '选填'"
/>
</el-form-item>
</template>
</el-form>
<!-- 上级参数管理 -->
<InputSourceManager
v-if="selectedNode.data?.nodeCode !== '__start__'"
:selected-node="selectedNode"
:nodes="allNodes"
:edges="allEdges"
@remove-field="(nodeId: string, fieldName: string) => emit('removeField', nodeId, fieldName)"
@toggle-output="(nodeId: string, enabled: boolean) => emit('toggleOutput', nodeId, enabled)"
@add-param="(paramValue: string) => emit('addParamByValue', paramValue)"
/>
</div>
<el-empty v-else description="请选择一个节点" :image-size="100" />
<!-- Schema 编辑器弹窗 -->
<el-dialog v-model="schemaEditorVisible" title="编辑 Schema" width="640px" append-to-body destroy-on-close>
<div style="height: 400px; overflow-y: auto">
<JsonEditor v-model="schemaEditorData" />
</div>
<template #footer>
<el-button @click="schemaEditorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmSchemaEditor">确 定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { Node } from '@vue-flow/core';
import InputSourceManager from './InputSourceManager.vue';
import KeyValueEditor from './KeyValueEditor.vue';
import { JsonEditor } from '/@/components/json-schema-editor';
interface NodeData {
label?: string;
nodeCode?: string;
inputSource?: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
formConfig?: any[];
modelConfig?: any;
skillName?: string;
patchLayout?: boolean;
}
interface ParamRef {
id: string;
label: string;
}
interface NodeConfig {
formConfig: any[];
modelConfig: any[];
skillOption: boolean;
patchLayout: boolean;
}
const props = defineProps<{
selectedNode: Node<NodeData, any, string> | null;
availableParams: ParamRef[];
nodeConfig: NodeConfig | null;
allNodes: Node<NodeData, any, string>[];
allEdges: any[];
}>();
const emit = defineEmits<{
(e: 'update:selectedNode', node: Node<NodeData, any, string>): void;
(e: 'addParam', param: ParamRef): void;
(e: 'openModelSelector'): void;
(e: 'removeModel'): void;
(e: 'openSkillSelector'): void;
(e: 'removeSkill'): void;
(e: 'removeField', nodeId: string, fieldName: string): void;
(e: 'toggleOutput', nodeId: string, enabled: boolean): void;
(e: 'addParamByValue', paramValue: string): void;
(e: 'update:patchLayout', value: boolean): void;
}>();
// Schema 编辑器弹窗
const schemaEditorVisible = ref(false);
const schemaEditorField = ref('');
const schemaEditorData = ref<any>({});
function openSchemaEditor(fieldName: string) {
schemaEditorField.value = fieldName;
const val = getFieldValue(fieldName);
schemaEditorData.value = val && typeof val === 'object' ? { ...val } : {};
schemaEditorVisible.value = true;
}
function confirmSchemaEditor() {
updateFieldValue(schemaEditorField.value, schemaEditorData.value);
schemaEditorVisible.value = false;
}
const updateNodeLabel = (newLabel: string) => {
if (!props.selectedNode?.data) return;
const updatedNode = {
...props.selectedNode,
data: {
...props.selectedNode.data,
label: newLabel,
},
};
emit('update:selectedNode', updatedNode);
};
const getFieldValue = (fieldName: string) => {
if (!props.selectedNode?.data?.formConfig) return '';
const field = props.selectedNode.data.formConfig.find((f: any) => f.field === fieldName);
return field?.value ?? '';
};
const updateFieldValue = (fieldName: string, value: any) => {
if (!props.selectedNode?.data) return;
const formConfig = props.selectedNode.data.formConfig || [];
const existingIndex = formConfig.findIndex((f: any) => f.field === fieldName);
let updatedFormConfig;
if (existingIndex >= 0) {
updatedFormConfig = [...formConfig];
updatedFormConfig[existingIndex] = { ...updatedFormConfig[existingIndex], value };
} else {
const fieldDef = props.nodeConfig?.formConfig.find((f) => f.field === fieldName);
if (fieldDef) {
updatedFormConfig = [...formConfig, { ...fieldDef, value }];
} else {
updatedFormConfig = formConfig;
}
}
const updatedNode = {
...props.selectedNode,
data: {
...props.selectedNode.data,
formConfig: updatedFormConfig,
},
};
emit('update:selectedNode', updatedNode);
};
</script>
<style scoped lang="scss">
.config-panel {
background: #ffffff;
border-radius: 10px;
padding: 16px;
overflow-y: auto;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
border: 1px solid #e6eaf0;
h3 {
margin: 0 0 16px 0;
font-size: 15px;
font-weight: 700;
color: #334155;
letter-spacing: 0;
}
:deep(.el-form) {
.el-form-item {
margin-bottom: 14px;
.el-form-item__label {
font-weight: 600;
color: #475569;
margin-bottom: 6px;
font-size: 13px;
}
.el-input__wrapper {
border-radius: 6px;
box-shadow: none;
border: 1px solid #dbe3ee;
transition: all 0.2s ease;
&:hover {
border-color: #b8c4d6;
}
&.is-focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12);
}
}
}
}
.param-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 8px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition: all 0.2s ease;
&:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
}
.selected-tag {
margin-top: 10px;
}
.w100 {
width: 100%;
}
:deep(.el-empty) {
padding: 32px 12px;
}
}
</style>