Files
EasyFlow/easyflow-ui-admin/packages/tinyflow-vue/src/Tinyflow.vue
陈子默 aedefe6b5e perf: 优化智能体与工作流幕布渲染性能
- 分阶段加载智能体配置并按需缓存 MCP 工具

- 合并画布状态更新与节点尺寸监听,启用大图可视区域渲染和静态连线

- 隔离 Tinyflow Store 实例并补充数据同步与回归测试
2026-07-27 18:27:01 +08:00

213 lines
4.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div ref="divRef" :class="['tinyflow', className]" :style="style" />
</template>
<script setup lang="ts">
import type { TinyflowOptions } from '@tinyflow-ai/ui';
import { Tinyflow as TinyflowNative } from '@tinyflow-ai/ui';
import '@tinyflow-ai/ui/dist/index.css';
import {
getCurrentInstance,
nextTick,
onMounted,
onUnmounted,
ref,
useAttrs,
watch,
} from 'vue';
import { createTinyflowDataSync } from './dataSync';
const props = defineProps<
{
className?: string;
style?: Record<string, string>;
} & Omit<TinyflowOptions, 'element'>
>();
const divRef = ref<HTMLDivElement | null>(null);
const attrs = useAttrs();
const instance = getCurrentInstance();
let tinyflow: TinyflowNative | null = null;
let mountedDataReady = false;
const dataSync = createTinyflowDataSync();
const optionalBooleanOptionKeys = new Set([
'readonly',
'hideBottomDock',
'hideEdgePanel',
'hideMiniMap',
'hideNodeHandles',
'hideNodeToolbar',
'hideNodePicker',
'hideNodeSetting',
'hideEdgeMarkers',
'edgeAnimated',
'nodesDraggable',
'nodesConnectable',
'elementsSelectable',
'onlyRenderVisibleElements',
'dropEnabled',
]);
function normalizeOptionKey(key: string) {
return key.replace(/-([a-z])/g, (_match: string, letter: string) =>
letter.toUpperCase(),
);
}
function normalizeOptions(source: Record<string, unknown>) {
return Object.fromEntries(
Object.entries(source).map(([key, value]) => [
normalizeOptionKey(key),
value,
]),
);
}
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);
}),
);
}
onMounted(() => {
if (divRef.value) {
// 净化 props.data避免响应式对象或函数污染
const cleanedProps = {
...normalizeOptions(attrs),
...normalizeProps(props),
} as any;
if ('data' in cleanedProps && cleanedProps.data != null) {
cleanedProps.data = dataSync.cloneExternalDataIfChanged(cleanedProps.data);
}
(['onDataChange', 'onDataCommit'] as const).forEach((callbackKey) => {
const callback = cleanedProps[callbackKey];
if (typeof callback !== 'function') {
return;
}
cleanedProps[callbackKey] = (
data: TinyflowOptions['data'],
...args: unknown[]
) => {
dataSync.markNativeData(data);
callback(data, ...args);
};
});
tinyflow = new TinyflowNative({
...cleanedProps,
element: divRef.value,
});
mountedDataReady = true;
}
});
onUnmounted(() => {
if (tinyflow) {
tinyflow.destroy();
tinyflow = null;
}
mountedDataReady = false;
dataSync.reset();
});
watch(
() => props.theme,
(theme) => {
if (tinyflow) {
tinyflow.setTheme(theme || 'light');
}
},
);
watch(
() => props.data,
(data) => {
if (
tinyflow &&
mountedDataReady &&
data != null &&
typeof data !== 'string'
) {
const clonedData = dataSync.cloneExternalDataIfChanged(data);
if (clonedData) {
tinyflow.updateData(clonedData, { preserveViewport: true });
}
}
},
{ deep: true },
);
const getData = () => {
if (tinyflow) {
return tinyflow.getData();
}
console.warn('Tinyflow instance is not initialized');
return null;
};
const getInstance = () => {
if (tinyflow) {
return tinyflow;
}
console.warn('Tinyflow instance is not initialized');
return null;
};
const focusNode = async (
nodeId: string,
options?: { duration?: number; zoom?: number },
) => {
if (tinyflow) {
return tinyflow.focusNode(nodeId, options);
}
console.warn('Tinyflow instance is not initialized');
return false;
};
const fitView = async (options?: { duration?: number; padding?: number }) => {
if (tinyflow) {
await nextTick();
return tinyflow.fitView(options);
}
console.warn('Tinyflow instance is not initialized');
return false;
};
const updateData = (
data: TinyflowOptions['data'],
options?: { preserveViewport?: boolean },
) => {
if (tinyflow && data != null && typeof data !== 'string') {
const clonedData = dataSync.cloneExternalDataIfChanged(data);
if (!clonedData) {
return true;
}
return tinyflow.updateData(clonedData, options);
}
console.warn('Tinyflow instance is not initialized');
return false;
};
defineExpose({
getData,
getInstance,
focusNode,
fitView,
updateData,
});
</script>