使用Vue 实现小小项目todolist

Kingsley

一个小项目todolist 各个功能的简单演示(代码在结尾)

1. 列表数据渲染
通过在ul 中的li 中v-for 绑定数组lists 进行列表数据渲染

2. 增加数据
在el-input 上通过 @keyup.enter.native="addItem" 通过按下回车键来增加数据。方法绑定对象来获取输入框数据,使用push 方法在lists 中插入新的对象。最后将输入框内容清空。

3. 删除数据
在span.close 上通过 @click="remove(index)" 来删除当前选项。方法放入index 参数,直接通过splice 方法根据index 删除lists 中对应的对象。

4. 编辑数据
在data 中新增属性 currentItem: null,后在存放内容的label 上绑定方法 @dblclick="currentItem = item" 当双击时currentItem 等于当前item,然后在通过v-show 默认隐藏的input 修改框中让 v-show="currentItem === item" ,只有currentItem 为当前对象时input 修改框才出现。input 修改框中默认的value 为当前对象的内容。

最后在input 框上绑定方法用来完成修改 @keyup.enter="editTitle(item, $event)" @blur="editTitle(item, $event)" 当按下回车键或者失去焦点时修改完成。修改完成后将currentItem 的内容重置为null。

5. 标记已完成的事项
在绑定了label 的input上设置 @click="item.completed = !item.completed" 通过点击事件来改变当前对象的completed 属性。并且 v-model="item.completed" 根据事项的完成状况来决定是否选中。然后将当前事项的li 父元素也设置 :class="{checked:item.completed}" 根据事项的完成情况来改变样式。这样当点击label 图片时将事项标记为已完成的状态。

6. 未完成事项个数
通过computed 来对未完成事项的数量进行监听,通过this.lists.filter(item => { return !item.completed }).length; 根据条件将原数组中未完成事项返回形成一个新数组,然后返回该数组的长度。

