如何在两个组件之间画线

How to draw line between 2 components

我有一些组件(语义-ui-react 中的分段组件),我正在尝试创建一个树状组件,它将由下图中给出的线组成。

我发现了一个名为 react-lineto 的库,但我无法获得我需要的东西。这是我试过的代码。

import React, { Component } from "react";
import { SteppedLineTo } from "react-lineto";
import { Segment, Grid, GridColumn, GridRow } from "semantic-ui-react";

const style = {
  delay: true,
  borderColor: "#ddd",
  borderStyle: "solid",
  borderWidth: 3
};

class App extends Component {
  render() {
    return (
      <Grid>
        <GridRow>
          <GridColumn width={1} />
          <GridColumn width={14}>
            <Segment raised compact className="A">
              Comapny A
            </Segment>
            <Segment raised compact className="B" style={{ margin: "20px" }}>
            Comapny B
            </Segment>
            <Segment raised compact className="C" style={{ margin: "40px" }}>
            Comapny C
            </Segment>
            <Segment raised compact className="D" style={{ margin: "20px" }}>
            Comapny D
            </Segment>
            <Segment raised compact className="E" style={{ margin: "0px" }}>
              Company E
            </Segment>
            <SteppedLineTo
              from="A"
              to="B"
              fromAnchor="left"
              toAnchor="0 50%"
              orientation="h"
              {...style}
            />
            <SteppedLineTo
              from="A"
              to="C"
              fromAnchor="left"
              toAnchor="0 50%"
              orientation="h"
              {...style}
            />
          </GridColumn>
          <GridColumn width={1} />
        </GridRow>
      </Grid>
    );
  }
}

export default App;

这呈现了这样的东西

我怎样才能做到这一点?除了使用这个库还有其他选择吗?也许是一个普通的 css 把戏?

很酷的挑战!

这是使用纯 React & CSS 的方法。此解决方案仅适用于没有循环的树,这似乎是您的用例。

我们的想法是从一棵树开始,用渲染框和 link 所需的所有信息丰富和展平节点,然后我们通过 CSS 和 [=12] 放置这些信息=].

希望对您有所帮助。

这里是最终的输出,框高20px,框间距10px,link偏移5px(框边和框边之间的space附上 link).

<div style="position: relative;"><div style="position: absolute; left: 0px; top: 0px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; top: 20px; left: 4px; height: 105px; border-left: 1px solid grey;"></div>Parent</div><div style="position: absolute; left: 10px; top: 30px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div><div style="position: absolute; top: 20px; left: 4px; height: 15px; border-left: 1px solid grey;"></div>Child 1</div><div style="position: absolute; left: 20px; top: 60px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Grandchild 1</div><div style="position: absolute; left: 10px; top: 90px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Child 2</div><div style="position: absolute; left: 10px; top: 120px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div><div style="position: absolute; top: 20px; left: 4px; height: 45px; border-left: 1px solid grey;"></div>Child 3</div><div style="position: absolute; left: 20px; top: 150px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Grandchild 2</div><div style="position: absolute; left: 20px; top: 180px; height: 20px; border: 1px solid grey; border-radius: 2px; padding: 10px; box-sizing: border-box; display: flex; align-items: center;"><div style="position: absolute; left: -5px; top: 4px; width: 5px; border-top: 1px solid grey;"></div>Grandchild 3</div></div>

下面的代码

import React from 'react'

// Example:
//
// const topLevelNode = {
//   text: 'Parent',
//   children: [
//     {
//       text: 'Child 1',
//       children: [
//         {
//           text: 'Grandchild 1',
//         },
//       ],
//     },
//     {
//       text: 'Child 2',
//     },
//   ],
// }
//
// flatten(enrich(topLevelNode))
//
// [
//   { text: 'Parent', depth: 0, descendentsCount: 3, heightDiffWithLastDirectChild: 3 },
//   { text: 'Child 1', depth: 1, descendentsCount: 1, heightDiffWithLastDirectChild: 1 },
//   { text: 'Grandchild 1', depth: 2, descendentsCount: 0, heightDiffWithLastDirectChild: 0 },
//   { text: 'Child 2', depth: 1, descendentsCount: 0, heightDiffWithLastDirectChild: 0 },
// ]

