连线题实现的几种方式

在这里插入图片描述

基础布局:

左侧:左侧红框
右侧:右侧红框
画布:绿色框

<div class='root'>
    <div class='left'></div> // 左侧
    <div class='right'></div> // 右侧
    <svg class='canvas-box'></svg> // 画布(根据不同方案的选择不同的dom元素)
</div>

划线核心逻辑

确定开始坐标位置
确定结束坐标位置
连线
获取元素坐标

getPoint方法(通过自定义position位置,设置连线起始点和结束点的位置)
const getPoint = (args) => {
        const { startDom, rootDom = containerRef.value, position = 'right' } = args || {};
        const { left: x1, top: y1 } = getElementPositionRelativeToDocument(rootDom);
        const { left: x2, top: y2, width, height } = getElementPositionRelativeToDocument(startDom);
        let startX = x2 - x1;
        let startY = y2 - y1;
        if (position === 'bottom') {
            startX = startX + width / 2;
            startY = startY + height + lineSpacingRef.value;
        }
        if (['rt', 'tr'].includes(position)) {
            startX = startX + width + lineSpacingRef.value;
            startY = startY + lineSpacingRef.value;
        }
        if (position === 'right') {
            startX = startX + width + lineSpacingRef.value;
            startY = startY + height / 2;
        }
        if (['rb', 'br'].includes(position)) {
            startX = startX + width + lineSpacingRef.value;
            startY = startY + height + lineSpacingRef.value;
        }
        if (['lt', 'tl'].includes(position)) {
            startX = startX - lineSpacingRef.value;
            startY = startY + lineSpacingRef.value;
        }
        if (position === 'left') {
            startX = startX - lineSpacingRef.value;
            startY = startY + height / 2;
        }
        if (['lb', 'bl'].includes(position)) {
            startX = startX - lineSpacingRef.value;
            startY = startY + height + lineSpacingRef.value;
        }
        if (position === 'top') {
            startX = startX + width / 2;
            startY = startY - lineSpacingRef.value;
        }
        return { startX, startY };
};
getElementPositionRelativeToDocument方法
/**
 * 获取目标输入参数元素所在位置
 * @param element
 */
const getElementPositionRelativeToDocument = (element) => {
    if (!element) return { left: 0, top: 0 };
    const rect = element.getBoundingClientRect();
    
    // 获取视口滚动位置
    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    
    // 计算元素相对于文档的位置
    const leftPosition = rect.left + scrollLeft;
    const topPosition = rect.top + scrollTop;
   
    
    return { left: leftPosition, top: topPosition, width: rect.width, height: rect.height };
}

标题方案一、svg

设置svg的开始和结束位置坐标进行划线

<svg ref="svgRef" class="svg-box">
    <g
        v-for="(link, index) in links"
        :key="index"
        @mouseenter="isHovered = index"
        @mouseleave="isHovered = null"
        @click.prevent="handleDelSvgPath(link, index)"
    >
        <defs>
            <marker :key="isHovered" :id="`arrow${index}`" v-bind="getMarkerInfo(index).marker" orient="auto-start-reverse">
                <path :d="getMarkerInfo(index).d" :style="{ fill: 'none', stroke: arrowFillMap[link.type], strokeWidth: 1 }"></path>
            </marker>
        </defs>
        <line
            class="svg-line"
            v-bind="getSvgInfo(link)"
            :stroke="arrowFillMap[link.type]"
            stroke-width="1"
            :marker-end="`url(#arrow${index})`"
            :stroke-dasharray="link.type === reverseCode ? '3, 3' : '0, 0'"
        ></line>
        <!-- 用于增加热区的透明路径 -->
        <line v-bind="getSvgInfo(link)" stroke="transparent" stroke-width="10" fill="none"></line>
    </g>
</svg>
const getSvgInfo = (link) => {
    const svgBox = svgRef.value;
    const fromIndex = inputParamList.value?.findIndex(item => item.value === link.from);
    const toIndex = outputParamList.value?.findIndex(item => item.value === link.to);

   // 连线开始元素-startBlockRef.value[fromIndex]  连线结束元素-endBlockRef.value[toIndex]

    const { startX: x2, startY: y2 } = getPoint({ startDom: endBlockRef.value[toIndex], position: 'left' });
    const { startX: x1, startY: y1 } = getPoint({ startDom: startBlockRef.value[fromIndex], position: 'right' });
    if (!svgBox || fromIndex === -1 || toIndex === -1) return { x1: 0, x2: 0, y1: 0, y2: 0 };
    return { x1, x2, y1, y2 };
};

