vue.js实现会动的简历(包含底部导航功能,编辑功能)


Posted in Javascript onApril 08, 2019

在网上看到一个这样的网站,STRML 它的效果看着十分有趣,如下图所示:

vue.js实现会动的简历(包含底部导航功能,编辑功能)

这个网站是用 react.js 来写的,于是,我就想着用 vue.js 也来写一版,开始撸代码。

首先要分析打字的原理实现,假设我们定义一个字符串 str ,它等于一长串注释加 CSS 代码,并且我们看到,当 css 代码写完一个分号的时候,它写的样式就会生效。我们知道要想让一段 CSS 代码在页面生效,只需要将其放在一对 <style> 标签对中即可。比如:

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width">
 <title>JS Bin</title>
</head>
<body>
 红色字体
 <style>
 body{
  color:#f00;
 }
 </style>
</body>
</html>

你可以狠狠点击此处 具体示例 查看效果。

当看到打字效果的时候,我们不难想到,这是要使用 间歇调用(定时函数:setInterval()) 或 超时调用(延迟函数:setTimeout()) 加 递归 去模拟实现 间歇调用 。一个包含一长串代码的字符串,它是一个个截取出来,然后分别写入页面中,在这里,我们需要用到字符串的截取方法,如 slice(),substr(),substring() 等,选择用哪个截取看个人,不过需要注意它们之间的区别。好了,让我们来实现一个简单的这样打字的效果,如下:

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width">
 <title>JS Bin</title>
</head>
<body>
 <div id="result"></div>
 <script>
  var r = document.getElementById('result');
  var c = 0;
  var code = 'body{background-color:#f00;color:#fff};'
  var timer = setInterval(function(){
   c++;
   r.innerHTML = code.substr(0,c);
   if(c >= code.length){
   clearTimeout(timer);
   }
  },50)
 </script>
</body>
</html>

你可以狠狠点击此处具体示例 查看效果。好的,让我们来分析一下以上代码的原理,首先放一个用于包含代码显示的标签,然后定义一个包含代码的字符串,接着定义一个初始值为 0 的变量,为什么要定义这样一个变量呢?我们从实际效果中看到,它是一个字一个字的写入到页面中的。初始值是没有一个字符的,所以,我们就从第 0 个开始写入, c 一个字一个字的加,然后不停的截取字符串,最后渲染到标签的内容当中去,当 c 的值大于等于了字符串的长度之后,我们需要清除定时器。定时函数看着有些不太好,让我们用超时调用结合递归来实现。

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width">
 <title>JS Bin</title>
</head>
<body>
 <div id="result"></div>
 <script>
  var r = document.getElementById('result');
  var c = 0;
  var code = 'body{background-color:#f00;color:#fff};';
  var timer;
  function write(){
  c++;
  r.innerHTML = code.substr(0,c);
  if(c >= code.length && timer){
   clearTimeout(timer)
  }else{
   setTimeout(write,50);
  }
 }
 write();
 </script>
</body>
</html>

你可以狠狠点击此处具体示例 查看效果。

好了,到此为止,算是实现了第一步,让我们继续,接下来,我们要让代码保持空白和缩进,这可以使用 <pre> 标签来实现,但其实我们还可以使用css代码的 white-space 属性来让一个普通的 div 标签保持这样的效果,为什么要这样做呢,因为我们还要实现一个功能,就是编辑它里面的代码,可以让它生效。更改一下代码,如下:

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width">
 <title>JS Bin</title>
 <style>
 #result{
  white-space:pre-wrap;
  oveflow:auto;
 }
 </style>
</head>
<body>
 <div id="result"></div>
 <script>
  var r = document.getElementById('result');
  var c = 0;
  var code = `
  body{
   background-color:#f00;
   color:#fff;
  }
  `
  var timer;
  function write(){
  c++;
  r.innerHTML = code.substr(0,c);
  if(c >= code.length && timer){
   clearTimeout(timer)
  }else{
   setTimeout(write,50);
  }
 }
 write();
 </script>
</body>
</html>

你可以狠狠点击此处 具体示例 查看效果。

接下来,我们还要让样式生效,这很简单,将代码在 style 标签中写一次即可,请看:

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width">
 <title>JS Bin</title>
 <style>
 #result{
  white-space:pre-wrap;
  overflow:auto;
 }
 </style>
</head>
<body>
 <div id="result"></div>
 <style id="myStyle"></style>
 <script>
  var r = document.getElementById('result'),
   t = document.getElementById('myStyle');
  var c = 0;
  var code = `
   body{
   background-color:#f00;
   color:#fff;
   }
  `;
  var timer;
  function write(){
  c++;
  r.innerHTML = code.substr(0,c);
  t.innerHTML = code.substr(0,c);
  if(c >= code.length){
   clearTimeout(timer);
  }else{
   setTimeout(write,50);
  }
  }
  write();
 </script>
 
