- 分阶段加载智能体配置并按需缓存 MCP 工具 - 合并画布状态更新与节点尺寸监听,启用大图可视区域渲染和静态连线 - 隔离 Tinyflow Store 实例并补充数据同步与回归测试
213 lines
4.7 KiB
Vue
213 lines
4.7 KiB
Vue
<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>
|