feat: 添加节点库功能和动态表单支持
- 新增节点库相关接口和类型定义,支持获取节点库列表 - 更新界面,添加节点库展示和动态表单功能,允许用户根据节点类型动态生成表单项 - 修改按钮事件名称以提高代码可读性
This commit is contained in:
@@ -6,6 +6,34 @@ export interface CreationListParams {
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface NodeLibraryFormItem {
|
||||
field: string;
|
||||
label: string;
|
||||
type: 'input' | 'number' | 'textarea' | 'switch' | string;
|
||||
required: boolean;
|
||||
default?: string | number | boolean;
|
||||
}
|
||||
|
||||
export interface NodeLibraryItem {
|
||||
nodeCode: string;
|
||||
nodeName: string;
|
||||
form: NodeLibraryFormItem[];
|
||||
}
|
||||
|
||||
export interface NodeLibraryGroup {
|
||||
group: string;
|
||||
label: string;
|
||||
items: NodeLibraryItem[];
|
||||
}
|
||||
|
||||
export interface NodeLibraryListResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: {
|
||||
groups: NodeLibraryGroup[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreationImageItem {
|
||||
name: string;
|
||||
url: string;
|
||||
@@ -71,6 +99,14 @@ export function getCreationList(params: CreationListParams, requestOptions?: Req
|
||||
}) as Promise<CreationListResponse>;
|
||||
}
|
||||
|
||||
export function getNodeLibraryList(requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/black-deacon/node/library/list',
|
||||
method: 'get',
|
||||
requestOptions,
|
||||
}) as Promise<NodeLibraryListResponse>;
|
||||
}
|
||||
|
||||
export function createCreation(data: CreationSubmitParams, requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/black-deacon/creation/info/creation',
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="actions">
|
||||
<el-button @click="resetFlow">重置示例</el-button>
|
||||
<el-button @click="showDsl = !showDsl">{{ showDsl ? '收起 DSL' : '展开 DSL' }}</el-button>
|
||||
<el-button type="primary" @click="syncDsl">同步 DSL</el-button>
|
||||
<el-button type="primary" @click="syncDslAction">同步 DSL</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
@@ -47,7 +47,29 @@
|
||||
<div class="meta">
|
||||
<span>工作流画布</span><span>节点 {{ flowDsl.nodes.length }} / 连线 {{ flowDsl.edges.length }}</span>
|
||||
</div>
|
||||
<div class="canvas-wrap"><div ref="logicFlowRef" class="logicflow-canvas"></div></div>
|
||||
<div class="canvas-layout">
|
||||
<div class="node-library">
|
||||
<div class="title-sm">节点库</div>
|
||||
<el-empty v-if="nodeLibraryGroups.length === 0" description="暂无节点" :image-size="60" />
|
||||
<div v-else class="node-library-groups">
|
||||
<div v-for="group in nodeLibraryGroups" :key="group.group" class="node-group">
|
||||
<div class="node-group-title">{{ group.label }}</div>
|
||||
<div class="node-group-items">
|
||||
<el-button
|
||||
v-for="item in group.items"
|
||||
:key="item.nodeCode"
|
||||
text
|
||||
class="node-item"
|
||||
@click="addNodeFromLibrary(item.nodeCode, item.nodeName)"
|
||||
>
|
||||
{{ item.nodeName }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="canvas-wrap"><div ref="logicFlowRef" class="logicflow-canvas"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel side">
|
||||
<div class="title-sm">当前选中元素</div>
|
||||
@@ -59,15 +81,26 @@
|
||||
<el-form label-position="top" class="prop-form">
|
||||
<el-form-item v-if="selectedElement.kind === 'node'" label="节点名称"><el-input v-model="formState.text" /></el-form-item>
|
||||
<el-form-item v-if="selectedElement.kind === 'node'" label="业务类型"><el-input v-model="formState.nodeCode" /></el-form-item>
|
||||
<el-form-item v-if="selectedElement.kind === 'edge' || formState.nodeCode === 'theme-input'" label="字段"
|
||||
><el-input v-model="formState.field"
|
||||
/></el-form-item>
|
||||
<el-form-item v-if="formState.nodeCode === 'copywriting-agent'" label="模型"><el-input v-model="formState.model" /></el-form-item>
|
||||
<el-form-item v-if="formState.nodeCode === 'copywriting-agent'" label="Temperature"
|
||||
><el-input-number v-model="formState.temperature" :min="0" :max="2" :step="0.1" class="w100"
|
||||
/></el-form-item>
|
||||
<el-form-item v-if="formState.nodeCode === 'image-agent'" label="图片比例"><el-input v-model="formState.imageRatio" /></el-form-item>
|
||||
<el-form-item v-if="formState.nodeCode === 'publish-output'" label="发布渠道"><el-input v-model="formState.channel" /></el-form-item>
|
||||
<el-form-item v-if="selectedElement.kind === 'edge'" label="字段"><el-input v-model="formState.field" /></el-form-item>
|
||||
<template v-if="selectedElement.kind === 'node'">
|
||||
<el-form-item v-for="fieldItem in currentNodeForm" :key="fieldItem.field" :label="fieldItem.label">
|
||||
<el-input
|
||||
v-if="fieldItem.type === 'input'"
|
||||
v-model="dynamicFormValues[fieldItem.field]"
|
||||
:placeholder="fieldItem.required ? '必填' : '选填'"
|
||||
/>
|
||||
<el-input-number v-else-if="fieldItem.type === 'number'" v-model="dynamicFormValues[fieldItem.field]" class="w100" />
|
||||
<el-input
|
||||
v-else-if="fieldItem.type === 'textarea'"
|
||||
v-model="dynamicFormValues[fieldItem.field]"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="fieldItem.required ? '必填' : '选填'"
|
||||
/>
|
||||
<el-switch v-else-if="fieldItem.type === 'switch'" v-model="dynamicFormValues[fieldItem.field]" />
|
||||
<el-input v-else v-model="dynamicFormValues[fieldItem.field]" :placeholder="fieldItem.required ? '必填' : '选填'" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-button type="primary" class="w100" @click="applySelected">应用到当前元素</el-button>
|
||||
</el-form>
|
||||
<pre class="json-box">{{ pretty(selectedElement.properties) }}</pre>
|
||||
@@ -88,13 +121,24 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import LogicFlow from '@logicflow/core';
|
||||
import { Control, DndPanel, SelectionSelect } from '@logicflow/extension';
|
||||
import { Control, SelectionSelect } from '@logicflow/extension';
|
||||
import '@logicflow/core/dist/index.css';
|
||||
import '@logicflow/extension/lib/style/index.css';
|
||||
import { downloadToFile, getCreationList, type CreationListParams, type CreationTreeItem } from '/@/api/digitalHuman/creation';
|
||||
import {
|
||||
downloadToFile,
|
||||
getCreationList,
|
||||
getNodeLibraryList,
|
||||
type CreationListParams,
|
||||
type CreationTreeItem,
|
||||
type NodeLibraryFormItem,
|
||||
type NodeLibraryGroup,
|
||||
} from '/@/api/digitalHuman/creation';
|
||||
|
||||
type NodeType = 'date' | 'contentType' | 'theme' | 'title' | 'html' | 'image';
|
||||
type Item = Record<string, any>;
|
||||
const START_NODE_CODE = '__start__';
|
||||
const START_NODE_TEXT = '开始';
|
||||
const JUDGE_KEYWORDS = ['判断', 'judge', 'condition', 'if', 'branch', 'gateway'];
|
||||
interface TreeNode {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -117,36 +161,23 @@ const flowDsl = ref<{ nodes: Item[]; edges: Item[] }>({ nodes: [], edges: [] });
|
||||
const logicFlowRef = ref<HTMLDivElement | null>(null);
|
||||
const logicFlowInstance = ref<LogicFlow | null>(null);
|
||||
const showDsl = ref(false);
|
||||
const formState = reactive({ text: '', nodeCode: '', field: '', model: '', temperature: 0.7, imageRatio: '3:4', channel: '' });
|
||||
const nodeSpawnIndex = ref(0);
|
||||
const formState = reactive({ text: '', nodeCode: '', field: '' });
|
||||
const dynamicFormValues = reactive<Record<string, any>>({});
|
||||
const nodeSchemaMap = computed(() => {
|
||||
const map: Record<string, NodeLibraryFormItem[]> = {};
|
||||
nodeLibraryGroups.value.forEach((group) => {
|
||||
(group.items || []).forEach((item) => {
|
||||
map[item.nodeCode] = item.form || [];
|
||||
});
|
||||
});
|
||||
return map;
|
||||
});
|
||||
const currentNodeForm = computed<NodeLibraryFormItem[]>(() => nodeSchemaMap.value[formState.nodeCode] || []);
|
||||
const treeProps = { children: 'children', label: 'label' };
|
||||
const queryParams: CreationListParams = { keyword: '', pageNum: 1, pageSize: 10 };
|
||||
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
||||
const nodePalette = [
|
||||
{ type: 'rect', text: '主题输入', label: '主题输入', properties: { nodeCode: 'theme-input', field: 'theme' } },
|
||||
{ type: 'rect', text: '文案生成', label: '文案生成', properties: { nodeCode: 'copywriting-agent', model: 'gpt-4o-mini', temperature: 0.7 } },
|
||||
{ type: 'rect', text: '图片生成', label: '图片生成', properties: { nodeCode: 'image-agent', imageRatio: '3:4' } },
|
||||
{ type: 'rect', text: '发布输出', label: '发布输出', properties: { nodeCode: 'publish-output', channel: 'xiaohongshu' } },
|
||||
];
|
||||
const defaultGraphData = {
|
||||
nodes: [
|
||||
{ id: 'theme-node', type: 'rect', x: 160, y: 170, text: '主题输入', properties: { nodeCode: 'theme-input', field: 'theme' } },
|
||||
{
|
||||
id: 'copy-node',
|
||||
type: 'rect',
|
||||
x: 420,
|
||||
y: 170,
|
||||
text: '文案生成',
|
||||
properties: { nodeCode: 'copywriting-agent', model: 'gpt-4o-mini', temperature: 0.7 },
|
||||
},
|
||||
{ id: 'image-node', type: 'rect', x: 680, y: 170, text: '图片生成', properties: { nodeCode: 'image-agent', imageRatio: '3:4' } },
|
||||
{ id: 'publish-node', type: 'rect', x: 940, y: 170, text: '发布输出', properties: { nodeCode: 'publish-output', channel: 'xiaohongshu' } },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge-1', type: 'polyline', sourceNodeId: 'theme-node', targetNodeId: 'copy-node', text: '主题变量', properties: { field: 'theme' } },
|
||||
{ id: 'edge-2', type: 'polyline', sourceNodeId: 'copy-node', targetNodeId: 'image-node', text: '文案结果', properties: { field: 'copywriting' } },
|
||||
{ id: 'edge-3', type: 'polyline', sourceNodeId: 'image-node', targetNodeId: 'publish-node', text: '图文结果', properties: { field: 'assets' } },
|
||||
],
|
||||
};
|
||||
const nodeLibraryGroups = ref<NodeLibraryGroup[]>([]);
|
||||
const workflowDsl = computed(() => ({
|
||||
version: '1.0.0',
|
||||
startNodeId: flowDsl.value.nodes[0]?.id || '',
|
||||
@@ -218,6 +249,15 @@ const getList = async () => {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
};
|
||||
const getNodeLibrary = async () => {
|
||||
try {
|
||||
const res = await getNodeLibraryList({ errorMode: 'page' });
|
||||
nodeLibraryGroups.value = res.data?.groups || [];
|
||||
} catch {
|
||||
nodeLibraryGroups.value = [];
|
||||
ElMessage.error('获取工作流节点库失败');
|
||||
}
|
||||
};
|
||||
const handleNodeClick = (d: TreeNode) => {
|
||||
if (d.nodeType !== 'html' && d.nodeType !== 'image') return;
|
||||
const url = buildAssetUrl(d.fileUrl);
|
||||
@@ -253,16 +293,72 @@ const syncDsl = () => {
|
||||
const data = lf.getGraphData() as { nodes?: Item[]; edges?: Item[] };
|
||||
flowDsl.value = { nodes: data.nodes || [], edges: data.edges || [] };
|
||||
};
|
||||
const getNodeText = (node: Item) => (typeof node?.text === 'string' ? node.text : node?.text?.value || '');
|
||||
const getNodeCode = (node: Item) => String(node?.properties?.nodeCode || '');
|
||||
const isStartNode = (node: Item) => getNodeCode(node) === START_NODE_CODE || getNodeText(node) === START_NODE_TEXT;
|
||||
const isJudgeNode = (node: Item) => {
|
||||
const code = getNodeCode(node).toLowerCase();
|
||||
const text = getNodeText(node).toLowerCase();
|
||||
return JUDGE_KEYWORDS.some((k) => code.includes(k) || text.includes(k));
|
||||
};
|
||||
const ensureDefaultStartNode = () => {
|
||||
const lf = logicFlowInstance.value;
|
||||
if (!lf) return;
|
||||
const g = lf.getGraphData() as { nodes?: Item[]; edges?: Item[] };
|
||||
const nodes = g.nodes || [];
|
||||
if (nodes.some((n) => isStartNode(n))) return;
|
||||
lf.addNode({
|
||||
type: 'rect',
|
||||
x: 220,
|
||||
y: 140,
|
||||
text: START_NODE_TEXT,
|
||||
properties: { nodeCode: START_NODE_CODE },
|
||||
});
|
||||
};
|
||||
const validateFlowConstraints = () => {
|
||||
const nodes = flowDsl.value.nodes || [];
|
||||
const edges = flowDsl.value.edges || [];
|
||||
if (!nodes.length) return { ok: true };
|
||||
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
||||
const startNode = nodes.find((n) => isStartNode(n));
|
||||
if (!startNode) return { ok: false, message: '工作流必须包含开始节点' };
|
||||
for (const e of edges) {
|
||||
if (e.targetNodeId === startNode.id) {
|
||||
return { ok: false, message: '开始节点不能被其他节点链接' };
|
||||
}
|
||||
if (e.sourceNodeId === startNode.id) {
|
||||
const target = nodeMap.get(e.targetNodeId);
|
||||
if (target && isJudgeNode(target)) {
|
||||
return { ok: false, message: '开始节点下一个节点不能是判断节点' };
|
||||
}
|
||||
}
|
||||
}
|
||||
const hasOutEdge = new Set(edges.map((e) => e.sourceNodeId));
|
||||
const endNodes = nodes.filter((n) => !hasOutEdge.has(n.id));
|
||||
if (endNodes.some((n) => isJudgeNode(n))) {
|
||||
return { ok: false, message: '结尾节点不能是判断节点' };
|
||||
}
|
||||
return { ok: true };
|
||||
};
|
||||
watch(
|
||||
selectedElement,
|
||||
(e) => {
|
||||
formState.text = String(e?.text || '');
|
||||
formState.nodeCode = String(e?.properties?.nodeCode || '');
|
||||
formState.field = String(e?.properties?.field || '');
|
||||
formState.model = String(e?.properties?.model || '');
|
||||
formState.temperature = Number(e?.properties?.temperature ?? 0.7);
|
||||
formState.imageRatio = String(e?.properties?.imageRatio || '3:4');
|
||||
formState.channel = String(e?.properties?.channel || '');
|
||||
Object.keys(dynamicFormValues).forEach((k) => delete dynamicFormValues[k]);
|
||||
currentNodeForm.value.forEach((fieldItem) => {
|
||||
const currentValue = e?.properties?.[fieldItem.field];
|
||||
if (currentValue !== undefined) {
|
||||
dynamicFormValues[fieldItem.field] = currentValue;
|
||||
return;
|
||||
}
|
||||
if (fieldItem.default !== undefined) {
|
||||
dynamicFormValues[fieldItem.field] = fieldItem.default;
|
||||
return;
|
||||
}
|
||||
dynamicFormValues[fieldItem.field] = fieldItem.type === 'switch' ? false : '';
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
@@ -270,12 +366,30 @@ const applySelected = () => {
|
||||
const lf = logicFlowInstance.value,
|
||||
cur = selectedElement.value;
|
||||
if (!lf || !cur) return;
|
||||
if (cur.kind === 'node') {
|
||||
const missingField = currentNodeForm.value.find(
|
||||
(fieldItem) =>
|
||||
fieldItem.required &&
|
||||
(dynamicFormValues[fieldItem.field] === '' || dynamicFormValues[fieldItem.field] === undefined || dynamicFormValues[fieldItem.field] === null)
|
||||
);
|
||||
if (missingField) {
|
||||
ElMessage.warning(`请填写必填项:${missingField.label}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const p: Item = { ...cur.properties, nodeCode: formState.nodeCode };
|
||||
formState.field ? (p.field = formState.field) : delete p.field;
|
||||
formState.model ? (p.model = formState.model) : delete p.model;
|
||||
formState.nodeCode === 'copywriting-agent' ? (p.temperature = formState.temperature) : delete p.temperature;
|
||||
formState.imageRatio ? (p.imageRatio = formState.imageRatio) : delete p.imageRatio;
|
||||
formState.channel ? (p.channel = formState.channel) : delete p.channel;
|
||||
if (cur.kind === 'edge') {
|
||||
formState.field ? (p.field = formState.field) : delete p.field;
|
||||
} else {
|
||||
Object.keys(p).forEach((key) => {
|
||||
if (key !== 'nodeCode') delete p[key];
|
||||
});
|
||||
Object.entries(dynamicFormValues).forEach(([key, value]) => {
|
||||
if (value !== '' && value !== undefined && value !== null) {
|
||||
p[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
lf.setProperties(cur.id, p);
|
||||
if (formState.text) lf.updateText(cur.id, formState.text);
|
||||
const g = lf.getGraphData() as { nodes: Item[]; edges: Item[] };
|
||||
@@ -289,11 +403,35 @@ const applySelected = () => {
|
||||
syncDsl();
|
||||
ElMessage.success('已更新当前元素配置');
|
||||
};
|
||||
const setupDndPanel = () => {
|
||||
const lf = logicFlowInstance.value as LogicFlow & {
|
||||
extension: { dndPanel?: { setPatternItems: (items: Array<Record<string, unknown>>) => void } };
|
||||
};
|
||||
lf.extension.dndPanel?.setPatternItems(nodePalette);
|
||||
const syncDslAction = () => {
|
||||
syncDsl();
|
||||
const validateResult = validateFlowConstraints();
|
||||
if (!validateResult.ok) {
|
||||
ElMessage.warning(validateResult.message);
|
||||
return;
|
||||
}
|
||||
ElMessage.success('DSL 已同步');
|
||||
};
|
||||
const addNodeFromLibrary = (nodeCode: string, nodeName: string) => {
|
||||
const lf = logicFlowInstance.value;
|
||||
if (!lf || !logicFlowRef.value) return;
|
||||
const rect = logicFlowRef.value.getBoundingClientRect();
|
||||
const center = lf.getPointByClient(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
||||
const x = center?.canvasOverlayPosition?.x ?? 420;
|
||||
const y = center?.canvasOverlayPosition?.y ?? 220;
|
||||
const offsetStep = 18;
|
||||
const offsetCount = nodeSpawnIndex.value % 6;
|
||||
const spawnX = x + offsetStep * offsetCount;
|
||||
const spawnY = y + offsetStep * offsetCount;
|
||||
lf.addNode({
|
||||
type: 'rect',
|
||||
x: spawnX,
|
||||
y: spawnY,
|
||||
text: nodeName,
|
||||
properties: { nodeCode },
|
||||
});
|
||||
nodeSpawnIndex.value += 1;
|
||||
syncDsl();
|
||||
};
|
||||
const bindEvents = () => {
|
||||
const lf = logicFlowInstance.value;
|
||||
@@ -319,12 +457,14 @@ const bindEvents = () => {
|
||||
lf.on('blank:click', () => {
|
||||
selectedElement.value = null;
|
||||
});
|
||||
lf.on('connection:not-allowed', ({ msg }: { msg?: string }) => {
|
||||
ElMessage.warning(msg || '当前连线不允许');
|
||||
});
|
||||
['history:change', 'node:add', 'edge:add', 'node:delete', 'edge:delete'].forEach((n) => lf.on(n, syncDsl));
|
||||
};
|
||||
const initLogicFlow = () => {
|
||||
if (!logicFlowRef.value) return;
|
||||
LogicFlow.use(Control);
|
||||
LogicFlow.use(DndPanel);
|
||||
LogicFlow.use(SelectionSelect);
|
||||
const lf = new LogicFlow({
|
||||
container: logicFlowRef.value,
|
||||
@@ -334,31 +474,64 @@ const initLogicFlow = () => {
|
||||
adjustEdge: true,
|
||||
edgeType: 'polyline',
|
||||
style: {
|
||||
rect: { width: 120, height: 54, radius: 10, stroke: '#334155', strokeWidth: 1.4, fill: '#fff' },
|
||||
rect: { width: 100, height: 44, radius: 8, stroke: '#334155', strokeWidth: 1.4, fill: '#fff' },
|
||||
polyline: { stroke: '#475569', strokeWidth: 1.4 },
|
||||
edgeText: { fill: '#64748b', fontSize: 12, textWidth: 120, background: { fill: '#fff' } },
|
||||
edgeText: { fill: '#64748b', fontSize: 12, textWidth: 100, background: { fill: '#fff' } },
|
||||
text: { fontSize: 13, fill: '#1f2937' },
|
||||
},
|
||||
});
|
||||
// 先设置实例引用
|
||||
logicFlowInstance.value = lf;
|
||||
lf.render(defaultGraphData);
|
||||
lf.fitView(60, 80);
|
||||
setupDndPanel();
|
||||
|
||||
// 渲染空画布
|
||||
lf.render({ nodes: [], edges: [] });
|
||||
|
||||
// 添加默认开始节点
|
||||
ensureDefaultStartNode();
|
||||
|
||||
// 设置连线验证规则
|
||||
if (typeof lf.setValidateConnection === 'function') {
|
||||
lf.setValidateConnection(({ sourceNode, targetNode }: any) => {
|
||||
if (!sourceNode || !targetNode) return true;
|
||||
const source = sourceNode?.model || sourceNode;
|
||||
const target = targetNode?.model || targetNode;
|
||||
if (isStartNode(target)) {
|
||||
return { isAllPass: false, msg: '开始节点不能被其他节点链接' } as any;
|
||||
}
|
||||
if (isStartNode(source) && isJudgeNode(target)) {
|
||||
return { isAllPass: false, msg: '开始节点下一个节点不能是判断节点' } as any;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents();
|
||||
|
||||
// 设置固定缩放比例,不使用 fitView 自动缩放
|
||||
lf.zoom(1);
|
||||
lf.translateCenter();
|
||||
|
||||
// 同步 DSL
|
||||
syncDsl();
|
||||
};
|
||||
const resetFlow = () => {
|
||||
const lf = logicFlowInstance.value;
|
||||
if (!lf) return;
|
||||
lf.render(defaultGraphData);
|
||||
lf.fitView(60, 80);
|
||||
lf.render({ nodes: [], edges: [] });
|
||||
ensureDefaultStartNode();
|
||||
lf.zoom(1);
|
||||
lf.translateCenter();
|
||||
nodeSpawnIndex.value = 0;
|
||||
selectedElement.value = null;
|
||||
syncDsl();
|
||||
ElMessage.success('示例流程已重置');
|
||||
ElMessage.success('流程已重置');
|
||||
};
|
||||
onMounted(async () => {
|
||||
await getList();
|
||||
await nextTick();
|
||||
initLogicFlow();
|
||||
await getNodeLibrary();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
logicFlowInstance.value?.destroy();
|
||||
@@ -443,6 +616,58 @@ onBeforeUnmount(() => {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.canvas-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-height: 560px;
|
||||
}
|
||||
.node-library {
|
||||
border: 1px solid #e8eef7;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.node-library :deep(.el-empty) {
|
||||
padding: 8px 0;
|
||||
}
|
||||
.node-library-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.node-group {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 10px 10px 8px;
|
||||
}
|
||||
.node-group-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
.node-group-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
}
|
||||
.node-item {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
color: #334155;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.node-item:hover {
|
||||
background: #eef4ff;
|
||||
}
|
||||
.canvas-wrap {
|
||||
flex: 1;
|
||||
min-height: 560px;
|
||||
@@ -483,10 +708,6 @@ onBeforeUnmount(() => {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
:deep(.lf-dndpanel) {
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
}
|
||||
:deep(.lf-control) {
|
||||
right: 14px;
|
||||
top: 14px;
|
||||
@@ -503,6 +724,9 @@ onBeforeUnmount(() => {
|
||||
.creation-page {
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
}
|
||||
.canvas-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user