目录
(完整项目及代码可见github)
六、人工智能建模DAG的demo(改官网react版本)
Tip:
- 需要学习vue3,了解不同于vue2的机制(比如setup、reactive、组合式API等)
- 需要学习typescript(anyscript,bushi),可以看下这篇博客
(1)利用vue-cli创建ts版本的vue项目
## 安装或者升级
npm install -g @vue/cli
## 保证 vue cli 版本在 4.5.0 以上
vue --version
## 创建项目
vue create vue_ts_x6_demo
然后的步骤
- Please pick a preset - 选择 Manually select features
- Check the features needed for your project - 选择上 TypeScript ,特别注意点空格是选择,点回车是下一步
- Choose a version of Vue.js that you want to start the project with - 选择 3.x (Preview)
- Use class-style component syntax - 直接回车
- Use Babel alongside TypeScript - 直接回车
- Pick a linter / formatter config - 直接回车
- Use history mode for router? - 直接回车
- Pick a linter / formatter config - 直接回车
- Pick additional lint features - 直接回车
- Where do you prefer placing config for Babel, ESLint, etc.? - 直接回车
- Save this as a preset for future projects? - 直接回车
(2)项目目录
(3)各vue组件
- App.vue
<template>
<!-- <img alt="Vue logo" src="./assets/logo.png">-->
<!-- <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>-->
<dagDemo/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import HelloWorld from './components/HelloWorld.vue';
import dagDemo from "@/components/dagDemo.vue";
export default defineComponent({
name: 'App',
components: {
// HelloWorld
dagDemo
}
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
- dagDemo.vue
<template>
<div id="container"></div>
</template>
<script lang="ts">
import {defineComponent, onMounted, createVNode} from "vue";
import {Graph, Node, Cell, Path} from '@antv/x6'
import {Selection} from "@antv/x6-plugin-selection";
import {register} from "@antv/x6-vue-shape";
import AlgoNode from './AlogNode.vue'
interface NodeStatus {
id: string
status: 'default' | 'success' | 'failed' | 'running'
label?: string
}
const nodeStatusList: NodeStatus[][] = [
[
{
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',
},
],
]
register({
shape: 'dag-node',
width: 180,
height: 36,
// component: {
// template: `<AlgoNode/>`,
// component: AlgoNode,
// },
// 仅一下方法可以使用
component: {
render: ()=>{
return createVNode(AlgoNode);
}
},
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',
},
},
},
},
},
},
)
// console.log('register node!')
Graph.registerEdge(
'dag-edge',
{
inherit: 'edge',
attrs: {
line: {
stroke: '#C2C8D5',
strokeWidth: 1,
targetMarker: null,
},
},
},
true,
)
// console.log('register edge!')
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,
)
// console.log('register connector!')
export default defineComponent({
name: "dagDemo",
setup() {
let graph: Graph;
onMounted(()=>{
graph = new Graph({
container: document.getElementById('container'),
height: 500,
background: {
// color: '#fffbe6',
color: 'white'
},
grid: {
size: 10,
visible: 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,
})
},
},
})
// console.log('create graph!')
graph.use(new Selection({
enabled: true,
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
}))
graph.on('edge:connected', ({ edge }) => {
edge.attr({
line: {
strokeDasharray: '',
},
})
})
graph.on('node:change:data', ({ node }) => {
const edges = graph.getIncomingEdges(node)
const { status } = node.getData() as NodeStatus
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', '')
}
})
})
})
// 初始化节点/边
const init = (data: Cell.Metadata[]) => {
const cells: Cell[] = []
data.forEach((item) => {
if (item.shape === 'dag-node') {
cells.push(graph.createNode(item))
} else {
cells.push(graph.createEdge(item))
}
})
graph.resetCells(cells)
// console.log('init node succeed!')
}
const showNodeStatus = async (statusList: NodeStatus[][]) => {
const status = statusList.shift()
status?.forEach((item) => {
const { id, status } = item
// console.log(status)
const node = graph.getCellById(id)
const data = node.getData() as NodeStatus
node.setData({
...data,
status: status,
})
// console.log(node.getData().status)
})
const timer = setTimeout(() => {
showNodeStatus(statusList)
}, 3000)
if(status === undefined){
clearTimeout(timer)
console.log('clear')
}
}
fetch("/dag.json")
.then((response) => response.json())
.then((data) => {
console.log(data)
init(data)
showNodeStatus(nodeStatusList)
graph.centerContent()
console.log(graph)
})
}
})
</script>
<style scoped>
</style>
- AlgoNode.vue
<template>
<div class="node" :class="status">
<img :src="image.logo" />
<span class="label">{{ label }}</span>
<span class="status" v-if="status === 'success'">
<img :src="image.success"/>
</span>
<span class="status" v-else-if="status === 'failed'">
<img :src="image.failed" />
</span>
<span class="status" v-else-if="status === 'running'">
<img :src="image.running" />
</span>
</div>
</template>
<script lang="ts">
interface NodeStatus {
id: string
status: 'default' | 'success' | 'failed' | 'running'
label?: string
}
export default {
name: "AlgoNode",
inject: ['getNode'],
data() {
//console.log(this)
return {
status: null,
label: null,
image: {
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',
}
}
},
mounted() {
const node = this.getNode()
const data = node?.getData() as NodeStatus
const { label, status = 'default' } = data
this.label = label
this.status = status
// 必须有,监听数据变化
node.on("change:data", ({ current }) => {
this.label = current.label
this.status = current.status
});
}
}
</script>
<style scoped>
.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>
- dag.json
[
{
"id": "1",
"shape": "dag-node",
"x": 290,
"y": 110,
"data": {
"label": "读数据",
"status": "success"
},
"ports": [
{
"id": "1-1",
"group": "bottom"
}
]
},
{
"id": "2",
"shape": "dag-node",
"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": "dag-node",
"x": 170,
"y": 350,
"data": {
"label": "模型预测",
"status": "success"
},
"ports": [
{
"id": "3-1",
"group": "top"
},
{
"id": "3-2",
"group": "bottom"
}
]
},
{
"id": "4",
"shape": "dag-node",
"x": 450,
"y": 350,
"data": {
"label": "读取参数",
"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
}
]
(4)效果图
The end——如果帮到你的话,可以点一个小小的赞吗~~