vue.js如何演示思维导图

背景

深交所在 2024 年请技术口全体人员参加名为“程序员的浪漫——新春送祝福”的活动。真不错!我喜欢这个活动,它很好地激发了我创作的热情。

经过策划,我决定用 vue.js 来演示思维导图,以全面宣传深交所。问题来了,如何实现?我搜索了全网,实在找不到一篇教程,教人如何用 vue.js 或 JavaScript 来演示思维导图中的每一个节点。面对这样的困难,我想过要不裁剪出每一个节点来播放吧?这样做起来就简单多了。哦,不!这样做太投机取巧了!毕竟我想夺取实施难度大的巧夺天工奖呢!因此,我决定做全网第一人,公开发表 vue.js 如何演示一张思维导图。

思维导图

下面这张思维导图,告诉我们深交所所有的宣传标语。我需要在活动作品中演示它的每一个节点。

深交所宣传标语

思路

首先,要知道这张图非常大,况且它只能被放在页面中一个指定的组件或容器中。其次,存放和演示思维导图的容器的尺寸是有限的,无法 100% 显示一个节点。因此,演示的时候,思维导图应缩放到特定的尺寸,使得容器恰好只显示一个节点,而其余节点则被隐藏或遮挡了。

我可以利用以下 CSS 特性(关注粗体字):

  • background-repeat:定义背景图像的重复方式。背景图像可以沿着水平轴,垂直轴,两个轴重复,或者根本不重复
  • background-size:设置背景图片大小。图片可以保有其原有的尺寸,或者拉伸到新的尺寸,或者在保持其原有比例的同时缩放到元素的可用空间的尺寸
  • background-position-x:设置水平方向的位置。
  • background-position-y:用于设置初始状态时背景图片在垂直方向上的位置。

我应先获取每个节点的位置和尺寸,所需的数据如下:

属性名 属性含义
x 横坐标
y 纵坐标
w 宽度
h 高度

接下来,用伪代码描述下放置和缩放图片的(核心)思路:

if 节点宽度 <= 容器最大宽度
   容器宽度 = 节点宽度
else
   容器宽度 = 容器最大宽度

演示缩放比 = 容器宽度 / 节点宽度
容器高度 = 节点高度 * 演示缩放比

if 容器高度 > 容器最大高度
   容器宽度 = 容器宽度 * 容器最大高度 / 容器高度
   演示缩放比 = 容器宽度 / 节点宽度

容器高度 = 节点高度 * 演示缩放比

background-size = 图片原始宽度 / 节点宽度
background-position-x = 0 - 节点横坐标 * 演示缩放比
background-position-y = 0 - 节点纵坐标 * 演示缩放比

实现

1、底层依赖

我选择的vue.js的版本是 2.7.7

2、容器

要知道,我们既可能在 14 寸电脑显示器上演示思维导图,也可能在 12 寸电脑显示器上演示,也可能在大屏电视机上演示,所以,我打算使容器的尺寸适应不同的显示器,小屏就小图显示,大屏则大图显示。

看以下代码:

<template>
  <div id="imgContainer" :style="imgContainerStyle"></div>
</template>

<script>
export default {
  name: 'MindMapping',
  data() {
    return {
      // 图片的原始尺寸
      imgSize: {
        w: 4195,
        h: 2037
      },
      imgContainerStyle: {
        top: 0,
        bottom: 0,
        left: 0,
        right: 0,
        margin: 'auto',
        width: '0px',
        height: '0px',
        backgroundSize: 'cover'
      }
    }
  },
  computed: {
    // 图片最大的宽度,希望不要被其它元素遮挡
    getMaxShowWidth() {
      return window.innerWidth - 605;
    },
    // 图片最大的高度,希望不要越出浏览器
    getMaxShowHeight() {
      return Math.floor(window.innerHeight * 0.7);
    }
  },
  mounted() {
    // 计算思维导图的尺寸,应保持图片的宽高比,否则会变形
    let h = Math.ceil(this.getMaxShowWidth * (this.imgSize.h / this.imgSize.w));
    this.imgContainerStyle.width = `${this.getMaxShowWidth}px`;
    this.imgContainerStyle.height = `${h}px`;
  }
}
</script>

<style scoped>
#imgContainer {
  display: inline-block;
  position: absolute;
  background-image: url(images/深交所宣传标语.v3.png);
  background-repeat: no-repeat;
}
</style>

一点点解释下:

  • 我使容器以绝对布局显示,这样就可以水平和垂直居中显示了。
  • 我使用 background-xxx 的方式,这样有助于只显示图片的一部分。
  • getMaxShowWidthgetMaxShowHeight 是我自己的计算,你可以用其它方式计算。
  • 首次加载思维导图时,计算容器最多可以多少像素来显示,然后为了保持宽高比,计算容器的高度,最后使图片铺满整个容器。

效果图:

首次加载思维导图

3、准备每个节点的坐标和尺寸信息

正如思路中所阐述的,我需要准备每个节点的坐标和尺寸信息。ImageGlass软件可以起到作用。

信息如下所示:

