Browse Source

feat: 优化节点测量功能,增加从 DOM 获取节点尺寸的支持

feature/add_provider_popi
huangmin 1 month ago
parent
commit
f1f31dbd29
  1. 118
      src/components/PopiaiEmbedBridge.tsx

118
src/components/PopiaiEmbedBridge.tsx

@ -4,7 +4,8 @@ import { useEffect, useMemo } from "react";
import { useWorkflowStore } from "@/store/workflowStore";
import type { WorkflowFile } from "@/store/workflowStore";
import type { EditOperation } from "@/lib/chat/editOperations";
import type { WorkflowEdge, WorkflowNode } from "@/types";
import { getDefaultNodeDimensions } from "@/constants/nodeDimensions";
import type { NodeType, WorkflowEdge, WorkflowNode } from "@/types";
const SOURCE_POPIAI = "popiai";
const SOURCE_POPITV = "popitv";
@ -28,39 +29,132 @@ interface BridgeMessage {
interface NodeDimensions {
id: string;
x: number;
y: number;
width: number;
height: number;
}
function measureNodes(nodeIds: string[]): NodeDimensions[] {
const state = useWorkflowStore.getState();
function measureNodesFromDom(nodeIds: string[]): NodeDimensions[] {
const measurements: NodeDimensions[] = [];
for (const nodeId of nodeIds) {
const node = state.nodes.find(n => n.id === nodeId);
if (node?.measured) {
measurements.push({
id: nodeId,
width: node.measured.width ?? 0,
height: node.measured.height ?? 0,
});
} else {
// 如果 measured 不存在,尝试从 DOM 获取
// 使用 try-catch 包装,避免 DOM 操作可能抛出的异常
try {
// 直接从 DOM 获取节点尺寸,避免访问 store 可能触发的状态更新
const nodeElement = document.querySelector(`[data-id="${nodeId}"]`);
if (nodeElement) {
const rect = nodeElement.getBoundingClientRect();
measurements.push({
id: nodeId,
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
});
} else {
// 如果 DOM 元素不存在,使用默认尺寸
measurements.push({
id: nodeId,
x: 0,
y: 0,
width: 0,
height: 0,
});
}
} catch {
measurements.push({
id: nodeId,
x: 0,
y: 0,
width: 0,
height: 0,
});
}
}
return measurements;
}
const MEASUREMENT_CACHE_TTL_MS = 500;
const nodeMeasurementCache = new Map<string, NodeDimensions & { timestamp: number }>();
function readPositiveNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
}
return null;
}
function dimensionsFromStoreNode(node: WorkflowNode): NodeDimensions {
const nodeWithRuntimeSize = node as WorkflowNode & {
width?: unknown;
height?: unknown;
style?: { width?: unknown; height?: unknown };
};
const defaults = getDefaultNodeDimensions(node.type as NodeType);
return {
id: node.id,
x: node.position.x,
y: node.position.y,
width:
readPositiveNumber(node.measured?.width) ??
readPositiveNumber(nodeWithRuntimeSize.width) ??
readPositiveNumber(nodeWithRuntimeSize.style?.width) ??
defaults.width,
height:
readPositiveNumber(node.measured?.height) ??
readPositiveNumber(nodeWithRuntimeSize.height) ??
readPositiveNumber(nodeWithRuntimeSize.style?.height) ??
defaults.height,
};
}
function measureNodes(nodeIds: string[]): NodeDimensions[] {
const now = Date.now();
const stateNodesById = new Map(useWorkflowStore.getState().nodes.map((node) => [node.id, node]));
const measurementsById = new Map<string, NodeDimensions>();
const idsMissingFromStore: string[] = [];
for (const nodeId of nodeIds) {
const node = stateNodesById.get(nodeId);
if (node) {
const dimensions = dimensionsFromStoreNode(node);
measurementsById.set(nodeId, dimensions);
nodeMeasurementCache.set(nodeId, { ...dimensions, timestamp: now });
continue;
}
const cached = nodeMeasurementCache.get(nodeId);
if (cached && now - cached.timestamp < MEASUREMENT_CACHE_TTL_MS) {
measurementsById.set(nodeId, {
id: cached.id,
x: cached.x,
y: cached.y,
width: cached.width,
height: cached.height,
});
continue;
}
idsMissingFromStore.push(nodeId);
}
if (idsMissingFromStore.length > 0) {
for (const dimensions of measureNodesFromDom(idsMissingFromStore)) {
measurementsById.set(dimensions.id, dimensions);
if (dimensions.width > 0 && dimensions.height > 0) {
nodeMeasurementCache.set(dimensions.id, { ...dimensions, timestamp: now });
}
}
}
return nodeIds.map((nodeId) => measurementsById.get(nodeId) ?? { id: nodeId, x: 0, y: 0, width: 0, height: 0 });
}
function isSafeParentOrigin(origin: string | null): origin is string {
if (!origin) return false;
return origin === "file://" || /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin);

Loading…
Cancel
Save