其實谓谦,只是想找個輪子
前序:
碎碎念:
- 此文個人筆記咽安,官網(wǎng)文檔搬運
- 因為官網(wǎng)對于vue結合Element-UI的示例就幾個,本意只是看中了人工智能建模 DAG 圖逃沿,但是這玩意demo是React,雖然核心的東西一樣幻锁,但是傻瓜式才方便凯亮,故特意按vue2寫了一次,沒用ts哄尔,用ts的其實轉(zhuǎn)換的成本不高
- demo 待上傳中【ps:因demo已魔改假消,那應該是不可能上傳了 →_→ 】
- v1版本過于繁瑣,想入手還是請看v2版本
- x6 版本:1.32.8
- x6-vue-shape 版本:1.4.0
- composition-api 版本:1.7.0
- vue 版本:2.6.11
- vue-template-compiler 版本:2.6.11
- Element-UI 版本:2.15.9
一:步驟:
1 —— 創(chuàng)建vue2項目:詳情請看 vue開發(fā) —— CLI(開發(fā)環(huán)境)搭建
2 —— 引入開發(fā)組件【Element-UI岭接、antv.x6】
npm i element-ui
npm install @antv/x6
npm install @antv/x6-vue-shape
// 在vue2.x 若你引入x6-vue-shape富拗,目前這個版本是必須要的,因由在常見問題2
npm install @vue/composition-api --dev
2.1 —— 生成的項目目錄如下:
Demo
├─ node_modules
├─ public
├─ favicon.ico
└─ index.html
├─ src
├─ assets
└─ logo.png
├─ components
└─ HelloWorld.vue
├─ App.vue
└─ main.js
├─ .browserslistrc
├─ .eslintrc.js
├─ babel.config.js
├─ package.json
├─ package-lock.json
└─ README.md
2.2 —— 修改生成的項目【編輯package.json鸣戴、編輯main.js啃沪、編輯App.vue、新增vue.config.js】
// package.json
// 此處修改為解決常見問題一
"dependencies": {
"vue": "2.6.11"
},
"devDependencies": {
"vue-template-compiler": "2.6.11"
}
// main.js
import Vue from "vue";
import App from "./App.vue";
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false;
Vue.use(ElementUI);
new Vue({
render: (h) => h(App),
}).$mount("#app");
// App.vue
<template>
<div id="app"></div>
</template>
<script>
export default {
name: 'App',
components: {},
};
</script>
<style></style>
// vue.config.js
// 此處新增為解決常見問題二
module.exports = {
runtimeCompiler: true
}
2.3 —— 開啟搬運工生涯【創(chuàng)建畫布窄锅、創(chuàng)建節(jié)點模板创千、新增節(jié)點、新增多個節(jié)點、定時改變多個節(jié)點狀態(tài)追驴、達成官網(wǎng)效果械哟、搭配Element-UI、收工】
官網(wǎng)效果圖
2.3.1 —— 創(chuàng)建畫布
// App.vue
<template>
<div id="app"></div>
</template>
<script>
import { Graph } from '@antv/x6';
import '@antv/x6-vue-shape'
let graph = null;
export default {
name: 'App',
components: {},
mounted() {
graph = new Graph({
container: document.getElementById('app'),
grid: true,
autoResize: true,
});
},
};
</script>
<style>
#app {
width: 100%;
min-height: 500px;
}
</style>
創(chuàng)建畫布
2.3.2 —— 創(chuàng)建節(jié)點模板
// components/NodeTemplate.vue
<template>
<div :class="nodeClass" class="node">
<img :src="logo" />
<span class="label">{{ label }}</span>
<span class="status">
<img :src="statusImg" v-if="statusImg" />
</span>
</div>
</template>
<script>
export default {
inject: ['getGraph', 'getNode'],
data() {
return {
status: '',
label: '',
statusImg: '',
logo: '',
};
},
methods: {
mapper(source, target) {
for (let key in target) {
target[key] = source?.[key] ?? target[key];
}
},
},
created() {
let node = this.getNode();
// 初始化數(shù)據(jù)綁定
this.mapper(node.data, this.$data);
console.info(node);
// 節(jié)點數(shù)據(jù)變化監(jiān)聽殿雪,從而綁定數(shù)據(jù)
node.on('change:data', ({ current }) => this.mapper(current, this.$data));
},
computed: {
nodeClass: function () {
let clazz = {};
if (this.status) clazz[this.status] = true;
return clazz;
},
},
};
</script>
2.3.3 —— 新增節(jié)點
// App.vue
<template>
<div id="app"></div>
</template>
<script>
import { Graph } from '@antv/x6';
import '@antv/x6-vue-shape';
import nodetemplate from '@/components/NodeTemplate';
let graph = null;
export default {
name: 'App',
data() {
return {
node: {
id: '1',
shape: 'vue-shape',
component: 'nodetemplate',
width: 180,
height: 36,
x: 290,
y: 110,
data: {
label: '讀數(shù)據(jù)',
status: 'success',
statusImg: '',
logo: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*evDjT5vjkX0AAAAAAAAAAAAAARQnAQ',
},
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
},
items: [
{
id: '1-1',
group: 'bottom',
},
],
},
},
};
},
mounted() {
graph = new Graph({
container: document.getElementById('app'),
grid: true,
autoResize: true,
});
// 注冊 nodeTemplate
Graph.registerVueComponent(
'nodetemplate',
{
template: `<nodetemplate />`,
components: {
nodetemplate,
},
},
true
);
graph.addNode(this.node);
graph.centerContent();
},
};
</script>
<style>
#app {
width: 100%;
min-height: 500px;
}
.node {
display: flex;
align-items: center;
width: 100%;
height: 100%;
background-color: #fff;
border: 1px solid #c2c8d5;
border-left: 4px solid #5f95ff;
border-radius: 4px;
box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
}
.node img {
width: 20px;
height: 20px;
flex-shrink: 0;
margin-left: 8px;
}
.node .label {
display: inline-block;
flex-shrink: 0;
width: 104px;
margin-left: 8px;
color: #666;
font-size: 12px;
}
.node .status {
flex-shrink: 0;
}
.node.success {
border-left: 4px solid #52c41a;
}
.node.failed {
border-left: 4px solid #ff4d4f;
}
.node.running .status img {
animation: spin 1s linear infinite;
}
.x6-node-selected .node {
border-color: #1890ff;
border-radius: 2px;
box-shadow: 0 0 0 4px #d4e8fe;
}
.x6-node-selected .node.success {
border-color: #52c41a;
border-radius: 2px;
box-shadow: 0 0 0 4px #ccecc0;
}
.x6-node-selected .node.failed {
border-color: #ff4d4f;
border-radius: 2px;
box-shadow: 0 0 0 4px #fedcdc;
}
.x6-edge:hover path:nth-child(2) {
stroke: #1890ff;
stroke-width: 1px;
}
.x6-edge-selected path:nth-child(2) {
stroke: #1890ff;
stroke-width: 1.5px !important;
}
@keyframes running-line {
to {
stroke-dashoffset: -1000;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
單個節(jié)點
2.3.4 —— 新增多個節(jié)點
// App.vue
// 因一些固定數(shù)據(jù)過于龐大暇咆,省略的部分請復制上一節(jié)的代碼數(shù)據(jù)
<template>
<!-- -->
<div id="app"></div>
</template>
<script>
import { Graph } from '@antv/x6';
import '@antv/x6-vue-shape';
import nodetemplate from '@/components/NodeTemplate';
let graph = null;
export default {
name: 'App',
data() {
return {
nodeData: [
{
id: '1',
shape: 'vue-shape',
x: 290,
y: 110,
data: {
label: '讀數(shù)據(jù)',
status: 'success',
},
ports: [
{
id: '1-1',
group: 'bottom',
},
],
},
{
id: '2',
shape: 'vue-shape',
x: 290,
y: 225,
data: {
label: '邏輯回歸',
status: 'success',
},
ports: [
{
id: '2-1',
group: 'top',
},
{
id: '2-2',
group: 'bottom',
},
{
id: '2-3',
group: 'bottom',
},
],
},
{
id: '3',
shape: 'vue-shape',
x: 170,
y: 350,
data: {
label: '模型預測',
status: 'success',
},
ports: [
{
id: '3-1',
group: 'top',
},
{
id: '3-2',
group: 'bottom',
},
],
},
{
id: '4',
shape: 'vue-shape',
x: 450,
y: 350,
data: {
label: '讀取參數(shù)',
status: 'success',
},
ports: [
{
id: '4-1',
group: 'top',
},
{
id: '4-2',
group: 'bottom',
},
],
},
{
id: '5',
shape: 'dag-edge',
source: {
cell: '1',
port: '1-1',
},
target: {
cell: '2',
port: '2-1',
},
zIndex: 0,
},
{
id: '6',
shape: 'dag-edge',
source: {
cell: '2',
port: '2-2',
},
target: {
cell: '3',
port: '3-1',
},
zIndex: 0,
},
{
id: '7',
shape: 'dag-edge',
source: {
cell: '2',
port: '2-3',
},
target: {
cell: '4',
port: '4-1',
},
zIndex: 0,
},
],
nodeImage: {
logo: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*evDjT5vjkX0AAAAAAAAAAAAAARQnAQ',
success:
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ',
failed:
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ',
running:
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*t8fURKfgSOgAAAAAAAAAAAAAARQnAQ',
},
};
},
methods: {
init(data) {
let cells = [];
data.forEach((item) => {
if (item.shape === 'vue-shape') {
item.width = 180;
item.height = 36;
item.component = 'nodetemplate';
item.data.logo = this.nodeImage.logo;
item.data.statusImg = '';
if (item.ports)
item.ports = {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
},
items: item.ports,
};
cells.push(graph.createNode(item));
} else {
cells.push(graph.createEdge(item));
}
});
graph.resetCells(cells);
},
},
mounted() {
graph = new Graph({
container: document.getElementById('app'),
grid: true,
autoResize: true,
});
// 注冊 nodeTemplate
Graph.registerVueComponent(
'nodetemplate',
{
template: `<nodetemplate />`,
components: {
nodetemplate,
},
},
true
);
// 注冊 nodeTemplate 的鏈接線
Graph.registerEdge(
'dag-edge',
{
inherit: 'edge',
attrs: {
line: {
stroke: '#C2C8D5',
strokeWidth: 1,
targetMarker: null,
},
},
},
true
);
this.init(this.nodeData);
graph.centerContent();
},
};
</script>
<style>
/* 和上一節(jié)一樣 */
</style>
多個節(jié)點
2.3.5 —— 定時改變多個節(jié)點狀態(tài)
// App.vue
// 因一些固定數(shù)據(jù)過于龐大,省略的部分請復制上一節(jié)的代碼數(shù)據(jù)
<template>
<div id="app"></div>
</template>
<script>
import { Graph } from '@antv/x6';
import '@antv/x6-vue-shape';
import nodetemplate from '@/components/NodeTemplate';
let graph = null;
export default {
name: 'App',
data() {
return {
// 和上節(jié)一樣
nodeStatusList: [
[
{
id: '1',
status: 'running',
},
{
id: '2',
status: 'default',
},
{
id: '3',
status: 'default',
},
{
id: '4',
status: 'default',
},
],
[
{
id: '1',
status: 'success',
},
{
id: '2',
status: 'running',
},
{
id: '3',
status: 'default',
},
{
id: '4',
status: 'default',
},
],
[
{
id: '1',
status: 'success',
},
{
id: '2',
status: 'success',
},
{
id: '3',
status: 'running',
},
{
id: '4',
status: 'running',
},
],
[
{
id: '1',
status: 'success',
},
{
id: '2',
status: 'success',
},
{
id: '3',
status: 'success',
},
{
id: '4',
status: 'failed',
},
],
],
};
},
methods: {
// 和上節(jié)一樣
showNodeStatus(statusList) {
let status = statusList.shift();
status?.forEach((item) => {
let { id, status } = item;
let node = graph.getCellById(id);
let data = node.getData();
node.setData({
...data,
status: status,
statusImg: this.nodeImage[status],
});
});
setTimeout(() => {
this.showNodeStatus(statusList);
}, 3000);
},
},
mounted() {
// 和上節(jié)一樣
this.init(this.nodeData);
this.showNodeStatus(this.nodeStatusList);
graph.centerContent();
},
};
</script>
<style>
// 和上節(jié)一樣
</style>
定時改變多個節(jié)點狀態(tài)
2.3.6 —— 添加屬性達成官網(wǎng)效果
// App.vue
// 因一些固定數(shù)據(jù)過于龐大冠摄,省略的部分請復制上一節(jié)的代碼數(shù)據(jù)
<template>
<div id="app"></div>
</template>
<script>
import { Graph, Path } from '@antv/x6';
import '@antv/x6-vue-shape';
import nodetemplate from '@/components/NodeTemplate';
let graph = null;
export default {
name: 'App',
data() {
return {
// 和上節(jié)一樣
};
},
methods: {
// 和上節(jié)一樣
},
mounted() {
graph = new Graph({
container: document.getElementById('app'),
grid: true,
autoResize: true,
panning: {
enabled: true,
eventTypes: ['leftMouseDown', 'mouseWheel'],
},
mousewheel: {
enabled: true,
modifiers: 'ctrl',
factor: 1.1,
maxScale: 1.5,
minScale: 0.5,
},
highlighting: {
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#fff',
stroke: '#31d0c6',
strokeWidth: 4,
},
},
},
},
connecting: {
snap: true,
allowBlank: false,
allowLoop: false,
highlight: true,
connector: 'algo-connector',
connectionPoint: 'anchor',
anchor: 'center',
validateMagnet({ magnet }) {
return magnet.getAttribute('port-group') !== 'top';
},
createEdge() {
return graph.createEdge({
shape: 'dag-edge',
attrs: {
line: {
strokeDasharray: '5 5',
},
},
zIndex: -1,
});
},
},
selecting: {
enabled: true,
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
},
});
// 注冊 nodeTemplate
Graph.registerVueComponent(
'nodetemplate',
{
template: `<nodetemplate />`,
components: {
nodetemplate,
},
},
true
);
// 注冊 nodeTemplate 的鏈接線
Graph.registerEdge(
'dag-edge',
{
inherit: 'edge',
attrs: {
line: {
stroke: '#C2C8D5',
strokeWidth: 1,
targetMarker: null,
},
},
},
true
);
// 注冊 nodeTemplate 的鏈接之間的線樣式
Graph.registerConnector(
'algo-connector',
(s, e) => {
const offset = 4;
const deltaY = Math.abs(e.y - s.y);
const control = Math.floor((deltaY / 3) * 2);
const v1 = { x: s.x, y: s.y + offset + control };
const v2 = { x: e.x, y: e.y - offset - control };
return Path.normalize(
`M ${s.x} ${s.y}
L ${s.x} ${s.y + offset}
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
L ${e.x} ${e.y}
`
);
},
true
);
// 控制線鏈接時的樣式
graph.on('edge:connected', ({ edge }) => {
edge.attr({
line: {
strokeDasharray: '',
},
});
});
// 控制節(jié)點數(shù)據(jù)變更時線的樣式
graph.on('node:change:data', ({ node }) => {
let edges = graph.getIncomingEdges(node);
let { status } = node.getData();
edges?.forEach((edge) => {
if (status === 'running') {
edge.attr('line/strokeDasharray', 5);
edge.attr('line/style/animation', 'running-line 30s infinite linear');
} else {
edge.attr('line/strokeDasharray', '');
edge.attr('line/style/animation', '');
}
});
});
this.init(this.nodeData);
this.showNodeStatus(this.nodeStatusList);
graph.centerContent();
},
};
</script>
<style>
// 和上節(jié)一樣
</style>
達成效果
2.3.6 —— 廢話了這么多糯崎,就是和一開始引入的Element-UI無關,客官莫急河泳,菜來也
// components/EleTemplate.vue
<template>
<el-alert title="瀟風劍易水" type="warning" close-text="賽雷"> </el-alert>
</template>
// App.vue
import eletemplate from '@/components/EleTemplate';
// 注冊 eletemplate
Graph.registerVueComponent(
'eletemplate',
{
template: `<eletemplate />`,
components: {
eletemplate,
},
},
true
);
graph.addNode({
id: '1',
shape: 'vue-shape',
component: 'eletemplate',
width: 180,
height: 36,
x: 290,
y: 110,
});
graph.centerContent();
搭配Element-UI效果圖
2.3.7 —— 收工沃呢,翻歸賣豉油
二:常見問題:
1 —— 運行時編譯報錯如下:
Vue packages version mismatch:
- vue@2.6.14 (D:\x6-dag\node_modules\vue\dist\vue.runtime.common.js)
- vue-template-compiler@2.7.5 (D:\x6-dag\node_modules\vue-template-compiler\package.json)
This may cause things to work incorrectly. Make sure to use the same version for both.
If you are using vue-loader@>=10.0, simply update vue-template-compiler.
If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump vue-template-compiler to the latest.
Error:
Vue packages version mismatch:
- vue@2.6.14 (D:\x6-dag\node_modules\vue\dist\vue.runtime.common.js)
- vue-template-compiler@2.7.5 (D:\x6-dag\node_modules\vue-template-compiler\package.json)
This may cause things to work incorrectly. Make sure to use the same version for both.
If you are using vue-loader@>=10.0, simply update vue-template-compiler.
If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump vue-template-compiler to the latest.
解決方法:
// package.json
// 僅修改 package.json的dependencies節(jié)點的vue版本和devDependencies節(jié)點的vue-template-compiler版本,
// 均需要去掉^拆挥,保持2者的版本一致薄霜,重新npm i 再啟動
"dependencies": {
"vue": "2.6.11"
},
"devDependencies": {
"vue-template-compiler": "2.6.11"
}
2 —— 運行時瀏覽器報錯如下:
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
解決方法:
// 項目根目錄創(chuàng)建vue.config.js并添加如下內(nèi)容
// vue.config.js
module.exports = {
runtimeCompiler: true
}
2 —— 編譯時報錯如下:
This dependency was not found:
* @vue/composition-api in ./node_modules/vue-demi/lib/index.esm.js
To install it, you can run: npm install --save @vue/composition-api
解決方法:
npm install --save @vue/composition-api
3 —— 當卸載composition-api在運行時不會報錯正常運行,問題2不會在提示纸兔,同時缺失這個惰瓜,導致vue-demi的判斷會一直是isVue2為false,isVue3為true汉矿,從而當渲染vue模板時會一直走vue3的方法崎坊,個人認為vue-demi應該去檢查運行中的vue對象,由vue對象的某些vue3特性來判斷是否走vue3內(nèi)容洲拇,vue-demi是x6-vue-shape的依賴非本項目的依賴:
4 —— 拖曳創(chuàng)建的節(jié)點奈揍,使用node.setData不觸發(fā)綁定的change:data事件,但是實際上打印出來的node的data的確改動了赋续,在數(shù)據(jù)為null的情況下男翰,這個的確生效,但是并不是通過change:data事件觸發(fā)的纽乱,暫時從源碼也沒法看出錯誤點蛾绎,還沒排除新版本是否已修復,源碼看的新版本的鸦列,后續(xù)會更新此版本寫的demo:setData為updateData
問題因由:并不是updateData還是setData的沒生效的問題或者拖曳生成或者版本的問題(源碼版本搜索里面也可以找到和版本有一點點的關系租冠,畢竟之前是沒加這個相同就不更新的原則),而是通過node.getData()直接操作了數(shù)據(jù)薯嗤,導致setData比較了一致就不更新肺稀,其實這也是可以后期修改下,直接通知vue組件更新的同時更新node數(shù)據(jù)应民,脫離這種setData的內(nèi)置方法,但是僅僅是一個權宜之計,也是自己寫的時候太過于不顧前后導致的低級bug
解決方法:
不要直接操縱node的data數(shù)據(jù)诲锹,需要通過其自身提供的方法比如setData或者updateData繁仁,但是在考慮到x6它自身這種結合vue這種框架的事件綁定脫離了這種框架定義的東西時,其實可以適當改造归园,讓x6更符合vue的寫法黄虱,而不是遵循它自身這種數(shù)據(jù)變化的寫法