workflow底层UI库整合至项目,优化构建逻辑
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
export const genShortId = (length = 16) => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => chars[byte % chars.length]).join('');
|
||||
};
|
||||
|
||||
export const genUuid = () => {
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import {getContext} from 'svelte';
|
||||
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||
import type {TinyflowOptions} from '#types';
|
||||
|
||||
export const getCurrentNodeId = () => {
|
||||
return getContext<string>('svelteflow__node_id');
|
||||
};
|
||||
|
||||
export const getOptions = () => {
|
||||
return getContext<TinyflowOptions>('tinyflow_options');
|
||||
};
|
||||
|
||||
export const useCurrentNodeData = () => {
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
return useNodesData<any>(currentNodeId);
|
||||
};
|
||||
|
||||
export const useUpdateNodeData = () => {
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
return {
|
||||
updateNodeData: (data: any) => {
|
||||
updateNodeData(currentNodeId, data);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
arrow,
|
||||
computePosition,
|
||||
flip,
|
||||
type FlipOptions,
|
||||
offset,
|
||||
type OffsetOptions,
|
||||
type Placement,
|
||||
shift,
|
||||
type ShiftOptions
|
||||
} from '@floating-ui/dom';
|
||||
|
||||
export type FloatingOptions = {
|
||||
trigger: string | HTMLElement;
|
||||
triggerEvent?: string[];
|
||||
floatContent: string | HTMLElement;
|
||||
placement?: Placement;
|
||||
offsetOptions?: OffsetOptions;
|
||||
flipOptions?: FlipOptions;
|
||||
shiftOptions?: ShiftOptions;
|
||||
interactive?: boolean;
|
||||
showArrow?: boolean;
|
||||
};
|
||||
|
||||
export type FloatingInstance = {
|
||||
destroy: () => void;
|
||||
hide: () => void;
|
||||
isVisible: () => boolean;
|
||||
};
|
||||
|
||||
export const createFloating = ({
|
||||
trigger,
|
||||
triggerEvent,
|
||||
floatContent,
|
||||
placement = 'bottom',
|
||||
offsetOptions,
|
||||
flipOptions,
|
||||
shiftOptions,
|
||||
interactive,
|
||||
showArrow
|
||||
}: FloatingOptions): FloatingInstance => {
|
||||
if (typeof trigger === 'string') {
|
||||
const triggerEl = document.querySelector(trigger);
|
||||
if (!triggerEl) {
|
||||
throw new Error("element not found by document.querySelector('" + trigger + "')");
|
||||
} else {
|
||||
trigger = triggerEl as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
let floating: HTMLElement;
|
||||
if (typeof floatContent === 'string') {
|
||||
const floatContentEl = document.querySelector(floatContent);
|
||||
if (!floatContentEl) {
|
||||
throw new Error("element not found by document.querySelector('" + floatContent + "')");
|
||||
} else {
|
||||
floating = floatContentEl as HTMLElement;
|
||||
}
|
||||
} else {
|
||||
floating = floatContent as HTMLElement;
|
||||
}
|
||||
|
||||
let arrowElement: HTMLElement;
|
||||
if (showArrow) {
|
||||
arrowElement = document.createElement('div');
|
||||
arrowElement.style.position = 'absolute';
|
||||
arrowElement.style.backgroundColor = '#222';
|
||||
arrowElement.style.width = '8px';
|
||||
arrowElement.style.height = '8px';
|
||||
arrowElement.style.transform = 'rotate(45deg)';
|
||||
arrowElement.style.display = 'none';
|
||||
|
||||
floating.firstElementChild!.before(arrowElement);
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
computePosition(trigger as Element, floating, {
|
||||
placement: placement,
|
||||
middleware: [
|
||||
offset(offsetOptions), // 手动偏移配置
|
||||
flip(flipOptions), //自动翻转
|
||||
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
|
||||
...(showArrow ? [arrow({ element: arrowElement })] : [])
|
||||
]
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(floating.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`
|
||||
});
|
||||
|
||||
if (showArrow) {
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow as { x: number; y: number };
|
||||
const staticSide = {
|
||||
top: 'bottom',
|
||||
right: 'left',
|
||||
bottom: 'top',
|
||||
left: 'right'
|
||||
}[placement.split('-')[0]] as string;
|
||||
|
||||
Object.assign(arrowElement.style, {
|
||||
zIndex: -1,
|
||||
left: arrowX != null ? `${arrowX}px` : '',
|
||||
top: arrowY != null ? `${arrowY}px` : '',
|
||||
right: '',
|
||||
bottom: '',
|
||||
[staticSide]: '2px'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let visible = false;
|
||||
|
||||
function showTooltip() {
|
||||
floating.style.display = 'block';
|
||||
floating.style.visibility = 'block';
|
||||
floating.style.position = 'absolute';
|
||||
|
||||
if (showArrow) {
|
||||
arrowElement.style.display = 'block';
|
||||
}
|
||||
|
||||
visible = true;
|
||||
updatePosition();
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
floating.style.display = 'none';
|
||||
if (showArrow) {
|
||||
arrowElement.style.display = 'none';
|
||||
}
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function onTrigger(event: any) {
|
||||
event.stopPropagation();
|
||||
if (!visible) {
|
||||
showTooltip();
|
||||
} else {
|
||||
hideTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltipCompute(event: any) {
|
||||
if (floating.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
hideTooltip();
|
||||
}
|
||||
|
||||
if (!triggerEvent || triggerEvent.length == 0) {
|
||||
if (interactive) {
|
||||
triggerEvent = ['click'];
|
||||
} else {
|
||||
triggerEvent = ['mouseenter', 'focus'];
|
||||
}
|
||||
}
|
||||
|
||||
triggerEvent.forEach((event) => {
|
||||
(trigger as HTMLElement).addEventListener(event, onTrigger);
|
||||
});
|
||||
|
||||
if (interactive) {
|
||||
document.addEventListener('click', hideTooltipCompute);
|
||||
} else {
|
||||
['mouseleave', 'blur'].forEach((event) => {
|
||||
trigger.addEventListener(event, hideTooltip);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
triggerEvent.forEach((event) => {
|
||||
(trigger as HTMLElement).removeEventListener(event, onTrigger);
|
||||
});
|
||||
|
||||
if (interactive) {
|
||||
document.removeEventListener('click', hideTooltipCompute);
|
||||
} else {
|
||||
['mouseleave', 'blur'].forEach((event) => {
|
||||
trigger.removeEventListener(event, hideTooltip);
|
||||
});
|
||||
}
|
||||
},
|
||||
hide() {
|
||||
hideTooltip();
|
||||
},
|
||||
|
||||
isVisible() {
|
||||
return visible;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const deepClone = <T>(obj: T): T => {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
export const deepEqual = <T>(obj1: T, obj2: T) => {
|
||||
if (obj1 === obj2) return true;
|
||||
|
||||
// 处理 null 和 object 类型
|
||||
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断是否都是数组
|
||||
const isArray1 = Array.isArray(obj1);
|
||||
const isArray2 = Array.isArray(obj2);
|
||||
|
||||
if (isArray1 !== isArray2) return false; // 一个是数组另一个不是,不相等
|
||||
|
||||
// 数组的情况
|
||||
if (isArray1 && isArray2) {
|
||||
if (obj1.length !== obj2.length) return false;
|
||||
for (let i = 0; i < obj1.length; i++) {
|
||||
if (!deepEqual(obj1[i], obj2[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 普通对象的情况
|
||||
else {
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!(key in obj2)) return false;
|
||||
if (!deepEqual((obj1 as any)[key], (obj2 as any)[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 判断当前焦点是否位于可编辑元素中(如 input、textarea 或 contenteditable 区域)。
|
||||
* 适用于快捷键、全局事件监听等需要避免干扰用户输入的场景。
|
||||
*/
|
||||
export const isInEditableElement = () => {
|
||||
const el = document.activeElement;
|
||||
if (!el || !(el instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
el instanceof HTMLInputElement ||
|
||||
el instanceof HTMLTextAreaElement ||
|
||||
el.isContentEditable
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import {genShortId} from './IdGen';
|
||||
import {useSvelteFlow} from '@xyflow/svelte';
|
||||
import type {Parameter} from '#types';
|
||||
|
||||
export const fillParameterId = (parameters?: Parameter[]) => {
|
||||
if (!parameters || parameters.length == 0) {
|
||||
return parameters;
|
||||
}
|
||||
parameters.forEach((parameter) => {
|
||||
if (!parameter.id) {
|
||||
parameter.id = genShortId();
|
||||
}
|
||||
fillParameterId(parameter.children);
|
||||
});
|
||||
|
||||
return parameters;
|
||||
};
|
||||
|
||||
export const useAddParameter = () => {
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
return {
|
||||
addParameter: (
|
||||
nodeId: string,
|
||||
dataKey: string = 'parameters',
|
||||
parameter?: Parameter | Parameter[]
|
||||
) => {
|
||||
if (Array.isArray(parameter)) {
|
||||
parameter.forEach((p) => fillParameterId(p?.children));
|
||||
} else {
|
||||
fillParameterId(parameter?.children);
|
||||
}
|
||||
|
||||
function createNewParameter(parameter: Parameter) {
|
||||
return {
|
||||
name: '',
|
||||
dataType: 'String',
|
||||
refType: 'ref',
|
||||
...parameter,
|
||||
id: genShortId()
|
||||
};
|
||||
}
|
||||
|
||||
const newParameters: Parameter[] = [];
|
||||
if (Array.isArray(parameter)) {
|
||||
newParameters.push(...parameter.map(createNewParameter));
|
||||
} else {
|
||||
newParameters.push(createNewParameter(parameter as Parameter));
|
||||
}
|
||||
|
||||
updateNodeData(nodeId, (node) => {
|
||||
let parameters = node.data[dataKey] as Array<any>;
|
||||
if (parameters) {
|
||||
parameters.push(...newParameters);
|
||||
} else {
|
||||
parameters = [...newParameters];
|
||||
}
|
||||
return {
|
||||
[dataKey]: [...parameters]
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import {genShortId} from './IdGen';
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useCopyNode = () => {
|
||||
const copyNode = (id: string) => {
|
||||
const node = store.getNode(id);
|
||||
if (node) {
|
||||
const newNodeId = genShortId();
|
||||
const newNode = {
|
||||
...node,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: node.position.x + 50,
|
||||
y: node.position.y + 50
|
||||
}
|
||||
};
|
||||
|
||||
store.updateNodes((nodes) => {
|
||||
const newNodes = nodes.map((n) => ({ ...n, selected: false }));
|
||||
return [...newNodes, newNode];
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
copyNode
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,282 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import {genShortId} from '#components/utils/IdGen';
|
||||
import {type Edge, type Node, useSvelteFlow} from '@xyflow/svelte';
|
||||
|
||||
interface ClipboardData {
|
||||
tinyflowNodes: Node[];
|
||||
tinyflowEdges?: Edge[];
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理节点中不可序列化的字段,确保可安全 JSON.stringify
|
||||
*/
|
||||
function sanitizeNode(node: Node): Node {
|
||||
const { id, type, position, data, parentId } = node;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position: { x: position.x, y: position.y },
|
||||
parentId: parentId ? parentId : undefined,
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
...(parentId !== undefined && { parentId })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 nodes 数组排序,确保每个父节点出现在其所有子节点之前。
|
||||
* 使用 Kahn 算法(拓扑排序)处理任意嵌套层级。
|
||||
*/
|
||||
export function sortNodesForSvelteFlow(nodes: Node[]): Node[] {
|
||||
const nodeMap = new Map<string, Node>();
|
||||
const inDegree = new Map<string, number>(); // 每个节点的“依赖数”(即是否为子节点)
|
||||
const childrenMap = new Map<string, string[]>(); // parentId -> childIds
|
||||
|
||||
// 初始化
|
||||
for (const node of nodes) {
|
||||
nodeMap.set(node.id, node);
|
||||
inDegree.set(node.id, 0); // 默认无依赖
|
||||
if (node.parentId) {
|
||||
// 子节点依赖父节点
|
||||
inDegree.set(node.id, 1);
|
||||
if (!childrenMap.has(node.parentId)) {
|
||||
childrenMap.set(node.parentId, []);
|
||||
}
|
||||
childrenMap.get(node.parentId)!.push(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 所有根节点(无 parentId 或父不存在)入队
|
||||
const queue: Node[] = [];
|
||||
for (const node of nodes) {
|
||||
if (!node.parentId || !nodeMap.has(node.parentId)) {
|
||||
queue.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const result: Node[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
// BFS 拓扑排序
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift()!;
|
||||
if (visited.has(node.id)) continue;
|
||||
visited.add(node.id);
|
||||
result.push(node);
|
||||
|
||||
// 将该节点的所有直接子节点入队(如果其父已处理)
|
||||
const children = childrenMap.get(node.id) || [];
|
||||
for (const childId of children) {
|
||||
if (!visited.has(childId)) {
|
||||
queue.push(nodeMap.get(childId)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 补充可能遗漏的节点(如循环引用或孤立子节点)
|
||||
for (const node of nodes) {
|
||||
if (!visited.has(node.id)) {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理边中不可序列化的字段
|
||||
*/
|
||||
function sanitizeEdge(edge: Edge): Edge {
|
||||
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
|
||||
return {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
...(sourceHandle !== undefined && { sourceHandle }),
|
||||
...(targetHandle !== undefined && { targetHandle }),
|
||||
...(type !== undefined && { type }),
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历对象,仅当遇到 { refType: 'ref', ref: string } 时重写 ref 的 id
|
||||
*/
|
||||
function rewriteRefsInData(obj: any, idMap: Map<string, string>): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 如果是数组,递归处理每个元素
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => rewriteRefsInData(item, idMap));
|
||||
}
|
||||
|
||||
// 如果是对象,检查是否为引用对象
|
||||
if (typeof obj === 'object') {
|
||||
// 检查是否是引用定义:refType === 'ref' 且有 ref 字段
|
||||
if (obj.refType === 'ref' && typeof obj.ref === 'string') {
|
||||
const match = obj.ref.match(/^([^.\s]+)\.(.+)$/);
|
||||
if (match) {
|
||||
const [_, oldNodeId, paramId] = match;
|
||||
const newNodeId = idMap.get(oldNodeId);
|
||||
if (newNodeId !== undefined) {
|
||||
// 返回新对象,避免修改原数据
|
||||
return {
|
||||
...obj,
|
||||
ref: `${newNodeId}.${paramId}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理所有子属性
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
result[key] = rewriteRefsInData(obj[key], idMap);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 原始类型(string/number/boolean)直接返回
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制粘贴处理器 Hook
|
||||
*/
|
||||
export const useCopyPasteHandler = () => {
|
||||
const svelteFlow = useSvelteFlow();
|
||||
|
||||
const copyHandler = async (event: ClipboardEvent | KeyboardEvent) => {
|
||||
const selectedNodes = store.getNodes().filter((node) => node.selected);
|
||||
if (selectedNodes.length === 0) return;
|
||||
|
||||
// 获取完全包含在选中节点之间的边(起点和终点都被选中)
|
||||
const allEdges = store.getEdges();
|
||||
const relatedEdges = allEdges.filter(
|
||||
(edge) =>
|
||||
selectedNodes.some((n) => n.id === edge.source) &&
|
||||
selectedNodes.some((n) => n.id === edge.target)
|
||||
);
|
||||
|
||||
const serializableNodes = selectedNodes.map(sanitizeNode);
|
||||
const serializableEdges = relatedEdges.map(sanitizeEdge);
|
||||
|
||||
const clipboardData: ClipboardData = {
|
||||
tinyflowNodes: serializableNodes,
|
||||
tinyflowEdges: serializableEdges,
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const jsonStr = JSON.stringify(clipboardData, null, 0);
|
||||
|
||||
try {
|
||||
// 优先使用 event.clipboardData(在 copy 事件中可用,无需权限)
|
||||
if ('clipboardData' in event && event.clipboardData) {
|
||||
event.clipboardData.setData('text/plain', jsonStr);
|
||||
if (event instanceof ClipboardEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
} else {
|
||||
// 降级到 navigator.clipboard(需用户手势)
|
||||
await navigator.clipboard.writeText(jsonStr);
|
||||
}
|
||||
console.log('Copied nodes and edges to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
// 可选:同源降级存储
|
||||
try {
|
||||
sessionStorage.setItem('tinyflow_clipboard', jsonStr);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const pasteHandler = (event: ClipboardEvent) => {
|
||||
const text = event.clipboardData?.getData('text/plain');
|
||||
if (!text) return;
|
||||
|
||||
let parsed: ClipboardData | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
return; // 忽略非 JSON 内容
|
||||
}
|
||||
|
||||
if (!parsed?.tinyflowNodes || !Array.isArray(parsed.tinyflowNodes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const pastedNodes = sortNodesForSvelteFlow(parsed.tinyflowNodes);
|
||||
const pastedEdges = parsed.tinyflowEdges || [];
|
||||
|
||||
// 创建新节点(带新 ID 和偏移)
|
||||
const newNodeIdMap = new Map<string, string>();
|
||||
const newNodes: Node[] = [];
|
||||
|
||||
for (const node of pastedNodes) {
|
||||
const newId = `node_${genShortId()}`;
|
||||
newNodeIdMap.set(node.id, newId);
|
||||
}
|
||||
|
||||
// 构建新节点(含重写后的 data)
|
||||
for (const node of pastedNodes) {
|
||||
const newId = newNodeIdMap.get(node.id)!;
|
||||
|
||||
const newParentId =
|
||||
node.parentId !== undefined
|
||||
? newNodeIdMap.get(node.parentId) // 安全:即使父不在粘贴范围内,也会是 undefined
|
||||
: undefined;
|
||||
|
||||
const newData = rewriteRefsInData(node.data, newNodeIdMap);
|
||||
|
||||
newNodes.push({
|
||||
...node,
|
||||
id: newId,
|
||||
parentId: newParentId,
|
||||
data: newData,
|
||||
position: {
|
||||
x: (node.position?.x ?? 0) + 50,
|
||||
y: (node.position?.y ?? 0) + 50
|
||||
},
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
|
||||
// 创建新边(仅当两端都在粘贴范围内)
|
||||
const newEdges: Edge[] = [];
|
||||
for (const edge of pastedEdges) {
|
||||
const newSource = newNodeIdMap.get(edge.source);
|
||||
const newTarget = newNodeIdMap.get(edge.target);
|
||||
if (newSource && newTarget) {
|
||||
newEdges.push({
|
||||
...edge,
|
||||
id: `edge_${genShortId()}`,
|
||||
source: newSource,
|
||||
target: newTarget
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 store:取消其他节点选中,添加新内容
|
||||
store.updateNodes((nodes) => {
|
||||
const unselected = nodes.map((n) => ({ ...n, selected: false }));
|
||||
return [...unselected, ...newNodes];
|
||||
});
|
||||
|
||||
store.updateEdges((edges) => {
|
||||
const unselected = edges.map((n) => ({ ...n, selected: false }));
|
||||
return [...unselected, ...newEdges];
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
copyHandler,
|
||||
pasteHandler
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useDeleteEdge = () => {
|
||||
const deleteEdge = (id: string) => {
|
||||
store.removeEdge( id);
|
||||
};
|
||||
return {
|
||||
deleteEdge
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useDeleteNode = () => {
|
||||
const deleteNode = (id: string) => {
|
||||
store.removeNode(id);
|
||||
store.updateEdges(edges => edges.filter(edge => edge.source !== id && edge.target !== id))
|
||||
};
|
||||
return {
|
||||
deleteNode
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import {store} from '../../store/stores.svelte';
|
||||
|
||||
export const useEnsureParentInNodesBefore = () => {
|
||||
const ensureParentInNodesBefore = (parentNodeId: string, childNodeId: string) => {
|
||||
store.updateNodes((nodeArray) => {
|
||||
let parentIndex = -1;
|
||||
for (let i = 0; i < nodeArray.length; i++) {
|
||||
if (nodeArray[i].id === parentNodeId) {
|
||||
parentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (parentIndex <= 0) {
|
||||
return nodeArray;
|
||||
}
|
||||
|
||||
let firstChildIndex = -1;
|
||||
for (let i = 0; i < parentIndex; i++) {
|
||||
if (nodeArray[i].parentId === parentNodeId || nodeArray[i].id === childNodeId) {
|
||||
firstChildIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstChildIndex == -1) {
|
||||
return nodeArray;
|
||||
}
|
||||
|
||||
const parentNode = nodeArray[parentIndex];
|
||||
for (let i = parentIndex; i > firstChildIndex; i--) {
|
||||
nodeArray[i] = nodeArray[i - 1];
|
||||
}
|
||||
nodeArray[firstChildIndex] = parentNode;
|
||||
|
||||
return nodeArray;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
ensureParentInNodesBefore
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useGetEdgesBySource = () => {
|
||||
const getEdgesBySource = (target: string) => {
|
||||
const edges = store.getEdges();
|
||||
return edges.filter((edge) => edge.source === target);
|
||||
};
|
||||
return {
|
||||
getEdgesBySource
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useGetEdgesByTarget = () => {
|
||||
const getEdgesByTarget = (target: string) => {
|
||||
const edges = store.getEdges();
|
||||
return edges.filter((edge) => edge.target === target);
|
||||
};
|
||||
return {
|
||||
getEdgesByTarget
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useGetNode = () => {
|
||||
const getNode = (id: string) => {
|
||||
return store.getNode(id);
|
||||
};
|
||||
return {
|
||||
getNode
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useGetNodeRelativePosition = () => {
|
||||
const getNodeRelativePosition = (parentNodeId: string) => {
|
||||
let node = store.getNode(parentNodeId);
|
||||
const position = { x: 0, y: 0 };
|
||||
while (node) {
|
||||
position.x += node.position.x;
|
||||
position.y += node.position.y;
|
||||
|
||||
if (node.parentId) {
|
||||
node = store.getNode(node.parentId);
|
||||
} else {
|
||||
node = undefined;
|
||||
}
|
||||
}
|
||||
return position;
|
||||
};
|
||||
return {
|
||||
getNodeRelativePosition
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import type {Edge, Node} from '@xyflow/svelte';
|
||||
|
||||
export const useGetNodesFromSource = () => {
|
||||
const getEdgesBySource = (target: string, edges: Edge[]) => {
|
||||
return edges.filter(
|
||||
// 排除循环节点的子节点,否则在多层循环嵌套时不正确
|
||||
(edge) => edge.source === target && edge.sourceHandle !== 'loop_handle'
|
||||
);
|
||||
};
|
||||
|
||||
const getNodesFromSource = (sourceNodeId: string) => {
|
||||
const edges = store.getEdges();
|
||||
const result: Node[] = [];
|
||||
|
||||
let edgesFromSource = getEdgesBySource(sourceNodeId, edges);
|
||||
while (edgesFromSource.length > 0) {
|
||||
const newEdgesFromSource: Edge[] = [];
|
||||
edgesFromSource.forEach((edge) => {
|
||||
result.push(store.getNode(edge.target)!);
|
||||
newEdgesFromSource.push(...getEdgesBySource(edge.target, edges));
|
||||
});
|
||||
edgesFromSource = newEdgesFromSource;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
getNodesFromSource
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import {type Edge, type Node, useNodesData, useStore} from '@xyflow/svelte';
|
||||
import type {Parameter} from '#types';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
|
||||
const fillRefNodeIds = (refNodeIds: string[], currentNodeId: string, edges: Edge[]) => {
|
||||
for (const edge of edges) {
|
||||
if (edge.target === currentNodeId && edge.source) {
|
||||
refNodeIds.push(edge.source);
|
||||
fillRefNodeIds(refNodeIds, edge.source, edges);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getChildren = (params: any, parentId: string, nodeIsChildren: boolean) => {
|
||||
if (!params || params.length === 0) return [];
|
||||
return params.map((param: any) => ({
|
||||
label:
|
||||
param.name +
|
||||
(nodeIsChildren
|
||||
? ` (Array<${param.dataType || 'String'}>)`
|
||||
: ` (${param.dataType || 'String'})`),
|
||||
value: parentId + '.' + param.name,
|
||||
children: getChildren(param.children, parentId + '.' + param.name, nodeIsChildren)
|
||||
}));
|
||||
};
|
||||
|
||||
const nodeToOptions = (node: Node, nodeIsChildren: boolean, currentNode: Node) => {
|
||||
if (node.type === 'startNode') {
|
||||
const parameters = node.data.parameters as Array<Parameter>;
|
||||
const children = [];
|
||||
if (parameters)
|
||||
for (const parameter of parameters) {
|
||||
children.push({
|
||||
label:
|
||||
parameter.name +
|
||||
(nodeIsChildren
|
||||
? ` (Array<${parameter.dataType || 'String'}>)`
|
||||
: ` (${parameter.dataType || 'String'})`),
|
||||
value: node.id + '.' + parameter.name
|
||||
});
|
||||
}
|
||||
return {
|
||||
label: node.data.title,
|
||||
value: node.id,
|
||||
children
|
||||
};
|
||||
} else if (node.type === 'loopNode' && currentNode.parentId) {
|
||||
return {
|
||||
label: node.data.title,
|
||||
value: node.id,
|
||||
children: [
|
||||
{
|
||||
label: 'loopItem',
|
||||
value: node.id + '.loopItem'
|
||||
},
|
||||
{
|
||||
label: 'index (Number)',
|
||||
value: node.id + '.index'
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
const outputDefs = node.data.outputDefs;
|
||||
if (outputDefs) {
|
||||
return {
|
||||
label: node.data.title,
|
||||
value: node.id,
|
||||
children: getChildren(outputDefs, node.id, nodeIsChildren)
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const useRefOptions: any = (useChildrenOnly: boolean = false) => {
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
const currentNode = useNodesData(currentNodeId);
|
||||
const { nodes, edges, nodeLookup } = $derived(useStore());
|
||||
|
||||
let selectItems = $derived.by(() => {
|
||||
const resultOptions = [];
|
||||
if (!currentNode.current) {
|
||||
return [];
|
||||
}
|
||||
|
||||
//通过 nodeLookup.get 才会得到有 parentId 的 node
|
||||
const cNode = nodeLookup.get(currentNodeId)!;
|
||||
|
||||
if (useChildrenOnly) {
|
||||
for (const node of nodes) {
|
||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
||||
if (nodeIsChildren) {
|
||||
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
|
||||
nodeOptions && resultOptions.push(nodeOptions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const refNodeIds: string[] = [];
|
||||
fillRefNodeIds(refNodeIds, currentNodeId, edges);
|
||||
|
||||
for (const node of nodes) {
|
||||
if (refNodeIds.includes(node.id)) {
|
||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
||||
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
|
||||
nodeOptions && resultOptions.push(nodeOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultOptions;
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return selectItems;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
|
||||
export const useUpdateEdgeData = () => {
|
||||
const updateEdgeData = (id: string, dataUpdate: any, options?: { replace: boolean }) => {
|
||||
const edge = store.getEdge(id);
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(edge) : dataUpdate;
|
||||
edge.data = options?.replace ? nextData : { ...edge.data, ...nextData };
|
||||
|
||||
store.updateEdges((edges) => {
|
||||
return edges.map((e) => {
|
||||
if (e.id === id) {
|
||||
return edge;
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
};
|
||||
return {
|
||||
updateEdgeData
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user