const getMarkerInfo = (index) => {
        if (isHovered.value === index) return {
            marker: {
                markerWidth: 4,
                markerHeight: 4,
                refX: 2,
                refY: 2
            },
            d: "M0,0 L2,2 L0,4"
        }
        return {
            marker: {
                markerWidth: 8,
                markerHeight: 8,
                refX: 4,
                refY: 4
            },
            d: "M0,0 L4,4 L0,8"
        }
    };
确认开始节点
const handleConnectType = (value, row, index) => {
        const id = row.value;
        connectType.value = value;
        popupVisibleMap.value[id] = false;
        currentSelectedId.value = id;
        svgCanvasListener();
        const { startX, startY } = getPoint({ startDom: startBlockRef.value[index], position: row.position });
        drawLine(startX, startY);
};
开始划线
let line = null;
const drawLine = (startX, startY) => {
        line = createLine(startX, startY, startX, startY);
        svgRef.value.appendChild(line);
};
划线createLine
const createLine = (x1, y1, x2, y2) => {
        const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line.setAttribute('x1', x1);
        line.setAttribute('y1', y1);
        line.setAttribute('x2', x2);
        line.setAttribute('y2', y2);
        line.setAttribute('stroke', arrowFillMap[connectType.value]);
        line.setAttribute('stroke-width', '1');
        if(connectType.value === reverseCode) line.setAttribute('stroke-dasharray', '3, 3'); // 设置虚线
        if (showArrow) line.setAttribute('marker-end', 'url(#arrow)');
};
鼠标移动,跟随鼠标移动划线
const mousemove = (e) => {
        if (!line) return;
        const { left: targetPageX, top: targetPageY } = getElementPositionRelativeToDocument(svgRef.value);
        const toX = e.pageX - targetPageX - lineSpacingRef.value;
        const toY = e.pageY - targetPageY - lineSpacingRef.value;
        line.setAttribute('x2', toX);
        line.setAttribute('y2', toY);
};
确认结束元素
const handleEndPoint = (row) => {
  // 数据处理
   upData(); // 更新数据
  if (line) svgRef.value.removeChild(line);
  line = null;
}

标题方案二、canvas

设置canvas的开始和结束位置坐标进行划线

<canvas
    ref="svgCanvasRef"
    class="svg-canvas-container"
    :width="root.offsetWidth"
    :height="root.offsetHeight"
></canvas>
canvas划线
/**
     * canvas画布划线
     * @param canvasDom
     * @param fromX
     * @param fromY
     * @param toX
     * @param toY
     */
    const drawCanvasLine = (ctx, fromX, fromY, toX, toY) => {
        if (!svgCanvasRef.value) return;
       
        const { arrowEnd1X, arrowEnd1Y, arrowEnd2X, arrowEnd2Y } = setArrowPath(fromX, fromY, toX, toY);
        
        ctx.lineWidth = 1;
        ctx.strokeStyle = arrowFillMap[connectType.value];
        
        ctx.beginPath();
        if (connectType.value === reverseCode) ctx.setLineDash([3, 3])
        ctx.moveTo(fromX, fromY);
        ctx.lineTo(toX, toY);
        ctx.stroke();
        
        if (!showArrow) return;
        // 箭头
        ctx.beginPath();
        ctx.moveTo(toX, toY);
        ctx.lineTo(arrowEnd1X, arrowEnd1Y);
        ctx.moveTo(toX, toY);
        ctx.lineTo(arrowEnd2X, arrowEnd2Y);
        ctx.stroke();
    }
/**
 * 设置箭头
 * @param startX
 * @param startY
 * @param endX
 * @param endY
 */
