Maotu流程图编辑器:Vue3项目中的集成实战与自定义流程开发指南

文章目录

前言

Maotu是一款功能强大的流程图编辑组件,在我们的项目中发挥了重要作用。它不仅提供了直观的可视化界面,还支持高度自定义的节点类型和交互逻辑,使复杂业务流程的设计变得简单高效。

相比其他流程图工具,Maotu的优势在于:

  • 完善的节点自定义能力
  • 灵活的数据导入导出
  • 与Vue3项目无缝集成
  • 支持复杂条件分支和业务规则

安装依赖

Maotu

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流程图编辑器有所帮助!如有问题,欢迎在评论区交流讨论。

相关推荐
锈儿海老师14 分钟前
AST 工具大PK!Biome 的 GritQL 插件 vs. ast-grep,谁是你的菜?
前端·javascript·eslint
令狐寻欢16 分钟前
JavaScript中 的 Object.defineProperty 和 defineProperties
javascript
快起来别睡了17 分钟前
代理模式:送花风波
前端·javascript·架构
FogLetter37 分钟前
从add函数类型判断说起:NaN的奇幻漂流与JS数据类型的奥秘
前端·javascript
兰贝达40 分钟前
商品SKU选择器实现思路,包简单
前端·javascript·vue.js
程序员小张丶1 小时前
React Native在HarmonyOS 5.0阅读类应用开发中的实践
javascript·react native·react.js·阅读·harmonyos5.0
EndingCoder1 小时前
React Native 是什么?为什么学它?
javascript·react native·react.js
摸鱼仙人~2 小时前
Redux Toolkit 快速入门指南:createSlice、configureStore、useSelector、useDispatch 全面解析
开发语言·javascript·ecmascript
程序员小张丶3 小时前
基于React Native开发HarmonyOS 5.0主题应用技术方案
javascript·react native·react.js·主题·harmonyos5.0
teeeeeeemo3 小时前
Vue数据响应式原理解析
前端·javascript·vue.js·笔记·前端框架·vue