React组件中左右两侧盒子间连接线对齐问题排查
React组件连接线对齐异常修复方案
问题根源
- 选错目标元素:当前代码用
document.querySelector选中的是带id的FormControlLabel(单选框容器),但实际要连线的是它旁边带灰色边框的Box盒子,导致坐标计算完全错误。 - 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
相关产品推荐
相关产品推荐

