前端Web实战:从零打造一个类Visio的流程图拓扑图绘图工具
前言
大家好,本系列从Web前端实战的角度,给大家分享介绍如何从零打造一个自己专属的绘图工具,实现流程图、拓扑图、脑图等类Visio的绘图工具。

你将收获
- 免费好用、专属自己的绘图工具
- 前端项目实战学习
- 如何从0搭建一个前端项目等基础框架
- 项目设计思路及优雅的架构技巧
- 开源项目学习
- 热门可视化引擎Meta2d.js等学习使用
技术栈
Meta2d.js - 国产开源免费好用的可视化引擎
Vue3 - 流行的简单易用等前端Web框架
Vite - 高效好用的前端热门构建工具
TDesign - 支持Vue3的前端UI组件库
需要提前掌握
- 前端基础工具node.js安装(仅安装即可)
- npm(pnpm、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安装依赖包后,发现运行错误。查看有新的vite@4.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直接由编辑器各个子组件构成:
<template><div class="app-page"><Header /><div class="designer"><Graphics /><View /><Props /></div></div>
</template><script lang="ts" 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';
</script><style lang="postcss" scoped>
.app-page {height: 100vh;overflow: hidden;
}
</style>
2.1 创建编辑器画布 View
2.1.1 挂载
Meta2d画布实例必须挂载在html中DOM元素上
<div id="meta2d"></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 class="app-header"><a class="logo" href="https://le5le.com" target="_blank"><img src="/favicon.ico" /><span>乐吾乐</span></a><t-dropdown:minColumnWidth="200":maxHeight="560"overlayClassName="header-dropdown"><a> 文件 </a><t-dropdown-menu><t-dropdown-item @click="newFile"><a>新建文件</a></t-dropdown-item><t-dropdown-item @click="openFile" divider="true"><a>打开文件</a></t-dropdown-item><t-dropdown-item divider="true"><a @click="downloadJson">下载JSON文件</a></t-dropdown-item><t-dropdown-item><a @click="downloadPng">下载为PNG</a></t-dropdown-item><t-dropdown-item><a @click="downloadSvg">下载为SVG</a></t-dropdown-item></t-dropdown-menu></t-dropdown><t-dropdown:minColumnWidth="180":maxHeight="500"overlayClassName="header-dropdown"><a> 编辑 </a><t-dropdown-menu><t-dropdown-item><a @click="onUndo"><div class="flex">撤销 <span class="flex-grow"></span> Ctrl + Z</div></a></t-dropdown-item><t-dropdown-item divider="true"><a @click="onRedo"><div class="flex">恢复 <span class="flex-grow"></span> Ctrl + Y</div></a></t-dropdown-item><t-dropdown-item><a @click="onCut"><div class="flex">剪切 <span class="flex-grow"></span> Ctrl + X</div></a></t-dropdown-item><t-dropdown-item><a @click="onCopy"><div class="flex">复制 <span class="flex-grow"></span> Ctrl + C</div></a></t-dropdown-item><t-dropdown-item divider="true"><a @click="onPaste"><div class="flex">粘贴 <span class="flex-grow"></span> Ctrl + V</div></a></t-dropdown-item><t-dropdown-item><a @click="onAll"><div class="flex">全选 <span class="flex-grow"></span> Ctrl + A</div></a></t-dropdown-item><t-dropdown-item><a @click="onDelete"><div class="flex">删除 <span class="flex-grow"></span> DELETE</div></a></t-dropdown-item></t-dropdown-menu></t-dropdown><t-dropdown:minColumnWidth="180":maxHeight="500":delay2="[10, 150]"overlayClassName="header-dropdown"><a> 帮助 </a><t-dropdown-menu><t-dropdown-item v-for="item in assets.helps" :divider="item.divider"><a :href="item.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 Promise<string>((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;charset=utf-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 x="0" y="0" width="100%" height="100%" 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:draggable="true"@dragstart="onAddShape($event, 'line')"@click="onAddShape($event, 'line')"><t-icon name="slash" /></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:draggable="true"@dragstart="onAddShape($event, 'text')"@click="onAddShape($event, 'text')"><svg class="l-icon" aria-hidden="true"><use 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="连线"><svgwidth="1em"height="1em"viewBox="0 0 1024 1024"xmlns="http://www.w3.org/2000/svg"@click="drawLine":style="{color: isDrawLine ? ' #1677ff' : '',}"><pathd="M192 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-128z"fill="currentColor"></path></svg>
</t-tooltip>
实现连线
// 连线状态
const isDrawLine = ref<boolean>(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:minColumnWidth="160":maxHeight="560"overlayClassName="header-dropdown"><a><svg class="l-icon" aria-hidden="true"><use:xlink:href="lineTypes.find((item) => item.value === currentLineType)?.icon"></use></svg></a><t-dropdown-menu><t-dropdown-item v-for="item in lineTypes"><div class="flex middle" @click="changeLineType(item.value)">{{ item.name }} <span class="flex-grow"></span><svg class="l-icon" aria-hidden="true"><use :xlink:href="item.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:minColumnWidth="160":maxHeight="560":delay2="[10, 150]"overlayClassName="header-dropdown"><a><svg class="l-icon" aria-hidden="true"><use:xlink:href="fromArrows.find((item) => item.value === fromArrow)?.icon"></use></svg></a><t-dropdown-menu><t-dropdown-item v-for="item in fromArrows"><divclass="flex middle"style="height: 30px"@click="changeFromArrow(item.value)"><svg class="l-icon" aria-hidden="true"><use :xlink:href="item.icon"></use></svg></div></t-dropdown-item></t-dropdown-menu></t-dropdown><t-dropdown:minColumnWidth="160":maxHeight="560":delay2="[10, 150]"overlayClassName="header-dropdown"><a><svg class="l-icon" aria-hidden="true"><use:xlink:href="toArrows.find((item) => item.value === toArrow)?.icon"></use></svg></a><t-dropdown-menu><t-dropdown-item v-for="item in toArrows"><divclass="flex middle"style="height: 30px"@click="changeToArrow(item.value)"><svg class="l-icon" aria-hidden="true"><use :xlink:href="item.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 name="play-circle-stroke" @click="onView" />
</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
<template><div class="app-page"><View /></div>
</template><script lang="ts" 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);
});
</script><style lang="postcss" 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 :defaultExpandAll="true"><t-collapse-panel:header="item.name"v-for="item in graphicGroups":key="item.name"><template v-for="elem in item.list"><divclass="graphic":draggable="true"@dragstart="dragStart($event, elem)"@click.prevent="dragStart($event, elem)"><svg class="l-icon" aria-hidden="true"><use :xlink:href="'#' + elem.icon"></use></svg><p :title="elem.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中根据不同的管理状态,显示不同子组件即可
<template><div class="app-props">{{ selections.mode }}<FileProps v-if="selections.mode === SelectionMode.File" /><PenProps v-else-if="selections.mode === SelectionMode.Pen" /></div>
</template><script lang="ts" setup>
import FileProps from './FileProps.vue';
import PenProps from './PenProps.vue';import { useSelection, SelectionMode } from '@/services/selections';const { selections } = useSelection();
</script>
<style lang="postcss" scoped>
.app-props {border-left: 1px solid var(--color-border);z-index: 2;height: calc(100vh - 80px);overflow-y: auto;
}
</style>
2.4.4 图纸属性面板
这里暂时设置图纸属性有:图纸名称、网格、标尺、颜色等。
【注意注意注意】:
图纸名称、颜色属于图纸数据,参考Meta2d.js文档。图纸名称属于自定义业务数据,自己扩展定义的;
网格、标尺即可以在图纸数据设置,也可以在Meta2d.js Options选项设置。这里,我们在Options选项设置。
Options被视为独立于图纸外的默认通用样式,而图纸数据则归属于图纸专属数据。
A. 定义Vue组件数据
// 图纸数据
const data = reactive<any>({name: '',background: undefined,color: undefined,
});// 画布选项
const options = reactive<any>({grid: false,gridSize: 10,gridRotate: undefined,gridColor: undefined,rule: true,
});
B. 定义组件UI
<template><div class="props-panel"><t-form label-align="left"><h5 class="mb-24">图纸</h5><t-form-item label="图纸名称" name="name"><t-input v-model="data.name" @change="onChangeData" /></t-form-item><t-divider /><t-form-item label="网格" name="grid"><t-switch v-model="options.grid" @change="onChangeOptions" /></t-form-item><t-form-item label="网格大小" name="gridSize"><t-input v-model.number="options.gridSize" @change="onChangeOptions" /></t-form-item><t-form-item label="网格角度" name="gridRotate"><t-inputv-model.number="options.gridRotate"@change="onChangeOptions"/></t-form-item><t-form-item label="网格颜色" name="gridColor"><t-color-pickerclass="w-full"v-model="options.gridColor":show-primary-color-preview="false"format="CSS":color-modes="['monochrome']"@change="onChangeOptions"/></t-form-item><t-divider /><t-form-item label="标尺" name="rule"><t-switch v-model="options.rule" @change="onChangeOptions" /></t-form-item><t-divider /><t-form-item label="背景颜色" name="background"><t-color-pickerclass="w-full"v-model="data.background":show-primary-color-preview="false"format="CSS":color-modes="['monochrome']"@change="onChangeData"/></t-form-item><t-form-item label="图元默认颜色" name="color"><t-color-pickerclass="w-full"v-model="data.color":show-primary-color-preview="false"format="CSS":color-modes="['monochrome']"@change="onChangeData"/></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 = ref<any>();
// 位置数据。当前版本位置需要动态计算获取
const rect = ref<any>();
这里由于图元位置需要动态计算,因此需要单独定义。
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
<template><div class="props-panel"><t-form label-align="left" v-if="pen"><h5 class="mb-24">图元</h5><t-form-item label="文本" name="text"><t-input v-model="pen.text" @change="changeValue('text')" /></t-form-item><t-form-item label="颜色" name="color"><t-color-pickerclass="w-full"v-model="pen.color":show-primary-color-preview="false"format="CSS":color-modes="['monochrome']"@change="changeValue('color')"/></t-form-item><t-form-item label="背景" name="background"><t-color-pickerclass="w-full"v-model="pen.background":show-primary-color-preview="false"format="CSS":color-modes="['monochrome']"@change="changeValue('background')"/></t-form-item><t-form-item label="线条" name="dash"><t-select v-model="pen.dash" @change="changeValue('dash')"><t-option :key="0" :value="0" label="实线"></t-option><t-option :key="1" :value="1" label="虚线"></t-option></t-select></t-form-item><t-form-item label="圆角" name="borderRadius"><t-input-number:min="0":max="1":step="0.01"v-model="pen.borderRadius"@change="changeValue('borderRadius')"/></t-form-item><t-form-item label="不透明度" name="globalAlpha"><t-sliderv-model="pen.globalAlpha":min="0":max="1":step="0.01"@change="changeValue('globalAlpha')"/><span class="ml-16" style="width: 50px; line-height: 30px">{{ pen.globalAlpha }}</span></t-form-item><t-divider /><t-form-item label="X" name="x"><t-input-number v-model="rect.x" @change="changeRect('x')" /></t-form-item><t-form-item label="Y" name="y"><t-input-number v-model="rect.y" @change="changeRect('y')" /></t-form-item><t-form-item label="宽" name="width"><t-input-number v-model="rect.width" @change="changeRect('width')" /></t-form-item><t-form-item label="高" name="height"><t-input-number v-model="rect.height" @change="changeRect('height')" /></t-form-item><t-divider /><t-form-item label="文字水平对齐" name="textAlign"><t-select v-model="pen.textAlign" @change="changeValue('textAlign')"><t-option key="left" value="left" label="左对齐"></t-option><t-option key="center" value="center" label="居中"></t-option><t-option key="right" value="right" label="右对齐"></t-option></t-select></t-form-item><t-form-item label="文字垂直对齐" name="textBaseline"><t-selectv-model="pen.textBaseline"@change="changeValue('textBaseline')"><t-option key="top" value="top" label="顶部对齐"></t-option><t-option key="middle" value="middle" label="居中"></t-option><t-option key="bottom" value="bottom" label="底部对齐"></t-option></t-select></t-form-item><t-divider /><t-space><t-button @click="top">置顶</t-button><t-button @click="bottom">置底</t-button><t-button @click="up">上一层</t-button><t-button @click="down">下一层</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子组件即可。整个页面只需短短几行代码即可:
<template><div class="app-page"><View /></div>
</template><script lang="ts" setup>
import View from '../components/View.vue';
</script><style lang="postcss" scoped>
.app-page {height: 100vh;
}
</style>
四、开源与代码
Meta2d.js开源地址
Github:https://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
相关文章:
前端Web实战:从零打造一个类Visio的流程图拓扑图绘图工具
前言 大家好,本系列从Web前端实战的角度,给大家分享介绍如何从零打造一个自己专属的绘图工具,实现流程图、拓扑图、脑图等类Visio的绘图工具。 你将收获 免费好用、专属自己的绘图工具前端项目实战学习如何从0搭建一个前端项目等基础框架项…...
2023牛客暑期多校第二场部分题解
索引 ABCDEFGHIK A 队友开的题,说是其实就是问能不能用若干个数异或出来某个数。 应该就是线性基板子,然后他写了一下就过了。 B 一开始看没什么人过不是很敢开,结果到后面一看题——这不是最大权闭合子图板子吗??…...
20230724将真我Realme手机GT NEO3连接到WIN10的电脑的步骤
20230724将真我Realme手机GT NEO3连接到WIN10的电脑的步骤 2023/7/24 23:23 缘起:因为找使用IMX766的手机,找到Realme手机GT NEO3了。 同样使用IMX766的还有:Redmi Note12Pro 5G IMX766 旗舰影像 OIS光学防抖 OLED柔性直屏 8GB256GB时光蓝 现…...
黑马 pink h5+css3+移动端前端
网页概念 网页是网站的一页,网页有很多元素组成,包括视频图片文字视频链接等等,以.htm和.html后缀结尾,俗称html文件 HTML 超文本标记语言,描述网页语言,不是编程语言,是标记语言,有标签组成 超文本指的是不光文本,还有图片视频等等标签 常用浏览器 firefox google safari…...
Docker的七项优秀实践
众所周知,作为一个文本文档,Dockerfile包含了用户创建镜像的所有命令和说明。Docker可以通过读取Dockerfile中指令的方式,去自动构建镜像。因此,大家往往认为编写Dockerfile理应非常简单,只需从互联网上选择一个示例&a…...
【数据结构】24王道考研笔记——图
六、图 目录 六、图定义及基本术语图的定义有向图以及无向图简单图以及多重图度顶点-顶点间关系连通图、强连通图子图连通分量强连通分量生成树生成森林边的权、带权网/图特殊形态的图 图的存储及基本操作邻接矩阵邻接表法十字链表邻接多重表分析对比图的基本操作 图的遍历广度…...
zabbix钉钉报警
登录钉钉客户端,创建一个群,把需要收到报警信息的人员都拉到这个群内. 然后点击群右上角 的"群机器人"->"添加机器人"->"自定义", 记录该机器人的webhook值。 添加机器人 在钉钉群中,找到只能群助手 添加机器人 选择自定义机…...
Spring 源码解读
1、Spring 的结构组成 1.1、核心类介绍 Spring 中有两个最核心的类 1 DefaultListableBeanFactory XmlBeanFactory 继承自 DefaultListableBeanFactory,而DefaultListableBeanFactory 是整个 bean加载的核心部分,是 Spring 注册及加载 bean 的默认实现…...
练习时长两年半的网络安全防御“first”
1.网络安全常识及术语 下边基于这次攻击演示我们介绍一下网络安全的一些常识和术语。 资产 任何对组织业务具有价值的信息资产,包括计算机硬件、通信设施、 IT 环境、数据库、软件、文档资料、信息服务和人员等。 网络安全 网络安全是指网络系统的硬件、软件及…...
HttpRunner自动化测试之响应中文乱码处理
响应中文乱码: 当调用接口,响应正文返回的中文是乱码时,一般是响应正文的编码格式不为 utf-8 导致,此时需要根据实际的编码格式处理 示例: 图1中 extract 提取title标题,output 输出 title 变量值&#x…...
idea使用命令将jar包导入到maven仓库中
因为今天突然忘了命令,记下来方便以后查看 pom文件的依赖 jar包路径 进入idea中命令窗 输入命令 mvn install:install-file -DfileD:\Project\spring-cloud\dubbo-api\target\dubbo-api-1.0-SNAPSHOT.jar -DgroupIdcom.wmx -DartifactIddubbo-api -Dversion1.0…...
zookeeper学习(一) Standalone模式(单机模式)安装
安装准备 centos7环境jdk1.8环境zookeeper安装包 安装jdk 上传jdk安装包解压安装包到目录中 tar -zxvf jdk-8u361-linux-x64.tar.gz如果需要指定目录可以在后面加上 -C,如 tar -zxvf jdk-8u361-linux-x64.tar.gz -C 目录配置jdk环境变量 vim /etc/profile打开…...
native webrtc支持切换音频采集设备和获取裸流
https://www.yuque.com/caokunchao/rtendq/oq8w3qgs3g59whru 前言 版本webrtc m96 1、修改webrtc m96代码,向外提供一个adm指针的接口出来 2、外部来获取指针进行设备的选择 3、外部获取音频裸流,麦克风或者扬声器的数据 修改webrtc代码 1、修改H:\w…...
HR怎么看待PMP证书呢?
在当今竞争激烈的职场环境中,拥有专业的证书已经成为了许多人提升职业竞争力的必要途径。PMP证书作为项目管理领域的国际认证,备受HR和企业的青睐。那么,HR在招聘和评估员工时,究竟是如何看待PMP证书的呢? 首先&#x…...
API接口:如何通过使用手机归属地查询
随着手机普及率的不断增加,手机号码的信息查询也成为了一个非常实用的功能。本文将介绍如何通过使用手机归属地查询API接口实现查询手机号码所在地的功能。 首先,我们需要一个可以查询手机号码所在地的API接口。目前市面上有很多免费或付费的API接口可供…...
小创业公司死亡剧本
感觉蛮真实的;很多小创业公司没有阿里华为的命,却得了阿里华为的病。小的创业公司要想活无非以下几点: 1 现金流,现金流,现金流; 2 产品,找痛点,不要搞伪需求; 3 根据公司…...
国产化的接口测试、接口自动化测试工具Apipost的介绍及使用
Apipost介绍: Apipost是 API 文档、API 调试、API Mock、API 自动化测试一体化的研发协作赋能平台,它的定位 Postman Swagger Mock JMeter。 Apipost 是接口管理、开发、测试全流程集成工具,能支撑整个研发技术团队同平台工作࿰…...
【MySQL】不允许你不知道如何插入数据
🎬 博客主页:博主链接 🎥 本文由 M malloc 原创,首发于 CSDN🙉 🎄 学习专栏推荐:LeetCode刷题集 🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正࿰…...
Vue 渲染流程详解
在 Vue 里渲染一块内容,会有以下步骤及流程: 第一步,解析语法,生成AST 第二步,根据AST结果,完成data数据初始化 第三步,根据AST结果和DATA数据绑定情况,生成虚拟DOM 第四步&…...
10分钟内入门 ArcGIS Pro
本文来源:GIS荟 大家好,这篇文章大概会花费你10分钟的时间,带你入门 ArcGIS Pro 的使用,不过前提是你有 ArcMap 使用经验。 我将从工程文件组织方式、软件界面、常用功能、编辑器、制图这5个维度给大家介绍。 演示使用的 ArcGI…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...
华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
Pinocchio 库详解及其在足式机器人上的应用
Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库,专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性,并提供了一个通用的框架&…...
系统掌握PyTorch:图解张量、Autograd、DataLoader、nn.Module与实战模型
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文通过代码驱动的方式,系统讲解PyTorch核心概念和实战技巧,涵盖张量操作、自动微分、数据加载、模型构建和训练全流程&#…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...