const setArrowPath = (startX, startY, endX, endY) => {
    // 箭头头部的大小和角度
    const arrowLength = 5; // 箭头长度
    const arrowAngle = 4; // 箭头角度
    
    // 计算箭头头部的两个点
    const angle = Math.atan2(endY - startY, endX - startX);
    const arrowEnd1X = endX - arrowLength * Math.cos(angle - Math.PI / arrowAngle);
    const arrowEnd1Y = endY - arrowLength * Math.sin(angle - Math.PI / arrowAngle);
    const arrowEnd2X = endX - arrowLength * Math.cos(angle + Math.PI / arrowAngle);
    const arrowEnd2Y = endY - arrowLength * Math.sin(angle + Math.PI / arrowAngle);
    return { arrowEnd1X, arrowEnd1Y, arrowEnd2X, arrowEnd2Y };
};
鼠标移动,跟随鼠标移动划线
const mousemove = (e) => {
    const { left: targetPageX, top: targetPageY } = getElementPositionRelativeToDocument(svgRef.value);
    const toX = e.pageX - targetPageX - lineSpacingRef.value;
    const toY = e.pageY - targetPageY - lineSpacingRef.value;
    const canvas = svgRef.value.getContext('2d');
    const width = svgRef.value.width;
    const height = svgRef.value.height;
    const ctx = canvas.clearRect(0, 0, width, height) || svgRef.value.getContext('2d');
    drawCanvasLine(ctx, startX1, startY1, toX, toY)
    links.value.forEach(item => {
        const {x1, y1, x2, y2} = getSvgInfo(item);
        drawCanvasLine(ctx, x1, y1, x2, y2);
    })        
};
确认数据,数据更新
const updateData = (links) => {
    $emit('update:modelValue', getAnswerData(), links);
    $emit('onAnswerChange', getAnswerData())
    const canvas = svgRef.value.getContext('2d');
    const width = svgRef.value.width;
    const height = svgRef.value.height;
    const ctx = canvas.clearRect(0, 0, width, height) || svgRef.value.getContext('2d')
    links.forEach(item => {
        const {x1, y1, x2, y2} = getSvgInfo(item);
        drawCanvasLine(ctx, x1, y1, x2, y2);
    })
};

标题方案三、svg(最终连线)+canvas(鼠标移动连线画布)

设置svg和canvas的开始和结束位置坐标进行划线

<svg v-if="draw" ref="svgRef" class="svg-box">
    <g
        v-for="(link, index) in links"
        :key="index"
        @mouseenter="isHovered = index"
        @mouseleave="isHovered = null"
        @click.prevent="handleDelSvgPath(link, index)"
    >
        <defs>
            <marker :key="isHovered" :id="`arrow${index}`" v-bind="getMarkerInfo(index).marker" orient="auto-start-reverse">
                <path :d="getMarkerInfo(index).d" :style="{ fill: 'none', stroke: arrowFillMap[link.type], strokeWidth: 1 }"></path>
            </marker>
        </defs>
        <line
            class="svg-line"
            v-bind="getSvgInfo(link)"
            :stroke="arrowFillMap[link.type]"
            stroke-width="1"
            :marker-end="`url(#arrow${index})`"
            :stroke-dasharray="link.type === reverseCode ? '3, 3' : '0, 0'"
        ></line>
        <!-- 用于增加热区的透明路径 -->
        <line v-bind="getSvgInfo(link)" stroke="transparent" stroke-width="10" fill="none"></line>
    </g>
</svg>
<canvas
    v-if="showSvgCanvas"
    ref="svgCanvasRef"
    :key="canvasKey"
    class="svg-canvas-container"
    :width="containerRef.offsetWidth"
    :height="containerRef.offsetHeight"
></canvas>
getSvgInfo、getMarkerInfo、获取开始位置、结束为止与方案一均一致
鼠标移动,跟随鼠标移动划线
const mousemove = (e) => {
        if (!svgCanvasRef.value) return;
        const { left: targetPageX, top: targetPageY } = getElementPositionRelativeToDocument(svgCanvasRef.value);
        const { startX, startY } = startConnectLineDomPoint.value;
        const toX = e.pageX - targetPageX;
        const toY = (e.pageY - targetPageY);
        drawCanvasLine(startX, startY, toX, toY);
    };
/**
     * canvas画布划线
     * @param canvasDom
     * @param fromX
     * @param fromY
     * @param toX
     * @param toY
     */
    const drawCanvasLine = (fromX, fromY, toX, toY) => {
        if (!svgCanvasRef.value) return;
        const canvas = svgCanvasRef.value.getContext('2d');
        const width = svgCanvasRef.value.width;
        const height = svgCanvasRef.value.height;
        const ctx = canvas.clearRect(0, 0, width, height) || svgCanvasRef.value.getContext('2d');
        const { arrowEnd1X, arrowEnd1Y, arrowEnd2X, arrowEnd2Y } = setArrowPath(fromX, fromY, toX, toY);
        
        ctx.lineWidth = 1;
        ctx.strokeStyle = arrowFillMap[connectType.value];
        
        ctx.beginPath();
        if (connectType.value === reverseCode) ctx.setLineDash([3, 3])
        ctx.moveTo(fromX, fromY);
        ctx.lineTo(toX, toY);
        ctx.stroke();
        
        if (!showArrow) return;
        // 箭头
        ctx.beginPath();
        ctx.moveTo(toX, toY);
        ctx.lineTo(arrowEnd1X, arrowEnd1Y);
        ctx.moveTo(toX, toY);
        ctx.lineTo(arrowEnd2X, arrowEnd2Y);
        ctx.stroke();
    }
