思维导图前端实现

目的:

借助思维导图的互动操作,让⽤户更深⼊地参与到教学内容中。

要求:第二个层级的所有节点都要在第一个节点及其所有子节点之后。

调研

svg动画

展开动画

动画效果:

xml 复制代码
<style>
    .curve {
      stroke: black;
      stroke-width: 2;
      fill: none;
      stroke-dasharray: 0, 1000; /* 初始值设置为 0 */
      animation: draw-curve 3s forwards; /* 使用 CSS 动画 */
    }
    @keyframes draw-curve {
      to {
        stroke-dasharray: 1000, 0; /* 动画结束时显示完整的路径 */
      }
    }
</style>
<svg width="600" height="400">
    <!-- 定义一个曲线路径 -->
    <path d="M0 395.3793C89.425 395.3793 89.425 133.8207 178.85 133.8207" class="curve" />
    <!-- <path d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" class="curve" /> -->
</svg>

移动动画

动画效果: 这两个动画能够实现导图从根节点一层一层展开的动画效果。注:该动画无法在 ios 设备下播放,需要使用svg中的标签支持

xml 复制代码
 <style>
      path {
        fill: none;
        stroke: blue;
        stroke-width: 2;
        /* 添加过渡效果,过渡时间为 1 秒 */
        transition: d 1s ease-in-out;
      }
      /* 定义一个类,用于改变 path 的 d 属性 */
      .changed {
        d: path("M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80");
      }
</style>
<svg width="200" height="200">
      <path d="M10 10 C 20 10, 30 20, 40 40 S 60 60, 80 80" />
</svg>
<button onclick="changePath()">Change Path</button>
<script>
      function changePath() {
        const path = document.querySelector("path");
        // 添加 changed 类,触发过渡效果
        path.classList.add("changed");
      }
</script>

echarts 中的 tree 布局

实现效果:

底层使用 d3.js 计算每个节点的位置,但是节点的渲染比较固定没办法进行扩展。

d3.js 中的tree布局

实现效果:

d3更加紧凑的布局,每个节点也是用svg元素实现的,很难控制拓展性,第一级的最后一个节点与第二级所有孩子的第一个节点的位置固定,无法特殊设置。

深度优先算法计算节点位置

算法思想:使用二叉树的深度优先算法,先计算第一级最深的一个自节点的所有位置,在遍历到最后第一集最后一个节点的位置后,把最后一级节点的y位置返回,之后以这个Y位置为基础遍历下一级的所有子节点位置,如果一级的孩子都便利完成则把该节点的Y设置为孩子节点的Y位置中间并把当前节点的Y位置设置为所有其孩子的最深的Y位置,最终可以拿到每个子节点的(x,y)位置,通过 transform 属性定位节点位置,然后每个节点间连线使用svg元素连线。

直线: 三次贝塞尔曲线:

ini 复制代码
const dfs = (obj, parent, depth, start) => {
              const node = {
                name: obj.name,
                data: obj,
                x: start + x,
                y: depth * y,
                depth: depth,
                parent: parent,
                endX: 0,
                children: obj.children,
              };
              if (!obj.children) {
                node.endX = node.x;
                return node;
              }
              let before = null;
              node.children = obj.children.map((child, index) => {
                const newNode = dfs(
                  child,
                  node,
                  depth + 1,
                  before ? before?.endX || start : start
                );
                before = newNode;
                return newNode;
              });
              node.x = getChildMiddleHeight(node);
              node.endX = node.children.at(-1).endX;
              return node;
};

项目中使用

基本使用

采用 dfs 方式来设计思维导图,实际的节点使用dom来实现,每个节点的连线使用 svg path 来连接

导图展开与收起

  • 展开的时候使用 animate 标签来处理,要求每级都是紧凑的布局,如果节点有下一级孩子,那么导图会整体放大
  • 收起时 使用 animation: hides 1s linear forwards;
  • 控制同一层级的所有连线同时开始,并同时连接完成,需要计算一个线条的长度,然后控制动画运动的长度

扣题&答题

  • 扣题答题的节点要是使用svg实现,扩展性很差,这里需要用dom节点来替代。
  • 一个节点有题还有正常的展示,并且需要保持答题的弹窗不能覆盖扣题节点,需要在点击节点的时候为容器增加问题弹窗的宽度,并把扣题节点移动到中间位置。
  • 处理答题的方块飞入到扣题位置

从扣题导图去往大导图

  • 在答题完成,会让思维导图每一级都收到跟节点,然后飞入到大导图的位置。
  • 大导图是一个直接展开完全的导图。

