316 lines
9.2 KiB
Vue
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>
|