workflow底层UI库整合至项目,优化构建逻辑

This commit is contained in:
2026-02-24 11:20:18 +08:00
parent 094b185c49
commit 12accb2575
91 changed files with 6820 additions and 115 deletions

View File

@@ -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
};
};