import G6 from '@antv/g6'; import { createStyles, css } from 'antd-style'; import { useRouteMeta } from 'dumi'; import React, { useEffect, useRef } from 'react'; G6.registerNode('behavior-start-node', { draw: (cfg, group) => { const textWidth = G6.Util.getTextSize(cfg!.label, 16)[0]; const size = [textWidth + 20 * 2, 48]; const keyShape = group!.addShape('rect', { name: 'start-node', attrs: { width: size[0], height: size[1], y: -size[1] / 2, radius: 8, fill: '#fff', }, }); group!.addShape('text', { attrs: { text: `${cfg!.label}`, fill: 'rgba(0, 0, 0, 0.88)', fontSize: 16, fontWeight: 500, x: 20, textBaseline: 'middle', }, name: 'start-node-text', }); return keyShape; }, getAnchorPoints() { return [ [0, 0.5], [1, 0.5], ]; }, }); G6.registerNode( 'behavior-sub-node', { draw: (cfg, group) => { const textWidth = G6.Util.getTextSize(cfg!.label, 14)[0]; const padding = 16; const size = [textWidth + 16 * 2 + (cfg!.targetType ? 12 : 0) + (cfg!.link ? 20 : 0), 40]; const keyShape = group!.addShape('rect', { name: 'sub-node', attrs: { width: size[0], height: size[1], y: -size[1] / 2, radius: 8, fill: '#fff', cursor: 'pointer', }, }); group!.addShape('text', { attrs: { text: `${cfg!.label}`, x: cfg!.targetType ? 12 + 16 : padding, fill: 'rgba(0, 0, 0, 0.88)', fontSize: 14, textBaseline: 'middle', cursor: 'pointer', }, name: 'sub-node-text', }); if (cfg!.targetType) { group!.addShape('rect', { name: 'sub-node-type', attrs: { width: 8, height: 8, radius: 4, y: -4, x: 12, fill: cfg!.targetType === 'mvp' ? '#1677ff' : '#A0A0A0', cursor: 'pointer', }, }); } if (cfg!.children) { const { length } = cfg!.children as any; group!.addShape('rect', { name: 'sub-node-children-length', attrs: { width: 20, height: 20, radius: 10, y: -10, x: size[0] - 4, fill: '#404040', cursor: 'pointer', }, }); group!.addShape('text', { name: 'sub-node-children-length-text', attrs: { text: `${length}`, x: size[0] + 6 - G6.Util.getTextSize(`${length}`, 12)[0] / 2, textBaseline: 'middle', fill: '#fff', fontSize: 12, cursor: 'pointer', }, }); } if (cfg!.link) { group!.addShape('dom', { attrs: { width: 16, height: 16, x: size[0] - 12 - 16, y: -8, cursor: 'pointer', // DOM's html html: `
`, }, // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性 name: 'sub-node-link', }); } return keyShape; }, getAnchorPoints() { return [ [0, 0.5], [1, 0.5], ]; }, options: { stateStyles: { hover: { stroke: '#1677ff', 'sub-node-link': { html: `
`, }, }, }, }, }, 'rect', ); const dataTransform = (data: BehaviorMapItem) => { const changeData = (d: any, level = 0) => { const clonedData: any = { ...d, }; switch (level) { case 0: clonedData.type = 'behavior-start-node'; break; case 1: clonedData.type = 'behavior-sub-node'; clonedData.collapsed = true; break; default: clonedData.type = 'behavior-sub-node'; break; } if (d.children) { clonedData.children = d.children.map((child: any) => changeData(child, level + 1)); } return clonedData; }; return changeData(data); }; type BehaviorMapItem = { id: string; label: string; targetType?: 'mvp' | 'extension'; children?: BehaviorMapItem[]; link?: string; }; const useStyle = createStyles(() => ({ container: css` width: 100%; height: 600px; background-color: #f5f5f5; border: 1px solid #e8e8e8; border-radius: 8px; overflow: hidden; position: relative; `, title: css` position: absolute; top: 20px; left: 20px; font-size: 16px; `, tips: css` display: flex; position: absolute; bottom: 20px; right: 20px; `, mvp: css` margin-right: 20px; display: flex; align-items: center; &::before { display: block; width: 8px; height: 8px; margin-right: 8px; background-color: #1677ff; border-radius: 50%; content: ''; } `, extension: css` display: flex; align-items: center; &::before { display: block; width: 8px; height: 8px; margin-right: 8px; background-color: #a0a0a0; border-radius: 50%; content: ''; } `, })); export type BehaviorMapProps = { data: BehaviorMapItem; }; const BehaviorMap: React.FC = ({ data }) => { const ref = useRef(null); const { styles } = useStyle(); const meta = useRouteMeta(); useEffect(() => { const graph = new G6.TreeGraph({ container: ref.current!, width: ref.current!.scrollWidth, height: ref.current!.scrollHeight, renderer: 'svg', modes: { default: ['collapse-expand', 'drag-canvas'], }, defaultEdge: { type: 'cubic-horizontal', style: { lineWidth: 1, stroke: '#BFBFBF', }, }, layout: { type: 'mindmap', direction: 'LR', getHeight: () => 48, getWidth: (node: any) => G6.Util.getTextSize(node.label, 16)[0] + 20 * 2, getVGap: () => 10, getHGap: () => 60, getSide: (node: any) => node.data.direction, }, }); graph.on('node:mouseenter', (e) => { graph.setItemState(e.item!, 'hover', true); }); graph.on('node:mouseleave', (e) => { graph.setItemState(e.item!, 'hover', false); }); graph.on('node:click', (e) => { const { link } = e.item!.getModel(); if (link) { window.location.hash = link as string; } }); graph.data(dataTransform(data)); graph.render(); graph.fitCenter(); }, []); return (
{`${meta.frontmatter.title} 行为模式地图`}
MVP 行为目的
拓展行为目的
); }; export default BehaviorMap;