feat: 添加删除选中元素功能与保存工作流对话框组件

- 新增删除选中元素的按钮,支持对节点的删除操作,并处理下级节点引用的清理。
- 将保存工作流对话框重构为独立组件,提升代码可读性与复用性。
- 优化了预览功能的代码结构,确保视频展示的样式一致性。
This commit is contained in:
2026-05-23 10:22:25 +08:00
parent ce70f86000
commit 38166cb0b8
2 changed files with 170 additions and 27 deletions

View File

@@ -0,0 +1,40 @@
<template>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" :close-on-click-modal="false">
<el-form :model="saveForm" label-position="top">
<el-form-item label="工作流名称" required>
<el-input v-model="saveForm.flowName" placeholder="请输入工作流名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="工作流描述">
<el-input v-model="saveForm.description" type="textarea" :rows="4" placeholder="请输入工作流描述(选填)" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="emit('confirm')">{{ confirmText }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
modelValue: boolean;
saveForm: { flowName: string; description: string };
currentEditingWorkflowId: string | null;
saving: boolean;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'confirm'): void;
}>();
const dialogVisible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value),
});
const dialogTitle = computed(() => (props.currentEditingWorkflowId ? '编辑工作流' : '保存工作流'));
const confirmText = computed(() => (props.currentEditingWorkflowId ? '确定更新' : '确定保存'));
</script>

View File