/**
 * 设置箭头
 * @param startX
 * @param startY
 * @param endX
 * @param endY
 */
const setArrowPath = (startX, startY, endX, endY) => {
    // 箭头头部的大小和角度
    const arrowLength = 5; // 箭头长度
    const arrowAngle = 4; // 箭头角度
    
    // 计算箭头头部的两个点
    const angle = Math.atan2(endY - startY, endX - startX);
    const arrowEnd1X = endX - arrowLength * Math.cos(angle - Math.PI / arrowAngle);
    const arrowEnd1Y = endY - arrowLength * Math.sin(angle - Math.PI / arrowAngle);
    const arrowEnd2X = endX - arrowLength * Math.cos(angle + Math.PI / arrowAngle);
    const arrowEnd2Y = endY - arrowLength * Math.sin(angle + Math.PI / arrowAngle);
    return { arrowEnd1X, arrowEnd1Y, arrowEnd2X, arrowEnd2Y };
};

标题方案四、css

通过设置两点之间的连线长度以及设置div元素的旋转角度进行划线

<div v-if="draw" ref="svgRef" class="svg-box">
    <div v-for="(link, index) in links" class="line" :style="{
        width: `${calculateDistance(getSvgInfo(link))}px`,
        transform: `translateX(0) translateY(${getSvgInfo(link).y1}px) rotate(${calculateAngleDegrees(getSvgInfo(link))}deg)`
    }">
</div>
.svg-box {
    flex: 1;
    position: absolute;
    left: 0;
    top: 0;
    pointer-events: none;
    .line {
        width: 300px; /* 线条的长度 */
        height: 0; /* 线条的宽度 */
        border-top: 1px solid red;
        &:after {
            content: '>';
            position: absolute;
            top: -12px;
            right: 6px;
            width: 0;
            height: 0;
            color: red;
        }
    }
}
getSvgInfo、getMarkerInfo、获取开始位置、结束为止与方案一均一致
calculateAngleDegrees方法(根据终点坐标计算角度)
const calculateAngleDegrees = ({x1, y1, x2, y2}) => {
    // 检查分母是否为0(即x1是否等于x2)
    if (x1 === x2) {
        // 如果x1等于x2,则线条是垂直的
        // 这里我们假设y2 > y1表示向上,y2 < y1表示向下
        return y2 > y1 ? 90 : -90; // 或者返回270而不是-90
    }

    // 计算斜率
    let slope = (y2 - y1) / (x2 - x1);

    // 使用Math.atan()计算角度(以弧度为单位)
    let angleRadians = Math.atan(slope);

    // 将弧度转换为度
    let angleDegrees = angleRadians * (180 / Math.PI);

    // 如果线条是从右向左倾斜的(x2 < x1),并且你想要一个正的角度值
    // (尽管CSS的rotate函数已经是以逆时针方向为正方向的)
   
    // if (x2 < x1) angleDegrees += 180;

    // 返回角度(以度为单位)
    return angleDegrees;
};
calculateDistance方法(获取两点之间的距离)
const calculateDistance = ({ x1, y1, x2, y2 }) => {
    // 计算两点在x轴和y轴上的距离差
    let dx = x2 - x1;
    let dy = y2 - y1;

    // 应用勾股定理计算斜线的长度
    let distance = Math.sqrt(dx * dx + dy * dy);
    return distance;
}
划线
let line = null;
const drawLine = (startX, startY) => {
    line = document.createElement("div");
    line.className = "line";
    svgRef.value.appendChild(line);
};
鼠标移动划线
const mousemove = (e) => {
    if (!line) return;
    const { left: targetPageX, top: targetPageY } = getElementPositionRelativeToDocument(svgRef.value);
    const toX = e.pageX - targetPageX - lineSpacingRef.value;
    const toY = e.pageY - targetPageY - lineSpacingRef.value;
    line.style.width = `${calculateDistance({x1, y1, x2: toX, y2: toY})}px`;
    line.style.borderTop = '1px solid red';
    line.style.transformOrigin = '0 0';
    line.style.transform = `translateX(0) translateY(${y1}px) rotate(${calculateAngleDegrees({x1, y1, x2: toX, y2: toY})}deg)`;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值