网站推广优化如何做,wordpress 导出 主题,网站制作内容,网站建设需要多大的空间前言
大家好#xff0c;本系列从Web前端实战的角度#xff0c;给大家分享介绍如何从零打造一个自己专属的绘图工具#xff0c;实现流程图、拓扑图、脑图等类Visio的绘图工具。 你将收获
免费好用、专属自己的绘图工具前端项目实战学习如何从0搭建一个前端项目等基础框架项…前言
大家好本系列从Web前端实战的角度给大家分享介绍如何从零打造一个自己专属的绘图工具实现流程图、拓扑图、脑图等类Visio的绘图工具。 你将收获
免费好用、专属自己的绘图工具前端项目实战学习如何从0搭建一个前端项目等基础框架项目设计思路及优雅的架构技巧开源项目学习热门可视化引擎Meta2d.js等学习使用
技术栈
Meta2d.js - 国产开源免费好用的可视化引擎
Vue3 - 流行的简单易用等前端Web框架
Vite - 高效好用的前端热门构建工具
TDesign - 支持Vue3的前端UI组件库 需要提前掌握
前端基础工具node.js安装仅安装即可npmpnpm、yarn基本使用package.json基本认识
以上基础知识可自行网上学习
一、 Vite Vue3框架搭建
1.1 搭建vue3的vite项目
参考vite文档开始 | Vite 官方中文文档的pnpm的方式创建项目
pnpm create vite
按照命令行提示简单设置如下配置 1.2 修改package.json
【注意】因为当前vite更新比较频繁经常直接使用脚手架命令生成的框架运行会报错。可以尝试切换不同的包管理工具pnpm、yarn、npm试试或看看vite、vue等是否有最新版本号修改package.json升级。 当前我们使用pnpm i安装依赖包后发现运行错误。查看有新的vite4.4.2手动修改package.json升级。 另外我个人习惯把package.json中的dev重命名为start。 1.3 运行检查基础框架
// 安装依赖包
pnpm i
// 本地运行。脚手架默认命令为pnpm dev
pnpm start
根据命令行提示在浏览器打开http://127.0.0.1:5173/ 正常运行基础框架完成。 1.4 丰富框架
在package.json中添加meta2d.js、vue-router、tdesign、postcss等项目需要用的依赖包。
{name: diagram-editor-vue3,private: true,version: 0.0.1,scripts: {start: vite,build: vue-tsc vite build,preview: vite preview},dependencies: {meta2d/activity-diagram: ^1.0.0,meta2d/chart-diagram: ^1.0.3,meta2d/class-diagram: ^1.0.0,meta2d/core: ^1.0.19,meta2d/flow-diagram: ^1.0.0,meta2d/form-diagram: ^1.0.3,meta2d/fta-diagram: ^1.0.0,meta2d/le5le-charts: ^1.0.2,meta2d/sequence-diagram: ^1.0.0,meta2d/svg: ^1.0.2,tdesign-vue-next: ^1.3.10,vue: ^3.3.4,vue-router: ^4.2.4},devDependencies: {vitejs/plugin-vue: ^4.2.3,autoprefixer: ^10.4.13,postcss: ^8.4.6,postcss-import: ^14.1.0,postcss-nested: ^6.0.1,typescript: ^5.0.2,vite: ^4.4.2,vue-tsc: ^1.8.3}
}
添加postcss支持 在package.json中删除type: module选项。添加postcss.config.js文件
module.exports {plugins: {postcss-import: {},postcss-nested: {},autoprefixer: {},},
};1.5 修改index.html
修改index.html为符合项目描述内容
1.6 初始化css
修改style.css为符合项目的默认初始样式 1.7 添加router
新增src/router.ts文件:
import { createRouter, createWebHistory } from vue-router;const routes [{ path: /, component: () import(./views/Index.vue) },{ path: /preview, component: () import(./views/Preview.vue) },
];const router createRouter({history: createWebHistory(/),routes,
});export default router;其中
/ - 编辑器页面
/preview - 预览页面
1.8 加载vue-router、tdesign
在main.ts中加载vue-router、tdesign等基础服务。
import { createApp } from vue;
import ./style.css;
import App from ./App.vue;import router from ./router.ts;
import TDesign from tdesign-vue-next;const app createApp(App);// 加载基础服务
app.use(router).use(TDesign);
// endapp.mount(#app);1.9 设置路由
添加路由页面src/views/Index.vue、src/views/Preview.vue修改App.vue内容为加载路由 1.10 设置路径支持
vue配置vite.config.ts
安装依赖库pnpm add -D path
import { defineConfig } from vite;
import vue from vitejs/plugin-vue;
import * as path from path;// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],resolve: {alias: {: path.resolve(__dirname, ./src/),},},
});typescript配置tsconfig.json
{compilerOptions: {...baseUrl: .,paths: {/*: [src/*],},}, ...
}1.11 运行
运行pnpm start并在浏览器打开 至此基础框架搭建完成。 二、创建编辑器
2.0 编辑器布局
拆分编辑器为菜单工具栏Header、图形库Graphics、编辑器画布View、属性面板Props Index.vue直接由编辑器各个子组件构成
templatediv classapp-pageHeader /div classdesignerGraphics /View /Props //div/div
/templatescript langts setup
import Header from ../components/Header.vue;
import Graphics from ../components/Graphics.vue;
import View from ../components/View.vue;
import Props from ../components/Props.vue;
/scriptstyle langpostcss scoped
.app-page {height: 100vh;overflow: hidden;
}
/style2.1 创建编辑器画布 View
2.1.1 挂载
Meta2d画布实例必须挂载在html中DOM元素上
div idmeta2d/div
2.1.2导入Meta2d类
import { Meta2d } from meta2d/core;
2.2.3 创建实例
创建实例必须等挂载容器DOM元素创建完成。因此我们一般在onMounted中创建实例。注意如果挂载容器存在动画或其他原因导致挂载容器大小、位置不稳定时需要等挂载容器样式稳定后在创建。
onMounted(() {const myMeta2d new Meta2d(meta2d, meta2dOptions);
});
通过new Meta2d创建实例后默认会把当前实例挂载到global.meta2d全局变量上。后续可以直接通过meta2d来操作画布。
2.2.4 注册图形库
根据需求按需注册图形库。
onMounted(() {// 创建实例new Meta2d(meta2d, meta2dOptions);// 按需注册图形库// 以下为自带基础图形库register(flowPens());registerAnchors(flowAnchors());register(activityDiagram());registerCanvasDraw(activityDiagramByCtx());register(classPens());register(sequencePens());registerCanvasDraw(sequencePensbyCtx());registerEcharts();registerCanvasDraw(formPens());registerCanvasDraw(chartsPens());register(ftaPens());registerCanvasDraw(ftaPensbyCtx());registerAnchors(ftaAnchors());// 注册其他自定义图形库// ...
});
2.2 创建菜单工具栏Header
2.2.1 创建菜单栏 使用TDesign的Dropdown下拉菜单创建菜单栏 div classapp-headera classlogo hrefhttps://le5le.com target_blankimg src/favicon.ico /span乐吾乐/span/at-dropdown:minColumnWidth200:maxHeight560overlayClassNameheader-dropdowna 文件 /at-dropdown-menut-dropdown-item clicknewFilea新建文件/a/t-dropdown-itemt-dropdown-item clickopenFile dividertruea打开文件/a/t-dropdown-itemt-dropdown-item dividertruea clickdownloadJson下载JSON文件/a/t-dropdown-itemt-dropdown-itema clickdownloadPng下载为PNG/a/t-dropdown-itemt-dropdown-itema clickdownloadSvg下载为SVG/a/t-dropdown-item/t-dropdown-menu/t-dropdownt-dropdown:minColumnWidth180:maxHeight500overlayClassNameheader-dropdowna 编辑 /at-dropdown-menut-dropdown-itema clickonUndodiv classflex撤销 span classflex-grow/span Ctrl Z/div/a/t-dropdown-itemt-dropdown-item dividertruea clickonRedodiv classflex恢复 span classflex-grow/span Ctrl Y/div/a/t-dropdown-itemt-dropdown-itema clickonCutdiv classflex剪切 span classflex-grow/span Ctrl X/div/a/t-dropdown-itemt-dropdown-itema clickonCopydiv classflex复制 span classflex-grow/span Ctrl C/div/a/t-dropdown-itemt-dropdown-item dividertruea clickonPastediv classflex粘贴 span classflex-grow/span Ctrl V/div/a/t-dropdown-itemt-dropdown-itema clickonAlldiv classflex全选 span classflex-grow/span Ctrl A/div/a/t-dropdown-itemt-dropdown-itema clickonDeletediv classflex删除 span classflex-grow/span DELETE/div/a/t-dropdown-item/t-dropdown-menu/t-dropdownt-dropdown:minColumnWidth180:maxHeight500:delay2[10, 150]overlayClassNameheader-dropdowna 帮助 /at-dropdown-menut-dropdown-item v-foritem in assets.helps :divideritem.dividera :hrefitem.url target_blank{{ item.name }}/a/t-dropdown-item/t-dropdown-menu/t-dropdown/div
菜单事件通过查阅Meta2d.js的API帮助文档来实现
新建文件
新建文件是通过打开一个空白画布来实现
// 打开默认空白文件
const newFile () {meta2d.open();
};// 打开一个指定名称的空白文件
const newFile () {meta2d.open({ name: 新建项目, pens: [] } as any);
};
打开文件
function readFile(file: Blob) {return new Promisestring((resolve, reject) {const reader new FileReader();reader.onload () {resolve(reader.result as string);};reader.onerror reject;reader.readAsText(file);});
}const openFile () {// 1. 显示选择文件对话框const input document.createElement(input);input.type file;input.onchange async (event) {const elem event.target as HTMLInputElement;if (elem.files elem.files[0]) {// 2. 读取文件字符串内容const text await readFile(elem.files[0]);try {// 3. 打开文件内容meta2d.open(JSON.parse(text));// 可选缩放到窗口大小展示meta2d.fitView();} catch (e) {console.log(e);}}};input.click();
};
保存为JSON文件
安装file-saver
pnpm add file-saver
下载文件
const downloadJson () {const data: any meta2d.data();FileSaver.saveAs(new Blob([JSON.stringify(data)], {type: text/plain;charsetutf-8,}),${data.name || le5le.meta2d}.json);
};
保存为PNG文件
const downloadPng () {let name (meta2d.store.data as any).name;if (name) {name .png;}meta2d.downloadPng(name);
};
保存为SVG文件
下载canvas2svg.js在index.html中加载 下载svg
// 判断该画笔 是否是组合为状态中 展示的画笔
function isShowChild(pen: any, store: any) {let selfPen pen;while (selfPen selfPen.parentId) {const oldPen selfPen;selfPen store.pens[selfPen.parentId];const showChildIndex selfPen?.calculative?.showChild;if (showChildIndex ! undefined) {const showChildId selfPen.children[showChildIndex];if (showChildId ! oldPen.id) {return false;}}}return true;
}const downloadSvg () {if (!C2S) {MessagePlugin.error(请先加载乐吾乐官网下的canvas2svg.js);return;}const rect: any meta2d.getRect();rect.x - 10;rect.y - 10;const ctx new C2S(rect.width 20, rect.height 20);ctx.textBaseline middle;for (const pen of meta2d.store.data.pens) {if (pen.visible false || !isShowChild(pen, meta2d.store)) {continue;}meta2d.renderPenRaw(ctx, pen, rect);}let mySerializedSVG ctx.getSerializedSvg();if (meta2d.store.data.background) {mySerializedSVG mySerializedSVG.replace({{bk}}, );mySerializedSVG mySerializedSVG.replace({{bkRect}},rect x0 y0 width100% height100% fill${meta2d.store.data.background}/rect);} else {mySerializedSVG mySerializedSVG.replace({{bk}}, );mySerializedSVG mySerializedSVG.replace({{bkRect}}, );}mySerializedSVG mySerializedSVG.replace(/--le5le--/g, #x);const urlObject: any (window as any).URL || window;const export_blob new Blob([mySerializedSVG]);const url urlObject.createObjectURL(export_blob);const a document.createElement(a);a.setAttribute(download,${(meta2d.store.data as any).name || le5le.meta2d}.svg);a.setAttribute(href, url);const evt document.createEvent(MouseEvents);evt.initEvent(click, true, true);a.dispatchEvent(evt);
};
撤销
const onUndo () {meta2d.undo();
};
重做
const onRedo () {meta2d.redo();
};
剪切
const onCut () {meta2d.cut();
};
复制
const onCopy () {meta2d.copy();
};
粘贴
const onPaste () {meta2d.paste();
};
全选
const onAll () {meta2d.activeAll();
};删除
const onPaste () {meta2d.paste();
};
其他
其他未操作可查阅Meta2d.js的API帮助文档来实现 2.2.2 创建工具栏 画直线
设置html DOM元素属性支持拖拽和点击
t-tooltip content直线span:draggabletruedragstartonAddShape($event, line)clickonAddShape($event, line)t-icon nameslash //span
/t-tooltip
设置图元数据
const onAddShape (event: DragEvent | MouseEvent, name: string) {event.stopPropagation();let data: any;if (name text) {data {text: text,width: 100,height: 20,name: text,};} else if (name line) {data {anchors: [{ id: 0, x: 1, y: 0 },{ id: 1, x: 0, y: 1 },],width: 100,height: 100,name: line,lineName: line,type: 1,};}if (!(event as DragEvent).dataTransfer) {meta2d.canvas.addCaches deepClone([data]);} else {(event as DragEvent).dataTransfer?.setData(Meta2d, JSON.stringify(data));}
};
添加文字
设置html DOM元素属性支持拖拽和点击 t-tooltip content文字span:draggabletruedragstartonAddShape($event, text)clickonAddShape($event, text)svg classl-icon aria-hiddentrueuse xlink:href#l-text/use/svg/span
/t-tooltip
设置图元数据
const onAddShape (event: DragEvent | MouseEvent, name: string) {event.stopPropagation();let data: any;if (name text) {data {text: text,width: 100,height: 20,name: text,};} else if (name line) {data {anchors: [{ id: 0, x: 1, y: 0 },{ id: 1, x: 0, y: 1 },],width: 100,height: 100,name: line,lineName: line,type: 1,};}if (!(event as DragEvent).dataTransfer) {meta2d.canvas.addCaches deepClone([data]);} else {(event as DragEvent).dataTransfer?.setData(Meta2d, JSON.stringify(data));}
};
连线
设置click事件
t-tooltip content连线svgwidth1emheight1emviewBox0 0 1024 1024xmlnshttp://www.w3.org/2000/svgclickdrawLine:style{color: isDrawLine ? #1677ff : ,}pathdM192 64a128 128 0 0 1 123.968 96H384a160 160 0 0 1 159.68 149.504L544 320v384a96 96 0 0 0 86.784 95.552L640 800h68.032a128 128 0 1 1 0 64.064L640 864a160 160 0 0 1-159.68-149.504L480 704V320a96 96 0 0 0-86.784-95.552L384 224l-68.032 0.064A128 128 0 1 1 192 64z m640 704a64 64 0 1 0 0 128 64 64 0 0 0 0-128zM192 128a64 64 0 1 0 0 128 64 64 0 0 0 0-128zfillcurrentColor/path/svg
/t-tooltip
实现连线
// 连线状态
const isDrawLine refboolean(false);// 连线实现
const drawLine () {if (isDrawLine.value) {isDrawLine.value false;meta2d.finishDrawLine();meta2d.drawLine();meta2d.store.options.disableAnchor true;} else {isDrawLine.value true;meta2d.drawLine(meta2d.store.options.drawingLineName);meta2d.store.options.disableAnchor false;}
};
设置连线类型
设置html属性 t-dropdown:minColumnWidth160:maxHeight560overlayClassNameheader-dropdownasvg classl-icon aria-hiddentrueuse:xlink:hreflineTypes.find((item) item.value currentLineType)?.icon/use/svg/at-dropdown-menut-dropdown-item v-foritem in lineTypesdiv classflex middle clickchangeLineType(item.value){{ item.name }} span classflex-grow/spansvg classl-icon aria-hiddentrueuse :xlink:hrefitem.icon/use/svg/div/t-dropdown-item/t-dropdown-menu/t-dropdown
连线类型设置
const lineTypes reactive([{ name: 曲线, icon: #l-curve2, value: curve },{ name: 线段, icon: #l-polyline, value: polyline },{ name: 直线, icon: #l-line, value: line },{ name: 脑图曲线, icon: #l-mind, value: mind },
]);
const currentLineType ref(curve);const changeLineType (value: string) {currentLineType.value value;if (meta2d) {meta2d.store.options.drawingLineName value;meta2d.canvas.drawingLineName (meta2d.canvas.drawingLineName value);meta2d.store.active?.forEach((pen) {meta2d.updateLineType(pen, value);});}
};
设置连线箭头
设置html属性 t-dropdown:minColumnWidth160:maxHeight560:delay2[10, 150]overlayClassNameheader-dropdownasvg classl-icon aria-hiddentrueuse:xlink:hreffromArrows.find((item) item.value fromArrow)?.icon/use/svg/at-dropdown-menut-dropdown-item v-foritem in fromArrowsdivclassflex middlestyleheight: 30pxclickchangeFromArrow(item.value)svg classl-icon aria-hiddentrueuse :xlink:hrefitem.icon/use/svg/div/t-dropdown-item/t-dropdown-menu/t-dropdownt-dropdown:minColumnWidth160:maxHeight560:delay2[10, 150]overlayClassNameheader-dropdownasvg classl-icon aria-hiddentrueuse:xlink:hreftoArrows.find((item) item.value toArrow)?.icon/use/svg/at-dropdown-menut-dropdown-item v-foritem in toArrowsdivclassflex middlestyleheight: 30pxclickchangeToArrow(item.value)svg classl-icon aria-hiddentrueuse :xlink:hrefitem.icon/use/svg/div/t-dropdown-item/t-dropdown-menu/t-dropdown
箭头设置
const fromArrow ref();
const fromArrows [{ icon: #l-line, value: },{ icon: #l-from-triangle, value: triangle },{ icon: #l-from-diamond, value: diamond },{ icon: #l-from-circle, value: circle },{ icon: #l-from-lineDown, value: lineDown },{ icon: #l-from-lineUp, value: lineUp },{ icon: #l-from-triangleSolid, value: triangleSolid },{ icon: #l-from-diamondSolid, value: diamondSolid },{ icon: #l-from-circleSolid, value: circleSolid },{ icon: #l-from-line, value: line },
];
const toArrow ref();
const toArrows [{ icon: #l-line, value: },{ icon: #l-to-triangle, value: triangle },{ icon: #l-to-diamond, value: diamond },{ icon: #l-to-circle, value: circle },{ icon: #l-to-lineDown, value: lineDown },{ icon: #l-to-lineUp, value: lineUp },{ icon: #l-to-triangleSolid, value: triangleSolid },{ icon: #l-to-diamondSolid, value: diamondSolid },{ icon: #l-to-circleSolid, value: circleSolid },{ icon: #l-to-line, value: line },
];const changeFromArrow (value: string) {fromArrow.value value;// 画布默认值meta2d.store.data.fromArrow value;// 活动层的箭头都变化if (meta2d.store.active) {meta2d.store.active.forEach((pen: Pen) {if (pen.type PenType.Line) {pen.fromArrow value;meta2d.setValue({id: pen.id,fromArrow: pen.fromArrow,},{render: false,});}});meta2d.render();}
};const changeToArrow (value: string) {toArrow.value value;// 画布默认值meta2d.store.data.toArrow value;// 活动层的箭头都变化if (meta2d.store.active) {meta2d.store.active.forEach((pen: Pen) {if (pen.type PenType.Line) {pen.toArrow value;meta2d.setValue({id: pen.id,toArrow: pen.toArrow,},{render: false,});}});meta2d.render();}
};
画布缩放
监听当前画布比例
onMounted(() {const timer setInterval(() {if (meta2d) {clearInterval(timer);// 获取初始缩放比例scaleSubscriber(meta2d.store.data.scale);// 监听缩放// ts-ignoremeta2d.on(scale, scaleSubscriber);}}, 200);
});const scaleSubscriber (val: number) {scale.value Math.round(val * 100);
};
缩放到100%
const onScaleDefault () {meta2d.scale(1);meta2d.centerView();
};
缩放到窗口大小
const onScaleWindow () {meta2d.fitView();
};
运行查看
这里由于是单机环境数据保存在前本地存储。
无论是否单机环境运行查看大致流程基本上是保存数据这里是前端本地存储- 跳转运行页面 - 新页面读取加载数据。 添加click事件
t-tooltip content运行查看t-icon nameplay-circle-stroke clickonView /
/t-tooltip
保存数据到本地存储跳转运行页面
const onView () {// 先停止动画避免数据波动meta2d.stopAnimate();// 本地存储const data: any meta2d.data();localStorage.setItem(meta2d, JSON.stringify(data));// 跳转到预览页面router.push({path: /preview,query: {r: Date.now() ,id: data._id,},});
};
加载数据
Preview.vue
templatediv classapp-pageView //div
/templatescript langts setup
import { onMounted } from vue;
import View from ../components/View.vue;onMounted(() {// 读取本地存储let data: any localStorage.getItem(meta2d);if (data) {data JSON.parse(data);// 设置为预览模式data.locked 1;}meta2d.open(data);
});
/scriptstyle langpostcss scoped
.app-page {height: 100vh;
}
/style返回编辑
返回编辑的基本流程是 跳转编辑页面 - 新页面读取加载数据。
这和运行查看有重复的逻辑新页面读取加载数据因此我们可以把这部分放到公共的View.vue组件里面实现。
View.vue
...onMounted(() {// 创建实例new Meta2d(meta2d, meta2dOptions);// 按需注册图形库// 以下为自带基础图形库register(flowPens());registerAnchors(flowAnchors());register(activityDiagram());registerCanvasDraw(activityDiagramByCtx());register(classPens());register(sequencePens());registerCanvasDraw(sequencePensbyCtx());registerEcharts();registerCanvasDraw(formPens());registerCanvasDraw(chartsPens());register(ftaPens());registerCanvasDraw(ftaPensbyCtx());registerAnchors(ftaAnchors());// 注册其他自定义图形库// ...// 加载数据let data: any localStorage.getItem(meta2d);if (data) {data JSON.parse(data);// 判断是否为运行查看是-设置为预览模式if (location.pathname /preview) {data.locked 1;} else {data.locked 0;}meta2d.open(data);}
});...
自动保存
这里是单机环境我们自动保存到前端本地存储。
监听数据变化自动保存
Index.Vue
let timer: any;
function save() {if (timer) {clearTimeout(timer);}timer setTimeout(() {const data: any meta2d.data();localStorage.setItem(meta2d, JSON.stringify(data));timer undefined;}, 1000);
}onMounted(() {meta2d.on(scale, save);meta2d.on(add, save);meta2d.on(opened, save);meta2d.on(undo, save);meta2d.on(redo, save);meta2d.on(add, save);meta2d.on(delete, save);meta2d.on(rotatePens, save);meta2d.on(translatePens, save);
});
2.3 创建图形库Graphics 2.3.1 定义图元数据列表
因为是内置基础图元我们暂时直接写死数组。实际项目中可以通过API接口获取图元数据列表。
const graphicGroups [{name: 基本形状, // 分组名称list: [{name: 正方形, // 图元显示名称icon: l-rect, // 图元显示图标这里用的是iconfont图标data: { // Meta2d.js图元数据width: 100,height: 100,name: square,},},]},{name: 脑图,list: [...]}
]
由于篇幅问题这里仅展示数据结构示意详细可参考文末教程相关代码。 上面数据结构列表包含2种数据
“Meta2d.js图元数据”- Meta2d.js可视化引擎需要的数据实际绘图数据其他 - Vue UI用的数据编辑器显示用的数据 2.3.2 显示图元列表
这里我们使用折叠面板来实现图元列表显示。
t-collapse :defaultExpandAlltruet-collapse-panel:headeritem.namev-foritem in graphicGroups:keyitem.nametemplate v-forelem in item.listdivclassgraphic:draggabletruedragstartdragStart($event, elem)click.preventdragStart($event, elem)svg classl-icon aria-hiddentrueuse :xlink:href# elem.icon/use/svgp :titleelem.name{{ elem.name }}/p/div/template/t-collapse-panel/t-collapse 2.3.3 图元拖拽
由于Meta2d.js已经内置接收拖拽数据的功能。这里我们只用实现拖拽绑定数据过程即可只需2步简单方便。 const dragStart (e: any, elem: any) {if (!elem) {return;}e.stopPropagation();// 拖拽事件if (e instanceof DragEvent) {// 设置拖拽数据e.dataTransfer?.setData(Meta2d, JSON.stringify(elem.data));} else {// 支持单击添加图元。平板模式meta2d.canvas.addCaches [elem.data];}
};
2.3.4 平板模式单击添加图元
Meta2d.js支持单击图元添加方便触摸场景。
设置单击事件 这里为了方便直接合并在拖拽函数里面了
绑定单击数据 2.4 创建属性面板Props
这里我们属性面板包含2种实际项目中根据需求设计 图纸属性、图元属性。
我们通过鼠标点击的不同切换不同的属性面板
点击画布空白地方显示图纸属性点击图元显示图元属性
2.4.1 组合式函数
这里我们学习下非常有用的Vue知识和一些优雅的架构技巧组合式函数、状态管理
什么是组合式函数
组合式函数Composite function是一种通过将多个独立的函数组合起来来解决复合问题的函数。组合式函数的好处在于可以通过简单地组合多个函数来减少代码量提高代码的可读性并提高程序的灵活性和可扩展性。以下是组合式函数的一些主要优点
代码重用通过组合多个函数可以减少代码量提高代码的可读性和可维护性。在实际编程过程中我们常常需要重复使用某些功能组合式函数可以帮助我们更轻松地实现代码重用。模块化通过将函数组合在一起可以实现程序的模块化使得代码结构更清晰模块之间的关系更明确。这有助于提高程序的可维护性和可读性。提高代码的可读性组合式函数将多个相关的函数组合在一起有助于提高代码的可读性。通过这种方式开发者可以更容易地理解函数的作用以及各个函数之间的关系。灵活性组合式函数可以根据需要动态地调整各个函数的顺序、参数或调用方式以便更好地满足问题的需求。这使得程序具有更高的灵活性和可扩展性。复用逻辑组合式函数可以将一些常用的逻辑代码封装起来使得这些代码可以在程序的多个地方复用。这有助于减少重复代码提高代码的质量。可测试性组合式函数更容易编写单元测试因为每个函数都可以独立测试。这有助于提高程序的可测试性降低调试成本。易于维护和扩展通过将函数组合在一起开发者可以更容易地发现和解决程序中的问题从而提高程序的维护和扩展能力。
总之组合式函数具有代码重用、模块化、提高可读性、灵活性、复用逻辑、可测试性和易于维护和扩展等优点可以帮助开发者编写更高效、更简洁的代码。 状态管理
【注意注意】【敲黑板】这里的状态管理不是Pinia而是我们自己实现的响应式组合式函数
为什么不用Pinia
不为了使用而使用有入侵性响应式组合式函数更高内聚低耦合 什么时候使用Pinia
项目规定时间轴或时间旅行等调试功能 组合式函数 useSelection
我们定义一个useSelection来表示图元不同的选中状态暂时2种选中图纸选中单个图元
新建一个src/services/selections.ts文件
import { Pen } from meta2d/core;
import { reactive } from vue;// 选中对象类型0 - 画布1 - 单个图元
export enum SelectionMode {File,Pen,
}const selections reactive{mode: SelectionMode;pen?: Pen;
}({mode: SelectionMode.File,pen: undefined,
});export const useSelection () {const select (pens?: Pen[]) {if (!pens || pens.length ! 1) {selections.mode SelectionMode.File;selections.pen undefined;return;}selections.mode SelectionMode.Pen;selections.pen pens[0];};return {selections,select,};
};【注意注意】【敲黑板】优雅的架构技巧
组合式函数的数据为什么放在组合式函数外面
方便实现状态管理
什么时候数据放在组合式函数里面
每次使用组合式函数希望拥有独立的数据拷贝不与其他使用者冲突 2.4.2 事件监听
监听画布的acitve事件实现面板切换。在View.vue文件中新增
import { useSelection } from /services/selections;const { select } useSelection();onMounted(() {// 创建实例new Meta2d(meta2d, meta2dOptions);...meta2d.on(active, active);meta2d.on(inactive, inactive);
});const active (pens?: Pen[]) {select(pens);
};const inactive () {select();
};
2.4.3 属性面板
Props.Vue中根据不同的管理状态显示不同子组件即可
templatediv classapp-props{{ selections.mode }}FileProps v-ifselections.mode SelectionMode.File /PenProps v-else-ifselections.mode SelectionMode.Pen //div
/templatescript langts setup
import FileProps from ./FileProps.vue;
import PenProps from ./PenProps.vue;import { useSelection, SelectionMode } from /services/selections;const { selections } useSelection();
/script
style langpostcss scoped
.app-props {border-left: 1px solid var(--color-border);z-index: 2;height: calc(100vh - 80px);overflow-y: auto;
}
/style2.4.4 图纸属性面板
这里暂时设置图纸属性有图纸名称、网格、标尺、颜色等。
【注意注意注意】
图纸名称、颜色属于图纸数据参考Meta2d.js文档。图纸名称属于自定义业务数据自己扩展定义的
网格、标尺即可以在图纸数据设置也可以在Meta2d.js Options选项设置。这里我们在Options选项设置。 Options被视为独立于图纸外的默认通用样式而图纸数据则归属于图纸专属数据。 A. 定义Vue组件数据
// 图纸数据
const data reactiveany({name: ,background: undefined,color: undefined,
});// 画布选项
const options reactiveany({grid: false,gridSize: 10,gridRotate: undefined,gridColor: undefined,rule: true,
});
B. 定义组件UI
templatediv classprops-panelt-form label-alignlefth5 classmb-24图纸/h5t-form-item label图纸名称 namenamet-input v-modeldata.name changeonChangeData //t-form-itemt-divider /t-form-item label网格 namegridt-switch v-modeloptions.grid changeonChangeOptions //t-form-itemt-form-item label网格大小 namegridSizet-input v-model.numberoptions.gridSize changeonChangeOptions //t-form-itemt-form-item label网格角度 namegridRotatet-inputv-model.numberoptions.gridRotatechangeonChangeOptions//t-form-itemt-form-item label网格颜色 namegridColort-color-pickerclassw-fullv-modeloptions.gridColor:show-primary-color-previewfalseformatCSS:color-modes[monochrome]changeonChangeOptions//t-form-itemt-divider /t-form-item label标尺 namerulet-switch v-modeloptions.rule changeonChangeOptions //t-form-itemt-divider /t-form-item label背景颜色 namebackgroundt-color-pickerclassw-fullv-modeldata.background:show-primary-color-previewfalseformatCSS:color-modes[monochrome]changeonChangeData//t-form-itemt-form-item label图元默认颜色 namecolort-color-pickerclassw-fullv-modeldata.color:show-primary-color-previewfalseformatCSS:color-modes[monochrome]changeonChangeData//t-form-item/t-form/div
/template
C. 设置图纸数据
const onChangeData () {Object.assign(meta2d.store.data, data);meta2d.store.patchFlagsBackground true;meta2d.render();
};
因为涉及到背景需要设置一个背景更新标志meta2d.store.patchFlagsBackground true;
D. 设置编辑器选项
const onChangeOptions () {meta2d.setOptions(options);meta2d.store.patchFlagsTop true;meta2d.store.patchFlagsBackground true;meta2d.render();
};
因为涉及到标尺需要设置一个标尺图层更新标志meta2d.store.patchFlagsTop true;
2.4.5 图元属性面板
A. 定义图元数据
const pen refany();
// 位置数据。当前版本位置需要动态计算获取
const rect refany();
这里由于图元位置需要动态计算因此需要单独定义。 B. 获取选中图元数据
import { onMounted, onUnmounted, ref, watch } from vue;
import { useSelection } from /services/selections;const { selections } useSelection();onMounted(() {getPen();
});const getPen () {pen.value selections.pen;if (pen.value.globalAlpha undefined) {pen.value.globalAlpha 1;}rect.value meta2d.getPenRect(pen.value);
};// 监听选中不同图元
// ts-ignore
const watcher watch(() selections.pen.id, getPen);onUnmounted(() {watcher();
});
C. 编写UI
templatediv classprops-panelt-form label-alignleft v-ifpenh5 classmb-24图元/h5t-form-item label文本 nametextt-input v-modelpen.text changechangeValue(text) //t-form-itemt-form-item label颜色 namecolort-color-pickerclassw-fullv-modelpen.color:show-primary-color-previewfalseformatCSS:color-modes[monochrome]changechangeValue(color)//t-form-itemt-form-item label背景 namebackgroundt-color-pickerclassw-fullv-modelpen.background:show-primary-color-previewfalseformatCSS:color-modes[monochrome]changechangeValue(background)//t-form-itemt-form-item label线条 namedasht-select v-modelpen.dash changechangeValue(dash)t-option :key0 :value0 label实线/t-optiont-option :key1 :value1 label虚线/t-option/t-select/t-form-itemt-form-item label圆角 nameborderRadiust-input-number:min0:max1:step0.01v-modelpen.borderRadiuschangechangeValue(borderRadius)//t-form-itemt-form-item label不透明度 nameglobalAlphat-sliderv-modelpen.globalAlpha:min0:max1:step0.01changechangeValue(globalAlpha)/span classml-16 stylewidth: 50px; line-height: 30px{{ pen.globalAlpha }}/span/t-form-itemt-divider /t-form-item labelX namext-input-number v-modelrect.x changechangeRect(x) //t-form-itemt-form-item labelY nameyt-input-number v-modelrect.y changechangeRect(y) //t-form-itemt-form-item label宽 namewidtht-input-number v-modelrect.width changechangeRect(width) //t-form-itemt-form-item label高 nameheightt-input-number v-modelrect.height changechangeRect(height) //t-form-itemt-divider /t-form-item label文字水平对齐 nametextAlignt-select v-modelpen.textAlign changechangeValue(textAlign)t-option keyleft valueleft label左对齐/t-optiont-option keycenter valuecenter label居中/t-optiont-option keyright valueright label右对齐/t-option/t-select/t-form-itemt-form-item label文字垂直对齐 nametextBaselinet-selectv-modelpen.textBaselinechangechangeValue(textBaseline)t-option keytop valuetop label顶部对齐/t-optiont-option keymiddle valuemiddle label居中/t-optiont-option keybottom valuebottom label底部对齐/t-option/t-select/t-form-itemt-divider /t-spacet-button clicktop置顶/t-buttont-button clickbottom置底/t-buttont-button clickup上一层/t-buttont-button clickdown下一层/t-button/t-space/t-form/div
/template
D. 设置图元数据
设置图元数据是调用meta2d.setValue实现。
当前需要注意的是 const lineDashs [undefined, [5, 5]];const changeValue (prop: string) {const v: any { id: pen.value.id };v[prop] pen.value[prop];if (prop dash) {v.lineDash lineDashs[v[prop]];}meta2d.setValue(v, { render: true });
};const changeRect (prop: string) {const v: any { id: pen.value.id };v[prop] rect.value[prop];meta2d.setValue(v, { render: true });
};
E. 设置图元层级
根据Meta2d.js 图元API文档调用相关函数即可
const top () {meta2d.top();meta2d.render();
};
const bottom () {meta2d.bottom();meta2d.render();
};
const up () {meta2d.up();meta2d.render();
};
const down () {meta2d.down();meta2d.render();
};
2.4.6 更多图元属性
更多属性功能可参考Meta2d.js 引擎API文档、图元API文档去编写
三、运行查看
因为前面结构规划清晰所以运行查看比较简单只需要加载View.vue子组件即可。整个页面只需短短几行代码即可
templatediv classapp-pageView //div
/templatescript langts setup
import View from ../components/View.vue;
/scriptstyle langpostcss scoped
.app-page {height: 100vh;
}
/style四、开源与代码
Meta2d.js开源地址
Githubhttps://github.com/le5le-com/meta2d.js
Gitee: meta2d.js: The meta2d.js is real-time data exchange and interactive web 2D engine. Developers are able to build Web SCADA, IoT, Digital twins and so on. Meta2d.js是一个实时数据响应和交互的2d引擎可用于Web组态物联网数字孪生等场景。
本教程相关代码开源地址
https://github.com/le5le-com/meta2d.js/tree/main/examples/diagram-editor-vue3 开源不易欢迎大家点星点赞支持
大家的热烈支持是我们做的更好的动力
Github Star地址GitHub - le5le-com/meta2d.js: The meta2d.js is real-time data exchange and interactive web 2D engine. Developers are able to build Web SCADA, IoT, Digital twins and so on. Meta2d.js是一个实时数据响应和交互的2d引擎可用于Web组态物联网数字孪生等场景。
五、其他
如果大家觉得实用、喜欢欢迎转发点赞留言共同学习由于教程都是按照作者自己的视角写的难免考虑不到所有细节欢迎大家写一些自己的学习心得分享 我们计划陆续推出一些系列文章欢迎关注。
最后开源不易写作更不易欢迎点星支持https://github.com/le5le-com/meta2d.js