主要功能展示
1、新增列表功能:在输入框内输入数据,点击键盘上的enter”按钮,将输入的数据添加到列表中,并清空输入框的内容。如果用户没有输入数据,将会提示。
2、高亮和删除功能:鼠标经过列表里的每一项,都会让当前列表背景颜色改变,并显示后面的删除按钮。点击删除将会提示是否删除,如果点击删除,则删除此项列表。鼠标离开时将消失。
3、计算属性:可以根据前面input框前面是否勾选对号来判断当前事项是否完成,并将已完成事项和总事项通过Footer子组件中的computed属性计算得出。
4、一次删除多个选中功能:点击 ‘清除已完成’ 按钮会弹出是否清除的弹窗,点击确定则会将选中的列表项清除,并提示并渲染出对应的列表数据。
5、实现数据存储:实现数据的本地存储,用户添加的选项列表如果没有被删除,则将存储在本地的浏览器中。
6、防止数据被恶意修改:如果主动删除了或更改了Application中的value值,使其不是一个完整的对象,则会自动删除所有数据,并提示本地缓存异常,数据已重置。
案例实现基本思路
在vue脚手架的基础上做案例,
在src文件夹下 新建文件main.js,App.vue。新建文件夹components,里面新建Header.vue,List.vue,Item.vue,Footer.vue,(分别代表不同的组件)编写组件的结构,样式。
这里面的List相当于Item的父组件
组件之间的层级结构图
组件使用步骤
1、定义
2、引入并注册
3、使用
编写程序之前更重要的是分析需求,我是一小块一小块功能实现的。做一做,测一测,遇到问题及时解决。
比如实现添加列表的功能,你可以通过自定义属性去完成。想要添加,输入框是在Header子组件里面,所以你就要在Header子组件里拿到用户输入的内容,然后给输入框绑定key.enter键盘事件,去调用methods里面的对应的方法,方法中首先判断用户输入的内容是否为空,如果不为空根据用户输入内容生成一个Todo对象,然后使用自定义事件去通知App在data中添加一个todo,这里要在App中提前写好对应的方法,将子组件中传递过来的数据进行渲染并放在列表的最前方。最后再清空输入框。最终实现这个小功能。
就这样根据所学知识,以及对功能需求的分析,一步一步来,最终完成案例。对于Vue初学者来说,Todolist这个案例还是很有必要去做一做的,下面是我做的案例对应的源码,上面的注释也很详细,有需要的小伙伴可以参考。
核心代码:
main.js
// 引入Vue
import Vue from 'vue';
// 引入APP
import App from './App.vue'
//关闭生产提示
Vue.config.productionTip = false
new Vue({
el:'#app',
/*
vue.runtime.common.js和vue.js有何区别?
vue.runtime.common.js(项目中用的多) :
1.不包含模板解析器,打包后体积小
2.配置项中的不能写template,要用render: h => h(App)代替
vue.js :
1.包含解模板析器,打包后体积大
2.配置可以写template
*/
// 调用h函数相当于帮你做了3件事
// components:{App},
// template:`<App/>`
// 加上请外援vue-template-compiler(Vue模板解析器)相当于一个loader
render: h => h(App)
})
App.vue
<template>
<div class="todo-container">
<div class="todo-wrap">
<!-- 头部 使用自定义事件去实现-->
<Header @add-todo="addtodo"/>
<!-- 列表 -->
<List :todos='todos' :updateTodo="updateTodo"
:deleteTodo="deleteTodo"/>
<!-- 底部 -->
<Footer :todos='todos' :updateAll="updateAll" :clearAll="clearAll"/>
</div>
</div>
</template>
<script>
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
export default {
name:'App',
components:{Header,List,Footer},
data(){
//数据
const localData = localStorage.getItem('todos')
let todos
try {
//尝试解析localStorage中的数据,如有数据,直接使用,无数据,使用空数组。
todos = JSON.parse(localData) || []
} catch (error) {
alert('本地缓存数据异常,数据已重置')
localStorage.removeItem('todos')
todos = []
}
return {
todos
}
},
methods: {
//添加一个Todo
addtodo(todoObj){
//unshift后添加的东西往前放
this.todos.unshift(todoObj)
},
//更新一个todo----通过ID
// updateTodo(id,done) {
// this.todos= this.todos.map((todo)=>{
// if(id===todo.id) return {...todo,done:done}
// else return todo
// })
// },
//更新一个todo----通过index
updateTodo(index,done) {
this.todos[index].done=done
},
//删除一个Todo
deleteTodo(index){
this.todos.splice(index,1)
// console.log(index);
},
//全选or全不选
updateAll(done){
this.todos=this.todos.map((todo)=>{
return {...todo,done}
})
},
// 清除所有已完成
clearAll(){
this.todos=this.todos.filter((todoObj)=>{
return !todoObj.done
})
},
},
watch:{
todos:{
deep:true, //开启深度监视
handler(value){
localStorage.setItem('todos',JSON.stringify(value))
}
}
}
}
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
Header.vue
<template>
<div class="todo-header">
<input
v-model="name"
@keyup.enter="add"
type="text"
placeholder="请输入你的任务名称,按回车键确认"
/>
</div>
</template>
<script>
export default {
name:'Header',
data(){
return{
name
}
},
methods:{
add(){
if(!this.name.trim()) return alert('输入信息不能为空')
//根据用户输入内容生成一个Todo对象
const todoObj={id:Date.now(),name:this.name,done:false}
//使用自定义事件去通知App在data中添加一个todo
this.$emit('add-todo',todoObj)
//清空输入
this.name=''
}
}
}
</script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
Footer.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" v-model="isAll"/>
</label>
<span>
<span>已完成{{doneCount}}</span> / 全部{{total}}
</span>
<button class="btn btn-danger" @click="clearAllDone">清除已完成任务</button>
</div>
</template>
<script>
export default {
name:'Footer',
props:['todos','updateAll','clearAll'],
computed:{
doneCount(){
//这里引入一些reduce的知识点
// arr.reduce(function(prev,cur,index,arr){
// ...
// }, init);
// 其中,
// arr 表示原数组;
// prev 表示上一次调用回调时的返回值,或者初始值 init;
// cur 表示当前正在处理的数组元素;
// index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1;
// init 表示初始值。
// 例子:
// var arr = [3,9,4,3,6,0,9];
// var sum = arr.reduce(function (prev, cur) {
// return prev + cur;
// },0);
// 由于传入了初始值0,所以开始时prev的值为0,cur的值为数组第一项3,
// 相加之后返回值为3作为下一轮回调的prev值,然后再继续与下一个数组项相加,
// 以此类推,直至完成所有数组项的和并返回。
return this.todos.reduce((pre,current)=>pre+=current.done?1:0,0)
},
total(){
return this.todos.length
},
isAll:{
//使用计算属性的set去实现全选
set(flag){
this.updateAll(flag)
},
get(){
return this.doneCount === this.total && this.total>0
}
}
},
methods:{
// 使用方法去全选
// checkAll(event){
// console.log('@',event.target.checked);
// this.updateAll(event.target.checked)
// }
clearAllDone(){
if(confirm('确定清除所有已完成的tido吗?')){
this.clearAll()
}
}
}
}
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
Item.vue
<template>
<li
@mouseenter="isEnter=true"
@mouseleave="isEnter=false"
:class="{'high-light':isEnter}">
<!-- 当更改数据和点击事件同时存在时,事件优先 -->
<label>
<input
type="checkbox"
:checked="todo.done"
@click="update(index,$event)"/>
<span>{{todo.name}}</span>
</label>
<button
class="btn btn-danger"
:style="{display:isEnter ?'block':'none'}"
@click="deleteT(index)">删除</button>
</li>
</template>
<script>
export default {
name:'Item',
props:['todo','updateTodo',"deleteTodo",'index'],
data(){
return{
isEnter:false//标识鼠标是否移入
}
},
methods: {
//用id去更新
// update(id,event){
// console.log(id,event.target.checked);
// //通知app去更新这个todo
// const{checked}=event.target
// this.updateTodo(id,checked)
// }
// },
//用index去更新
deleteT(index){
if(confirm('确定删除吗')){
this.deleteTodo(index)
// this.deteleTodo(index)
}
},
update(index,event){
// console.log(id,event.target.checked);
//通知app去更新这个todo
const{checked}=event.target
this.updateTodo(index,checked)
}
},
}
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
.high-light{
background-color: #bbb;
}
</style>
List.vue
<template>
<div>
<ul class="todo-main">
<Item
v-for="(t,index) in todos"
:key="t.id"
:todo="t"
:updateTodo="updateTodo"
:index='index'
:deleteTodo="deleteTodo"
/>
</ul>
</div>
</template>
<script>
import Item from './Item'
export default {
name:'List',
components:{Item},
props:['todos','updateTodo','deleteTodo']//声明接收props,声明后可以在vc上找的到
}
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>