@@ -449,6 +449,18 @@
</div>
<div class="meta-actions">
<el-button size="small" @click="resetFlow">清空画布</el-button>
<el-button
size="small"
type="danger"
:disabled="
!selectedElement ||
(selectedElement.kind === 'node' &&
(selectedElement.properties?.nodeCode === START_NODE_CODE || selectedElement.text === START_NODE_TEXT))
"
@click="deleteSelectedElement"
>
删除选中
</el-button>
<el-button type="primary" size="small" @click="saveWorkflowAction" :loading="saving">保存工作流</el-button>
</div>
</div>
@@ -564,25 +576,13 @@
</div>
<!-- 保存工作流对话框 -->
<el-dialog
<SaveWorkflowDialog
v-model="saveDialogVisible"
:title="currentEditingWorkflowId ? '编辑工作流' : '保存工作流'"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="saveForm" label-position="top">
<el-form-item label="工作流名称" required>
<el-input v-model="saveForm.flowName" placeholder="请输入工作流名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="工作流描述">
<el-input v-model="saveForm.description" type="textarea" :rows="4" placeholder="请输入工作流描述选填" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="saveDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmSaveWorkflow" :loading="saving">{{ currentEditingWorkflowId ? '确定更新' : '确定保存' }}</el-button>
</template>
</el-dialog>
:save-form="saveForm"
:current-editing-workflow-id="currentEditingWorkflowId"
:saving="saving"
@confirm="confirmSaveWorkflow"
/>
<!-- 技能选择器 -->
<SkillSelector v-model="showSkillSelector" :default-skill="selectedSkill" @confirm="handleSkillConfirm" />
@@ -770,7 +770,12 @@
<el-dialog v-model="previewDialogVisible" title="预览" width="95%" top="2vh" :close-on-click-modal="false" destroy-on-close>
<div class="preview-container">
<el-image v-if="previewUrl && previewMode === 'image'" :src="previewUrl" fit="contain" style="width: 100%; height: 100%" />
<video v-else-if="previewUrl && previewMode === 'video'" :src="previewUrl" controls style="width: 100%; height: 100%; background: #000"></video>
<video
v-else-if="previewUrl && previewMode === 'video'"
:src="previewUrl"
controls
style="width: 100%; height: 100%; background: #000"
></video>
<audio v-else-if="previewUrl && previewMode === 'audio'" :src="previewUrl" controls style="width: 100%"></audio>
<iframe v-else-if="previewUrl" :src="previewUrl" class="preview-iframe" frameborder="0"></iframe>
<el-empty v-else description="无法加载预览内容" />
@@ -789,6 +794,7 @@ import '@logicflow/core/dist/index.css';
import '@logicflow/extension/lib/style/index.css';
import SkillSelector from '/@/components/skill/NodeSkillSelector.vue';
import ModelSelector from '/@/components/model/ModelSelector.vue';
import SaveWorkflowDialog from './component/SaveWorkflowDialog.vue';
import type { SkillItem } from '/@/api/settings/skill';
import {
downloadToFile,
@@ -3236,14 +3242,14 @@ watch(
currentHttpBodyField.value = '';
showHttpBodyDialog.value = false;
// 重置 dynamicFormValues不删除固定字段键动态 expand 键按节点切换清理)
for (const key in dynamicFormValues) {
if (key.includes('_responseType_expand_')) {
delete dynamicFormValues[key];
continue;
}
dynamicFormValues[key] = '';
// 重置 dynamicFormValues不删除固定字段键动态 expand 键按节点切换清理)
for (const key in dynamicFormValues) {
if (key.includes('_responseType_expand_')) {
delete dynamicFormValues[key];
continue;
}
dynamicFormValues[key] = '';
}
const currentNodeCode = formState.nodeCode;
const baseFormFields = nodeSchemaMap.value[currentNodeCode] || [];
const baseFieldNames = new Set(baseFormFields.map((f) => f.field));
@@ -3779,7 +3785,100 @@ const resetFlow = () => {
selectedElement.value = null;
syncDsl();
};
// 从后端 DSL 恢复工作流
const cleanupReferencesToNode = (deletedNodeId: string) => {
const lf = logicFlowInstance.value;
if (!lf) return 0;
const graphData = lf.getGraphData() as { nodes?: Item[] };
const nodes = graphData.nodes || [];
let affectedCount = 0;
nodes.forEach((node: any) => {
if (node.id === deletedNodeId) return;
const props = node.properties || {};
const inputSource = Array.isArray(props.inputSource) ? props.inputSource : [];
const nextInputSource = inputSource.filter((item: any) => item?.nodeId !== deletedNodeId);
if (nextInputSource.length === inputSource.length) return;
affectedCount += 1;
const normalizedInputSource = nextInputSource.length > 0 ? nextInputSource : null;
lf.setProperties(node.id, {
...props,
inputSource: normalizedInputSource,
});
if (selectedElement.value?.id === node.id) {
selectedElement.value.properties = {
...props,
inputSource: normalizedInputSource,
};
}
});
return affectedCount;
};
const getAffectedDownstreamNodeNames = (deletedNodeId: string) => {
const lf = logicFlowInstance.value;
if (!lf) return [] as string[];
const graphData = lf.getGraphData() as { nodes?: Item[] };
const nodes = graphData.nodes || [];
const names: string[] = [];
nodes.forEach((node: any) => {
if (node.id === deletedNodeId) return;
const props = node.properties || {};
const inputSource = Array.isArray(props.inputSource) ? props.inputSource : [];
const referenced = inputSource.some((item: any) => item?.nodeId === deletedNodeId);
if (!referenced) return;
const nodeName = typeof node.text === 'string' ? node.text : node.text?.value || node.id;
names.push(String(nodeName));
});
return names;
};
const deleteSelectedElement = async () => {
const lf = logicFlowInstance.value;
const cur = selectedElement.value;
if (!lf || !cur) return;
if (cur.kind === 'node' && (cur.properties?.nodeCode === START_NODE_CODE || cur.text === START_NODE_TEXT)) {
ElMessage.warning('开始节点不能删除');
return;
}
try {
let affectedCount = 0;
if (cur.kind === 'node') {
const affectedNodeNames = getAffectedDownstreamNodeNames(cur.id);
if (affectedNodeNames.length > 0) {
const previewNames = affectedNodeNames.slice(0, 8);
const overflowText = affectedNodeNames.length > 8 ? `\n...等 ${affectedNodeNames.length} 个节点` : '';
await ElMessageBox.confirm(
`删除该节点将清理以下下级节点中的引用:\n${previewNames.join('、')}${overflowText}`,
'删除确认',
{
confirmButtonText: '继续删除',
cancelButtonText: '取消',
type: 'warning',
}
);
}
affectedCount = cleanupReferencesToNode(cur.id);
lf.deleteNode(cur.id);
} else {
lf.deleteEdge(cur.id);
}
selectedElement.value = null;
ElMessage.success(affectedCount > 0 ? `删除成功,已清理 ${affectedCount} 个下级节点引用` : '删除成功');
} catch (error) {
if (error === 'cancel') return;
ElMessage.error('删除失败');
}
};// 从后端 DSL 恢复工作流
const loadWorkflowFromDsl = (dsl: any) => {
const lf = logicFlowInstance.value;
if (!lf || !dsl) return;
@@ -5527,3 +5626,7 @@ onBeforeUnmount(() => {
justify-content: center;
}
</style>