7. 清除已完成事项
使用方法removeCompleted 来清除已完成的事项。
先根据remaining 的值来判断此时是否有已完成的事项,无则警告框弹出,有则询问是否删除。删除方法为更新lists 的值,通过
this.lists = this.lists.filter(element => { return !element.completed;}
过滤completed 为true 的事项,返回未完成事项组成的数组。

8. 事项全选状态的改变
设置一个向下箭头符号用来改变所有事项的全选状态,并根据事项全选和未全选的情况来反馈箭头符号的状态。
在全选框checkbox 中绑定computed 计算属性 v-model="toggleAll"。在get 方法中根据未完成数remaining 是否为0来判断此时所有事项是否全选。
在set 方法中监听toggleAll 数值,对所有事项状态进行改变,当toggle 为true 时遍历lists 的所有对象,将computed 属性修改为true,反之亦然。

9. 根据哈希值hash 来显示事项
在data 新增属性filterStatus 来获取当前哈希值,在Vue 实例外使用方法

//当路由hash 值发生变化之后,会自动调用该函数
window.onhashchange = function(){
  const hash = window.location.hash.substr(2) || "all";
  app.filterStatus = hash;
}

然后在computed 中新增计算属性filterItems 用来监听filterStatus,并根据filterStatus 的数值来调用array.filter() 方法过滤数组并返回。这里使用了switch 语句。
回到li 标签中,将 v-for="(item, index) in lists" 更改为 v-for="(item, index) in filterItems",默认全部显示。
对于All Active Completed 三个选项的class 属性是否改变也与filterStatus 的值相关联。

10.使用自定义组件实现自动聚焦
将自动聚焦指令绑定到input.editInput 上实现自动聚焦。

Vue.directive("app-focus", {
  inserted(el, binding){
    el.focus();
  },
  update(el, binding){    //更新之后也能调用
    el.focus();
  }
})

此处需要调用update 方法不然无法作用。具体原因暂不清楚,有懂的看官欢迎来评论告知!
本弱鸡目前也还不清楚怎么让此处的el-input 实现自动聚焦功能,autofocus 和自定义指令都无效。。。

11. 调用localStorage 实现本地存储
在Vue 实例外对localStorage.getItemlocalStorage.setItem 方法进行封装。这两个方法都是根据特定的key 值进行数据储存和提取的。
使用watch 监听lists 的变化,此处要监听对象属性的改变需要开启深度监听deep:true。当lists 数组的内容改变时调用 localStorage.setItem 将newValue 存入localStorage。
关于提取localStorage 的数据。将data 中的lists 变为lists:itemStorage.fetch(),这样就可以将localStorage 内JSON 格式的数据赋值给lists 数组。

注意:代码中的link 和script 需要自己重新引入,主输入框和警告框用了elementUI 组件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./node_modules/element-ui/lib/theme-chalk/index.css">
    <title>Document</title>
</head>

<style>
body, html, ul, h1{
    margin: 0;
    padding: 0;
    font-size: 12px;    
    color: black;
}
div#app{
    max-width: 600px;
    margin: 0 auto;
}
h1.title{    
    padding: 10px 0;
    font-size: 80px;
    font-weight: normal;
    color: brown;
    opacity: 0.5;
}
div.todolist{
    border: 1px solid gainsboro;
    box-shadow: 2px 0 5px lightgray;
}
div.header{
    position: relative;
    border-bottom: 1px solid gainsboro;
}
input.el-input__inner{
    padding: 25px 0px 25px 60px;
    border: none;
    outline: none;
    font-size: 24px;
    text-indent: 5px;
}
input.el-input__inner::placeholder{
    color: lightgrey;
}
div.el-input span.el-input__suffix{
    font-size: 24px;
    margin: 2px 10px 0px 0px;
}
input#toggle-all{
    position: absolute;
    border: none;
    opacity: 0;
}
label.toggle-all-label{
    position: absolute;
    display: block;
    box-sizing: border-box;
    top: 1px;
    left: 1px;
    z-index: 1;
    width: 48px;
    height: 48px;
    font-size: 32px;
    color: gainsboro;
    transform: rotate(90deg);
    -webkit-user-select: none;
}
label.toggle-all-label::before{
    content: "❯";
    position: absolute;
    left: 16px;
}
input#toggle-all:checked + label.toggle-all-label{
    color: gray;
}
div.body ul li{
    position: relative;
    padding: 10px 10px 10px 65px;
    border-bottom: 1px solid lightsteelblue;
    font-size: 20px;
    font-weight: normal;
    font-family: 楷体;
    color: rgb(36, 78, 121);
}
div.body ul li:last-child{
    border-bottom: none;
}
div.body ul li.checked{
    color: lightgray;
    text-decoration: line-through;
}
input.toggle{
    position: absolute;
    border: none;
    opacity: 0;
}
label.toggle-label{
    position: absolute;
    top: 2px;
    left: 4px;
    z-index: 1;
    width: 40px;
    height: 40px;
    border-radius: 20px;
    background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
}
input.toggle:checked + label.toggle-label{
    background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
span.close{
    position: absolute;
    right: 10px;
    top: 12px;
    font-weight: bold;
    color: firebrick;
    opacity: 0;
    transition: opacity .2s;
    cursor: pointer;
}
div.body ul li:hover span.close{
    opacity: 1;
}
input.editInput{
    position: absolute;
    top: 0;
    left: 0;
    z-index: 2;
    width: 89.1%;
    padding: 10px 0 10.5px 65px;
    font-size: 20px;
    font-weight: normal;
    font-family: 楷体;
    border: none;
    outline: none;
    box-shadow: inset 0px -1px 5px 0px rgba(0, 0, 0, 0.3);
}
div.footer{
    padding: 12px;
    border-top: 1px solid lightgray;
    overflow: hidden;
}
span.itemNum{
    margin-right: 20%;
}
a.operate{
    display: inline-block;
    padding: 5px 8px;
    margin-right: 2px;
    border: 1px solid transparent;
    border-radius: 5px;
    text-decoration: none;
}
a.operate:hover{
    border: 1px solid lightgray;
}
a.checked{
    border: 1px solid lightgray;
}
a.clearAll{
    margin-left: 10%;
    text-decoration: none;
}
a.clearAll:hover{
    text-decoration: underline;
    cursor: pointer;
}
span.itemNum, a.operate, a.clearAll{
    font-size: 14px;
    font-weight: normal;
    color: gray;
}
</style>
<body>
<div id="app">
  <div style="padding: 0 10px; text-align: center;">
    <h1 class="title">todos</h1>
  </div>
  <div class="todolist">
    <div class="header">
      <input type="checkbox" id="toggle-all" v-model="toggleAll">
      <label for="toggle-all" class="toggle-all-label"></label>
      <el-input v-model="input" suffix-icon="el-icon-edit" placeholder="What needs to be done..." 
      @keyup.enter.native="addItem"></el-input>
    </div>
    <div class="body">
      <ul style="list-style-type: none;">
        <li v-for="(item, index) in filterItems" :key="item.id" :class="{checked:item.completed}">
          <label @dblclick="currentItem = item">{{ item.title }}</label>
          <input type="checkbox" :id="'toggle' + item.id" class="toggle" v-model="item.completed" 
          @click="item.completed = !item.completed">
          <label :for="'toggle' + item.id" class="toggle-label"></label>
          <span class="close" @click="remove(index)">×</span>
          <input type="text" class="editInput" :value="item.title" v-show="currentItem === item" 
          @keyup.enter="editTitle(item, $event)" @blur="editTitle(item, $event)" 
          v-app-focus="item === currentItem">
        </li>
      </ul>
    </div>
    <div class="footer">
      <span class="itemNum">{{ remaining }} item<span v-show="false">s</span> left</span>
      <a href="#/" :class="{operate:true, checked:filterStatus === 'all'}">All</a>    
      <a href="#/active" :class="{operate:true, checked:filterStatus === 'active'}">Active</a>    
      <a href="#/completed" :class="{operate:true, checked:filterStatus === 'completed'}" >Completed</a>    
      <a class="clearAll" @click="removeCompleted">Clear Completed</a>
    </div>
  </div>
    
</div>

    <script src="./node_modules/vue/dist/vue.js"></script>
    <script src="./node_modules/element-ui/lib/index.js"></script>
<script>
//自定义个storage 的key
const STORAGEKEY = "items-vuejs";    
//拓展功能:将数据保存在本地
const itemStorage = {
  //获取数据
  fetch(){
    let data = localStorage.getItem(STORAGEKEY) || "[]";//通过key 获取数据,当数据为空时返回空数组
    //通过JSON.parse 将JSON 字符串转换为JSON 格式
    return JSON.parse(data);
  },
  //保存数据(传入要保存的数据)
  save(items){
    localStorage.setItem(STORAGEKEY, JSON.stringify(items));    //通过JSON 形式保存
  }
}
const lists = [        //初始化数据(使用localStorage 后可有可无)
    {id:1, title:"吃饭", completed:false,},
    {id:2, title:"学习", completed:true,},
    {id:3, title:"休息", completed:true,},
];
Vue.directive("app-focus", {
  inserted(el, binding){
    el.focus();
  },
  update(el, binding){    //更新之后也能调用
    el.focus();
  }
})
var app = new Vue({
  el:"#app",
  data() {
      return {
        input:"",
        lists:itemStorage.fetch(),
        currentItem:null,
        filterStatus:"all",
      }
  },
  computed: {
    remaining(){
      return this.lists.filter(item => { return !item.completed }).length;
    },
    toggleAll:{
      get:function(){
        return this.remaining === 0? true:false;
      },
      set:function(newValue){
        this.lists.forEach(element => {
          element.completed = newValue;
        });
      }
    },
    filterItems(){
      switch (this.filterStatus) {
          case "active":
            return this.lists.filter(item => !item.completed);
            break;
          case "completed":
            return this.lists.filter(item => item.completed)
          default:
            return this.lists;
            break;
        }
    }
  },
  methods: {
    addItem(event){
      let inputVal = event.target.value.trim();
      if(inputVal === ""){
        this.$message.error({ message:"Writing something here..."} );
      }else{
        this.lists.push({
          id:this.lists.length + 1, 
          title:inputVal, 
          completed:false, 
        });
        event.target.value = "";
      }
    },
    remove(index){
      this.lists.splice(index, 1);
    },
    removeCompleted(){
      if(this.remaining === this.lists.length){
        this.$message.warning({
          message:"无已完成的事项。"
        });
        return;
      }else{
        this.$confirm("将清空所以已完成选项,是否继续?", "提示", {
        confirmButtonText:"清空",
        cancelButtonText:"取消",
        type: "warning"
      }).then(() => {
        this.lists = this.lists.filter(element => { return !element.completed;}    //返回未完成的
        );
        this.$message.success({
          message:"清空成功!"
        });
      }).catch(() => {
        this.$message.info({
          message:"取消清空。"
        });
      });
      }
    },
    editTitle(item, event){
      let value = event.target.value;
      if(value.trim() === ""){
        this.$message.warning({
          message:"修改后的内容不能为空。"
        });
      }else{
        item.title = event.target.value;
        this.currentItem = null;
      }
    },
  },
  watch: {
    //深度监听,当对象中的属性发生改变后,使用deep:true 选择则可以实现监听
    lists:{
      handler: function(newValue, oldValue){    //回调函数
        //数组变化时,将数据保存到本地
        itemStorage.save(newValue);
      }    ,
      deep:true
    }
  },
})

//当路由hash 值发生变化之后,会自动调用该函数
window.onhashchange = function(){
  const hash = window.location.hash.substr(2) || "all";
  app.filterStatus = hash;
}
//页面刷新时清除掉上一次的哈希值,避免因地址栏的哈希值导致按钮点击无效果
window.onload = function(){
  window.location.hash = "";
}
</script>
</body>

</html>
阅读 1.6k
2 声望
0 粉丝
0 条评论
2 声望
0 粉丝
文章目录
宣传栏