// 思维导图的演示路径
showPath: [{// 中心主题
    x: 1870, y: 850, w: 837, h: 258
}, {// 价值观
    x: 2640, y: 0, w: 800, h: 230
}, {// 使命
    x: 2640, y: 178, w: 800, h: 280
}, {// 愿景
    x: 2640, y: 395, w: 1000, h: 270
}, {// 深交所“十四五”发展战略规划与“优一”建设纲要的总体原则
    x: 2640, y: 623, w: 1440, h: 555
}, {// 深交所“十四五”发展战略规划的主要目标
    x: 2640, y: 1164, w: 1440, h: 492
}, {// “优一”建设纲要的主要目标
    x: 2640, y: 1662, w: 1558, h: 375
}, {// 十四五IT发展战略目标
    x: 0, y: 0, w: 1890, h: 2037
}, {// 战略目标之使命&愿景
    x: 326, y: 140, w: 978, h: 586
}, {// 战略目标之原则&策略
    x: 244, y: 718, w: 982, h: 660
}, {// 战略目标之战略目标
    x: 42, y: 1364, w: 1188, h: 256
}, {// 战略目标之重点任务
    x: 432, y: 1600, w: 794, h: 390
}]

我把这些信息放在数组里,这样我就可以使程序依次演示每个节点了。

4、实现伪代码

思路中的伪代码是此篇文章的核心算法,我需要实现它。

getShowProp(x, y, w, h) {
    let displayWidth, displayHeight, displayX, displayY, scale;
    if (w > this.getMaxShowWidth) {
        displayWidth = this.getMaxShowWidth;
    } else {
        displayWidth = w;
    }
    scale = Math.round(displayWidth / w * 10000) / 10000;
    displayHeight = Math.floor(h * scale);
    if (displayHeight > this.getMaxShowHeight) {
        displayWidth = parseInt(displayWidth * this.getMaxShowHeight / displayHeight);
        scale = Math.round(displayWidth / w * 10000) / 10000;
    }
    displayHeight = Math.floor(h * scale);
    displayX = 0 - Math.floor(x * scale);
    displayY = 0 - Math.floor(y * scale);
    return {
        x: displayX,
        y: displayY,
        w: displayWidth,
        h: displayHeight
    };
}

有了这个函数,我就可以用它的返回值来设置容器的宽度、高度和 background-position 了。等等,background-size 值怎么设置?还需要以下实现代码:

getBackgroundSize(w) {
    let scale = Math.floor(this.imgSize.w / w * 100);
    return `${scale}%`;
}

我只需要把节点的宽度值传给这个函数,就可以用它的返回值设置 background-size 了。

5、演示函数

有了以上核心实现,我就可以执行起来了。此时我需要执行核心代码的代码——演示函数。

// 演示节点
internalShow() {
    const next = this.showPath[this.nextPath];
    const showProp = this.getShowProp(next.x, next.y, next.w, next.h);

    // 使节点进场
    this.$nextTick(() => {
        this.imgContainerStyle.width = `${showProp.w}px`;
        this.imgContainerStyle.height = `${showProp.h}px`;
        this.imgContainerStyle.backgroundPosition = `${showProp.x}px ${showProp.y}px`;
        this.imgContainerStyle.backgroundSize = this.getBackgroundSize(next.w);
        this.imgContainerStyle.left = 0;
        this.imgContainerStyle.right = 0;
        this.imgContainerStyle.top = 0;
        this.imgContainerStyle.bottom = 0;
        this.imgContainerStyle.margin = 'auto';
    });
}

其中,nextPath 是演示节点的指示器,它在 data 里设置。初始设置如下:

nextPath: -1

到此,我可以迫不及待地测试一下。我可以在 mounted 里添加如下代码:

setTimeout(() => {
    this.nextPath = 0;
    this.internalShow();
}, 1000);

以上代码表示页面加载 1 秒后,就演示第 1 个节点。亲测成功!

6、完善演示函数

我希望程序能像 PPT 一样,既可以演示下一个节点,也可以演示上一个节点。而且,我还需要检查 nextPath 值,使它不能越界,也就是说,当演示到最后一个节点时,就不能再往下演示了,当回退到第一个节点时,就不能再回退了。于是,有了以下代码:

// 演示下一个节点
showNext() {
    // 如果演示到最后一个节点,就不再往后演示
    if (++this.nextPath === this.showPath.length) {
        this.nextPath--;
        return;
    }
    this.internalShow();
},
// 演示上一个节点
showPrevious() {
    // 如果后退到第一个节点,就不再后退
    if (--this.nextPath < 0) {
        this.nextPath++;
        return;
    }
    this.internalShow();
}

mounted 里使用以下代码测试下:

setTimeout(() => {
    this.showNext();
    setTimeout(() => {
        this.showNext();
        setTimeout(this.showPrevious, 500);
    }, 500);
}, 1000);

7、键盘指令

有没有发现,我刚陷入了回调地狱中?因为 setTimeout 嵌套太复杂了,让人抓狂!

所以,为了演示每个节点,我决定使用键盘指令——按下方向左右键——来指示程序演示,所以我应绑定键盘事件。

mounted 里加入以下代码:

window.addEventListener('keyup', event => {
    if ('ArrowRight' === event.key) {
        // 按下`right`键时,使思维导图向后演示
        this.showNext();
    } else if ('ArrowLeft' === event.key) {
        // 按下`left`键时,使思维导图向前演示
        this.showPrevious();
    }
});

到此,按下方向右键时,程序就演示下一个节点,按下方向左键时,程序就回到上一个节点。

8、最终效果

最终效果

更多

嘿!你可能认为这样做效果太简陋了,应当要加入动画啥的。我已经考虑过,也加入了。只是因为这些增强效果不在本文范围内,所以我就不写了。

我的全部代码都放在 GitHub 里,欢迎来看看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容