Browse Source

feat: topo节点 和 流动动画

main
betaqi 3 weeks ago
parent
commit
608ce47a13
  1. 41
      package-lock.json
  2. 1
      package.json
  3. 97
      src/views/testG6/components/Node.vue
  4. 16
      src/views/testG6/index.vue
  5. 5
      src/views/testG6/type/index.ts
  6. 17
      src/views/testG6/utils/GenerateGraphData.ts
  7. 60
      src/views/testG6/utils/MyLineEdge.ts
  8. 38
      src/views/testG6/utils/MyNode.ts
  9. 13
      src/views/testG6/utils/data.ts
  10. 5
      src/views/testG6/utils/index.ts

41
package-lock.json generated

@ -17,6 +17,7 @@
"dexie": "^4.0.11", "dexie": "^4.0.11",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.5", "element-plus": "^2.9.5",
"g6-extension-vue": "^0.1.0",
"jszmq": "^0.1.2", "jszmq": "^0.1.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pinia": "^3.0.1", "pinia": "^3.0.1",
@ -5020,6 +5021,46 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/g6-extension-vue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/g6-extension-vue/-/g6-extension-vue-0.1.0.tgz",
"integrity": "sha512-2RtL2CdcIdnrCFJYETxsI7TNtsE2m3y1oNyXQwM3EUciPMdxjet5okLk6dzY8ohLRUz8sMk4dM4cAE3ljtQXfA==",
"license": "MIT",
"dependencies": {
"@antv/g": "^6.1.27",
"vue-demi": "^0.14.10"
},
"peerDependencies": {
"@antv/g6": ">=5.0.0",
"vue": "^2.0.0 || >=3.0.0"
}
},
"node_modules/g6-extension-vue/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",

1
package.json

@ -22,6 +22,7 @@
"dexie": "^4.0.11", "dexie": "^4.0.11",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.5", "element-plus": "^2.9.5",
"g6-extension-vue": "^0.1.0",
"jszmq": "^0.1.2", "jszmq": "^0.1.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pinia": "^3.0.1", "pinia": "^3.0.1",

97
src/views/testG6/components/Node.vue

@ -1,10 +1,99 @@
<template>
<div :style="{
width: NODE_SIZE[0] + 'px',
height: NODE_SIZE[1] + 'px',
borderRadius: '6px',
marginLeft: -(NODE_SIZE[0] / 2 - 10) + 'px',
}" class="box-border rounded-md overflow-hidden flex">
<div class="h-100% p-4px" :style="{ background: Device.bgColor}">
<img :src="Device.icon" style="height: 100%;" alt="">
</div>
<div class="bg-white flex-1 flex items-center justify-center">
<span class="text-12px">{{ Device.name }}</span>
</div>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import type { MyNodeData } from "../type/index.ts";
import { DeviceType, NODE_SIZE } from "../utils/index";
import EmsIcon from '@/assets/images/device/ems.png';
import EcuIcon from '@/assets/images/device/ecu.png';
import EmIcon from '@/assets/images/device/em.png';
import EmMeasureIcon from '@/assets/images/device/em.png';
import BmsIcon from '@/assets/images/device/bms.png';
import CacIcon from '@/assets/images/device/cac.png';
import DgsIcon from '@/assets/images/device/dgs.png';
import FfsIcon from '@/assets/images/device/ffs.png';
import MpptIcon from '@/assets/images/device/mppt.png';
import PcsIcon from '@/assets/images/device/pcs.png';
import ThsIcon from '@/assets/images/device/ths.png';
import TmsIcon from '@/assets/images/device/tms.png';
import UnitIcon from '@/assets/images/device/unit.png';
import WppIcon from '@/assets/images/device/wpp.png';
import { computed } from "vue";
</script> const typeToIconMap: Record<string, Object> = {
[DeviceType.Ems]: {
icon: EmsIcon,
bgColor: '#0769FF'
},
[DeviceType.Ecu]: {
icon: EcuIcon,
bgColor: '#0769FF'
},
[DeviceType.Em]: {
icon: EmIcon,
bgColor: '#2CA02C'
},
[DeviceType['Em-Measure']]: {
icon: EmIcon,
bgColor: '#2CA02C'
},
[DeviceType.Bms]: {
icon: BmsIcon,
bgColor: '#FF7F50'
},
[DeviceType.Cac]: {
icon: CacIcon,
bgColor: '#17BECF'
},
[DeviceType.Dgs]: {
icon: DgsIcon,
bgColor: '#9467BD'
},
[DeviceType.Ffs]: {
icon: FfsIcon,
bgColor: '#17BECF'
},
[DeviceType.Mppt]: {
icon: MpptIcon,
bgColor: '#9467BD'
},
[DeviceType.Pcs]: {
icon: PcsIcon,
bgColor: '#FF7F50'
},
[DeviceType.Wpp]: {
icon: WppIcon,
bgColor: '#9467BD'
},
[DeviceType.Tms]: {
icon: TmsIcon,
bgColor: '#17BECF'
},
}
<template> const props = defineProps<{
<div>node</div> data: MyNodeData;
</template> }>()
const Device = computed(() => {
const style = typeToIconMap[props.data.data.type];
return Object.assign(props.data.data, style);
})
</script>
<style scoped lang="scss"> <style scoped lang="scss">

