You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React组件中左右两侧盒子间连接线对齐问题排查

React组件连接线对齐异常修复方案

问题根源

  1. 选错目标元素:当前代码用document.querySelector选中的是带id的FormControlLabel(单选框容器),但实际要连线的是它旁边带灰色边框的Box盒子,导致坐标计算完全错误。
  2. SVG线段偏移:SVG的line元素设置strokeWidth后,线段会向两侧扩展,默认端点是居中的,视觉上会出现没对齐盒子边缘的情况。

修复步骤

1. 给目标Box添加唯一标识

修改OptionsBox组件,把标识移到实际要连线的Box上:

// OptionsBox内的Box部分
<Box
  data-id={`${idPrefix}${name.replace(/\s+/g, '-')}`}
  data-x-start={0}
  data-y-start={1}
  data-x-end={2}
  data-y-end={1}
  height={50}
  width={100}
  display="flex"
  alignItems="center"
  justifyContent="center"
  p={1}
  sx={{ border: '2px solid grey' }}
>
  {name}
</Box>

2. 修正坐标计算逻辑

修改handleConnectClick,选中正确的Box盒子,同时调整坐标抵消SVG线段的宽度偏移:

const handleConnectClick = () => {
  if (isConnectEnabled) {
    const leftBox = document.querySelector(`[data-id="left-${leftSelected.replace(/\s+/g, '-')}"]`);
    const rightBox = document.querySelector(`[data-id="right-${rightSelected.replace(/\s+/g, '-')}"]`);

    if (leftBox && rightBox) {
      const leftBoxRect = leftBox.getBoundingClientRect();
      const rightBoxRect = rightBox.getBoundingClientRect();
      const containerRect = document.querySelector('#connector-container').getBoundingClientRect();

      // 抵消strokeWidth的一半,让端点精准贴合盒子边缘
      const x1 = leftBoxRect.right - containerRect.left - 1;
      const y1 = leftBoxRect.top + leftBoxRect.height / 2 - containerRect.top;
      const x2 = rightBoxRect.left - containerRect.left + 1;
      const y2 = rightBoxRect.top + rightBoxRect.height / 2 - containerRect.top;

      setConnectorPosition({ x1, y1, x2, y2 });
      setShowConnector(true);
    }
  }
};

3. 优化SVG线段渲染(可选)

给SVG的line元素添加属性,避免缩放时线段宽度变化:

const ConnectorLine = React.forwardRef(({ x1, y1, x2, y2 }, ref) => {
  return (
    <svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}>
      <line 
        ref={ref} 
        x1={x1} 
        y1={y1} 
        x2={x2} 
        y2={y2} 
        style={{ 
          stroke: 'black', 
          strokeWidth: 2,
          vectorEffect: 'non-scaling-stroke'
        }} 
      />
    </svg>
  );
});

完整修改后的代码

import React, { useState, useLayoutEffect, useRef } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Radio from '@mui/material/Radio';
import FormControlLabel from '@mui/material/FormControlLabel';
import RadioGroup from '@mui/material/RadioGroup';
import Button from '@mui/material/Button';

const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2'
    }
  }
});

const UI_OPTIONS_LEFT = [ 'Protopie', 'Unity', 'QT', 'Figma' ];
const UI_OPTIONS_RIGHT = [ 'VRED', 'Unreal Engine' ];