// Enrich nodes with information needed for render
const enrich = (node, depthOffset = 0) => {
  if (!node.children) {
    return {
      ...node,
      depth: depthOffset,
      descendentsCount: 0,
      heightDiffWithLastDirectChild: 0,
    }
  }

  const enrichedChildren = node.children.map((child) => enrich(child, depthOffset + 1))
  const descendentsCount = node.children.length + enrichedChildren.reduce(
    (acc, enrichedChild) => acc + enrichedChild.descendentsCount,
    0,
  )

  const heightDiffWithLastDirectChild = descendentsCount - enrichedChildren[node.children.length - 1].descendentsCount
  return {
    ...node,
    children: enrichedChildren,
    depth: depthOffset,
    descendentsCount,
    heightDiffWithLastDirectChild,
  }
}

// Flatten nodes with a depth first search
const flatten = (node) => {
  const { children = [], ...nodeWithoutChildren } = node
  return [
    { ...nodeWithoutChildren },
    ...children.map((childNode) => flatten(childNode)).flat(),
  ]
}

const boxHeight = 20
const boxGap = 10
const linkPositionOffset = 5

const LinkedBox = ({ node, order }) => (
  <div
    style={{
      position: 'absolute',
      left: `${node.depth * boxGap}px`,
      top: `${order * (boxHeight + boxGap)}px`,
      height: `${boxHeight}px`,
      border: '1px solid grey',
      borderRadius: '2px',
      padding: '10px',
      boxSizing: 'border-box',
      display: 'flex',
      alignItems: 'center',
    }}
  >
    {node.depth > 0 && (
      <div
        style={{
          position: 'absolute',
          left: `-${boxGap - linkPositionOffset}px`,
          top: `${linkPositionOffset - 1}px`,
          width: `${boxGap - linkPositionOffset}px`,
          borderTop: 'solid 1px grey',
        }}
      />
    )}

    {node.heightDiffWithLastDirectChild > 0 && (
      <div
        style={{
          position: 'absolute',
          top: `${boxHeight}px`,
          left: `${linkPositionOffset - 1}px`,
          height: `${boxGap + (node.heightDiffWithLastDirectChild - 1) * (boxGap + boxHeight) + linkPositionOffset}px`,
          borderLeft: 'solid 1px grey',
        }}
      />
    )}
    {node.text}
  </div>
)

const Diagram = ({ topLevelNode }) => (
  <div style={{ position: 'relative' }}>
    {flatten(enrich(topLevelNode)).map((enrichedNode, order) => (
      <LinkedBox node={enrichedNode} order={order} key={JSON.stringify(enrichedNode)} />
    ))}
  </div>
)

export default () => (
  <Diagram
    topLevelNode={{
      text: 'Parent',
      children: [
        {
          text: 'Child 1',
          children: [
            { text: 'Grandchild 1' },
          ],
        },
        { text: 'Child 2' },
        {
          text: 'Child 3',
          children: [
            { text: 'Grandchild 2' },
            { text: 'Grandchild 3' },
          ],
        },
      ],
    }}
  />
)

你可以使用 react-xarrows

import React, {useRef} from "react";
import Xarrow from "react-xarrows";

const boxStyle = {
  border: '1px #999 solid',
  borderRadius: '10px',
  textAlign: 'center',
  width: '100px',
  height: '30px',
  color: 'black',
  alignItems: 'center',
  display: 'flex',
  justifyContent: 'center',
};


export const Test = () => {
  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        width: '500px',
        position: 'absolute',
      }}>
      <div id="elem1" style={boxStyle}>
        elem1
      </div>
      <div id="elem2" style={{ ...boxStyle, position: 'relative', left: '100px' }}>
        elem2
      </div>
      <div id="elem3" style={{ ...boxStyle, position: 'relative', left: '100px' }}>
        elem2
      </div>
      <Xarrow start="elem1" end="elem2" showHead={false} startAnchor={'bottom'} path={'grid'} />
      <Xarrow start="elem1" end="elem3" showHead={false} startAnchor={'bottom'} path={'grid'} />
    </div>
  );
};

会给你结果

如果您希望线路从左侧连接,您可以使用自定义偏移它 startAnchor 属性:

export const Test = () => {
  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        width: '500px',
        position: 'absolute',
      }}>
      <div id="elem1" style={boxStyle}>
        elem1
      </div>
      <div id="elem2" style={{ ...boxStyle, position: 'relative', left: '100px' }}>
        elem2
      </div>
      <div id="elem3" style={{ ...boxStyle, position: 'relative', left: '100px' }}>
        elem2
      </div>
      <Xarrow
        start="elem1"
        end="elem2"
        showHead={false}
        startAnchor={{ position: 'bottom', offset: { rightness: -30 } }}
        path={'grid'}
      />
      <Xarrow
        start="elem1"
        end="elem3"
        showHead={false}
        startAnchor={{ position: 'bottom', offset: { rightness: -30 } }}
        path={'grid'}
      />
    </div>
  );
};

会给你结果: