fix: 修复工作流节点交互与旧浏览器兼容
- 修复 Tinyflow Vue wrapper 可选 boolean 默认关闭节点拖拽和连线的问题 - 为 Tinyflow 增加 structuredClone 兼容与 Chrome 90 构建目标 - 增加节点交互归一化与 Tab 空值防御
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user