</body>
</html>

你可以狠狠点击此处 具体示例 查看效果。

我们看到代码还会有高亮效果,这可以用正则表达式来实现,比如以下一个 demo :

<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="UTF-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <meta http-equiv="X-UA-Compatible" content="ie=edge" />
 <title>代码编辑器</title>
 <style>
  * {
   margin: 0;
   padding: 0;
  }

  .ew-code {
   tab-size: 4;
   -moz-tab-size: 4;
   -o-tab-size: 4;
   margin-left: .6em;
   background-color: #345;
   white-space: pre-wrap;
   color: #f2f2f2;
   text-indent: 0;
   margin-right: 1em;
   display: block;
   overflow: auto;
   font-size: 20px;
   border-radius: 5px;
   font-style: normal;
   font-weight: 400;
   line-height: 1.4;
   font-family: Consolas, Monaco, "宋体";
   margin-top: 1em;
  }

  .ew-code span {
   font-weight: bold;
  }
 </style>
</head>

<body>
 <code class="ew-code">
  <div id="app">
   <p>{{ greeting }} world!</p>
  </div>
 </code>
 <code class="ew-code">
  //定义一个javascript对象
  var obj = { 
   greeting: "Hello," 
  }; 
  //创建一个实例
  var vm = new Vue({ 
   data: obj 
  });
  /*将实例挂载到根元素上*/
  vm.$mount(document.getElementById('app'));
 </code>
 <script>
  var lightColorCode = {
   importantObj: ['JSON', 'window', 'document', 'function', 'navigator', 'console', 'screen', 'location'],
   keywords: ['if', 'else if', 'var', 'this', 'alert', 'return', 'typeof', 'default', 'with', 'class', 'export', 'import', 'new'],
   method: ['Vue', 'React', 'html', 'css', 'js', 'webpack', 'babel', 'angular', 'bootstap', 'jquery', 'gulp','dom'],
   // special: ["*", ".", "?", "+", "$", "^", "[", "]", "{", "}", "|", "\\", "(", ")", "/", "%", ":", "=", ';']
  }
  function setHighLight(el) {
   var htmlStr = el.innerHTML;
   //匹配单行和多行注释
   var regxSpace = /(\/\/\s?[^\s]+\s?)|(\/\*(.|\s)*?\*\/)/gm,
    matchStrSpace = htmlStr.match(regxSpace),
    spaceLen;
   //匹配特殊字符
   var regxSpecial = /[`~!@#$%^&.{}()_\-+?|]/gim,
    matchStrSpecial = htmlStr.match(regxSpecial),
    specialLen;
   var flag = false;
   if(!!matchStrSpecial){
     specialLen = matchStrSpecial.length;
    }else{
     specialLen = 0;
     return;
    }
    for(var k = 0;k < specialLen;k++){
     htmlStr = htmlStr.replace(matchStrSpecial[k],'<span style="color:#b9ff01;">' + matchStrSpecial[k] + '</span>');
    }
   for (var key in lightColorCode) {
    if (key === 'keywords') {
     lightColorCode[key].forEach(function (imp) {
      htmlStr = htmlStr.replace(new RegExp(imp, 'gim'), '<span style="color:#00ff78;">' + imp + '</span>')
     })
     flag = true;
    } else if (key === 'importantObj') {
     lightColorCode[key].forEach(function (kw) {
      htmlStr = htmlStr.replace(new RegExp(kw, 'gim'), '<span style="color:#ec1277;">' + kw + '</span>')
     })
     flag = true;
    } else if (key === 'method') {
     lightColorCode[key].forEach(function (mt) {
      htmlStr = htmlStr.replace(new RegExp(mt, 'gim'), '<span style="color:#52eeff;">' + mt + '</span>')
     })
     flag = true;
    }
   }
   if (flag) {
    if (!!matchStrSpace) {
     spaceLen = matchStrSpace.length;
    } else {
     spaceLen = 0;
     return;
    }
    for(var i = 0;i < spaceLen;i++){
     var curFont;
     if(window.innerWidth <= 1200){
      curFont = '12px';
     }else{
      curFont = '14px';
     }
     htmlStr = htmlStr.replace(matchStrSpace[i],'<span style="color:#899;font-size:'+curFont+';">' + matchStrSpace[i] + '</span>');
    }
    el.innerHTML = htmlStr;
   }
  }
  var codes = document.querySelectorAll('.ew-code');
  for (var i = 0, len = codes.length; i < len; i++) {
   setHighLight(codes[i])
  }

 </script>
</body>
</html>

你可以狠狠点击此处 具体示例 查看效果。

不过这里为了方便,我还是使用插件 Prism.js ,另外在这里,我们还要用到将一个普通文本打造成 HTML 网页的插件 marked.js 。

接下来分析如何暂停动画和继续动画,很简单,就是清除定时器,然后重新调用即可。如何让编辑的代码生效呢,这就需要用到自定义事件 .sync 事件修饰符,自行查看官网 vue.js 。

虽然这里用原生 js 也可以实现,但我们用 vue-cli 结合组件的方式来实现,这样更简单一些。好了,让我们开始吧:

新建一个 vue-cli 工程(步骤自行百度):

新建一个 styleEditor.vue 组件,代码如下:

<template>
 <div class="container">
  <div class="code" v-html="codeInstyleTag"></div>
  <div class="styleEditor" ref="container" contenteditable="true" @input="updateCode($event)" v-html="highlightedCode"></div>
 </div>
</template>
<script>
 import Prism from 'prismjs'
 export default {
  name:'Editor',
  props:['code'],
  computed:{
   highlightedCode:function(){
    //代码高亮
    return Prism.highlight(this.code,Prism.languages.css);
   },
   // 让代码生效
   codeInstyleTag:function(){
    return `<style>${this.code}</style>`
   }
  },
  methods:{
   //每次打字到最底部,就要滚动
   goBottom(){
    this.$refs.container.scrollTop = 10000;
   },
   //代码修改之后,可以重新生效
   updateCode(e){
    this.$emit('update:code',e.target.textContent);
   }
  }
 }
</script>
<style scoped>
 .code{
  display:none;
 }
</style>

新建一个 resumeEditor.vue 组件,代码如下:

<template>
 <div class = "resumeEditor" :class="{htmlMode:enableHtml}" ref = "container">
  <div v-if="enableHtml" v-html="result"></div>
  <pre v-else>{{result}}</pre>
 </div>
</template>
<script>
 import marked from 'marked'
 export default {
  props:['markdown','enableHtml'],
  name:'ResumeEditor',
  computed:{
   result:function(){
    return this.enableHtml ? marked(this.markdown) : this.markdown
   }
  },
  methods:{
   goBottom:function(){
    this.$refs.container.scrollTop = 10000
   }
  }
 }
</script>
<style scoped>
 .htmlMode{
  anmation:flip 3s;
 }
 @keyframes flip{
  0%{
   opactiy:0;
  }
  100%{
   opactiy:1;
  }
 }
</style>

新建一个底部导航菜单组件 bottomNav.vue ,代码如下:

<template>
 <div id="bottom">
  <a id="pause" @click="pauseFun">{{ !paused ? '暂停动画' : '继续动画 ||' }}</a>
  <a id="skipAnimation" @click="skipAnimationFun">跳过动画</a>
  <p>
   <span v-for="(url,index) in demourl" :key="index">
    <a :href="url.url" rel="external nofollow" >{{ url.title }}</a>
   </span>
  </p>
  <div id="music" @click="musicPause" :class="playing ? 'rotate' : ''" ref="music"></div>
 </div>
</template>
<script>
 export default{
  name:'bottom',
  data(){
   return{
    demourl:[
     {url:'http://eveningwater.com/',title:'个人网站'},
     {url:'https://github.com/eveningwater',title:'github'}
    ],
    paused:false,//暂停
    playing:false,//播放图标动画
    autoPlaying:false,//播放音频
    audio:''
   }
  },
  mounted(){
   
  },
  methods:{
   // 播放音乐
   playMusic(){
    this.playing = true;
    this.autoPlaying = true;
    // 创建audio标签
    this.audio = new Audio();
    this.audio.src = "http://eveningwater.com/project/newReact-music-player/audio/%E9%BB%84%E5%9B%BD%E4%BF%8A%20-%20%E7%9C%9F%E7%88%B1%E4%BD%A0%E7%9A%84%E4%BA%91.mp3";
    this.audio.loop = 'loop';
    this.audio.autoplay = 'autoplay';
    this.$refs.music.appendChild(this.audio);
   },
   // 跳过动画
   skipAnimationFun(e){
    e.preventDefault();
    this.$emit('on-skip');
   },
   // 暂停动画
   pauseFun(e){
    e.preventDefault();
    this.paused = !this.paused;
    this.$emit('on-pause',this.paused);
   },
   // 暂停音乐
   musicPause(){
    this.playing = !this.playing;
    if(!this.playing){
     this.audio.pause();
    }else{
     this.audio.play();
    }
   }
  }
 }
</script>
<style scoped>
 #bottom{
  position:fixed;
  bottom:5px;
  left:0;
  right:0;
 }
 #bottom p{
  float:right;
 }
 #bottom a{
  text-decoration: none;
  color: #999;
  cursor:pointer;
  margin-left:5px;
 }
 #bottom a:hover,#bottom a:active{
  color: #010a11;
 }
</style>

接下来是核心 APP.vue 组件代码:

<template>
 <div id="app">
  <div class="main">
   <StyleEditor ref="styleEditor" v-bind.sync="currentStyle"></StyleEditor>
   <ResumeEditor ref="resumeEditor" :markdown = "currentMarkdown" :enableHtml="enableHtml"></ResumeEditor>
  </div>
  <BottomNav ref ="bottomNav" @on-pause="pauseAnimation" @on-skip="skipAnimation"></BottomNav>
 </div>
</template>
<script>
 import ResumeEditor from './components/resumeEditor'
 import StyleEditor from './components/styleEditor'
 import BottomNav from './components/bottomNav'
 import './assets/common.css'
 import fullStyle from './style.js'
 import my from './my.js'
 export default {
  name: 'app',
  components: {
   ResumeEditor,
   StyleEditor,
   BottomNav
  },
  data() {
   return {
    interval: 40,//写入字的速度
    currentStyle: {
     code: ''
    },
    enableHtml: false,//是否打造成HTML网页
    fullStyle: fullStyle,
    currentMarkdown: '',
    fullMarkdown: my,
    timer: null
   }
  },
  created() {
   this.makeResume();
  },
  methods: {
   // 暂停动画
   pauseAnimation(bool) {
    if(bool && this.timer){
     clearTimeout(this.timer);
    }else{
     this.makeResume();
    }
   },
   // 快速跳过动画
   skipAnimation(){
    if(this.timer){
     clearTimeout(this.timer);
    }
    let str = '';
    this.fullStyle.map((f) => {
     str += f;
    })
    setTimeout(() => {
     this.$set(this.currentStyle,'code',str);
    },100)
    this.currentMarkdown = my;
    this.enableHtml = true;
    this.$refs.bottomNav.playMusic();
   },
   // 加载动画
   makeResume: async function() {
    await this.writeShowStyle(0)
    await this.writeShowResume()
    await this.writeShowStyle(1)
    await this.writeShowHtml()
    await this.writeShowStyle(2)
    await this.$nextTick(() => {this.$refs.bottomNav.playMusic()});
   },
   // 打造成HTML网页
   writeShowHtml: function() {
    return new Promise((resolve, reject) => {
     this.enableHtml = true;
     resolve();
    })
   },
   // 写入css代码
   writeShowStyle(n) {
    return new Promise((resolve, reject) => {
     let showStyle = (async function() {
      let style = this.fullStyle[n];
      if (!style) return;
      //计算出数组每一项的长度
      let length = this.fullStyle.filter((f, i) => i <= n).map((it) => it.length).reduce((t, c) => t + c, 0);
      //当前要写入的长度等于数组每一项的长度减去当前正在写的字符串的长度
      let prefixLength = length - style.length;
      if (this.currentStyle.code.length < length) {
       let l = this.currentStyle.code.length - prefixLength;
       let char = style.substring(l, l + 1) || ' ';
       this.currentStyle.code += char;
       if (style.substring(l - 1, l) === '\n' && this.$refs.styleEditor) {
        this.$nextTick(() => {
         this.$refs.styleEditor.goBottom();
        })
       }
       this.timer = setTimeout(showStyle, this.interval);
      } else {
       resolve();
      }
     }).bind(this)
     showStyle();
    })
   },
   // 写入简历
   writeShowResume() {
    return new Promise((resolve, reject) => {
     let length = this.fullMarkdown.length;
     let showResume = () => {
      if (this.currentMarkdown.length < length) {
       this.currentMarkdown = this.fullMarkdown.substring(0, this.currentMarkdown.length + 1);
       let lastChar = this.currentMarkdown[this.currentMarkdown.length - 1];
       let prevChar = this.currentMarkdown[this.currentMarkdown.length - 2];
       if (prevChar === '\n' && this.$refs.resumeEditor) {
        this.$nextTick(() => {
         this.$refs.resumeEditor.goBottom()
        });
       }
       this.timer = setTimeout(showResume, this.interval);
      } else {
       resolve()
      }
     }
     showResume();
    })
   }
  }
 }
</script>
<style scoped>
 #app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
 }

 .main {
  position: relative;
 }

 html {
  min-height: 100vh;
 }

 * {
  transition: all 1.3s;
 }
</style>

到此为止,一个可以快速跳过动画,可以暂停动画,还有音乐播放,还能自由编辑代码的会动的简历已经完成,代码已上传至 git源码 ,欢迎 fork ,也望不吝啬 star 。

在线预览

总结

以上所述是小编给大家介绍的vue.js实现会动的简历(包含底部导航功能,编辑功能),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
Mootools 1.2教程 输入过滤第二部分(字符串)
Sep 15 Javascript
javascript中不等于的代码是什么怎么写
Dec 29 Javascript
jquery通过select列表选择框对表格数据进行过滤示例
May 07 Javascript
JS实现的通用表单验证插件完整实例
Aug 20 Javascript
JavaScript实现身份证验证代码
Feb 17 Javascript
JS阻止事件冒泡行为和闭包的方法
Jun 16 Javascript
vue中的event bus非父子组件通信解析
Oct 27 Javascript
微信小程序自定义toast实现方法详解【附demo源码下载】
Nov 28 Javascript
原生JS实现网页手机音乐播放器 歌词同步播放的示例
Feb 02 Javascript
vue 组件中使用 transition 和 transition-group实现过渡动画
Jul 09 Javascript
在Layui中操作数据表格,给指定单元格添加事件示例
Oct 26 Javascript
一看就会的vuex实现登录验证(附案例)
Jan 09 Javascript
微信小程序视图控件与bindtap之间的问题的解决
Apr 08 #Javascript
微信小程序实现bindtap等事件传参
Apr 08 #Javascript
详解vue中axios请求的封装
Apr 08 #Javascript
使用taro开发微信小程序遇到的坑总结
Apr 08 #Javascript
vue+element+Java实现批量删除功能
Apr 08 #Javascript
如何为你的JavaScript代码日志着色详解
Apr 08 #Javascript
详解element-ui日期时间选择器的日期格式化问题
Apr 08 #Javascript
You might like
关于Appserv无法打开localhost问题的解决方法
2009/10/16 PHP
php实现图片转换成ASCII码的方法
2015/04/03 PHP
PHP观察者模式原理与简单实现方法示例
2017/08/25 PHP
PHP filter_var() 函数, 验证判断EMAIL,URL等
2021/03/09 PHP
JavaScript面向对象之体会[总结]
2008/11/13 Javascript
js数值和和字符串进行转换时可以对不同进制进行操作
2014/03/05 Javascript
nodejs中实现路由功能
2014/12/29 NodeJs
JavaScript中使用Math.floor()方法对数字取整
2015/06/15 Javascript
javascript实现input file上传图片预览效果
2015/12/31 Javascript
浅谈JavaScript对象的创建方式
2016/06/13 Javascript
jQuery+css实现非常漂亮的水平导航菜单效果
2016/07/27 Javascript
基于JavaScript实现复选框的全选和取消全选
2017/02/09 Javascript
vue.js+Echarts开发图表放大缩小功能实例
2017/06/09 Javascript
vue watch深度监听对象实现数据联动效果
2018/08/16 Javascript
详解Vue源码中一些util函数
2019/04/24 Javascript
[00:31]DOTA2荣耀之路7:Miracle-空血无敌斩
2018/05/31 DOTA
[41:17]VG vs Optic 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
Python实现全局变量的两个解决方法
2014/07/03 Python
状态机的概念和在Python下使用状态机的教程
2015/04/11 Python
Python中字符串对齐方法介绍
2015/05/21 Python
python实现用户答题功能
2018/01/17 Python
numpy返回array中元素的index方法
2018/06/27 Python
Python OpenCV读取png图像转成jpg图像存储的方法
2018/10/28 Python
Python实现操纵控制windows注册表的方法分析
2019/05/24 Python
Django如何使用redis作为缓存
2020/05/21 Python
如何以Winsows Service方式运行JupyterLab
2020/08/30 Python
英国皇家邮政海外旗舰店:Royal Mail
2018/02/21 全球购物
Tiqets英国:智能手机上的文化和娱乐门票
2019/07/10 全球购物
我们没有写servlet的构造方法,那么容器是怎么创建servlet的实例呢
2013/04/24 面试题
初任培训自我鉴定
2013/10/07 职场文书
个人函授自我鉴定
2014/03/25 职场文书
教师四风自我剖析材料
2014/09/30 职场文书
小学音乐教师个人工作总结
2015/02/05 职场文书
2015暑假实习报告范文
2015/07/13 职场文书
企业法人任命书
2015/09/21 职场文书
Springboot如何使用logback实现多环境配置?
2021/06/16 Java/Android