export default function Flowchart() {
  const [ leftSelected, setLeftSelected ] = useState('');
  const [ rightSelected, setRightSelected ] = useState('');
  const [ isConnectEnabled, setIsConnectEnabled ] = useState(false);
  const [ showConnector, setShowConnector ] = useState(false);
  const [ connectorPosition, setConnectorPosition ] = useState({ x1: 0, y1: 0, x2: 0, y2: 0 });
  const connectorRef = useRef(null);

  useLayoutEffect(
    () => {
      setIsConnectEnabled(!!leftSelected && !!rightSelected);
    },
    [ leftSelected, rightSelected ]
  );

  const handleLeftChange = (event) => {
    setLeftSelected(event.target.value);
  };

  const handleRightChange = (event) => {
    setRightSelected(event.target.value);
  };

  const handleConnectClick = () => {
    if (isConnectEnabled) {
      const leftBox = document.querySelector(`[data-id="left-${leftSelected.replace(/\s+/g, '-')}"]`);
      const rightBox = document.querySelector(`[data-id="right-${rightSelected.replace(/\s+/g, '-')}"]`);

      if (leftBox && rightBox) {
        const leftBoxRect = leftBox.getBoundingClientRect();
        const rightBoxRect = rightBox.getBoundingClientRect();
        const containerRect = document.querySelector('#connector-container').getBoundingClientRect();

        const x1 = leftBoxRect.right - containerRect.left - 1;
        const y1 = leftBoxRect.top + leftBoxRect.height / 2 - containerRect.top;
        const x2 = rightBoxRect.left - containerRect.left + 1;
        const y2 = rightBoxRect.top + rightBoxRect.height / 2 - containerRect.top;

        setConnectorPosition({ x1, y1, x2, y2 });
        setShowConnector(true);
      }
    }
  };

  return (
    <ThemeProvider theme={theme}>
      <Box display="flex" flexDirection="column" alignItems="center" width="100%" position="relative">
        <Typography variant="h5" mb={4}>
          Select UI Flow
        </Typography>
        <Box id="connector-container" display="flex" flex={1} mb={4} position="relative" width="100%">
          <OptionsBox
            options={UI_OPTIONS_LEFT}
            selected={leftSelected}
            onChange={handleLeftChange}
            idPrefix="left-"
          />
          <OptionsBox
            options={UI_OPTIONS_RIGHT}
            selected={rightSelected}
            onChange={handleRightChange}
            idPrefix="right-"
          />
          {showConnector && (
            <ConnectorLine
              ref={connectorRef}
              x1={connectorPosition.x1}
              y1={connectorPosition.y1}
              x2={connectorPosition.x2}
              y2={connectorPosition.y2}
            />
          )}
        </Box>
        <Button variant="contained" color="primary" onClick={handleConnectClick} disabled={!isConnectEnabled}>
          Connect
        </Button>
      </Box>
    </ThemeProvider>
  );
}


function OptionsBox({ options, selected, onChange, idPrefix, alignRight }) {
  return (
    <Box display="flex" flexDirection="column" gap={4} flex={1}>
      <RadioGroup value={selected} onChange={onChange}>
        {options.map((name) => (
          <Box key={name} display="flex" alignItems="center" mb={2}>
            <FormControlLabel
              value={name}
              control={<Radio />}
              label=""
              sx={{ marginRight: 2 }}
              style={{ alignItems: alignRight ? 'flex-end' : 'flex-start' }}
            />
            <Box
              data-id={`${idPrefix}${name.replace(/\s+/g, '-')}`}
              data-x-start={0}
              data-y-start={1}
              data-x-end={2}
              data-y-end={1}
              height={50}
              width={100}
              display="flex"
              alignItems="center"
              justifyContent="center"
              p={1}
              sx={{ border: '2px solid grey' }}
            >
              {name}
            </Box>
          </Box>
        ))}
      </RadioGroup>
    </Box>
  );
}

const ConnectorLine = React.forwardRef(({ x1, y1, x2, y2 }, ref) => {
  return (
    <svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}>
      <line 
        ref={ref} 
        x1={x1} 
        y1={y1} 
        x2={x2} 
        y2={y2} 
        style={{ 
          stroke: 'black', 
          strokeWidth: 2,
          vectorEffect: 'non-scaling-stroke'
        }} 
      />
    </svg>
  );
});

内容的提问来源于stack exchange,提问作者kannan srank

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 17:15:57