16
src/views/testG6/index.vue

@ -5,14 +5,15 @@
<script setup lang="ts"> <script setup lang="ts">
import DeviceData from './utils/data'; import DeviceData from './utils/data';
import { EmLineEdge, MyLineEdge } from './utils/MyLineEdge'; import { EmLineEdge, MyLineEdge } from './utils/MyLineEdge';
import { DeviceType, flattenTree, NODE_SIZE } from "./utils"; import { DeviceType, flattenTree, NODE_SIZE, VIRTUAL_NODE } from "./utils";
import { ExtensionCategory, Graph, register } from '@antv/g6'; import { ExtensionCategory, Graph, register } from '@antv/g6';
import UserCardNode from "./utils/MyNode";
import GenerateGraphData from "./utils/GenerateGraphData"; import GenerateGraphData from "./utils/GenerateGraphData";
import { VueNode } from 'g6-extension-vue';
import Node from './components/Node.vue'
onMounted(() => { onMounted(() => {
register(ExtensionCategory.NODE, 'vue-node', VueNode);
register(ExtensionCategory.EDGE, 'em-line-edge', EmLineEdge) register(ExtensionCategory.EDGE, 'em-line-edge', EmLineEdge)
register(ExtensionCategory.NODE, 'user-card-node', UserCardNode);
register(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge); register(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);
const device = flattenTree(DeviceData) const device = flattenTree(DeviceData)
const graphData = new GenerateGraphData(device) const graphData = new GenerateGraphData(device)
@ -24,15 +25,16 @@ onMounted(() => {
}, },
layout: { layout: {
type: 'dagre', type: 'dagre',
nodesep: 50, nodesep: 50 + NODE_SIZE[0],
ranksep: 50, ranksep: 50,
}, },
animation: false, animation: false,
node: { node: {
type: 'user-card-node', type: 'vue-node',
style: { style: {
size: NODE_SIZE, component: (data: any) => {
customData: (d: any) => d.data, return data.data.type !== VIRTUAL_NODE ? h(Node, { data: data }) : ''
},
}, },
}, },
data: { data: {

5
src/views/testG6/type/index.ts

@ -0,0 +1,5 @@
import type { NodeData } from "@antv/g6";
export type Device = any
export interface MyNodeData extends Omit<NodeData, 'data'> {
data?: Device;
}

17
src/views/testG6/utils/GenerateGraphData.ts

@ -1,9 +1,11 @@
import type { EdgeData, NodeData } from "@antv/g6"; import type { EdgeData, NodeData } from "@antv/g6";
import { type Device, DeviceType,VIRTUAL_NODE } from "./index"; import { DeviceType, VIRTUAL_NODE } from "./index";
import EmsIcon from '@/assets/images/device/ems.png';
import type { Device, MyNodeData } from "../type";
export default class GenerateGraphData { export default class GenerateGraphData {
private readonly devices: Device[] = []; private readonly devices: Device[] = [];
private nodes: NodeData[] = []; private nodes: MyNodeData[] = [];
private edges: EdgeData[] = []; private edges: EdgeData[] = [];
constructor(devices: Device[]) { constructor(devices: Device[]) {
@ -18,14 +20,15 @@ export default class GenerateGraphData {
const isNeedPoint = device.type === DeviceType.Ems || device.type === DeviceType['Em-Measure'] || device.type === DeviceType.Em const isNeedPoint = device.type === DeviceType.Ems || device.type === DeviceType['Em-Measure'] || device.type === DeviceType.Em
this.nodes.push({ this.nodes.push({
id: String(device.id), id: String(device.id),
type: 'user-card-node',
label: device.name, label: device.name,
data: device, data: Object.assign(device, {
typeString: DeviceType[device.type],
}),
style: Object.assign( style: Object.assign(
{ {
// labelText: device.name, // labelText: device.name,
opacity: .3, opacity: .3,
deviceName: (d: any) => d.data.name, icon: (d: any) => EmsIcon,
}) })
}) })
@ -53,7 +56,7 @@ export default class GenerateGraphData {
for (const node of this.nodes) { for (const node of this.nodes) {
const customData: Device = node.data const customData: Device = node.data
const virtualId = `${customData.parentId}-virtual`; const virtualId = `${customData.parentId}-virtual`;
const findParentNode: Device = this.nodes.some((n: NodeData) => n.id === String(customData.parentId)); const findParentNode: Device = this.nodes.some((n: MyNodeData) => n.id === String(customData.parentId));
if (this.isVirtualNode(customData.type)) { if (this.isVirtualNode(customData.type)) {
this.edges.push({ this.edges.push({
@ -102,7 +105,7 @@ export default class GenerateGraphData {
} }
getNodes(): NodeData[] { getNodes(): NodeData[] {
return this.nodes; return this.nodes as NodeData[];
} }
getEdges(): EdgeData[] { getEdges(): EdgeData[] {

60
src/views/testG6/utils/MyLineEdge.ts

@ -2,7 +2,8 @@ import { BaseEdge } from '@antv/g6';
import type { PathArray } from "@antv/util"; import type { PathArray } from "@antv/util";
import { Circle } from '@antv/g'; import { Circle } from '@antv/g';
import { subStyleProps } from '@antv/g6'; import { subStyleProps } from '@antv/g6';
import { type Device, NODE_SIZE } from "./index"; import { NODE_SIZE } from "./index";
import type { Device } from "../type";
export class MyLineEdge extends BaseEdge { export class MyLineEdge extends BaseEdge {
@ -10,7 +11,7 @@ export class MyLineEdge extends BaseEdge {
getMarkerStyle(attributes: object) { getMarkerStyle(attributes: object) {
return { return {
r: 4, r: 4,
fill: '#c3d5f9', fill: '#58C448',
offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker') offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker')
}; };
} }
@ -32,6 +33,33 @@ export class MyLineEdge extends BaseEdge {
delay, delay,
} }
); );
// 涟漪效果:在 marker 外套一个圆
const ripple = this.upsert(
'ripple',
Circle,
{
r: 4, // 初始半径和 marker 一样
stroke: '#58C448',
lineWidth: 2,
fill: 'none',
offsetPath: this.shapeMap.key,
},
this
)!;
// 涟漪动画:半径变大 + 透明度变小
ripple.animate(
[
{ offsetDistance: 0, r: 4, opacity: 0.7 },
{ offsetDistance: 1, r: 6, opacity: 0 },
],
{
duration: 3000,
iterations: Infinity,
delay,
}
);
// marker.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], { // marker.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {
// duration: 3000, // duration: 3000,
// iterations: Infinity, // iterations: Infinity,
@ -56,13 +84,12 @@ export class MyLineEdge extends BaseEdge {
} }
export class EmLineEdge extends BaseEdge { export class EmLineEdge extends BaseEdge {
getMarkerStyle(attributes: object) { getMarkerStyle(attributes: object) {
return { return {
r: 4, r: 4,
fill: '#c3d5f9', fill: '#58C448',
offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker') offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker')
}; };
} }
@ -80,6 +107,31 @@ export class EmLineEdge extends BaseEdge {
} }
); );
// 涟漪效果:在 marker 外套一个圆
const ripple = this.upsert(
'ripple',
Circle,
{
r: 4, // 初始半径和 marker 一样
stroke: '#58C448',
lineWidth: 2,
fill: 'none',
offsetPath: this.shapeMap.key,
},
this
)!;
// 涟漪动画:半径变大 + 透明度变小
ripple.animate(
[
{ offsetDistance: 0, r: 4, opacity: 0.7 },
{ offsetDistance: 1, r: 6, opacity: 0 },
],
{
duration: 3000,
iterations: Infinity,
}
);
} }
getKeyPath(attributes: any): PathArray { getKeyPath(attributes: any): PathArray {

38
src/views/testG6/utils/MyNode.ts

@ -1,5 +1,9 @@
import { Graph, register, Rect, ExtensionCategory } from '@antv/g6'; import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
import { NODE_SIZE } from "./index"; import { DeviceType, NODE_SIZE } from "./index";
// const typeToIconMap = Object.entries(DeviceType).filter(([key, value]) => typeof value === 'number')
// console.log(typeToIconMap)
export default class UserCardNode extends Rect { export default class UserCardNode extends Rect {
get nodeData() { get nodeData() {
@ -14,17 +18,19 @@ export default class UserCardNode extends Rect {
drawNameShape(attributes: any, container: any) { drawNameShape(attributes: any, container: any) {
if (!attributes.customData.name) return; if (!this.data.name) return;
const getUsernameStyle = (attributes: any) => { const getUsernameStyle = (attributes: any) => {
return { return {
// x: -20, x: -14,
text: this.data.name || '', text: this.data.name || '',
fontSize: 12, // fontSize: 12,
width: 20,
fill: '#262626', fill: '#262626',
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'left',
textBaseline: 'middle', textBaseline: 'middle',
maxWidth: 80, // 限制最大宽度
ellipsis: true, // 超出显示 ...
}; };
} }
@ -33,8 +39,24 @@ export default class UserCardNode extends Rect {
} }
drawTypeShape(attributes: any, container: any) { // 头像样式
getAvatarStyle(attributes: any) {
const [width, height] = this.getSize(attributes);
const type = this!.data!.type
return {
x: -width / 2 + 4,
y: -height / 2 + 6,
width: 24,
height: 24,
src: typeToIconMap[type as keyof typeof type] || '',
};
}
drawAvatarShape(attributes: any, container: any) {
if (!this.data.typeString) return;
const avatarStyle = this.getAvatarStyle(attributes);
this.upsert('avatar', 'image', avatarStyle, container);
} }
@ -42,7 +64,7 @@ export default class UserCardNode extends Rect {
// 渲染基础矩形 // 渲染基础矩形
super.render(attributes, container); super.render(attributes, container);
this.drawTypeShape(attributes, container); this.drawAvatarShape(attributes, container);
this.drawNameShape(attributes, container); this.drawNameShape(attributes, container);
} }
} }

13
src/views/testG6/utils/data.ts

@ -75,6 +75,19 @@ export default [{
"description": "", "description": "",
"children": null "children": null
}, },
{
"id": 4022,
"sn": "B48100B210TF36003K-bms",
"name": "BMS-3",
"sort": 1,
"parentId": 401,
"deviceId": "t005104205",
"dictName": "",
"level": 2,
"type": 2,
"description": "",
"children": null
},
{ {
"id": 409, "id": 409,
"sn": "B48100B210TF36003K-pcs", "sn": "B48100B210TF36003K-pcs",

5
src/views/testG6/utils/index.ts

@ -1,8 +1,9 @@
import { flatMap } from 'lodash-es' import { flatMap } from 'lodash-es'
import type { Device } from "../type";
export type Device = any
export const NODE_SIZE: [number, number] = [100, 36]
export const NODE_SIZE: [number, number] = [130, 36]
export enum DeviceType { export enum DeviceType {
Ems, Ems,

Loading…
Cancel
Save