文章目录
- 前言
- 安装依赖
- [一、Maotu 组件的集成](#一、Maotu 组件的集成)
-
- 1.目录结构设计
- 2.编辑页面核心代码
- 3.预览页面
-
- 3.1预览页面代码
- [3.2 API 请求与 Token 统一封装](#3.2 API 请求与 Token 统一封装)
- 二、自定义流程
- 三、数据更新处理办法
- 四、常见问题与解决方案
- 总结
前言
Maotu是一款功能强大的流程图编辑组件,在我们的项目中发挥了重要作用。它不仅提供了直观的可视化界面,还支持高度自定义的节点类型和交互逻辑,使复杂业务流程的设计变得简单高效。
相比其他流程图工具,Maotu的优势在于:
- 完善的节点自定义能力
- 灵活的数据导入导出
- 与Vue3项目无缝集成
- 支持复杂条件分支和业务规则
安装依赖
javascript
$ npm install maotu
或
$ yarn add maotu
或
$ pnpm install maotu
一、Maotu 组件的集成
1.目录结构设计
javascript
src/
├── views/
│ └── maotu/
│ ├── MtEdit.vue // 编辑页面
│ └── MtPre.vue // 预览页面
├── network/
│ └── maotuRequest.ts // 专用API请求封装
└── router/
└── index.ts // 路由配置
2.编辑页面核心代码

javascript
<template>
<div class="mt-edit-container">
<div v-if="loading" class="loading-overlay">
<el-icon class="loading-icon">
<Loading />
</el-icon>
<span>加载中...</span>
</div>
<mt-edit ref="mtEditRef" @on-preview-click="onPreviewClick" @on-return-click="onReturnClick"
@on-save-click="onSaveClick"></mt-edit>
</div>
</template>
<script setup lang="ts">
import 'maotu/dist/style.css';
import { MtEdit, leftAsideStore } from 'maotu';
import getDatas from "@/network/index";
import { ref, onMounted, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
// 定义编辑器数据类型
interface EditorData {
[key: string]: any;
}
const router = useRouter();
const route = useRoute();
const mtEditRef = ref<InstanceType<typeof MtEdit>>();
const id = route.query.id;
const loading = ref(true); // 加载状态
// 组件挂载后加载数据
onMounted(async () => {
try {
// 设置页面标题
document.title = '项目编辑';
// 等待组件完全渲染
await nextTick();
// 先加载项目数据
await EditXmInfo();
// 再加载图标数据
await edit_iconAll();
} catch (error) {
console.error('初始化错误:', error);
ElMessage.error('加载数据失败,请刷新页面重试');
loading.value = false;
}
});
// 预览项目
const onPreviewClick = (exportJson: EditorData) => {
console.log(exportJson, '这是要传给预览组件的数据');
const url = window.location.origin + window.location.pathname + '#' + 'TxPre' + '?id=' + id;
window.open(url, '_blank',);
};
// 返回上一页
const onReturnClick = () => {
router.go(-1);
};
// 保存项目数据
const onSaveClick = async (exportJson: EditorData) => {
if (!exportJson) {
ElMessage.error('保存失败:数据无效');
return;
}
try {
loading.value = true; // 开始保存时显示加载状态
console.log(exportJson, '这是要保存的数据');
const json = JSON.stringify(exportJson);
const params = {
id: id,
json: json,
};
const res = await getDatas("zutai/EditXmSave_json", params);
console.log("保存的数据请求", JSON.parse(JSON.stringify(res)));
if (res.data.code == 0) {
ElMessage({ message: '保存成功!', type: 'success', });
EditXmInfo();
} else {
ElMessage.error(res.data.msg || '保存失败');
}
} catch (error) {
console.error('保存错误:', error);
ElMessage.error('保存失败,请稍后重试');
} finally {
loading.value = false; // 无论成功失败,都结束加载状态
}
};
// 加载项目数据
const EditXmInfo = async () => {
loading.value = true;
try {
const res = await getDatas("zutai/EditXmInfo", {
id: id,
});
console.log("加载项目数据", JSON.parse(JSON.stringify(res)));
if (res.data.code == 0) {
if (res.data.result.json) {
await nextTick(); // 确保组件已经渲染
mtEditRef.value?.setImportJson(res.data.result.json);
} else {
console.log("首次json为空");
}
} else {
ElMessage.error(res.data.msg || '加载数据失败');
}
} catch (error) {
console.error('加载项目数据错误:', error);
ElMessage.error('加载数据失败,请稍后重试');
} finally {
loading.value = false;
}
};
// 所有图标
const edit_iconAll = async () => {
try {
const res = await getDatas("zutai/edit_iconAll");
// 检查res是否存在
if (!res || !res.data) {
console.error('获取图标数据返回为空');
ElMessage.error('获取图标数据失败');
return;
}
console.log("所有图标", JSON.parse(JSON.stringify(res.data)));
if (res.data.code == 0) {
// 添加安全检查,确保lists存在且是数组
const iconLists = res.data.lists || [];
iconLists.forEach((item: any) => {
if (item && item.title && item.icon) {
leftAsideStore.registerConfig(item.title, item.icon);
}
});
} else {
ElMessage.error(res.data.msg || '获取图标失败');
}
} catch (error) {
console.error('获取图标出错:', error);
ElMessage.error('获取图标数据失败,请稍后重试');
}
};
</script>
<style lang="less">
// 变量定义
@editor-height: 92vh;
@loading-bg: rgba(0, 0, 0, 0.7);
.mt-edit-container {
position: relative;
height: @editor-height;
// 加载状态覆盖层
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: @loading-bg;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
color: white;
.loading-icon {
font-size: 2rem;
margin-bottom: 1rem;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
}
.items-center {
padding-top: 5px;
}
.w-45px {
padding-left: 10px !important;
}
.h-45px {
width: 80px !important;
}
.pl-20px {
height: 30px !important;
}
</style>
2.1编辑页面得图标导入
编辑页面里面有图片图标导入,这里是导入得数据格式,按着这个格式来注册就会生成对应得title和内容,(所有图标那段就是,具体得单独上传,修改,删除让后端添加接口就行了,)
3.预览页面

我这里得温度和湿度得数据都是请求的来得数据,方法再后边
3.1预览页面代码
javascript
<template>
<div class="MtPre">
<div v-if="loadError" class="error-container">
<div class="error-message">
<i class="el-icon-warning"></i>
<p>{{ errorMsg }}</p>
<el-button type="primary" @click="reloadData">重试</el-button>
</div>
</div>
<div v-else class="preview-container" v-loading="loading" element-loading-text="加载中...">
<mt-preview :device-info="deviceInfo" ref="MtPreviewRef" @on-event-call-back="onEventCallBack">
</mt-preview>
</div>
</div>
</template>
<script setup lang="ts">
import 'maotu/dist/style.css';
import { MtEdit, MtPreview } from 'maotu';
import { ref, onMounted, provide, reactive } from 'vue';
import getDatas from "@/network/index";
import { ElMessage } from 'element-plus';
import service from '@/network/maotuRequest';
import { useRouter, useRoute } from 'vue-router';
// 组件引用
const MtPreviewRef = ref<InstanceType<typeof MtEdit>>();
// 提供自定义请求方法给maotu组件
provide('mt-request', service);
// 路由参数
const route = useRoute();
const id = route.query.id;
// 状态管理
const loading = ref(true);
const loadError = ref(false);
const errorMsg = ref('数据加载失败,请重试');
const deviceInfo = ref<any[]>([]);
// 设置页面标题
document.title = '组态预览';
/**
* 加载组态数据
*/
const loadData = async () => {
ElMessage({ message: '先保存再预览才可以看到最新数据!', type: 'warning', });
if (!id) {
loadError.value = true;
errorMsg.value = '缺少组态ID参数';
loading.value = false;
return;
}
loading.value = true;
loadError.value = false;
try {
// 获取组态数据
const res = await getDatas("zutai/EditXmInfo", { id });
if (res.data && res.data.code === 0 && res.data.result?.json) {
MtPreviewRef.value?.setImportJson(res.data.result.json);
loading.value = false;
} else {
throw new Error(res.data?.msg || '数据加载失败');
}
} catch (error) {
console.error('组态加载错误:', error);
loadError.value = true;
errorMsg.value = error instanceof Error ? error.message : '数据加载失败,请重试';
loading.value = false;
}
};
/**
* 重新加载数据
*/
const reloadData = () => {
loadData();
};
/**
* 事件回调处理
*/
const onEventCallBack = (event: any) => {
console.log("事件回调:", JSON.parse(JSON.stringify(event)));
// 在此处理事件回调逻辑
};
// 生命周期钩子
onMounted(() => {
loadData();
});
</script>
<style lang="less" scoped>
::deep {
.el-image {
display: none;
}
}
.MtPre {
height: 88.5vh;
position: relative;
//background-color: #f5f7fa;
}
.preview-container {
height: 100%;
width: 100%;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
}
.error-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
.error-message {
text-align: center;
padding: 24px;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
i {
font-size: 48px;
color: #f56c6c;
margin-bottom: 16px;
}
p {
margin-bottom: 16px;
color: #606266;
}
}
}
</style>
3.2 API 请求与 Token 统一封装
为保证 Maotu 相关请求与主项目风格一致,我们新建了 src/network/maotuRequest.ts,实现了如下功能:
Token 统一:所有请求 header 统一携带 access-token,值为 token。
状态码处理:对 401/100 等特殊状态码做统一拦截与提示。
错误处理:请求失败时自动弹窗提示,并支持重试机制。
HTTP 方法封装:常用的 GET/POST 方法统一封装,便于流程图节点内直接调用。
Maotu 组件通过 provide('mt-request', service) 注入自定义请求方法,保证流程图节点的 HTTP 请求与主项目一致。
javascript
import axios from 'axios';
import { ElMessage } from 'element-plus';
// 创建axios实例
const service = axios.create({
baseURL: '/api',//请求头
timeout: 30000,
headers: {
"Access-Control-Allow-Origin": "*",
"Connection": "Keep-Alive",
"Content-Type": "application/json; charset = utf-8"
}
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 从localStorage获取token
const token = localStorage.getItem('token');
if (token) {
config.headers['access-token'] = `${token}`;
}
return config;
},
(error) => {
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(res): any => {
const code = res.data.code || 200;
const message = res.data.message || '请求成功';
// 处理不同的状态码
switch (code) {
case 200:
return Promise.resolve(res.data);
case 401:
ElMessage.error('登录信息已过期,请重新登录');
// 可以在这里处理登出逻辑
localStorage.removeItem('token');
window.location.href = '/login';
return Promise.reject(new Error('登录信息已过期'));
case 403:
ElMessage.error('没有权限访问该资源');
return Promise.reject(new Error('没有权限'));
case 500:
ElMessage.error('服务器错误');
return Promise.reject(new Error('服务器错误'));
default:
ElMessage.error(message);
return Promise.reject(new Error(message));
}
},
(error) => {
console.error('响应错误:', error);
ElMessage.error(error.message || '请求失败');
return Promise.reject(error);
}
);
// 封装GET请求
export const get = (url: string, params?: any) => {
return service({
url,
method: 'get',
params
});
};
// 封装POST请求
export const post = (url: string, data?: any) => {
return service({
url,
method: 'post',
data
});
};
// 封装PUT请求
export const put = (url: string, data?: any) => {
return service({
url,
method: 'put',
data
});
};
// 封装DELETE请求
export const del = (url: string, params?: any) => {
return service({
url,
method: 'delete',
params
});
};
export default service;
二、自定义流程

1.新建自定义流程
2.http请求
鼠标放到开始节点和结束节点中间,添加节点
选中http请求节点
地址写你自己得请求地址
3.解析请求数据
从控制台看这个数据:
一定要写args来点数据,这个是maotu写死得名称,(一开始我用我自己得名称一直拿不到数据)
res:是自定义得,后边选中刚才得http请求得节点
javascript
console.log(`zigbee:`,args.res)
这也就可以看到请求数据
整体得节点配置,根据我的接口返回的数据和节点配置数据进行对比,
4.设置图元属性
就是绑定文字,键值对等数据使用请求拿到得数据,我刚才配置了温度,湿度,门窗等信息;
绑定之后预览看到的数据就是这里绑定得数据,就可以实现数据交互。
5.调用流程
先再编辑页面点击空白处,左侧弹出层,选中全局事件,添加事件,事件类型选中页面初始化,事件行为选择调用流程,选择我们刚才添加得流程,就可以了,不然预览页面不生效。
三、数据更新处理办法
再添加一个流程,为计时器流程,就可以解决数据更新得问题。
记得再页面初始化调用计时器流程,不然不生效!!!!
四、常见问题与解决方案
1.Token失效问题
解决方案:在请求拦截器中统一处理401状态,自动刷新token或跳转登录页
2.节点数据格式不一致
解决方案:统一使用JSON Schema校验数据格式,确保前后端数据结构一致
3.自定义节点样式丢失
解决方案:将样式定义放入节点配置中,随节点数据一起保存和加载
总结
Maotu流程图编辑器在我们的项目中发挥了重要作用,通过合理的集成和定制开发,我们不仅实现了复杂业务流程的可视化设计,还提升了整体系统的自动化水平和用户体验。
希望本文对你在Vue3项目中集成和定制Maotu流程图编辑器有所帮助!如有问题,欢迎在评论区交流讨论。