fix: 修复工作流节点交互与旧浏览器兼容

- 修复 Tinyflow Vue wrapper 可选 boolean 默认关闭节点拖拽和连线的问题

- 为 Tinyflow 增加 structuredClone 兼容与 Chrome 90 构建目标

- 增加节点交互归一化与 Tab 空值防御
This commit is contained in:
2026-06-23 15:59:03 +08:00
parent 999a21e2d9
commit 03ad011f64
11 changed files with 183 additions and 14 deletions

View File

@@ -5,9 +5,18 @@
<script setup lang="ts">
import {Tinyflow as TinyflowNative, TinyflowOptions} from '@tinyflow-ai/ui';
import '@tinyflow-ai/ui/dist/index.css';
import {nextTick, onMounted, onUnmounted, ref, useAttrs, watch} from 'vue';
import {
getCurrentInstance,
nextTick,
onMounted,
onUnmounted,
ref,
useAttrs,
watch,
} from 'vue';
type TinyflowDataOption = Exclude<TinyflowOptions['data'], string | undefined>;
type StructuredCloneFn = <T>(value: T) => T;
const props = defineProps<
{
@@ -18,9 +27,26 @@ const props = defineProps<
const divRef = ref<HTMLDivElement | null>(null);
const attrs = useAttrs();
const instance = getCurrentInstance();
let tinyflow: TinyflowNative | null = null;
let mountedDataReady = false;
let lastAppliedDataSignature = '';
const optionalBooleanOptionKeys = new Set([
'readonly',
'hideBottomDock',
'hideEdgePanel',
'hideMiniMap',
'hideNodeHandles',
'hideNodeToolbar',
'hideNodePicker',
'hideNodeSetting',
'hideEdgeMarkers',
'edgeAnimated',
'nodesDraggable',
'nodesConnectable',
'elementsSelectable',
'dropEnabled',
]);
function normalizeOptionKey(key: string) {
return key.replace(/-([a-z])/g, (_match: string, letter: string) =>
@@ -37,21 +63,49 @@ function normalizeOptions(source: Record<string, unknown>) {
);
}
function getProvidedPropKeys() {
return new Set(
Object.keys(instance?.vnode.props || {}).map((key) => normalizeOptionKey(key)),
);
}
function normalizeProps(source: Record<string, unknown>) {
const providedKeys = getProvidedPropKeys();
return Object.fromEntries(
Object.entries(source).filter(([key, value]) => {
if (!optionalBooleanOptionKeys.has(key)) {
return true;
}
return value !== false || providedKeys.has(key);
}),
);
}
// 安全深拷贝工具函数
function safeDeepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') return obj;
try {
return structuredClone(obj);
const clone = (globalThis as { structuredClone?: StructuredCloneFn })
.structuredClone;
if (clone) {
return clone(obj);
}
} catch {
// Fall through to JSON cloning for browsers without native structuredClone.
}
try {
return JSON.parse(JSON.stringify(obj));
} catch {
try {
return JSON.parse(JSON.stringify(obj));
} catch {
console.warn(
'Failed to clone object, returning original (may cause issues)',
obj,
);
return obj;
} catch {
return obj;
}
}
}
@@ -84,7 +138,7 @@ onMounted(() => {
// 净化 props.data避免响应式对象或函数污染
const cleanedProps = {
...normalizeOptions(attrs),
...props,
...normalizeProps(props),
} as any;
if ('data' in cleanedProps && cleanedProps.data != null) {
cleanedProps.data = cloneDataIfChanged(cleanedProps.data);