注意:写文章的时候,我用的xflow版本是1.x的
1. 自定义位置
<CanvasToolbar
position={{ top: -ToolBarHeight, left: 0, right: 0, height: ToolBarHeight }}
/>
注意的是,这个位置是相对于XFlowCanvas组件位置的
<XFlowCanvas
position={{ top: ToolBarHeight }}
>
<CanvasToolbar
position={{ top: -ToolBarHeight, left: 0, right: 0, height: ToolBarHeight }}
/>
</XFlowCanvas>
上面的例子中,画布向下移动了ToolBarHeight像素,工具栏向上移动了ToolBarHeight像素。
2. 自定义工具栏元素
<CanvasToolbar
// 在config里面自定义
config={toolbarConfig()}
/>
// toolbarConfig
import { createToolbarConfig } from '@antv/xflow';
import Toolbar from '../toolbar';
export default createToolbarConfig((config, proxy) => {
config.setCustomToolbarRender(async (modelService, updateComponent) => {
// Toolbar就是普通的react组件
updateComponent(Toolbar);
// 一定要返回toolbar
return Toolbar;
});
});
自定义工具栏后,你就丢失了xflow自带的功能按钮,你得自己实现一遍。
3. 自定义工具栏功能
3.1. 按钮切换 (自定义model的使用)
这个编辑开关,做简单点就是组件内部的状态,复杂点就是组件外部也能控制是否可以编辑。
内部就不说了,就是setState。这里讲讲外部控制的思路,还记得第一章的时候那个数据流图吗,这里我们就用到model来存储改变可编辑状态。
3.1.1. 注册自定义model
自定义工具栏元素很难传递props,所以直接使用model来监听属性变化更方便。
<XFlow
// 这个就是注册model的地方
modelServiceConfig={modelServiceConfig()}
>
<XFlowCanvas>
<CanvasToolbar/>
</XFlowCanvas>
</XFlow>
// modelServiceConfig
import { createModelServiceConfig, Disposable, DisposableCollection } from '@antv/xflow';
// 图只读状态
export const GraphReadOnlyModel = {
id: 'GraphReadOnly',
DefaultReadOnly: true,
};
// 大部分都是模板代码,不需要深究为什么这么写
// 要深究可以看看源码,就是对rxjs的一些操作封装了一下
export default createModelServiceConfig((config) => {
config.registerModel((registry, graph) => {
const list: Disposable[] = [
// 主要就是这里
// 这个数组可以注册很多和model,写法都是这样
registry.registerModel({
// id,唯一字符串就行
id: GraphReadOnlyModel.id,
// 初始值函数,用来设定初始值
getInitialValue: () => GraphReadOnlyModel.DefaultReadOnly,
}),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(list);
return toDispose;
});
});
3.1.2. 获取model
在XFlow内部的组件可以通过一些hook获取指定的model。
useModelAsync函数用来获取model,同时监听model变化,返回一个state。等于是结合了model和useState。
// 获取xflowapp实例
const app: IApplication = useXFlowApp();
// 返回[state, setState, model本身]
const [graphReadOnly, _, graphReadOnlyModel] = useModelAsync({
// 获取model的方法
// 这个获取方法都是固定的
// id就是上面我们定义的id
getModel: async () => await app.modelService.awaitModel(GraphReadOnlyModel.id),
// state初始值
initialState: GraphReadOnlyModel.DefaultReadOnly,
});
3.1.3. 使用model
使用就和普通的state一样,这里说一下怎么修改model。
// useModelAsync返回的第三个参数就是model本身
// model里面有方法可以改变值
graphReadOnlyModel.setValue(true);
结合上面三步,我们就可以在工具栏之外(但是要在XFlow上下文中)控制里面的状态。
3.2. 新建节点
很简单就是调用命令就行,
const add = useCallback(async () => {
const nodeId = numberId();
const graph = await app.getGraphInstance();
// 设置位置
const { x, y } = graph.graphToLocal(100, 100);
// 触发XFlowNodeCommands.ADD_NODE命令,这个executeCommand方法文档上有,就是执行命令的意思
app.commandService.executeCommand<NsNodeCmd.AddNode.IArgs>(XFlowNodeCommands.ADD_NODE.id, {
// 参数就是node的一些参数
nodeConfig: {
id: nodeId,
label: '',
renderKey: 'NODE',
x,
y,
width: 180,
height: 40,
extra: { name: '新建节点', id: nodeId },
ports: [
{
id: numberId(5),
type: NsGraph.AnchorType.INPUT,
group: NsGraph.AnchorGroup.TOP,
tooltip: '输入',
},
{
id: numberId(5),
type: NsGraph.AnchorType.OUTPUT,
group: NsGraph.AnchorGroup.BOTTOM,
tooltip: '输出',
},
],
},
});
}, []);
3.2.1 节点位置
这里值得注意的是设置位置那里,如果不设置位置,当你移动画布之后新建节点的位置始终在固定的位置,而不是相对的位置。
graphToLocal的意思是,将画布坐标转化成画布本地坐标。
这里解释一下:
- 画布坐标:就是整个画布的坐标,它不考虑画布的平移缩放
- 画布本地坐标
local,考虑了平移缩放,是一个相对值
如下图,红色框就是我们可以看到的部分,黑色框就是整个画布。现在我们拖动了画布,将画布往上移动了一点。图中的(0,0)坐标就是两种的区别。
所以为了,拖动画布的情况下,新建的节点仍然是在可见范围里面,我们需要指定节点位置。
3.3. 删除节点
选中节点后删除,可以支持多选删除。
const del = useCallback(async () => {
// 获取所有已经选择的cell
// cell就是node和edge的父类
const cells: Cell[] = await MODELS.SELECTED_CELLS.useValue(app.modelService);
// 遍历删除
await Promise.all(
cells.map((cell) => {
const isEdge = cell.isEdge();
const isNode = cell.isNode();
if (isEdge || isNode) {
// 删除边和删除节点的参数不一样,所以需要判断一下
return app.commandService.executeCommand(
isEdge ? XFlowEdgeCommands.DEL_EDGE.id : XFlowNodeCommands.DEL_NODE.id,
isEdge
? {
edgeConfig: { id: cell.id },
}
: {
nodeConfig: {
id: cell.id,
},
},
);
}
return [];
}),
);
}, []);
3.3.1. 删除按钮根据选择判断激活
选择了节点之后,删除按钮才会激活。原理就是使用model监听选择节点,这个model是自带的。文档上有
const [isNodeSelected] = useModelAsync({
getModel: async () => await MODELS.IS_NODE_SELECTED.getModel(app.modelService),
initialState: false,
});
3.4. 群组操作
可以新建解散分组。
3.4.1. 新建群组
const createGroup = useCallback(async () => {
const cells = await MODELS.SELECTED_CELLS.useValue(app.modelService);
const groupChildren: string[] = [];
// 收集应该加入群组的节点
cells.forEach((cell) => {
if (cell.isNode()) {
groupChildren.push(cell.id);
}
});
// 新建群组命令
app.commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(XFlowGroupCommands.ADD_GROUP.id, {
// 这里配置的是群组节点
// 群组其实也是一种节点
nodeConfig: {
id: numberId(),
renderKey: 'GROUPNODE',
groupChildren,
groupCollapsedSize: { width: 200, height: 40 },
label: '新建群组',
},
});
}, []);
3.4.2. 解散群组
const unGroup = useCallback(async () => {
const cell = await MODELS.SELECTED_NODE.useValue(app.modelService);
if (cell) {
const nodeConfig = cell.getData();
app.commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(
XFlowGroupCommands.DEL_GROUP.id,
{
nodeConfig: nodeConfig,
},
);
}
}, []);
这里还有一个边界条件,能够解散的节点要判断一下是不是群组节点:
const [isGroupSelected] = useModelAsync({
// IS_GROUP_SELECTED是xflow自带的model
getModel: async () => await MODELS.IS_GROUP_SELECTED.getModel(app.modelService),
initialState: false,
});
3.4.3. 折叠群组
这个功能xflow是有bug的,在嵌套群组的条件下,直接使用折叠命令会有显示问题。
- 嵌套元素没有隐藏完
翻看源码可以发现,原始的折叠代码只是隐藏了直接子节点,但是子节点是群组的情况,并没有再隐藏群组里面的节点。
所以我们需要自定义这个命令,也就是自己写命令。
自定义命令也是一堆模板代码,直接复制原始的COLLAPSE_GROUP命令代码修改。
下面代码大部分是复制的,看着有点复杂,但是只需要关注修改部分,修改部分有注释标注。
3.4.3.1. 命令实现类
import type { Cell, Graph, Node as X6Node } from '@antv/x6';
import {
Disposable,
IArgsBase,
ICommandContextProvider,
ICommandContributionConfig,
ICommandHandler,
IHooks,
NsGraph,
} from '@antv/xflow';
import type { HookHub } from '@antv/xflow-hook';
import { inject, injectable } from 'mana-syringe';
interface IToggleGroupCollapseService {
(args: ToggleCollapseGroupCommandArgs): Promise<boolean>;
}
// 命令的参数
export interface ToggleCollapseGroupCommandArgs extends IArgsBase {
/** 折叠的group node id */
nodeId: string;
/** 是否折叠 */
isCollapsed: boolean;
/** 折叠后的大小 */
collapsedSize?: { width: number; height: number };
/** 间距 */
gap?: number;
/** 更新群组是否折叠的状态,返回false时不执行 */
toggleService?: IToggleGroupCollapseService;
}
export interface ToggleCollapseGroupCommandArgsResult {
err: null | string;
}
export interface ToggleCollapseGroupCommandArgsCmdHooks extends IHooks {
collapseGroup: HookHub<ToggleCollapseGroupCommandArgs, ToggleCollapseGroupCommandArgsResult>;
}
type ICommand = ICommandHandler<
ToggleCollapseGroupCommandArgs,
ToggleCollapseGroupCommandArgsResult,
ToggleCollapseGroupCommandArgsCmdHooks
>;
@injectable()
/** 添加子节点命令 */
export class ToggleCollapseGroupCommand implements ICommand {
@inject(ICommandContextProvider) contextProvider: ICommand['contextProvider'];
toggleVisible = (cells: Cell[], visible: boolean, graph: Graph) => {
cells.forEach((cell) => {
const view = graph.findViewByCell(cell)!.container as HTMLElement;
view.style.visibility = visible ? 'visible' : 'hidden';
});
};
//***************************** 自定义代码开始*****************************//
// 递归获取子节点,并隐藏,这样才能获取正确的相对位置
toggleChildrenCollapse(
groupNode: X6Node,
graph: Graph,
args: ToggleCollapseGroupCommandArgs,
// 是否在递归里面
// 判断这个可以间接的知道,此时处理的是不是嵌套群组的子节点
inRecursion = false,
) {
const childrens = groupNode.getChildren()!.filter((n) => n.isNode()) as X6Node[];
const { isCollapsed, gap = 0 } = args;
if (childrens) {
childrens.forEach((item) => {
/**
先隐藏群组的子节点
*/
// 群组折叠除了隐藏节点和边,还需要记住原本的位置
// 不然再次展开的时候位置就是错的
// 这里的节点位置都是相对于父节点的位置,所以父节点不能比子节点先隐藏,不然获取的位置就是不正确的
if (item.getData().isGroup) {
this.toggleChildrenCollapse(item, graph, args, true);
}
/**
再隐藏群组节点
*/
const position = groupNode.position();
// 获取需要隐藏的边
let innerEdges = graph.getConnectedEdges(item).filter((edge) => {
const sourceNode = edge.getSourceNode();
const targetNode = edge.getTargetNode();
// 如果在递归中(嵌套群组的子节点)
// 那么所有的边都隐藏
return inRecursion
? true
// 否则就只隐藏群组内部节点之间的边
: childrens.includes(sourceNode!) && childrens.includes(targetNode!);
});
// 需要折叠
if (isCollapsed) {
// 把边和节点隐藏
this.toggleVisible([item, ...innerEdges], false, graph);
// 把节点折叠之前的尺寸和相对位置都保存在他自己身上
item.prop('previousSize', item.size());
item.prop('previousRelativePosition', item.position({ relative: true }));
// 下面是复制的
// 估计是为了gap这个属性
item.position(position.x + gap, position.y + gap);
const size = groupNode.size();
item.size({
width: size.width - gap * 2,
height: size.height - gap * 2,
});
} else {
// 展开的时候
this.toggleVisible([item, ...innerEdges], true, graph);
const pos = item.prop('previousRelativePosition');
const size = item.prop('previousSize');
// 读取之前存储的位置和大小,重新设置上去
// position第二个参数可以设置位置为相对位置
item.position(pos.x, pos.y, { relative: true, deep: true });
item.size(size);
}
});
}
}
toggleCollapse = (groupNode: X6Node, graph: Graph, args: ToggleCollapseGroupCommandArgs) => {
const groupData = groupNode.getData<NsGraph.INodeConfig>();
const { isCollapsed, gap = 0 } = args;
// 保存群组尺寸
if (isCollapsed) {
const collapsedSize = args.collapsedSize ||
groupData.groupCollapsedSize || { width: 180, height: 36 };
groupNode.prop('previousSize', groupNode.size());
groupNode.size(collapsedSize);
} else {
groupNode.size(groupNode.prop('previousSize'));
}
// !!!!!!!!!!关键修改这里,增加了一个递归!!!!!
// 隐藏子节点
this.toggleChildrenCollapse(groupNode, graph, args);
groupNode.prop('isCollapsed', isCollapsed);
groupNode.setData({
...groupNode.getData(),
isCollapsed,
});
};
//***************************** 自定义代码结束*****************************//
// 命令调用函数
execute = async () => {
const ctx = this.contextProvider();
const { args, hooks: runtimeHook } = ctx.getArgs();
const hooks = ctx.getHooks();
const result = await hooks.collapseGroup.call(
args,
async (handlerArgs) => {
const x6Graph = await ctx.getX6Graph();
const node = x6Graph.getCellById(args.nodeId) as X6Node;
const { toggleService } = handlerArgs;
if (toggleService) {
const canToggle = await toggleService(handlerArgs);
if (!canToggle) return { err: 'service rejected' };
}
if (node) {
this.toggleCollapse(node, x6Graph, args);
ctx.addUndo(
Disposable.create(async () => {
if (node) {
this.toggleCollapse(
node,
x6Graph,
Object.assign(args, { isCollapsed: !args.isCollapsed }),
);
}
}),
);
}
return { err: null };
},
runtimeHook,
);
ctx.setResult(result!);
return this;
};
// 下面都是一些历史操作相关的,用来控制是否可以撤销恢复的逻辑
undo = async () => {
const ctx = this.contextProvider();
ctx.undo();
return this;
};
redo = async () => {
const ctx = this.contextProvider();
if (!ctx.isUndoable) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}
// 就是一些配置,比如id啥的方便之后使用
const ToggleCollapseGroupCmd: ICommandContributionConfig = {
/** Command: 用于注册named factory */
command: {
id: 'xflow:toggle-collapseGroup',
label: '折叠展开群组',
category: '节点操作',
},
/** hook name */
hookKey: 'toggleCollapseGroup',
CommandHandler: ToggleCollapseGroupCommand,
};
export default ToggleCollapseGroupCmd;
这段代码有几个注意的点:
toggleVisible方法中,隐藏节点使用的是设置visibility,而不是x6文档中的setVisible方法,因为setVisible是真的会移除元素,当有大量元素的时候,频繁移除重建元素会有点卡。- 主要起作用的是
toggleChildrenCollapse方法,这个方法不仅要处理子节点是群组的情况,还要注意节点原本的相对位置。
3.4.3.2. 注册命令
<XFlow
commandConfig={commandConfig()}
></XFlow>
// commandConfig
import { createCmdConfig, Disposable, DisposableCollection } from '@antv/xflow';
import ToggleCollapseGroup from '../commands/toggleCollapseGroup';
export default createCmdConfig((config) => {
config.setCommandContributions(() => [ToggleCollapseGroup]);
});
3.4.3.3. 使用命令
因为我们自定义了群组折叠功能,所以我们需要自定义一个群组元素来调用这个命令
import { Cell } from '@antv/x6';
import { NsGraph, useXFlowApp } from '@antv/xflow';
import ToggleCollapseGroupCmd, {
ToggleCollapseGroupCommandArgs,
} from '../../commands/toggleCollapseGroup';
const GraphGroupNode: NsGraph.INodeRender<NsGraph.INodeConfig> = (props) => {
const cell: Cell = props.cell;
const app = useXFlowApp();
// 这个data就是命令里面setData设置的
const isCollapsed = props.data.isCollapsed || false;
// 假设某个按钮点击之后展开
const onExpand = () => {
app.executeCommand(ToggleCollapseGroupCmd.command.id, {
nodeId: cell.id,
isCollapsed: false,
collapsedSize: { width: 200, height: 40 },
gap: 3,
} as ToggleCollapseGroupCommandArgs);
};
// 折叠
const onCollapse = () => {
app.executeCommand(ToggleCollapseGroupCmd.command.id, {
nodeId: cell.id,
isCollapsed: true,
collapsedSize: { width: 200, height: 40 },
gap: 3,
} as ToggleCollapseGroupCommandArgs);
};
return (
<div>
</div>
);
};
export default GraphGroupNode;
自定义节点使用文档上有,就是在graphConfig中注册一下
// 群组节点
config.setNodeRender('GROUPNODE', GroupNode);
3.4.3.4. 最简指令模板
上面的代码有点多,这里给一个模板,一个什么也不做的空指令
// 空的一个命令,可以用来在executeCommandPipeline执行任何命令
import type { HookHub, ICmdHooks as IHooks, ICommandContributionConfig } from '@antv/xflow';
import { ManaSyringe } from '@antv/xflow';
import type { IArgsBase, ICommandHandler } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
export interface LoopCommandArgs extends IArgsBase {
loop: (ctx: ReturnType<ICommand['contextProvider']>) => any;
}
export interface LoopCommandResult {
[key: string]: any;
}
export interface LoopCommandHooks extends IHooks {
loop: HookHub<LoopCommandArgs, LoopCommandResult>;
}
type ICommand = ICommandHandler<LoopCommandArgs, LoopCommandResult, LoopCommandHooks>;
@ManaSyringe.injectable()
/** 部署画布数据 */
export class LoopCommand implements ICommand {
@ManaSyringe.inject(ICommandContextProvider) contextProvider: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
const {
args: { loop },
} = ctx.getArgs();
await loop?.(ctx);
return this;
};
/** undo cmd */
undo = async () => {
//const ctx = this.contextProvider();
//ctx.undo();
return this;
};
/** redo cmd */
redo = async () => {
//const ctx = this.contextProvider();
//if (!ctx.isUndoable) {
// await this.execute();
// }
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}
const LoopCmd: ICommandContributionConfig = {
command: {
id: 'xflow:loop',
label: '空操作',
category: '空操作',
},
// 这个命令hook的配置
/** hook name */
hookKey: 'loop',
CommandHandler: LoopCommand,
};
export default LoopCmd;
3.5. 导出图片
建议使用自带的api,不要用htmnl2canvas之类的,因为x6的元素都是svg,而且自定义元素使用了foreignObject标签,导出有点麻烦。
const exportImage = useCallback(async () => {
const dataUri = await exportGraph2PNG(app);
const a = document.createElement('a');
a.href = dataUri;
a.download = `文件名.png`;
a.click();
}, []);
// exportGraph2PNG
export function exportGraph2PNG<T extends boolean>(
app: IApplication,
options?: ExportGraph2JPGOptions<T>,
): Promise<Blob | string> {
// toPNG有回调函数,所以用promise包了一下
return new Promise((resolve) => {
app.getGraphInstance().then((graph) => {
graph.toPNG(
// 参数1,回调返回的是一个datauri
// 类似data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAF
(dataUri: string) => {
if (options?.toBlob) {
// DataUri是x6自带的工具类
// 把datauri转化为blob方便下载
resolve(DataUri.dataUriToBlob(dataUri));
return;
}
resolve(dataUri);
},
// 参数2是一些配置,具体可以看看x6官网
// 大概就是设置导出图像的显示范围,一般就是全部
{
preserveDimensions: {
width: document.body.clientWidth,
height: document.body.clientHeight,
},
viewBox: {
x: 0,
y: 0,
width: document.body.clientWidth,
height: document.body.clientHeight,
},
// 这是在生成图像之前做处理
beforeSerialize(svg) {
// 比如我可以删掉群组节点的展开按钮
const groupNodeSpanIcon = svg.querySelectorAll(
'div[class*="xflow-group-node"] span[role="img"]',
);
groupNodeSpanIcon.forEach((ele) => ele.remove());
},
},
);
});
});
}
3.6. 保存图数据
很常见的功能就是保存图数据上传至后端
const saveData = useCallback(async () => {
setLoading(true);
app.commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: async (meta, graphData) => {
try {
// 请求后端
} catch (err) {
console.log(err);
message.error('保存失败');
} finally {
setLoading(false);
}
},
},
);
}, []);
但是这样的话不好封装组件,这样相当于写死在toolbar组件里面了,不同页面请求的接口参数都不一样所以我想了一个方法,把回调函数放进x6配置里面,在这里面调用。其实相同的思路可以放在其他地方。
// 封装的Graph组件
// 接受参数onSaveData
const Graph = ({onSaveData}) => {
return <XFlow>
<XFlowCanvas
// 传进去的参数会作为proxy的value
config={getGraphConfig({ onSaveData })}
>
</XFlow>
}
// getGraphConfig
// createGraphConfig函数返回的是一个函数,这个函数的参数可以使用proxy获取
// 其实xflow里面的配置都可以这样传参数
const getGraphConfig = createGraphConfig<GetGraphConfigProps>((config, proxy) => {
/** 设置画布配置项,会覆盖XFlow默认画布配置项 */
config.setX6Config({
// 注入的额外配置
// @ts-ignore
onSaveData: proxy.getValue().onSaveData,
});
});
// toolbar
const saveData = useCallback(async () => {
// 获取x6配置,获取我们传进来的回调函数
const {
x6Options: {
// @ts-ignore
onSaveData,
},
} = await app.getGraphConfig();
app.commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: async (meta, graphData) => {
try {
// 调用回调
await onSaveData?.(app, meta, graphData);
} catch (err) {
console.log(err);
} finally {
}
},
},
);
}, []);
3.7. 复原
不小心操作了图,或者想重新来,这时候复原功能就很有用
复原直接重新设置数据是不够的,因为展示的数据还有额外的布局信息,如果没有布局信息,复原的就是乱的。
我的思路是:
- 在渲染完成之后保存图数据
- 复原的时候重新加载这些数据
3.7.1 保存节点变数据
要做到这一点,你得知道什么时候渲染完成,所以我不让XFlow自动渲染而是手动渲染。
// 自定义一个model用来保存初始数据
import { createModelServiceConfig, Disposable, DisposableCollection } from '@antv/xflow';
// 图默认数据
export const DefaultGraphDataModel = {
id: 'DefaultGraphDataModel',
};
export default createModelServiceConfig((config) => {
config.registerModel((registry, graph) => {
const list: Disposable[] = [
registry.registerModel({
id: DefaultGraphDataModel.id,
getInitialValue: () => null,
}),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(list);
return toDispose;
});
});
// 原本只要给XFlow组件传递graphData属性就能自动渲染,但是这里为了手动渲染就不传
useEffect(() => {
// 从后端请求回来了数据
if (graphData) {
// executeCommandPipeline就是流水线模式执行一系列命令
/**
[
{
命令id,
参数: {
命令id,
async getCommandOption(ctx) {
return 命令参数
}
}
}
]
*/
graphApp.current?.commandService.executeCommandPipeline([
// 设置渲染
{
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
async getCommandOption(ctx) {
return {
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
args: {
graphData,
// 节点渲染完成后的回调
// 同样的功能还可以在render的hook之后
// 但是hook触发的时机不对,并不能保证真的渲染完成
// 这个可以参考GRAPH_RENDER命令的源码
afterRender(graphData, graphMeta) {
const { nodes, edges } = graphData;
// 创建命令列表
const groupCmdList: IGraphPipelineCommand[] = [];
// 所有渲染完成后保存初始数据
groupCmdList.push({
// 这个LoopCmd就是前面那节创建的一个空的命令
commandId: LoopCmd.command.id,
async getCommandOption(ctx) {
return {
commandId: LoopCmd.command.id,
args: {
async loop(ctx) {
const graphData = await graphApp.current?.getGraphData();
// 保存初始数据
const defaultGraphDataModel =
await graphApp.current?.modelService.awaitModel(
DefaultGraphDataModel.id,
);
if (defaultGraphDataModel) {
defaultGraphDataModel.setValue(graphData);
}
},
} as LoopCommandArgs,
};
},
});
graphApp.current?.commandService.executeCommandPipeline(groupCmdList);
},
} as NsGraphCmd.GraphRender.IArgs,
};
},
},
// 设置布局
{
commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
async getCommandOption(ctx) {
return {
commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
args: {
graphData,
...graphLayout,
},
};
},
},
// 设置缩放
{
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
async getCommandOption(ctx) {
return {
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
args: {
factor: 'fit',
zoomOptions: CANVAS_SCALE_TOOLBAR_CONFIG.zoomOptions,
} as NsGraphCmd.GraphZoom.IArgs,
};
},
},
]);
}
}, [graphData]);
3.7.2. 重新加载数据
// 获取原始数据model
const [defaultGraphData] = useModelAsync({
getModel: async () => await app.modelService.awaitModel(DefaultGraphDataModel.id),
initialState: null,
});
// 重置
const resetData = useCallback(async () => {
if (defaultGraphData) {
app.commandService.executeCommandPipeline([
{
// 重新渲染
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
async getCommandOption(ctx) {
return {
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
args: {
graphData: defaultGraphData,
} as NsGraphCmd.GraphRender.IArgs,
};
},
},
{
// 缩放
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
async getCommandOption(ctx) {
return {
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
args: {
factor: 'fit',
zoomOptions: CANVAS_SCALE_TOOLBAR_CONFIG.zoomOptions,
} as NsGraphCmd.GraphZoom.IArgs,
};
},
},
]);
}
}, [defaultGraphData]);