从大导图前往细节图

  • 在课节的大导图每个最后一级的节点都能去小导图的详情页,这里导图都是要完全展开。

缩/放问题

  • 思维导图要求支持缩/放功能,并且在动画的时候禁用。
  • 放大,在放大的时候导图会有部分被隐藏,因此最大为导图渲染完毕的场景。
  • 缩小,缩小的时候要求能正常滚动,故而需要一个占位dom来撑起滚动。
ini 复制代码
import React, { useEffect, useRef } from 'react';

/**
 * 控制页面缩放的 hooks 目前如果放大会导致页面内容被隐藏,先处理了缩小的操作
 */
const useScale = ({
  min = 1,
  max = 1,
  beforeCb,
  handleMove,
}: {
  min?: number;
  max?: number;
  beforeCb?: () => boolean;
  handleMove?: (val: number) => void;
}) => {
  const initialDistance = useRef<number>(0);
  const scaleRef = useRef<number>(1);
  const scaleDom = useRef<HTMLDivElement>(null);
  const scaleDirection = useRef(0);

  const hasBeforeFn = () => {
    if (beforeCb) {
      return beforeCb();
    }
    return true;
  };

  const handleTouchStart = (e: TouchEvent) => {
    if (e.touches.length === 2) {
      if (hasBeforeFn()) {
        scaleDirection.current = 0;
        const x1 = e.touches[0].clientX;
        const y1 = e.touches[0].clientY;
        const x2 = e.touches[1].clientX;
        const y2 = e.touches[1].clientY;
        initialDistance.current = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
        if (scaleDom.current) {
          const a = scaleDom.current.getAttribute('style') || '';
          const [_, num] = a.match(/scale((.+?))/) || [];
          if (num) {
            scaleRef.current = Number(num);
          }
        }
      }
    }
  };

  const handleTouchMove = (e: TouchEvent) => {
    if (e.touches.length === 2) {
      if (hasBeforeFn()) {
        const x1 = e.touches[0].clientX;
        const y1 = e.touches[0].clientY;
        const x2 = e.touches[1].clientX;
        const y2 = e.touches[1].clientY;
        const currentDistance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
        const scaleVal = currentDistance - initialDistance.current;
        const newScale = scaleRef.current + scaleVal / 500;
        scaleRef.current = Math.max(min, Math.min(newScale, max));
        if (scaleDom.current) {
          scaleDom.current.style.transform = `scale(${scaleRef.current})`;
        }
        if (scaleDirection.current === 0) {
          scaleDirection.current = scaleVal > 0 ? 1 : -1;
          handleMove?.(scaleDirection.current);
        }
      }
    }
  };

  useEffect(() => {
    document.addEventListener('touchstart', handleTouchStart, {
      passive: false,
    });
    document.addEventListener('touchmove', handleTouchMove, { passive: false });
    return () => {
      document.removeEventListener('touchstart', handleTouchStart);
      document.removeEventListener('touchmove', handleTouchMove);
    };
  }, []);
  return {
    scaleDom,
  };
};

export { useScale };

实现

扣题

效果:

问题:

  1. ios 无法播放svg动画

    1. 使用 svg 的animation 标签实现
  2. 事件监听器

    1. 在页面销毁的时候都清除
  3. 扣题导图,每次答完题,根节点会被隐藏(在小导图完成后会回到大导图的位置,由于节点都是设置的id选择器,会有相同的id-dom元素影响)

    1. 为小导图的根节点增加额外的唯一属性。
相关推荐
蓝婷儿3 小时前
前端面试每日三题 - Day 32
前端·面试·职场和发展
星空寻流年4 小时前
CSS3(BFC)
前端·microsoft·css3
九月TTS4 小时前
开源分享:TTS-Web-Vue系列:Vue3实现固定顶部与吸顶模式组件
前端·vue.js·开源
CodeCraft Studio4 小时前
数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能
前端·javascript·ui·甘特图
一把年纪学编程4 小时前
【牛马技巧】word统计每一段的字数接近“字数统计”
前端·数据库·word
llc的足迹4 小时前
el-menu 折叠后小箭头不会消失
前端·javascript·vue.js
九月TTS5 小时前
TTS-Web-Vue系列:移动端侧边栏与响应式布局深度优化
前端·javascript·vue.js
Johnstons5 小时前
AnaTraf:深度解析网络性能分析(NPM)
前端·网络·安全·web安全·npm·网络流量监控·网络流量分析
whatever who cares5 小时前
CSS3 伪元素(Pseudo-elements)大全
前端·css·css3
若愚67925 小时前
前端取经路——性能优化:唐僧的九道心经
前端·性能优化